From 6946ede384a903194f0154d01a82f0365fe86e44 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 16 Jun 2022 13:03:09 +0200 Subject: [PATCH 1/4] feat: variable binding --- src/runtime/components/MarkdownRenderer.ts | 60 +++++++++++----- .../remark-mdc/from-markdown.ts | 18 ++++- .../remark-mdc/micromark-extension/index.ts | 3 +- .../micromark-extension/tokenize-binding.ts | 71 +++++++++++++++++++ 4 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 src/runtime/markdown-parser/remark-mdc/micromark-extension/tokenize-binding.ts diff --git a/src/runtime/components/MarkdownRenderer.ts b/src/runtime/components/MarkdownRenderer.ts index aa268ab8d..74bd45f1f 100644 --- a/src/runtime/components/MarkdownRenderer.ts +++ b/src/runtime/components/MarkdownRenderer.ts @@ -5,7 +5,7 @@ import { find, html } from 'property-information' // eslint-disable-next-line import/no-named-as-default import htmlTags from 'html-tags' import type { VNode, ConcreteComponent } from 'vue' -import { useRuntimeConfig } from '#app' +import { useRuntimeConfig, useRoute } from '#app' import type { MarkdownNode, ParsedContentMeta } from '../types' type CreateElement = typeof h @@ -106,11 +106,7 @@ export default defineComponent({ /** * Render a markdown node */ -function renderNode (node: MarkdownNode, h: CreateElement, documentMeta: ParsedContentMeta): ContentVNode { - const originalTag = node.tag - // `_ignoreMap` is an special prop to disables tag-mapper - const renderTag: string = (typeof node.props?.__ignoreMap === 'undefined' && documentMeta.tags[node.tag]) || node.tag - +function renderNode (node: MarkdownNode, h: CreateElement, documentMeta: ParsedContentMeta, parentScope: any = {}): ContentVNode { /** * Render Text node */ @@ -118,46 +114,78 @@ function renderNode (node: MarkdownNode, h: CreateElement, documentMeta: ParsedC return h(Text, node.value) } + const originalTag = node.tag! + // `_ignoreMap` is an special prop to disables tag-mapper + const renderTag: string = (typeof node.props?.__ignoreMap === 'undefined' && documentMeta.tags[originalTag]) || originalTag + + if (node.tag === 'binding') { + return renderBinding(node, h, documentMeta, parentScope) + } + const component = resolveVueComponent(renderTag) if (typeof component === 'object') { component.tag = originalTag } + const props = propsToData(node, documentMeta) return h( component as any, - propsToData(node, documentMeta), - renderSlots(node, h, documentMeta) + props, + renderSlots(node, h, documentMeta, { ...parentScope, ...props }) ) } +function renderBinding (node: MarkdownNode, h: CreateElement, documentMeta: ParsedContentMeta, parentScope: any = {}): ContentVNode { + const data = { + ...parentScope, + $route: () => useRoute(), + $document: documentMeta, + $doc: documentMeta + } + const splitter = /\.|\[(\d+)\]/ + const keys = node.props?.value.trim().split(splitter).filter(Boolean) + const value = keys.reduce((data, key) => { + if (key in data) { + if (typeof data[key] === 'function') { + return data[key]() + } else { + return data[key] + } + } + return {} + }, data) + + return h(Text, value) +} + /** * Create slots from `node` template children. */ -function renderSlots (node: MarkdownNode, h: CreateElement, documentMeta: ParsedContentMeta) { +function renderSlots (node: MarkdownNode, h: CreateElement, documentMeta: ParsedContentMeta, parentProps: any): ContentVNode[] { const children: MarkdownNode[] = node.children || [] const slots: Record> = children.reduce((data, node) => { if (!isTemplate(node)) { - data[DEFAULT_SLOT].push(renderNode(node, h, documentMeta)) + data[DEFAULT_SLOT].push(renderNode(node, h, documentMeta, parentProps)) return data } if (isDefaultTemplate(node)) { - data[DEFAULT_SLOT].push(...node.children.map(child => renderNode(child, h, documentMeta))) + data[DEFAULT_SLOT].push(...(node.children || []).map(child => renderNode(child, h, documentMeta, parentProps))) return data } const slotName = getSlotName(node) - data[slotName] = node.children.map(child => renderNode(child, h, documentMeta)) + data[slotName] = (node.children || []).map(child => renderNode(child, h, documentMeta, parentProps)) return data }, { - [DEFAULT_SLOT]: [] + [DEFAULT_SLOT]: [] as any[] }) - return Object.fromEntries( - Object.entries(slots).map(([name, vDom]) => ([name, createSlotFunction(vDom)])) - ) + const slotEntries = Object.entries(slots).map(([name, vDom]) => ([name, createSlotFunction(vDom)])) + + return Object.fromEntries(slotEntries) } /** diff --git a/src/runtime/markdown-parser/remark-mdc/from-markdown.ts b/src/runtime/markdown-parser/remark-mdc/from-markdown.ts index 86ed884ac..89c111401 100644 --- a/src/runtime/markdown-parser/remark-mdc/from-markdown.ts +++ b/src/runtime/markdown-parser/remark-mdc/from-markdown.ts @@ -15,7 +15,7 @@ const enter = { componentContainerDataSection: enterContainerDataSection, componentContainerAttributes: enterAttributes, componentContainerLabel: enterContainerLabel, - + bindingContent: enterBindingContent, componentLeaf: enterLeaf, componentLeafAttributes: enterAttributes, @@ -24,6 +24,7 @@ const enter = { componentTextAttributes: enterAttributes } const exit = { + bindingContent: exitBindingContent, componentContainerSectionTitle: exitContainerSectionTitle, listUnordered: conditionalExit, listOrdered: conditionalExit, @@ -63,6 +64,21 @@ const exit = { componentTextName: exitName } +// Bindings +function enterBindingContent (token) { + this.enter({ + type: 'textComponent', + name: 'binding', + attributes: { + value: this.sliceSerialize(token).trim() + } + }, token) +} + +function exitBindingContent (token) { + this.exit(token) +} + function enterContainer (token: Token) { enterToken.call(this, 'containerComponent', token) } diff --git a/src/runtime/markdown-parser/remark-mdc/micromark-extension/index.ts b/src/runtime/markdown-parser/remark-mdc/micromark-extension/index.ts index f51f07950..881b19211 100644 --- a/src/runtime/markdown-parser/remark-mdc/micromark-extension/index.ts +++ b/src/runtime/markdown-parser/remark-mdc/micromark-extension/index.ts @@ -6,6 +6,7 @@ import tokenizeSpan from './tokenize-span' import tokenizeAttribute from './tokenize-attribute' +import tokenizeBinding from './tokenize-binding' import tokenizeInline from './tokenize-inline' import tokenizeContainer from './tokenize-container' import tokenizeContainerIndented from './tokenize-container-indented' @@ -16,7 +17,7 @@ export default function micromarkComponentsExtension () { text: { [Codes.colon]: tokenizeInline, [Codes.openingSquareBracket]: [tokenizeSpan], - [Codes.openingCurlyBracket]: tokenizeAttribute + [Codes.openingCurlyBracket]: [tokenizeBinding, tokenizeAttribute] }, flow: { [Codes.colon]: [tokenizeContainer] diff --git a/src/runtime/markdown-parser/remark-mdc/micromark-extension/tokenize-binding.ts b/src/runtime/markdown-parser/remark-mdc/micromark-extension/tokenize-binding.ts new file mode 100644 index 000000000..4aab7cd4e --- /dev/null +++ b/src/runtime/markdown-parser/remark-mdc/micromark-extension/tokenize-binding.ts @@ -0,0 +1,71 @@ +import type { Effects, State, Code, TokenizeContext } from 'micromark-util-types' +import { Codes } from './constants' + +function attempClose (this: TokenizeContext, effects: Effects, ok: State, nok: State) { + return start + + function start (code: Code) { + if (code !== Codes.closingCurlyBracket) { + return nok(code) + } + effects.exit('bindingContent') + effects.enter('bindingFence') + effects.consume(code) + return secondBracket + } + + function secondBracket (code: Code) { + if (code !== Codes.closingCurlyBracket) { + return nok(code) + } + effects.consume(code) + effects.exit('bindingFence') + + return ok + } +} + +function tokenize (this: TokenizeContext, effects: Effects, ok: State, nok: State) { + return start + + function start (code: Code): void | State { + if (code !== Codes.openingCurlyBracket) { + throw new Error('expected `{`') + } + + effects.enter('bindingFence') + effects.consume(code) + return secondBracket + } + + function secondBracket (code: Code): void | State { + if (code !== Codes.openingCurlyBracket) { + return nok(code) + } + effects.consume(code) + effects.exit('bindingFence') + effects.enter('bindingContent') + + return content + } + + function content (code: Code): void | State { + if (code === Codes.closingCurlyBracket) { + return effects.attempt({ tokenize: attempClose, partial: true }, close, (code) => { + effects.consume(code) + return content + })(code) + } + + effects.consume(code) + return content + } + + function close (code: Code): void | State { + return ok(code) + } +} + +export default { + tokenize +} From e434a1a7a7b882d23e844faa23f38a065e1516aa Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 16 Jun 2022 13:53:40 +0200 Subject: [PATCH 2/4] test: add test --- test/basic.test.ts | 2 ++ test/features/highlighter.ts | 1 - test/features/renderer-markdown.ts | 56 +++++++++++++++++++++++++++++ test/fixtures/basic/pages/parse.vue | 19 ++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 test/features/renderer-markdown.ts create mode 100644 test/fixtures/basic/pages/parse.vue diff --git a/test/basic.test.ts b/test/basic.test.ts index f4910a7c5..6c1611fab 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -15,6 +15,7 @@ import { testParserHooks } from './features/parser-hooks' import { testModuleOption } from './features/module-options' import { testContentQuery } from './features/content-query' import { testHighlighter } from './features/highlighter' +import { testMarkdownRenderer } from './features/renderer-markdown' const spyConsoleWarn = vi.spyOn(global.console, 'warn') @@ -119,6 +120,7 @@ describe('fixtures:basic', async () => { testNavigation() testMarkdownParser() + testMarkdownRenderer() testMarkdownParserExcerpt() diff --git a/test/features/highlighter.ts b/test/features/highlighter.ts index 9755b136c..d986961cc 100644 --- a/test/features/highlighter.ts +++ b/test/features/highlighter.ts @@ -24,7 +24,6 @@ export const testHighlighter = () => { const styleElement = parsed.body.children.pop() expect(styleElement.tag).toBe('style') const style = styleElement.children[0].value - console.log(style) const code = parsed.body.children[0].children[0].children[0].children[0].children diff --git a/test/features/renderer-markdown.ts b/test/features/renderer-markdown.ts new file mode 100644 index 000000000..c91882e49 --- /dev/null +++ b/test/features/renderer-markdown.ts @@ -0,0 +1,56 @@ +import { describe, test, expect, assert } from 'vitest' +import { $fetch } from '@nuxt/test-utils' + +const content = `--- +title: MDC +cover: https://nuxtjs.org/design-kit/colored-logo.svg +--- +:img{:src="cover"} + +# {{ $doc.title }} + +MDC stands for _**M**ark**D**own **C**omponents_. + +This syntax supercharges regular Markdown to write documents interacting deeply with any Vue component from your \`components/content/\` directory or provided by a module. + +## Next steps +- [Install Nuxt Content](/get-started) +- [Explore the MDC syntax](/guide/writing/mdc) + + +You are visiting document: {{ $doc._id }}. +Current route is: {{ $route.path }} + + +::alert +--- +type: success +--- +This is an alert for {{ type }} +:: + +::alert{type="danger"} +This is an alert for {{ type }} +:: + +` + +export const testMarkdownRenderer = () => { + describe('renderer:markdown', () => { + test('bindings', async () => { + const rendered = await $fetch('/parse', { + params: { + content + } + }) + + expect(rendered).toContain('') + + expect(rendered).toContain('

MDC

') + expect(rendered).toContain('You are visiting document: content:index.md.') + expect(rendered).toContain('Current route is: /parse') + expect(rendered).toContain('This is an alert for success') + expect(rendered).toContain('This is an alert for danger') + }) + }) +} diff --git a/test/fixtures/basic/pages/parse.vue b/test/fixtures/basic/pages/parse.vue new file mode 100644 index 000000000..512012e7b --- /dev/null +++ b/test/fixtures/basic/pages/parse.vue @@ -0,0 +1,19 @@ + + + From b96f3dc0d5d8dfab9940ea200e193ac693b76b61 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 16 Jun 2022 13:54:52 +0200 Subject: [PATCH 3/4] docs: update playground --- docs/components/content/Playground.vue | 8 +++++++- playground/pages/playground.vue | 28 ++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/components/content/Playground.vue b/docs/components/content/Playground.vue index 91743b513..c62c9d932 100644 --- a/docs/components/content/Playground.vue +++ b/docs/components/content/Playground.vue @@ -1,8 +1,13 @@