Skip to content

Commit 3cb1179

Browse files
authoredMar 11, 2024
Escape {{ after the rehype phase instead of during the remark phase (#1704)
1 parent ae7e596 commit 3cb1179

File tree

6 files changed

+132
-39
lines changed

6 files changed

+132
-39
lines changed
 

‎packages/ember-repl/addon/src/compile/formats/markdown.ts

+20-25
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,6 @@ const ALLOWED_LANGUAGES = ['gjs', 'hbs'] as const;
4949
type AllowedLanguage = (typeof ALLOWED_LANGUAGES)[number];
5050
type RelevantCode = Omit<Code, 'lang'> & { lang: AllowedLanguage };
5151

52-
const escapeCurlies = (node: Text | Parent) => {
53-
if ('value' in node && node.value) {
54-
node.value = node.value.replace(/{{/g, '\\{{');
55-
}
56-
57-
if ('children' in node && node.children) {
58-
node.children.forEach((child) => escapeCurlies(child as Parent));
59-
}
60-
61-
if (!node.data) {
62-
return;
63-
}
64-
65-
if ('hChildren' in node.data && Array.isArray(node.data['hChildren'])) {
66-
node.data['hChildren'].forEach(escapeCurlies);
67-
68-
return;
69-
}
70-
};
71-
7252
function isLive(meta: string) {
7353
return meta.includes('live');
7454
}
@@ -218,6 +198,24 @@ function liveCodeExtraction(options: Options = {}) {
218198
};
219199
}
220200

201+
function sanitizeForGlimmer(/* options */) {
202+
return (tree: Parent) => {
203+
visit(tree, 'element', (node: Parent) => {
204+
if ('tagName' in node) {
205+
if (node.tagName !== 'pre') return;
206+
207+
visit(node, 'text', (textNode: Text) => {
208+
if ('value' in textNode && textNode.value) {
209+
textNode.value = textNode.value.replace(/{{/g, '\\{{');
210+
}
211+
});
212+
213+
return 'skip';
214+
}
215+
});
216+
};
217+
}
218+
221219
function buildCompiler(options: ParseMarkdownOptions) {
222220
let compiler = unified().use(remarkParse).use(remarkGfm);
223221

@@ -261,9 +259,6 @@ function buildCompiler(options: ParseMarkdownOptions) {
261259
let properties = (node as any).properties;
262260

263261
if (properties?.[GLIMDOWN_PREVIEW]) {
264-
// Have to sanitize anything Glimmer could try to render
265-
escapeCurlies(node as Parent);
266-
267262
return 'skip';
268263
}
269264

@@ -274,8 +269,6 @@ function buildCompiler(options: ParseMarkdownOptions) {
274269
return;
275270
}
276271

277-
escapeCurlies(node as Parent);
278-
279272
return 'skip';
280273
}
281274

@@ -314,6 +307,8 @@ function buildCompiler(options: ParseMarkdownOptions) {
314307
});
315308
});
316309

310+
compiler = compiler.use(sanitizeForGlimmer);
311+
317312
// Finally convert to string! oofta!
318313
compiler = compiler.use(rehypeStringify, {
319314
collapseEmptyAttributes: true,

‎packages/ember-repl/test-app/app/app.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import config from 'test-app/config/environment';
1010
// But they aren't used.... so.. that's fun.
1111
Object.assign(window, {
1212
process: { env: {} },
13-
Buffer: {},
13+
// Polyfilled in webpack
14+
// Buffer: {},
1415
});
1516

1617
export default class App extends Application {

‎packages/ember-repl/test-app/ember-cli-build.js

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module.exports = function (defaults) {
5151
resolve: {
5252
fallback: {
5353
path: 'path-browserify',
54+
buffer: require.resolve('buffer/'),
5455
},
5556
},
5657
},

‎packages/ember-repl/test-app/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
"lint:prettier": "pnpm -w exec lint prettier"
2626
},
2727
"dependencies": {
28+
"@shikijs/rehype": "^1.1.7",
2829
"@types/unist": "^3.0.2",
30+
"buffer": "^6.0.3",
2931
"common-tags": "^1.8.2",
3032
"ember-repl": "workspace:*",
3133
"ember-resources": "^7.0.0",

‎packages/ember-repl/test-app/tests/unit/markdown-test.ts

+68-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { module, test } from 'qunit';
22

3+
import rehypeShiki from '@shikijs/rehype';
34
import { stripIndent } from 'common-tags';
45
import { invocationOf, nameFor } from 'ember-repl';
56
import { parseMarkdown } from 'ember-repl/formats/markdown';
@@ -10,13 +11,7 @@ import { visit } from 'unist-util-visit';
1011
* indentation are stripped
1112
*/
1213
function assertOutput(actual: string, expected: string) {
13-
let _actual = actual
14-
.split('\n')
15-
.filter(Boolean)
16-
.join('\n')
17-
.trim()
18-
.replace(/<div class="glimdown-render">/, '')
19-
.replace(/<\/div>/, '');
14+
let _actual = actual.split('\n').filter(Boolean).join('\n').trim();
2015
let _expected = expected.split('\n').filter(Boolean).join('\n').trim();
2116

2217
QUnit.assert.equal(_actual, _expected);
@@ -60,7 +55,7 @@ module('Unit | parseMarkdown()', function () {
6055
<h1>Title</h1>
6156
6257
<div class=\"glimdown-snippet relative\"><pre><code class=\"language-js\"> const two = 2;
63-
</code></pre><CopyMenu />
58+
</code></pre><CopyMenu /></div>
6459
`
6560
);
6661

@@ -178,6 +173,38 @@ module('Unit | parseMarkdown()', function () {
178173

179174
assert.deepEqual(result.blocks, []);
180175
});
176+
177+
test('rehypePlugins retain {{ }} escaping', async function () {
178+
let result = await parseMarkdown(
179+
stripIndent`
180+
# Title
181+
182+
\`\`\`gjs
183+
const two = 2
184+
185+
<template>
186+
{{two}}
187+
</template>
188+
\`\`\`
189+
`,
190+
{
191+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
192+
rehypePlugins: [[rehypeShiki as any, { theme: 'github-dark' }]],
193+
}
194+
);
195+
196+
assertOutput(
197+
result.templateOnlyGlimdown,
198+
`<h1>Title</h1>
199+
<div class="glimdown-snippet relative"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> two</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 2</span></span>
200+
<span class="line"></span>
201+
<span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">template</span><span style="color:#E1E4E8">></span></span>
202+
<span class="line"><span style="color:#F97583"> \\{{</span><span style="color:#79B8FF">two</span><span style="color:#F97583">}}</span></span>
203+
<span class="line"><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#85E89D">template</span><span style="color:#E1E4E8">></span></span>
204+
<span class="line"></span></code></pre></div>
205+
`
206+
);
207+
});
181208
});
182209

183210
module('hbs', function () {
@@ -199,7 +226,7 @@ module('Unit | parseMarkdown()', function () {
199226
stripIndent`
200227
<h1>Title</h1>
201228
202-
${invocationOf(name)}
229+
<div class=\"glimdown-render\">${invocationOf(name)}</div>
203230
`
204231
);
205232

@@ -232,7 +259,7 @@ module('Unit | parseMarkdown()', function () {
232259
<h1>Title</h1>
233260
234261
<div class=\"glimdown-snippet relative\"><pre><code class=\"language-gjs\"> const two = 2;
235-
</code></pre><CopyMenu />
262+
</code></pre><CopyMenu /></div>
236263
`
237264
);
238265

@@ -255,7 +282,7 @@ module('Unit | parseMarkdown()', function () {
255282
stripIndent`
256283
<h1>Title</h1>
257284
258-
${invocationOf(name)}
285+
<div class=\"glimdown-render\">${invocationOf(name)}</div>
259286
`
260287
);
261288

@@ -268,6 +295,34 @@ module('Unit | parseMarkdown()', function () {
268295
]);
269296
});
270297

298+
test('Code with preview fence has {{ }} tokens escaped', async function () {
299+
let result = await parseMarkdown(stripIndent`
300+
# Title
301+
302+
\`\`\`gjs
303+
const two = 2
304+
305+
<template>
306+
{{two}}
307+
</template>
308+
\`\`\`
309+
`);
310+
311+
assertOutput(
312+
result.templateOnlyGlimdown,
313+
stripIndent`
314+
<h1>Title</h1>
315+
316+
<div class=\"glimdown-snippet relative\"><pre><code class=\"language-gjs\">const two = 2
317+
318+
&#x3C;template>
319+
\\{{two}}
320+
&#x3C;/template>
321+
</code></pre></div>
322+
`
323+
);
324+
});
325+
271326
test('Can invoke a component again when defined in a live fence', async function (assert) {
272327
let snippet = `const two = 2`;
273328
let name = nameFor(snippet);
@@ -285,7 +340,7 @@ module('Unit | parseMarkdown()', function () {
285340
stripIndent`
286341
<h1>Title</h1>
287342
288-
${invocationOf(name)}
343+
<div class=\"glimdown-render\">${invocationOf(name)}</div>
289344
<Demo />
290345
`
291346
);
@@ -319,7 +374,7 @@ module('Unit | parseMarkdown()', function () {
319374
stripIndent`
320375
<p>hi</p>
321376
322-
${invocationOf(name)}
377+
<div class=\"glimdown-render\">${invocationOf(name)}</div>
323378
<div class=\"glimdown-snippet relative\"><pre><code class=\"language-gjs\">import Component from '@glimmer/component';
324379
import { on } from '@ember/modifier';
325380
&#x3C;template>

‎pnpm-lock.yaml

+39
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)