Skip to content

Commit a326062

Browse files
fix(parser): simplify filename regex in parseThematicBlock function (#367)
1 parent d6bf41c commit a326062

File tree

2 files changed

+265
-2
lines changed

2 files changed

+265
-2
lines changed

src/runtime/parser/handlers/utils.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,26 @@ export function parseThematicBlock(lang: string) {
1818

1919
const languageMatches = lang.replace(/[{|[](.+)/, '').match(/^[^ \t]+(?=[ \t]|$)/)
2020
const highlightTokensMatches = lang.match(/\{([^}]*)\}/)
21-
const filenameMatches = lang.match(/\[((\\\]|[^\]])*)\]/)
21+
const filenameMatches = lang.match(/\[(.*)\]/)
2222

2323
const meta = lang
2424
.replace(languageMatches?.[0] ?? '', '')
2525
.replace(highlightTokensMatches?.[0] ?? '', '')
2626
.replace(filenameMatches?.[0] ?? '', '')
2727
.trim()
2828

29+
// Process filename to handle backslashes correctly
30+
let filename = undefined
31+
if (filenameMatches?.[1]) {
32+
// Only unescape special regex characters but preserve path backslashes
33+
filename = filenameMatches[1].replace(/\\([[\]{}().*+?^$|])/g, '$1')
34+
}
35+
2936
return {
3037
language: languageMatches?.[0] || undefined,
3138
highlights: parseHighlightedLines(highlightTokensMatches?.[1] || undefined),
3239
// https://github.com/nuxt/content/pull/2169
33-
filename: filenameMatches?.[1].replace(/\\\]/g, ']') || undefined,
40+
filename,
3441
meta
3542
}
3643
}
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { expect, it } from 'vitest'
2+
import { parseMarkdown } from '../utils/parser'
3+
4+
const md = `
5+
\`\`\`ts {1-3} [server/api/products/[id].ts] meta=meta-value
6+
class C {
7+
private name: string = "foo"
8+
}
9+
10+
const c = new C()
11+
\`\`\`
12+
`.trim()
13+
14+
it('Code block with server API route filename with single brackets', async () => {
15+
const { body } = await parseMarkdown(md, {
16+
highlight: false
17+
})
18+
expect(body).toHaveProperty('type', 'root')
19+
expect(body.children).toHaveLength(1)
20+
expect(body.children).toMatchInlineSnapshot(`
21+
[
22+
{
23+
"children": [
24+
{
25+
"children": [
26+
{
27+
"type": "text",
28+
"value": "class C {
29+
private name: string = "foo"
30+
}
31+
32+
const c = new C()
33+
",
34+
},
35+
],
36+
"props": {
37+
"__ignoreMap": "",
38+
},
39+
"tag": "code",
40+
"type": "element",
41+
},
42+
],
43+
"props": {
44+
"className": [
45+
"language-ts",
46+
],
47+
"code": "class C {
48+
private name: string = "foo"
49+
}
50+
51+
const c = new C()
52+
",
53+
"filename": "server/api/products/[id].ts",
54+
"highlights": [
55+
1,
56+
2,
57+
3,
58+
],
59+
"language": "ts",
60+
"meta": "meta=meta-value",
61+
},
62+
"tag": "pre",
63+
"type": "element",
64+
},
65+
]
66+
`)
67+
})
68+
69+
const md2 = `
70+
\`\`\`ts {1-3} [server/api/products/[[id]].ts] meta=meta-value
71+
class C {
72+
private name: string = "foo"
73+
}
74+
75+
const c = new C()
76+
\`\`\`
77+
`.trim()
78+
79+
it('Code block with server API route filename with double brackets', async () => {
80+
const { body } = await parseMarkdown(md2, {
81+
highlight: false
82+
})
83+
expect(body).toHaveProperty('type', 'root')
84+
expect(body.children).toHaveLength(1)
85+
expect(body.children).toMatchInlineSnapshot(`
86+
[
87+
{
88+
"children": [
89+
{
90+
"children": [
91+
{
92+
"type": "text",
93+
"value": "class C {
94+
private name: string = "foo"
95+
}
96+
97+
const c = new C()
98+
",
99+
},
100+
],
101+
"props": {
102+
"__ignoreMap": "",
103+
},
104+
"tag": "code",
105+
"type": "element",
106+
},
107+
],
108+
"props": {
109+
"className": [
110+
"language-ts",
111+
],
112+
"code": "class C {
113+
private name: string = "foo"
114+
}
115+
116+
const c = new C()
117+
",
118+
"filename": "server/api/products/[[id]].ts",
119+
"highlights": [
120+
1,
121+
2,
122+
3,
123+
],
124+
"language": "ts",
125+
"meta": "meta=meta-value",
126+
},
127+
"tag": "pre",
128+
"type": "element",
129+
},
130+
]
131+
`)
132+
})
133+
134+
const mdQueryParams = `
135+
\`\`\`js [api/search?query=test&page=1] showLineNumbers
136+
function search(params) {
137+
return fetch(\`/api/search?q=\${params.query}&page=\${params.page}\`)
138+
}
139+
\`\`\`
140+
`.trim()
141+
142+
it('Code block with filename containing query parameters', async () => {
143+
const { body } = await parseMarkdown(mdQueryParams, {
144+
highlight: false
145+
})
146+
expect(body).toHaveProperty('type', 'root')
147+
expect(body.children).toHaveLength(1)
148+
expect(body.children[0].props.filename).toBe('api/search?query=test&page=1')
149+
expect(body.children[0].props.language).toBe('js')
150+
})
151+
152+
const mdNestedPath = `
153+
\`\`\`ts [services/auth/strategies/oauth2/github.ts]
154+
export default defineAuthStrategy({
155+
name: 'github',
156+
handler: (req, res) => {
157+
// OAuth implementation
158+
}
159+
})
160+
\`\`\`
161+
`.trim()
162+
163+
it('Code block with deeply nested filepath', async () => {
164+
const { body } = await parseMarkdown(mdNestedPath, {
165+
highlight: false
166+
})
167+
expect(body).toHaveProperty('type', 'root')
168+
expect(body.children).toHaveLength(1)
169+
expect(body.children[0].props.filename).toBe('services/auth/strategies/oauth2/github.ts')
170+
})
171+
172+
const mdSpecialChars = `
173+
\`\`\`json [utils/i18n/translations-[locale].json]
174+
{
175+
"welcome": "Welcome to our site",
176+
"login": "Log in",
177+
"signup": "Sign up"
178+
}
179+
\`\`\`
180+
`.trim()
181+
182+
it('Code block with filename containing square brackets as part of the filename', async () => {
183+
const { body } = await parseMarkdown(mdSpecialChars, {
184+
highlight: false
185+
})
186+
expect(body).toHaveProperty('type', 'root')
187+
expect(body.children).toHaveLength(1)
188+
expect(body.children[0].props.filename).toBe('utils/i18n/translations-[locale].json')
189+
})
190+
191+
const mdMultipleParams = `
192+
\`\`\`js {2-5} [api/users/[userId]/posts/[postId].js]
193+
export default defineEventHandler(async (event) => {
194+
const { userId, postId } = event.context.params
195+
const post = await prisma.post.findUnique({
196+
where: { id: postId, authorId: userId }
197+
})
198+
return post
199+
})
200+
\`\`\`
201+
`.trim()
202+
203+
it('Code block with path containing multiple parameters', async () => {
204+
const { body } = await parseMarkdown(mdMultipleParams, {
205+
highlight: false
206+
})
207+
expect(body).toHaveProperty('type', 'root')
208+
expect(body.children).toHaveLength(1)
209+
expect(body.children[0].props.filename).toBe('api/users/[userId]/posts/[postId].js')
210+
expect(body.children[0].props.highlights).toEqual([2, 3, 4, 5])
211+
})
212+
213+
const mdDashesAndUnderscores = `
214+
\`\`\`vue [components/user-settings/profile_image.vue]
215+
<template>
216+
<div class="profile-image">
217+
<img :src="profileImageUrl" alt="User profile" />
218+
</div>
219+
</template>
220+
\`\`\`
221+
`.trim()
222+
223+
it('Code block with dashes and underscores in filename', async () => {
224+
const { body } = await parseMarkdown(mdDashesAndUnderscores, {
225+
highlight: false
226+
})
227+
expect(body).toHaveProperty('type', 'root')
228+
expect(body.children).toHaveLength(1)
229+
expect(body.children[0].props.filename).toBe('components/user-settings/profile_image.vue')
230+
expect(body.children[0].props.language).toBe('vue')
231+
})
232+
233+
const mdWindowsPath = `
234+
\`\`\`tsx [components\\Header\\Navigation.tsx]
235+
export function Navigation() {
236+
return (
237+
<nav className="main-nav">
238+
<ul>
239+
<li><Link href="/">Home</Link></li>
240+
<li><Link href="/about">About</Link></li>
241+
</ul>
242+
</nav>
243+
)
244+
}
245+
\`\`\`
246+
`.trim()
247+
248+
it('Code block with Windows-style backslash path separators', async () => {
249+
const { body } = await parseMarkdown(mdWindowsPath, {
250+
highlight: false
251+
})
252+
expect(body).toHaveProperty('type', 'root')
253+
expect(body.children).toHaveLength(1)
254+
expect(body.children[0].props.filename).toBe('components\\Header\\Navigation.tsx')
255+
expect(body.children[0].props.language).toBe('tsx')
256+
})

0 commit comments

Comments
 (0)