Skip to content

Commit 4ae764b

Browse files
committed
fix: support @click in html, close #1
1 parent 345143d commit 4ae764b

File tree

7 files changed

+253
-36
lines changed

7 files changed

+253
-36
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@
5555
"@antfu/ni": "^0.16.2",
5656
"@types/node": "^18.0.0",
5757
"@vue/test-utils": "^2.0.1",
58-
"bumpp": "^8.2.1",
58+
"bumpp": "^8.2.0",
5959
"eslint": "^8.18.0",
60-
"rollup": "^2.75.7",
60+
"rollup": "^2.75.6",
6161
"tsup": "^6.1.2",
6262
"typescript": "^4.7.4",
6363
"vite": "^2.9.12",

pnpm-lock.yaml

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/markdown.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import MarkdownIt from 'markdown-it'
22
import matter from 'gray-matter'
33
import { toArray, uniq } from '@antfu/utils'
44
import type { ResolvedOptions } from './types'
5+
import { componentPlugin } from './plugins/component'
56

67
const scriptSetupRE = /<\s*script([^>]*)\bsetup\b([^>]*)>([\s\S]*)<\/script>/mg
78
const defineExposeRE = /defineExpose\s*\(/mg
@@ -46,6 +47,8 @@ export function createMarkdown(options: ResolvedOptions) {
4647
...options.markdownItOptions,
4748
})
4849

50+
markdown.use(componentPlugin)
51+
4952
markdown.linkify.set({ fuzzyLink: false })
5053

5154
options.markdownItUses.forEach((e) => {
@@ -168,8 +171,10 @@ export function createMarkdown(options: ResolvedOptions) {
168171
: []),
169172
]
170173

171-
const sfc = `<template>${html}</template>\n${scripts.filter(Boolean).join('\n')}\n${customBlocks.blocks.join('\n')}\n`
172-
173-
return sfc
174+
return [
175+
`<template>${html}</template>`,
176+
...scripts.map(i => i.trim()).filter(Boolean),
177+
...customBlocks.blocks,
178+
].join('\n')
174179
}
175180
}

src/plugins/component.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// ported from https://github.com/vuejs/vitepress/blob/d0fdda69045bb280dbf39e035f1f8474247196a6/src/node/markdown/plugins/component.ts
2+
3+
import type MarkdownIt from 'markdown-it'
4+
import blockNames from 'markdown-it/lib/common/html_blocks'
5+
6+
/**
7+
* Vue reserved tags
8+
*
9+
* @see https://vuejs.org/api/built-in-components.html
10+
*/
11+
const vueReservedTags = [
12+
'template',
13+
'component',
14+
'transition',
15+
'transition-group',
16+
'keep-alive',
17+
'slot',
18+
'teleport',
19+
]
20+
21+
/**
22+
* According to markdown spec, all non-block html tags are treated as "inline"
23+
* tags (wrapped with <p></p>), including those "unknown" tags.
24+
*
25+
* Therefore, markdown-it processes "inline" tags and "unknown" tags in the
26+
* same way, and does not care if a tag is "inline" or "unknown".
27+
*
28+
* As we want to take those "unknown" tags as custom components, we should
29+
* treat them as "block" tags.
30+
*
31+
* So we have to distinguish between "inline" and "unknown" tags ourselves.
32+
*
33+
* The inline tags list comes from MDN.
34+
*
35+
* @see https://spec.commonmark.org/0.29/#raw-html
36+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
37+
*/
38+
const inlineTags = [
39+
'a',
40+
'abbr',
41+
'acronym',
42+
'audio',
43+
'b',
44+
'bdi',
45+
'bdo',
46+
'big',
47+
'br',
48+
'button',
49+
'canvas',
50+
'cite',
51+
'code',
52+
'data',
53+
'datalist',
54+
'del',
55+
'dfn',
56+
'em',
57+
'embed',
58+
'i',
59+
// iframe is treated as HTML blocks in markdown spec
60+
// 'iframe',
61+
'img',
62+
'input',
63+
'ins',
64+
'kbd',
65+
'label',
66+
'map',
67+
'mark',
68+
'meter',
69+
'noscript',
70+
'object',
71+
'output',
72+
'picture',
73+
'progress',
74+
'q',
75+
'ruby',
76+
's',
77+
'samp',
78+
'script',
79+
'select',
80+
'slot',
81+
'small',
82+
'span',
83+
'strong',
84+
'sub',
85+
'sup',
86+
'svg',
87+
'template',
88+
'textarea',
89+
'time',
90+
'u',
91+
'tt',
92+
'var',
93+
'video',
94+
'wbr',
95+
]
96+
97+
// replacing the default htmlBlock rule to allow using custom components at
98+
// root level
99+
//
100+
// an array of opening and corresponding closing sequences for html tags,
101+
// last argument defines whether it can terminate a paragraph or not
102+
const HTML_SEQUENCES: [RegExp, RegExp, boolean][] = [
103+
[/^<(script|pre|style)(?=(\s|>|$))/i, /<\/(script|pre|style)>/i, true],
104+
[/^<!--/, /-->/, true],
105+
[/^<\?/, /\?>/, true],
106+
[/^<![A-Z]/, />/, true],
107+
[/^<!\[CDATA\[/, /\]\]>/, true],
108+
109+
// MODIFIED HERE: treat vue reserved tags as block tags
110+
[
111+
new RegExp(`^</?(${vueReservedTags.join('|')})(?=(\\s|/?>|$))`, 'i'),
112+
/^$/,
113+
true,
114+
],
115+
116+
// MODIFIED HERE: treat unknown tags as block tags (custom components),
117+
// excluding known inline tags
118+
[
119+
new RegExp(
120+
`^</?(?!(${inlineTags.join('|')})(?![\\w-]))\\w[\\w-]*[\\s/>]`,
121+
),
122+
/^$/,
123+
true,
124+
],
125+
126+
[
127+
new RegExp(`^</?(${blockNames.join('|')})(?=(\\s|/?>|$))`, 'i'),
128+
/^$/,
129+
true,
130+
],
131+
132+
[
133+
// eslint-disable-next-line no-control-regex
134+
/^(?:<[A-Za-z][A-Za-z0-9\-]*(?:\s+[a-zA-Z_:@][a-zA-Z0-9:._-]*(?:\s*=\s*(?:[^"'=<>`\x00-\x20]+|'[^']*'|"[^"]*"))?)*\s*\/?>|<\/[A-Za-z][A-Za-z0-9\-]*\s*>)/,
135+
/^$/,
136+
true,
137+
],
138+
]
139+
140+
export const componentPlugin = (md: MarkdownIt) => {
141+
md.block.ruler.at('html_block', (state, startLine, endLine, silent): boolean => {
142+
let i, nextLine, lineText
143+
let pos = state.bMarks[startLine] + state.tShift[startLine]
144+
let max = state.eMarks[startLine]
145+
146+
// if it's indented more than 3 spaces, it should be a code block
147+
if (state.sCount[startLine] - state.blkIndent >= 4)
148+
return false
149+
150+
if (!state.md.options.html)
151+
return false
152+
153+
if (state.src.charCodeAt(pos) !== 0x3C /* < */)
154+
return false
155+
156+
lineText = state.src.slice(pos, max)
157+
158+
for (i = 0; i < HTML_SEQUENCES.length; i++) {
159+
if (HTML_SEQUENCES[i][0].test(lineText))
160+
break
161+
}
162+
163+
if (i === HTML_SEQUENCES.length)
164+
return false
165+
166+
if (silent) {
167+
// true if this sequence can be a terminator, false otherwise
168+
return HTML_SEQUENCES[i][2]
169+
}
170+
171+
nextLine = startLine + 1
172+
173+
// if we are here - we detected HTML block. let's roll down till block end
174+
if (!HTML_SEQUENCES[i][1].test(lineText)) {
175+
for (; nextLine < endLine; nextLine++) {
176+
if (state.sCount[nextLine] < state.blkIndent)
177+
break
178+
179+
pos = state.bMarks[nextLine] + state.tShift[nextLine]
180+
max = state.eMarks[nextLine]
181+
lineText = state.src.slice(pos, max)
182+
183+
if (HTML_SEQUENCES[i][1].test(lineText)) {
184+
if (lineText.length !== 0)
185+
nextLine++
186+
break
187+
}
188+
}
189+
}
190+
191+
state.line = nextLine
192+
193+
const token = state.push('html_block', '', 0)
194+
token.map = [startLine, nextLine]
195+
token.content = state.getLines(startLine, nextLine, state.blkIndent, true)
196+
197+
return true
198+
})
199+
}

test/__snapshots__/excerpt.test.ts.snap

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,5 @@ const excerpt = \\"\\\\nThis is an excerpt.\\\\n\\\\n\\"
1818
<script>
1919
export const title = \\"Hey\\"
2020
export const excerpt = \\"\\\\nThis is an excerpt.\\\\n\\\\n\\"
21-
22-
</script>
23-
24-
"
21+
</script>"
2522
`;
Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
// Vitest Snapshot v1
22

3+
exports[`transform > Vue directives 1`] = `
4+
"<template><div class=\\"markdown-body\\">
5+
<button @click=\\"onClick\\"></button>
6+
</div></template>
7+
<script setup lang=\\"ts\\">
8+
const frontmatter = {\\"name\\":\\"My Cool App\\"}
9+
defineExpose({ frontmatter })
10+
function onClick() {
11+
// ...
12+
}
13+
</script>
14+
<script lang=\\"ts\\">
15+
export const name = \\"My Cool App\\"
16+
</script>"
17+
`;
18+
319
exports[`transform > basic 1`] = `
420
"<template><div class=\\"markdown-body\\"><h1>Hello</h1>
521
<ul>
@@ -14,25 +30,19 @@ defineExpose({ frontmatter })
1430
</script>
1531
<script>
1632
export const title = \\"Hey\\"
17-
</script>
18-
19-
"
33+
</script>"
2034
`;
2135
2236
exports[`transform > couldn't expose frontmatter 1`] = `
2337
"<template><div class=\\"markdown-body\\">
2438
</div></template>
2539
<script setup>
2640
const frontmatter = {\\"title\\":\\"Hey\\"}
27-
2841
defineExpose({ test: 'test'})
29-
3042
</script>
3143
<script>
3244
export const title = \\"Hey\\"
33-
</script>
34-
35-
"
45+
</script>"
3646
`;
3747
3848
exports[`transform > escapeCodeTagInterpolation 1`] = `
@@ -43,9 +53,7 @@ exports[`transform > escapeCodeTagInterpolation 1`] = `
4353
<script setup>
4454
const frontmatter = {}
4555
defineExpose({ frontmatter })
46-
</script>
47-
48-
"
56+
</script>"
4957
`;
5058
5159
exports[`transform > exposes frontmatter 1`] = `
@@ -57,9 +65,7 @@ defineExpose({ frontmatter })
5765
</script>
5866
<script>
5967
export const title = \\"Hey\\"
60-
</script>
61-
62-
"
68+
</script>"
6369
`;
6470
6571
exports[`transform > frontmatter interpolation 1`] = `
@@ -72,9 +78,7 @@ defineExpose({ frontmatter })
7278
</script>
7379
<script>
7480
export const name = \\"My Cool App\\"
75-
</script>
76-
77-
"
81+
</script>"
7882
`;
7983
8084
exports[`transform > script setup 1`] = `
@@ -84,12 +88,8 @@ exports[`transform > script setup 1`] = `
8488
<script setup lang=\\"ts\\">
8589
const frontmatter = {}
8690
defineExpose({ frontmatter })
87-
8891
import Foo from './Foo.vue'
89-
90-
</script>
91-
92-
"
92+
</script>"
9393
`;
9494
9595
exports[`transform > style 1`] = `
@@ -100,6 +100,5 @@ exports[`transform > style 1`] = `
100100
const frontmatter = {}
101101
defineExpose({ frontmatter })
102102
</script>
103-
<style>h1 { color: red }</style>
104-
"
103+
<style>h1 { color: red }</style>"
105104
`;

0 commit comments

Comments
 (0)