@void/md 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -0
- package/dist/compile-BRZW8ppH.mjs +333 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +2 -0
- package/dist/plugin-BpyOmjmC.d.mts +56 -0
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.mjs +703 -0
- package/dist/runtime/index.d.mts +6 -0
- package/dist/runtime/index.mjs +8 -0
- package/dist/theme/code.css +188 -0
- package/dist/theme/containers.css +125 -0
- package/dist/theme/content.css +4 -0
- package/dist/theme/index.css +7 -0
- package/dist/theme/prose.css +294 -0
- package/dist/theme/reset.css +107 -0
- package/package.json +60 -0
- package/pages.d.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @void/md
|
|
2
|
+
|
|
3
|
+
Markdown pages plugin for Void — `.md` files as first-class routes with zero client JS, islands support, and Shiki syntax highlighting.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// vite.config.ts
|
|
9
|
+
import { defineConfig } from 'vite';
|
|
10
|
+
import { voidPlugin } from 'void';
|
|
11
|
+
import { voidVue } from '@void/vue/plugin';
|
|
12
|
+
import { voidMarkdown } from '@void/md/plugin';
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
plugins: [voidPlugin(), voidVue(), voidMarkdown()],
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Works with any framework adapter — auto-detects Vue, React, Svelte, or Solid from the plugin list.
|
|
20
|
+
|
|
21
|
+
## Plugin
|
|
22
|
+
|
|
23
|
+
`voidMarkdown(options?)` returns a Vite plugin array (`Plugin[]`). The plugin runs with `enforce: "pre"`.
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
|
|
27
|
+
- `shiki?.themes` — `{ light: string; dark: string }` (default: `github-light` / `github-dark`)
|
|
28
|
+
- `shiki?.langs` — additional Shiki languages to load (default: all bundled languages, lazy-loaded)
|
|
29
|
+
|
|
30
|
+
The plugin exposes an API:
|
|
31
|
+
|
|
32
|
+
- `api.getClientScripts()` — returns `Map<string, string>` of component ID → client code (used by the islands plugin)
|
|
33
|
+
|
|
34
|
+
## Runtime
|
|
35
|
+
|
|
36
|
+
Import from `@void/md`:
|
|
37
|
+
|
|
38
|
+
- **`useFrontmatter()`** — access the current page's frontmatter object. Uses framework-native context (`provide`/`inject` for Vue, `createContext`/`useContext` for React/Solid, `setContext`/`getContext` for Svelte).
|
|
39
|
+
|
|
40
|
+
## Virtual Modules
|
|
41
|
+
|
|
42
|
+
- **`@void/md`** — resolves to `useFrontmatter()` runtime
|
|
43
|
+
- **`@void/md/pages`** — flat array of all `MdPage` objects (HMR-aware), useful for building navs, sidebars, and search indexes
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import pages from '@void/md/pages';
|
|
47
|
+
// MdPage { path, title, frontmatter, headings }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Pages
|
|
51
|
+
|
|
52
|
+
`.md` files in the `pages/` directory become routes. They compile to static HTML at build time with zero client JS by default — auto-prerendered like static island pages.
|
|
53
|
+
|
|
54
|
+
```md
|
|
55
|
+
---
|
|
56
|
+
title: Getting Started
|
|
57
|
+
author: Jane
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# Getting Started
|
|
61
|
+
|
|
62
|
+
Regular markdown content with **full** syntax support.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Frontmatter
|
|
66
|
+
|
|
67
|
+
YAML frontmatter between `---` fences. Title is extracted from `frontmatter.title` or the first `<h1>`.
|
|
68
|
+
|
|
69
|
+
### Islands
|
|
70
|
+
|
|
71
|
+
Embed interactive components via a `<script>` block with `island` import attributes:
|
|
72
|
+
|
|
73
|
+
```md
|
|
74
|
+
---
|
|
75
|
+
title: Interactive Page
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
<script>
|
|
79
|
+
import Counter from "../components/Counter.vue" with { island: "visible" }
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
# Page with Islands
|
|
83
|
+
|
|
84
|
+
Static content above, interactive island below:
|
|
85
|
+
|
|
86
|
+
<Counter />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Island strategies: `idle` (default), `visible`, `media(...)`.
|
|
90
|
+
|
|
91
|
+
### Client Scripts
|
|
92
|
+
|
|
93
|
+
Regular imports and statements in `<script>` blocks (without `island` attribute) are bundled as a client module, code-split per page:
|
|
94
|
+
|
|
95
|
+
```md
|
|
96
|
+
<script>
|
|
97
|
+
import { initAnalytics } from "../lib/analytics"
|
|
98
|
+
initAnalytics()
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
# My Page
|
|
102
|
+
|
|
103
|
+
Content here.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Markdown Features
|
|
107
|
+
|
|
108
|
+
Full VitePress-parity feature set:
|
|
109
|
+
|
|
110
|
+
- **Syntax highlighting** — Shiki with dual light/dark themes
|
|
111
|
+
- **Line highlighting** — `{1,3-5}` meta in code fences
|
|
112
|
+
- **Notation transforms** — `[!code ++]`, `[!code --]`, `[!code highlight]`, `[!code focus]`, `[!code error]`, `[!code warning]`
|
|
113
|
+
- **Line numbers** — `:line-numbers` / `:no-line-numbers` per fence, `{=N}` start line
|
|
114
|
+
- **Code snippet imports** — `<<< ./file.ext` with region and line range support
|
|
115
|
+
- **Custom containers** — `:::tip`, `:::info`, `:::warning`, `:::danger`, `:::details`
|
|
116
|
+
- **GitHub alerts** — `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!IMPORTANT]`, `> [!CAUTION]`
|
|
117
|
+
- **Emoji** — `:emoji_name:` shortcodes
|
|
118
|
+
- **Attributes** — `{.class #id}` on elements via `markdown-it-attrs`
|
|
119
|
+
- **Anchor links** — auto-generated heading anchors
|
|
120
|
+
- **Table of contents** — `[[toc]]` directive
|
|
121
|
+
- **Image lazy loading** — automatic `loading="lazy"` on images
|
|
122
|
+
- **Link handling** — external links get `target="_blank"`, internal `.md` links normalized
|
|
123
|
+
|
|
124
|
+
## Theme
|
|
125
|
+
|
|
126
|
+
Two CSS exports for styling markdown content:
|
|
127
|
+
|
|
128
|
+
- **`@void/md/theme.css`** — full standalone theme (reset + prose + code + containers)
|
|
129
|
+
- **`@void/md/theme-content.css`** — content-only styles scoped to `.void-md` (prose + code + containers)
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// In your layout or entry
|
|
133
|
+
import '@void/md/theme.css'; // or theme-content.css
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Architecture
|
|
137
|
+
|
|
138
|
+
- **Compilation pipeline** — `extractScript()` strips `<script>` blocks and collects island imports → `compile()` runs gray-matter + markdown-exit + Shiki → `emit-{framework}()` generates a framework-specific virtual component
|
|
139
|
+
- **Framework-agnostic** — adapter auto-detected from plugin list; emitters for Vue (SFC), React (JSX), Svelte, and Solid
|
|
140
|
+
- **Lightweight metadata extraction** — `extractMetadata()` parses frontmatter + headings without running Shiki, used for `@void/md/pages` scanning
|
|
141
|
+
- **Islands integration** — pages with island imports split HTML into render chunks with `data-island` markers; pages without islands emit static HTML with a frontmatter context provider
|
|
142
|
+
- **Svelte/Solid secondary modules** — emits a JS re-export pointing to a secondary virtual module (`/@void-md-svelte/` or `/@void-md-solid/`) so Vite processes through the framework's own plugin
|
|
143
|
+
|
|
144
|
+
## Tests
|
|
145
|
+
|
|
146
|
+
- `test/unit/compile.test.ts` — compilation pipeline: basic, frontmatter, title extraction, headings, Shiki, emoji, anchors
|
|
147
|
+
- `test/unit/extract-script.test.ts` — script extraction: island imports, client code, edge cases
|
|
148
|
+
- `test/unit/plugins.test.ts` — markdown-it plugins: containers, GitHub alerts, links, images, line numbers, emoji, TOC
|
|
149
|
+
- `test/unit/emit-vue.test.ts` — Vue component emission with and without islands
|
|
150
|
+
- `test/unit/emit-react.test.ts` — React component emission with and without islands
|
|
151
|
+
- `test/unit/emit-svelte.test.ts` — Svelte component emission with and without islands
|
|
152
|
+
- `test/unit/emit-solid.test.ts` — Solid component emission with and without islands
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import matter from "gray-matter";
|
|
2
|
+
import MarkdownExit from "markdown-exit";
|
|
3
|
+
import anchor from "markdown-it-anchor";
|
|
4
|
+
import { createJavaScriptRegexEngine } from "@shikijs/engine-javascript";
|
|
5
|
+
import { transformerMetaHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight } from "@shikijs/transformers";
|
|
6
|
+
import attrs from "markdown-it-attrs";
|
|
7
|
+
import { full } from "markdown-it-emoji";
|
|
8
|
+
import { bundledLanguages, createHighlighter, isSpecialLang } from "shiki";
|
|
9
|
+
import container from "markdown-it-container";
|
|
10
|
+
//#region src/plugins/containers.ts
|
|
11
|
+
function createContainer(md, klass, defaultTitle) {
|
|
12
|
+
return [
|
|
13
|
+
container,
|
|
14
|
+
klass,
|
|
15
|
+
{ render(tokens, idx, _options, env) {
|
|
16
|
+
const token = tokens[idx];
|
|
17
|
+
if (token.nesting === 1) {
|
|
18
|
+
token.attrJoin("class", `${klass} custom-block`);
|
|
19
|
+
const attrs = md.renderer.renderAttrs(token);
|
|
20
|
+
const info = token.info.trim().slice(klass.length).trim();
|
|
21
|
+
const title = md.renderInline(info || defaultTitle, { references: env.references });
|
|
22
|
+
const titleClass = "custom-block-title" + (info ? "" : " custom-block-title-default");
|
|
23
|
+
if (klass === "details") return `<details${attrs}><summary>${title}</summary>\n`;
|
|
24
|
+
return `<div${attrs}><p class="${titleClass}">${title}</p>\n`;
|
|
25
|
+
}
|
|
26
|
+
return klass === "details" ? `</details>\n` : `</div>\n`;
|
|
27
|
+
} }
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Custom container blocks: :::tip, :::info, :::warning, :::danger, :::details
|
|
32
|
+
* Ported from VitePress.
|
|
33
|
+
*/
|
|
34
|
+
function containerPlugin(md, options) {
|
|
35
|
+
md.use(...createContainer(md, "tip", options?.tipLabel ?? "TIP")).use(...createContainer(md, "info", options?.infoLabel ?? "INFO")).use(...createContainer(md, "warning", options?.warningLabel ?? "WARNING")).use(...createContainer(md, "danger", options?.dangerLabel ?? "DANGER")).use(...createContainer(md, "details", options?.detailsLabel ?? "Details"));
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/plugins/github-alerts.ts
|
|
39
|
+
const markerRE = /^\[!(TIP|NOTE|INFO|IMPORTANT|WARNING|CAUTION|DANGER)\]([^\n\r]*)/i;
|
|
40
|
+
function capitalize(str) {
|
|
41
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* GitHub-flavored alerts: > [!NOTE], > [!TIP], > [!WARNING], etc.
|
|
45
|
+
* Ported from VitePress.
|
|
46
|
+
*/
|
|
47
|
+
function gitHubAlertsPlugin(md, options) {
|
|
48
|
+
const titleMark = {
|
|
49
|
+
tip: options?.tipLabel ?? "TIP",
|
|
50
|
+
note: options?.noteLabel ?? "NOTE",
|
|
51
|
+
info: options?.infoLabel ?? "INFO",
|
|
52
|
+
important: options?.importantLabel ?? "IMPORTANT",
|
|
53
|
+
warning: options?.warningLabel ?? "WARNING",
|
|
54
|
+
caution: options?.cautionLabel ?? "CAUTION",
|
|
55
|
+
danger: options?.dangerLabel ?? "DANGER"
|
|
56
|
+
};
|
|
57
|
+
const rule = (state) => {
|
|
58
|
+
const tokens = state.tokens;
|
|
59
|
+
for (let i = 0; i < tokens.length; i++) if (tokens[i].type === "blockquote_open") {
|
|
60
|
+
const startIndex = i;
|
|
61
|
+
const open = tokens[startIndex];
|
|
62
|
+
let endIndex = i + 1;
|
|
63
|
+
while (endIndex < tokens.length && (tokens[endIndex].type !== "blockquote_close" || tokens[endIndex].level !== open.level)) endIndex++;
|
|
64
|
+
if (endIndex === tokens.length) continue;
|
|
65
|
+
const close = tokens[endIndex];
|
|
66
|
+
const firstContent = tokens.slice(startIndex, endIndex + 1).find((token) => token.type === "inline");
|
|
67
|
+
if (!firstContent) continue;
|
|
68
|
+
const match = firstContent.content.match(markerRE);
|
|
69
|
+
if (!match) continue;
|
|
70
|
+
const type = match[1].toLowerCase();
|
|
71
|
+
const title = match[2].trim() || titleMark[type] || capitalize(type);
|
|
72
|
+
firstContent.content = firstContent.content.slice(match[0].length).trimStart();
|
|
73
|
+
open.type = "github_alert_open";
|
|
74
|
+
open.tag = "div";
|
|
75
|
+
open.meta = {
|
|
76
|
+
title,
|
|
77
|
+
type
|
|
78
|
+
};
|
|
79
|
+
close.type = "github_alert_close";
|
|
80
|
+
close.tag = "div";
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
md.core.ruler.after("block", "github-alerts", rule);
|
|
84
|
+
md.renderer.rules.github_alert_open = function(tokens, idx) {
|
|
85
|
+
const { title, type } = tokens[idx].meta;
|
|
86
|
+
return `<div class="${type} custom-block github-alert"><p class="custom-block-title">${title}</p>\n`;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/plugins/image.ts
|
|
91
|
+
const EXTERNAL_URL_RE$1 = /^[a-z]+:/i;
|
|
92
|
+
/**
|
|
93
|
+
* Normalizes relative image paths and optionally adds loading="lazy".
|
|
94
|
+
* Ported from VitePress.
|
|
95
|
+
*/
|
|
96
|
+
function imagePlugin(md, options) {
|
|
97
|
+
const imageRule = md.renderer.rules.image;
|
|
98
|
+
md.renderer.rules.image = (tokens, idx, mdOptions, env, self) => {
|
|
99
|
+
const token = tokens[idx];
|
|
100
|
+
let url = token.attrGet("src");
|
|
101
|
+
if (url && !EXTERNAL_URL_RE$1.test(url)) {
|
|
102
|
+
if (!/^\.?\//.test(url)) url = "./" + url;
|
|
103
|
+
token.attrSet("src", decodeURIComponent(url));
|
|
104
|
+
}
|
|
105
|
+
if (options?.lazyLoading) token.attrSet("loading", "lazy");
|
|
106
|
+
return imageRule(tokens, idx, mdOptions, env, self);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/plugins/line-numbers.ts
|
|
111
|
+
/**
|
|
112
|
+
* Adds line number gutter to code blocks.
|
|
113
|
+
* Supports `:line-numbers` / `:no-line-numbers` per-block control and `{=N}` start line.
|
|
114
|
+
* Ported from VitePress.
|
|
115
|
+
*/
|
|
116
|
+
function lineNumberPlugin(md, enable = false) {
|
|
117
|
+
const fence = md.renderer.rules.fence;
|
|
118
|
+
md.renderer.rules.fence = async (...args) => {
|
|
119
|
+
const rawCode = await fence(...args);
|
|
120
|
+
const [tokens, idx] = args;
|
|
121
|
+
const info = tokens[idx].info;
|
|
122
|
+
if (!enable && !/:line-numbers\b/.test(info) || enable && /:no-line-numbers\b/.test(info)) return rawCode;
|
|
123
|
+
let startLineNumber = 1;
|
|
124
|
+
const matchStart = info.match(/=(\d+)/);
|
|
125
|
+
if (matchStart?.[1]) startLineNumber = parseInt(matchStart[1]);
|
|
126
|
+
const lines = rawCode.slice(rawCode.indexOf("<code>"), rawCode.indexOf("</code>")).split("\n");
|
|
127
|
+
const lineNumbersWrapper = `<div class="line-numbers-wrapper" aria-hidden="true">${[...Array(lines.length)].map((_, i) => `<span class="line-number">${i + startLineNumber}</span><br>`).join("")}</div>`;
|
|
128
|
+
return rawCode.replace(/<\/div>$/, `${lineNumbersWrapper}</div>`).replace(/"(language-[^"]*?)"/, "\"$1 line-numbers-mode\"");
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/plugins/link.ts
|
|
133
|
+
const EXTERNAL_URL_RE = /^[a-z]+:/i;
|
|
134
|
+
/**
|
|
135
|
+
* Adds target="_blank" and rel="noreferrer" to external links.
|
|
136
|
+
* Normalizes internal .md links to route paths.
|
|
137
|
+
* Ported from VitePress.
|
|
138
|
+
*/
|
|
139
|
+
function linkPlugin(md, options) {
|
|
140
|
+
const externalAttrs = options?.externalAttrs ?? {
|
|
141
|
+
target: "_blank",
|
|
142
|
+
rel: "noreferrer"
|
|
143
|
+
};
|
|
144
|
+
md.renderer.rules.link_open = (tokens, idx, mdOptions, _env, self) => {
|
|
145
|
+
const token = tokens[idx];
|
|
146
|
+
const hrefIndex = token.attrIndex("href");
|
|
147
|
+
if (hrefIndex < 0) return self.renderToken(tokens, idx, mdOptions);
|
|
148
|
+
const url = token.attrs?.[hrefIndex]?.[1] ?? "";
|
|
149
|
+
if (url.startsWith("#") || token.attrGet("class") === "header-anchor") return self.renderToken(tokens, idx, mdOptions);
|
|
150
|
+
if (EXTERNAL_URL_RE.test(url)) for (const [key, val] of Object.entries(externalAttrs)) token.attrSet(key, val);
|
|
151
|
+
else {
|
|
152
|
+
let normalized = url;
|
|
153
|
+
if (normalized.endsWith(".md")) normalized = normalized.slice(0, -3);
|
|
154
|
+
else if (normalized.includes(".md#")) normalized = normalized.replace(".md#", "#");
|
|
155
|
+
if (normalized !== url) token.attrs[hrefIndex][1] = normalized;
|
|
156
|
+
}
|
|
157
|
+
return self.renderToken(tokens, idx, mdOptions);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/plugins/pre-wrapper.ts
|
|
162
|
+
function extractLang(info) {
|
|
163
|
+
return /^[a-zA-Z0-9-_]+/.exec(info)?.[0].replace(/-vue$/, "").replace(/^vue-html$/, "template").replace(/^ansi$/, "") || "";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Wraps fenced code blocks with a language label and copy button.
|
|
167
|
+
* Ported from VitePress.
|
|
168
|
+
*/
|
|
169
|
+
function preWrapperPlugin(md, options) {
|
|
170
|
+
const title = options?.codeCopyButtonTitle ?? "Copy Code";
|
|
171
|
+
const fence = md.renderer.rules.fence;
|
|
172
|
+
md.renderer.rules.fence = async (...args) => {
|
|
173
|
+
const [tokens, idx] = args;
|
|
174
|
+
const token = tokens[idx];
|
|
175
|
+
token.info = token.info.replace(/\[.*\]/, "");
|
|
176
|
+
const active = / active( |$)/.test(token.info) ? " active" : "";
|
|
177
|
+
token.info = token.info.replace(/ active$/, "").replace(/ active /, " ");
|
|
178
|
+
const lang = extractLang(token.info);
|
|
179
|
+
const highlighted = await fence(...args);
|
|
180
|
+
return `<div class="language-${lang}${active}"><button title="${title}" class="copy"></button><span class="lang">${lang}</span>` + highlighted + `</div>`;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/plugins/toc.ts
|
|
185
|
+
function renderHeaders(headers, listTag) {
|
|
186
|
+
if (headers.length === 0) return "";
|
|
187
|
+
let result = `<${listTag}>`;
|
|
188
|
+
for (const h of headers) result += `<li><a href="#${h.slug}">${h.text}</a></li>`;
|
|
189
|
+
result += `</${listTag}>`;
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* [[toc]] directive — renders inline table of contents.
|
|
194
|
+
* Ported from VitePress.
|
|
195
|
+
*/
|
|
196
|
+
function tocPlugin(md, options) {
|
|
197
|
+
const pattern = options?.pattern ?? /^\[\[toc\]\]$/i;
|
|
198
|
+
const containerTag = options?.containerTag ?? "nav";
|
|
199
|
+
const containerClass = options?.containerClass ?? "table-of-contents";
|
|
200
|
+
const listTag = options?.listTag ?? "ul";
|
|
201
|
+
const level = options?.level ?? [2, 3];
|
|
202
|
+
md.block.ruler.before("heading", "toc", ((state, startLine, _endLine, silent) => {
|
|
203
|
+
const pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
204
|
+
const max = state.eMarks[startLine];
|
|
205
|
+
const content = state.src.slice(pos, max).trim();
|
|
206
|
+
if (!pattern.test(content)) return false;
|
|
207
|
+
if (silent) return true;
|
|
208
|
+
state.line = startLine + 1;
|
|
209
|
+
const openToken = state.push("toc_open", containerTag, 1);
|
|
210
|
+
openToken.markup = content;
|
|
211
|
+
openToken.attrSet("class", containerClass);
|
|
212
|
+
const bodyToken = state.push("toc_body", "", 0);
|
|
213
|
+
bodyToken.markup = content;
|
|
214
|
+
const closeToken = state.push("toc_close", containerTag, -1);
|
|
215
|
+
closeToken.markup = content;
|
|
216
|
+
return true;
|
|
217
|
+
}), { alt: [
|
|
218
|
+
"paragraph",
|
|
219
|
+
"reference",
|
|
220
|
+
"blockquote"
|
|
221
|
+
] });
|
|
222
|
+
md.renderer.rules.toc_open = (tokens, idx, options, _env, self) => {
|
|
223
|
+
return self.renderToken(tokens, idx, options);
|
|
224
|
+
};
|
|
225
|
+
md.renderer.rules.toc_body = (tokens) => {
|
|
226
|
+
const headers = [];
|
|
227
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
228
|
+
const token = tokens[i];
|
|
229
|
+
if (token.type === "heading_open") {
|
|
230
|
+
const depth = Number(token.tag.slice(1));
|
|
231
|
+
if (level.includes(depth)) {
|
|
232
|
+
const slug = token.attrGet?.("id") ?? "";
|
|
233
|
+
const text = (tokens[i + 1]?.children?.filter((child) => child.type === "text" || child.type === "code_inline").map((child) => child.content).join("") ?? "").trim();
|
|
234
|
+
headers.push({
|
|
235
|
+
depth,
|
|
236
|
+
slug,
|
|
237
|
+
text
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return renderHeaders(headers, listTag);
|
|
243
|
+
};
|
|
244
|
+
md.renderer.rules.toc_close = (tokens, idx, options, _env, self) => {
|
|
245
|
+
return self.renderToken(tokens, idx, options);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/compile.ts
|
|
250
|
+
let compilerPromise = null;
|
|
251
|
+
async function getCompiler(options) {
|
|
252
|
+
if (compilerPromise) return compilerPromise;
|
|
253
|
+
compilerPromise = createCompiler(options);
|
|
254
|
+
return compilerPromise;
|
|
255
|
+
}
|
|
256
|
+
async function createCompiler(options) {
|
|
257
|
+
const md = new MarkdownExit({
|
|
258
|
+
html: true,
|
|
259
|
+
linkify: true
|
|
260
|
+
});
|
|
261
|
+
const themes = options?.shiki?.themes ?? {
|
|
262
|
+
light: "github-light",
|
|
263
|
+
dark: "github-dark"
|
|
264
|
+
};
|
|
265
|
+
const highlighter = await createHighlighter({
|
|
266
|
+
engine: createJavaScriptRegexEngine(),
|
|
267
|
+
themes: [themes.light, themes.dark],
|
|
268
|
+
langs: []
|
|
269
|
+
});
|
|
270
|
+
const shikiTransformers = [
|
|
271
|
+
transformerMetaHighlight(),
|
|
272
|
+
transformerNotationDiff(),
|
|
273
|
+
transformerNotationHighlight(),
|
|
274
|
+
transformerNotationFocus(),
|
|
275
|
+
transformerNotationErrorLevel()
|
|
276
|
+
];
|
|
277
|
+
md.renderer.rules.fence = async (tokens, idx) => {
|
|
278
|
+
const token = tokens[idx];
|
|
279
|
+
const code = token.content;
|
|
280
|
+
let lang = token.info ? token.info.split(/\s+/g)[0] : "text";
|
|
281
|
+
if (!isSpecialLang(lang) && !highlighter.getLoadedLanguages().includes(lang)) if (lang in bundledLanguages) await highlighter.loadLanguage(lang);
|
|
282
|
+
else lang = "text";
|
|
283
|
+
return highlighter.codeToHtml(code, {
|
|
284
|
+
lang,
|
|
285
|
+
themes,
|
|
286
|
+
defaultColor: "light",
|
|
287
|
+
transformers: shikiTransformers,
|
|
288
|
+
meta: { __raw: token.info }
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
md.use(preWrapperPlugin);
|
|
292
|
+
md.use(containerPlugin);
|
|
293
|
+
md.use(imagePlugin, { lazyLoading: true });
|
|
294
|
+
md.use(linkPlugin);
|
|
295
|
+
md.use(lineNumberPlugin, false);
|
|
296
|
+
md.use(gitHubAlertsPlugin);
|
|
297
|
+
md.use(attrs);
|
|
298
|
+
md.use(full);
|
|
299
|
+
md.use(anchor, { permalink: anchor.permalink.ariaHidden({ placement: "before" }) });
|
|
300
|
+
md.use(tocPlugin);
|
|
301
|
+
return md;
|
|
302
|
+
}
|
|
303
|
+
function extractHeadings(tokens) {
|
|
304
|
+
const headings = [];
|
|
305
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
306
|
+
const token = tokens[i];
|
|
307
|
+
if (token.type === "heading_open") {
|
|
308
|
+
const depth = Number(token.tag.slice(1));
|
|
309
|
+
const slug = token.attrGet?.("id") ?? "";
|
|
310
|
+
const text = (tokens[i + 1]?.children?.filter((child) => child.type === "text" || child.type === "code_inline").map((child) => child.content).join("") ?? "").trim();
|
|
311
|
+
headings.push({
|
|
312
|
+
depth,
|
|
313
|
+
slug,
|
|
314
|
+
text
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return headings;
|
|
319
|
+
}
|
|
320
|
+
async function compile(source, options) {
|
|
321
|
+
const { data: frontmatter, content } = matter(source);
|
|
322
|
+
const compiler = await getCompiler(options);
|
|
323
|
+
const env = {};
|
|
324
|
+
const headings = extractHeadings(compiler.parse(content, env));
|
|
325
|
+
return {
|
|
326
|
+
html: await compiler.renderAsync(content, env),
|
|
327
|
+
frontmatter,
|
|
328
|
+
headings,
|
|
329
|
+
title: frontmatter.title ?? headings.find((h) => h.depth === 1)?.text ?? ""
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
export { compile };
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/compile.d.ts
|
|
4
|
+
interface Heading {
|
|
5
|
+
depth: number;
|
|
6
|
+
slug: string;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
interface CompileResult {
|
|
10
|
+
html: string;
|
|
11
|
+
frontmatter: Record<string, unknown>;
|
|
12
|
+
headings: Array<Heading>;
|
|
13
|
+
title: string;
|
|
14
|
+
}
|
|
15
|
+
interface CompileOptions {
|
|
16
|
+
shiki?: {
|
|
17
|
+
themes?: {
|
|
18
|
+
light: string;
|
|
19
|
+
dark: string;
|
|
20
|
+
};
|
|
21
|
+
langs?: Array<string>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/extract-script.d.ts
|
|
26
|
+
interface IslandImport {
|
|
27
|
+
name: string;
|
|
28
|
+
specifier: string;
|
|
29
|
+
strategy: string;
|
|
30
|
+
/** Root-relative island ID (set during transform when file context is available). */
|
|
31
|
+
islandId?: string;
|
|
32
|
+
}
|
|
33
|
+
interface ScriptExtraction {
|
|
34
|
+
script: string | null;
|
|
35
|
+
body: string;
|
|
36
|
+
islandImports: Array<IslandImport>;
|
|
37
|
+
clientCode: string | null;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/plugin.d.ts
|
|
41
|
+
interface MdPage {
|
|
42
|
+
path: string;
|
|
43
|
+
title: string;
|
|
44
|
+
frontmatter: Record<string, unknown>;
|
|
45
|
+
headings: Array<{
|
|
46
|
+
depth: number;
|
|
47
|
+
slug: string;
|
|
48
|
+
text: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
interface MarkdownOptions {
|
|
52
|
+
shiki?: CompileOptions["shiki"];
|
|
53
|
+
}
|
|
54
|
+
declare function voidMarkdown(options?: MarkdownOptions): Array<Plugin>;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { ScriptExtraction as a, IslandImport as i, MdPage as n, CompileOptions as o, voidMarkdown as r, CompileResult as s, MarkdownOptions as t };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as ScriptExtraction, i as IslandImport, n as MdPage, o as CompileOptions, r as voidMarkdown, s as CompileResult, t as MarkdownOptions } from "./plugin-BpyOmjmC.mjs";
|
|
2
|
+
export { CompileOptions, CompileResult, IslandImport, MarkdownOptions, MdPage, ScriptExtraction, voidMarkdown };
|