repl-sdk 1.5.1 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repl-sdk",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -45,11 +45,12 @@
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",
51
52
  "typescript": "^5.9.3",
52
- "vite": "^7.3.1",
53
+ "vite": "^8.0.8",
53
54
  "vite-plugin-dts": "4.5.4",
54
55
  "vitest": "^4.0.18"
55
56
  },
@@ -95,21 +96,22 @@
95
96
  "unified": "^11.0.5",
96
97
  "unist-util-visit": "^5.0.0",
97
98
  "vfile": "^6.0.3",
98
- "codemirror-lang-glimdown": "^2.0.3",
99
- "codemirror-lang-glimmer": "^2.0.3",
100
- "codemirror-lang-glimmer-js": "^2.0.3"
99
+ "codemirror-lang-glimdown": "^2.0.4",
100
+ "codemirror-lang-glimmer": "^2.0.4",
101
+ "codemirror-lang-glimmer-js": "^2.0.4"
101
102
  },
102
103
  "volta": {
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
  }
@@ -170,7 +170,18 @@ export async function compiler(config, api) {
170
170
  const { renderComponent } = await compiler.tryResolve('@ember/renderer');
171
171
 
172
172
  const owner = makeOwner(config.owner);
173
- const result = renderComponent(compiled, { into: element, owner });
173
+ const args = /** @type {Record<string, unknown> | undefined} */ (
174
+ extra && typeof extra === 'object' && 'args' in extra
175
+ ? /** @type {Record<string, unknown>} */ (extra).args
176
+ : undefined
177
+ );
178
+ const result = renderComponent(compiled, {
179
+ into: element,
180
+ owner,
181
+ ...(args ? { args } : {}),
182
+ });
183
+
184
+ compiler.announce('info', 'Ember Island Rendered');
174
185
 
175
186
  return () => result.destroy();
176
187
  },
@@ -98,9 +98,16 @@ export async function compiler(config, api) {
98
98
 
99
99
  const { renderComponent } = await compiler.tryResolve('@ember/renderer');
100
100
 
101
+ const args = /** @type {Record<string, unknown> | undefined} */ (
102
+ extra && typeof extra === 'object' && 'args' in extra
103
+ ? /** @type {Record<string, unknown>} */ (extra).args
104
+ : undefined
105
+ );
106
+
101
107
  const result = renderComponent(compiled, {
102
108
  into: element,
103
109
  owner: userOptions.owner,
110
+ ...(args ? { args } : {}),
104
111
  });
105
112
 
106
113
  const destroy = () => result.destroy();
@@ -67,7 +67,18 @@ export async function compiler(config, api) {
67
67
 
68
68
  const { renderComponent } = await compiler.tryResolve('@ember/renderer');
69
69
  const owner = makeOwner(config.owner);
70
- const result = renderComponent(compiled, { into: element, owner });
70
+ const args = /** @type {Record<string, unknown> | undefined} */ (
71
+ extra && typeof extra === 'object' && 'args' in extra
72
+ ? /** @type {Record<string, unknown>} */ (extra).args
73
+ : undefined
74
+ );
75
+ const result = renderComponent(compiled, {
76
+ into: element,
77
+ owner,
78
+ ...(args ? { args } : {}),
79
+ });
80
+
81
+ compiler.announce('info', 'Ember Island Rendered');
71
82
 
72
83
  return () => result.destroy();
73
84
  },
@@ -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
  });
package/src/index.js CHANGED
@@ -345,7 +345,7 @@ export class Compiler {
345
345
  /**
346
346
  * @param {string} format
347
347
  * @param {string} text
348
- * @param {{ fileName?: string, flavor?: string, [key: string]: unknown }} [ options ]
348
+ * @param {{ fileName?: string, flavor?: string, args?: Record<string, unknown>, [key: string]: unknown }} [ options ]
349
349
  * @returns {Promise<{ element: HTMLElement, destroy: () => void }>}
350
350
  */
351
351
  async compile(format, text, options = {}) {
@@ -411,6 +411,7 @@ export class Compiler {
411
411
  return this.#render(compiler, value, {
412
412
  ...extras,
413
413
  compiled: value,
414
+ ...(opts.args ? { args: opts.args } : {}),
414
415
  });
415
416
  }
416
417
 
@@ -421,7 +422,10 @@ export class Compiler {
421
422
 
422
423
  this.#log('[compile] preparing to render', defaultExport, extras);
423
424
 
424
- return this.#render(compiler, defaultExport, extras);
425
+ return this.#render(compiler, defaultExport, {
426
+ ...extras,
427
+ ...(opts.args ? { args: opts.args } : {}),
428
+ });
425
429
  }
426
430
 
427
431
  #compilerCache = new WeakMap();
@@ -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;