Skip to content

Commit 10feeec

Browse files
Ariges770farnabaz
andauthored
feat: introduce mdc.componenets.customElements (#444)
Co-authored-by: Farnabaz <farnabaz@gmail.com>
1 parent dad4ebf commit 10feeec

File tree

9 files changed

+128
-10
lines changed

9 files changed

+128
-10
lines changed

docs/0.index.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,76 @@ export default defineNuxtConfig({
247247
prose: false, // Add predefined map to render Prose Components instead of HTML tags, like p, ul, code
248248
map: {
249249
// This map will be used in `<MDCRenderer>` to control rendered components
250-
}
250+
},
251+
customElements: [], // Custom element tags to ignore at MDC runtime (ex: ['mjx-container', 'my-element'])
251252
}
252253
}
253254
})
254255
```
255256

256257
Checkout [`ModuleOptions` types↗︎](https://github.com/nuxt-content/mdc/blob/main/src/module.ts).
257258

259+
## Using Custom Elements
260+
There are two places where “custom elements” matter:
261+
262+
- **Vue template compiler (compile-time)**: controls whether Vue treats `<my-element>` as a component or a custom element.
263+
- **MDC renderer (runtime)**: controls whether MDC tries to resolve a tag as a Vue component when rendering markdown.
264+
265+
### Vue (compile-time)
266+
When using custom elements in Vue templates, configure Vue to recognize them by adding `vue.compilerOptions.isCustomElement` in your `nuxt.config.ts`.
267+
268+
```ts [nuxt.config.ts]
269+
export default defineNuxtConfig({
270+
vue: {
271+
compilerOptions: {
272+
isCustomElement: (tag) => tag.startsWith('mjx')
273+
}
274+
}
275+
})
276+
```
277+
278+
### MDC (runtime)
279+
MDC does not use Vue’s template compiler options when rendering markdown. To tell MDC which tags should be treated as custom elements (and therefore **not** resolved as Vue components), add them to `mdc.components.customElements`.
280+
281+
```ts [nuxt.config.ts]
282+
export default defineNuxtConfig({
283+
mdc: {
284+
components: {
285+
customElements: ['mjx-container']
286+
}
287+
}
288+
})
289+
```
290+
291+
When `mdc.components.customElements` is provided and `vue.compilerOptions.isCustomElement` is not already set, MDC will also configure Vue to treat those same tags as custom elements.
292+
293+
Now you can use custom elements as below.
294+
295+
```html [custom-element.vue]
296+
<script setup>
297+
const mdc = `
298+
# Custom elements can now be used
299+
300+
::mjx-container
301+
This is rendered as a custom element
302+
::
303+
`
304+
</script>
305+
306+
<style>
307+
my-element {
308+
color: red;
309+
}
310+
</style>
311+
312+
<template>
313+
<div>
314+
<MDC :value="mdc" />
315+
<my-element>Another custom element</my-element>
316+
</div>
317+
</template>
318+
```
319+
258320
## Contributing
259321

260322
You can contribute to this module online with CodeSandbox:

src/module.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,29 @@ export default defineNuxtModule<ModuleOptions>({
5353
components: {
5454
prose: true,
5555
map: {},
56+
customElements: [],
5657
},
5758
},
5859
async setup(options, nuxt) {
5960
resolveOptions(options)
6061

6162
const resolver = createResolver(import.meta.url)
6263

63-
nuxt.options.runtimeConfig.public.mdc = defu(nuxt.options.runtimeConfig.public.mdc, {
64+
const mdc = nuxt.options.runtimeConfig.public.mdc = defu(nuxt.options.runtimeConfig.public.mdc, {
6465
components: {
6566
prose: options.components!.prose!,
6667
map: options.components!.map!,
68+
customElements: options.components?.customElements || [],
6769
},
6870
headings: options.headings!,
6971
highlight: options.highlight!,
7072
})
7173

74+
if (mdc.components.customElements.length > 0 && !nuxt.options.vue.compilerOptions?.isCustomElement) {
75+
nuxt.options.vue.compilerOptions ||= {}
76+
nuxt.options.vue.compilerOptions.isCustomElement = (tag: string) => mdc.components.customElements.includes(tag)
77+
}
78+
7279
nuxt.options.build.transpile ||= []
7380
nuxt.options.build.transpile.push('yaml')
7481

@@ -273,6 +280,7 @@ declare module '@nuxt/schema' {
273280
components: {
274281
prose: boolean
275282
map: Record<string, string>
283+
customElements: string[]
276284
}
277285
headings: ModuleOptions['headings']
278286
highlight: ModuleOptions['highlight']
@@ -286,6 +294,7 @@ declare module '@nuxt/schema' {
286294
components: {
287295
prose: boolean
288296
map: Record<string, string>
297+
customElements: string[]
289298
}
290299
}
291300
headings: ModuleOptions['headings']

src/runtime/components/MDCRenderer.vue

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const rxOn = /^@|^v-on:/
2020
const rxBind = /^:|^v-bind:/
2121
const rxModel = /^v-model/
2222
const nativeInputs = ['select', 'textarea', 'input']
23-
const specialParentTags = ['math', 'svg']
23+
const specialParentTags = new Set(['math', 'svg'])
24+
const customElements = new Set<string>()
2425
2526
const proseComponentMap = Object.fromEntries(['p', 'a', 'blockquote', 'code', 'pre', 'code', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'ul', 'ol', 'li', 'strong', 'table', 'thead', 'tbody', 'td', 'th', 'tr', 'script'].map(t => [t, `prose-${t}`]))
2627
@@ -89,6 +90,12 @@ export default defineComponent({
8990
const route = $nuxt?.$route || $nuxt?._route
9091
const { mdc } = $nuxt?.$config?.public || {}
9192
93+
// Custom elements
94+
const customElementTags = mdc?.components?.customElements || mdc?.components?.custom
95+
if (customElementTags) {
96+
customElementTags.forEach((tag: string) => customElements.add(tag))
97+
}
98+
9299
const tags = computed(() => ({
93100
...(mdc?.components?.prose && props.prose !== false ? proseComponentMap : {}),
94101
...(mdc?.components?.map || {}),
@@ -99,7 +106,7 @@ export default defineComponent({
99106
const contentKey = computed(() => {
100107
const components = (props.body?.children || [])
101108
.map(n => (n as any).tag || n.type)
102-
.filter(t => !htmlTags.includes(t))
109+
.filter(t => !ignoreTag(t))
103110
104111
return Array.from(new Set(components)).sort().join('.')
105112
})
@@ -400,7 +407,7 @@ function propsToDataRxBind(key: string, value: any, data: any, documentMeta: MDC
400407
*/
401408
const resolveComponentInstance = (component: any) => {
402409
if (typeof component === 'string') {
403-
if (htmlTags.includes(component)) {
410+
if (ignoreTag(component)) {
404411
return component
405412
}
406413
@@ -463,7 +470,7 @@ function isTemplate(node: MDCNode) {
463470
* Check if tag is a special tag that should not be resolved to a component
464471
*/
465472
function isUnresolvableTag(tag: unknown) {
466-
return specialParentTags.includes(tag as string)
473+
return specialParentTags.has(tag as string)
467474
}
468475
469476
/**
@@ -514,7 +521,7 @@ async function resolveContentComponents(body: MDCRoot, meta: Record<string, any>
514521
515522
const components: string[] = []
516523
517-
if (node.type !== 'root' && !htmlTags.includes(renderTag as any)) {
524+
if (node.type !== 'root' && !ignoreTag(renderTag as any)) {
518525
components.push(renderTag)
519526
}
520527
for (const child of (node.children || [])) {
@@ -533,4 +540,12 @@ function findMappedTag(node: MDCElement, tags: Record<string, string>) {
533540
534541
return tags[tag] || tags[pascalCase(tag)] || tags[kebabCase(node.tag)] || tag
535542
}
543+
544+
function ignoreTag(tag: string) {
545+
// Checks if input tag is an html tag or
546+
const isCustomEl = (typeof tag === 'string')
547+
? customElements.has(tag)
548+
: false
549+
return isCustomEl || htmlTags.has(tag)
550+
}
536551
</script>

src/runtime/parser/handlers/paragraph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default function paragraph(state: State, node: Paragraph) {
99
if (node.children && node.children[0] && node.children[0].type === 'html') {
1010
const tagName = kebabCase(getTagName(node.children[0].value) || 'div')
1111
// Unwrap if component
12-
if (!htmlTags.includes(tagName)) {
12+
if (!htmlTags.has(tagName)) {
1313
return state.all(node)
1414
}
1515
}

src/runtime/parser/utils/html-tags-list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Ref https://github.com/sindresorhus/html-tags/blob/v3.2.0/html-tags.json
2-
export default [
2+
export default new Set([
33
'a',
44
'abbr',
55
'address',
@@ -117,4 +117,4 @@ export default [
117117
'var',
118118
'video',
119119
'wbr',
120-
]
120+
])

src/types/module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,9 @@ export interface ModuleOptions {
8787
components?: {
8888
prose?: boolean
8989
map?: Record<string, string>
90+
/**
91+
* Custom element tags to ignore at MDC runtime (ex: ['mjx-container', 'my-element'])
92+
*/
93+
customElements?: string[]
9094
}
9195
}

test/basic.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,12 @@ describe('ssr', async () => {
2121
expect(html).toContain('[Global Paragraph]')
2222
expect(html).toContain('Sample paragraph')
2323
})
24+
25+
it('respects mdc.components.customElements at MDC runtime', async () => {
26+
const res = await $fetch('/api/is-custom-element', { query: { tag: 'x-foo' } }) as any
27+
expect(res).toEqual({ tag: 'x-foo', isCustomElement: true })
28+
29+
const res2 = await $fetch('/api/is-custom-element', { query: { tag: 'div' } }) as any
30+
expect(res2).toEqual({ tag: 'div', isCustomElement: false })
31+
})
2432
})

test/fixtures/basic/nuxt.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ export default defineNuxtConfig({
44
modules: [
55
NuxtMDC,
66
],
7+
mdc: {
8+
components: {
9+
customElements: ['x-foo'],
10+
},
11+
},
712
future: {
813
compatibilityVersion: 4,
914
},
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { getQuery } from 'h3'
2+
import { useRuntimeConfig } from '#imports'
3+
4+
export default defineEventHandler((event) => {
5+
const query = getQuery(event)
6+
const tag = String(query.tag || '')
7+
8+
const config = useRuntimeConfig()
9+
const customElements = config.public?.mdc?.components?.customElements || []
10+
11+
return {
12+
tag,
13+
isCustomElement: customElements.includes(tag),
14+
}
15+
})

0 commit comments

Comments
 (0)