diff --git a/docs/content/1.get-started/1.installation.md b/docs/content/1.get-started/1.installation.md index b6e1dfef0..e8ac76f0a 100644 --- a/docs/content/1.get-started/1.installation.md +++ b/docs/content/1.get-started/1.installation.md @@ -15,9 +15,9 @@ Docus is a pre-configured [Nuxt](https://nuxtjs.org) application, with [Windi CS Start your documentation in a new GitHub repository by using our [GitHub template](https://github.com/nuxtlabs/docus-starter): -:::button-link{size="medium" blank href="https://github.com/nuxtlabs/docus-starter/generate"} +::button-link{size="medium" blank href="https://github.com/nuxtlabs/docus-starter/generate"} Create a repo with the Docus starter -::: +:: ### Download locally @@ -33,23 +33,23 @@ This command will create a new folder named `docs/` and download the Docus start Vercel lets you set up the starter on your favorite Git provider (GitHub, GitLab or Bitbucket) - and deploy for free. -:::button-link{blank href="https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fnuxtlabs%2Fdocus-starter"} +::button-link{blank href="https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fnuxtlabs%2Fdocus-starter"} Create and deploy using Vercel -::: +:: **See it in action**: -:::video-player{yml loop playsinline controls} -poster: https://res.cloudinary.com/nuxt/video/upload/v1612886404/docus/docus-vercel_wwaryz.jpg +::video-player{loop playsinline controls} sources: - - src: https://res.cloudinary.com/nuxt/video/upload/q_auto/v1612886404/docus/docus-vercel_wwaryz.webm type: video/webm - src: https://res.cloudinary.com/nuxt/video/upload/q_auto/v1612886404/docus/docus-vercel_wwaryz.mp4 type: video/mp4 - src: https//res.cloudinary.com/nuxt/video/upload/q_auto/v1612886404/docus/docus-vercel_wwaryz.ogv type: video/ogg - ::: +poster: https://res.cloudinary.com/nuxt/video/upload/v1612886404/docus/docus-vercel_wwaryz.jpg +--- +:: ## Directory Structure diff --git a/docs/content/2.usage/1.content.md b/docs/content/2.usage/1.content.md index f9a72d6de..767e81805 100644 --- a/docs/content/2.usage/1.content.md +++ b/docs/content/2.usage/1.content.md @@ -19,9 +19,11 @@ category: 'Getting started' Introducing my awesome Nuxt module! ``` -:::alert{type="info"} +::alert +type: info +--- Checkout Nuxt Content documentation on [writing markdown content](https://content.nuxtjs.org/writing#markdown). -::: +:: ## Front-matter @@ -115,9 +117,9 @@ content/ setting.json ``` -:::alert{type="info"} +::alert{type="info"} As explained in the [Nuxt config](/get-started/configuration#nuxt) section, we use `defu.arrayFn` to merge your config. You can override the `i18n.locales` array by using a function, or you can pass an array to concat with the default one (which only includes the `en` locale). -::: +:: ## Routing @@ -127,7 +129,7 @@ Each markdown page in the `content/` directory will become a page and will be li **Example** -:::code-group +::code-group ``` [Directory structure] content/ @@ -141,6 +143,6 @@ content/ /setup ``` -::: +:: > You can take a look at our [docs content folder](https://github.com/nuxt/content/tree/dev/docs/content/en) to see an example diff --git a/docs/content/2.usage/2.components.md b/docs/content/2.usage/2.components.md index cd2feb1f7..82a6687d5 100644 --- a/docs/content/2.usage/2.components.md +++ b/docs/content/2.usage/2.components.md @@ -4,125 +4,123 @@ You can create your own Vue components in the `components/` folder. Check out [t ### `alert` -:::::code-group +::code-group -::::code-block{label="Preview" preview} -:::alert{type="info" style="margin-top: 0;"} -Check out an **info** alert with `code` and a [link](/). -::: + ::code-block{label="Preview" preview} + ::alert{type="info" style="margin-top: 0;"} + Check out an **info** alert with `code` and a [link](/). + :: -:::alert{type="success"} -Check out a **success** alert with `code` and a [link](/). -::: + ::alert{type="success"} + Check out a **success** alert with `code` and a [link](/). + :: -:::alert{type="warning"} -Check out a **warning** alert with `code` and a [link](/). -::: + ::alert{type="warning"} + Check out a **warning** alert with `code` and a [link](/). + :: -:::alert{type="danger" style="margin-bottom: 0;"} -Check out a **danger** alert with `code` and a [link](/). -::: -:::: + ::alert{type="danger" style="margin-bottom: 0;"} + Check out a **danger** alert with `code` and a [link](/). + :: + :: -```md [Code] -:::alert{type="info" style="margin-top: 0;"} -Check out an **info** alert with `code` and a [link](/). -::: + ```md [Code] + ::alert{type="info" style="margin-top: 0;"} + Check out an **info** alert with `code` and a [link](/). + :: -:::alert{type="success"} -Check out a **success** alert with `code` and a [link](/). -::: + ::alert{type="success"} + Check out a **success** alert with `code` and a [link](/). + :: -:::alert{type="warning"} -Check out a **warning** alert with `code` and a [link](/). -::: + ::alert{type="warning"} + Check out a **warning** alert with `code` and a [link](/). + :: -:::alert{type="danger" style="margin-bottom: 0;"} -Check out a **danger** alert with `code` and a [link](/). -::: -``` + ::alert{type="danger" style="margin-bottom: 0;"} + Check out a **danger** alert with `code` and a [link](/). + :: + ``` -::::: +:: -::props{of="components/atoms/Alert"} +:props{of="components/atoms/Alert"} ### `list` -:::::code-group -::::code-block{label="Preview" active preview} - -:::list{type="primary"} -- **Important** -- Always -::: - -:::list{type="success"} -- Amazing -- Congrats -::: - -:::list{type="info"} -- Do you know? -- You can also do this -::: +::code-group + ::code-block{label="Preview" active preview} -:::list{type="warning"} -- Be careful -- Use with precautions -::: + ::list{type="primary"} + - **Important** + - Always + :: -:::list{type="danger"} -- Drinking too much -- Driving drunk -::: + ::list{type="success"} + - Amazing + - Congrats + :: -:::: + ::list{type="info"} + - Do you know? + - You can also do this + :: -```md [Code] -:::list{type="primary"} -- **Important** -- Always -::: + ::list{type="warning"} + - Be careful + - Use with precautions + :: -:::list{type="success"} -- Amazing -- Congrats -::: + ::list{type="danger"} + - Drinking too much + - Driving drunk + :: -:::list{type="info"} -- Do you know? -- You can also do this -::: + :: -:::list{type="warning"} -- Be careful -- Use with precautions -::: - -:::list{type="danger"} -- Drinking too much -- Driving drunk -::: -``` + ```md [Code] + ::list{type="primary"} + - **Important** + - Always + :: + + ::list{type="success"} + - Amazing + - Congrats + :: + + ::list{type="info"} + - Do you know? + - You can also do this + :: + + ::list{type="warning"} + - Be careful + - Use with precautions + :: + + ::list{type="danger"} + - Drinking too much + - Driving drunk + :: + ``` -::::: +:: -::props{of="components/atoms/List"} +:props{of="components/atoms/List"} ### `badge` -::::code-group -:::code-block{label="Preview" active preview} - -:badge[v1.2+] +::code-group + ::code-block{label="Preview" active preview} + :badge[v1.2+] + :: -::: - -```md [Code] -:badge[v1.2+] -``` + ```md [Code] + :badge[v1.2+] + ``` -:::: +:: ### `code-group` @@ -135,86 +133,84 @@ This component uses `slots`. See [`code-block`](#code-block) below. ````html ℹ️ Backslashes are for demonstration -::::code-group +::code-group -:::code-block{label="Yarn" active} +::code-block{label="Yarn" active} ```bash yarn add docus \``` -::: +:: ```bash [NPM] npm install docus \``` -:::: +:: ```` **Result** -::::code-group +::code-group -:::code-block{label="Yarn" active} -```bash -yarn add docus -``` -::: - -```bash [NPM] -npm install docus -``` + ```bash [Yarn] + yarn add docus + ``` + + ```bash [NPM] + npm install docus + ``` -:::: +:: -::props{of="components/atoms/CodeBlock"} +:props{of="components/atoms/CodeBlock"} ### `inject-content` Cross-reference other files within your documentation (such as example code you want to include on multiple pages or across all languages). -::::code-group - :::code-block{label="Preview" active preview} - ::inject-content{query="2.usage/_example"} - ::: +::code-group + ::code-block{label="Preview" active preview} + :inject-content{query="2.usage/_example"} + :: ```md [Code] - ::inject-content{query="2.usage/_example"} + :inject-content{query="2.usage/_example"} ``` -:::: +:: -::props{of="components/atoms/InjectContent"} +:props{of="components/atoms/InjectContent"} ### `code-sandbox` Embed CodeSandbox easily in your documentation with great performances, using the [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to load when visible in the viewport. -::::code-group - :::code-block{label="Preview" active preview} - ::code-sandbox{src="https://codesandbox.io/embed/nuxt-content-l164h?hidenavigation=1&theme=dark"} - ::: +::code-group + ::code-block{label="Preview" active preview} + :code-sandbox{src="https://codesandbox.io/embed/nuxt-content-l164h?hidenavigation=1&theme=dark"} + :: ```md [Code] - ::code-sandbox{src="https://codesandbox.io/embed/nuxt-content-l164h?hidenavigation=1&theme=dark"} + :code-sandbox{src="https://codesandbox.io/embed/nuxt-content-l164h?hidenavigation=1&theme=dark"} ``` -:::: +:: -::props{of="components/atoms/CodeSandbox"} +:props{of="components/atoms/CodeSandbox"} ### `tweet` Embed tweets easily in your documentation - with great performance. Tweets will be embedded statically without using any runtime JS. -::::code-group - :::code-block{label="Preview" active preview} +::code-group + ::code-block{label="Preview" active preview} - ::tweet{id="1314628331841761289"} + :tweet{id="1314628331841761289"} - ::: + :: ```md [Code] - ::tweet{id="1314628331841761289"} + :tweet{id="1314628331841761289"} ``` -:::: +:: @@ -222,17 +218,17 @@ Embed tweets easily in your documentation - with great performance. Tweets will List accepted properties of a component. -::::code-group - :::code-block{label="Preview" active preview} +::code-group + ::code-block{label="Preview" active preview} - ::props{of="components/atoms/CodeBlock"} + :props{of="components/atoms/CodeBlock"} - ::: + :: ```md [Code] - ::props{of="components/atoms/CodeBlock"} + :props{of="components/atoms/CodeBlock"} ``` -:::: +:: -::props{of="components/atoms/Props"} +:props{of="components/atoms/Props"} diff --git a/docs/content/2.usage/3.assets.md b/docs/content/2.usage/3.assets.md index 5b151a333..d295e0f4f 100644 --- a/docs/content/2.usage/3.assets.md +++ b/docs/content/2.usage/3.assets.md @@ -9,21 +9,17 @@ navigation: You can add a `static/icon.png` image to enable [nuxt-pwa](https://pwa.nuxtjs.org) and generate a favicon automatically. - - +::alert `icon.png` should be a square of at least 512x512px. - - +:: ## Social preview You can add a `static/preview.png` image to have a social preview image in your metas. - - +::alert `preview.png` should be at least 640×320px (1280×640px for the best results). - - +:: ## Images with dark mode @@ -31,26 +27,18 @@ Docus supports light and dark mode 🌗. In order to display an image for a specific mode, you can use `dark-img` and `light-img` classes. - - - -
- Logo light - Logo dark -

Switch between light and dark mode: 

-
- -
+::code-group +::code-block{label="Preview" active preview} + :img{src="/logo-light.svg" class="light-img" alt="Logo light" style="margin:0;" width="219" height="40"} + :img{src="/logo-dark.svg" class="dark-img" alt="Logo dark" style="margin:0;" width="219" height="40"} +

Switch between light and dark mode: 

+:: - - -```md -Logo light -Logo dark +```md [Code] +:img{src="/logo-light.svg" class="light-img" alt="Logo light"} +:img{src="/logo-dark.svg" class="dark-img" alt="Logo dark"} ``` - - -
+:: ## High performance images @@ -58,10 +46,9 @@ Docus uses [@nuxt/image](https://images.nuxtjs.org) to resize and optimize image In order to allow `@nuxt/image` to optimize _remote_ images you will need to add the remote host into the domains list in your `nuxt.config.js`. - - +::code-group -```js +```js [nuxt.config] export default { image: { domains: ['https://image.nuxtjs.org'] @@ -69,20 +56,11 @@ export default { } ``` - - - -```md +```md [Code] ``` - - - - -
- -
- -
-
+::code-block{label="Preview" preview} + :nuxt-picture{src="https://image.nuxtjs.org/preview.png" width="1280" height="640"} +:: +:: diff --git a/docs/content/2.usage/_5.admin.md b/docs/content/2.usage/_5.admin.md index 83f5196b6..767002a1c 100644 --- a/docs/content/2.usage/_5.admin.md +++ b/docs/content/2.usage/_5.admin.md @@ -1,8 +1,8 @@ # Admin -:::alert{type="info"} +::alert{type="info"} Admin is currently under development. Informations below are related to developer experience for the Admin UI. -::: +:: Admin application is a **Vue 3** app built aside from the current app. diff --git a/docs/content/3.more/1.example.md b/docs/content/3.more/1.example.md index f0b2a72e8..ce30c0aa1 100644 --- a/docs/content/3.more/1.example.md +++ b/docs/content/3.more/1.example.md @@ -1,5 +1,4 @@ --- -csb_link: https://codesandbox.io/embed/docus-starter-1xsm7?hidenavigation=1&theme=dark fullscreen: true --- @@ -7,4 +6,4 @@ fullscreen: true > Live example of Docus on CodeSandbox :eyes: - +:code-sandbox{src="https://codesandbox.io/embed/docus-starter-1xsm7?hidenavigation=1&theme=dark"} diff --git a/docs/content/3.more/2.showcases.md b/docs/content/3.more/2.showcases.md index 98bcb9b04..5dab6cc2e 100644 --- a/docs/content/3.more/2.showcases.md +++ b/docs/content/3.more/2.showcases.md @@ -1,6 +1,11 @@ --- fullscreen: true draft: true +--- + +# Showcases + +::showcases showcases: - https://strapi.nuxtjs.org - https://tailwindcss.nuxtjs.org @@ -19,7 +24,4 @@ showcases: - https://sanity.nuxtjs.org - https://speedcurve.nuxtjs.org --- - -# Showcases - - +:: diff --git a/docs/content/3.more/3.deployment.md b/docs/content/3.more/3.deployment.md index 9f8ba0c53..db9eb122d 100644 --- a/docs/content/3.more/3.deployment.md +++ b/docs/content/3.more/3.deployment.md @@ -4,17 +4,17 @@ To generate the documentation for production, run the following command: - +::code-group -```bash [Yarn] -yarn build -``` + ```bash [Yarn] + yarn build + ``` -```bash [NPM] -npm run build -``` + ```bash [NPM] + npm run build + ``` - +:: This command will run `nuxt generate` with [fast static deployments](https://nuxtjs.org/blog/nuxt-static-improvements#faster-static-deployments). @@ -142,41 +142,30 @@ After your project has been imported, all subsequent pushes to main branch will 1. Install `surge` package globally: - - + ::code-group - ```bash + ```bash [Yarn] yarn global add surge ``` - - - - ```bash + ```bash [NPM] npm install --global surge ``` - - - + :: 2. Generate the documentation. Generating Docus project will create `dist` directory that contains all contents of your documentation. - - + ::code-group - ```bash + ```bash [Yarn] yarn build ``` - - - - ```bash + ```bash [NPM] npm run build ``` - - + :: 3. Use `surge` CLI to publish the `dist` directory diff --git a/docs/content/3.more/4.migration.md b/docs/content/3.more/4.migration.md index 01284025a..aba759429 100644 --- a/docs/content/3.more/4.migration.md +++ b/docs/content/3.more/4.migration.md @@ -42,65 +42,65 @@ By migrating to Docus, you will have a fresh new design for your documentation : In order to move from `@nuxt/content-theme-docs` to `docus` you need to install the new package: - +::code-group -```bash [Yarn] -yarn remove @nuxt/content-theme-docs && yarn add docus -``` + ```bash [Yarn] + yarn remove @nuxt/content-theme-docs && yarn add docus + ``` -```bash [NPM] -npm uninstall @nuxt/content-theme-docs && npm install docus -``` + ```bash [NPM] + npm uninstall @nuxt/content-theme-docs && npm install docus + ``` - +:: ### `nuxt.config.js` Then, instead of importing `@nuxt/content-theme-docs`, you need to import `docus` in your `nuxt.config.js`: - +::code-group -```ts [New] -import { withDocus } from 'docus' + ```ts [New] + import { withDocus } from 'docus' -export default withDocus({ - // Additional nuxt configuration -}) -``` + export default withDocus({ + // Additional nuxt configuration + }) + ``` -```ts [Old] -import theme from '@nuxt/content-theme-docs' + ```ts [Old] + import theme from '@nuxt/content-theme-docs' -export default theme({ - // [additional nuxt configuration] -}) -``` + export default theme({ + // [additional nuxt configuration] + }) + ``` - +:: If you specified a primary color, you need to move `docs.primaryColor` to `contents/setting.json` in `colors.primary`: - +::code-group -```json [New: content/settings.json] -{ - "colors": { - "primary": "#E24F55" + ```json [New: content/settings.json] + { + "colors": { + "primary": "#E24F55" + } } -} -``` + ``` -```ts [Old: nuxt.config.js] -import theme from '@nuxt/content-theme-docs' + ```ts [Old: nuxt.config.js] + import theme from '@nuxt/content-theme-docs' -export default theme({ - docs: { - primaryColor: '#E24F55' - } -}) -``` + export default theme({ + docs: { + primaryColor: '#E24F55' + } + }) + ``` - +:: ### `content/settings.json` @@ -108,27 +108,27 @@ If you specified `githubApi`, `defaultBranch` or `defaultDir` in `content/settin The `github` key can now be a `String` to act as `github.repo` or an object with `repo`, `branch`, `dir`, `url` and `apiUrl` keys: - +::code-group -```json [New] -{ - "github": { - "repo": "nuxtlabs/docus", - "branch": "main", - "dir": "docs" + ```json [New] + { + "github": { + "repo": "nuxtlabs/docus", + "branch": "main", + "dir": "docs" + } } -} -``` + ``` -```json [Old] -{ - "github": "nuxtlabs/docus", - "defaultBranch": "main", - "defaultDir": "docs" -} -``` + ```json [Old] + { + "github": "nuxtlabs/docus", + "defaultBranch": "main", + "defaultDir": "docs" + } + ``` - +:: ### tailwind.config.js @@ -136,8 +136,8 @@ Docus is now using [Windi CSS](https://windicss.org), if you had a `tailwind.con --- - - +::alert +type: success +--- 🎉  Congrats, you can now redeploy your application to take advantage of Docus and its new features! - - +:: diff --git a/docs/content/3.more/5.extend.md b/docs/content/3.more/5.extend.md index a64bc7b61..b7cc40e7b 100644 --- a/docs/content/3.more/5.extend.md +++ b/docs/content/3.more/5.extend.md @@ -21,17 +21,17 @@ If you want to use Plausible, you can do so with the `vue-plausible` plugin. 2. Install the Plausible plugin in your project. - +::code-group -```bash [Yarn] -yarn install vue-plausible -``` + ```bash [Yarn] + yarn install vue-plausible + ``` -```bash [NPM] -npm install vue-plausible -``` + ```bash [NPM] + npm install vue-plausible + ``` - +:: 3. Register the plugin @@ -43,11 +43,11 @@ export default { } ``` - - +::alert +type: success +--- That's it! Deploy your project and enjoy Plausible. - - +:: ### Custom integration diff --git a/docs/content/index.md b/docs/content/index.md index 720872ace..cbfbd1e2d 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -6,9 +6,7 @@ description: >- navigation: false --- -:::block-hero{yml} -title: title -description: description +::block-hero cta: - Get started - /get-started/installation @@ -16,61 +14,65 @@ secondary: - Open on GitHub → - https://github.com/nuxtlabs/docus snippet: npx degit nuxtlabs/docus-starter#main docs -::: +---title +Documentation Generator based on Nuxt and Windi. +---description +Write pages in markdown, use Vue components, add style with Windi CSS and enjoy the power of Nuxt with a blazing fast developer experience. +:: -::::card-grid{title="What's included?"} - :::card{yml} +::card-grid{title="What's included?"} + ::card icon: IconNuxt description: Harness the full power of Nuxt and the Nuxt ecosystem. iconClass: 'text-hex-00DC82' title: Nuxt Architecture. - ::: + :: - :::card{yml} + ::card icon: IconVue title: Vue Components. description: Use built-in components (or your own!) inside your content. - ::: + :: - :::card{yml} + ::card icon: IconMarkdown title: Write Markdown. description: Enjoy the ease and simplicity of Markdown as you write your documentation. - ::: + :: - :::card{yml} + ::card icon: IconWindi title: Windi CSS. description: Windi CSS is built in for great developer experience and rapid customization. - ::: + :: - :::card{yml} + ::card icon: IconSSG title: Static Generation. description: Generate your documentation as a static website and host it anywhere. - ::: + :: - :::card{yml} + ::card icon: IconLighthouse title: Lighthouse Optimised. description: Start with a blazing fast site with a perfect Lighthouse score. - ::: + :: - :::card{yml} + ::card icon: IconZap title: Smart Generation. description: Automatically skip a full build if you've only changed Markdown files. - ::: + :: - :::card{yml} + ::card icon: IconPuzzle title: Extensible. description: Customize the whole design, or add components using slots - you can make Docus your own. - ::: + :: - :::card{yml} + ::card icon: IconGitHub title: Open Source. description: Docus is released under the MIT license and made with love by the NuxtLabs team. - ::: -:::: + :: +:: diff --git a/docs/content/templates/pre-launch.md b/docs/content/templates/pre-launch.md index 832199365..d19d70046 100644 --- a/docs/content/templates/pre-launch.md +++ b/docs/content/templates/pre-launch.md @@ -4,11 +4,12 @@ title: Pre-launch template --- -:::pre-launch-hero{yml} +::pre-launch-hero title: Awesome startup description: Pretty long awesome startup description cta: description: Request an invite placeholder: Your email label: Get Invite -::: +--- +:: diff --git a/docs/content/templates/pricing.md b/docs/content/templates/pricing.md index eb36a6935..9dbc79bf1 100644 --- a/docs/content/templates/pricing.md +++ b/docs/content/templates/pricing.md @@ -3,7 +3,7 @@ template: marketing footer: false --- -:::pricing-block{yml} +::pricing-block plans: monthly: @@ -55,6 +55,6 @@ meta: preSelectedBadge: Popular checkoutText: 'Total:' checkoutButtonText: Checkout - -::: +--- +:: \ No newline at end of file diff --git a/src/core/parser/markdown/compiler.ts b/src/core/parser/markdown/compiler.ts index c84d561bd..28b196d38 100644 --- a/src/core/parser/markdown/compiler.ts +++ b/src/core/parser/markdown/compiler.ts @@ -34,6 +34,14 @@ function parseAsJSON(node: Node, parent: DocusMarkdownNode[]) { }) } + /** + * rename directive slots tags name + */ + if (node.tagName === 'directive-slot') { + node.tagName = 'template' + node.content = { ...node } + } + /** * Replace a tag with nuxt-link if relative */ diff --git a/src/core/parser/markdown/directive/index.ts b/src/core/parser/markdown/directive/index.ts new file mode 100644 index 000000000..a26ec5d1e --- /dev/null +++ b/src/core/parser/markdown/directive/index.ts @@ -0,0 +1,21 @@ +// https://github.com/remarkjs/remark-directive/blob/main/index.js +import syntax from './micromark-directive' +import fromMarkdown from './remark-directive/from-markdown' +import toMarkdown from './remark-directive/to-markdown' + +export default function directive() { + const data = this.data() + + add('micromarkExtensions', syntax()) + add('fromMarkdownExtensions', fromMarkdown) + add('toMarkdownExtensions', toMarkdown) + + function add(field, value) { + /* istanbul ignore if - other extensions. */ + if (!data[field]) { + data[field] = [] + } + + data[field].push(value) + } +} diff --git a/src/core/parser/markdown/directive/micromark-directive/constants.ts b/src/core/parser/markdown/directive/micromark-directive/constants.ts new file mode 100644 index 000000000..98144a7e9 --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/constants.ts @@ -0,0 +1,46 @@ +export const ContainerSequenceSize = 2 + +export const SectionSequenceSize = 3 + +export const Codes = { + /** + * '`' + */ + backTick: 96, + /** + * '\' + */ + backSlash: 92, + /** + * ':' + */ + colon: 58, + /** + * '-' + */ + dash: 45, + /** + * '.' + */ + dot: 46, + /** + * ' ' + */ + space: 32, + /** + * '[' + */ + openningSquareBracket: 91, + /** + * ']' + */ + closingSquareBracket: 93, + /** + * '{' + */ + openningCurlyBracket: 123, + /** + * '_' + */ + underscore: 95 +} diff --git a/src/core/parser/markdown/directive/micromark-directive/factory-attributes.ts b/src/core/parser/markdown/directive/micromark-directive/factory-attributes.ts new file mode 100644 index 000000000..082ab1d7d --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/factory-attributes.ts @@ -0,0 +1,283 @@ +import { Effects, Okay, NotOkay } from 'micromark/dist/shared-types' +import asciiAlpha from 'micromark/dist/character/ascii-alpha' +import asciiAlphanumeric from 'micromark/dist/character/ascii-alphanumeric' +import markdownLineEnding from 'micromark/dist/character/markdown-line-ending' +import markdownLineEndingOrSpace from 'micromark/dist/character/markdown-line-ending-or-space' +import markdownSpace from 'micromark/dist/character/markdown-space' +import createWhitespace from 'micromark/dist/tokenize/factory-whitespace' +import createSpace from 'micromark/dist/tokenize/factory-space' + +export default function createAttributes( + effects: Effects, + ok: Okay, + nok: NotOkay, + attributesType: string, + attributesMarkerType: string, + attributeType: string, + attributeIdType: string, + attributeClassType: string, + attributeNameType: string, + attributeInitializerType: string, + attributeValueLiteralType: string, + attributeValueType: string, + attributeValueMarker: string, + attributeValueData: string, + disallowEol?: boolean +) { + let type + let marker + + return start + + function start(code: number) { + // Always a `{` + effects.enter(attributesType) + effects.enter(attributesMarkerType) + effects.consume(code) + effects.exit(attributesMarkerType) + return between + } + + function between(code: number) { + if (code === 35 /* `#` */) { + type = attributeIdType + return shortcutStart(code) + } + + if (code === 46 /* `.` */) { + type = attributeClassType + return shortcutStart(code) + } + + if (code === 58 /* `:` */ || code === 95 /* `_` */ || asciiAlpha(code)) { + effects.enter(attributeType) + effects.enter(attributeNameType) + effects.consume(code) + return name + } + + if (disallowEol && markdownSpace(code)) { + return createSpace(effects, between, 'whitespace')(code) + } + + if (!disallowEol && markdownLineEndingOrSpace(code)) { + return createWhitespace(effects, between)(code) + } + + return end(code) + } + + function shortcutStart(code: number) { + effects.enter(attributeType) + effects.enter(type) + effects.enter(type + 'Marker') + effects.consume(code) + effects.exit(type + 'Marker') + return shortcutStartAfter + } + + function shortcutStartAfter(code: number) { + if ( + code === null /* EOF */ || + code === 34 /* `"` */ || + code === 35 /* `#` */ || + code === 39 /* `'` */ || + code === 46 /* `.` */ || + code === 60 /* `<` */ || + code === 61 /* `=` */ || + code === 62 /* `>` */ || + code === 96 /* `` ` `` */ || + code === 125 /* `}` */ || + markdownLineEndingOrSpace(code) + ) { + return nok(code) + } + + effects.enter(type + 'Value') + effects.consume(code) + return shortcut + } + + function shortcut(code: number) { + if ( + code === null /* EOF */ || + code === 34 /* `"` */ || + code === 39 /* `'` */ || + code === 60 /* `<` */ || + code === 61 /* `=` */ || + code === 62 /* `>` */ || + code === 96 /* `` ` `` */ + ) { + return nok(code) + } + + if (code === 35 /* `#` */ || code === 46 /* `.` */ || code === 125 /* `}` */ || markdownLineEndingOrSpace(code)) { + effects.exit(type + 'Value') + effects.exit(type) + effects.exit(attributeType) + return between(code) + } + + effects.consume(code) + return shortcut + } + + function name(code: number) { + if ( + code === 45 /* `-` */ || + code === 46 /* `.` */ || + code === 58 /* `:` */ || + code === 95 /* `_` */ || + asciiAlphanumeric(code) + ) { + effects.consume(code) + return name + } + + effects.exit(attributeNameType) + + if (disallowEol && markdownSpace(code)) { + return createSpace(effects, nameAfter, 'whitespace')(code) + } + + if (!disallowEol && markdownLineEndingOrSpace(code)) { + return createWhitespace(effects, nameAfter)(code) + } + + return nameAfter(code) + } + + function nameAfter(code: number) { + if (code === 61 /* `=` */) { + effects.enter(attributeInitializerType) + effects.consume(code) + effects.exit(attributeInitializerType) + return valueBefore + } + + // Attribute w/o value. + effects.exit(attributeType) + return between(code) + } + + function valueBefore(code: number) { + if ( + code === null /* EOF */ || + code === 60 /* `<` */ || + code === 61 /* `=` */ || + code === 62 /* `>` */ || + code === 96 /* `` ` `` */ || + code === 125 /* `}` */ || + (disallowEol && markdownLineEnding(code)) + ) { + return nok(code) + } + + if (code === 34 /* `"` */ || code === 39 /* `'` */) { + effects.enter(attributeValueLiteralType) + effects.enter(attributeValueMarker) + effects.consume(code) + effects.exit(attributeValueMarker) + marker = code + return valueQuotedStart + } + + if (disallowEol && markdownSpace(code)) { + return createSpace(effects, valueBefore, 'whitespace')(code) + } + + if (!disallowEol && markdownLineEndingOrSpace(code)) { + return createWhitespace(effects, valueBefore)(code) + } + + effects.enter(attributeValueType) + effects.enter(attributeValueData) + effects.consume(code) + marker = undefined + return valueUnquoted + } + + function valueUnquoted(code: number) { + if ( + code === null /* EOF */ || + code === 34 /* `"` */ || + code === 39 /* `'` */ || + code === 60 /* `<` */ || + code === 61 /* `=` */ || + code === 62 /* `>` */ || + code === 96 /* `` ` `` */ + ) { + return nok(code) + } + + if (code === 125 /* `}` */ || markdownLineEndingOrSpace(code)) { + effects.exit(attributeValueData) + effects.exit(attributeValueType) + effects.exit(attributeType) + return between(code) + } + + effects.consume(code) + return valueUnquoted + } + + function valueQuotedStart(code: number) { + if (code === marker) { + effects.enter(attributeValueMarker) + effects.consume(code) + effects.exit(attributeValueMarker) + effects.exit(attributeValueLiteralType) + effects.exit(attributeType) + return valueQuotedAfter + } + + effects.enter(attributeValueType) + return valueQuotedBetween(code) + } + + function valueQuotedBetween(code: number) { + if (code === marker) { + effects.exit(attributeValueType) + return valueQuotedStart(code) + } + + if (code === null /* EOF */) { + return nok(code) + } + + // Note: blank lines can’t exist in content. + if (markdownLineEnding(code)) { + return disallowEol ? nok(code) : createWhitespace(effects, valueQuotedBetween)(code) + } + + effects.enter(attributeValueData) + effects.consume(code) + return valueQuoted + } + + function valueQuoted(code: number) { + if (code === marker || code === null /* EOF */ || markdownLineEnding(code)) { + effects.exit(attributeValueData) + return valueQuotedBetween(code) + } + + effects.consume(code) + return valueQuoted + } + + function valueQuotedAfter(code: number) { + return code === 125 /* `}` */ || markdownLineEndingOrSpace(code) ? between(code) : end(code) + } + + function end(code: number) { + if (code === 125 /* `}` */) { + effects.enter(attributesMarkerType) + effects.consume(code) + effects.exit(attributesMarkerType) + effects.exit(attributesType) + return ok + } + + return nok(code) + } +} diff --git a/src/core/parser/markdown/directive/micromark-directive/factory-label.ts b/src/core/parser/markdown/directive/micromark-directive/factory-label.ts new file mode 100644 index 000000000..6cbd8e950 --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/factory-label.ts @@ -0,0 +1,118 @@ +import { Effects, Okay, NotOkay } from 'micromark/dist/shared-types' +import markdownLineEnding from 'micromark/dist/character/markdown-line-ending' + +// This is a fork of: +// +// to allow empty labels, balanced brackets (such as for nested directives), +// text instead of strings, and optionally disallows EOLs. + +// eslint-disable-next-line max-params +export default function createLabel( + effects: Effects, + ok: Okay, + nok: NotOkay, + type: string, + markerType: string, + stringType: string, + disallowEol?: boolean +) { + let size = 0 + let balance = 0 + + return start + + function start(code: number) { + /* istanbul ignore if - always `[` */ + if (code !== 91 /* `[` */) throw new Error('expected `[`') + effects.enter(type) + effects.enter(markerType) + effects.consume(code) + effects.exit(markerType) + return afterStart + } + + function afterStart(code: number) { + if (code === 93 /* `]` */) { + effects.enter(markerType) + effects.consume(code) + effects.exit(markerType) + effects.exit(type) + return ok + } + + effects.enter(stringType) + return atBreak(code) + } + + function atBreak(code: number) { + if ( + code === null /* EOF */ || + /* */ + size > 999 + ) { + return nok(code) + } + + if (code === 93 /* `]` */ && !balance--) { + return atClosingBrace(code) + } + + if (markdownLineEnding(code)) { + if (disallowEol) { + return nok(code) + } + + effects.enter('lineEnding') + effects.consume(code) + effects.exit('lineEnding') + return atBreak + } + + // @ts-ignore + effects.enter('chunkText', { contentType: 'text' }) + return label(code) + } + + function label(code: number) { + if ( + code === null /* EOF */ || + markdownLineEnding(code) || + /* */ + size > 999 + ) { + effects.exit('chunkText') + return atBreak(code) + } + + if (code === 91 /* `[` */ && ++balance > 3) { + return nok(code) + } + + if (code === 93 /* `]` */ && !balance--) { + effects.exit('chunkText') + return atClosingBrace(code) + } + + effects.consume(code) + return code === 92 /* `\` */ ? labelEscape : label + } + + function atClosingBrace(code: number) { + effects.exit(stringType) + effects.enter(markerType) + effects.consume(code) + effects.exit(markerType) + effects.exit(type) + return ok + } + + function labelEscape(code: number) { + if (code === 91 /* `[` */ || code === 92 /* `\` */ || code === 93 /* `]` */) { + effects.consume(code) + size++ + return label + } + + return label(code) + } +} diff --git a/src/core/parser/markdown/directive/micromark-directive/factory-name.ts b/src/core/parser/markdown/directive/micromark-directive/factory-name.ts new file mode 100644 index 000000000..14b0e31d3 --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/factory-name.ts @@ -0,0 +1,30 @@ +import { Effects, Okay, NotOkay } from 'micromark/dist/shared-types' +import asciiAlpha from 'micromark/dist/character/ascii-alpha' +import asciiAlphanumeric from 'micromark/dist/character/ascii-alphanumeric' + +export default function createName(effects: Effects, ok: Okay, nok: NotOkay, nameType: string) { + const self = this + + return start + + function start(code: number) { + if (asciiAlpha(code)) { + effects.enter(nameType) + effects.consume(code) + return name + } + + return nok(code) + } + + function name(code: number) { + if (code === 45 /* `-` */ || code === 95 /* `_` */ || asciiAlphanumeric(code)) { + effects.consume(code) + return name + } + + effects.exit(nameType) + // To do next major: disallow `-` at end of name too, for consistency. + return self.previous === 95 /* `_` */ ? nok(code) : ok(code) + } +} diff --git a/src/core/parser/markdown/directive/micromark-directive/html.ts b/src/core/parser/markdown/directive/micromark-directive/html.ts new file mode 100644 index 000000000..c6e5a7579 --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/html.ts @@ -0,0 +1,179 @@ +import decode from 'parse-entities/decode-entity' + +const own = {}.hasOwnProperty + +export default function createDirectiveHtmlExtension(options) { + const extensions = options || {} + + return { + enter: { + directiveContainer: enterContainer, + directiveContainerAttributes: enterAttributes, + directiveContainerContent: enterContainerContent, + directiveContainerLabel: enterLabel, + + directiveLeaf: enterLeaf, + directiveLeafAttributes: enterAttributes, + directiveLeafLabel: enterLabel, + + directiveText: enterText, + directiveTextAttributes: enterAttributes, + directiveTextLabel: enterLabel + }, + exit: { + directiveContainer: exit, + directiveContainerAttributeClassValue: exitAttributeClassValue, + directiveContainerAttributeIdValue: exitAttributeIdValue, + directiveContainerAttributeName: exitAttributeName, + directiveContainerAttributeValue: exitAttributeValue, + directiveContainerAttributes: exitAttributes, + directiveContainerContent: exitContainerContent, + directiveContainerFence: exitContainerFence, + directiveContainerLabel: exitLabel, + directiveContainerName: exitName, + + directiveLeaf: exit, + directiveLeafAttributeClassValue: exitAttributeClassValue, + directiveLeafAttributeIdValue: exitAttributeIdValue, + directiveLeafAttributeName: exitAttributeName, + directiveLeafAttributeValue: exitAttributeValue, + directiveLeafAttributes: exitAttributes, + directiveLeafLabel: exitLabel, + directiveLeafName: exitName, + + directiveText: exit, + directiveTextAttributeClassValue: exitAttributeClassValue, + directiveTextAttributeIdValue: exitAttributeIdValue, + directiveTextAttributeName: exitAttributeName, + directiveTextAttributeValue: exitAttributeValue, + directiveTextAttributes: exitAttributes, + directiveTextLabel: exitLabel, + directiveTextName: exitName + } + } + + function enterContainer() { + return enter.call(this, 'containerDirective') + } + + function enterLeaf() { + return enter.call(this, 'leafDirective') + } + + function enterText() { + return enter.call(this, 'textDirective') + } + + function enter(type) { + let stack = this.getData('directiveStack') + if (!stack) this.setData('directiveStack', (stack = [])) + stack.push({ type }) + } + + function exitName(token) { + const stack = this.getData('directiveStack') + stack[stack.length - 1].name = this.sliceSerialize(token) + } + + function enterLabel() { + this.buffer() + } + + function exitLabel() { + const data = this.resume() + const stack = this.getData('directiveStack') + stack[stack.length - 1].label = data + } + + function enterAttributes() { + this.buffer() + this.setData('directiveAttributes', []) + } + + function exitAttributeIdValue(token) { + this.getData('directiveAttributes').push(['id', decodeLight(this.sliceSerialize(token))]) + } + + function exitAttributeClassValue(token) { + this.getData('directiveAttributes').push(['class', decodeLight(this.sliceSerialize(token))]) + } + + function exitAttributeName(token) { + // Attribute names in CommonMark are significantly limited, so character + // references can’t exist. + this.getData('directiveAttributes').push([this.sliceSerialize(token), '']) + } + + function exitAttributeValue(token) { + const attributes = this.getData('directiveAttributes') + attributes[attributes.length - 1][1] = decodeLight(this.sliceSerialize(token)) + } + + function exitAttributes() { + const stack = this.getData('directiveStack') + const attributes = this.getData('directiveAttributes') + const cleaned: any = {} + let index = -1 + let attribute + + while (++index < attributes.length) { + attribute = attributes[index] + + if (attribute[0] === 'class' && cleaned.class) { + cleaned.class += ' ' + attribute[1] + } else { + cleaned[attribute[0]] = attribute[1] + } + } + + this.resume() + this.setData('directiveAttributes') + stack[stack.length - 1].attributes = cleaned + } + + function enterContainerContent() { + this.buffer() + } + + function exitContainerContent() { + const data = this.resume() + const stack = this.getData('directiveStack') + stack[stack.length - 1].content = data + } + + function exitContainerFence() { + const stack = this.getData('directiveStack') + const directive = stack[stack.length - 1] + if (!directive.fenceCount) directive.fenceCount = 0 + directive.fenceCount++ + if (directive.fenceCount === 1) this.setData('slurpOneLineEnding', true) + } + + function exit() { + const directive = this.getData('directiveStack').pop() + let found + let result + + if (own.call(extensions, directive.name)) { + result = extensions[directive.name].call(this, directive) + found = result !== false + } + + if (!found && own.call(extensions, '*')) { + result = extensions['*'].call(this, directive) + found = result !== false + } + + if (!found && directive.type !== 'textDirective') { + this.setData('slurpOneLineEnding', true) + } + } +} + +function decodeLight(value) { + return value.replace(/&(#(\d{1,7}|x[\da-f]{1,6})|[\da-z]{1,31});/gi, decodeIfPossible) +} + +function decodeIfPossible($0, $1) { + return decode($1) || $0 +} diff --git a/src/core/parser/markdown/directive/micromark-directive/index.ts b/src/core/parser/markdown/directive/micromark-directive/index.ts new file mode 100644 index 000000000..36cb9df08 --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/index.ts @@ -0,0 +1,17 @@ +// https://github.com/micromark/micromark-extension-directive/blob/main/lib/syntax.js + +import directiveText from './tokenize-directive-text' +import directiveContainer from './tokenize-directive-container' +import directiveContainerIndented from './tokenize-directive-container-indented' + +export default function directive() { + return { + text: { 58: directiveText }, + flow: { 58: [directiveContainer] }, + flowInitial: { + '-2': directiveContainerIndented, + '-1': directiveContainerIndented, + 32: directiveContainerIndented + } + } +} diff --git a/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-container-indented.ts b/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-container-indented.ts new file mode 100644 index 000000000..972fd4ee8 --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-container-indented.ts @@ -0,0 +1,30 @@ +import { Effects, Okay, NotOkay } from 'micromark/dist/shared-types' +import createSpace from 'micromark/dist/tokenize/factory-space' +import codeFenced from 'micromark/dist/tokenize/code-fenced.js' +import prefixSize from 'micromark/dist/util/prefix-size' +import directiveContainer from './tokenize-directive-container' +import { Codes } from './constants' + +function tokenize(effects: Effects, ok: Okay, nok: NotOkay) { + const self = this + return createSpace(effects, lineStart, 'linePrefix') + + function lineStart(code) { + // skip if line prefix is smaller than markdown code indent + if (prefixSize(self.events, 'linePrefix') < 4) { + return nok(code) + } + switch (code) { + case Codes.backTick: + return codeFenced.tokenize.call(self, effects, ok, nok)(code) + case Codes.colon: + return directiveContainer.tokenize.call(self, effects, ok, nok)(code) + default: + return nok(code) + } + } +} + +export default { + tokenize +} diff --git a/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-container.ts b/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-container.ts new file mode 100644 index 000000000..bfe90b1b5 --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-container.ts @@ -0,0 +1,332 @@ +import { Effects, Okay, NotOkay } from 'micromark/dist/shared-types' +import markdownLineEnding from 'micromark/dist/character/markdown-line-ending' +import createSpace from 'micromark/dist/tokenize/factory-space' +import sizeChunks from 'micromark/dist/util/size-chunks' +import createAttributes from './factory-attributes' +import createLabel from './factory-label' +import createName from './factory-name' +import { Codes, ContainerSequenceSize, SectionSequenceSize } from './constants' + +const label: any = { tokenize: tokenizeLabel, partial: true } +const attributes: any = { tokenize: tokenizeAttributes, partial: true } + +/** + * Calculate line indention size, line indention could be consists of multiple `linePrefix` events + * @param events parser tokens + * @returns line indention size + */ +function linePrefixSize(events) { + let size = 0 + let index = events.length - 1 + let tail = events[index] + while (index >= 0 && tail && tail[1].type === 'linePrefix' && tail[0] === 'exit') { + size += sizeChunks(tail[2].sliceStream(tail[1])) + index -= 1 + tail = events[index] + } + + return size +} + +function tokenize(effects: Effects, ok: Okay, nok: NotOkay) { + const self = this + const initialPrefix = linePrefixSize(this.events) + let sizeOpen = 0 + let previous + const containerSequenceSize = [] + + return start + + function start(code: number) { + /* istanbul ignore if - handled by mm */ + if (code !== Codes.colon) throw new Error('expected `:`') + effects.enter('directiveContainer') + effects.enter('directiveContainerFence') + effects.enter('directiveContainerSequence') + return sequenceOpen(code) + } + + function tokenizeSectionClosing(effects: Effects, ok: Okay, nok: NotOkay) { + let size = 0 + let sectionIndentSize = 0 + + return closingPrefixAfter + + function closingPrefixAfter(code: number) { + sectionIndentSize = linePrefixSize(self.events) + effects.exit('directiveContainerSection') + effects.enter('directiveContainerSectionSequence') + return closingSectionSequence(code) + } + + function closingSectionSequence(code: number) { + if (code === Codes.dash) { + effects.consume(code) + size++ + return closingSectionSequence + } + + if (size < SectionSequenceSize) return nok(code) + if (sectionIndentSize !== initialPrefix) return nok(code) + + effects.exit('directiveContainerSectionSequence') + return createSpace(effects, ok, 'whitespace')(code) + } + } + function sectionOpen(code: number) { + effects.enter('directiveContainerSection') + + if (markdownLineEnding(code)) { + return createSpace(effects, lineStart, 'whitespace')(code) + } + + effects.enter('directiveContainerSectionTitle') + return sectionTitle + } + + function sectionTitle(code: number) { + if (markdownLineEnding(code)) { + effects.exit('directiveContainerSectionTitle') + return createSpace(effects, lineStart, 'linePrefix', 4)(code) + } + effects.consume(code) + return sectionTitle + } + + function sequenceOpen(code: number) { + if (code === Codes.colon) { + effects.consume(code) + sizeOpen++ + return sequenceOpen + } + + if (sizeOpen < ContainerSequenceSize) { + return nok(code) + } + + effects.exit('directiveContainerSequence') + return createName.call(self, effects, afterName, nok, 'directiveContainerName')(code) + } + + function afterName(code: number) { + return code === Codes.openningSquareBracket + ? effects.attempt(label, afterLabel, afterLabel)(code) + : afterLabel(code) + } + + function afterLabel(code: number) { + return code === Codes.openningCurlyBracket + ? effects.attempt(attributes, afterAttributes, afterAttributes)(code) + : afterAttributes(code) + } + + function afterAttributes(code: number) { + return createSpace(effects, openAfter, 'whitespace')(code) + } + + function openAfter(code: number) { + effects.exit('directiveContainerFence') + + if (code === null) { + effects.exit('directiveContainer') + return ok(code) + } + + if (markdownLineEnding(code)) { + effects.enter('lineEnding') + effects.consume(code) + effects.exit('lineEnding') + return self.interrupt ? ok : contentStart + } + + return nok(code) + } + + function contentStart(code: number) { + if (code === null) { + effects.exit('directiveContainer') + return ok(code) + } + + effects.enter('directiveContainerContent') + effects.enter('directiveContainerSection') + return lineStart(code) + } + + function lineStartAfterPrefix(code: number) { + if (code === null) { + return after(code) + } + + if (!containerSequenceSize.length && (code === Codes.dash || code === Codes.space)) { + return effects.attempt({ tokenize: tokenizeSectionClosing, partial: true } as any, sectionOpen, chunkStart) + } + + const attempt = effects.attempt({ tokenize: tokenizeClosingFence, partial: true } as any, after, chunkStart) + + /** + * disbale spliting inner sections + */ + if (code === Codes.colon) { + return effects.check({ tokenize: detectContainer, partial: true } as any, chunkStart, attempt)(code) + } + + return attempt + } + + function lineStart(code: number) { + if (code === null) { + return after(code) + } + + return initialPrefix + ? createSpace(effects, lineStartAfterPrefix, 'linePrefix', initialPrefix + 1) + : lineStartAfterPrefix + } + + function chunkStart(code: number) { + if (code === null) { + return after(code) + } + + // @ts-ignore + const token = effects.enter('chunkDocument', { + contentType: 'document', + previous + }) + if (previous) previous.next = token + previous = token + return contentContinue(code) + } + + function contentContinue(code: number) { + if (code === null) { + effects.exit('chunkDocument') + return after(code) + } + + if (markdownLineEnding(code)) { + effects.consume(code) + effects.exit('chunkDocument') + return lineStart + } + + effects.consume(code) + return contentContinue + } + + function after(code: number) { + effects.exit('directiveContainerSection') + effects.exit('directiveContainerContent') + effects.exit('directiveContainer') + return ok(code) + } + + function tokenizeClosingFence(effects: Effects, ok: Okay, nok: NotOkay) { + let size = 0 + + return createSpace(effects, closingPrefixAfter, 'linePrefix', 4) + + function closingPrefixAfter(code: number) { + effects.enter('directiveContainerFence') + effects.enter('directiveContainerSequence') + return closingSequence(code) + } + + function closingSequence(code: number) { + if (code === Codes.colon) { + effects.consume(code) + size++ + return closingSequence + } + + if (containerSequenceSize.length) { + if (size === containerSequenceSize[containerSequenceSize.length - 1]) { + containerSequenceSize.pop() + } + return nok(code) + } + + // it is important to match sequence + if (size !== sizeOpen) return nok(code) + effects.exit('directiveContainerSequence') + return createSpace(effects, closingSequenceEnd, 'whitespace')(code) + } + + function closingSequenceEnd(code: number) { + if (code === null || markdownLineEnding(code)) { + effects.exit('directiveContainerFence') + return ok(code) + } + + return nok(code) + } + } + function detectContainer(effects: Effects, ok: Okay, nok: NotOkay) { + let size = 0 + + return openingSequence + + function openingSequence(code: number) { + if (code === Codes.colon) { + effects.consume(code) + size++ + return openingSequence + } + + if (size < ContainerSequenceSize) return nok(code) + + return openingSequenceEnd + } + + function openingSequenceEnd(code: number) { + if (code === null || markdownLineEnding(code)) { + return nok(code) + } + + // memorize cotainer sequence + containerSequenceSize.push(size) + + return ok(code) + } + } +} + +function tokenizeLabel(effects: Effects, ok: Okay, nok: NotOkay) { + // Always a `[` + return createLabel( + effects, + ok, + nok, + 'directiveContainerLabel', + 'directiveContainerLabelMarker', + 'directiveContainerLabelString', + true + ) +} + +function tokenizeAttributes(effects: Effects, ok: Okay, nok: NotOkay) { + // Always a `{` + return createAttributes( + effects, + ok, + nok, + 'directiveContainerAttributes', + 'directiveContainerAttributesMarker', + 'directiveContainerAttribute', + 'directiveContainerAttributeId', + 'directiveContainerAttributeClass', + 'directiveContainerAttributeName', + 'directiveContainerAttributeInitializerMarker', + 'directiveContainerAttributeValueLiteral', + 'directiveContainerAttributeValue', + 'directiveContainerAttributeValueMarker', + 'directiveContainerAttributeValueData', + true + ) +} + +export default { + tokenize, + concrete: true +} diff --git a/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-text.ts b/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-text.ts new file mode 100644 index 000000000..c575a848e --- /dev/null +++ b/src/core/parser/markdown/directive/micromark-directive/tokenize-directive-text.ts @@ -0,0 +1,82 @@ +import { Effects, Okay, NotOkay } from 'micromark/dist/shared-types' +import createAttributes from './factory-attributes' +import createLabel from './factory-label' +import createName from './factory-name' + +const label: any = { tokenize: tokenizeLabel, partial: true } +const attributes: any = { tokenize: tokenizeAttributes, partial: true } + +function previous(code: number) { + // If there is a previous code, there will always be a tail. + return code !== 58 /* `:` */ || this.events[this.events.length - 1][1].type === 'characterEscape' +} + +function tokenize(effects: Effects, ok: Okay, nok: NotOkay) { + const self = this + + return start + + function start(code: number) { + /* istanbul ignore if - handled by mm */ + if (code !== 58 /* `:` */) throw new Error('expected `:`') + + /* istanbul ignore if - handled by mm */ + if (!previous.call(self, self.previous)) { + throw new Error('expected correct previous') + } + + effects.enter('directiveText') + effects.enter('directiveTextMarker') + effects.consume(code) + effects.exit('directiveTextMarker') + return createName.call(self, effects, afterName, nok, 'directiveTextName') + } + + function afterName(code: number) { + if (code === 58 /* `:` */) { + return nok(code) + } + return code === 91 /* `[` */ ? effects.attempt(label, afterLabel, afterLabel)(code) : afterLabel(code) + } + + function afterLabel(code: number) { + return code === 123 /* `{` */ + ? effects.attempt(attributes, afterAttributes, afterAttributes)(code) + : afterAttributes(code) + } + + function afterAttributes(code: number) { + effects.exit('directiveText') + return ok(code) + } +} + +function tokenizeLabel(effects: Effects, ok: Okay, nok: NotOkay) { + // Always a `[` + return createLabel(effects, ok, nok, 'directiveTextLabel', 'directiveTextLabelMarker', 'directiveTextLabelString') +} + +function tokenizeAttributes(effects: Effects, ok: Okay, nok: NotOkay) { + // Always a `{` + return createAttributes( + effects, + ok, + nok, + 'directiveTextAttributes', + 'directiveTextAttributesMarker', + 'directiveTextAttribute', + 'directiveTextAttributeId', + 'directiveTextAttributeClass', + 'directiveTextAttributeName', + 'directiveTextAttributeInitializerMarker', + 'directiveTextAttributeValueLiteral', + 'directiveTextAttributeValue', + 'directiveTextAttributeValueMarker', + 'directiveTextAttributeValueData' + ) +} + +export default { + tokenize, + previous +} diff --git a/src/core/parser/markdown/directive/remark-directive/from-markdown.ts b/src/core/parser/markdown/directive/remark-directive/from-markdown.ts new file mode 100644 index 000000000..2a8d2a7ba --- /dev/null +++ b/src/core/parser/markdown/directive/remark-directive/from-markdown.ts @@ -0,0 +1,194 @@ +import { Token } from 'micromark/dist/shared-types' +import decode from 'parse-entities/decode-entity' + +const canContainEols = ['textDirective'] +const enter = { + directiveContainer: enterContainer, + directiveContainerSection: enterContainerSection, + directiveContainerAttributes: enterAttributes, + directiveContainerLabel: enterContainerLabel, + + directiveLeaf: enterLeaf, + directiveLeafAttributes: enterAttributes, + + directiveText: enterText, + directiveTextAttributes: enterAttributes +} +const exit = { + directiveContainerSectionTitle: exitContainerSectionTitle, + listUnordered: conditionalExit, + listOrdered: conditionalExit, + listItem: conditionalExit, + directiveContainerSection: exitContainerSection, + directiveContainer: exitContainer, + directiveContainerAttributeClassValue: exitAttributeClassValue, + directiveContainerAttributeIdValue: exitAttributeIdValue, + directiveContainerAttributeName: exitAttributeName, + directiveContainerAttributeValue: exitAttributeValue, + directiveContainerAttributes: exitAttributes, + directiveContainerLabel: exitContainerLabel, + directiveContainerName: exitName, + + directiveLeaf: exitToken, + directiveLeafAttributeClassValue: exitAttributeClassValue, + directiveLeafAttributeIdValue: exitAttributeIdValue, + directiveLeafAttributeName: exitAttributeName, + directiveLeafAttributeValue: exitAttributeValue, + directiveLeafAttributes: exitAttributes, + directiveLeafName: exitName, + + directiveText: exitToken, + directiveTextAttributeClassValue: exitAttributeClassValue, + directiveTextAttributeIdValue: exitAttributeIdValue, + directiveTextAttributeName: exitAttributeName, + directiveTextAttributeValue: exitAttributeValue, + directiveTextAttributes: exitAttributes, + directiveTextName: exitName +} + +function enterContainer(token: Token) { + enterToken.call(this, 'containerDirective', token) +} + +function exitContainer(token: Token) { + const container = this.stack[this.stack.length - 1] + if (container.children.length > 1) { + const dataSection = container.children.shift() + container.rawData = dataSection.raw + } + + container.children = container.children.flatMap(child => { + if (child.name === 'default' || !child.name) { + return child.children + } + child.data = { + hName: 'directive-slot', + hProperties: { + ...child.attributes, + [`v-slot:${child.name}`]: '' + } + } + return child + }) + + this.exit(token) +} + +function enterContainerSection(token: Token) { + enterToken.call(this, 'directiveContainerSection', token) +} + +function exitContainerSection(token: Token) { + let section = this.stack[this.stack.length - 1] + /** + * Ensure lists and list-items are closed before closing section + * This issue occurs because `---` separtors ar conflict with markdown lists + */ + while (section.type === 'listItem' || section.type === 'list') { + this.exit(this.tokenStack[this.tokenStack.length - 1]) + section = this.stack[this.stack.length - 1] + } + + if (section.type === 'directiveContainerSection') { + section.raw = this.sliceSerialize(token) + this.exit(token) + } +} + +function exitContainerSectionTitle(token: Token) { + this.stack[this.stack.length - 1].name = this.sliceSerialize(token) +} + +function enterLeaf(token: Token) { + enterToken.call(this, 'leafDirective', token) +} + +function enterText(token: Token) { + enterToken.call(this, 'textDirective', token) +} + +function enterToken(type, token) { + this.enter({ type, name: '', attributes: {}, children: [] }, token) +} + +function exitName(token: Token) { + this.stack[this.stack.length - 1].name = this.sliceSerialize(token) +} + +function enterContainerLabel(token: Token) { + this.enter({ type: 'paragraph', data: { directiveLabel: true }, children: [] }, token) +} + +function exitContainerLabel(token: Token) { + this.exit(token) +} + +function enterAttributes() { + this.setData('directiveAttributes', []) + this.buffer() // Capture EOLs +} + +function exitAttributeIdValue(token: Token) { + this.getData('directiveAttributes').push(['id', decodeLight(this.sliceSerialize(token))]) +} + +function exitAttributeClassValue(token: Token) { + this.getData('directiveAttributes').push(['class', decodeLight(this.sliceSerialize(token))]) +} + +function exitAttributeValue(token: Token) { + const attributes = this.getData('directiveAttributes') + attributes[attributes.length - 1][1] = decodeLight(this.sliceSerialize(token)) +} + +function exitAttributeName(token: Token) { + // Attribute names in CommonMark are significantly limited, so character + // references can’t exist. + this.getData('directiveAttributes').push([this.sliceSerialize(token), '']) +} + +function exitAttributes() { + const attributes = this.getData('directiveAttributes') + const cleaned: any = {} + let index = -1 + let attribute + + while (++index < attributes.length) { + attribute = attributes[index] + + if (attribute[0] === 'class' && cleaned.class) { + cleaned.class += ' ' + attribute[1] + } else { + cleaned[attribute[0]] = attribute[1] + } + } + + this.setData('directiveAttributes') + this.resume() // Drop EOLs + this.stack[this.stack.length - 1].attributes = cleaned +} + +function exitToken(token: Token) { + this.exit(token) +} + +function conditionalExit(token: Token) { + const section: Token = this.tokenStack[this.tokenStack.length - 1] + if (section.type === token.type) { + this.exit(token) + } +} + +function decodeLight(value: string) { + return value.replace(/&(#(\d{1,7}|x[\da-f]{1,6})|[\da-z]{1,31});/gi, decodeIfPossible) +} + +function decodeIfPossible($0: string, $1: string) { + return decode($1) || $0 +} + +export default { + canContainEols, + enter, + exit +} diff --git a/src/core/parser/markdown/directive/remark-directive/to-markdown.ts b/src/core/parser/markdown/directive/remark-directive/to-markdown.ts new file mode 100644 index 000000000..cace7e903 --- /dev/null +++ b/src/core/parser/markdown/directive/remark-directive/to-markdown.ts @@ -0,0 +1,171 @@ +import repeatString from 'repeat-string' +import encode from 'stringify-entities/light' +import visit from 'unist-util-visit-parents' +import flow from 'mdast-util-to-markdown/lib/util/container-flow' +import phrasing from 'mdast-util-to-markdown/lib/util/container-phrasing' +import checkQuote from 'mdast-util-to-markdown/lib/util/check-quote' + +const own = {}.hasOwnProperty + +const shortcut = /^[^\t\n\r "#'.<=>`}]+$/ + +// TODO: convert container sections to markdown +const unsafe = [ + { + character: '\r', + inConstruct: ['leafDirectiveLabel', 'containerDirectiveLabel'] + }, + { + character: '\n', + inConstruct: ['leafDirectiveLabel', 'containerDirectiveLabel'] + }, + { + before: '[^:]', + character: ':', + after: '[A-Za-z]', + inConstruct: ['phrasing'] + }, + { atBreak: true, character: ':', after: ':' } +] + +const handlers = { + containerDirective: handleDirective, + leafDirective: handleDirective, + textDirective: handleDirective +} + +handleDirective.peek = peekDirective + +function handleDirective(node, _, context) { + const prefix = fence(node) + const exit = context.enter(node.type) + let value = prefix + (node.name || '') + label(node, context) + attributes(node, context) + let subvalue + + if (node.type === 'containerDirective') { + subvalue = content(node, context) + if (subvalue) value += '\n' + subvalue + value += '\n' + prefix + } + + exit() + return value +} + +function peekDirective() { + return ':' +} + +function label(node, context) { + let label = node + + if (node.type === 'containerDirective') { + if (!inlineDirectiveLabel(node)) return '' + label = node.children[0] + } + + const exit = context.enter('label') + const subexit = context.enter(node.type + 'Label') + const value = phrasing(label, context, { before: '[', after: ']' }) + subexit() + exit() + return value ? '[' + value + ']' : '' +} + +function attributes(node, context) { + const quote = checkQuote(context) + const subset = node.type === 'textDirective' ? [quote] : [quote, '\n', '\r'] + const attrs = node.attributes || {} + const values = [] + let id + let classesFull + let classes + let value + let key + let index + + for (key in attrs) { + if (own.call(attrs, key) && attrs[key] != null) { + value = String(attrs[key]) + + if (key === 'id') { + id = shortcut.test(value) ? '#' + value : quoted('id', value) + } else if (key === 'class') { + value = value.split(/[\t\n\r ]+/g) + classesFull = [] + classes = [] + index = -1 + + while (++index < value.length) { + ;(shortcut.test(value[index]) ? classes : classesFull).push(value[index]) + } + + classesFull = classesFull.length ? quoted('class', classesFull.join(' ')) : '' + classes = classes.length ? '.' + classes.join('.') : '' + } else { + values.push(quoted(key, value)) + } + } + } + + if (classesFull) { + values.unshift(classesFull) + } + + if (classes) { + values.unshift(classes) + } + + if (id) { + values.unshift(id) + } + + return values.length ? '{' + values.join(' ') + '}' : '' + + function quoted(key, value) { + return key + (value ? '=' + quote + encode(value, { subset }) + quote : '') + } +} + +function content(node, context) { + const content = inlineDirectiveLabel(node) ? Object.assign({}, node, { children: node.children.slice(1) }) : node + + return flow(content, context) +} + +function inlineDirectiveLabel(node) { + return node.children && node.children[0] && node.children[0].data && node.children[0].data.directiveLabel +} + +function fence(node) { + let size = 0 + + if (node.type === 'containerDirective') { + visit(node, 'containerDirective', onvisit) + size += 3 + } else if (node.type === 'leafDirective') { + size = 2 + } else { + size = 1 + } + + return repeatString(':', size) + + function onvisit(_node, parents) { + let index = parents.length + let nesting = 0 + + while (index--) { + if (parents[index].type === 'containerDirective') { + nesting++ + } + } + + if (nesting > size) size = nesting + } +} + +export default { + handlers, + unsafe +} diff --git a/src/core/parser/markdown/plugin/directive.ts b/src/core/parser/markdown/directive/remark-plugin.ts similarity index 52% rename from src/core/parser/markdown/plugin/directive.ts rename to src/core/parser/markdown/directive/remark-plugin.ts index 1a94d312b..f6a9ea1c7 100644 --- a/src/core/parser/markdown/plugin/directive.ts +++ b/src/core/parser/markdown/directive/remark-plugin.ts @@ -1,19 +1,21 @@ import visit from 'unist-util-visit' import h from 'hastscript' -import { useMarkdownParser } from '../' +import { useMarkdownParser } from '..' const toFrontMatter = (yamlString: string) => `--- ${yamlString} ---` -export default function htmlDirectives({ directives, dataComponents }) { +export default function htmlDirectives({ directives }) { const parser = useMarkdownParser() - function toData(raw) { - const lines = raw.split('\n') - const markdown = lines.slice(1, lines.length - 1).join('\n') + function getNodeData(node) { + if (!node.rawData) { + return {} + } - const { data } = parser.parseFrontMatter(toFrontMatter(markdown)) + const yaml = node.rawData + const { data } = parser.parseFrontMatter(toFrontMatter(yaml)) return data } @@ -31,29 +33,25 @@ export default function htmlDirectives({ directives, dataComponents }) { return Object.fromEntries(enteries) } - return async (tree, { data: pageData, contents }) => { + return async (tree, { data: pageData }) => { const jobs = [] visit(tree, ['textDirective', 'leafDirective', 'containerDirective'], visitor) function visitor(node) { const directive = directives[node.name] const data = node.data || (node.data = {}) - const hast = h(node.name, node.attributes) - - if (dataComponents.includes(node.name) || typeof node.attributes.yml !== 'undefined') { - const { start, end } = node.position - hast.properties = bindData( - { - ...hast.properties, - ...toData(contents.substr(start.offset, end.offset - start.offset)) - }, - pageData - ) - } - - data.hName = hast.tagName - data.hProperties = hast.properties + // parse data slots and retrive data + const nodeData = getNodeData(node) + + data.hName = node.name + data.hProperties = bindData( + { + ...node.attributes, + ...nodeData + }, + pageData + ) if (directive) { jobs.push(directive(node, pageData)) } diff --git a/src/core/parser/markdown/index.ts b/src/core/parser/markdown/index.ts index 706267032..c1a1b56d3 100644 --- a/src/core/parser/markdown/index.ts +++ b/src/core/parser/markdown/index.ts @@ -15,10 +15,9 @@ const DEFAULTS: MarkdownParserOptions = { directives: { props: propsDirective }, - dataComponents: ['video-player', 'block-hero', 'block-features'], remarkPlugins: [ + resolve(__dirname, './directive'), 'remark-emoji', - 'remark-directive', 'remark-squeeze-paragraphs', 'remark-slug', ['remark-autolink-headings', { behavior: 'wrap' }], @@ -72,10 +71,7 @@ async function parse(file, options) { export function useMarkdownParser(options: Partial = {}) { options = defu(options, DEFAULTS) - options.remarkPlugins.unshift([ - resolve(__dirname, './plugin/directive'), - { directives: options.directives, dataComponents: options.dataComponents } - ]) + options.remarkPlugins.unshift([resolve(__dirname, './directive/remark-plugin'), { directives: options.directives }]) processOptions(options) return { diff --git a/src/core/parser/markdown/plugin/remark-prose.ts b/src/core/parser/markdown/plugin/remark-prose.ts deleted file mode 100644 index 08892fb25..000000000 --- a/src/core/parser/markdown/plugin/remark-prose.ts +++ /dev/null @@ -1,53 +0,0 @@ -const TAG_REGEX = /^\s*<\/?([A-Za-z0-9-_]+) ?[^>]*>/ -const PROSE_ELEMENTS = [ - // HTML tags - 'div', - 'p', - 'ul', - - // Global tags - 'props', - 'Props' -] - -const isJsNode = (node, customProsElements = []) => { - let match - if (node.type === 'containerDirective') { - return !PROSE_ELEMENTS.includes(node.name) && !customProsElements.includes(node.name) - } - if (node.type === 'html') { - match = node.value.match(TAG_REGEX) - } - if (!match && node.children && node.children[0] && node.children[0].type === 'html') { - match = node.children[0].value.match(TAG_REGEX) - } - return ( - match && - !PROSE_ELEMENTS.includes(match[1]) && // ensure tag is not a valid prose tag - !customProsElements.includes(match[1]) - ) -} - -export default ({ prosElements = [], proseClass = 'prose' }) => { - return tree => { - let insideProse = false - tree.children = tree.children.flatMap((node, i) => { - if (insideProse && isJsNode(node, prosElements)) { - insideProse = false - return [{ type: 'html', value: '' }, node] - } - if (!insideProse && !isJsNode(node, prosElements)) { - insideProse = true - return [ - { type: 'html', value: `
` }, - node, - ...(i === tree.children.length - 1 ? [{ type: 'html', value: '
' }] : []) - ] - } - if (i === tree.children.length - 1 && insideProse) { - return [node, { type: 'html', value: '' }] - } - return [node] - }) - } -} diff --git a/src/core/runtime/utils.ts b/src/core/runtime/utils.ts index b1741b9ec..192384c4f 100644 --- a/src/core/runtime/utils.ts +++ b/src/core/runtime/utils.ts @@ -17,11 +17,15 @@ export const expandTags = (_tags: string[]) => _tags.flatMap(t => TAGS_MAP[t]) export const Markdown = { functional: true, render: (_h, ctx) => { - let node = ctx.props.node + const slot = ctx.props.slot || 'default' + let node = ctx.props.node || ctx.parent.$scopedSlots[slot] || ctx.parent.$slots[slot] + if (typeof node === 'function') { + node = node() + } if (typeof node === 'string') { return [node] } - if (ctx.props.unwrap) { + if (node && ctx.props.unwrap) { const tags = ctx.props.unwrap.split(/[,\s]/) node = flatUnwrap(node, tags) } diff --git a/src/defaultTheme/components/atoms/Alert.vue b/src/defaultTheme/components/atoms/Alert.vue index e97e82b73..c96842e31 100644 --- a/src/defaultTheme/components/atoms/Alert.vue +++ b/src/defaultTheme/components/atoms/Alert.vue @@ -2,7 +2,7 @@
- +
diff --git a/src/defaultTheme/components/atoms/ButtonLink.vue b/src/defaultTheme/components/atoms/ButtonLink.vue index fbf8583d5..c5285ddd9 100644 --- a/src/defaultTheme/components/atoms/ButtonLink.vue +++ b/src/defaultTheme/components/atoms/ButtonLink.vue @@ -1,17 +1,17 @@ diff --git a/src/defaultTheme/components/organisms/BlockHero.vue b/src/defaultTheme/components/organisms/BlockHero.vue index 728d16246..8e5e860aa 100644 --- a/src/defaultTheme/components/organisms/BlockHero.vue +++ b/src/defaultTheme/components/organisms/BlockHero.vue @@ -18,7 +18,7 @@ sm:mb-8 " > - {{ title }} +

- {{ description }} +

@@ -73,8 +73,10 @@