vuejs / vitepress

Vite & Vue powered static site generator.
https://vitepress.dev
MIT License
12.93k stars 2.09k forks source link

Import custom component in MarkdownIt plugin #1349

Closed turbosheep44 closed 1 year ago

turbosheep44 commented 2 years ago

I have a VitePress plugin which allows me to render a component next to the code for that component. This is very useful for showcasing how to use components in a library.

From the Markdown input:

# MyButton
An example of the button component!
:@/ButtonSample.vue:

The desired output is the following:

image

Where ButtonSample.vue is a file containing the code for the 2 snippets and renders to show the two buttons at the bottom.

As you can see, this plugin was working with Vitepress 1.0.0-alpha.10 but after upgrading to 1.0.0-alpha.11, the snippets are rendered but the component is missing.

Here is the relevant snippet from the plugin:

const TEMPLATE_START = /^<template>$/
const TEMPLATE_END = /^<\/template>$/
const SCRIPT_START = /^<script setup lang="ts">$/
const SCRIPT_END = /^<\/script>$/

const COMPONENT_SNIPPET = (file: string) => `
<script setup lang="ts">
import DemoComponent from "${file}";
</script>
<div class="vp-raw">
  <demo-component/>
</div>
`

// when creating a demo section, `@/` will be replaced with `srcDir`
export const demoPlugin = (md: MarkdownIt, srcDir: string) => {
  const parser: RuleBlock = (state, startLine, _endLine, _silent) => {
    /*... some code to extract the file name from the markdown ... */
    token.attrSet('src', resolve(filename))
    return true
  }

  const renderer: RenderRule = (...args) => {
    /* ... some code to get `src` and check that the file exists ... */

    const file = readFileSync(src, 'utf8')

    // script
    token.info = `ts`
    token.content = findSection(file, SCRIPT_START, SCRIPT_END)
    const script = md.renderer.rules.fence!(...args)

    // template
    token.info = `vue-html`
    token.content = findSection(file, TEMPLATE_START, TEMPLATE_END)
    const template = md.renderer.rules.fence!(...args)

    // component
    const demo = md.render(COMPONENT_SNIPPET(src))

    return script + template + demo
  }

  md.renderer.rules.demo = renderer
  md.block.ruler.before(md.block.ruler.getRules('')[0].name, 'demo', parser)
}

// returns everything between the first match of `start` and the subsequent first match for `end`
function findSection(content: string, start: RegExp, end: RegExp): string {
 //...
}

So the markdown rendered used to take the line const demo = md.render(COMPONENT_SNIPPET(src)) and automagically put the import statement where it needs to be and happily render the component. However, since updating to 1.0.0-alpha.11 this no longer works. In fact if I manually write out the import statement in the markdown file, everything works - so something is wrong with the way my plugin is handling the import.

I looked into the way plugin-sfc works and it looks like there was a change from 0.10.0 to 0.11.0 in the way script SFC blocks are handled.

I tried to copy the way the changed code works in my plugin, but I have not managed to get it working:

    const block = `<script setup lang="ts"> import DemoComponent from "${src}"; </script>`
    sfcBlocks.scriptSetup = block
    sfcBlocks.scripts.push(block)

    token.content = `<div class="vp-raw"> <demo-component/> </div>`
    const demo = md.renderer.rules.html_block!(...args)

Does anyone know how this can be done? How can my plugin tell VitePress that it needs to import the demo component for this page?

emersonbottero commented 2 years ago

I tried to do that.. (I think I managed.. but since I needed to add it once I change it again.. to be imported only once in another place...)

The step I did was 1 - Test adding the component in the md file itself by hand(testing it if is working this way) 2 - Remove the script section manually and in the plugin only for md files try inserting the script portion exactly as worked in your test (mine was one single line) 3 - If this worked you are halfway there.. Now you need to check if there is already an script tag in the file and change that , I always use script setup but we can't have 2 of those.

I don't think you should register the component in the markdown plugin. you should use an rollup hook for that.

I do that in here but it is always the same component and I have to register it globally

turbosheep44 commented 2 years ago

@emersonbottero Yes, as I mentioned, manually adding the import statement in the Markdown file does work. When I try to add the import statement from the plugin it doesn't work.

In the code you linked, you are just registering one component globally, where as I am trying to register arbitrary components for different pages. If I knew all the components ahead of time I would just register them globally as shown in the docs.

brc-dd commented 2 years ago

You seem to be just pushing a string to scripts. You need to push a SfcBlock instead. It looks something like this:

 {
      content: `<script setup lang="ts"> import DemoComponent from './DemoComponent.vue'; </script>`,
      tagOpen: '<script setup lang="ts">',
      type: 'script',
      contentStripped: " import DemoComponent from './DemoComponent.vue'; ",
      tagClose: '</script>'
}
kiaking commented 1 year ago

Closing due to inactivity.