@markup-carve/carve-wasm 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -689
- package/carve_wasm.d.ts +160 -276
- package/carve_wasm.js +1 -1
- package/carve_wasm_bg.js +112 -38
- package/carve_wasm_bg.wasm +0 -0
- package/docs/compatibility.md +89 -0
- package/docs/reference.md +785 -0
- package/examples/carve-worker.mjs +35 -0
- package/examples/offsets.d.ts +3 -0
- package/examples/offsets.mjs +18 -0
- package/node/LICENSE +21 -0
- package/node/README.md +105 -0
- package/node/carve_wasm.d.ts +740 -0
- package/node/carve_wasm.js +2362 -0
- package/node/carve_wasm_bg.wasm.d.ts +75 -0
- package/node/package.json +19 -0
- package/package.json +44 -3
- package/render/LICENSE +21 -0
- package/render/README.md +105 -0
- package/render/carve_wasm.d.ts +339 -0
- package/render/carve_wasm.js +726 -0
- package/render/carve_wasm_bg.wasm +0 -0
- package/render/carve_wasm_bg.wasm.d.ts +21 -0
- package/render/package.json +22 -0
- package/web/LICENSE +21 -0
- package/web/README.md +105 -0
- package/web/carve_wasm.d.ts +840 -0
- package/web/carve_wasm.js +2397 -0
- package/web/carve_wasm_bg.wasm.d.ts +75 -0
- package/web/package.json +22 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
# carve-wasm API and usage reference
|
|
2
|
+
|
|
3
|
+
This page documents the complete browser API. Start with the [package README](../README.md) for installation and a short example.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### HTML migration
|
|
8
|
+
|
|
9
|
+
`htmlToCarve(html, mode)` returns `{ value, report }`, using the same HTML5
|
|
10
|
+
import policy and canonical writer as carve-rs. Modes are `safe` (default),
|
|
11
|
+
`semantic`, and trusted-only `roundtrip`.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { value, report } = htmlToCarve('<p>Hello <strong>world</strong></p>', 'safe')
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`htmlToAst(html, mode)` is the same importer returning the TREE instead of the
|
|
18
|
+
source, so a host that wants a tree does not re-parse what the importer just
|
|
19
|
+
wrote. It returns the same `{ value, report }` shape, with `value` holding AST
|
|
20
|
+
JSON.
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
const { value, report } = htmlToAst('<p>Hello <b>world</b></p>', 'safe')
|
|
24
|
+
astJsonToHtml(value)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
> **The report is not always identical, and the difference is the point.**
|
|
28
|
+
> A loss only a WRITER takes is not reported by `htmlToAst`, because no writer
|
|
29
|
+
> ran (PART 12 §16). A `<figure>` wrapping a table, or a table with an explicit
|
|
30
|
+
> head/body/foot grouping, has no Carve spelling: `htmlToCarve` reports
|
|
31
|
+
> `structure-unspellable` for it and `htmlToAst` does not, because the tree
|
|
32
|
+
> keeps the thing the source would have lost.
|
|
33
|
+
|
|
34
|
+
Portable migration code can use `fromHtml(html, mode)` and
|
|
35
|
+
`fromMarkdown(markdown)`. Both return `{ value, report }`. Version 2 reports use
|
|
36
|
+
the shared fidelity vocabulary. Markdown, Djot, and BBCode conservatively emit
|
|
37
|
+
`fidelity-unverified` as `dropped` / `fallback` until their importers expose
|
|
38
|
+
construct-level outcomes; an empty diagnostic list is therefore never used to
|
|
39
|
+
imply fidelity that was not assessed.
|
|
40
|
+
|
|
41
|
+
### Core renderer
|
|
42
|
+
|
|
43
|
+
Renders Carve markup to HTML with no extensions enabled.
|
|
44
|
+
|
|
45
|
+
The published package is the **bundler** target (webpack, Vite, Rollup, ...):
|
|
46
|
+
the wasm initializes automatically, so the exports are synchronous - no `init()`
|
|
47
|
+
call.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
import { toHtml } from '@markup-carve/carve-wasm'
|
|
51
|
+
|
|
52
|
+
// The other core targets are toMarkdown, toPlainText, toAnsi, and toCarve,
|
|
53
|
+
// each with a *WithOptions form that takes the options object below.
|
|
54
|
+
|
|
55
|
+
const html = toHtml('# Hello, Carve!')
|
|
56
|
+
document.body.innerHTML = html
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Source-preserving patches
|
|
60
|
+
|
|
61
|
+
Tools can prepare canonical formatting as stale-safe UTF-8 byte edits:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const patch = toCarvePatch(source)
|
|
65
|
+
const formatted = applySourcePatch(source, patch)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`createSourcePatch(source, replacement, kind, code)` prepares the same wire
|
|
69
|
+
shape for another complete replacement. Its ranges are UTF-8 byte offsets. The
|
|
70
|
+
fingerprint and byte length catch accidental staleness; they are not a
|
|
71
|
+
cryptographic signature. Treat a patch as trusted edit instructions.
|
|
72
|
+
|
|
73
|
+
### Extensions
|
|
74
|
+
|
|
75
|
+
`extensions()` reports every extension this build accepts. The list comes from
|
|
76
|
+
the engine, so it cannot fall behind what the engine has:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { extensions, toHtmlWithOptions } from '@markup-carve/carve-wasm'
|
|
80
|
+
|
|
81
|
+
extensions()
|
|
82
|
+
// ['autolink', 'citations', 'code-callouts', 'code-group', ...]
|
|
83
|
+
|
|
84
|
+
toHtmlWithOptions(src, { extensions: ['glossary', 'table-of-contents'] })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Names are kebab-case; snake_case (`math_block`) is accepted too. An unknown name
|
|
88
|
+
throws, because an ignored extension renders as missing behavior that looks
|
|
89
|
+
like a Carve bug.
|
|
90
|
+
|
|
91
|
+
### Full renderer (preview set)
|
|
92
|
+
|
|
93
|
+
`toHtmlFull` enables the preview set the playground uses: tab normalisation,
|
|
94
|
+
`<details>` fences, Mermaid diagrams, wikilinks, autolink, list-table, math
|
|
95
|
+
blocks, heading permalinks, citations, code callouts, external-link decoration,
|
|
96
|
+
code groups, and tabs.
|
|
97
|
+
|
|
98
|
+
It is a curated subset rather than everything registered. Extensions that
|
|
99
|
+
rewrite a document that never asked - `heading-numbers` numbers every heading,
|
|
100
|
+
`table-of-contents` injects a TOC - are wrong for a preview. Name them
|
|
101
|
+
explicitly through `toHtmlWithOptions` when you want them.
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { toHtmlFull } from '@markup-carve/carve-wasm'
|
|
105
|
+
|
|
106
|
+
const html = toHtmlFull('# Hello\n\n``` mermaid\ngraph TD; A-->B\n```\n')
|
|
107
|
+
document.body.innerHTML = html
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Symbols
|
|
111
|
+
|
|
112
|
+
A `:name:` symbol renders its literal `:name:` source unless the name is in the
|
|
113
|
+
**symbols map**. Pass one as a plain object (or a `Map`) to `toHtmlWithSymbols`,
|
|
114
|
+
or as the optional second argument of `toHtmlFull`:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import { toHtmlWithSymbols } from '@markup-carve/carve-wasm'
|
|
118
|
+
|
|
119
|
+
toHtmlWithSymbols('Ship it :rocket:', { rocket: '🚀' })
|
|
120
|
+
// => '<p>Ship it 🚀</p>'
|
|
121
|
+
|
|
122
|
+
toHtmlWithSymbols('Ship it :rocket: :shrug:', { rocket: '🚀' })
|
|
123
|
+
// => '<p>Ship it 🚀 :shrug:</p>' (an unmapped name stays literal)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The word-boundary guard is unaffected by an active map: `a:b:c`, `10:30:` and
|
|
127
|
+
`me@example.com` never become symbols. Names and values must both be strings; a
|
|
128
|
+
non-string value throws a `TypeError`.
|
|
129
|
+
|
|
130
|
+
> **Security: symbol values are TRUSTED RAW output.**
|
|
131
|
+
> A mapped value is inserted into the output **unescaped** - the same trust
|
|
132
|
+
> class as a static `renderers` callback. `{ b: '<b>x</b>' }` emits a real
|
|
133
|
+
> `<b>` element, not escaped text. This is deliberate (processor configuration
|
|
134
|
+
> is trusted). **Never build a symbols map out of untrusted / user-supplied
|
|
135
|
+
> input.**
|
|
136
|
+
|
|
137
|
+
### Section wrappers
|
|
138
|
+
|
|
139
|
+
A top-level heading is wrapped, along with the content following it up to the
|
|
140
|
+
next same-or-shallower heading, in a `<section>` carrying the heading's id (spec
|
|
141
|
+
PART 9 §13). Only the id moves - `{#install .featured}` gives
|
|
142
|
+
`<section id="install"><h2 class="featured">` - and a heading inside a
|
|
143
|
+
blockquote, div or list item is not wrapped at all.
|
|
144
|
+
|
|
145
|
+
`toHtmlWithOptions` is the general entry point, and `sections: false` renders
|
|
146
|
+
headings flat with the id back on the `<h*>`:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { toHtmlWithOptions } from '@markup-carve/carve-wasm'
|
|
150
|
+
|
|
151
|
+
toHtmlWithOptions('# A\n\np\n', { sections: false })
|
|
152
|
+
// '<h1 id="A">A</h1>\n<p>p</p>'
|
|
153
|
+
|
|
154
|
+
toHtmlWithOptions(src, { sections: false, symbols: { rocket: '🚀' }, full: true })
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Every field is optional. Omitting the object, or passing `null`, renders with
|
|
158
|
+
defaults, so the three shorthands above use those defaults.
|
|
159
|
+
|
|
160
|
+
| Field | Default | What it does |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `sections` | `true` | Wrap each top-level heading and its content in a `<section>` |
|
|
163
|
+
| `symbols` | none | `:name:` to value, TRUSTED-RAW (see above) |
|
|
164
|
+
| `extensions` | none | Array of registry names; takes precedence over `full` |
|
|
165
|
+
| `full` | `false` | Enable the preview extension set |
|
|
166
|
+
| `rawHtml` | `true` | Emit an explicit passthrough as markup; `false` escapes it |
|
|
167
|
+
| `profile` | none | `full` / `article` / `comment` / `minimal`; rejection throws |
|
|
168
|
+
| `profileBaseHost` | none | The host the profile's link policy counts as internal |
|
|
169
|
+
| `mode` | `interactive` | `static` renders the self-contained form: no client scripts |
|
|
170
|
+
| `sourceLine` | `false` | Stamp top-level blocks with `data-source-line` |
|
|
171
|
+
| `positions` | `false` | Keep source offsets on the nodes |
|
|
172
|
+
| `labels` | none | Override engine-written strings, for a page not in English |
|
|
173
|
+
| `smartTypography` | `glyph` | `source` keeps the author's run instead of the glyph |
|
|
174
|
+
| `lowercaseHeadingIds` | `false` | Lowercase the generated heading ids |
|
|
175
|
+
| `asciiHeadingIds` | `off` | `fold` transliterates, `strict` guarantees ASCII |
|
|
176
|
+
| `mentionUrl` | none | URL template for `@mention`; `{name}` / `{user}` take the encoded name |
|
|
177
|
+
| `tagUrl` | none | URL template for `#tag`; `{name}` takes the encoded name |
|
|
178
|
+
|
|
179
|
+
An unrecognized key is ignored, because the object is configuration and a typo
|
|
180
|
+
should not break a render. A recognized key with the wrong type throws a
|
|
181
|
+
`TypeError` instead of being coerced: JS truthiness would read
|
|
182
|
+
`{ sections: 'false' }` as `true`, the opposite of what was written.
|
|
183
|
+
|
|
184
|
+
`renderers` is the one recognized key `toHtmlWithOptions` refuses; it belongs to
|
|
185
|
+
[`toHtmlWithRenderers`](#static-renderers).
|
|
186
|
+
|
|
187
|
+
This exists for a host whose CSS or JS assumes rendered blocks are direct
|
|
188
|
+
children of the content container - the `.stack > * + *` spacing idiom,
|
|
189
|
+
`:first-child`, `nth-child()` counting, `element.children` walks - all of which
|
|
190
|
+
stop matching once a wrapper sits in between. It is the one output change that
|
|
191
|
+
breaks a document whose *source* migrated cleanly.
|
|
192
|
+
|
|
193
|
+
Nothing else changes: ids, collision dedup, `</#id>` cross-references, implicit
|
|
194
|
+
`[Heading][]` references and heading numbering all resolve against the slug
|
|
195
|
+
rather than the element carrying it. The endnotes
|
|
196
|
+
`<section role="doc-endnotes">` is a separate construct and is still emitted.
|
|
197
|
+
|
|
198
|
+
### Rendering a document you did not write
|
|
199
|
+
|
|
200
|
+
`rawHtml: false` renders an explicit passthrough - the `=html` raw block and the
|
|
201
|
+
`` `…`{=html} `` inline raw span - as escaped text instead of markup. It is the
|
|
202
|
+
switch carve-js spells `allowRawHtml`.
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
toHtmlWithOptions(fromTheReader, { rawHtml: false })
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Reach for it whenever the document comes from somewhere other than the person
|
|
209
|
+
running the page: a shared link, a comment field, a pasted file. A passthrough is
|
|
210
|
+
the one construct that puts author-controlled markup on your origin, so leaving
|
|
211
|
+
it on for a document a reader supplied is a way to run their script.
|
|
212
|
+
|
|
213
|
+
The symbols map is unaffected and stays TRUSTED-RAW either way - it is
|
|
214
|
+
configuration the host wrote, not content the document carries.
|
|
215
|
+
|
|
216
|
+
### Rendering with a profile
|
|
217
|
+
|
|
218
|
+
`rawHtml: false` closes the passthrough vector. It does not cap input length,
|
|
219
|
+
deny a construct, or constrain link schemes - that is what a profile is for, and
|
|
220
|
+
the four presets match the ones the spec and the sibling bindings describe.
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
toHtmlWithOptions(fromTheReader, { profile: 'comment', rawHtml: false })
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**A rejected document throws.** The engine's infallible entry point turns a
|
|
227
|
+
profile rejection - input past `max_length`, or a denied construct when the
|
|
228
|
+
action is Error - into an EMPTY STRING, and a caller cannot tell that from a
|
|
229
|
+
document that legitimately rendered to nothing. This binding renders through the
|
|
230
|
+
fallible one, so a rejection arrives as an `Error` whose `name` is
|
|
231
|
+
`ProfileViolationError` and whose `violations` array carries one message per
|
|
232
|
+
refused construct.
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
try {
|
|
236
|
+
html = toHtmlWithOptions(fromTheReader, { profile: 'comment' })
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error.name === 'ProfileViolationError') report(error.violations)
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Every target takes the profile, not only HTML.** `toMarkdownWithOptions`,
|
|
243
|
+
`toPlainTextWithOptions`, `toAnsiWithOptions`, `toCarveWithOptions` and
|
|
244
|
+
`parseJsonWithOptions` read the same options object, so a document held to a
|
|
245
|
+
profile on its way to HTML is held to it on its way to Markdown or into a stored
|
|
246
|
+
tree. Without them a host could export the same untrusted document unfiltered.
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
toMarkdownWithOptions(fromTheReader, { profile: 'comment' })
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
What those targets actually read is narrower than HTML's list: `profile` and
|
|
253
|
+
`smartTypography` change their output, extensions run, and `symbols`, `labels`,
|
|
254
|
+
`sections`, `sourceLine`, `mode` and the heading-id switches are HTML-side
|
|
255
|
+
concerns the engine's other renderers do not consult. `renderers` is refused
|
|
256
|
+
there as it is on `toHtmlWithOptions`.
|
|
257
|
+
|
|
258
|
+
`toCarveWithOptions` is narrower again and reads `profile` alone. The canonical
|
|
259
|
+
writer is parse-only by contract, so extensions and `smartTypography` are inert
|
|
260
|
+
there; they are accepted rather than refused so that one options object can be
|
|
261
|
+
handed to every target.
|
|
262
|
+
|
|
263
|
+
It has a second exception the other targets do not. The canonical writer can
|
|
264
|
+
refuse a tree on its own account, apart from any profile, and that arrives as an
|
|
265
|
+
error named `CarveWriteError` carrying `reason` (`depth-exceeded` or
|
|
266
|
+
`source-unspellable`) and `nodeType`. A profile rejection still arrives as
|
|
267
|
+
`ProfileViolationError`, so a caller that only handles the one name will miss
|
|
268
|
+
the other:
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
try {
|
|
272
|
+
return toCarveWithOptions(source, { profile: 'comment' })
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error.name === 'ProfileViolationError') return report(error.violations)
|
|
275
|
+
if (error.name === 'CarveWriteError') return report([error.reason, error.nodeType])
|
|
276
|
+
throw error
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`applyProfile` runs the same filter over an AST-JSON document and hands back the
|
|
281
|
+
filtered tree instead of HTML, for a host that wants to store, diff or re-render
|
|
282
|
+
what the filter left. `violations` reports what it degraded or stripped, which
|
|
283
|
+
the HTML path discards.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
const { json, violations } = applyProfile(parseJson(fromTheReader), 'comment')
|
|
287
|
+
if (violations.length > 0) tell(violations.map((v) => v.message))
|
|
288
|
+
const html = astJsonToHtml(json)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
It reads `profileBaseHost` and `smartTypography` from its options object and
|
|
292
|
+
nothing else. It does **not** enforce the profile's `max_length`, which the
|
|
293
|
+
engine applies to the SOURCE bytes before a parse - a host filtering untrusted
|
|
294
|
+
input still needs that bound on the way in.
|
|
295
|
+
|
|
296
|
+
### Static renderers
|
|
297
|
+
|
|
298
|
+
`mode: 'static'` renders the self-contained form, which carries no client
|
|
299
|
+
scripts - so a formula in the document has to be typeset while the HTML is being
|
|
300
|
+
written, by the host. `toHtmlWithRenderers(source, options)` takes the same
|
|
301
|
+
options object plus a `renderers.math` callback and returns
|
|
302
|
+
`{ html, rendererErrors }`.
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
import katex from 'katex'
|
|
306
|
+
import { toHtmlWithRenderers } from '@markup-carve/carve-wasm'
|
|
307
|
+
|
|
308
|
+
const { html, rendererErrors } = toHtmlWithRenderers(source, {
|
|
309
|
+
mode: 'static',
|
|
310
|
+
extensions: ['math-block'],
|
|
311
|
+
renderers: {
|
|
312
|
+
math: (tex, display) => katex.renderToString(tex, { displayMode: display }),
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The callback is called once per ` ```math ` fence, with the TeX source and a
|
|
318
|
+
display flag. Without it, static output keeps the `\[…\]` source for a client to
|
|
319
|
+
typeset later - never blank.
|
|
320
|
+
|
|
321
|
+
> **Security: what a renderer returns is TRUSTED RAW HTML.**
|
|
322
|
+
> The string is inserted **unescaped**, the same trust class as a `symbols`
|
|
323
|
+
> value. There is one difference worth stating: a symbol value is host
|
|
324
|
+
> configuration keyed by a **name**, while a renderer is host configuration that
|
|
325
|
+
> is **handed document content** and typically echoes some of it back. A host
|
|
326
|
+
> rendering documents it did not author is accepting whatever its renderer makes
|
|
327
|
+
> of that input, so the escaping is the renderer's job.
|
|
328
|
+
|
|
329
|
+
**The callback must be synchronous.** wasm-bindgen cannot await across it, so an
|
|
330
|
+
`async` renderer returns a Promise the engine has no way to resolve. That is
|
|
331
|
+
reported as a failure rather than stringified into the document.
|
|
332
|
+
|
|
333
|
+
`renderers.diagrams` is the same callback one level down, keyed by the fence's
|
|
334
|
+
css class:
|
|
335
|
+
|
|
336
|
+
```js
|
|
337
|
+
const { html, rendererErrors } = toHtmlWithRenderers(source, {
|
|
338
|
+
mode: 'static',
|
|
339
|
+
extensions: ['fenced-render', 'fenced-render-graphviz'],
|
|
340
|
+
renderers: {
|
|
341
|
+
diagrams: {
|
|
342
|
+
mermaid: (source) => prerendered.get(source),
|
|
343
|
+
graphviz: (source) => dotToSvg(source),
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
})
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Mermaid itself cannot be passed here: its `render` returns a Promise from v10
|
|
350
|
+
on. What a browser host can do is render its diagrams beforehand, by whatever
|
|
351
|
+
async means it likes, and pass the lookup as the one-line callback above.
|
|
352
|
+
|
|
353
|
+
A fence whose class nobody configured degrades to an escaped source block, and
|
|
354
|
+
that is reported by nothing - the callback is never called, so the binding
|
|
355
|
+
cannot see the miss. A configured key the document never uses is silent for the
|
|
356
|
+
same reason. Keys are checked for shape only: an unknown one is accepted,
|
|
357
|
+
because validating it would mean keeping the list of fence classes by hand here
|
|
358
|
+
until [markup-carve/carve-rs#1670](https://github.com/markup-carve/carve-rs/issues/1670)
|
|
359
|
+
puts the class on the registry entry.
|
|
360
|
+
|
|
361
|
+
A callback that throws, or returns anything but a string, does not abort the
|
|
362
|
+
render. The node it was called for emits nothing and the failure is reported:
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
const { html, rendererErrors } = toHtmlWithRenderers(source, {
|
|
366
|
+
mode: 'static',
|
|
367
|
+
extensions: ['math-block'],
|
|
368
|
+
renderers: { math: () => { throw new Error('bad TeX') } },
|
|
369
|
+
})
|
|
370
|
+
// rendererErrors: [{ renderer: 'math', display: true, source: 'E = mc^2', message: '…bad TeX' }]
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
A diagram failure is an entry in the same array, with `renderer` naming the css
|
|
374
|
+
class and no `display` - that flag is the math callback's second argument.
|
|
375
|
+
|
|
376
|
+
That is why this is a separate entry point: `toHtmlWithOptions` returns a bare
|
|
377
|
+
string with nowhere to report a failing callback, and a silently empty figure is
|
|
378
|
+
the degradation `lintCarve` exists to warn about. Every other check is at READ
|
|
379
|
+
time, matching the rest of the options object - a non-callable `math`, a
|
|
380
|
+
non-object `renderers` and a non-callable `diagrams` value all throw a
|
|
381
|
+
`TypeError` before the render starts, so a misconfigured host finds out without
|
|
382
|
+
needing a document that happens to contain a formula.
|
|
383
|
+
|
|
384
|
+
### Editing a tree, and reading one back
|
|
385
|
+
|
|
386
|
+
`parseJson` serializes a document out. `astJsonToHtml` renders one back, and
|
|
387
|
+
`astJsonToCarve` writes one back as source, so a host that reads and changes the tree can display and save the result without a server.
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
const tree = JSON.parse(parseJson(source))
|
|
391
|
+
tree.children.unshift({ type: 'heading', level: 1, children: [{ type: 'text', value: 'Added' }] })
|
|
392
|
+
const html = astJsonToHtml(JSON.stringify(tree), { full: true })
|
|
393
|
+
const carve = astJsonToCarve(JSON.stringify(tree))
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`astJsonToMarkdown`, `astJsonToPlainText` and `astJsonToAnsi` are the same seam
|
|
397
|
+
for the other targets. Each takes the options object, and the profile filter and
|
|
398
|
+
the `before_render` hooks run as they do on the HTML path. Without them a host
|
|
399
|
+
holding a tree reached Markdown through `astJsonToCarve` and `toMarkdown`: a
|
|
400
|
+
canonical write, a re-parse and a second render for one answer.
|
|
401
|
+
|
|
402
|
+
A tree carrying something no Carve source can spell is refused by
|
|
403
|
+
`astJsonToCarve` rather than written approximately, and an invalid tree throws
|
|
404
|
+
from either.
|
|
405
|
+
|
|
406
|
+
Ordering follows the tree. Section 7 orders collected definitions by source
|
|
407
|
+
position, so a tree carrying spans prints its footnote and link definitions in
|
|
408
|
+
source order and a tree carrying none prints them in label order.
|
|
409
|
+
`toMarkdown` parses with positions on and always gets the first; these entry
|
|
410
|
+
points get whatever their producer built.
|
|
411
|
+
|
|
412
|
+
`lintCarve` returns the degradation diagnostics as
|
|
413
|
+
`{ line, column, rule, message, start, end }`, with the rule ids carve-js and
|
|
414
|
+
carve-php use for the same triggers.
|
|
415
|
+
|
|
416
|
+
#### Which unit each offset counts in
|
|
417
|
+
|
|
418
|
+
Three APIs here carry source offsets and they do not agree, because each follows
|
|
419
|
+
its own contract. Pick by what you are going to do with the number, not by
|
|
420
|
+
which one is shortest:
|
|
421
|
+
|
|
422
|
+
| API | `start` / `startOffset` | `startUtf16` |
|
|
423
|
+
|---|---|---|
|
|
424
|
+
| `lintCarve`, `lintCarveWithOptions` | **UTF-16 code units** | not emitted |
|
|
425
|
+
| `lintAccessibility` | Unicode **codepoints** | UTF-16 code units |
|
|
426
|
+
| `parseJson` positions | Unicode **codepoints** (PART 12 §4) | not emitted |
|
|
427
|
+
| `reparse`, `ParserSession.edit`, source patches | UTF-8 **bytes** | not emitted |
|
|
428
|
+
|
|
429
|
+
**To index a JavaScript string, use UTF-16.** `String.prototype.slice` counts
|
|
430
|
+
UTF-16 code units, so neither a byte nor a codepoint offset selects the right
|
|
431
|
+
span once the document leaves ASCII - and inside ASCII all three numbers are
|
|
432
|
+
equal, so a test on English prose will not show the difference. The lint family
|
|
433
|
+
answers in that unit directly; `lintAccessibility` keeps codepoints and carries
|
|
434
|
+
the `Utf16` pair beside them:
|
|
435
|
+
|
|
436
|
+
```js
|
|
437
|
+
const source = '\u{1F680}\n\n[t][missing]\n'
|
|
438
|
+
const [warning] = lintCarve(source)
|
|
439
|
+
source.slice(warning.start, warning.end) // "[t][missing]"
|
|
440
|
+
|
|
441
|
+
const [diagnostic] = lintAccessibility('\u{1F680}\n\n\n')
|
|
442
|
+
source.slice(diagnostic.startOffset, diagnostic.endOffset) // wrong unit
|
|
443
|
+
source.slice(diagnostic.startUtf16, diagnostic.endUtf16) // ""
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
`lintCarve` and `lintCarveWithOptions` answered in UTF-8 bytes before 0.1.6.
|
|
447
|
+
The change is silent on ASCII, so a caller feeding these offsets to a byte-range
|
|
448
|
+
API finds out from a document with an accented character rather than from an
|
|
449
|
+
error.
|
|
450
|
+
|
|
451
|
+
`parseJson` positions stay in codepoints deliberately: that is the cross-engine
|
|
452
|
+
wire format, not a local convenience, and a consumer comparing trees across
|
|
453
|
+
engines depends on it. The byte offsets are equally deliberate where they
|
|
454
|
+
appear: `reparse` and the source-patch family replace byte ranges, so a
|
|
455
|
+
codepoint there would be a different edit.
|
|
456
|
+
|
|
457
|
+
### Include expansion
|
|
458
|
+
|
|
459
|
+
`expandIncludes(source, options)` runs the `{{ path }}` pass (spec PART 9 §19)
|
|
460
|
+
over a document and hands back the expanded tree as AST JSON, alongside the
|
|
461
|
+
warnings and the dependency list a host needs for reporting and file watching.
|
|
462
|
+
|
|
463
|
+
```js
|
|
464
|
+
import { expandIncludes, astJsonToHtml } from '@markup-carve/carve-wasm'
|
|
465
|
+
|
|
466
|
+
const files = new Map([['child.crv', 'Included body.\n']])
|
|
467
|
+
|
|
468
|
+
const { json, warnings, dependencies } = expandIncludes(source, {
|
|
469
|
+
resolve: (path, ctx) => files.get(path) ?? null,
|
|
470
|
+
})
|
|
471
|
+
document.body.innerHTML = astJsonToHtml(json)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**The resolver has to be synchronous, and that decides who can use this.** A
|
|
475
|
+
browser host that would resolve a path over the network cannot: `fetch` returns
|
|
476
|
+
a Promise and wasm-bindgen cannot await across the call. A host whose files are
|
|
477
|
+
already in memory can - an editor with its open buffers, a bundler, a VFS, a
|
|
478
|
+
test harness. An `async` resolver is not silently ignored: the Promise is
|
|
479
|
+
reported in `resolverErrors` and the directive stays literal, the same way an
|
|
480
|
+
`async` `renderers.math` is handled.
|
|
481
|
+
|
|
482
|
+
`resolve` returns the child's source as a string, or `{ source, id }` when it
|
|
483
|
+
can name the file canonically. **Supply the id where you have one** - it is what
|
|
484
|
+
the cycle guard compares, so two spellings of one file (`b.crv`, `./b.crv`)
|
|
485
|
+
defeat it without one and only the depth limit stops the recursion. `null`
|
|
486
|
+
refuses the directive as `not-found`, and `{ denial }` refuses it in one of the
|
|
487
|
+
classes §19 names: `outside-root`, `not-found`, `no-root`, `include-denied`,
|
|
488
|
+
`include-unresolved`. The class reaches the caller on the dependency.
|
|
489
|
+
|
|
490
|
+
`ctx` carries `{ sourcePath, stack, depth }`. `stack` is the include chain, root
|
|
491
|
+
first, and its last entry is the file containing the directive - which is what
|
|
492
|
+
relative resolution keys off.
|
|
493
|
+
|
|
494
|
+
| Field | Default | What it does |
|
|
495
|
+
|---|---|---|
|
|
496
|
+
| `resolve` | required | `(path, ctx) => source \| { source, id } \| { denial } \| null` |
|
|
497
|
+
| `sourcePath` | none | Identity of the root document, for cycle detection and warning attribution |
|
|
498
|
+
| `extensions` | none | Registry names to parse each CHILD with; pass the set the parent was parsed with |
|
|
499
|
+
| `maxDepth` | `16` | Transitive include depth |
|
|
500
|
+
| `maxBytes` | `max(1 MB, 8 x source bytes)` | Expanded-source byte budget |
|
|
501
|
+
| `maxResolverCalls` | `1000` | Resolver calls for one expansion |
|
|
502
|
+
| `maxWarnings` | `100` | Warnings retained; one per distinct rule always survives |
|
|
503
|
+
|
|
504
|
+
Each budget must be a finite integer in `0..4294967295`; fractions, strings,
|
|
505
|
+
negative values and nonfinite numbers throw before the resolver runs.
|
|
506
|
+
|
|
507
|
+
The budgets are part of the contract rather than a detail. They bound what a
|
|
508
|
+
document of directives can make a host do, and a host serving documents it did
|
|
509
|
+
not write is the one that should lower them.
|
|
510
|
+
|
|
511
|
+
The result is `{ json, warnings, suppressedWarnings, dependencies,
|
|
512
|
+
chargedBytes, resolverErrors }`. `dependencies` lists every target touched,
|
|
513
|
+
including the ones that did not resolve: a host watching only the files it read
|
|
514
|
+
would never learn that a missing target now exists, so the preview would stay
|
|
515
|
+
stale at the moment the author fixes it. `chargedBytes` is what the budget was
|
|
516
|
+
charged, which is otherwise unobservable from outside.
|
|
517
|
+
|
|
518
|
+
The tree carries no positions. Expansion merges nodes from several files, and a
|
|
519
|
+
span on a node that came from a child would point into a source the caller
|
|
520
|
+
never passed.
|
|
521
|
+
### Re-parsing while someone types
|
|
522
|
+
|
|
523
|
+
`parseSnapshot(source)` parses a document and keeps what a later `reparse` needs.
|
|
524
|
+
`reparse(source, changes)` applies a batch of edits and parses the result. Both
|
|
525
|
+
return one JSON string.
|
|
526
|
+
|
|
527
|
+
```js
|
|
528
|
+
import { parseSnapshot, reparse } from '@markup-carve/carve-wasm'
|
|
529
|
+
|
|
530
|
+
const first = JSON.parse(parseSnapshot('# One\n'))
|
|
531
|
+
const next = JSON.parse(reparse(first.source, JSON.stringify([
|
|
532
|
+
{ range: [2, 5], replacement: 'Two' },
|
|
533
|
+
])))
|
|
534
|
+
next.source // '# Two\n'
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Each result is `{ source, document, sourceLayout, changedSource,
|
|
538
|
+
reusedPreviousTree, parsedSourceBytes }`. `document` is the PART 12 tree,
|
|
539
|
+
`sourceLayout` is what `parseSourceLayoutJson` produces, and `changedSource`
|
|
540
|
+
contains UTF-8 byte ranges. `parsedSourceBytes` measures the source the engine
|
|
541
|
+
parsed, excluding reused paragraphs.
|
|
542
|
+
|
|
543
|
+
`reparse` constructs the previous snapshot on every call. Use `ParserSession`
|
|
544
|
+
when an editor needs to retain it between changes:
|
|
545
|
+
|
|
546
|
+
```js
|
|
547
|
+
import { ParserSession } from '@markup-carve/carve-wasm'
|
|
548
|
+
|
|
549
|
+
const session = new ParserSession('First paragraph.\n\nSecond paragraph.\n')
|
|
550
|
+
try {
|
|
551
|
+
const next = JSON.parse(session.edit(JSON.stringify([
|
|
552
|
+
{ range: [0, 5], replacement: 'Other' },
|
|
553
|
+
])))
|
|
554
|
+
console.log(next.reusedPreviousTree, next.parsedSourceBytes)
|
|
555
|
+
} finally {
|
|
556
|
+
session.free()
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
`session.snapshot()` returns the current snapshot as JSON. A rejected edit
|
|
561
|
+
leaves the session unchanged. An empty edit list returns the retained snapshot
|
|
562
|
+
with no parsed bytes. Plain-paragraph edits can reuse the previous tree;
|
|
563
|
+
syntax-bearing documents fall back to a full parse. Read `reusedPreviousTree`
|
|
564
|
+
and `parsedSourceBytes` to determine which path ran. Snapshot serialization
|
|
565
|
+
still visits the whole tree.
|
|
566
|
+
|
|
567
|
+
Edit ranges and source patches use UTF-8 bytes. AST positions from `parseJson`
|
|
568
|
+
use Unicode codepoints; browser `selectionStart` uses UTF-16 code units. Import
|
|
569
|
+
`utf16ToUtf8`, `codepointToUtf8` or `utf8ToUtf16` from the package's `/offsets`
|
|
570
|
+
entry to convert. The helpers reject a boundary inside a surrogate pair or
|
|
571
|
+
UTF-8 sequence. Tests cover emoji, combining marks, CRLF and a leading BOM.
|
|
572
|
+
|
|
573
|
+
Malformed changes throw `TypeError`: overlapping ranges, an end past the source,
|
|
574
|
+
a range splitting a codepoint, or an offset outside the integer range
|
|
575
|
+
`0..4294967295`. Offsets are checked before conversion to WASM's 32-bit size.
|
|
576
|
+
|
|
577
|
+
### Tree patches
|
|
578
|
+
|
|
579
|
+
`createAstPatch(before, after)` is the difference between two PART 12 trees,
|
|
580
|
+
and `applyAstPatch(ast, patch)` replays it. Trees and patches both cross as
|
|
581
|
+
JSON strings, the same way `parseJson` and `astJsonToHtml` hand a tree across.
|
|
582
|
+
|
|
583
|
+
```js
|
|
584
|
+
import { parseJson, createAstPatch, applyAstPatch } from '@markup-carve/carve-wasm'
|
|
585
|
+
|
|
586
|
+
const patch = createAstPatch(parseJson('# One\n'), parseJson('# Two\n'))
|
|
587
|
+
applyAstPatch(parseJson('# One\n'), patch) // the '# Two' tree, as JSON
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
The operations are the position-independent `{ op, path, value }` wire shape,
|
|
591
|
+
so a patch stays meaningful against a tree that moved underneath it. That is
|
|
592
|
+
the opposite trade from `createSourcePatch`, which pins byte ranges and
|
|
593
|
+
fingerprints them against staleness.
|
|
594
|
+
|
|
595
|
+
The engine's `ast_patch_to_json` and `ast_patch_from_json` have no binding of
|
|
596
|
+
their own. With the patch crossing as JSON they are exactly this pair's
|
|
597
|
+
encoding, and calling them would convert JSON a host already holds.
|
|
598
|
+
|
|
599
|
+
`createReversibleAstPatch(before, after)` returns
|
|
600
|
+
`{ forward, inverse, beforeFingerprint, afterFingerprint }` as one JSON string,
|
|
601
|
+
and `applyReversibleAstPatch(ast, patch, inverse)` replays either direction.
|
|
602
|
+
|
|
603
|
+
```js
|
|
604
|
+
const step = createReversibleAstPatch(before, after)
|
|
605
|
+
const redone = applyReversibleAstPatch(before, step)
|
|
606
|
+
const undone = applyReversibleAstPatch(after, step, true)
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**The undo stack is yours.** Every entry point here is a pure function that
|
|
610
|
+
owns nothing, so there is no history kept in this package to undo against, and
|
|
611
|
+
a browser host already has somewhere to keep one. What the reversible pair adds
|
|
612
|
+
over two `createAstPatch` calls is the precondition: applying a step to a tree
|
|
613
|
+
whose fingerprint is not the one it was made against throws, so an undo cannot
|
|
614
|
+
land on a document that has moved on.
|
|
615
|
+
|
|
616
|
+
### Three-way merge
|
|
617
|
+
|
|
618
|
+
`mergeAst(base, ours, theirs, options)` merges two edits of one document. Two
|
|
619
|
+
people editing one document is a browser problem, so the audience is here.
|
|
620
|
+
|
|
621
|
+
```js
|
|
622
|
+
import { parseJson, mergeAst } from '@markup-carve/carve-wasm'
|
|
623
|
+
|
|
624
|
+
const { ok, ast, conflicts } = JSON.parse(mergeAst(base, ours, theirs))
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**A conflict is a value, not a throw.** It is the result you asked for: `ok` is
|
|
628
|
+
false, `ast` is null, and `conflicts` says where and why. A throw is for a
|
|
629
|
+
contract you broke, and an unparseable tree still gets one, naming which of the
|
|
630
|
+
three it was.
|
|
631
|
+
|
|
632
|
+
Each conflict is `{ path, reason, base, ours, theirs }`, with `reason` one of
|
|
633
|
+
`both-changed`, `delete-edit` or `concurrent-sequence-edit`. That shape and
|
|
634
|
+
those names are carve-js's, so a host merging with either engine reads one
|
|
635
|
+
contract. The Rust engine carries no `deleted` flags, so carve-js's optional
|
|
636
|
+
`deleted` field is absent here rather than guessed at.
|
|
637
|
+
|
|
638
|
+
`options.resolve` answers conflicts while the merge runs:
|
|
639
|
+
|
|
640
|
+
```js
|
|
641
|
+
const merged = JSON.parse(mergeAst(base, ours, theirs, {
|
|
642
|
+
resolve: (conflict) => (conflict.path.startsWith('/children/0') ? 'ours' : null),
|
|
643
|
+
}))
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
It returns `'base'`, `'ours'`, `'theirs'`, `{ value }` to replace the field
|
|
647
|
+
outright, or `null` to leave that conflict standing.
|
|
648
|
+
|
|
649
|
+
> **The resolver must be synchronous.**
|
|
650
|
+
> A resolver that asks a server, or asks the user, returns a Promise the merge
|
|
651
|
+
> cannot await. That is reported in `resolverErrors` with the conflict's path,
|
|
652
|
+
> and the conflict is left unresolved - never swallowed, and never stringified
|
|
653
|
+
> into the tree. Returning `null` is the supported way to decline; a Promise is
|
|
654
|
+
> a mistake.
|
|
655
|
+
|
|
656
|
+
### ProseMirror
|
|
657
|
+
|
|
658
|
+
`toProseMirror(source)` converts Carve to the ProseMirror document shape, and
|
|
659
|
+
`fromProseMirror(doc)` writes one back as canonical Carve source. ProseMirror
|
|
660
|
+
runs in a browser and nowhere else, so this binding is the whole audience for
|
|
661
|
+
the engine's bridge - without it a host wiring a Carve editor either round-trips
|
|
662
|
+
to a server or reimplements the node mapping in JS, where it drifts from the
|
|
663
|
+
engine's.
|
|
664
|
+
|
|
665
|
+
```js
|
|
666
|
+
const { json, dropped, degraded } = toProseMirror(source)
|
|
667
|
+
editor.commands.setContent(JSON.parse(json))
|
|
668
|
+
const saved = fromProseMirror(JSON.stringify(editor.getJSON()))
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
`json` is a JSON string, the same choice `parseJson` makes. The two maps are
|
|
672
|
+
`Carve node type -> reason`: `dropped` where the content is gone (an
|
|
673
|
+
abbreviation definition has no editor node), `degraded` where the text survives
|
|
674
|
+
without its node type (a soft break becomes whitespace, smart typography
|
|
675
|
+
resolves to the glyph). When node-level reports are empty, the binding also compares canonical source
|
|
676
|
+
before and after the bridge. A difference is reported under `degraded.document`.
|
|
677
|
+
An empty pair of maps means that this immediate round trip preserves canonical
|
|
678
|
+
source, not that subsequent edits in an editor preserve every Carve construct.
|
|
679
|
+
|
|
680
|
+
A payload the schema map does not describe is refused rather than written
|
|
681
|
+
approximately. Round-tripping normalizes the source the way `toCarve` does, and
|
|
682
|
+
the reported degradations do not come back - `a ... b` returns as `a … b`.
|
|
683
|
+
|
|
684
|
+
The checked renderers accept only boolean `strict` values and integer
|
|
685
|
+
`maximum` values in `0..4294967295`. Omitted or null arguments use the defaults.
|
|
686
|
+
A string such as `"false"` throws rather than enabling strict mode through
|
|
687
|
+
JavaScript coercion.
|
|
688
|
+
|
|
689
|
+
### TypeScript
|
|
690
|
+
|
|
691
|
+
The package ships `.d.ts` declarations. Types are inferred automatically when
|
|
692
|
+
imported from `@markup-carve/carve-wasm`.
|
|
693
|
+
|
|
694
|
+
```ts
|
|
695
|
+
import { toHtml, toHtmlFull, version } from '@markup-carve/carve-wasm'
|
|
696
|
+
|
|
697
|
+
console.log(`carve-wasm v${version()}`)
|
|
698
|
+
const html: string = toHtml('_Hello_')
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
## API
|
|
702
|
+
|
|
703
|
+
| Export | Signature | Description |
|
|
704
|
+
|--------|-----------|-------------|
|
|
705
|
+
| `toHtml` | `(source: string) => string` | Core renderer, no extensions |
|
|
706
|
+
| `toHtmlWithSymbols` | `(source: string, symbols?: object \| null) => string` | Core renderer + a `:name:` -> value symbols map (values are raw, see above) |
|
|
707
|
+
| `toHtmlFull` | `(source: string, symbols?: object \| null) => string` | Core + common extensions (matches playground), optional symbols map |
|
|
708
|
+
| `toHtmlWithOptions` | `(source: string, options?: object \| null) => string` | General form; see the options table above. Throws `ProfileViolationError` when a profile rejects the document |
|
|
709
|
+
| `toHtmlWithRenderers` | `(source: string, options?: object \| null) => StaticRenderResult` | The options object plus `renderers.math` and `renderers.diagrams`, for `mode: 'static'`; returns `{ html, rendererErrors }` |
|
|
710
|
+
| `toMarkdownWithOptions` | `(source: string, options?: object \| null) => string` | Markdown under the same options object. Throws `ProfileViolationError` when a profile rejects the document |
|
|
711
|
+
| `toPlainTextWithOptions` | `(source: string, options?: object \| null) => string` | Plain text under the same options object |
|
|
712
|
+
| `toAnsiWithOptions` | `(source: string, options?: object \| null) => string` | ANSI text under the same options object |
|
|
713
|
+
| `toCarveWithOptions` | `(source: string, options?: object \| null) => string` | Canonical Carve under the same options object; reads `profile` only. Throws `ProfileViolationError` when a profile rejects the document, or `CarveWriteError` when the writer itself refuses the tree |
|
|
714
|
+
| `parseJsonWithOptions` | `(source: string, options?: object \| null) => string` | The AST as JSON under the same options object; positions are always on |
|
|
715
|
+
| `toHtmlWithReport` | `(source: string, strict?: boolean, maximum?: number) => RenderResult` | HTML plus bounded losses: `raw-format-dropped`, and `destination-denied` for each link, autolink or image URL whose scheme is denied and blanked (the HTML keeps the empty attribute). `losses` is capped by `maximum`, so read `totalsByCode` rather than counting the array when `truncated` is true. Strict mode throws `RenderLossError` ("render would lose N nodes"), which carries the same three fields |
|
|
716
|
+
| `toMarkdownWithReport` | `(source: string, strict?: boolean, maximum?: number) => RenderResult` | Checked Markdown render |
|
|
717
|
+
| `toPlainTextWithReport` | `(source: string, strict?: boolean, maximum?: number) => RenderResult` | Checked plain-text render |
|
|
718
|
+
| `toAnsiWithReport` | `(source: string, strict?: boolean, maximum?: number) => RenderResult` | Checked ANSI render |
|
|
719
|
+
| `toCarveWithReport` | `(source: string, strict?: boolean, maximum?: number) => RenderResult` | Checked canonical Carve render (lossless) |
|
|
720
|
+
| `parseJson` | `(source: string) => string` | The parsed AST as JSON (PART 12 exchange shape) |
|
|
721
|
+
| `astJsonToHtml` | `(json: string, options?: object \| null) => string` | Render an AST-JSON document; takes the same options object |
|
|
722
|
+
| `astJsonToCarve` | `(json: string) => string` | Write an AST-JSON document back as canonical Carve source |
|
|
723
|
+
| `astJsonToMarkdown` | `(json: string, options?: object \| null) => string` | Render an AST-JSON document to Markdown; takes the same options object |
|
|
724
|
+
| `astJsonToPlainText` | `(json: string, options?: object \| null) => string` | Render an AST-JSON document to plain text |
|
|
725
|
+
| `astJsonToAnsi` | `(json: string, options?: object \| null) => string` | Render an AST-JSON document to ANSI-styled text |
|
|
726
|
+
| `applyProfile` | `(json: string, profile: string, options?: object \| null) => ProfileFilterResult` | Filter an AST-JSON document through a profile, keeping the tree and what the filter did |
|
|
727
|
+
| `lintCarve` | `(source: string) => LintWarning[]` | Degradation diagnostics, with the rule ids carve-js and carve-php share. `start` / `end` are UTF-16 code units, so they index a JS string |
|
|
728
|
+
| `lintCarveWithOptions` | `(source: string, options?: object \| null) => LintWarning[]` | The same linter for the extension set the host renders with |
|
|
729
|
+
| `lintAccessibility` | `(source: string) => AccessibilityDiagnostic[]` | The second diagnostic family, with its own rule ids and a severity. `startOffset` / `endOffset` are codepoints, not the UTF-16 units `lintCarve` counts; `startUtf16` / `endUtf16` index a JS string |
|
|
730
|
+
| `stampCarve` | `(formatted: string, generatedBy: string, form?: "line" \| "block") => string` | Write the provenance marker `readStamp` reads |
|
|
731
|
+
| `sanitizeSvg` | `(source: string, options?: object \| null) => SanitizeResult` | Sanitize an SVG document; strict unless an option says otherwise |
|
|
732
|
+
| `parseLocator` | `(loc: string) => ParsedLocator` | Parse a citation locator into label, value and suffix |
|
|
733
|
+
| `parseSourceLayoutJson` | `(source: string) => string` | The PART 12 §13 source-layout sidecar |
|
|
734
|
+
| `markdownToAstJson` | `(source: string) => string` | Import Markdown straight to the tree, skipping the Carve-source round trip |
|
|
735
|
+
| `htmlToAst` | `(html: string, mode?: string) => MigrationResult` | Import HTML straight to the tree; `{ value, report }` with the tree in `value` |
|
|
736
|
+
| `expandIncludes` | `(source: string, options: object) => IncludeExpansion` | Expand `{{ path }}` through a SYNCHRONOUS `resolve`; returns the tree plus warnings, dependencies and resolver failures |
|
|
737
|
+
| `parseSnapshot` | `(source: string) => string` | Parse and keep what a `reparse` needs; JSON `{ source, document, sourceLayout, changedSource, reusedPreviousTree, parsedSourceBytes }` |
|
|
738
|
+
| `ParserSession` | `new (source: string)` | Retained parsing state with `snapshot()`, `edit(changes: string)` and `free()` |
|
|
739
|
+
| `reparse` | `(source: string, changes: string) => string` | Apply a JSON array of `{ range: [start, end], replacement }` in UTF-8 BYTE offsets and re-parse |
|
|
740
|
+
| `mergeAst` | `(base: string, ours: string, theirs: string, options?: object) => string` | Three-way merge; JSON `{ ok, ast, conflicts, resolverErrors }`, a conflict being a value |
|
|
741
|
+
| `createAstPatch` | `(before: string, after: string) => string` | The difference between two PART 12 trees, as `{ op, path, value }` patch JSON |
|
|
742
|
+
| `applyAstPatch` | `(ast: string, patch: string) => string` | Replay patch JSON onto a tree |
|
|
743
|
+
| `createReversibleAstPatch` | `(before: string, after: string) => string` | The same difference with its inverse and both fingerprints |
|
|
744
|
+
| `applyReversibleAstPatch` | `(ast: string, patch: string, inverse?: boolean) => string` | Replay a reversible patch either way; a fingerprint mismatch throws |
|
|
745
|
+
| `toProseMirror` | `(source: string) => ProseMirrorResult` | Convert to the ProseMirror document shape, with what the model could not hold |
|
|
746
|
+
| `fromProseMirror` | `(doc: string) => string` | Convert a ProseMirror document back to canonical Carve source |
|
|
747
|
+
| `readStamp` | `(source: string) => { version, generatedBy } \| null` | The document's provenance marker, if it carries one |
|
|
748
|
+
| `needsReview` | `(source: string, currentVersion: string) => boolean` | Whether the stamp predates `currentVersion`; unstamped counts as yes |
|
|
749
|
+
| `fromDjot` | `(source: string) => string` | Convert Djot source to Carve |
|
|
750
|
+
| `migrateDjot` | `(source: string) => { value, report }` | Convert Djot with a v2 fidelity report |
|
|
751
|
+
| `fromBbcode` | `(source: string) => string` | Convert BBCode source to Carve; throws past the engine's size cap |
|
|
752
|
+
| `migrateBbcode` | `(source: string) => { value, report }` | Convert BBCode with a v2 fidelity report; throws past the engine's size cap |
|
|
753
|
+
| `createSourcePatch` | `(source, replacement, kind, code) => SourcePatch` | Build a minimal UTF-8 byte-range patch |
|
|
754
|
+
| `toCarvePatch` | `(source: string) => SourcePatch` | Preview canonical formatting as a patch |
|
|
755
|
+
| `applySourcePatch` | `(source: string, patch: SourcePatch) => string` | Validate and apply trusted patch instructions |
|
|
756
|
+
| `version` | `() => string` | Returns the carve-wasm package version |
|
|
757
|
+
|
|
758
|
+
### The parsed AST
|
|
759
|
+
|
|
760
|
+
`parseJson` returns the document as a JSON string - the [PART 12 exchange
|
|
761
|
+
shape](https://markup-carve.github.io/carve/ast-json), the same tree every Carve
|
|
762
|
+
engine publishes, so a consumer written against one implementation reads
|
|
763
|
+
another's output.
|
|
764
|
+
|
|
765
|
+
```js
|
|
766
|
+
import { parseJson } from '@markup-carve/carve-wasm'
|
|
767
|
+
|
|
768
|
+
const ast = JSON.parse(parseJson('# Title\n\nBody[^a].\n\n[^a]: note\n'))
|
|
769
|
+
ast.children.map((n) => n.type) // ['heading', 'paragraph', 'footnote']
|
|
770
|
+
ast.children[0].pos // { startLine: 1, startColumn: 1, ... }
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
The root carries exactly `type`, `children` and `srcByteLength`; frontmatter and
|
|
774
|
+
footnote definitions are block nodes inside `children`, not root fields. Every
|
|
775
|
+
node except the root carries `pos` when the engine could place it - 1-based
|
|
776
|
+
lines and columns, 0-based offsets, ends exclusive, counted in Unicode
|
|
777
|
+
**codepoints**, not bytes or UTF-16 units. A node the engine could not place,
|
|
778
|
+
such as reassembled table-cell text, carries no `pos` at all rather than an
|
|
779
|
+
invented one.
|
|
780
|
+
|
|
781
|
+
A string rather than a JS object: the caller runs `JSON.parse`, which the
|
|
782
|
+
browser does natively and faster than building the object graph across the wasm
|
|
783
|
+
boundary one property at a time - and it keeps the bytes available for a caller
|
|
784
|
+
that stores or forwards them.
|
|
785
|
+
|