@fusion.dev/helix-headless 0.215.3 → 0.216.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.json +1 -1
- package/custom-elements.json +1 -1
- package/dist/area-chart/root/area-chart-root.d.ts +1 -0
- package/dist/area-chart/root/area-chart-root.js +1 -1
- package/dist/bar-chart/root/bar-chart-root.d.ts +1 -0
- package/dist/bar-chart/root/bar-chart-root.js +1 -1
- package/dist/button/root/button-root.d.ts +3 -1
- package/dist/button/root/button-root.js +2 -1
- package/dist/chat/composer/chat-composer.d.ts +18 -1
- package/dist/chat/composer/chat-composer.js +8 -3
- package/dist/checkbox/root/checkbox-root.d.ts +3 -1
- package/dist/checkbox/root/checkbox-root.js +2 -1
- package/dist/code-block/highlight.d.ts +9 -0
- package/dist/code-block/highlight.js +39 -0
- package/dist/code-block/highlighter.d.ts +44 -0
- package/dist/code-block/highlighter.js +44 -0
- package/dist/code-block/index.d.ts +1 -0
- package/dist/code-block/index.js +1 -0
- package/dist/code-block/keyed-lines.d.ts +23 -0
- package/dist/code-block/keyed-lines.js +75 -0
- package/dist/code-block/root/code-block-root.d.ts +6 -23
- package/dist/code-block/root/code-block-root.js +8 -25
- package/dist/code-editor/index.d.ts +1 -0
- package/dist/code-editor/index.js +1 -0
- package/dist/code-editor/root/code-editor-root.d.ts +367 -0
- package/dist/code-editor/root/code-editor-root.js +746 -0
- package/dist/code-input/index.d.ts +1 -0
- package/dist/code-input/index.js +1 -0
- package/dist/code-input/root/code-input-root.d.ts +119 -0
- package/dist/code-input/root/code-input-root.js +142 -0
- package/dist/combobox/root/combobox-root.d.ts +6 -1
- package/dist/combobox/root/combobox-root.js +4 -2
- package/dist/command/root/command-root.d.ts +61 -11
- package/dist/command/root/command-root.js +88 -3
- package/dist/donut-chart/root/donut-chart-root.d.ts +1 -0
- package/dist/donut-chart/root/donut-chart-root.js +1 -1
- package/dist/element-events.generated.js +36 -3
- package/dist/filter-input/root/filter-input-root.d.ts +9 -1
- package/dist/filter-input/root/filter-input-root.js +7 -3
- package/dist/heatmap/root/heatmap-root.d.ts +1 -0
- package/dist/heatmap/root/heatmap-root.js +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/input/root/input-root.d.ts +14 -2
- package/dist/input/root/input-root.js +11 -3
- package/dist/kbd/root/kbd-root.js +9 -2
- package/dist/line-chart/root/line-chart-root.d.ts +1 -0
- package/dist/line-chart/root/line-chart-root.js +1 -1
- package/dist/listbox/root/listbox-root.d.ts +8 -1
- package/dist/listbox/root/listbox-root.js +7 -4
- package/dist/markdown/blocks.d.ts +66 -0
- package/dist/markdown/blocks.js +326 -0
- package/dist/markdown/index.d.ts +6 -0
- package/dist/markdown/index.js +5 -0
- package/dist/markdown/inline.d.ts +57 -0
- package/dist/markdown/inline.js +483 -0
- package/dist/markdown/root/markdown-root.d.ts +99 -0
- package/dist/markdown/root/markdown-root.js +93 -0
- package/dist/markdown/stream.d.ts +47 -0
- package/dist/markdown/stream.js +62 -0
- package/dist/markdown/tree.d.ts +57 -0
- package/dist/markdown/tree.js +144 -0
- package/dist/markdown/writer.d.ts +33 -0
- package/dist/markdown/writer.js +139 -0
- package/dist/menu/root/menu-root.d.ts +53 -0
- package/dist/menu/root/menu-root.js +92 -15
- package/dist/pagination/size/pagination-size.d.ts +3 -0
- package/dist/pagination/size/pagination-size.js +2 -1
- package/dist/scatter-chart/root/scatter-chart-root.d.ts +1 -0
- package/dist/scatter-chart/root/scatter-chart-root.js +1 -1
- package/dist/select/root/select-root.d.ts +6 -1
- package/dist/select/root/select-root.js +4 -2
- package/dist/shortcut-overlay/index.d.ts +1 -0
- package/dist/shortcut-overlay/index.js +1 -0
- package/dist/shortcut-overlay/root/shortcut-overlay-root.d.ts +128 -0
- package/dist/shortcut-overlay/root/shortcut-overlay-root.js +275 -0
- package/dist/slider/root/slider-root.d.ts +3 -1
- package/dist/slider/root/slider-root.js +2 -1
- package/dist/spatial-bar-chart/root/spatial-bar-chart-root.d.ts +1 -0
- package/dist/spatial-bar-chart/root/spatial-bar-chart-root.js +1 -1
- package/dist/spatial-heatmap/root/spatial-heatmap-root.d.ts +1 -0
- package/dist/spatial-heatmap/root/spatial-heatmap-root.js +1 -1
- package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.d.ts +1 -0
- package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.js +1 -1
- package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +1 -0
- package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +1 -1
- package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +1 -0
- package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +1 -1
- package/dist/spatial-treemap/root/spatial-treemap-root.d.ts +1 -0
- package/dist/spatial-treemap/root/spatial-treemap-root.js +1 -1
- package/dist/switch/root/switch-root.d.ts +3 -1
- package/dist/switch/root/switch-root.js +2 -1
- package/dist/tabs/tab/tab.d.ts +3 -1
- package/dist/tabs/tab/tab.js +2 -1
- package/dist/textarea/root/textarea-root.d.ts +20 -4
- package/dist/textarea/root/textarea-root.js +31 -23
- package/dist/time-input/root/time-input-root.d.ts +6 -0
- package/dist/time-input/root/time-input-root.js +4 -2
- package/dist/time-picker/root/time-picker-root.d.ts +6 -0
- package/dist/time-picker/root/time-picker-root.js +4 -2
- package/dist/toast/root/toast-root.d.ts +64 -0
- package/dist/toast/root/toast-root.js +128 -3
- package/dist/toggle/root/toggle-root.d.ts +3 -1
- package/dist/toggle/root/toggle-root.js +2 -1
- package/dist/virtual-list/root/virtual-list-root.d.ts +18 -2
- package/dist/virtual-list/root/virtual-list-root.js +103 -5
- package/examples.json +1 -1
- package/lint.json +1 -1
- package/package.json +6 -6
- package/types/jsx.d.ts +5315 -596
- package/types/svelte.d.ts +4 -0
- package/types/vue.d.ts +1085 -4
- package/types/words.d.ts +491 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
const ENTITIES = {
|
|
2
|
+
amp: '&',
|
|
3
|
+
apos: "'",
|
|
4
|
+
copy: '©',
|
|
5
|
+
gt: '>',
|
|
6
|
+
hellip: '…',
|
|
7
|
+
larr: '←',
|
|
8
|
+
ldquo: '“',
|
|
9
|
+
lsquo: '‘',
|
|
10
|
+
lt: '<',
|
|
11
|
+
mdash: '\u2014',
|
|
12
|
+
nbsp: ' ',
|
|
13
|
+
ndash: '–',
|
|
14
|
+
quot: '"',
|
|
15
|
+
rarr: '→',
|
|
16
|
+
rdquo: '”',
|
|
17
|
+
reg: '®',
|
|
18
|
+
rsquo: '’',
|
|
19
|
+
times: '×',
|
|
20
|
+
trade: '™',
|
|
21
|
+
};
|
|
22
|
+
const PUNCTUATION = /[\p{P}\p{S}]/u;
|
|
23
|
+
const WHITESPACE = /\s/u;
|
|
24
|
+
const ESCAPABLE = /[!-/:-@[-`{-~]/;
|
|
25
|
+
const AUTOLINK = /^<([A-Za-z][A-Za-z0-9+.-]{1,31}:[^\s<>]*|[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*)>/;
|
|
26
|
+
const ENTITY = /^&(?:#[xX]([0-9a-fA-F]{1,6})|#([0-9]{1,7})|([A-Za-z][A-Za-z0-9]{1,31}));/;
|
|
27
|
+
const PENDING_ENTITY = /^&#?[A-Za-z0-9]{0,31}$/;
|
|
28
|
+
const PENDING_AUTOLINK = /^<[A-Za-z][A-Za-z0-9+.-]{0,31}(?::[^\s<>]*)?$/;
|
|
29
|
+
const readDestination = (text, at) => {
|
|
30
|
+
let index = at;
|
|
31
|
+
const skip = () => {
|
|
32
|
+
while (index < text.length && /[ \t\n]/.test(text[index] ?? '')) {
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
skip();
|
|
37
|
+
let href = '';
|
|
38
|
+
if (text[index] === '<') {
|
|
39
|
+
const close = text.indexOf('>', index + 1);
|
|
40
|
+
if (close < 0 || /[\n<]/.test(text.slice(index + 1, close))) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
href = text.slice(index + 1, close);
|
|
44
|
+
index = close + 1;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
let depth = 0;
|
|
48
|
+
const begin = index;
|
|
49
|
+
while (index < text.length) {
|
|
50
|
+
const character = text[index] ?? '';
|
|
51
|
+
if (character === '\\' && ESCAPABLE.test(text[index + 1] ?? '')) {
|
|
52
|
+
index += 2;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (/[\s]/.test(character)) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (character === '(') {
|
|
59
|
+
depth += 1;
|
|
60
|
+
}
|
|
61
|
+
else if (character === ')') {
|
|
62
|
+
if (depth === 0) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
depth -= 1;
|
|
66
|
+
}
|
|
67
|
+
index += 1;
|
|
68
|
+
}
|
|
69
|
+
href = text.slice(begin, index).replace(/\\([!-/:-@[-`{-~])/g, '$1');
|
|
70
|
+
}
|
|
71
|
+
skip();
|
|
72
|
+
let title;
|
|
73
|
+
const quote = text[index];
|
|
74
|
+
if (quote === '"' || quote === "'" || quote === '(') {
|
|
75
|
+
const closer = quote === '(' ? ')' : quote;
|
|
76
|
+
let end = index + 1;
|
|
77
|
+
while (end < text.length && text[end] !== closer) {
|
|
78
|
+
end += text[end] === '\\' ? 2 : 1;
|
|
79
|
+
}
|
|
80
|
+
if (end >= text.length) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
title = text.slice(index + 1, end).replace(/\\([!-/:-@[-`{-~])/g, '$1');
|
|
84
|
+
index = end + 1;
|
|
85
|
+
skip();
|
|
86
|
+
}
|
|
87
|
+
if (text[index] !== ')') {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return title === undefined ? { end: index + 1, href } : { end: index + 1, href, title };
|
|
91
|
+
};
|
|
92
|
+
const merge = (nodes) => {
|
|
93
|
+
const merged = [];
|
|
94
|
+
for (const node of nodes) {
|
|
95
|
+
const last = merged.at(-1);
|
|
96
|
+
if (node.kind === 'text' && last?.kind === 'text') {
|
|
97
|
+
merged[merged.length - 1] = { kind: 'text', text: last.text + node.text };
|
|
98
|
+
}
|
|
99
|
+
else if (node.kind !== 'text' || node.text !== '') {
|
|
100
|
+
merged.push(node);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return merged;
|
|
104
|
+
};
|
|
105
|
+
const processEmphasis = (tokens, from) => {
|
|
106
|
+
let closerAt = from;
|
|
107
|
+
while (closerAt < tokens.length) {
|
|
108
|
+
const closer = tokens[closerAt]?.delimiter;
|
|
109
|
+
if (!closer?.canClose || closer.count === 0) {
|
|
110
|
+
closerAt += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
let openerAt = closerAt - 1;
|
|
114
|
+
let found = false;
|
|
115
|
+
while (openerAt >= from) {
|
|
116
|
+
const opener = tokens[openerAt]?.delimiter;
|
|
117
|
+
if (opener?.canOpen &&
|
|
118
|
+
opener.count > 0 &&
|
|
119
|
+
opener.character === closer.character &&
|
|
120
|
+
(closer.character === '~'
|
|
121
|
+
? opener.count === closer.count
|
|
122
|
+
: !((opener.canClose || closer.canOpen) &&
|
|
123
|
+
(opener.original + closer.original) % 3 === 0 &&
|
|
124
|
+
!(opener.original % 3 === 0 && closer.original % 3 === 0)))) {
|
|
125
|
+
found = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
openerAt -= 1;
|
|
129
|
+
}
|
|
130
|
+
if (!found) {
|
|
131
|
+
closerAt += 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const opener = tokens[openerAt]?.delimiter;
|
|
135
|
+
const used = closer.character === '~' ? closer.count : opener.count >= 2 && closer.count >= 2 ? 2 : 1;
|
|
136
|
+
const inside = tokens.splice(openerAt + 1, closerAt - openerAt - 1);
|
|
137
|
+
const kind = closer.character === '~' ? 'del' : used === 2 ? 'strong' : 'em';
|
|
138
|
+
const wrapped = { node: { children: merge(inside.map((token) => token.node)), kind } };
|
|
139
|
+
tokens.splice(openerAt + 1, 0, wrapped);
|
|
140
|
+
closerAt = openerAt + 2;
|
|
141
|
+
opener.count -= used;
|
|
142
|
+
closer.count -= used;
|
|
143
|
+
const openerToken = tokens[openerAt];
|
|
144
|
+
openerToken.node = { kind: 'text', text: opener.character.repeat(opener.count) };
|
|
145
|
+
const closerToken = tokens[closerAt];
|
|
146
|
+
closerToken.node = { kind: 'text', text: closer.character.repeat(closer.count) };
|
|
147
|
+
if (opener.count === 0) {
|
|
148
|
+
delete openerToken.delimiter;
|
|
149
|
+
}
|
|
150
|
+
if (closer.count === 0) {
|
|
151
|
+
delete closerToken.delimiter;
|
|
152
|
+
closerAt += 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const flanking = (before, after, character) => {
|
|
157
|
+
const beforeSpace = before === '' || WHITESPACE.test(before);
|
|
158
|
+
const afterSpace = after === '' || WHITESPACE.test(after);
|
|
159
|
+
const beforePunctuation = PUNCTUATION.test(before);
|
|
160
|
+
const afterPunctuation = PUNCTUATION.test(after);
|
|
161
|
+
const left = !afterSpace && (!afterPunctuation || beforeSpace || beforePunctuation);
|
|
162
|
+
const right = !beforeSpace && (!beforePunctuation || afterSpace || afterPunctuation);
|
|
163
|
+
if (character === '_') {
|
|
164
|
+
return {
|
|
165
|
+
canClose: right && (!left || afterPunctuation),
|
|
166
|
+
canOpen: left && (!right || beforePunctuation),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { canClose: right, canOpen: left };
|
|
170
|
+
};
|
|
171
|
+
const trimUrl = (url) => {
|
|
172
|
+
let end = url.length;
|
|
173
|
+
while (end > 0 && /[?!.,:*_~'"]/.test(url[end - 1] ?? '')) {
|
|
174
|
+
end -= 1;
|
|
175
|
+
}
|
|
176
|
+
let trimmed = url.slice(0, end);
|
|
177
|
+
while (trimmed.endsWith(')')) {
|
|
178
|
+
const opens = trimmed.split('(').length - 1;
|
|
179
|
+
const closes = trimmed.split(')').length - 1;
|
|
180
|
+
if (closes <= opens) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
trimmed = trimmed.slice(0, -1);
|
|
184
|
+
}
|
|
185
|
+
return trimmed;
|
|
186
|
+
};
|
|
187
|
+
const BARE_URL = /(?:https?:\/\/|www\.)[^\s<]+/g;
|
|
188
|
+
const bareLinks = (nodes) => nodes.flatMap((node) => {
|
|
189
|
+
if (node.kind === 'link') {
|
|
190
|
+
return [node];
|
|
191
|
+
}
|
|
192
|
+
if (node.kind === 'em' || node.kind === 'strong' || node.kind === 'del') {
|
|
193
|
+
return [{ ...node, children: bareLinks(node.children) }];
|
|
194
|
+
}
|
|
195
|
+
if (node.kind !== 'text' || !/https?:\/\/|www\./.test(node.text)) {
|
|
196
|
+
return [node];
|
|
197
|
+
}
|
|
198
|
+
const out = [];
|
|
199
|
+
let last = 0;
|
|
200
|
+
for (const match of node.text.matchAll(BARE_URL)) {
|
|
201
|
+
const before = node.text[match.index - 1];
|
|
202
|
+
if (match.index > 0 && before !== undefined && !/[\s*_~(]/.test(before)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const url = trimUrl(match[0]);
|
|
206
|
+
if (url === '' || (url.startsWith('www.') && !/^www\.[^.]+\./.test(`${url}.`))) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
out.push({ kind: 'text', text: node.text.slice(last, match.index) });
|
|
210
|
+
out.push({
|
|
211
|
+
children: [{ kind: 'text', text: url }],
|
|
212
|
+
href: url.startsWith('www.') ? `http://${url}` : url,
|
|
213
|
+
kind: 'link',
|
|
214
|
+
});
|
|
215
|
+
last = match.index + url.length;
|
|
216
|
+
}
|
|
217
|
+
out.push({ kind: 'text', text: node.text.slice(last) });
|
|
218
|
+
return merge(out);
|
|
219
|
+
});
|
|
220
|
+
export const parseInline = (text, options = {}) => {
|
|
221
|
+
const open = options.open === true;
|
|
222
|
+
const tokens = [];
|
|
223
|
+
let plain = '';
|
|
224
|
+
const flush = () => {
|
|
225
|
+
if (plain !== '') {
|
|
226
|
+
tokens.push({ node: { kind: 'text', text: plain } });
|
|
227
|
+
plain = '';
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
let at = 0;
|
|
231
|
+
while (at < text.length) {
|
|
232
|
+
const character = text[at] ?? '';
|
|
233
|
+
const rest = text.slice(at);
|
|
234
|
+
if (character === '\\') {
|
|
235
|
+
const next = text[at + 1];
|
|
236
|
+
if (next === '\n') {
|
|
237
|
+
flush();
|
|
238
|
+
tokens.push({ node: { kind: 'break' } });
|
|
239
|
+
at += 2;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (next !== undefined && ESCAPABLE.test(next)) {
|
|
243
|
+
plain += next;
|
|
244
|
+
at += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (next === undefined && open) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
plain += character;
|
|
251
|
+
at += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (character === '`') {
|
|
255
|
+
const run = /^`+/.exec(rest)?.[0] ?? '`';
|
|
256
|
+
const closer = new RegExp(`(?<!\`)${run}(?!\`)`, 'g');
|
|
257
|
+
closer.lastIndex = at + run.length;
|
|
258
|
+
const match = closer.exec(text);
|
|
259
|
+
if (match) {
|
|
260
|
+
flush();
|
|
261
|
+
let code = text.slice(at + run.length, match.index).replace(/\n/g, ' ');
|
|
262
|
+
if (/^ .*[^ ].* $/.test(code) || /^ [^ ]+ $/.test(code)) {
|
|
263
|
+
code = code.slice(1, -1);
|
|
264
|
+
}
|
|
265
|
+
tokens.push({ node: { kind: 'code', text: code } });
|
|
266
|
+
at = match.index + run.length;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (open && at + run.length >= text.length) {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
if (open) {
|
|
273
|
+
flush();
|
|
274
|
+
tokens.push({
|
|
275
|
+
node: { kind: 'code', text: text.slice(at + run.length).replace(/\n/g, ' ') },
|
|
276
|
+
});
|
|
277
|
+
at = text.length;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
plain += run;
|
|
281
|
+
at += run.length;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (character === '&') {
|
|
285
|
+
const match = ENTITY.exec(rest);
|
|
286
|
+
if (match) {
|
|
287
|
+
const [whole, hex, decimal, name] = match;
|
|
288
|
+
const point = hex ? Number.parseInt(hex, 16) : decimal ? Number(decimal) : undefined;
|
|
289
|
+
const decoded = point !== undefined
|
|
290
|
+
? String.fromCodePoint(point > 0x10ffff || point === 0 ? 0xfffd : point)
|
|
291
|
+
: ENTITIES[name ?? ''];
|
|
292
|
+
if (decoded !== undefined) {
|
|
293
|
+
plain += decoded;
|
|
294
|
+
at += whole.length;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (open && PENDING_ENTITY.test(rest)) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
plain += character;
|
|
302
|
+
at += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (character === '<') {
|
|
306
|
+
const match = AUTOLINK.exec(rest);
|
|
307
|
+
if (match) {
|
|
308
|
+
flush();
|
|
309
|
+
const target = match[1] ?? '';
|
|
310
|
+
const href = target.includes(':') ? target : `mailto:${target}`;
|
|
311
|
+
tokens.push({ node: { children: [{ kind: 'text', text: target }], href, kind: 'link' } });
|
|
312
|
+
at += match[0].length;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (open && PENDING_AUTOLINK.test(rest)) {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
plain += character;
|
|
319
|
+
at += 1;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (character === '*' || character === '_' || character === '~') {
|
|
323
|
+
const run = new RegExp(`^\\${character}+`).exec(rest)?.[0] ?? character;
|
|
324
|
+
const after = text[at + run.length] ?? '';
|
|
325
|
+
if (open && after === '') {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
if (character === '~' && run.length > 2) {
|
|
329
|
+
plain += run;
|
|
330
|
+
at += run.length;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
flush();
|
|
334
|
+
const sides = flanking(text[at - 1] ?? '', after, character);
|
|
335
|
+
tokens.push({
|
|
336
|
+
delimiter: { ...sides, character, count: run.length, original: run.length },
|
|
337
|
+
node: { kind: 'text', text: run },
|
|
338
|
+
});
|
|
339
|
+
at += run.length;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (character === '!' && text[at + 1] === '[') {
|
|
343
|
+
flush();
|
|
344
|
+
tokens.push({ bracket: { active: true, image: true }, node: { kind: 'text', text: '![' } });
|
|
345
|
+
at += 2;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (character === '!' && open && at + 1 >= text.length) {
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
if (character === '[') {
|
|
352
|
+
flush();
|
|
353
|
+
tokens.push({ bracket: { active: true, image: false }, node: { kind: 'text', text: '[' } });
|
|
354
|
+
at += 1;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (character === ']') {
|
|
358
|
+
flush();
|
|
359
|
+
let openerAt = tokens.length - 1;
|
|
360
|
+
while (openerAt >= 0 && !tokens[openerAt]?.bracket) {
|
|
361
|
+
openerAt -= 1;
|
|
362
|
+
}
|
|
363
|
+
const opener = tokens[openerAt];
|
|
364
|
+
if (!opener?.bracket) {
|
|
365
|
+
plain += ']';
|
|
366
|
+
at += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (!opener.bracket.active) {
|
|
370
|
+
tokens.splice(openerAt, 1, { node: opener.node });
|
|
371
|
+
plain += ']';
|
|
372
|
+
at += 1;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const destination = text[at + 1] === '(' ? readDestination(text, at + 2) : undefined;
|
|
376
|
+
const pending = open && destination === undefined && (at + 1 >= text.length || text[at + 1] === '(');
|
|
377
|
+
if (!destination && !pending) {
|
|
378
|
+
tokens.splice(openerAt, 1, { node: opener.node });
|
|
379
|
+
plain += ']';
|
|
380
|
+
at += 1;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
processEmphasis(tokens, openerAt + 1);
|
|
384
|
+
const inside = tokens.splice(openerAt + 1).map((token) => token.node);
|
|
385
|
+
tokens.pop();
|
|
386
|
+
if (opener.bracket.image) {
|
|
387
|
+
const alt = textOf(inside);
|
|
388
|
+
tokens.push({
|
|
389
|
+
node: destination
|
|
390
|
+
? {
|
|
391
|
+
alt,
|
|
392
|
+
kind: 'image',
|
|
393
|
+
src: destination.href,
|
|
394
|
+
...(destination.title === undefined ? {} : { title: destination.title }),
|
|
395
|
+
}
|
|
396
|
+
: { children: [{ kind: 'text', text: alt }], kind: 'link' },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
tokens.push({
|
|
401
|
+
node: {
|
|
402
|
+
children: merge(inside),
|
|
403
|
+
kind: 'link',
|
|
404
|
+
...(destination ? { href: destination.href } : {}),
|
|
405
|
+
...(destination?.title === undefined ? {} : { title: destination.title }),
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
for (const token of tokens) {
|
|
409
|
+
if (token.bracket && !token.bracket.image) {
|
|
410
|
+
token.bracket.active = false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!destination) {
|
|
415
|
+
at = text.length;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
at = destination.end;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (character === '\n') {
|
|
422
|
+
const hard = / {2,}$/.test(plain);
|
|
423
|
+
plain = plain.replace(/ +$/, '');
|
|
424
|
+
flush();
|
|
425
|
+
tokens.push({ node: hard ? { kind: 'break' } : { kind: 'text', text: '\n' } });
|
|
426
|
+
at += 1;
|
|
427
|
+
while (text[at] === ' ' || text[at] === '\t') {
|
|
428
|
+
at += 1;
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
plain += character;
|
|
433
|
+
at += 1;
|
|
434
|
+
}
|
|
435
|
+
flush();
|
|
436
|
+
processEmphasis(tokens, 0);
|
|
437
|
+
if (open) {
|
|
438
|
+
closeOpen(tokens);
|
|
439
|
+
}
|
|
440
|
+
return bareLinks(merge(tokens.map((token) => token.node)));
|
|
441
|
+
};
|
|
442
|
+
const closeOpen = (tokens) => {
|
|
443
|
+
for (let at = tokens.length - 1; at >= 0; at -= 1) {
|
|
444
|
+
const token = tokens[at];
|
|
445
|
+
if (token.bracket) {
|
|
446
|
+
token.node = { kind: 'text', text: '' };
|
|
447
|
+
delete token.bracket;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const delimiter = token.delimiter;
|
|
451
|
+
if (!delimiter?.canOpen || delimiter.count === 0 || at === tokens.length - 1) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const inside = merge(tokens.splice(at + 1).map((one) => one.node));
|
|
455
|
+
let node;
|
|
456
|
+
if (delimiter.character === '~') {
|
|
457
|
+
node = { children: inside, kind: 'del' };
|
|
458
|
+
}
|
|
459
|
+
else if (delimiter.count >= 3) {
|
|
460
|
+
node = { children: [{ children: inside, kind: 'em' }], kind: 'strong' };
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
node = { children: inside, kind: delimiter.count === 2 ? 'strong' : 'em' };
|
|
464
|
+
}
|
|
465
|
+
token.node = node;
|
|
466
|
+
delete token.delimiter;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
export const textOf = (nodes) => nodes
|
|
470
|
+
.map((node) => {
|
|
471
|
+
switch (node.kind) {
|
|
472
|
+
case 'break':
|
|
473
|
+
return '\n';
|
|
474
|
+
case 'code':
|
|
475
|
+
case 'text':
|
|
476
|
+
return node.text;
|
|
477
|
+
case 'image':
|
|
478
|
+
return node.alt;
|
|
479
|
+
default:
|
|
480
|
+
return textOf(node.children);
|
|
481
|
+
}
|
|
482
|
+
})
|
|
483
|
+
.join('');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { HelixElement } from '@fusion.dev/helix-core';
|
|
2
|
+
import { type CSSResultGroup, type PropertyValues, type TemplateResult } from 'lit';
|
|
3
|
+
import '../../callout/index.js';
|
|
4
|
+
import '../../code-block/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* @summary Markdown drawn as a document, which streams in place as a reply arrives.
|
|
7
|
+
*
|
|
8
|
+
* @description
|
|
9
|
+
* Set `text` to the Markdown, or to everything a reply has said so far, with
|
|
10
|
+
* `streaming` while more is coming. Each time `text` grows, only what changed
|
|
11
|
+
* is drawn: a block that has ended is never drawn again, a word arriving is
|
|
12
|
+
* added to the line it ends, and a code block, a list, or a table fills in
|
|
13
|
+
* where it stands. Text you can already read does not move.
|
|
14
|
+
*
|
|
15
|
+
* - A fence becomes a `Code Block`, with its language as its `label` and its
|
|
16
|
+
* `language`, so a highlighter given to `registerHighlighter` colors it as it
|
|
17
|
+
* grows.
|
|
18
|
+
* - An alert, `> [!NOTE]` through `> [!CAUTION]`, becomes a `Callout` in the
|
|
19
|
+
* tone `rehypeHelix` gives it.
|
|
20
|
+
* - A table's columns are sized once from its header row, so the rows that
|
|
21
|
+
* follow never widen or narrow them.
|
|
22
|
+
* - A list item's first paragraph is drawn the same whether the list is tight
|
|
23
|
+
* or loose, so a blank line arriving between items moves nothing.
|
|
24
|
+
*
|
|
25
|
+
* While `streaming`, the end of the text is read as still arriving. A `**`
|
|
26
|
+
* whose closing pair has not come yet draws bold at once, a code span draws as
|
|
27
|
+
* code, and a link draws as a link before its address arrives. A marker that
|
|
28
|
+
* the next character decides, such as a `#` at the start of a line, waits for
|
|
29
|
+
* that character.
|
|
30
|
+
*
|
|
31
|
+
* **The document is drawn in the element's own children**, so the page's
|
|
32
|
+
* `data-prose` styles reach it, a reader can select and find its text, and a
|
|
33
|
+
* server can send it. Put `data-prose` on the element or on a container. On a
|
|
34
|
+
* server, render `markdownToHtml(text)` inside the element: where what it was
|
|
35
|
+
* sent is exactly what it would draw, it keeps it and draws nothing again.
|
|
36
|
+
*
|
|
37
|
+
* What it reads is CommonMark with GitHub's tables, task lists,
|
|
38
|
+
* strikethrough, autolinks, and alerts. A line of `===` or `---` under a
|
|
39
|
+
* paragraph is not a heading, four spaces of indent are not a code block, and
|
|
40
|
+
* reference links are not read, because each of those changes text above it
|
|
41
|
+
* after it was drawn. HTML is drawn as text. An image is drawn as a link to the
|
|
42
|
+
* picture, which loads nothing until a reader follows it. A link's address is
|
|
43
|
+
* kept only where it is relative or on `http`, `https`, `mailto`, or `tel`.
|
|
44
|
+
*
|
|
45
|
+
* @since 0.216.0
|
|
46
|
+
* @status experimental
|
|
47
|
+
* @category typography
|
|
48
|
+
* @aliases Markdown renderer, Streaming text, Rich text
|
|
49
|
+
* @documentation https://fusion.dev/helix/components/markdown
|
|
50
|
+
*
|
|
51
|
+
* @slot - The drawn document. Written by the element; leave it empty, or fill it with `markdownToHtml(text)` on a server.
|
|
52
|
+
*
|
|
53
|
+
* @cssstate streaming - While `streaming` is set and more text is expected.
|
|
54
|
+
*/
|
|
55
|
+
export declare class HelixHeadlessMarkdown extends HelixElement {
|
|
56
|
+
static styles: CSSResultGroup;
|
|
57
|
+
/**
|
|
58
|
+
* The key this family's words are registered under, its styled tag:
|
|
59
|
+
* `registerWords('de', { 'helix-markdown': { noteHeading: 'Hinweis' } })`.
|
|
60
|
+
*/
|
|
61
|
+
static words: string;
|
|
62
|
+
/** The heading of a `> [!CAUTION]` alert. */
|
|
63
|
+
cautionHeading: string;
|
|
64
|
+
/** The heading of a `> [!IMPORTANT]` alert. */
|
|
65
|
+
importantHeading: string;
|
|
66
|
+
/** The heading of a `> [!NOTE]` alert. */
|
|
67
|
+
noteHeading: string;
|
|
68
|
+
/**
|
|
69
|
+
* Whether more text is coming. While it is set, the end of `text` is read
|
|
70
|
+
* as a block still arriving, and the element is busy to assistive
|
|
71
|
+
* technology. Clear it when the reply ends, and the last block is read as
|
|
72
|
+
* finished.
|
|
73
|
+
*/
|
|
74
|
+
streaming: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The Markdown. Set it to the whole reply so far each time more arrives: a
|
|
77
|
+
* text that starts with the one before is drawn by its new end alone.
|
|
78
|
+
*/
|
|
79
|
+
text: string;
|
|
80
|
+
/** The heading of a `> [!TIP]` alert. */
|
|
81
|
+
tipHeading: string;
|
|
82
|
+
/** The heading of a `> [!WARNING]` alert. */
|
|
83
|
+
warningHeading: string;
|
|
84
|
+
private writer;
|
|
85
|
+
/**
|
|
86
|
+
* Adds text to the end of the document, as a chunk of a stream arrives.
|
|
87
|
+
* The same as setting `text` to the text so far.
|
|
88
|
+
*/
|
|
89
|
+
appendText(chunk: string): void;
|
|
90
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
91
|
+
protected firstUpdated(changed: PropertyValues): void;
|
|
92
|
+
protected updated(changed: PropertyValues): void;
|
|
93
|
+
protected render(): TemplateResult;
|
|
94
|
+
}
|
|
95
|
+
declare global {
|
|
96
|
+
interface HTMLElementTagNameMap {
|
|
97
|
+
'helix-headless-markdown': HelixHeadlessMarkdown;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { customElement, HelixElement, words } from '@fusion.dev/helix-core';
|
|
3
|
+
import { css, html } from 'lit';
|
|
4
|
+
import { property } from 'lit/decorators.js';
|
|
5
|
+
import '../../callout/index.js';
|
|
6
|
+
import '../../code-block/index.js';
|
|
7
|
+
import { MarkdownWriter } from '../writer.js';
|
|
8
|
+
let HelixHeadlessMarkdown = class HelixHeadlessMarkdown extends HelixElement {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(...arguments);
|
|
11
|
+
this.cautionHeading = 'Caution';
|
|
12
|
+
this.importantHeading = 'Important';
|
|
13
|
+
this.noteHeading = 'Note';
|
|
14
|
+
this.streaming = false;
|
|
15
|
+
this.text = '';
|
|
16
|
+
this.tipHeading = 'Tip';
|
|
17
|
+
this.warningHeading = 'Warning';
|
|
18
|
+
}
|
|
19
|
+
static { this.styles = css `
|
|
20
|
+
:host {
|
|
21
|
+
display: block;
|
|
22
|
+
}
|
|
23
|
+
`; }
|
|
24
|
+
static { this.words = 'helix-markdown'; }
|
|
25
|
+
appendText(chunk) {
|
|
26
|
+
this.text += chunk;
|
|
27
|
+
}
|
|
28
|
+
willUpdate(changed) {
|
|
29
|
+
super.willUpdate(changed);
|
|
30
|
+
this.internals.ariaBusy = this.streaming ? 'true' : null;
|
|
31
|
+
this.toggleState('streaming', this.streaming);
|
|
32
|
+
}
|
|
33
|
+
firstUpdated(changed) {
|
|
34
|
+
super.firstUpdated(changed);
|
|
35
|
+
this.finishHydration();
|
|
36
|
+
}
|
|
37
|
+
updated(changed) {
|
|
38
|
+
super.updated(changed);
|
|
39
|
+
const tier = this.constructor.tier;
|
|
40
|
+
const options = {
|
|
41
|
+
headings: {
|
|
42
|
+
caution: this.cautionHeading,
|
|
43
|
+
important: this.importantHeading,
|
|
44
|
+
note: this.noteHeading,
|
|
45
|
+
tip: this.tipHeading,
|
|
46
|
+
warning: this.warningHeading,
|
|
47
|
+
},
|
|
48
|
+
tags: { callout: `${tier}callout`, codeBlock: `${tier}code-block` },
|
|
49
|
+
};
|
|
50
|
+
if (!this.writer) {
|
|
51
|
+
this.writer = new MarkdownWriter(this, options);
|
|
52
|
+
}
|
|
53
|
+
else if (['cautionHeading', 'importantHeading', 'noteHeading', 'tipHeading', 'warningHeading'].some((name) => changed.has(name))) {
|
|
54
|
+
this.writer.configure(options);
|
|
55
|
+
}
|
|
56
|
+
if (changed.has('text') || changed.has('streaming')) {
|
|
57
|
+
this.writer.write(this.text, !this.streaming);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
render() {
|
|
61
|
+
return html `<slot></slot>`;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
__decorate([
|
|
65
|
+
words,
|
|
66
|
+
property({ attribute: 'caution-heading' })
|
|
67
|
+
], HelixHeadlessMarkdown.prototype, "cautionHeading", void 0);
|
|
68
|
+
__decorate([
|
|
69
|
+
words,
|
|
70
|
+
property({ attribute: 'important-heading' })
|
|
71
|
+
], HelixHeadlessMarkdown.prototype, "importantHeading", void 0);
|
|
72
|
+
__decorate([
|
|
73
|
+
words,
|
|
74
|
+
property({ attribute: 'note-heading' })
|
|
75
|
+
], HelixHeadlessMarkdown.prototype, "noteHeading", void 0);
|
|
76
|
+
__decorate([
|
|
77
|
+
property({ type: Boolean })
|
|
78
|
+
], HelixHeadlessMarkdown.prototype, "streaming", void 0);
|
|
79
|
+
__decorate([
|
|
80
|
+
property()
|
|
81
|
+
], HelixHeadlessMarkdown.prototype, "text", void 0);
|
|
82
|
+
__decorate([
|
|
83
|
+
words,
|
|
84
|
+
property({ attribute: 'tip-heading' })
|
|
85
|
+
], HelixHeadlessMarkdown.prototype, "tipHeading", void 0);
|
|
86
|
+
__decorate([
|
|
87
|
+
words,
|
|
88
|
+
property({ attribute: 'warning-heading' })
|
|
89
|
+
], HelixHeadlessMarkdown.prototype, "warningHeading", void 0);
|
|
90
|
+
HelixHeadlessMarkdown = __decorate([
|
|
91
|
+
customElement('helix-headless-markdown')
|
|
92
|
+
], HelixHeadlessMarkdown);
|
|
93
|
+
export { HelixHeadlessMarkdown };
|