vitepress icon indicating copy to clipboard operation
vitepress copied to clipboard

Import custom component in MarkdownIt plugin

Open turbosheep44 opened this issue 2 years ago • 2 comments

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?

turbosheep44 avatar Sep 16 '22 15:09 turbosheep44

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

emersonbottero avatar Sep 17 '22 16:09 emersonbottero

@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.

turbosheep44 avatar Sep 20 '22 07:09 turbosheep44

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>'
}

brc-dd avatar Sep 26 '22 17:09 brc-dd

Closing due to inactivity.

kiaking avatar Oct 31 '22 10:10 kiaking