@rohal12/spindle 0.43.3 → 0.43.5
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/dist/pkg/format.js +1 -1
- package/package.json +1 -1
- package/src/markup/markdown.ts +19 -2
- package/src/markup/render.tsx +91 -5
package/package.json
CHANGED
package/src/markup/markdown.ts
CHANGED
|
@@ -8,14 +8,31 @@ import {
|
|
|
8
8
|
/**
|
|
9
9
|
* Parse a text string as CommonMark markdown and return an HTML string.
|
|
10
10
|
* Includes GFM table and strikethrough extensions.
|
|
11
|
+
*
|
|
12
|
+
* When `inline` is true, block-level constructs (lists, headings, blockquotes,
|
|
13
|
+
* thematic breaks) are disabled. This is used when rendering content inside
|
|
14
|
+
* inline HTML elements like `<span>` where block-level output is invalid.
|
|
11
15
|
*/
|
|
12
|
-
export function markdownToHtml(
|
|
16
|
+
export function markdownToHtml(
|
|
17
|
+
text: string,
|
|
18
|
+
options?: { inline?: boolean },
|
|
19
|
+
): string {
|
|
20
|
+
const disabled: string[] = ['codeIndented'];
|
|
21
|
+
if (options?.inline) {
|
|
22
|
+
disabled.push(
|
|
23
|
+
'list',
|
|
24
|
+
'headingAtx',
|
|
25
|
+
'setextUnderline',
|
|
26
|
+
'thematicBreak',
|
|
27
|
+
'blockQuote',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
13
30
|
return micromark(text, {
|
|
14
31
|
allowDangerousHtml: true,
|
|
15
32
|
extensions: [
|
|
16
33
|
gfmTable(),
|
|
17
34
|
gfmStrikethrough(),
|
|
18
|
-
{ disable: { null:
|
|
35
|
+
{ disable: { null: disabled } },
|
|
19
36
|
],
|
|
20
37
|
htmlExtensions: [gfmTableHtml(), gfmStrikethroughHtml()],
|
|
21
38
|
});
|
package/src/markup/render.tsx
CHANGED
|
@@ -87,6 +87,43 @@ function convertDomNode(
|
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/** Inline elements where block-level markdown (lists, headings) is invalid. */
|
|
91
|
+
const INLINE_ELEMENTS = new Set([
|
|
92
|
+
'a',
|
|
93
|
+
'abbr',
|
|
94
|
+
'b',
|
|
95
|
+
'bdi',
|
|
96
|
+
'bdo',
|
|
97
|
+
'br',
|
|
98
|
+
'cite',
|
|
99
|
+
'code',
|
|
100
|
+
'data',
|
|
101
|
+
'dfn',
|
|
102
|
+
'em',
|
|
103
|
+
'i',
|
|
104
|
+
'kbd',
|
|
105
|
+
'label',
|
|
106
|
+
'mark',
|
|
107
|
+
'meter',
|
|
108
|
+
'output',
|
|
109
|
+
'progress',
|
|
110
|
+
'q',
|
|
111
|
+
'rp',
|
|
112
|
+
'rt',
|
|
113
|
+
'ruby',
|
|
114
|
+
's',
|
|
115
|
+
'samp',
|
|
116
|
+
'small',
|
|
117
|
+
'span',
|
|
118
|
+
'strong',
|
|
119
|
+
'sub',
|
|
120
|
+
'sup',
|
|
121
|
+
'time',
|
|
122
|
+
'u',
|
|
123
|
+
'var',
|
|
124
|
+
'wbr',
|
|
125
|
+
]);
|
|
126
|
+
|
|
90
127
|
function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
|
|
91
128
|
const resolve = useInterpolate();
|
|
92
129
|
const nobr = useContext(NobrContext);
|
|
@@ -97,13 +134,16 @@ function HtmlNodeRenderer({ node }: { node: HtmlNode }) {
|
|
|
97
134
|
attrs[k] = resolve(v) ?? v;
|
|
98
135
|
}
|
|
99
136
|
const isSvgRoot = node.tag.toLowerCase() === 'svg';
|
|
100
|
-
|
|
101
|
-
//
|
|
137
|
+
const isInline = INLINE_ELEMENTS.has(node.tag.toLowerCase());
|
|
138
|
+
// Inside SVG, skip markdown processing entirely — markdown wraps content
|
|
139
|
+
// in <p> tags which break the SVG namespace.
|
|
140
|
+
// Inside inline elements, disable block-level markdown (lists, headings,
|
|
141
|
+
// blockquotes) since those produce invalid HTML inside inline containers.
|
|
102
142
|
const children =
|
|
103
143
|
node.children.length > 0
|
|
104
144
|
? inSvg || isSvgRoot
|
|
105
145
|
? renderInlineNodes(node.children)
|
|
106
|
-
: renderNodes(node.children, { nobr, locals })
|
|
146
|
+
: renderNodes(node.children, { nobr, locals, inline: isInline })
|
|
107
147
|
: undefined;
|
|
108
148
|
const element = h(node.tag, attrs, children);
|
|
109
149
|
return isSvgRoot ? (
|
|
@@ -254,6 +294,39 @@ function getVariableTextValue(
|
|
|
254
294
|
return value == null ? '' : String(value);
|
|
255
295
|
}
|
|
256
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Characters/patterns that trigger CommonMark or GFM transformations.
|
|
299
|
+
* Any match → fall through to the full micromark pipeline.
|
|
300
|
+
* False positives (e.g. `-` used as text, not list) just use the slower path.
|
|
301
|
+
*/
|
|
302
|
+
const MARKDOWN_SYNTAX_RE = /[*_`#|~\[>\\\-+=]|!\[|\d+\./;
|
|
303
|
+
const BLANK_LINE_RE = /\n\s*\n/;
|
|
304
|
+
const PLACEHOLDER_SPLIT_RE = /(<span data-tw="\d+"><\/span>)/;
|
|
305
|
+
const PLACEHOLDER_IDX_RE = /^<span data-tw="(\d+)"><\/span>$/;
|
|
306
|
+
const PLACEHOLDER_STRIP_RE = /<span data-tw="\d+"><\/span>/g;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build Preact vnodes from a combined string that contains only plain text
|
|
310
|
+
* and <span data-tw="N"></span> placeholders. No micromark, no innerHTML.
|
|
311
|
+
*/
|
|
312
|
+
function buildPlainTextVnodes(
|
|
313
|
+
combined: string,
|
|
314
|
+
components: preact.ComponentChildren[],
|
|
315
|
+
nobr?: boolean,
|
|
316
|
+
): preact.ComponentChildren {
|
|
317
|
+
const parts = combined.split(PLACEHOLDER_SPLIT_RE);
|
|
318
|
+
const children: preact.ComponentChildren[] = [];
|
|
319
|
+
for (const part of parts) {
|
|
320
|
+
const m = PLACEHOLDER_IDX_RE.exec(part);
|
|
321
|
+
if (m) {
|
|
322
|
+
children.push(components[parseInt(m[1]!, 10)]);
|
|
323
|
+
} else if (part) {
|
|
324
|
+
children.push(part);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return nobr ? <>{children}</> : h('p', null, ...children);
|
|
328
|
+
}
|
|
329
|
+
|
|
257
330
|
/**
|
|
258
331
|
* Render AST nodes with full CommonMark markdown support.
|
|
259
332
|
*
|
|
@@ -267,7 +340,11 @@ function getVariableTextValue(
|
|
|
267
340
|
*/
|
|
268
341
|
export function renderNodes(
|
|
269
342
|
nodes: ASTNode[],
|
|
270
|
-
options?: {
|
|
343
|
+
options?: {
|
|
344
|
+
nobr?: boolean;
|
|
345
|
+
locals?: Record<string, unknown>;
|
|
346
|
+
inline?: boolean;
|
|
347
|
+
},
|
|
271
348
|
): preact.ComponentChildren {
|
|
272
349
|
if (nodes.length === 0) return null;
|
|
273
350
|
|
|
@@ -303,8 +380,17 @@ export function renderNodes(
|
|
|
303
380
|
}
|
|
304
381
|
}
|
|
305
382
|
|
|
383
|
+
// Fast path: skip micromark + innerHTML when text has no markdown syntax.
|
|
384
|
+
// This eliminates ~655 innerHTML calls on plain UI text like "ALMA",
|
|
385
|
+
// "▸ Crew", "Activate" that pass through the full pipeline only to
|
|
386
|
+
// produce the same text they started with (issue #145).
|
|
387
|
+
const textOnly = combined.replace(PLACEHOLDER_STRIP_RE, '');
|
|
388
|
+
if (!MARKDOWN_SYNTAX_RE.test(textOnly) && !BLANK_LINE_RE.test(textOnly)) {
|
|
389
|
+
return buildPlainTextVnodes(combined, components, options?.nobr);
|
|
390
|
+
}
|
|
391
|
+
|
|
306
392
|
// Run combined text through markdown
|
|
307
|
-
const html = markdownToHtml(combined);
|
|
393
|
+
const html = markdownToHtml(combined, { inline: options?.inline });
|
|
308
394
|
|
|
309
395
|
// Convert HTML to Preact VNodes, replacing placeholders with components
|
|
310
396
|
return htmlToPreact(html, components, options?.nobr);
|