@takazudo/zfb-md-wasm 0.1.0-next.80
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 +217 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +229 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/wasm/zfb_md_wasm.d.ts +63 -0
- package/dist/wasm/zfb_md_wasm.js +275 -0
- package/dist/wasm/zfb_md_wasm_bg.wasm +0 -0
- package/dist/wasm/zfb_md_wasm_bg.wasm.d.ts +10 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Takeshi Takatsudo
|
|
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
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# @takazudo/zfb-md-wasm
|
|
2
|
+
|
|
3
|
+
A WebAssembly build of [zfb](https://github.com/Takazudo/zudo-front-builder)'s
|
|
4
|
+
markdown / MDX → JavaScript conversion pipeline, for **browser-side dynamic
|
|
5
|
+
conversion**. The headline use case is **CMS live preview**: convert MDX to a
|
|
6
|
+
runnable ES module (or markdown to HTML) in the browser, on every keystroke,
|
|
7
|
+
with output **parity** to what zfb produces at build time.
|
|
8
|
+
|
|
9
|
+
Parity is the reason this package exists instead of running
|
|
10
|
+
[`@mdx-js/mdx`](https://mdxjs.com/) in the browser: `@mdx-js/mdx` is a
|
|
11
|
+
_different_ pipeline, so its preview would not match zfb's built output.
|
|
12
|
+
`zfb-md-wasm` compiles the same Rust pipeline zfb itself uses, so a preview is
|
|
13
|
+
faithful to the real thing.
|
|
14
|
+
|
|
15
|
+
Node ≥ 20 can also load the package (this is what the test suite uses), but the
|
|
16
|
+
browser is the target. A server with shell access should keep calling the `zfb`
|
|
17
|
+
binary directly.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pnpm add @takazudo/zfb-md-wasm
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Two API tiers
|
|
26
|
+
|
|
27
|
+
Both functions take the markdown/MDX `source` and an options object (every
|
|
28
|
+
field optional; `{}` selects all defaults). Both return a result object plus a
|
|
29
|
+
`diagnostics` array — **expected failures come back as diagnostics, never as a
|
|
30
|
+
thrown error** (see the trap contract below for what _does_ throw).
|
|
31
|
+
|
|
32
|
+
### `compile(source, options?)` — MDX → ES-module JS
|
|
33
|
+
|
|
34
|
+
Full MDX → JSX → SWC → ES module. The emitted module has a `MDXContent`
|
|
35
|
+
default export (a component function) using the automatic JSX runtime.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { compile } from "@takazudo/zfb-md-wasm";
|
|
39
|
+
|
|
40
|
+
const { code, frontmatter, diagnostics } = await compile(
|
|
41
|
+
"---\ntitle: Hello\n---\n\n# Welcome\n\n<Callout>Sum is {1 + 2}</Callout>\n",
|
|
42
|
+
{ filename: "post.mdx", jsxRuntime: "preact" },
|
|
43
|
+
);
|
|
44
|
+
// code -> ES-module JS source (string) or null on failure
|
|
45
|
+
// frontmatter -> { title: "Hello" }
|
|
46
|
+
// diagnostics -> []
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Frontmatter values are returned in the `frontmatter` field — they are **not**
|
|
50
|
+
exposed as an in-content binding. The compiled module has no `frontmatter`
|
|
51
|
+
variable in scope, so a `{frontmatter.title}` reference inside the source
|
|
52
|
+
would throw `ReferenceError` at runtime; read the values from the result
|
|
53
|
+
object instead.
|
|
54
|
+
|
|
55
|
+
### `renderHtml(source, options?)` — markdown → HTML
|
|
56
|
+
|
|
57
|
+
Markdown → hast → HTML string, **skipping SWC at runtime**. Use this for a
|
|
58
|
+
plain-markdown preview when you don't need to evaluate a component module.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { renderHtml } from "@takazudo/zfb-md-wasm";
|
|
62
|
+
|
|
63
|
+
const { html, frontmatter, diagnostics } = await renderHtml(
|
|
64
|
+
"# Heading\n\nSome **bold** text.\n",
|
|
65
|
+
{ filename: "post.md" },
|
|
66
|
+
);
|
|
67
|
+
// html -> "<h1>Heading</h1><p>Some <strong>bold</strong> text.</p>"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`renderHtml` accepts and ignores `jsxRuntime` / `development`, so one options
|
|
71
|
+
object can serve both tiers.
|
|
72
|
+
|
|
73
|
+
### `version()` / `init()`
|
|
74
|
+
|
|
75
|
+
`version()` returns the package version for host-side compatibility checks.
|
|
76
|
+
Published artifacts are stamped with the release semver at build time; local
|
|
77
|
+
development builds fall back to the Rust manifest placeholder.
|
|
78
|
+
`init()` eagerly loads and instantiates the wasm module; it's optional (every
|
|
79
|
+
call instantiates on first use) but useful to front-load the one-time
|
|
80
|
+
fetch/compile cost at app startup.
|
|
81
|
+
|
|
82
|
+
## Options shape
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
interface ZfbMdWasmOptions {
|
|
86
|
+
filename?: string; // must end .md/.mdx; drives frontmatter dispatch + diagnostics
|
|
87
|
+
jsxRuntime?: "preact" | "react"; // compile only; default "preact"
|
|
88
|
+
development?: boolean; // compile only; default false
|
|
89
|
+
pipeline?: {
|
|
90
|
+
theme?: string | null; // a syntect theme name, or null for no highlighting
|
|
91
|
+
gfm?: {
|
|
92
|
+
strikethrough?: boolean; table?: boolean; autolinkLiteral?: boolean;
|
|
93
|
+
taskListItem?: boolean; footnoteDefinition?: boolean;
|
|
94
|
+
};
|
|
95
|
+
cjkFriendly?: boolean;
|
|
96
|
+
hardBreaks?: boolean;
|
|
97
|
+
features?: Record<string, unknown>; // zfb's MarkdownFeaturesConfig, verbatim
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`pipeline` is zfb's **resolved features config** as JSON — the same shape zfb
|
|
103
|
+
derives from `zfb.config.ts` at build time. See "Limitations" for why it's
|
|
104
|
+
resolved JSON rather than a config file. Unknown fields are rejected at both
|
|
105
|
+
nesting levels (an `options`-source diagnostic).
|
|
106
|
+
|
|
107
|
+
## Evaluating compiled modules in a browser
|
|
108
|
+
|
|
109
|
+
`compile()` returns ES-module _source_. To run it, turn it into a module (a
|
|
110
|
+
blob URL is the usual trick) and dynamic-import it:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const { code, diagnostics } = await compile(source, { filename: "preview.mdx" });
|
|
114
|
+
if (code === null) {
|
|
115
|
+
// Compilation failed — render `diagnostics` instead of a module.
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const url = URL.createObjectURL(new Blob([code], { type: "text/javascript" }));
|
|
119
|
+
const { default: MDXContent } = await import(/* @vite-ignore */ url);
|
|
120
|
+
URL.revokeObjectURL(url);
|
|
121
|
+
// render <MDXContent components={{ Callout }} /> with your framework
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Pass your PascalCase components (the `<Callout>` in the source above) through
|
|
125
|
+
the module's `components` prop.
|
|
126
|
+
|
|
127
|
+
### ⚠️ You must supply the JSX runtime — and the preact case needs one alias
|
|
128
|
+
|
|
129
|
+
The compiled module imports its JSX runtime by bare specifier
|
|
130
|
+
(`preact/jsx-runtime` or `react/jsx-runtime`), so the page must resolve those —
|
|
131
|
+
via an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap)
|
|
132
|
+
or your bundler.
|
|
133
|
+
|
|
134
|
+
**There is one asymmetry to know about.** zfb's emitter takes the JSX _factory_
|
|
135
|
+
from your chosen runtime but **always imports `Fragment` from
|
|
136
|
+
`react/jsx-runtime`**, regardless of `jsxRuntime`. This is zfb's production
|
|
137
|
+
emitter shape (so it's parity-correct, not a bug) — but it means a **preact**
|
|
138
|
+
consumer must alias `react/jsx-runtime` onto preact's, or `Fragment` will fail
|
|
139
|
+
to resolve at runtime:
|
|
140
|
+
|
|
141
|
+
```html
|
|
142
|
+
<script type="importmap">
|
|
143
|
+
{
|
|
144
|
+
"imports": {
|
|
145
|
+
"preact/jsx-runtime": "https://esm.sh/preact/jsx-runtime",
|
|
146
|
+
"react/jsx-runtime": "https://esm.sh/preact/jsx-runtime"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A `react` consumer just maps `react/jsx-runtime` to React's own and needs no
|
|
153
|
+
alias.
|
|
154
|
+
|
|
155
|
+
## Node usage
|
|
156
|
+
|
|
157
|
+
For tests and tooling, the package loads and runs under Node ≥ 20 with no extra
|
|
158
|
+
setup — the same `compile` / `renderHtml` / `version` API. This is exactly how
|
|
159
|
+
this package's own vitest suite exercises the wasm.
|
|
160
|
+
|
|
161
|
+
## Parity guarantee & limitations
|
|
162
|
+
|
|
163
|
+
Output matches zfb's native pipeline on a fixed fixture corpus (the parity
|
|
164
|
+
suite gates exact-match). Deliberate limitations of the browser build:
|
|
165
|
+
|
|
166
|
+
- **Filesystem-bound plugins are inert.** `transclude`, `imageDimensions`, and
|
|
167
|
+
`linkValidation` are registered but never touch a filesystem (there is none),
|
|
168
|
+
exactly as zfb's own MDX loader runs them with build-context roots unarmed.
|
|
169
|
+
Host-callback versions are a possible future epic.
|
|
170
|
+
- **Config is resolved JSON, not `zfb.config.ts`.** Evaluating a TypeScript
|
|
171
|
+
config needs a JS engine; that stays build-side. Resolve your config to JSON
|
|
172
|
+
first and pass it as `pipeline`.
|
|
173
|
+
- **No cross-file features.** Route-table link resolution and cross-file anchor
|
|
174
|
+
resolution need the whole project graph, which a single-document browser call
|
|
175
|
+
doesn't have.
|
|
176
|
+
- **The single artifact carries SWC even for `renderHtml`-only use.** One
|
|
177
|
+
cdylib can't tree-shake SWC away when only `renderHtml` is called; a slim
|
|
178
|
+
`renderHtml`-only artifact is a documented possible follow-up.
|
|
179
|
+
- **Syntax highlighting uses syntect's `fancy-regex` backend** (native zfb uses
|
|
180
|
+
`oniguruma`, which can't compile to wasm). The two are byte-identical on
|
|
181
|
+
zfb's fixture corpus; any grammar-level divergences are tracked in the
|
|
182
|
+
crate's informational backend-divergence test.
|
|
183
|
+
|
|
184
|
+
## Artifact size
|
|
185
|
+
|
|
186
|
+
Shipping SWC in the bytes makes this a large module. The build applies a
|
|
187
|
+
size-optimized cargo profile (`opt-level = "z"`, LTO, one codegen unit,
|
|
188
|
+
`panic = "abort"`) plus `wasm-opt`, which roughly halves the raw binary. The
|
|
189
|
+
current build produces **~2.9 MB raw / ~1.3 MB gzipped** for the `.wasm`. The CI
|
|
190
|
+
`wasm-md` job prints the authoritative gzipped size on every run — treat that
|
|
191
|
+
as the source of truth rather than this figure, which can drift.
|
|
192
|
+
|
|
193
|
+
## Error / trap / re-init contract
|
|
194
|
+
|
|
195
|
+
- **Expected failures never throw.** Parse errors, malformed options JSON,
|
|
196
|
+
unknown themes, a bad filename — all come back as structured `Diagnostic[]`
|
|
197
|
+
entries (`{ severity, source, message, line, column }`) on a normal result,
|
|
198
|
+
with `code` / `html` set to `null`. `line`/`column` are 1-based; for
|
|
199
|
+
`markdown` / `frontmatter` sources they point into the original source, for
|
|
200
|
+
`options` into the options JSON.
|
|
201
|
+
- **A wasm _trap_ is always a bug, and it poisons the instance.** On
|
|
202
|
+
`wasm32-unknown-unknown` a Rust panic (or other internal fault) lowers to a
|
|
203
|
+
wasm trap; `catch_unwind` is not reliable recovery, so the instance is dead
|
|
204
|
+
afterward. This wrapper handles that for you: it catches the
|
|
205
|
+
`WebAssembly.RuntimeError`, **drops the poisoned instance and re-instantiates
|
|
206
|
+
a fresh one in the background** (from the cached compiled module, so no
|
|
207
|
+
recompile), and throws a `ZfbMdWasmTrapError` for that one call. The next
|
|
208
|
+
`compile` / `renderHtml` / `version` call transparently uses the fresh
|
|
209
|
+
instance. The API is stateless per call, so re-init is lossless.
|
|
210
|
+
|
|
211
|
+
If you ever see a `ZfbMdWasmTrapError`, please report it with the input that
|
|
212
|
+
triggered it — the crate is designed never to trap on structured input.
|
|
213
|
+
(Fuzzing the trap surface is a documented follow-up.)
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT © Takeshi Takatsudo
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { CompileResult, RenderHtmlResult, ZfbMdWasmOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a wasm call traps (a Rust panic, or another internal fault
|
|
4
|
+
* that lowers to a wasm trap). This is always a bug, never an expected
|
|
5
|
+
* input-validation failure -- expected failures (parse errors, malformed
|
|
6
|
+
* options JSON, unknown themes, ...) come back as structured
|
|
7
|
+
* `Diagnostic[]` in a normal `CompileResult`/`RenderHtmlResult`, never as a
|
|
8
|
+
* thrown error. See this package's README "Error / trap / re-init
|
|
9
|
+
* contract" section.
|
|
10
|
+
*
|
|
11
|
+
* By the time this is thrown, the poisoned instance has already been dropped
|
|
12
|
+
* and a replacement has been instantiated (from the cached compiled module, so
|
|
13
|
+
* no recompile). Concurrent trap reporters for the same poisoned generation
|
|
14
|
+
* wait for that same replacement instead of each starting another one; the
|
|
15
|
+
* next `compile` / `renderHtml` / `version` call transparently uses the fresh
|
|
16
|
+
* instance.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ZfbMdWasmTrapError extends Error {
|
|
19
|
+
constructor(cause: unknown);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Thrown once this wrapper has already recovered from too many wasm traps in
|
|
23
|
+
* one module lifetime. ES module records cannot be evicted, and wasm-bindgen
|
|
24
|
+
* `--target web` glue cannot be safely re-used after initialization, so the
|
|
25
|
+
* only bounded behavior after repeated poisoning is to stop recovering and ask
|
|
26
|
+
* the host to reload/recreate the JS realm before trying again.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ZfbMdWasmTrapRecoveryLimitError extends Error {
|
|
29
|
+
constructor(maxRecoveries: number, cause: unknown);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Eagerly loads and instantiates the wasm module. Optional -- `compile` /
|
|
33
|
+
* `renderHtml` / `version` all call this implicitly on first use -- but
|
|
34
|
+
* useful to front-load the one-time fetch/compile cost, e.g. on app
|
|
35
|
+
* startup before the first user-triggered call.
|
|
36
|
+
*/
|
|
37
|
+
export declare function init(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Compile MDX source into ES-module JavaScript. Mirrors the crate's
|
|
40
|
+
* `compile(source, options_json) -> string` export, with JSON
|
|
41
|
+
* marshaling handled for you.
|
|
42
|
+
*/
|
|
43
|
+
export declare function compile(source: string, options?: ZfbMdWasmOptions): Promise<CompileResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Render markdown source to an HTML fragment (no SWC at runtime). Mirrors
|
|
46
|
+
* the crate's `renderHtml(source, options_json) -> string` export, with
|
|
47
|
+
* JSON marshaling handled for you.
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderHtml(source: string, options?: ZfbMdWasmOptions): Promise<RenderHtmlResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Package version for host-side compatibility checks.
|
|
52
|
+
*
|
|
53
|
+
* Release artifacts are stamped with the published package semver at build
|
|
54
|
+
* time; local development builds fall back to the Rust manifest placeholder.
|
|
55
|
+
*/
|
|
56
|
+
export declare function version(): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* @internal Test-only (zfb#1577's trap/re-init test). Forces a genuine wasm
|
|
59
|
+
* trap on the currently-active instance by invoking its raw `compile` export
|
|
60
|
+
* with a garbage return pointer, so the result-doubleword `i32.store` lands
|
|
61
|
+
* outside the instance's linear memory -- a `WebAssembly.RuntimeError:
|
|
62
|
+
* memory access out of bounds`. From the JS host's point of view this is
|
|
63
|
+
* the same exception class a real Rust panic->unreachable trap produces
|
|
64
|
+
* (both are `instanceof WebAssembly.RuntimeError`), which is what
|
|
65
|
+
* `callWasm`'s catch clause keys on -- so this exercises the real
|
|
66
|
+
* catch-and-reinstantiate path without needing to coax an actual Rust panic
|
|
67
|
+
* out of a crate that is deliberately designed to never panic on structured
|
|
68
|
+
* input. This is NOT part of the supported public API: it is a named export
|
|
69
|
+
* of the package entry, so a `import { __forceTrapForTests } from
|
|
70
|
+
* "@takazudo/zfb-md-wasm"` does resolve -- but the `__`-prefix marks it
|
|
71
|
+
* internal, and the raw ABI details it pokes at (argument order, retptr
|
|
72
|
+
* convention) are wasm-bindgen internals that can change on a wasm-bindgen
|
|
73
|
+
* version bump. Do not call it outside this package's own tests.
|
|
74
|
+
*/
|
|
75
|
+
export declare function __forceTrapForTests(): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* @internal Test-only observability for the trap recovery contract. This is a
|
|
78
|
+
* named export only so the built-package Vitest suite can assert the public
|
|
79
|
+
* wrapper's concurrency/cap behavior without mocking the wasm-bindgen glue.
|
|
80
|
+
*/
|
|
81
|
+
export declare function __getTrapRecoveryStateForTests(): {
|
|
82
|
+
currentGeneration: number;
|
|
83
|
+
freshInstanceStarts: number;
|
|
84
|
+
maxTrapRecoveries: number;
|
|
85
|
+
trapRecoveriesStarted: number;
|
|
86
|
+
terminal: boolean;
|
|
87
|
+
};
|
|
88
|
+
export type { CompileResult, RenderHtmlResult, Diagnostic, DiagnosticSource, ZfbMdWasmOptions, PipelineOptions, GfmOptions, MarkdownFeaturesConfig, JsxRuntime, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const GLUE_URL = new URL("./wasm/zfb_md_wasm.js", import.meta.url);
|
|
2
|
+
const WASM_URL = new URL("./wasm/zfb_md_wasm_bg.wasm", import.meta.url);
|
|
3
|
+
function isNode() {
|
|
4
|
+
return typeof process !== "undefined" && !!process.versions?.node;
|
|
5
|
+
}
|
|
6
|
+
// Node's built-in `fetch` does not support `file:` URLs, so the wasm bytes
|
|
7
|
+
// must be read from disk directly there; browsers (and other fetch-capable
|
|
8
|
+
// hosts, e.g. a bundler-served dev server) go through `fetch` against the
|
|
9
|
+
// module-relative URL, which is the standard wasm-bindgen `--target web`
|
|
10
|
+
// browser consumption path.
|
|
11
|
+
async function loadWasmBytes() {
|
|
12
|
+
if (isNode()) {
|
|
13
|
+
const [{ readFile }, { fileURLToPath }] = await Promise.all([
|
|
14
|
+
import("node:fs/promises"),
|
|
15
|
+
import("node:url"),
|
|
16
|
+
]);
|
|
17
|
+
const buf = await readFile(fileURLToPath(WASM_URL));
|
|
18
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
19
|
+
}
|
|
20
|
+
const res = await fetch(WASM_URL);
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
throw new Error(`zfb-md-wasm: failed to fetch wasm binary: ${res.status} ${res.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
return res.arrayBuffer();
|
|
25
|
+
}
|
|
26
|
+
let compiledModulePromise;
|
|
27
|
+
function getCompiledModule() {
|
|
28
|
+
compiledModulePromise ??= loadWasmBytes().then((bytes) => WebAssembly.compile(bytes));
|
|
29
|
+
return compiledModulePromise;
|
|
30
|
+
}
|
|
31
|
+
const MAX_TRAP_RECOVERIES = 16;
|
|
32
|
+
let currentGeneration = 0;
|
|
33
|
+
let trapRecoveriesStarted = 0;
|
|
34
|
+
let freshInstanceStarts = 0;
|
|
35
|
+
let terminalTrapRecoveryError;
|
|
36
|
+
/**
|
|
37
|
+
* Instantiates a fresh copy of the wasm-bindgen glue module.
|
|
38
|
+
*
|
|
39
|
+
* wasm-bindgen's `--target web` output guards `init`/`initSync` with an
|
|
40
|
+
* "already initialized" early return keyed on a module-scope singleton, so
|
|
41
|
+
* recovering from a trapped (poisoned) instance requires a genuinely fresh ES
|
|
42
|
+
* module record -- re-calling `initSync` on the existing module record is a
|
|
43
|
+
* no-op. Re-importing the same specifier with a distinct query string yields a
|
|
44
|
+
* new module record per the ECMAScript module spec (Node and browsers agree on
|
|
45
|
+
* this); the underlying `WebAssembly.Module` (compiled code) is cached and
|
|
46
|
+
* reused via `getCompiledModule`, so this only re-runs cheap instantiation,
|
|
47
|
+
* never a recompile. Module-record growth is bounded by
|
|
48
|
+
* `MAX_TRAP_RECOVERIES`: once that many poisoned instances have been replaced,
|
|
49
|
+
* recovery stops permanently and callers receive
|
|
50
|
+
* `ZfbMdWasmTrapRecoveryLimitError` instead of minting another module record.
|
|
51
|
+
*/
|
|
52
|
+
async function freshInstance(generation) {
|
|
53
|
+
freshInstanceStarts += 1;
|
|
54
|
+
const [module, glue] = await Promise.all([
|
|
55
|
+
getCompiledModule(),
|
|
56
|
+
import(
|
|
57
|
+
/* @vite-ignore */ `${GLUE_URL.href}?zfbMdWasmGen=${generation}`),
|
|
58
|
+
]);
|
|
59
|
+
const raw = glue.initSync({ module });
|
|
60
|
+
return { generation, glue, raw };
|
|
61
|
+
}
|
|
62
|
+
let instancePromise;
|
|
63
|
+
function getInstance() {
|
|
64
|
+
if (terminalTrapRecoveryError) {
|
|
65
|
+
return Promise.reject(terminalTrapRecoveryError);
|
|
66
|
+
}
|
|
67
|
+
instancePromise ??= freshInstance(currentGeneration);
|
|
68
|
+
return instancePromise;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Thrown when a wasm call traps (a Rust panic, or another internal fault
|
|
72
|
+
* that lowers to a wasm trap). This is always a bug, never an expected
|
|
73
|
+
* input-validation failure -- expected failures (parse errors, malformed
|
|
74
|
+
* options JSON, unknown themes, ...) come back as structured
|
|
75
|
+
* `Diagnostic[]` in a normal `CompileResult`/`RenderHtmlResult`, never as a
|
|
76
|
+
* thrown error. See this package's README "Error / trap / re-init
|
|
77
|
+
* contract" section.
|
|
78
|
+
*
|
|
79
|
+
* By the time this is thrown, the poisoned instance has already been dropped
|
|
80
|
+
* and a replacement has been instantiated (from the cached compiled module, so
|
|
81
|
+
* no recompile). Concurrent trap reporters for the same poisoned generation
|
|
82
|
+
* wait for that same replacement instead of each starting another one; the
|
|
83
|
+
* next `compile` / `renderHtml` / `version` call transparently uses the fresh
|
|
84
|
+
* instance.
|
|
85
|
+
*/
|
|
86
|
+
export class ZfbMdWasmTrapError extends Error {
|
|
87
|
+
constructor(cause) {
|
|
88
|
+
super("zfb-md-wasm: the wasm instance trapped (a Rust panic or internal fault) and has been " +
|
|
89
|
+
"automatically re-instantiated. This is always a bug in zfb-md-wasm -- please report it " +
|
|
90
|
+
"with the input that triggered it.");
|
|
91
|
+
this.name = "ZfbMdWasmTrapError";
|
|
92
|
+
this.cause = cause;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Thrown once this wrapper has already recovered from too many wasm traps in
|
|
97
|
+
* one module lifetime. ES module records cannot be evicted, and wasm-bindgen
|
|
98
|
+
* `--target web` glue cannot be safely re-used after initialization, so the
|
|
99
|
+
* only bounded behavior after repeated poisoning is to stop recovering and ask
|
|
100
|
+
* the host to reload/recreate the JS realm before trying again.
|
|
101
|
+
*/
|
|
102
|
+
export class ZfbMdWasmTrapRecoveryLimitError extends Error {
|
|
103
|
+
constructor(maxRecoveries, cause) {
|
|
104
|
+
super(`zfb-md-wasm: wasm trap recovery limit reached after ${maxRecoveries} ` +
|
|
105
|
+
`successful re-instantiations. Further automatic recovery is disabled to avoid ` +
|
|
106
|
+
`unbounded ES module record growth. Reload the JS realm before using zfb-md-wasm ` +
|
|
107
|
+
`again, and please report the input that triggered the repeated traps.`);
|
|
108
|
+
this.name = "ZfbMdWasmTrapRecoveryLimitError";
|
|
109
|
+
this.cause = cause;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function isTrap(err) {
|
|
113
|
+
return typeof WebAssembly !== "undefined" && err instanceof WebAssembly.RuntimeError;
|
|
114
|
+
}
|
|
115
|
+
async function recoverAfterTrap(observedGeneration, cause) {
|
|
116
|
+
if (terminalTrapRecoveryError) {
|
|
117
|
+
throw terminalTrapRecoveryError;
|
|
118
|
+
}
|
|
119
|
+
// CAS-style single-flight: only the reporter that observed the currently
|
|
120
|
+
// active generation advances it and starts a fresh instantiation. Other
|
|
121
|
+
// concurrent reporters trapped on an older instance, so they await the
|
|
122
|
+
// already-started replacement instead of minting another module record.
|
|
123
|
+
if (observedGeneration !== currentGeneration) {
|
|
124
|
+
await getInstance();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (trapRecoveriesStarted >= MAX_TRAP_RECOVERIES) {
|
|
128
|
+
terminalTrapRecoveryError = new ZfbMdWasmTrapRecoveryLimitError(MAX_TRAP_RECOVERIES, cause);
|
|
129
|
+
instancePromise = undefined;
|
|
130
|
+
throw terminalTrapRecoveryError;
|
|
131
|
+
}
|
|
132
|
+
trapRecoveriesStarted += 1;
|
|
133
|
+
currentGeneration += 1;
|
|
134
|
+
instancePromise = freshInstance(currentGeneration);
|
|
135
|
+
await instancePromise;
|
|
136
|
+
}
|
|
137
|
+
async function callWasm(fn) {
|
|
138
|
+
const instance = await getInstance();
|
|
139
|
+
try {
|
|
140
|
+
return fn(instance);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (!isTrap(err)) {
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
// Drop the poisoned instance and re-instantiate it from the cached compiled
|
|
147
|
+
// module. Concurrent trap reporters single-flight through the same
|
|
148
|
+
// generation advance, so one poisoned generation creates at most one fresh
|
|
149
|
+
// module record.
|
|
150
|
+
await recoverAfterTrap(instance.generation, err);
|
|
151
|
+
throw new ZfbMdWasmTrapError(err);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Eagerly loads and instantiates the wasm module. Optional -- `compile` /
|
|
156
|
+
* `renderHtml` / `version` all call this implicitly on first use -- but
|
|
157
|
+
* useful to front-load the one-time fetch/compile cost, e.g. on app
|
|
158
|
+
* startup before the first user-triggered call.
|
|
159
|
+
*/
|
|
160
|
+
export async function init() {
|
|
161
|
+
await getInstance();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Compile MDX source into ES-module JavaScript. Mirrors the crate's
|
|
165
|
+
* `compile(source, options_json) -> string` export, with JSON
|
|
166
|
+
* marshaling handled for you.
|
|
167
|
+
*/
|
|
168
|
+
export async function compile(source, options = {}) {
|
|
169
|
+
const optionsJson = JSON.stringify(options);
|
|
170
|
+
const json = await callWasm(({ glue }) => glue.compile(source, optionsJson));
|
|
171
|
+
return JSON.parse(json);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Render markdown source to an HTML fragment (no SWC at runtime). Mirrors
|
|
175
|
+
* the crate's `renderHtml(source, options_json) -> string` export, with
|
|
176
|
+
* JSON marshaling handled for you.
|
|
177
|
+
*/
|
|
178
|
+
export async function renderHtml(source, options = {}) {
|
|
179
|
+
const optionsJson = JSON.stringify(options);
|
|
180
|
+
const json = await callWasm(({ glue }) => glue.renderHtml(source, optionsJson));
|
|
181
|
+
return JSON.parse(json);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Package version for host-side compatibility checks.
|
|
185
|
+
*
|
|
186
|
+
* Release artifacts are stamped with the published package semver at build
|
|
187
|
+
* time; local development builds fall back to the Rust manifest placeholder.
|
|
188
|
+
*/
|
|
189
|
+
export async function version() {
|
|
190
|
+
return callWasm(({ glue }) => glue.version());
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* @internal Test-only (zfb#1577's trap/re-init test). Forces a genuine wasm
|
|
194
|
+
* trap on the currently-active instance by invoking its raw `compile` export
|
|
195
|
+
* with a garbage return pointer, so the result-doubleword `i32.store` lands
|
|
196
|
+
* outside the instance's linear memory -- a `WebAssembly.RuntimeError:
|
|
197
|
+
* memory access out of bounds`. From the JS host's point of view this is
|
|
198
|
+
* the same exception class a real Rust panic->unreachable trap produces
|
|
199
|
+
* (both are `instanceof WebAssembly.RuntimeError`), which is what
|
|
200
|
+
* `callWasm`'s catch clause keys on -- so this exercises the real
|
|
201
|
+
* catch-and-reinstantiate path without needing to coax an actual Rust panic
|
|
202
|
+
* out of a crate that is deliberately designed to never panic on structured
|
|
203
|
+
* input. This is NOT part of the supported public API: it is a named export
|
|
204
|
+
* of the package entry, so a `import { __forceTrapForTests } from
|
|
205
|
+
* "@takazudo/zfb-md-wasm"` does resolve -- but the `__`-prefix marks it
|
|
206
|
+
* internal, and the raw ABI details it pokes at (argument order, retptr
|
|
207
|
+
* convention) are wasm-bindgen internals that can change on a wasm-bindgen
|
|
208
|
+
* version bump. Do not call it outside this package's own tests.
|
|
209
|
+
*/
|
|
210
|
+
export async function __forceTrapForTests() {
|
|
211
|
+
await callWasm(({ raw }) => {
|
|
212
|
+
raw.compile(0xfffffff0, 0, 0, 0, 0);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* @internal Test-only observability for the trap recovery contract. This is a
|
|
217
|
+
* named export only so the built-package Vitest suite can assert the public
|
|
218
|
+
* wrapper's concurrency/cap behavior without mocking the wasm-bindgen glue.
|
|
219
|
+
*/
|
|
220
|
+
export function __getTrapRecoveryStateForTests() {
|
|
221
|
+
return {
|
|
222
|
+
currentGeneration,
|
|
223
|
+
freshInstanceStarts,
|
|
224
|
+
maxTrapRecoveries: MAX_TRAP_RECOVERIES,
|
|
225
|
+
trapRecoveriesStarted,
|
|
226
|
+
terminal: !!terminalTrapRecoveryError,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CompileResult, RenderHtmlResult, ZfbMdWasmOptions } from \"./types.js\";\n\n// The wasm-bindgen-generated glue lives at ./wasm/zfb_md_wasm.{js,d.ts} plus\n// the ./wasm/zfb_md_wasm_bg.wasm binary, produced by `pnpm build`\n// (scripts/build.mjs) -- not checked in, see .gitignore. `pnpm build` must\n// run before this module is imported (or before `tsc` can typecheck it).\ntype WasmGlueModule = typeof import(\"./wasm/zfb_md_wasm.js\");\ntype WasmRawExports = import(\"./wasm/zfb_md_wasm.js\").InitOutput;\n\nconst GLUE_URL = new URL(\"./wasm/zfb_md_wasm.js\", import.meta.url);\nconst WASM_URL = new URL(\"./wasm/zfb_md_wasm_bg.wasm\", import.meta.url);\n\nfunction isNode(): boolean {\n return typeof process !== \"undefined\" && !!process.versions?.node;\n}\n\n// Node's built-in `fetch` does not support `file:` URLs, so the wasm bytes\n// must be read from disk directly there; browsers (and other fetch-capable\n// hosts, e.g. a bundler-served dev server) go through `fetch` against the\n// module-relative URL, which is the standard wasm-bindgen `--target web`\n// browser consumption path.\nasync function loadWasmBytes(): Promise<ArrayBuffer> {\n if (isNode()) {\n const [{ readFile }, { fileURLToPath }] = await Promise.all([\n import(\"node:fs/promises\"),\n import(\"node:url\"),\n ]);\n const buf = await readFile(fileURLToPath(WASM_URL));\n return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;\n }\n const res = await fetch(WASM_URL);\n if (!res.ok) {\n throw new Error(`zfb-md-wasm: failed to fetch wasm binary: ${res.status} ${res.statusText}`);\n }\n return res.arrayBuffer();\n}\n\nlet compiledModulePromise: Promise<WebAssembly.Module> | undefined;\n\nfunction getCompiledModule(): Promise<WebAssembly.Module> {\n compiledModulePromise ??= loadWasmBytes().then((bytes) => WebAssembly.compile(bytes));\n return compiledModulePromise;\n}\n\nconst MAX_TRAP_RECOVERIES = 16;\n\nlet currentGeneration = 0;\nlet trapRecoveriesStarted = 0;\nlet freshInstanceStarts = 0;\nlet terminalTrapRecoveryError: ZfbMdWasmTrapRecoveryLimitError | undefined;\n\ninterface Instance {\n generation: number;\n glue: WasmGlueModule;\n raw: WasmRawExports;\n}\n\n/**\n * Instantiates a fresh copy of the wasm-bindgen glue module.\n *\n * wasm-bindgen's `--target web` output guards `init`/`initSync` with an\n * \"already initialized\" early return keyed on a module-scope singleton, so\n * recovering from a trapped (poisoned) instance requires a genuinely fresh ES\n * module record -- re-calling `initSync` on the existing module record is a\n * no-op. Re-importing the same specifier with a distinct query string yields a\n * new module record per the ECMAScript module spec (Node and browsers agree on\n * this); the underlying `WebAssembly.Module` (compiled code) is cached and\n * reused via `getCompiledModule`, so this only re-runs cheap instantiation,\n * never a recompile. Module-record growth is bounded by\n * `MAX_TRAP_RECOVERIES`: once that many poisoned instances have been replaced,\n * recovery stops permanently and callers receive\n * `ZfbMdWasmTrapRecoveryLimitError` instead of minting another module record.\n */\nasync function freshInstance(generation: number): Promise<Instance> {\n freshInstanceStarts += 1;\n const [module, glue] = await Promise.all([\n getCompiledModule(),\n import(\n /* @vite-ignore */ `${GLUE_URL.href}?zfbMdWasmGen=${generation}`\n ) as Promise<WasmGlueModule>,\n ]);\n const raw = glue.initSync({ module });\n return { generation, glue, raw };\n}\n\nlet instancePromise: Promise<Instance> | undefined;\n\nfunction getInstance(): Promise<Instance> {\n if (terminalTrapRecoveryError) {\n return Promise.reject(terminalTrapRecoveryError);\n }\n instancePromise ??= freshInstance(currentGeneration);\n return instancePromise;\n}\n\n/**\n * Thrown when a wasm call traps (a Rust panic, or another internal fault\n * that lowers to a wasm trap). This is always a bug, never an expected\n * input-validation failure -- expected failures (parse errors, malformed\n * options JSON, unknown themes, ...) come back as structured\n * `Diagnostic[]` in a normal `CompileResult`/`RenderHtmlResult`, never as a\n * thrown error. See this package's README \"Error / trap / re-init\n * contract\" section.\n *\n * By the time this is thrown, the poisoned instance has already been dropped\n * and a replacement has been instantiated (from the cached compiled module, so\n * no recompile). Concurrent trap reporters for the same poisoned generation\n * wait for that same replacement instead of each starting another one; the\n * next `compile` / `renderHtml` / `version` call transparently uses the fresh\n * instance.\n */\nexport class ZfbMdWasmTrapError extends Error {\n constructor(cause: unknown) {\n super(\n \"zfb-md-wasm: the wasm instance trapped (a Rust panic or internal fault) and has been \" +\n \"automatically re-instantiated. This is always a bug in zfb-md-wasm -- please report it \" +\n \"with the input that triggered it.\",\n );\n this.name = \"ZfbMdWasmTrapError\";\n this.cause = cause;\n }\n}\n\n/**\n * Thrown once this wrapper has already recovered from too many wasm traps in\n * one module lifetime. ES module records cannot be evicted, and wasm-bindgen\n * `--target web` glue cannot be safely re-used after initialization, so the\n * only bounded behavior after repeated poisoning is to stop recovering and ask\n * the host to reload/recreate the JS realm before trying again.\n */\nexport class ZfbMdWasmTrapRecoveryLimitError extends Error {\n constructor(maxRecoveries: number, cause: unknown) {\n super(\n `zfb-md-wasm: wasm trap recovery limit reached after ${maxRecoveries} ` +\n `successful re-instantiations. Further automatic recovery is disabled to avoid ` +\n `unbounded ES module record growth. Reload the JS realm before using zfb-md-wasm ` +\n `again, and please report the input that triggered the repeated traps.`,\n );\n this.name = \"ZfbMdWasmTrapRecoveryLimitError\";\n this.cause = cause;\n }\n}\n\nfunction isTrap(err: unknown): boolean {\n return typeof WebAssembly !== \"undefined\" && err instanceof WebAssembly.RuntimeError;\n}\n\nasync function recoverAfterTrap(observedGeneration: number, cause: unknown): Promise<void> {\n if (terminalTrapRecoveryError) {\n throw terminalTrapRecoveryError;\n }\n\n // CAS-style single-flight: only the reporter that observed the currently\n // active generation advances it and starts a fresh instantiation. Other\n // concurrent reporters trapped on an older instance, so they await the\n // already-started replacement instead of minting another module record.\n if (observedGeneration !== currentGeneration) {\n await getInstance();\n return;\n }\n\n if (trapRecoveriesStarted >= MAX_TRAP_RECOVERIES) {\n terminalTrapRecoveryError = new ZfbMdWasmTrapRecoveryLimitError(MAX_TRAP_RECOVERIES, cause);\n instancePromise = undefined;\n throw terminalTrapRecoveryError;\n }\n\n trapRecoveriesStarted += 1;\n currentGeneration += 1;\n instancePromise = freshInstance(currentGeneration);\n await instancePromise;\n}\n\nasync function callWasm<T>(fn: (instance: Instance) => T): Promise<T> {\n const instance = await getInstance();\n try {\n return fn(instance);\n } catch (err) {\n if (!isTrap(err)) {\n throw err;\n }\n // Drop the poisoned instance and re-instantiate it from the cached compiled\n // module. Concurrent trap reporters single-flight through the same\n // generation advance, so one poisoned generation creates at most one fresh\n // module record.\n await recoverAfterTrap(instance.generation, err);\n throw new ZfbMdWasmTrapError(err);\n }\n}\n\n/**\n * Eagerly loads and instantiates the wasm module. Optional -- `compile` /\n * `renderHtml` / `version` all call this implicitly on first use -- but\n * useful to front-load the one-time fetch/compile cost, e.g. on app\n * startup before the first user-triggered call.\n */\nexport async function init(): Promise<void> {\n await getInstance();\n}\n\n/**\n * Compile MDX source into ES-module JavaScript. Mirrors the crate's\n * `compile(source, options_json) -> string` export, with JSON\n * marshaling handled for you.\n */\nexport async function compile(\n source: string,\n options: ZfbMdWasmOptions = {},\n): Promise<CompileResult> {\n const optionsJson = JSON.stringify(options);\n const json = await callWasm(({ glue }) => glue.compile(source, optionsJson));\n return JSON.parse(json) as CompileResult;\n}\n\n/**\n * Render markdown source to an HTML fragment (no SWC at runtime). Mirrors\n * the crate's `renderHtml(source, options_json) -> string` export, with\n * JSON marshaling handled for you.\n */\nexport async function renderHtml(\n source: string,\n options: ZfbMdWasmOptions = {},\n): Promise<RenderHtmlResult> {\n const optionsJson = JSON.stringify(options);\n const json = await callWasm(({ glue }) => glue.renderHtml(source, optionsJson));\n return JSON.parse(json) as RenderHtmlResult;\n}\n\n/**\n * Package version for host-side compatibility checks.\n *\n * Release artifacts are stamped with the published package semver at build\n * time; local development builds fall back to the Rust manifest placeholder.\n */\nexport async function version(): Promise<string> {\n return callWasm(({ glue }) => glue.version());\n}\n\n/**\n * @internal Test-only (zfb#1577's trap/re-init test). Forces a genuine wasm\n * trap on the currently-active instance by invoking its raw `compile` export\n * with a garbage return pointer, so the result-doubleword `i32.store` lands\n * outside the instance's linear memory -- a `WebAssembly.RuntimeError:\n * memory access out of bounds`. From the JS host's point of view this is\n * the same exception class a real Rust panic->unreachable trap produces\n * (both are `instanceof WebAssembly.RuntimeError`), which is what\n * `callWasm`'s catch clause keys on -- so this exercises the real\n * catch-and-reinstantiate path without needing to coax an actual Rust panic\n * out of a crate that is deliberately designed to never panic on structured\n * input. This is NOT part of the supported public API: it is a named export\n * of the package entry, so a `import { __forceTrapForTests } from\n * \"@takazudo/zfb-md-wasm\"` does resolve -- but the `__`-prefix marks it\n * internal, and the raw ABI details it pokes at (argument order, retptr\n * convention) are wasm-bindgen internals that can change on a wasm-bindgen\n * version bump. Do not call it outside this package's own tests.\n */\nexport async function __forceTrapForTests(): Promise<void> {\n await callWasm(({ raw }) => {\n raw.compile(0xfffffff0, 0, 0, 0, 0);\n });\n}\n\n/**\n * @internal Test-only observability for the trap recovery contract. This is a\n * named export only so the built-package Vitest suite can assert the public\n * wrapper's concurrency/cap behavior without mocking the wasm-bindgen glue.\n */\nexport function __getTrapRecoveryStateForTests(): {\n currentGeneration: number;\n freshInstanceStarts: number;\n maxTrapRecoveries: number;\n trapRecoveriesStarted: number;\n terminal: boolean;\n} {\n return {\n currentGeneration,\n freshInstanceStarts,\n maxTrapRecoveries: MAX_TRAP_RECOVERIES,\n trapRecoveriesStarted,\n terminal: !!terminalTrapRecoveryError,\n };\n}\n\nexport type {\n CompileResult,\n RenderHtmlResult,\n Diagnostic,\n DiagnosticSource,\n ZfbMdWasmOptions,\n PipelineOptions,\n GfmOptions,\n MarkdownFeaturesConfig,\n JsxRuntime,\n} from \"./types.js\";\n"]}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** `zfb_content::facade::PipelineOptions`'s `gfm` sub-object, verbatim. */
|
|
2
|
+
export interface GfmOptions {
|
|
3
|
+
strikethrough?: boolean;
|
|
4
|
+
table?: boolean;
|
|
5
|
+
autolinkLiteral?: boolean;
|
|
6
|
+
taskListItem?: boolean;
|
|
7
|
+
footnoteDefinition?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `MarkdownFeaturesConfig` (see `crates/zfb-md-ast/src/features_config.rs`).
|
|
11
|
+
* Left as an open record here -- the wasm boundary passes it through to the
|
|
12
|
+
* Rust `deny_unknown_fields` deserializer verbatim, which is the
|
|
13
|
+
* authoritative validator for its keys.
|
|
14
|
+
*/
|
|
15
|
+
export type MarkdownFeaturesConfig = Record<string, unknown>;
|
|
16
|
+
/** `zfb_content::facade::PipelineOptions`, verbatim. */
|
|
17
|
+
export interface PipelineOptions {
|
|
18
|
+
/** A syntect theme name, or `null` for no syntax highlighting. */
|
|
19
|
+
theme?: string | null;
|
|
20
|
+
gfm?: GfmOptions;
|
|
21
|
+
cjkFriendly?: boolean;
|
|
22
|
+
hardBreaks?: boolean;
|
|
23
|
+
features?: MarkdownFeaturesConfig;
|
|
24
|
+
}
|
|
25
|
+
/** `jsxRuntime` option values. Consumed only by `compile`. */
|
|
26
|
+
export type JsxRuntime = "preact" | "react";
|
|
27
|
+
/**
|
|
28
|
+
* The options JSON document shared by `compile` and `renderHtml`. Every
|
|
29
|
+
* field is optional; `{}` selects all defaults. Unknown fields are
|
|
30
|
+
* rejected by the Rust side at both nesting levels (`deny_unknown_fields`).
|
|
31
|
+
*/
|
|
32
|
+
export interface ZfbMdWasmOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Must end in `.md` or `.mdx`. Drives frontmatter dispatch and
|
|
35
|
+
* diagnostics display. Defaults to `<anonymous>.mdx` for `compile` and
|
|
36
|
+
* `<anonymous>.md` for `renderHtml`.
|
|
37
|
+
*/
|
|
38
|
+
filename?: string;
|
|
39
|
+
/** Consumed only by `compile`; `renderHtml` accepts and ignores it. */
|
|
40
|
+
jsxRuntime?: JsxRuntime;
|
|
41
|
+
/** Consumed only by `compile`; `renderHtml` accepts and ignores it. */
|
|
42
|
+
development?: boolean;
|
|
43
|
+
pipeline?: PipelineOptions;
|
|
44
|
+
}
|
|
45
|
+
/** `source` values a `Diagnostic` can carry. */
|
|
46
|
+
export type DiagnosticSource = "options" | "frontmatter" | "markdown" | "compile";
|
|
47
|
+
/**
|
|
48
|
+
* One diagnostic entry. `line`/`column` are 1-based. For `"markdown"` /
|
|
49
|
+
* `"frontmatter"` they point into the *original source* (frontmatter lines
|
|
50
|
+
* included). For `"options"` they point into the *options JSON document*.
|
|
51
|
+
* `null` when the underlying error carries no location.
|
|
52
|
+
*/
|
|
53
|
+
export interface Diagnostic {
|
|
54
|
+
severity: "error";
|
|
55
|
+
source: DiagnosticSource;
|
|
56
|
+
message: string;
|
|
57
|
+
line: number | null;
|
|
58
|
+
column: number | null;
|
|
59
|
+
}
|
|
60
|
+
/** Result document of `compile`. */
|
|
61
|
+
export interface CompileResult {
|
|
62
|
+
/** ES-module JS source on success, `null` on failure. */
|
|
63
|
+
code: string | null;
|
|
64
|
+
/** Parsed YAML frontmatter as JSON, `null` when absent or unextractable. */
|
|
65
|
+
frontmatter: unknown;
|
|
66
|
+
diagnostics: Diagnostic[];
|
|
67
|
+
}
|
|
68
|
+
/** Result document of `renderHtml`. */
|
|
69
|
+
export interface RenderHtmlResult {
|
|
70
|
+
/** HTML fragment on success, `null` on failure. */
|
|
71
|
+
html: string | null;
|
|
72
|
+
frontmatter: unknown;
|
|
73
|
+
diagnostics: Diagnostic[];
|
|
74
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Mirrors the JSON contracts of the `zfb-md-wasm` Rust crate (zfb#1576)
|
|
2
|
+
// literally -- see that crate's `src/lib.rs` rustdoc for the authoritative
|
|
3
|
+
// shape. Keep this file in lock-step with the crate when the crate's
|
|
4
|
+
// options/result shapes change.
|
|
5
|
+
export {};
|
|
6
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,2EAA2E;AAC3E,qEAAqE;AACrE,gCAAgC","sourcesContent":["// Mirrors the JSON contracts of the `zfb-md-wasm` Rust crate (zfb#1576)\n// literally -- see that crate's `src/lib.rs` rustdoc for the authoritative\n// shape. Keep this file in lock-step with the crate when the crate's\n// options/result shapes change.\n\n/** `zfb_content::facade::PipelineOptions`'s `gfm` sub-object, verbatim. */\nexport interface GfmOptions {\n strikethrough?: boolean;\n table?: boolean;\n autolinkLiteral?: boolean;\n taskListItem?: boolean;\n footnoteDefinition?: boolean;\n}\n\n/**\n * `MarkdownFeaturesConfig` (see `crates/zfb-md-ast/src/features_config.rs`).\n * Left as an open record here -- the wasm boundary passes it through to the\n * Rust `deny_unknown_fields` deserializer verbatim, which is the\n * authoritative validator for its keys.\n */\nexport type MarkdownFeaturesConfig = Record<string, unknown>;\n\n/** `zfb_content::facade::PipelineOptions`, verbatim. */\nexport interface PipelineOptions {\n /** A syntect theme name, or `null` for no syntax highlighting. */\n theme?: string | null;\n gfm?: GfmOptions;\n cjkFriendly?: boolean;\n hardBreaks?: boolean;\n features?: MarkdownFeaturesConfig;\n}\n\n/** `jsxRuntime` option values. Consumed only by `compile`. */\nexport type JsxRuntime = \"preact\" | \"react\";\n\n/**\n * The options JSON document shared by `compile` and `renderHtml`. Every\n * field is optional; `{}` selects all defaults. Unknown fields are\n * rejected by the Rust side at both nesting levels (`deny_unknown_fields`).\n */\nexport interface ZfbMdWasmOptions {\n /**\n * Must end in `.md` or `.mdx`. Drives frontmatter dispatch and\n * diagnostics display. Defaults to `<anonymous>.mdx` for `compile` and\n * `<anonymous>.md` for `renderHtml`.\n */\n filename?: string;\n /** Consumed only by `compile`; `renderHtml` accepts and ignores it. */\n jsxRuntime?: JsxRuntime;\n /** Consumed only by `compile`; `renderHtml` accepts and ignores it. */\n development?: boolean;\n pipeline?: PipelineOptions;\n}\n\n/** `source` values a `Diagnostic` can carry. */\nexport type DiagnosticSource = \"options\" | \"frontmatter\" | \"markdown\" | \"compile\";\n\n/**\n * One diagnostic entry. `line`/`column` are 1-based. For `\"markdown\"` /\n * `\"frontmatter\"` they point into the *original source* (frontmatter lines\n * included). For `\"options\"` they point into the *options JSON document*.\n * `null` when the underlying error carries no location.\n */\nexport interface Diagnostic {\n severity: \"error\";\n source: DiagnosticSource;\n message: string;\n line: number | null;\n column: number | null;\n}\n\n/** Result document of `compile`. */\nexport interface CompileResult {\n /** ES-module JS source on success, `null` on failure. */\n code: string | null;\n /** Parsed YAML frontmatter as JSON, `null` when absent or unextractable. */\n frontmatter: unknown;\n diagnostics: Diagnostic[];\n}\n\n/** Result document of `renderHtml`. */\nexport interface RenderHtmlResult {\n /** HTML fragment on success, `null` on failure. */\n html: string | null;\n frontmatter: unknown;\n diagnostics: Diagnostic[];\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compile MDX source into ES-module JavaScript.
|
|
6
|
+
*
|
|
7
|
+
* Returns a JSON string: `{ "code": string|null, "frontmatter": json,
|
|
8
|
+
* "diagnostics": Diagnostic[] }` — see the crate docs for the options
|
|
9
|
+
* and diagnostics shapes.
|
|
10
|
+
*/
|
|
11
|
+
export function compile(source: string, options_json: string): string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render markdown source to an HTML fragment (no SWC at runtime).
|
|
15
|
+
*
|
|
16
|
+
* Returns a JSON string: `{ "html": string|null, "frontmatter": json,
|
|
17
|
+
* "diagnostics": Diagnostic[] }` — see the crate docs for the options
|
|
18
|
+
* and diagnostics shapes. Exported to JS as `renderHtml`.
|
|
19
|
+
*/
|
|
20
|
+
export function renderHtml(source: string, options_json: string): string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* This package's release version, stamped by CI at compile time.
|
|
24
|
+
*
|
|
25
|
+
* Development builds without `ZFB_RELEASE_VERSION` fall back to this crate's
|
|
26
|
+
* manifest version (`CARGO_PKG_VERSION`).
|
|
27
|
+
*/
|
|
28
|
+
export function version(): string;
|
|
29
|
+
|
|
30
|
+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
31
|
+
|
|
32
|
+
export interface InitOutput {
|
|
33
|
+
readonly memory: WebAssembly.Memory;
|
|
34
|
+
readonly compile: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
35
|
+
readonly renderHtml: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
36
|
+
readonly version: (a: number) => void;
|
|
37
|
+
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
38
|
+
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
39
|
+
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
40
|
+
readonly __wbindgen_export3: (a: number, b: number, c: number) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
47
|
+
* a precompiled `WebAssembly.Module`.
|
|
48
|
+
*
|
|
49
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
50
|
+
*
|
|
51
|
+
* @returns {InitOutput}
|
|
52
|
+
*/
|
|
53
|
+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
57
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
58
|
+
*
|
|
59
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
60
|
+
*
|
|
61
|
+
* @returns {Promise<InitOutput>}
|
|
62
|
+
*/
|
|
63
|
+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/* @ts-self-types="./zfb_md_wasm.d.ts" */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compile MDX source into ES-module JavaScript.
|
|
5
|
+
*
|
|
6
|
+
* Returns a JSON string: `{ "code": string|null, "frontmatter": json,
|
|
7
|
+
* "diagnostics": Diagnostic[] }` — see the crate docs for the options
|
|
8
|
+
* and diagnostics shapes.
|
|
9
|
+
* @param {string} source
|
|
10
|
+
* @param {string} options_json
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function compile(source, options_json) {
|
|
14
|
+
let deferred3_0;
|
|
15
|
+
let deferred3_1;
|
|
16
|
+
try {
|
|
17
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
18
|
+
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
19
|
+
const len0 = WASM_VECTOR_LEN;
|
|
20
|
+
const ptr1 = passStringToWasm0(options_json, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
21
|
+
const len1 = WASM_VECTOR_LEN;
|
|
22
|
+
wasm.compile(retptr, ptr0, len0, ptr1, len1);
|
|
23
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
24
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
25
|
+
deferred3_0 = r0;
|
|
26
|
+
deferred3_1 = r1;
|
|
27
|
+
return getStringFromWasm0(r0, r1);
|
|
28
|
+
} finally {
|
|
29
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
30
|
+
wasm.__wbindgen_export3(deferred3_0, deferred3_1, 1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render markdown source to an HTML fragment (no SWC at runtime).
|
|
36
|
+
*
|
|
37
|
+
* Returns a JSON string: `{ "html": string|null, "frontmatter": json,
|
|
38
|
+
* "diagnostics": Diagnostic[] }` — see the crate docs for the options
|
|
39
|
+
* and diagnostics shapes. Exported to JS as `renderHtml`.
|
|
40
|
+
* @param {string} source
|
|
41
|
+
* @param {string} options_json
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function renderHtml(source, options_json) {
|
|
45
|
+
let deferred3_0;
|
|
46
|
+
let deferred3_1;
|
|
47
|
+
try {
|
|
48
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
49
|
+
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
50
|
+
const len0 = WASM_VECTOR_LEN;
|
|
51
|
+
const ptr1 = passStringToWasm0(options_json, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
52
|
+
const len1 = WASM_VECTOR_LEN;
|
|
53
|
+
wasm.renderHtml(retptr, ptr0, len0, ptr1, len1);
|
|
54
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
55
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
56
|
+
deferred3_0 = r0;
|
|
57
|
+
deferred3_1 = r1;
|
|
58
|
+
return getStringFromWasm0(r0, r1);
|
|
59
|
+
} finally {
|
|
60
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
61
|
+
wasm.__wbindgen_export3(deferred3_0, deferred3_1, 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* This package's release version, stamped by CI at compile time.
|
|
67
|
+
*
|
|
68
|
+
* Development builds without `ZFB_RELEASE_VERSION` fall back to this crate's
|
|
69
|
+
* manifest version (`CARGO_PKG_VERSION`).
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function version() {
|
|
73
|
+
let deferred1_0;
|
|
74
|
+
let deferred1_1;
|
|
75
|
+
try {
|
|
76
|
+
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
77
|
+
wasm.version(retptr);
|
|
78
|
+
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
79
|
+
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
80
|
+
deferred1_0 = r0;
|
|
81
|
+
deferred1_1 = r1;
|
|
82
|
+
return getStringFromWasm0(r0, r1);
|
|
83
|
+
} finally {
|
|
84
|
+
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
85
|
+
wasm.__wbindgen_export3(deferred1_0, deferred1_1, 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function __wbg_get_imports() {
|
|
89
|
+
const import0 = {
|
|
90
|
+
__proto__: null,
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
__proto__: null,
|
|
94
|
+
"./zfb_md_wasm_bg.js": import0,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let cachedDataViewMemory0 = null;
|
|
99
|
+
function getDataViewMemory0() {
|
|
100
|
+
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
|
101
|
+
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
|
102
|
+
}
|
|
103
|
+
return cachedDataViewMemory0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getStringFromWasm0(ptr, len) {
|
|
107
|
+
return decodeText(ptr >>> 0, len);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let cachedUint8ArrayMemory0 = null;
|
|
111
|
+
function getUint8ArrayMemory0() {
|
|
112
|
+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
|
113
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
114
|
+
}
|
|
115
|
+
return cachedUint8ArrayMemory0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
119
|
+
if (realloc === undefined) {
|
|
120
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
121
|
+
const ptr = malloc(buf.length, 1) >>> 0;
|
|
122
|
+
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
|
123
|
+
WASM_VECTOR_LEN = buf.length;
|
|
124
|
+
return ptr;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let len = arg.length;
|
|
128
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
129
|
+
|
|
130
|
+
const mem = getUint8ArrayMemory0();
|
|
131
|
+
|
|
132
|
+
let offset = 0;
|
|
133
|
+
|
|
134
|
+
for (; offset < len; offset++) {
|
|
135
|
+
const code = arg.charCodeAt(offset);
|
|
136
|
+
if (code > 0x7F) break;
|
|
137
|
+
mem[ptr + offset] = code;
|
|
138
|
+
}
|
|
139
|
+
if (offset !== len) {
|
|
140
|
+
if (offset !== 0) {
|
|
141
|
+
arg = arg.slice(offset);
|
|
142
|
+
}
|
|
143
|
+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
144
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
145
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
146
|
+
|
|
147
|
+
offset += ret.written;
|
|
148
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
WASM_VECTOR_LEN = offset;
|
|
152
|
+
return ptr;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
156
|
+
cachedTextDecoder.decode();
|
|
157
|
+
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
|
158
|
+
let numBytesDecoded = 0;
|
|
159
|
+
function decodeText(ptr, len) {
|
|
160
|
+
numBytesDecoded += len;
|
|
161
|
+
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
|
162
|
+
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
163
|
+
cachedTextDecoder.decode();
|
|
164
|
+
numBytesDecoded = len;
|
|
165
|
+
}
|
|
166
|
+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const cachedTextEncoder = new TextEncoder();
|
|
170
|
+
|
|
171
|
+
if (!('encodeInto' in cachedTextEncoder)) {
|
|
172
|
+
cachedTextEncoder.encodeInto = function (arg, view) {
|
|
173
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
174
|
+
view.set(buf);
|
|
175
|
+
return {
|
|
176
|
+
read: arg.length,
|
|
177
|
+
written: buf.length
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let WASM_VECTOR_LEN = 0;
|
|
183
|
+
|
|
184
|
+
let wasmModule, wasmInstance, wasm;
|
|
185
|
+
function __wbg_finalize_init(instance, module) {
|
|
186
|
+
wasmInstance = instance;
|
|
187
|
+
wasm = instance.exports;
|
|
188
|
+
wasmModule = module;
|
|
189
|
+
cachedDataViewMemory0 = null;
|
|
190
|
+
cachedUint8ArrayMemory0 = null;
|
|
191
|
+
return wasm;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function __wbg_load(module, imports) {
|
|
195
|
+
if (typeof Response === 'function' && module instanceof Response) {
|
|
196
|
+
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
|
197
|
+
try {
|
|
198
|
+
return await WebAssembly.instantiateStreaming(module, imports);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
const validResponse = module.ok && expectedResponseType(module.type);
|
|
201
|
+
|
|
202
|
+
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
|
203
|
+
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
|
204
|
+
|
|
205
|
+
} else { throw e; }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const bytes = await module.arrayBuffer();
|
|
210
|
+
return await WebAssembly.instantiate(bytes, imports);
|
|
211
|
+
} else {
|
|
212
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
213
|
+
|
|
214
|
+
if (instance instanceof WebAssembly.Instance) {
|
|
215
|
+
return { instance, module };
|
|
216
|
+
} else {
|
|
217
|
+
return instance;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function expectedResponseType(type) {
|
|
222
|
+
switch (type) {
|
|
223
|
+
case 'basic': case 'cors': case 'default': return true;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function initSync(module) {
|
|
230
|
+
if (wasm !== undefined) return wasm;
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
if (module !== undefined) {
|
|
234
|
+
if (Object.getPrototypeOf(module) === Object.prototype) {
|
|
235
|
+
({module} = module)
|
|
236
|
+
} else {
|
|
237
|
+
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const imports = __wbg_get_imports();
|
|
242
|
+
if (!(module instanceof WebAssembly.Module)) {
|
|
243
|
+
module = new WebAssembly.Module(module);
|
|
244
|
+
}
|
|
245
|
+
const instance = new WebAssembly.Instance(module, imports);
|
|
246
|
+
return __wbg_finalize_init(instance, module);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function __wbg_init(module_or_path) {
|
|
250
|
+
if (wasm !== undefined) return wasm;
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if (module_or_path !== undefined) {
|
|
254
|
+
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
|
255
|
+
({module_or_path} = module_or_path)
|
|
256
|
+
} else {
|
|
257
|
+
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (module_or_path === undefined) {
|
|
262
|
+
module_or_path = new URL('zfb_md_wasm_bg.wasm', import.meta.url);
|
|
263
|
+
}
|
|
264
|
+
const imports = __wbg_get_imports();
|
|
265
|
+
|
|
266
|
+
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
|
267
|
+
module_or_path = fetch(module_or_path);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
|
271
|
+
|
|
272
|
+
return __wbg_finalize_init(instance, module);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export { initSync, __wbg_init as default };
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const compile: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
5
|
+
export const renderHtml: (a: number, b: number, c: number, d: number, e: number) => void;
|
|
6
|
+
export const version: (a: number) => void;
|
|
7
|
+
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
8
|
+
export const __wbindgen_export: (a: number, b: number) => number;
|
|
9
|
+
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
10
|
+
export const __wbindgen_export3: (a: number, b: number, c: number) => void;
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@takazudo/zfb-md-wasm",
|
|
3
|
+
"version": "0.1.0-next.80",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "zfb's md/mdx -> JS/HTML conversion pipeline compiled to WebAssembly, for browser-side dynamic conversion (CMS live preview with parity to zfb's production output).",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"zfb",
|
|
9
|
+
"zfb-md-wasm",
|
|
10
|
+
"markdown",
|
|
11
|
+
"mdx",
|
|
12
|
+
"wasm",
|
|
13
|
+
"webassembly",
|
|
14
|
+
"cms",
|
|
15
|
+
"live-preview"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Takeshi Takatsudo <takazudo@gmail.com> (https://github.com/Takazudo)",
|
|
19
|
+
"homepage": "https://takazudomodular.com/pj/zudo-front-builder/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Takazudo/zudo-front-builder.git",
|
|
23
|
+
"directory": "crates/zfb-md-wasm/npm"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Takazudo/zudo-front-builder/issues"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20.0.0",
|
|
46
|
+
"pnpm": ">=10.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.0.0",
|
|
50
|
+
"binaryen": "130.0.0",
|
|
51
|
+
"typescript": "^5.9.0",
|
|
52
|
+
"vitest": "^2.1.9"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "node scripts/build.mjs",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest",
|
|
58
|
+
"typecheck": "tsc --noEmit"
|
|
59
|
+
}
|
|
60
|
+
}
|