vuejs / vitepress

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

Proper processing of Vue Component slot content #135

Closed yankeeinlondon closed 1 year ago

yankeeinlondon commented 3 years ago

Describe the bug/feature

In both Vitepress and Vuepress the way in which the Markdown-it rule htmlBlock is implemented is highly subpar. This is particularly true because the primary use-case for both is documentation but currently adding in a VueJS component which takes advantage of slot content tends to break rather quickly. This is because -- by default web-components are considered inline components by Markdown (as per the spec). When Markdown encounters inline components it treats the interior scope of the web-component as Markdown content and therefore tends to wreck havoc on slot content.

Looking at the src/node/markdown/plugins/component.ts file you can start to see the problem when looking at the HTML_SEQUENCES symbol (some lines removed for brevity):

// An array of opening and corresponding closing sequences for html tags,
// last argument defines whether it can terminate a paragraph or not
const HTML_SEQUENCES: [RegExp, RegExp, boolean][] = [
  // PascalCase Components
  [/^<[A-Z]/, />/, true],
  // custom elements with hyphens
  [/^<\w+\-/, />/, true],
]

The intent here is to mark the opening and closing of scope for a component but as you can see from the RegEx it is only looking for inline components not block components!

For example, the component p-table has several named slots which help it to render the way the user intends:

<p-table>

  <template #name="{ row }">
    {{ row.name }}
  </template>

  <template #age="{ row }">
    {{ row.age }}
  </template>

</p-table>

Put this into the template section of a Vue CLI app and it works just fine. Put it into Vuepress or Vitepress and watch the cloud of smoke start to rise as it fumbles around; particularly failing on the # symbols used as shorthand for named slots. This is particularly troublesome for a documentation site that is trying to document a set of VueJS components! There should be a way to ensure that this VueJS component's interior scope is left untouched by Markdown-it ... at least when that's what you want.

There is a second use-case that I would expect Vitepress to just handle and that is kind of what it's doing today but in a hit and miss fashion currently. Let's say that I just have a default slot in my component and therefore I want my component to be seen by the Markdown engine as being an "inline" component and process the interior as markdown. Great, then the following should work:

<super-sexy>
# some heading
## some sub heading
</super-sexy>

This would just provide some HTML wrapper elements that would in this case make this very dull markdown sexy.

Finally there is a third use-case that really should be considered required too ... this is the hybrid but it will allow for all sorts of simple but useful documentation components. The use-case presents as a component that exposes named slots -- and therefore is seen by Markdown as a block component -- but these named slots are processed by markdown. An obvious example might be a <two-columns> component which provides a left and right slot and allows authors to more readily leverage their horizontal space.

Imagine the following:

<two-columns>

  <template #left>
   I'm a lumberjack and I'm ok
  </template>
  <template #right>
  I sleep all night and I work all day
  </template>

</two-columns>

This will fail miserably today but it could be grand!

Proposed Solution

Rework the src/node/markdown/plugins/components.ts file in Vitepress (same change can be applied to Vuepress) to:

This solution so far could pretty easily support the first two use cases which is a big step forward. The third use-case would certainly be nice but would need a bit more thought. I could imagine something as simple as adding the md prop to the parent component being used as a means to pass in the Markdownit object into the top-level component so it could chain the rendering into the various named slots:

```html
<my-crazy-idea md>
  <template #left>
    There I was, _there I was_, 
   <template #right>
     I sleep all night and I work all day
   </template>
</my-crazy-idea>
```

In all likelihood this last option should be done as an optional second step.

yankeeinlondon commented 3 years ago

There is one limitation that will be hard to get around with a RegEx approach -- though this is a problem today too -- and that's in the rare case that you have VueJS component recursion. For instance you have a component which has itself in it's interior scope. This is pretty rare but it starts to make RegEx look like the wrong tool and instead require something like AST.

kiaking commented 3 years ago

Thanks for the report! Sorry I think I'm still not getting the issue here. If I paste in:

```vue
<p-table>

  <template #name="{ row }">
    {{ row.name }}
  </template>

  <template #age="{ row }">
    {{ row.age }}
  </template>

</p-table>
```

To the markdown file, it looks like working fine. What is the problem here?

Screen Shot 2020-11-19 at 18 01 42
yankeeinlondon commented 3 years ago

Sorry i must have done a really poor job describing things. You're just wrapping code into a code fence! I am talking about USING CODE.

Imagine the following markdown:


\`\`\`html
<p-table class="rounded-lg overflow-hidden body-text-blue-800" 
    :rows="[{ id: 1, name: 'Joe', age: 23 }, {id: 2, name: 'Sandy', age: 33}]" 
    :columns="[{id: 'name', name: 'Name'}, {id: 'age', name: 'Age'}]"  >

  <template #name="{ row }">
    {{ row.name }}
  </template>

  <template #age="{ row }">
    {{ row.age }}
  </template>

</p-table>
\`\`\`

<p-table class="rounded-lg overflow-hidden body-text-blue-800" 
    :rows="[{ id: 1, name: 'Joe', age: 23 }, {id: 2, name: 'Sandy', age: 33}]" 
    :columns="[{id: 'name', name: 'Name'}, {id: 'age', name: 'Age'}]"  >

  <template #name="{ row }">
    {{ row.name }}
  </template>

  <template #age="{ row }">
    {{ row.age }}
  </template>

</p-table>

the first block is in a code fence and is demonstrating to the user what you can do with the component, the lower part is showing what that code actually does.

yankeeinlondon commented 3 years ago

image

yankeeinlondon commented 3 years ago

There is zero problem with Vite/Vuepress presenting code blocks but you'll find actually trying to run the component will fail for almost any slot content (especially if it's beyond the default slot).

yankeeinlondon commented 3 years ago

I have already fixed this by hacking Vitepress's code to give me the markdown config option (as I understand there will be an updated release of Vitepress soon which fixes this formally) and this allows me to redefine the markdown-it rule externally to Vitepress but this really is a glaring issue for a solution like Vite/Vuepress and I feel it is better addressed with a PR.

yankeeinlondon commented 3 years ago

The power of Vite/Vuepress has been quite limited by this restriction since inception but it could quite quickly become a Storybook killer for Vue users but this kind of functionality is critical. I have seen people posting questions about this for two years WRT to Vuepress but never had the time to dig into it. I now fully understand it and it is pretty easily addressed for the 2 of the 3 use cases I mention.

yankeeinlondon commented 3 years ago

@kiaking can you let me know when the next Vitepress release will be completed? as discussed I have already done this locally in a repo but it's fragile to yarn update and would like to be able to rely on the Markdown extension foundation so I can focus on this issue (which I need at the very least via the extension mechanism but ideally ends up as a PR). Alternatively if this Vitepress fix is not actually done yet, i can just send you a PR if you'll accept it. I need this done today so I'll have to fork soon if it's not actually coming.

it appears that @master is not building right now due to a type conflict between Vite and Vitepress ... is there a different branch I can build locally for now? Nevermind, i fixed this typing issue locally.

yankeeinlondon commented 3 years ago

@kiaking I have solved 2 of the 3 use-cases now in what I feel is a graceful solution. I will create a short video to describe it. In the final use case, I will have to dig into the markdown-it a bit more to understand what my target should be but the "tools" are now in place to achieve it once I understand what markdown-it needs a bit better.

petedavisdev commented 3 years ago

As I mentioned in a reply to an earlier issue, slots are usable in markdown, but you have to leave empty lines at the start and end of your slotted content and it is also true that you cannot use the # shorthand for named slots (I have a PR open to add this to the VuePress docs).

@ksnyde if you can get it to "just work" without needing to know about these gotchas that would be awesome!

yankeeinlondon commented 3 years ago

i should be able to this plus a few more fixes which should be helpful. i will create a plugin first and we can talk about whether we should add into core once that's done.

kiaking commented 3 years ago

Nice, looking forward to the plugin! It should be really helpful yes.

BWalti commented 2 years ago

Maybe this is off topic, if so I'm sorry! I just assume a "Layout" is also some kind of "Component"..

I tried to "reference" a named slot (sample) in Markdown using a custom Layout:

Layout:

<template>
  <Content />
  <hr />
  <slot name="sample" />
</template>

in Markdown I tried:

# Hello

<template slot="sample">
World!
</template>

or:

# Hello

::: slot sample

World!

:::

Result

But both seem not to result in what I'd expect (slot content rendered below horizontal ruler). Instead in the first case using "template" it's written to the DOM (above hr) but not displayed in the browser.

In the latter cases it just gets rendered to the DOM at the end of the "Content" place (so no slot usage either).

Is this the intended behavior? What am I missing? I scanned the VitePress documentation, there the documentation about Slots was pointing to the VuePress documentation, which ought to be this ":::" syntax thingy..

Is it not possible to reference a slot from within a MarkDown? Do I have to write some "vue page" using the corresponding slots in a "Layout"? I'd be very happy to enhance the documentation as soon as I understand how this magic would work :)

yankeeinlondon commented 2 years ago

The first example is close to being right, you should try:

<template v-slot:sample>
World!
</template>

This is just per the docs on named slots.

BWalti commented 2 years ago

Ah yes, as per the vue documentation? right, I believe I also tried this but forgot to re-do and capture results for my above comment. I tried it right now again and upon accessing the application, I receive following on the console:

08:00:40 [vite] Internal server error: Codegen node is missing for element/if/for node. Apply appropriate transforms first.   
  Plugin: vite:vue
  File: [...]/docs/index.md

I'm unsure on how to debug this error, as the error message is not helpful for me. I mean I have basically no idea, what I'm doing anyways :)

I am trying to use "windicss" in combination with vitepress according to https://github.com/seonglae/windipress maybe something is wrongly configured? does vite.config.ts and windi.config.ts really belong into the "docs" folder?

aboutsimon commented 2 years ago

Could you add to the documentation, that the named slots shorthand is not supported? At least until a more robust solution is developed?

This just cost me quite a bit of time, due to confusing error messages.. ;)

kiaking commented 1 year ago

OK so if you wrap slot item with element, it works.

<!-- WORKS -->
<MyComponent>
  <p>Hello</p>
</MyComponent>

But not if you don't have element wrapping.

<!-- NOPE -->
<MyComponent>
  Hello
</MyComponent>

Seems like the later one is being processed by markdown parser...? Not sure.

yankeeinlondon commented 1 year ago

I was looking at this issue because it came into my inbox and then I realized that I started the thread. Ha! Well I do want to get back to this soon. I've been working on the vite-plugin-md plugin a lot recently and had intended to slip some fixes in there but to be honest it would be better to do this more globally in ViteJS scope rather than just this plugin.

brc-dd commented 1 year ago

Hi! I guess this was fixed by #665. Can someone try this on latest VitePress release and see if it works?

brc-dd commented 1 year ago

Okay so if I have a component (BaseLayout) like this:

<template>
  <div class="container">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

Then I am able to do this inside my markdown without any errors:

<BaseLayout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    Here's some contact info
  </template>
</BaseLayout>

<script setup>
import BaseLayout from './BaseLayout.vue'
</script>

Closing this for now, let us know if there is still some issue.

akil-rails commented 1 year ago

I tried using markdown instead of html in the md file (as below)

<BaseLayout>
  <template v-slot:header>
    # This won't show as a heading
  </template>

  <template #default>

  # But this will

  </template>

  <template #footer>

    ## but this won't

  </template>
</BaseLayout>

Is this the expected behaviour?

  1. ensure that there's an empty line above & below the markdown content
  2. do not indent more than 2 spaces

https://stackblitz.com/edit/github-zbjrsg-czuw6p?file=package.json,index.md,BaseLayout.vue

brc-dd commented 1 year ago

Is this the expected behaviour?

  1. ensure that there's an empty line above & below the markdown content

@akil-rails Yes. It's part of the CommonMark Spec: https://spec.commonmark.org/current/#html-blocks

  1. do not indent more than 2 spaces

Yes. It'll be treated like indented code blocks: https://spec.commonmark.org/current/#indented-code-blocks

akil-rails commented 1 year ago

Thank you !