rspress-plugin-typst 0.0.0-stage → 0.0.2-alpha.1
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/LICENSE +21 -0
- package/README.md +154 -2
- package/dist/index.d.ts +128 -0
- package/dist/index.js +341 -0
- package/dist/loader.js +244 -0
- package/dist/runtime/index.d.ts +21 -0
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/typst.css +43 -0
- package/package.json +81 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Glimverge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,155 @@
|
|
|
1
|
-
#
|
|
1
|
+
# rspress-plugin-typst
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use [Typst](https://github.com/typst/typst) `.typ` files as [Rspress](https://rspress.rs/) documentation pages.
|
|
4
|
+
|
|
5
|
+
**Docs:** https://glimverge.github.io/rspress-plugin-typst/
|
|
6
|
+
|
|
7
|
+
Typst's experimental HTML export is compiled through [`@myriaddreamin/typst-ts-node-compiler`](https://www.npmjs.com/package/@myriaddreamin/typst-ts-node-compiler), then rendered inside the Rspress theme.
|
|
8
|
+
|
|
9
|
+
> Typst HTML export is still experimental (`--features html`). Behavior may change; do not rely on it for production-critical publishing yet.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add rspress-plugin-typst
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// rspress.config.ts
|
|
21
|
+
import { defineConfig } from '@rspress/core';
|
|
22
|
+
import { pluginTypst } from 'rspress-plugin-typst';
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
root: 'docs',
|
|
26
|
+
plugins: [pluginTypst()],
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add Typst pages next to your Markdown docs:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
docs/
|
|
34
|
+
index.mdx
|
|
35
|
+
guide/
|
|
36
|
+
hello.typ → /guide/hello
|
|
37
|
+
math.typ → /guide/math
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Frontmatter
|
|
41
|
+
|
|
42
|
+
Prefer Typst document metadata:
|
|
43
|
+
|
|
44
|
+
```typ
|
|
45
|
+
#set document(
|
|
46
|
+
title: "Hello Typst",
|
|
47
|
+
description: "Rendered by rspress-plugin-typst",
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Optional Rspress-oriented frontmatter via a labeled metadata block:
|
|
52
|
+
|
|
53
|
+
```typ
|
|
54
|
+
#metadata((
|
|
55
|
+
title: "Hello Typst",
|
|
56
|
+
description: "Rendered by rspress-plugin-typst",
|
|
57
|
+
outline: true,
|
|
58
|
+
)) <frontmatter>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Options
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
pluginTypst({
|
|
65
|
+
extensions: ['.typ'],
|
|
66
|
+
include: ['**/*.typ'],
|
|
67
|
+
exclude: ['**/_*', '**/_*/**'],
|
|
68
|
+
fontPaths: ['assets/fonts'],
|
|
69
|
+
inputs: { site: 'My Docs' }, // → sys.inputs.site
|
|
70
|
+
workspace: undefined, // defaults to docs root
|
|
71
|
+
className: 'rspress-typst',
|
|
72
|
+
routeExtensions: true,
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Option | Default | Description |
|
|
77
|
+
| --- | --- | --- |
|
|
78
|
+
| `extensions` | `['.typ']` | File extensions treated as Typst docs |
|
|
79
|
+
| `include` | all `.typ` under docs root | Globs relative to the docs root |
|
|
80
|
+
| `exclude` | underscore-prefixed files/dirs | Ignore patterns |
|
|
81
|
+
| `fontPaths` | `undefined` | Extra font directories for the compiler |
|
|
82
|
+
| `inputs` | `undefined` | Extra `sys.inputs` string pairs |
|
|
83
|
+
| `workspace` | docs root | Typst workspace / package root |
|
|
84
|
+
| `className` | `'rspress-typst'` | Wrapper class on the rendered body |
|
|
85
|
+
| `routeExtensions` | `true` | Register extensions on `route.extensions` |
|
|
86
|
+
|
|
87
|
+
## How it works
|
|
88
|
+
|
|
89
|
+
1. **`config`** — registers `.typ` on Rspress `route.extensions`.
|
|
90
|
+
2. **`beforeBuild`** — compiles every Typst page once so titles, TOC, and search text are available to `extendPageData` / `modifySearchIndexData`.
|
|
91
|
+
3. **Rspack loader** — transforms each `.typ` route module into a React page that renders the HTML body through `TypstDoc`.
|
|
92
|
+
4. **Compiler** — `@myriaddreamin/typst-ts-node-compiler` embeds Typst via N-API.
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
.typ → (Typst HTML via N-API) → React page → Rspress route
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Documentation site
|
|
99
|
+
|
|
100
|
+
The `playground/` package is this project's docs site (Rspress + Typst examples). It deploys to GitHub Pages on every push to `main` via [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml).
|
|
101
|
+
|
|
102
|
+
Local preview:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pnpm install
|
|
106
|
+
pnpm build
|
|
107
|
+
pnpm playground:dev
|
|
108
|
+
# with production base path:
|
|
109
|
+
pnpm playground:build && pnpm --filter playground preview
|
|
110
|
+
# then open http://localhost:4173/rspress-plugin-typst/
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Enable GitHub Pages (one-time)
|
|
114
|
+
|
|
115
|
+
In the repository settings:
|
|
116
|
+
|
|
117
|
+
1. **Settings → Pages**
|
|
118
|
+
2. **Build and deployment → Source**: GitHub Actions
|
|
119
|
+
|
|
120
|
+
After the first successful `Deploy docs to GitHub Pages` workflow on `main`, the site is available at:
|
|
121
|
+
|
|
122
|
+
https://glimverge.github.io/rspress-plugin-typst/
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pnpm install
|
|
128
|
+
pnpm build
|
|
129
|
+
pnpm test
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Publishing
|
|
133
|
+
|
|
134
|
+
The [Release](.github/workflows/release.yml) workflow follows the
|
|
135
|
+
[`sxzz/workflows`](https://github.com/sxzz/workflows) publish flow (checkout →
|
|
136
|
+
install → changelogithub → build → `pnpm publish`), inlined so every action can
|
|
137
|
+
stay SHA-pinned under this repo’s policy.
|
|
138
|
+
|
|
139
|
+
1. Configure npm trusted publishing for this GitHub repo (OIDC).
|
|
140
|
+
2. Bump `package.json` version, then push a tag:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git tag v0.0.2-alpha.0
|
|
144
|
+
git push origin v0.0.2-alpha.0
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Prereleases currently publish with npm dist-tag `alpha`:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pnpm add rspress-plugin-typst@alpha
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { RspressPlugin } from '@rspress/core';
|
|
2
|
+
|
|
3
|
+
declare interface CompilerContext {
|
|
4
|
+
workspace: string;
|
|
5
|
+
fontPaths?: string[];
|
|
6
|
+
inputs?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export declare function compileTypstFile(filepath: string, routePath: string, ctx: CompilerContext): TypstCompileResult;
|
|
10
|
+
|
|
11
|
+
export declare function createCompilerContext(docsRoot: string, options: TypstPluginOptions): CompilerContext;
|
|
12
|
+
|
|
13
|
+
/** Extract heading TOC from Typst HTML body. */
|
|
14
|
+
export declare function extractToc(html: string): TypstTocItem[];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a docs-relative file path to a Rspress route path.
|
|
18
|
+
* Mirrors conventional routing: `guide/index.typ` → `/guide/`, `a.typ` → `/a`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function filePathToRoutePath(relativePath: string, extensions: string[]): string;
|
|
21
|
+
|
|
22
|
+
/** Strip tags and collapse whitespace for search / plain text. */
|
|
23
|
+
export declare function htmlToPlainText(html: string): string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Typst's HTML export currently maps `#=` to `<h2>`. Remap depths so Rspress
|
|
27
|
+
* TOC treats the first content heading level as depth 2 (matching MDX).
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeTocDepths(toc: TypstTocItem[]): TypstTocItem[];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Use Typst (`.typ`) files as Rspress documentation pages.
|
|
33
|
+
*
|
|
34
|
+
* Compilation uses Typst's experimental HTML export through the Rust-powered
|
|
35
|
+
* `@myriaddreamin/typst-ts-node-compiler` Node-API binding.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { defineConfig } from '@rspress/core';
|
|
40
|
+
* import { pluginTypst } from 'rspress-plugin-typst';
|
|
41
|
+
*
|
|
42
|
+
* export default defineConfig({
|
|
43
|
+
* root: 'docs',
|
|
44
|
+
* plugins: [pluginTypst()],
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare function pluginTypst(options?: TypstPluginOptions): RspressPlugin;
|
|
49
|
+
export default pluginTypst;
|
|
50
|
+
export { pluginTypst }
|
|
51
|
+
|
|
52
|
+
declare interface ScannedTypstPage {
|
|
53
|
+
absolutePath: string;
|
|
54
|
+
relativePath: string;
|
|
55
|
+
routePath: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export declare function scanTypstPages(docsRoot: string, options: TypstPluginOptions): Promise<ScannedTypstPage[]>;
|
|
59
|
+
|
|
60
|
+
export declare interface TypstCompileResult {
|
|
61
|
+
filepath: string;
|
|
62
|
+
routePath: string;
|
|
63
|
+
title: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
body: string;
|
|
66
|
+
html: string;
|
|
67
|
+
frontmatter: TypstFrontmatter;
|
|
68
|
+
/** Plain text used for search indexing. */
|
|
69
|
+
textContent: string;
|
|
70
|
+
/** Table of contents extracted from HTML headings. */
|
|
71
|
+
toc: TypstTocItem[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export declare interface TypstFrontmatter {
|
|
75
|
+
title?: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export declare interface TypstPluginOptions {
|
|
81
|
+
/**
|
|
82
|
+
* File extensions treated as Typst documents.
|
|
83
|
+
* @default ['.typ']
|
|
84
|
+
*/
|
|
85
|
+
extensions?: string[];
|
|
86
|
+
/**
|
|
87
|
+
* Glob patterns (relative to the docs root) to include.
|
|
88
|
+
* @default all `.typ` files under the docs root
|
|
89
|
+
*/
|
|
90
|
+
include?: string[];
|
|
91
|
+
/**
|
|
92
|
+
* Glob patterns (relative to the docs root) to exclude.
|
|
93
|
+
* @default underscore-prefixed files and directories
|
|
94
|
+
*/
|
|
95
|
+
exclude?: string[];
|
|
96
|
+
/**
|
|
97
|
+
* Extra font directories passed to the Typst compiler.
|
|
98
|
+
*/
|
|
99
|
+
fontPaths?: string[];
|
|
100
|
+
/**
|
|
101
|
+
* Extra `sys.inputs` string pairs for Typst compilation.
|
|
102
|
+
*/
|
|
103
|
+
inputs?: Record<string, string>;
|
|
104
|
+
/**
|
|
105
|
+
* Typst workspace root. Defaults to the Rspress docs root.
|
|
106
|
+
*/
|
|
107
|
+
workspace?: string;
|
|
108
|
+
/**
|
|
109
|
+
* CSS class applied to the rendered Typst body wrapper.
|
|
110
|
+
* @default 'rspress-typst'
|
|
111
|
+
*/
|
|
112
|
+
className?: string;
|
|
113
|
+
/**
|
|
114
|
+
* When true, also register `.typ` on `route.extensions` so conventional
|
|
115
|
+
* routing discovers pages without `addPages`.
|
|
116
|
+
* Prefer leaving this enabled.
|
|
117
|
+
* @default true
|
|
118
|
+
*/
|
|
119
|
+
routeExtensions?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export declare interface TypstTocItem {
|
|
123
|
+
id: string;
|
|
124
|
+
text: string;
|
|
125
|
+
depth: number;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import node_path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import node_fs from "node:fs";
|
|
4
|
+
import { NodeCompiler } from "@myriaddreamin/typst-ts-node-compiler";
|
|
5
|
+
import fast_glob from "fast-glob";
|
|
6
|
+
function defaultCachePath(docsRoot) {
|
|
7
|
+
return node_path.join(docsRoot, '.rspress', 'typst-page-cache.json');
|
|
8
|
+
}
|
|
9
|
+
function writePageCache(cachePath, cache) {
|
|
10
|
+
node_fs.mkdirSync(node_path.dirname(cachePath), {
|
|
11
|
+
recursive: true
|
|
12
|
+
});
|
|
13
|
+
node_fs.writeFileSync(cachePath, `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
function readPageCache(cachePath) {
|
|
16
|
+
try {
|
|
17
|
+
if (!node_fs.existsSync(cachePath)) return {};
|
|
18
|
+
return JSON.parse(node_fs.readFileSync(cachePath, 'utf8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function cacheKey(filepath) {
|
|
24
|
+
return node_path.resolve(filepath);
|
|
25
|
+
}
|
|
26
|
+
const TAG_RE = /<[^>]+>/g;
|
|
27
|
+
const HEADING_RE = /<h([1-6])(\s[^>]*)?>([\s\S]*?)<\/h\1>/gi;
|
|
28
|
+
const ID_RE = /\bid\s*=\s*["']([^"']+)["']/i;
|
|
29
|
+
function htmlToPlainText(html) {
|
|
30
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ').replace(TAG_RE, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, ' ').trim();
|
|
31
|
+
}
|
|
32
|
+
function slugify(text) {
|
|
33
|
+
return text.toLowerCase().trim().replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
34
|
+
}
|
|
35
|
+
function enhanceHeadings(html) {
|
|
36
|
+
const toc = [];
|
|
37
|
+
const seen = new Map();
|
|
38
|
+
const nextHtml = html.replace(HEADING_RE, (full, depthRaw, attrsRaw, inner)=>{
|
|
39
|
+
const depth = Number(depthRaw);
|
|
40
|
+
const attrs = attrsRaw ?? '';
|
|
41
|
+
const text = htmlToPlainText(inner);
|
|
42
|
+
if (!text) return full;
|
|
43
|
+
let id = attrs.match(ID_RE)?.[1];
|
|
44
|
+
if (!id) {
|
|
45
|
+
const base = slugify(text) || `heading-${toc.length}`;
|
|
46
|
+
const count = seen.get(base) ?? 0;
|
|
47
|
+
seen.set(base, count + 1);
|
|
48
|
+
id = 0 === count ? base : `${base}-${count}`;
|
|
49
|
+
}
|
|
50
|
+
toc.push({
|
|
51
|
+
id,
|
|
52
|
+
text,
|
|
53
|
+
depth
|
|
54
|
+
});
|
|
55
|
+
if (ID_RE.test(attrs)) return full;
|
|
56
|
+
const spacer = attrs.length > 0 ? attrs : '';
|
|
57
|
+
return `<h${depth}${spacer} id="${id}">${inner}</h${depth}>`;
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
html: nextHtml,
|
|
61
|
+
toc: normalizeTocDepths(toc)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function extractToc(html) {
|
|
65
|
+
return enhanceHeadings(html).toc;
|
|
66
|
+
}
|
|
67
|
+
function normalizeTocDepths(toc) {
|
|
68
|
+
if (0 === toc.length) return toc;
|
|
69
|
+
const min = Math.min(...toc.map((item)=>item.depth));
|
|
70
|
+
const shift = 2 - min;
|
|
71
|
+
if (0 === shift) return toc;
|
|
72
|
+
return toc.map((item)=>({
|
|
73
|
+
...item,
|
|
74
|
+
depth: Math.min(6, Math.max(1, item.depth + shift))
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
let sharedCompiler = null;
|
|
78
|
+
let sharedKey = '';
|
|
79
|
+
function compilerKey(ctx) {
|
|
80
|
+
return JSON.stringify({
|
|
81
|
+
workspace: ctx.workspace,
|
|
82
|
+
fontPaths: ctx.fontPaths ?? [],
|
|
83
|
+
inputs: ctx.inputs ?? {}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function getCompiler(ctx) {
|
|
87
|
+
const key = compilerKey(ctx);
|
|
88
|
+
if (!sharedCompiler || sharedKey !== key) {
|
|
89
|
+
sharedCompiler = NodeCompiler.create({
|
|
90
|
+
workspace: ctx.workspace,
|
|
91
|
+
fontArgs: ctx.fontPaths?.length ? [
|
|
92
|
+
{
|
|
93
|
+
fontPaths: ctx.fontPaths
|
|
94
|
+
}
|
|
95
|
+
] : void 0,
|
|
96
|
+
inputs: ctx.inputs
|
|
97
|
+
});
|
|
98
|
+
sharedKey = key;
|
|
99
|
+
}
|
|
100
|
+
return sharedCompiler;
|
|
101
|
+
}
|
|
102
|
+
function asFrontmatter(value) {
|
|
103
|
+
if (!value || 'object' != typeof value || Array.isArray(value)) return {};
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function queryFrontmatter(compiler, mainFilePath) {
|
|
107
|
+
try {
|
|
108
|
+
const result = compiler.query({
|
|
109
|
+
mainFilePath
|
|
110
|
+
}, {
|
|
111
|
+
selector: '<frontmatter>',
|
|
112
|
+
field: 'value'
|
|
113
|
+
});
|
|
114
|
+
if (Array.isArray(result) && result.length > 0) return asFrontmatter(result[0]);
|
|
115
|
+
} catch {}
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
function compileTypstFile(filepath, routePath, ctx) {
|
|
119
|
+
const compiler = getCompiler(ctx);
|
|
120
|
+
const mainFilePath = node_path.resolve(filepath);
|
|
121
|
+
const exec = compiler.tryHtml({
|
|
122
|
+
mainFilePath
|
|
123
|
+
});
|
|
124
|
+
if (exec.hasError()) {
|
|
125
|
+
exec.printDiagnostics();
|
|
126
|
+
throw new Error(`[rspress-plugin-typst] Failed to compile Typst document: ${mainFilePath}`);
|
|
127
|
+
}
|
|
128
|
+
const output = exec.result;
|
|
129
|
+
if (!output) throw new Error(`[rspress-plugin-typst] Empty HTML output for ${mainFilePath}`);
|
|
130
|
+
const frontmatter = queryFrontmatter(compiler, mainFilePath);
|
|
131
|
+
const { html: body, toc } = enhanceHeadings(output.body());
|
|
132
|
+
const html = output.html();
|
|
133
|
+
const title = 'string' == typeof frontmatter.title && frontmatter.title || output.title() || node_path.basename(filepath, node_path.extname(filepath));
|
|
134
|
+
const description = 'string' == typeof frontmatter.description && frontmatter.description || output.description() || void 0;
|
|
135
|
+
const textContent = htmlToPlainText(body);
|
|
136
|
+
compiler.evictCache(10);
|
|
137
|
+
return {
|
|
138
|
+
filepath: mainFilePath,
|
|
139
|
+
routePath,
|
|
140
|
+
title,
|
|
141
|
+
description,
|
|
142
|
+
body,
|
|
143
|
+
html,
|
|
144
|
+
frontmatter: {
|
|
145
|
+
...frontmatter,
|
|
146
|
+
title,
|
|
147
|
+
...description ? {
|
|
148
|
+
description
|
|
149
|
+
} : {}
|
|
150
|
+
},
|
|
151
|
+
textContent,
|
|
152
|
+
toc
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function createCompilerContext(docsRoot, options) {
|
|
156
|
+
return {
|
|
157
|
+
workspace: options.workspace ? node_path.resolve(options.workspace) : node_path.resolve(docsRoot),
|
|
158
|
+
fontPaths: options.fontPaths,
|
|
159
|
+
inputs: options.inputs
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const DEFAULT_INCLUDE = [
|
|
163
|
+
'**/*.typ'
|
|
164
|
+
];
|
|
165
|
+
const DEFAULT_EXCLUDE = [
|
|
166
|
+
'**/_*',
|
|
167
|
+
'**/_*/**'
|
|
168
|
+
];
|
|
169
|
+
function toPosix(input) {
|
|
170
|
+
return input.split(node_path.sep).join('/');
|
|
171
|
+
}
|
|
172
|
+
function stripExtension(filePath, extensions) {
|
|
173
|
+
const lower = filePath.toLowerCase();
|
|
174
|
+
for (const ext of extensions)if (lower.endsWith(ext.toLowerCase())) return filePath.slice(0, -ext.length);
|
|
175
|
+
return filePath;
|
|
176
|
+
}
|
|
177
|
+
function filePathToRoutePath(relativePath, extensions) {
|
|
178
|
+
let route = toPosix(stripExtension(relativePath, extensions));
|
|
179
|
+
if ('index' === route || route.endsWith('/index')) route = route.replace(/\/?index$/, '') || '';
|
|
180
|
+
if (!route.startsWith('/')) route = `/${route}`;
|
|
181
|
+
if ('/' !== route && route.endsWith('/')) route = route.slice(0, -1);
|
|
182
|
+
return route || '/';
|
|
183
|
+
}
|
|
184
|
+
async function scanTypstPages(docsRoot, options) {
|
|
185
|
+
const extensions = options.extensions ?? [
|
|
186
|
+
'.typ'
|
|
187
|
+
];
|
|
188
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
189
|
+
const exclude = options.exclude ?? DEFAULT_EXCLUDE;
|
|
190
|
+
const files = await fast_glob(include, {
|
|
191
|
+
cwd: docsRoot,
|
|
192
|
+
absolute: true,
|
|
193
|
+
onlyFiles: true,
|
|
194
|
+
ignore: exclude,
|
|
195
|
+
dot: false
|
|
196
|
+
});
|
|
197
|
+
const extSet = new Set(extensions.map((e)=>e.toLowerCase()));
|
|
198
|
+
return files.filter((file)=>extSet.has(node_path.extname(file).toLowerCase())).map((absolutePath)=>{
|
|
199
|
+
const relativePath = toPosix(node_path.relative(docsRoot, absolutePath));
|
|
200
|
+
return {
|
|
201
|
+
absolutePath,
|
|
202
|
+
relativePath,
|
|
203
|
+
routePath: filePathToRoutePath(relativePath, extensions)
|
|
204
|
+
};
|
|
205
|
+
}).sort((a, b)=>a.relativePath.localeCompare(b.relativePath));
|
|
206
|
+
}
|
|
207
|
+
const src_dirname = node_path.dirname(fileURLToPath(import.meta.url));
|
|
208
|
+
function uniqExtensions(current, extra) {
|
|
209
|
+
const base = current ?? [
|
|
210
|
+
'.md',
|
|
211
|
+
'.mdx',
|
|
212
|
+
'.js',
|
|
213
|
+
'.jsx',
|
|
214
|
+
'.ts',
|
|
215
|
+
'.tsx'
|
|
216
|
+
];
|
|
217
|
+
return [
|
|
218
|
+
...new Set([
|
|
219
|
+
...base,
|
|
220
|
+
...extra
|
|
221
|
+
])
|
|
222
|
+
];
|
|
223
|
+
}
|
|
224
|
+
function pluginTypst(options = {}) {
|
|
225
|
+
const extensions = options.extensions ?? [
|
|
226
|
+
'.typ'
|
|
227
|
+
];
|
|
228
|
+
const className = options.className ?? 'rspress-typst';
|
|
229
|
+
const enableRouteExtensions = false !== options.routeExtensions;
|
|
230
|
+
let docsRoot = '';
|
|
231
|
+
let cachePath = '';
|
|
232
|
+
let pageCache = {};
|
|
233
|
+
return {
|
|
234
|
+
name: 'rspress-plugin-typst',
|
|
235
|
+
globalStyles: node_path.join(src_dirname, 'runtime/typst.css'),
|
|
236
|
+
config (config) {
|
|
237
|
+
docsRoot = node_path.resolve(config.root ?? 'docs');
|
|
238
|
+
cachePath = defaultCachePath(docsRoot);
|
|
239
|
+
const next = {
|
|
240
|
+
...config
|
|
241
|
+
};
|
|
242
|
+
if (enableRouteExtensions) next.route = {
|
|
243
|
+
...next.route,
|
|
244
|
+
extensions: uniqExtensions(next.route?.extensions, extensions)
|
|
245
|
+
};
|
|
246
|
+
return next;
|
|
247
|
+
},
|
|
248
|
+
async beforeBuild (config) {
|
|
249
|
+
docsRoot = node_path.resolve(config.root ?? (docsRoot || 'docs'));
|
|
250
|
+
cachePath = defaultCachePath(docsRoot);
|
|
251
|
+
const ctx = createCompilerContext(docsRoot, options);
|
|
252
|
+
const pages = await scanTypstPages(docsRoot, options);
|
|
253
|
+
const nextCache = {};
|
|
254
|
+
for (const page of pages){
|
|
255
|
+
const result = compileTypstFile(page.absolutePath, page.routePath, ctx);
|
|
256
|
+
nextCache[cacheKey(page.absolutePath)] = {
|
|
257
|
+
title: result.title,
|
|
258
|
+
description: result.description,
|
|
259
|
+
textContent: result.textContent,
|
|
260
|
+
toc: result.toc,
|
|
261
|
+
frontmatter: result.frontmatter,
|
|
262
|
+
routePath: result.routePath,
|
|
263
|
+
filepath: result.filepath
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
pageCache = nextCache;
|
|
267
|
+
writePageCache(cachePath, pageCache);
|
|
268
|
+
},
|
|
269
|
+
async extendPageData (pageData) {
|
|
270
|
+
const filepath = pageData._filepath;
|
|
271
|
+
if (!filepath || !extensions.some((ext)=>filepath.endsWith(ext))) return;
|
|
272
|
+
if (!Object.keys(pageCache).length && cachePath) pageCache = readPageCache(cachePath);
|
|
273
|
+
const cached = pageCache[cacheKey(filepath)];
|
|
274
|
+
if (!cached) return;
|
|
275
|
+
pageData.title = cached.title;
|
|
276
|
+
if (cached.description) pageData.description = cached.description;
|
|
277
|
+
pageData.toc = cached.toc.map((item)=>({
|
|
278
|
+
id: item.id,
|
|
279
|
+
text: item.text,
|
|
280
|
+
depth: item.depth,
|
|
281
|
+
charIndex: 0
|
|
282
|
+
}));
|
|
283
|
+
pageData.content = cached.textContent;
|
|
284
|
+
pageData.frontmatter = {
|
|
285
|
+
...pageData.frontmatter,
|
|
286
|
+
...cached.frontmatter,
|
|
287
|
+
title: cached.title,
|
|
288
|
+
...cached.description ? {
|
|
289
|
+
description: cached.description
|
|
290
|
+
} : {}
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
async modifySearchIndexData (pages) {
|
|
294
|
+
if (!Object.keys(pageCache).length && cachePath) pageCache = readPageCache(cachePath);
|
|
295
|
+
for (const page of pages){
|
|
296
|
+
const filepath = page._filepath;
|
|
297
|
+
if (!filepath) continue;
|
|
298
|
+
const cached = pageCache[cacheKey(filepath)];
|
|
299
|
+
if (cached) {
|
|
300
|
+
page.title = cached.title;
|
|
301
|
+
page.content = cached.textContent;
|
|
302
|
+
if (cached.description) page.description = cached.description;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
builderConfig: {
|
|
307
|
+
tools: {
|
|
308
|
+
rspack (config) {
|
|
309
|
+
config.resolve ??= {};
|
|
310
|
+
config.resolve.extensions = uniqExtensions(config.resolve.extensions, extensions);
|
|
311
|
+
config.module ??= {};
|
|
312
|
+
config.module.rules ??= [];
|
|
313
|
+
config.module.rules.unshift({
|
|
314
|
+
test: /\.typ$/,
|
|
315
|
+
type: "javascript/auto",
|
|
316
|
+
use: [
|
|
317
|
+
{
|
|
318
|
+
loader: node_path.join(src_dirname, 'loader.js'),
|
|
319
|
+
options: {
|
|
320
|
+
workspace: null != options.workspace ? node_path.resolve(options.workspace) : docsRoot || node_path.resolve('docs'),
|
|
321
|
+
fontPaths: options.fontPaths,
|
|
322
|
+
inputs: options.inputs,
|
|
323
|
+
className,
|
|
324
|
+
cachePath: cachePath || defaultCachePath(docsRoot || 'docs')
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
source: {
|
|
332
|
+
include: [
|
|
333
|
+
/rspress-plugin-typst/
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const src = pluginTypst;
|
|
340
|
+
export default src;
|
|
341
|
+
export { compileTypstFile, createCompilerContext, extractToc, filePathToRoutePath, htmlToPlainText, normalizeTocDepths, pluginTypst, scanTypstPages };
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import node_fs from "node:fs";
|
|
2
|
+
import node_path from "node:path";
|
|
3
|
+
import { NodeCompiler } from "@myriaddreamin/typst-ts-node-compiler";
|
|
4
|
+
import "fast-glob";
|
|
5
|
+
function writePageCache(cachePath, cache) {
|
|
6
|
+
node_fs.mkdirSync(node_path.dirname(cachePath), {
|
|
7
|
+
recursive: true
|
|
8
|
+
});
|
|
9
|
+
node_fs.writeFileSync(cachePath, `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
function readPageCache(cachePath) {
|
|
12
|
+
try {
|
|
13
|
+
if (!node_fs.existsSync(cachePath)) return {};
|
|
14
|
+
return JSON.parse(node_fs.readFileSync(cachePath, 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function cacheKey(filepath) {
|
|
20
|
+
return node_path.resolve(filepath);
|
|
21
|
+
}
|
|
22
|
+
function generateTypstPageModule(result, className) {
|
|
23
|
+
const frontmatter = {
|
|
24
|
+
...result.frontmatter,
|
|
25
|
+
title: result.title,
|
|
26
|
+
...result.description ? {
|
|
27
|
+
description: result.description
|
|
28
|
+
} : {}
|
|
29
|
+
};
|
|
30
|
+
const metaKey = node_path.basename(result.filepath);
|
|
31
|
+
const pageMeta = {
|
|
32
|
+
toc: result.toc,
|
|
33
|
+
title: result.title,
|
|
34
|
+
headingTitle: result.title,
|
|
35
|
+
description: result.description,
|
|
36
|
+
frontmatter
|
|
37
|
+
};
|
|
38
|
+
return `/* Auto-generated by rspress-plugin-typst — do not edit */
|
|
39
|
+
import { jsx as _jsx } from 'react/jsx-runtime';
|
|
40
|
+
import { TypstDoc } from 'rspress-plugin-typst/runtime';
|
|
41
|
+
|
|
42
|
+
export const frontmatter = ${JSON.stringify(frontmatter, null, 2)};
|
|
43
|
+
|
|
44
|
+
const __typstBody = ${JSON.stringify(result.body)};
|
|
45
|
+
const __typstToc = ${JSON.stringify(result.toc)};
|
|
46
|
+
const __typstPageMeta = ${JSON.stringify(pageMeta, null, 2)};
|
|
47
|
+
|
|
48
|
+
export default function TypstPage() {
|
|
49
|
+
return _jsx(TypstDoc, {
|
|
50
|
+
html: __typstBody,
|
|
51
|
+
className: ${JSON.stringify(className)},
|
|
52
|
+
toc: __typstToc,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
TypstPage.__RSPRESS_PAGE_META = {};
|
|
57
|
+
TypstPage.__RSPRESS_PAGE_META[${JSON.stringify(metaKey)}] = __typstPageMeta;
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
const TAG_RE = /<[^>]+>/g;
|
|
61
|
+
const HEADING_RE = /<h([1-6])(\s[^>]*)?>([\s\S]*?)<\/h\1>/gi;
|
|
62
|
+
const ID_RE = /\bid\s*=\s*["']([^"']+)["']/i;
|
|
63
|
+
function htmlToPlainText(html) {
|
|
64
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, ' ').replace(/<style[\s\S]*?<\/style>/gi, ' ').replace(TAG_RE, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/\s+/g, ' ').trim();
|
|
65
|
+
}
|
|
66
|
+
function slugify(text) {
|
|
67
|
+
return text.toLowerCase().trim().replace(/[^\p{L}\p{N}\s-]/gu, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
68
|
+
}
|
|
69
|
+
function enhanceHeadings(html) {
|
|
70
|
+
const toc = [];
|
|
71
|
+
const seen = new Map();
|
|
72
|
+
const nextHtml = html.replace(HEADING_RE, (full, depthRaw, attrsRaw, inner)=>{
|
|
73
|
+
const depth = Number(depthRaw);
|
|
74
|
+
const attrs = attrsRaw ?? '';
|
|
75
|
+
const text = htmlToPlainText(inner);
|
|
76
|
+
if (!text) return full;
|
|
77
|
+
let id = attrs.match(ID_RE)?.[1];
|
|
78
|
+
if (!id) {
|
|
79
|
+
const base = slugify(text) || `heading-${toc.length}`;
|
|
80
|
+
const count = seen.get(base) ?? 0;
|
|
81
|
+
seen.set(base, count + 1);
|
|
82
|
+
id = 0 === count ? base : `${base}-${count}`;
|
|
83
|
+
}
|
|
84
|
+
toc.push({
|
|
85
|
+
id,
|
|
86
|
+
text,
|
|
87
|
+
depth
|
|
88
|
+
});
|
|
89
|
+
if (ID_RE.test(attrs)) return full;
|
|
90
|
+
const spacer = attrs.length > 0 ? attrs : '';
|
|
91
|
+
return `<h${depth}${spacer} id="${id}">${inner}</h${depth}>`;
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
html: nextHtml,
|
|
95
|
+
toc: normalizeTocDepths(toc)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function normalizeTocDepths(toc) {
|
|
99
|
+
if (0 === toc.length) return toc;
|
|
100
|
+
const min = Math.min(...toc.map((item)=>item.depth));
|
|
101
|
+
const shift = 2 - min;
|
|
102
|
+
if (0 === shift) return toc;
|
|
103
|
+
return toc.map((item)=>({
|
|
104
|
+
...item,
|
|
105
|
+
depth: Math.min(6, Math.max(1, item.depth + shift))
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
let sharedCompiler = null;
|
|
109
|
+
let sharedKey = '';
|
|
110
|
+
function compilerKey(ctx) {
|
|
111
|
+
return JSON.stringify({
|
|
112
|
+
workspace: ctx.workspace,
|
|
113
|
+
fontPaths: ctx.fontPaths ?? [],
|
|
114
|
+
inputs: ctx.inputs ?? {}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function getCompiler(ctx) {
|
|
118
|
+
const key = compilerKey(ctx);
|
|
119
|
+
if (!sharedCompiler || sharedKey !== key) {
|
|
120
|
+
sharedCompiler = NodeCompiler.create({
|
|
121
|
+
workspace: ctx.workspace,
|
|
122
|
+
fontArgs: ctx.fontPaths?.length ? [
|
|
123
|
+
{
|
|
124
|
+
fontPaths: ctx.fontPaths
|
|
125
|
+
}
|
|
126
|
+
] : void 0,
|
|
127
|
+
inputs: ctx.inputs
|
|
128
|
+
});
|
|
129
|
+
sharedKey = key;
|
|
130
|
+
}
|
|
131
|
+
return sharedCompiler;
|
|
132
|
+
}
|
|
133
|
+
function asFrontmatter(value) {
|
|
134
|
+
if (!value || 'object' != typeof value || Array.isArray(value)) return {};
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function queryFrontmatter(compiler, mainFilePath) {
|
|
138
|
+
try {
|
|
139
|
+
const result = compiler.query({
|
|
140
|
+
mainFilePath
|
|
141
|
+
}, {
|
|
142
|
+
selector: '<frontmatter>',
|
|
143
|
+
field: 'value'
|
|
144
|
+
});
|
|
145
|
+
if (Array.isArray(result) && result.length > 0) return asFrontmatter(result[0]);
|
|
146
|
+
} catch {}
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
function compileTypstFile(filepath, routePath, ctx) {
|
|
150
|
+
const compiler = getCompiler(ctx);
|
|
151
|
+
const mainFilePath = node_path.resolve(filepath);
|
|
152
|
+
const exec = compiler.tryHtml({
|
|
153
|
+
mainFilePath
|
|
154
|
+
});
|
|
155
|
+
if (exec.hasError()) {
|
|
156
|
+
exec.printDiagnostics();
|
|
157
|
+
throw new Error(`[rspress-plugin-typst] Failed to compile Typst document: ${mainFilePath}`);
|
|
158
|
+
}
|
|
159
|
+
const output = exec.result;
|
|
160
|
+
if (!output) throw new Error(`[rspress-plugin-typst] Empty HTML output for ${mainFilePath}`);
|
|
161
|
+
const frontmatter = queryFrontmatter(compiler, mainFilePath);
|
|
162
|
+
const { html: body, toc } = enhanceHeadings(output.body());
|
|
163
|
+
const html = output.html();
|
|
164
|
+
const title = 'string' == typeof frontmatter.title && frontmatter.title || output.title() || node_path.basename(filepath, node_path.extname(filepath));
|
|
165
|
+
const description = 'string' == typeof frontmatter.description && frontmatter.description || output.description() || void 0;
|
|
166
|
+
const textContent = htmlToPlainText(body);
|
|
167
|
+
compiler.evictCache(10);
|
|
168
|
+
return {
|
|
169
|
+
filepath: mainFilePath,
|
|
170
|
+
routePath,
|
|
171
|
+
title,
|
|
172
|
+
description,
|
|
173
|
+
body,
|
|
174
|
+
html,
|
|
175
|
+
frontmatter: {
|
|
176
|
+
...frontmatter,
|
|
177
|
+
title,
|
|
178
|
+
...description ? {
|
|
179
|
+
description
|
|
180
|
+
} : {}
|
|
181
|
+
},
|
|
182
|
+
textContent,
|
|
183
|
+
toc
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function createCompilerContext(docsRoot, options) {
|
|
187
|
+
return {
|
|
188
|
+
workspace: options.workspace ? node_path.resolve(options.workspace) : node_path.resolve(docsRoot),
|
|
189
|
+
fontPaths: options.fontPaths,
|
|
190
|
+
inputs: options.inputs
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function toPosix(input) {
|
|
194
|
+
return input.split(node_path.sep).join('/');
|
|
195
|
+
}
|
|
196
|
+
function stripExtension(filePath, extensions) {
|
|
197
|
+
const lower = filePath.toLowerCase();
|
|
198
|
+
for (const ext of extensions)if (lower.endsWith(ext.toLowerCase())) return filePath.slice(0, -ext.length);
|
|
199
|
+
return filePath;
|
|
200
|
+
}
|
|
201
|
+
function filePathToRoutePath(relativePath, extensions) {
|
|
202
|
+
let route = toPosix(stripExtension(relativePath, extensions));
|
|
203
|
+
if ('index' === route || route.endsWith('/index')) route = route.replace(/\/?index$/, '') || '';
|
|
204
|
+
if (!route.startsWith('/')) route = `/${route}`;
|
|
205
|
+
if ('/' !== route && route.endsWith('/')) route = route.slice(0, -1);
|
|
206
|
+
return route || '/';
|
|
207
|
+
}
|
|
208
|
+
function updateCache(cachePath, filepath, entry) {
|
|
209
|
+
const cache = readPageCache(cachePath);
|
|
210
|
+
cache[cacheKey(filepath)] = entry;
|
|
211
|
+
writePageCache(cachePath, cache);
|
|
212
|
+
}
|
|
213
|
+
function typstLoader() {
|
|
214
|
+
this.cacheable(true);
|
|
215
|
+
const callback = this.async();
|
|
216
|
+
const options = this.getOptions();
|
|
217
|
+
const filepath = this.resourcePath;
|
|
218
|
+
try {
|
|
219
|
+
const ctx = createCompilerContext(options.workspace, {
|
|
220
|
+
workspace: options.workspace,
|
|
221
|
+
fontPaths: options.fontPaths,
|
|
222
|
+
inputs: options.inputs
|
|
223
|
+
});
|
|
224
|
+
const relative = filepath.startsWith(options.workspace) ? filepath.slice(options.workspace.length).replace(/^[/\\]/, '') : filepath;
|
|
225
|
+
const routePath = filePathToRoutePath(relative, [
|
|
226
|
+
'.typ'
|
|
227
|
+
]);
|
|
228
|
+
const result = compileTypstFile(filepath, routePath, ctx);
|
|
229
|
+
updateCache(options.cachePath, filepath, {
|
|
230
|
+
title: result.title,
|
|
231
|
+
description: result.description,
|
|
232
|
+
textContent: result.textContent,
|
|
233
|
+
toc: result.toc,
|
|
234
|
+
frontmatter: result.frontmatter,
|
|
235
|
+
routePath: result.routePath,
|
|
236
|
+
filepath: result.filepath
|
|
237
|
+
});
|
|
238
|
+
const code = generateTypstPageModule(result, options.className);
|
|
239
|
+
callback(null, code);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export default typstLoader;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders Typst experimental HTML export inside the Rspress doc shell.
|
|
5
|
+
*/
|
|
6
|
+
declare function TypstDoc({ html, className, }: TypstDocProps): ReactElement;
|
|
7
|
+
export { TypstDoc }
|
|
8
|
+
export default TypstDoc;
|
|
9
|
+
|
|
10
|
+
export declare interface TypstDocProps {
|
|
11
|
+
html: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Reserved for future client-side TOC enhancements. */
|
|
14
|
+
toc?: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
text: string;
|
|
17
|
+
depth: number;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
function TypstDoc({ html, className = 'rspress-typst' }) {
|
|
3
|
+
return /*#__PURE__*/ jsx("div", {
|
|
4
|
+
className: `rp-doc ${className}`,
|
|
5
|
+
dangerouslySetInnerHTML: {
|
|
6
|
+
__html: html
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
const runtime_TypstDoc = TypstDoc;
|
|
11
|
+
export default runtime_TypstDoc;
|
|
12
|
+
export { TypstDoc };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Minimal polish for Typst experimental HTML inside Rspress docs. */
|
|
2
|
+
.rspress-typst {
|
|
3
|
+
min-width: 0;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.rspress-typst > div {
|
|
7
|
+
display: contents;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.rspress-typst :where(h1, h2, h3, h4, h5, h6) {
|
|
11
|
+
scroll-margin-top: 5rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.rspress-typst :where(pre, code) {
|
|
15
|
+
font-family: var(
|
|
16
|
+
--rp-font-family-mono,
|
|
17
|
+
ui-monospace,
|
|
18
|
+
SFMono-Regular,
|
|
19
|
+
Menlo,
|
|
20
|
+
Monaco,
|
|
21
|
+
Consolas,
|
|
22
|
+
'Liberation Mono',
|
|
23
|
+
'Courier New',
|
|
24
|
+
monospace
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.rspress-typst :where(table) {
|
|
29
|
+
display: block;
|
|
30
|
+
width: 100%;
|
|
31
|
+
overflow-x: auto;
|
|
32
|
+
border-collapse: collapse;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.rspress-typst :where(th, td) {
|
|
36
|
+
border: 1px solid var(--rp-c-divider, #e2e8f0);
|
|
37
|
+
padding: 0.5rem 0.75rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.rspress-typst :where(img, svg) {
|
|
41
|
+
max-width: 100%;
|
|
42
|
+
height: auto;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rspress-plugin-typst",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.2-alpha.1",
|
|
4
|
+
"description": "Rspress plugin that treats Typst (.typ) files as documentation pages via experimental HTML export.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rspress",
|
|
7
|
+
"rspress-plugin",
|
|
8
|
+
"typst",
|
|
9
|
+
"documentation",
|
|
10
|
+
"html"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://glimverge.github.io/rspress-plugin-typst/",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/glimverge/rspress-plugin-typst/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/glimverge/rspress-plugin-typst.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Glimverge",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./runtime": {
|
|
30
|
+
"types": "./dist/runtime/index.d.ts",
|
|
31
|
+
"import": "./dist/runtime/index.js",
|
|
32
|
+
"default": "./dist/runtime/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./loader": {
|
|
35
|
+
"types": "./dist/loader.d.ts",
|
|
36
|
+
"import": "./dist/loader.js",
|
|
37
|
+
"default": "./dist/loader.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"main": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@myriaddreamin/typst-ts-node-compiler": "^0.7.0",
|
|
49
|
+
"fast-glob": "^3.3.3"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@microsoft/api-extractor": "^7.59.4",
|
|
53
|
+
"@rsbuild/plugin-react": "^1.4.0",
|
|
54
|
+
"@rslib/core": "^1.0.3",
|
|
55
|
+
"@rspress/core": "^2.0.23",
|
|
56
|
+
"@types/node": "^22.13.10",
|
|
57
|
+
"@types/react": "^19.0.12",
|
|
58
|
+
"react": "^19.0.0",
|
|
59
|
+
"typescript": "^5.8.2"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@rspress/core": "^1.40.0 || ^2.0.0",
|
|
63
|
+
"react": ">=17.0.0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"react": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=18.0.0"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "rslib build",
|
|
78
|
+
"dev": "rslib build --watch",
|
|
79
|
+
"test": "pnpm build && node --experimental-strip-types --test tests/**/*.test.ts",
|
|
80
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
81
|
+
"playground:dev": "pnpm --filter playground dev",
|
|
82
|
+
"playground:build": "pnpm --filter playground build"
|
|
83
|
+
}
|
|
6
84
|
}
|