Repository navigation
Expand file tree
/
Copy pathinlineAutoformat.ts
More file actions
129 lines (117 loc) · 5.14 KB
/
Copy pathinlineAutoformat.ts
File metadata and controls
129 lines (117 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* inlineAutoformat.ts :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: dlesieur <dlesieur@student.42.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/07/07 00:00:00 by dlesieur #+# #+# */
/* Updated: 2026/07/07 00:00:00 by dlesieur ### ########.fr */
/* */
/* ************************************************************************** */
/**
* Live markdown autoformat (Notion-style input rule).
*
* As the user types, an INCOMPLETE delimiter pair (`**Hello`) stays raw. The
* moment a pair COMPLETES at the caret (`**Hello**`, `` `code` ``, `*i*`,
* `__u__`, `~~s~~`, `==h==`, `***bi***`), the delimiters are consumed and the
* span becomes canonical bracket source (`[b]Hello[/b]`) which renders as styled
* HTML — no waiting for blur.
*
* Disambiguation (`*` vs `**`, `_` word-boundaries, code spans literal) is NOT
* re-implemented here: we defer to the real inline parser. We only fire when the
* just-typed char is a closing delimiter AND parsing the text before the caret
* yields a styled node as its LAST child — i.e. the parser itself recognized a
* freshly-closed pair ending exactly at the caret. Typing plain text, or inside
* an already-styled span, leaves the last node as text (or unchanged), so the
* rule is inert and never churns.
*/
import { parseInline } from "./markdown/parserInline";
import { getInlineNodesTextLength, serializeInlineNodes } from "./inlineAst";
import type { InlineNode } from "./markdown/ast";
export interface InlineAutoformatResult {
/** New canonical (bracket) source with the completed pair's delimiters consumed. */
source: string;
/** New caret offset in RENDERED-plain-text coordinates (end of the styled content). */
caret: number;
}
/** Types the parser produces from typed markdown-delimiter pairs. */
const STYLED_TYPES = new Set<InlineNode["type"]>([
"bold",
"italic",
"bold_italic",
"strikethrough",
"underline",
"highlight",
"code",
"code_rich",
"subscript",
"superscript",
"spoiler",
"kbd",
]);
/** The final char of every inline closing delimiter this rule handles
* ("^" closes ^sup^, "|" closes ||spoiler||, ">" closes </kbd> etc.).
* Exported so the editor's keystroke gate and this rule can never drift. */
export const INLINE_CLOSING_DELIMITERS: ReadonlySet<string> = new Set([
"*",
"_",
"`",
"~",
"=",
"^",
"|",
">",
]);
/**
* If a markdown style pair just closed at `caret`, return the canonicalized
* source + caret; otherwise null (no transform). `caret` is a source-string
* offset (== DOM plain-text offset in the common no-preceding-styled-span case).
*/
/**
* Mid-run shape: a text node ending in a stacking delimiter right before a
* styled last node — "***bold italic*" parses as "**" + italic("bold italic")
* with two more stars still to come. Acting on that state (autoformatting it,
* or restyling the live DOM from it) canonicalizes the partial pair and
* scrambles the remaining keystrokes; both paths stay raw until the whole run
* closes.
*/
function isMidRunShape(nodes: InlineNode[]): boolean {
const last = nodes[nodes.length - 1];
if (!last || !STYLED_TYPES.has(last.type)) return false;
const previous = nodes[nodes.length - 2];
if (previous?.type !== "text") return false;
const tail = previous.value[previous.value.length - 1];
return tail === "*" || tail === "_" || tail === "~";
}
/**
* True when the caret sits midway through closing a longer delimiter run —
* the editor holds its live restyle on these states (see isMidRunShape).
*/
export function isMidDelimiterRun(source: string, caret: number): boolean {
if (caret <= 0 || caret > source.length) return false;
return isMidRunShape(parseInline(source.slice(0, caret)));
}
export function autoformatInlineMarkdown(
source: string,
caret: number,
): InlineAutoformatResult | null {
if (caret <= 0 || caret > source.length) return null;
const left = source.slice(0, caret);
const lastChar = left[left.length - 1];
if (!lastChar || !INLINE_CLOSING_DELIMITERS.has(lastChar)) return null;
const nodes = parseInline(left);
if (nodes.length === 0) return null;
const last = nodes[nodes.length - 1];
if (!STYLED_TYPES.has(last.type)) return null;
if (isMidRunShape(nodes)) return null;
// A pair closed at the caret. Canonicalize the whole left side (idempotent for
// parts that were already bracketed) and land the caret at the end of the now
// styled content — the delimiters have no visible width, so that is the
// visible length of everything before the caret.
const newLeft = serializeInlineNodes(nodes);
return {
source: newLeft + source.slice(caret),
caret: getInlineNodesTextLength(nodes),
};
}