repl-sdk 1.5.1 → 1.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repl-sdk",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -45,6 +45,7 @@
45
45
  "@types/hast": "^3.0.4",
46
46
  "@types/mdast": "^4.0.4",
47
47
  "common-tags": "^1.8.2",
48
+ "decorator-transforms": "2.3.1",
48
49
  "eslint": "^9.39.1",
49
50
  "prettier": "^3.7.4",
50
51
  "publint": "^0.3.16",
@@ -103,13 +104,14 @@
103
104
  "extends": "../../package.json"
104
105
  },
105
106
  "scripts": {
106
- "lint:fix": "pnpm -w exec lint fix",
107
+ "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
108
+ "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm run format",
107
109
  "example": "cd example && vite",
108
110
  "lint:package": "pnpm publint",
109
111
  "lint:js": "pnpm -w exec lint js",
110
112
  "lint:types": "tsc --noEmit",
111
113
  "lint:js:fix": "pnpm -w exec lint js:fix",
112
- "lint:prettier:fix": "pnpm -w exec lint prettier:fix",
114
+ "format": "pnpm -w exec lint prettier:fix",
113
115
  "lint:prettier": "pnpm -w exec lint prettier",
114
116
  "test:node": "vitest"
115
117
  }
@@ -12,6 +12,7 @@
12
12
  * @property {CodeBlock[]} codeBlocks
13
13
  */
14
14
 
15
+ import remarkEscapeComponents, { REPL_LT } from '../../remark-escape-components.js';
15
16
  import { buildCompiler } from './build-compiler.js';
16
17
 
17
18
  export { buildCompiler } from './build-compiler.js';
@@ -22,36 +23,67 @@ export { buildCompiler } from './build-compiler.js';
22
23
  * @returns {Promise<ParseResult>}
23
24
  */
24
25
  export async function parseMarkdown(input, options) {
25
- const markdownCompiler = options?.compiler ?? buildCompiler(options);
26
+ const markdownCompiler =
27
+ options?.compiler ??
28
+ buildCompiler({
29
+ ...options,
30
+ remarkPlugins: [...(options?.remarkPlugins || []), remarkEscapeComponents],
31
+ });
26
32
  const processed = await markdownCompiler.process(input);
27
33
  const liveCode = /** @type {CodeBlock[]} */ (processed.data.liveCode || []);
28
34
  // @ts-ignore - processed is typed as unknown due to unified processor complexity
29
35
  let templateOnly = processed.toString();
30
36
 
31
- // Unescape PascalCase components that had only the opening < HTML-entity escaped
32
- // BUT only outside of <pre><code> blocks where escaping should be preserved
33
- // (inline <code> tags should have components unescaped)
34
- // Split by <pre><code>...</code></pre> to exclude only code blocks
35
- const parts = templateOnly.split(/(<pre[^>]*>.*?<\/pre>)/is);
37
+ // 1. Convert the placeholder written by the remark plugin to &#x3C;
38
+ // This placeholder survives the entire unified pipeline without being
39
+ // entity-encoded, so no double-escaping can occur.
40
+ if (REPL_LT) {
41
+ templateOnly = templateOnly.replaceAll(REPL_LT, '&#x3C;');
42
+ }
43
+
44
+ // 2. The pipeline may HTML-escape `<` for PascalCase component invocations
45
+ // that appear in regular markdown (outside code/backticks). Undo that so
46
+ // Glimmer can still invoke them. We only unescape outside <code> elements
47
+ // (and outside <pre> blocks) to preserve escaping in code.
48
+ templateOnly = unescapeComponentsOutsideCode(templateOnly);
49
+
50
+ return { text: templateOnly, codeBlocks: liveCode };
51
+ }
52
+
53
+ /**
54
+ * Undo HTML-escaping of PascalCase component tags that appear outside
55
+ * `<code>` and `<pre>` blocks so Glimmer can invoke them.
56
+ *
57
+ * @param {string} html
58
+ * @returns {string}
59
+ */
60
+ function unescapeComponentsOutsideCode(html) {
61
+ // Split by <pre>…</pre> blocks first – never touch code fences.
62
+ const parts = html.split(/(<pre[\s\S]*?<\/pre>)/gi);
36
63
 
37
64
  for (let i = 0; i < parts.length; i++) {
38
- const part = parts[i];
39
-
40
- // Only process parts that are NOT pre blocks (odd indices are pre blocks)
41
- if (i % 2 === 0 && part) {
42
- // Pattern: &#x3C;ComponentName ... / > (only < is escaped as &#x3C;)
43
- parts[i] = part.replace(/&#x3C;([A-Z][a-zA-Z0-9]*\s[^<]*?)>/g, (match, content) => {
44
- // Only unescape if it contains @ (attribute) indicating a component
45
- if (content.includes('@')) {
46
- return `<${content}>`;
65
+ // Only touch content outside <pre>
66
+ if (i % 2 === 0) {
67
+ // Split by <code>…</code> so we skip inline code too
68
+ const part = parts[i] ?? '';
69
+ const codeParts = part.split(/(<code[^>]*>[\s\S]*?<\/code>)/gi);
70
+
71
+ for (let j = 0; j < codeParts.length; j++) {
72
+ const segment = codeParts[j];
73
+
74
+ // Even indices are outside <code> – unescape PascalCase there
75
+ if (j % 2 === 0 && segment) {
76
+ codeParts[j] = segment
77
+ .replace(/&#x3C;([A-Z][a-zA-Z0-9]*\s[^<]*?)>/g, (_m, content) =>
78
+ content.includes('@') ? `<${content}>` : _m
79
+ )
80
+ .replace(/&#x3C;\/([A-Z][a-zA-Z0-9]*)>/g, '</$1>');
47
81
  }
82
+ }
48
83
 
49
- return match;
50
- });
84
+ parts[i] = codeParts.join('');
51
85
  }
52
86
  }
53
87
 
54
- templateOnly = parts.join('');
55
-
56
- return { text: templateOnly, codeBlocks: liveCode };
88
+ return parts.join('');
57
89
  }
@@ -230,7 +230,7 @@ describe('default features', () => {
230
230
  expect(result).toMatchInlineSnapshot(`
231
231
  {
232
232
  "codeBlocks": [],
233
- "text": "<h2 id="hello-foo-two"><code><Hello @foo="two" /></code></h2>",
233
+ "text": "<h2 id="hello-foo-two"><code>&#x3C;Hello @foo="two" /></code></h2>",
234
234
  }
235
235
  `);
236
236
  });
@@ -0,0 +1,39 @@
1
+ import { visit } from 'unist-util-visit';
2
+
3
+ /**
4
+ * A unique placeholder that replaces `<` in PascalCase component tags
5
+ * inside inline code and non-live code fences. It survives the unified
6
+ * pipeline without being entity-encoded, and is converted to `&#x3C;`
7
+ * in the final post-processing step inside `parseMarkdown()`.
8
+ */
9
+ export const REPL_LT = '__REPL_LT__';
10
+
11
+ /**
12
+ * Remark plugin: escape PascalCase component tags in `inlineCode` and
13
+ * non-live `code` (code fence) nodes by replacing `<` with a placeholder.
14
+ */
15
+ function remarkEscapeComponents() {
16
+ /** @param {import('mdast').Root} tree */
17
+ return (tree) => {
18
+ visit(tree, (node) => {
19
+ // Inline code (backticks)
20
+ if (node.type === 'inlineCode') {
21
+ node.value = node.value
22
+ .replace(/<([A-Z][a-zA-Z0-9]*(?:\s[^<]*)?)>/g, REPL_LT + '$1>')
23
+ .replace(/<\/([A-Z][a-zA-Z0-9]*)>/g, REPL_LT + '/$1>');
24
+ }
25
+
26
+ // Code fences (``` blocks)
27
+ if (node.type === 'code') {
28
+ // Only escape if not live
29
+ if (!/\blive\b/.test(node.meta || '')) {
30
+ node.value = node.value
31
+ .replace(/<([A-Z][a-zA-Z0-9]*(?:\s[^<]*)?)>/g, REPL_LT + '$1>')
32
+ .replace(/<\/([A-Z][a-zA-Z0-9]*)>/g, REPL_LT + '/$1>');
33
+ }
34
+ }
35
+ });
36
+ };
37
+ }
38
+
39
+ export default remarkEscapeComponents;