rapier-markdown-kit 1.2.19 → 1.2.21
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 +47 -1
- package/dist/images/playback.mjs +9 -6
- package/dist/kit/markdown-it.mjs +9 -4
- package/dist/kit/parsed.d.mts +78 -0
- package/dist/kit/parsed.mjs +157 -0
- package/dist/kit/render-markdown.mjs +16 -9
- package/dist/kit/render-print.mjs +22 -5
- package/dist/spec/document-settings.mjs +14 -5
- package/dist/spec/md-assets.mjs +2 -1
- package/dist/spec/md-engine.mjs +90 -0
- package/dist/spec/md-marks.mjs +3 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ const edited = replaceLedgerText(base, 'Draft, revised\n', {actor: {kind: 'human
|
|
|
44
44
|
console.log(merge(edited, base).text, authorship(edited).runs);
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
TypeScript declarations ship for `markdown-it`, `will`, `rapier-ledger`, `ledger/carried` and `pdf`.
|
|
47
|
+
TypeScript declarations ship for `markdown-it`, `parsed`, `will`, `rapier-ledger`, `ledger/carried` and `pdf`.
|
|
48
48
|
|
|
49
49
|
```js
|
|
50
50
|
import {parseLayout} from 'rapier-markdown-kit/layout'; // the md-layout comment a picture carries
|
|
@@ -165,6 +165,7 @@ family name share the host's font namespace. Print retains its existing factory
|
|
|
165
165
|
| Import | What it is |
|
|
166
166
|
| --- | --- |
|
|
167
167
|
| `rapier-markdown-kit/markdown-it` | `md.use(rapierMarkdown)`: the reference style's markup in markdown-it, in one line. |
|
|
168
|
+
| `rapier-markdown-kit/parsed` | A frozen, serializable view of completed Markdown tokens, bound to source, revision and grammar. |
|
|
168
169
|
| `rapier-markdown-kit/layout` | The `md-layout:v1` comment a picture carries: parse it, validate it, write it back. |
|
|
169
170
|
| `rapier-markdown-kit/marks` | Unified typography values, span writer and parser installer; preserved color/font comments, ink and page breaks. |
|
|
170
171
|
| `rapier-markdown-kit/model` | Occupancy profiles and the line planner: the wrapping itself. |
|
|
@@ -180,6 +181,51 @@ family name share the host's font namespace. Print retains its existing factory
|
|
|
180
181
|
The canonical inline form is ordinary HTML inside Markdown. CSS support varies; the host's font metrics still
|
|
181
182
|
determine line layout (below).
|
|
182
183
|
|
|
184
|
+
## Read a parsed document
|
|
185
|
+
|
|
186
|
+
`projectMarkdown` copies the tokens from your completed parse into JSON data. Pass the exact source and revision
|
|
187
|
+
captured for that parse, and a grammar identity that includes its parser, plugins and options. The function does not
|
|
188
|
+
parse again. Project before renderer hooks mutate the tokens; the resulting snapshot is detached from those later
|
|
189
|
+
changes. Import this optional subpath only where you need it; the package's main entry does not import it.
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
import MarkdownIt from 'markdown-it';
|
|
193
|
+
import {rapierMarkdown} from 'rapier-markdown-kit/markdown-it';
|
|
194
|
+
import {projectMarkdown, projectionMatches} from 'rapier-markdown-kit/parsed';
|
|
195
|
+
|
|
196
|
+
const source = '# Plan\n\nShips **Friday**.\n';
|
|
197
|
+
const revision = 7;
|
|
198
|
+
const options = {html: true};
|
|
199
|
+
const grammar = {id: 'my-app-markdown-1', options, plugins: ['rapierMarkdown']};
|
|
200
|
+
const md = new MarkdownIt(options).use(rapierMarkdown);
|
|
201
|
+
const env = {};
|
|
202
|
+
const tokens = md.parse(source, env); // The same tokens can feed your renderer.
|
|
203
|
+
const parsed = projectMarkdown({source, revision, grammar, tokens, env, complete: true});
|
|
204
|
+
const json = JSON.stringify(parsed);
|
|
205
|
+
console.log(projectionMatches(parsed, {source, revision, grammar})); // true
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Change the grammar identity when its implementation changes. Grammar options must be JSON data; describe a custom
|
|
209
|
+
rule or renderer by its identity, not by a function. If your parse excludes opening front matter, pass `bodyOffset`
|
|
210
|
+
from that split and keep `source` as the full original document. Token line maps must remain relative to the parsed body.
|
|
211
|
+
|
|
212
|
+
The source binding uses SHA-256 over its UTF-16LE code units, including BOM, original line endings and isolated
|
|
213
|
+
surrogates. `span` ranges are half-open UTF-16 offsets into that source. A span with `scope: 'lines'` covers the token's
|
|
214
|
+
source lines, including container prefixes and the final line ending. Inline delimiters and generated tokens without
|
|
215
|
+
recorded ranges have `span: null`; `coverage` and `omissions` report these missing ranges. Never infer an exact inline
|
|
216
|
+
edit boundary from a containing line span.
|
|
217
|
+
|
|
218
|
+
Recorded `meta.mdLayoutSource` positions are translated from the parser's normalized body into the same original
|
|
219
|
+
UTF-16 source coordinates and labeled `unit: 'utf16'`. Their marker text and any reason for an unmapped target remain
|
|
220
|
+
with the layout record. Other JSON metadata retains the grammar's values; internal probe identities are not exported.
|
|
221
|
+
|
|
222
|
+
The projection includes canonical token content, attributes, JSON metadata and reference, abbreviation and footnote
|
|
223
|
+
definitions. It omits original source gaps, interpreted front matter, rendered HTML and geometry, decoded pictures,
|
|
224
|
+
final heading IDs, and evaluated Will permissions. Non-JSON token metadata becomes `null` with a counted omission.
|
|
225
|
+
Token strings remain document data for your existing renderer and sanitizer. `projectionMatches` checks currentness;
|
|
226
|
+
it does not authenticate a projection received from another party or authorize an edit. Source transactions, Will,
|
|
227
|
+
Undo and Save stay with their existing owners.
|
|
228
|
+
|
|
183
229
|
## Refined paragraphs
|
|
184
230
|
|
|
185
231
|
The existing comment expresses the paragraph's intent without storing font-dependent line breaks:
|
package/dist/images/playback.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
export function createMediaPlayback(win, inspectGIF) {
|
|
3
|
+
const FRAMES = 3600, LOOP_MS = 3600000;
|
|
3
4
|
const active = new Map(), encode = bytes => bytes.toBase64();
|
|
4
5
|
const bytes = async url => {
|
|
5
6
|
const text = url.slice(url.indexOf(',') + 1), slice = 1 << 20;
|
|
@@ -14,7 +15,7 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
14
15
|
function raster(url, canvas, changed = () => {}, dimensions = null) {
|
|
15
16
|
if (url.length > 24 * 1024 * 1024 || !/^data:image\/(gif|jxl);base64,/i.test(url) || (url.length - url.indexOf(',') - 1) % 4) throw new Error('Unsupported animation source');
|
|
16
17
|
const type = url.slice(5, url.indexOf(';')).toLowerCase();
|
|
17
|
-
let data = null, info = null, size = null, decoder = null, timer = 0, closed = false, paused = true, ticket = 0, next = 0, loops = 0, duration = 0, single = false, held = false;
|
|
18
|
+
let data = null, info = null, size = null, decoder = null, timer = 0, closed = false, paused = true, ticket = 0, next = 0, loops = 0, duration = 0, single = false, held = false, due = 0;
|
|
18
19
|
const state = {animated: true, status: 'loading', first: '', frames: 0};
|
|
19
20
|
const admitted = (async () => {
|
|
20
21
|
const buffer = await bytes(url); info = type === 'image/gif' ? inspectGIF(buffer) : null;
|
|
@@ -47,11 +48,12 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
47
48
|
state.status = 'playback-budget'; paused = true; changed(state); return;
|
|
48
49
|
}
|
|
49
50
|
active.set(canvas, size.width * size.height);
|
|
51
|
+
next = 0; duration = 0; due = 0;
|
|
50
52
|
decoder = new win.ImageDecoder({data, type, preferAnimation: true});
|
|
51
53
|
const current = decoder; await settle(current.tracks.ready);
|
|
52
54
|
if (closed || paused || stamp !== ticket || decoder !== current) return;
|
|
53
55
|
const track = current.tracks.selectedTrack;
|
|
54
|
-
if (!track || track.frameCount < 1 || track.frameCount >
|
|
56
|
+
if (!track || track.frameCount < 1 || track.frameCount > FRAMES || info && track.frameCount !== info.frames) throw new Error('Animation frame budget or container mismatch');
|
|
55
57
|
state.frames = track.frameCount; state.animated = track.animated;
|
|
56
58
|
}
|
|
57
59
|
const current = decoder, decoding = current.decode({frameIndex: next, completeFramesOnly: true});
|
|
@@ -62,13 +64,13 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
62
64
|
try {
|
|
63
65
|
if (closed || paused || stamp !== ticket || decoder !== current) return;
|
|
64
66
|
const w = frame.displayWidth, h = frame.displayHeight;
|
|
65
|
-
if (!result.complete || w !== size.width || h !== size.height
|
|
67
|
+
if (!result.complete || w !== size.width || h !== size.height) throw new Error('Decoded animation exceeds the image budget');
|
|
66
68
|
if (canvas.width !== w) canvas.width = w;
|
|
67
69
|
if (canvas.height !== h) canvas.height = h;
|
|
68
70
|
const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, w, h); ctx.drawImage(frame, 0, 0);
|
|
69
71
|
if (!state.first && next === 0) state.first = canvas.toDataURL('image/png');
|
|
70
72
|
const delay = type !== 'image/gif' ? Math.max(8, frame.duration / 1000) : frame.duration > 10000 ? Math.max(20, frame.duration / 1000) : 100;
|
|
71
|
-
duration += delay; if (duration >
|
|
73
|
+
duration += delay; if (duration > LOOP_MS) throw new Error('Animation exceeds the duration budget');
|
|
72
74
|
state.status = 'ready'; state.frame = next; changed(state);
|
|
73
75
|
if (single) {paused = true; closeDecoder(); return;}
|
|
74
76
|
next++;
|
|
@@ -76,7 +78,8 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
76
78
|
next = 0; duration = 0; loops++;
|
|
77
79
|
if (!state.animated || Number.isFinite(current.tracks.selectedTrack.repetitionCount) && loops > current.tracks.selectedTrack.repetitionCount) {paused = true; closeDecoder(); return;}
|
|
78
80
|
}
|
|
79
|
-
|
|
81
|
+
const now = win.performance.now(); due = due && now - due < 1000 ? due + delay : now + delay;
|
|
82
|
+
timer = win.setTimeout(step, Math.max(0, due - now));
|
|
80
83
|
} finally {frame.close();}
|
|
81
84
|
} catch (error) {
|
|
82
85
|
if (closed || stamp !== ticket) return;
|
|
@@ -85,7 +88,7 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
85
88
|
}
|
|
86
89
|
return {state, play() {if (!closed && paused && !['decode-failed', 'admission-failed'].includes(state.status)) {paused = false; void step();}},
|
|
87
90
|
async first() {if (!state.first && !closed) {single = true; next = 0; paused = false; try {await step();} finally {single = false; paused = true; closeDecoder(); if (held) {held = false; this.suspend();}}} return state.first;},
|
|
88
|
-
pause() {paused = true; ticket++; win.clearTimeout(timer); timer = 0;},
|
|
91
|
+
pause() {paused = true; ticket++; win.clearTimeout(timer); timer = 0; due = 0;},
|
|
89
92
|
suspend() {if (single) {held = true; return;} paused = true; closeDecoder(); canvas.width = canvas.height = 0; state.status = 'suspended'; changed(state);},
|
|
90
93
|
close() {closed = true; paused = true; closeDecoder(); canvas.width = canvas.height = 0;},
|
|
91
94
|
get paused() {return paused;}};
|
package/dist/kit/markdown-it.mjs
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import {parseLayout, imageStyle} from './layout.mjs';
|
|
3
3
|
import {typography, isPageBreakBlock} from './marks.mjs';
|
|
4
4
|
import {installMarkdownImages} from './assets.mjs';
|
|
5
|
+
import {tuneMarkdownEngine} from '../spec/md-engine.mjs';
|
|
5
6
|
|
|
6
7
|
const PAGE_BREAK = '<div class="rapier-page-break" data-md-break="page" role="separator" aria-label="Page break"></div>\n';
|
|
7
8
|
|
|
8
9
|
export function rapierMarkdown(md) {
|
|
9
10
|
if (md.__rapierMarkdownInstalled) return md;
|
|
10
11
|
md.__rapierMarkdownInstalled = true;
|
|
12
|
+
tuneMarkdownEngine(md);
|
|
11
13
|
typography.install(installMarkdownImages(md));
|
|
12
14
|
md.renderer.rules.table_open = (tokens, index, options, env, renderer) =>
|
|
13
15
|
'<div class="table-scroll-wrap">\n' + renderer.renderToken(tokens, index, options);
|
|
@@ -15,10 +17,13 @@ export function rapierMarkdown(md) {
|
|
|
15
17
|
renderer.renderToken(tokens, index, options) + '</div>\n';
|
|
16
18
|
md.core.ruler.after('inline', 'rapier-layout', state => {
|
|
17
19
|
for (let i = 1; i < state.tokens.length; i++) {
|
|
18
|
-
const inline = state.tokens[i]
|
|
19
|
-
if (inline.type !== 'inline'
|
|
20
|
-
const children = inline.children, marker = children.
|
|
21
|
-
|
|
20
|
+
const inline = state.tokens[i];
|
|
21
|
+
if (inline.type !== 'inline') continue;
|
|
22
|
+
const children = inline.children, marker = children[children.length - 1];
|
|
23
|
+
if (marker?.type !== 'html_inline') continue;
|
|
24
|
+
const owner = state.tokens[i - 1];
|
|
25
|
+
if (owner.type !== 'paragraph_open' && owner.type !== 'heading_open') continue;
|
|
26
|
+
const layout = parseLayout(marker.content);
|
|
22
27
|
if (!layout) continue;
|
|
23
28
|
children.pop();
|
|
24
29
|
if (layout.align) owner.attrSet('data-md-align', layout.align);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export type Json = null | string | number | boolean | readonly Json[] | {readonly [key: string]: Json};
|
|
3
|
+
export type Revision = string | number;
|
|
4
|
+
export interface GrammarIdentity {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly options: {readonly [key: string]: Json};
|
|
7
|
+
readonly [key: string]: Json;
|
|
8
|
+
}
|
|
9
|
+
export interface Utf16Span { readonly start: number; readonly end: number; readonly unit: 'utf16'; }
|
|
10
|
+
export interface TokenSpan extends Utf16Span { readonly scope: 'lines'; }
|
|
11
|
+
export interface MarkdownTokenInput {
|
|
12
|
+
readonly type: string;
|
|
13
|
+
readonly tag?: string;
|
|
14
|
+
readonly nesting: -1 | 0 | 1;
|
|
15
|
+
readonly level?: number;
|
|
16
|
+
readonly block?: boolean;
|
|
17
|
+
readonly hidden?: boolean;
|
|
18
|
+
readonly content?: string;
|
|
19
|
+
readonly markup?: string;
|
|
20
|
+
readonly info?: string;
|
|
21
|
+
readonly attrs?: unknown;
|
|
22
|
+
readonly meta?: unknown;
|
|
23
|
+
readonly map?: readonly [number, number] | null;
|
|
24
|
+
readonly children?: readonly MarkdownTokenInput[] | null;
|
|
25
|
+
}
|
|
26
|
+
export interface ParsedMarkdown {
|
|
27
|
+
/** The exact full document, before normalization or front matter removal. */
|
|
28
|
+
readonly source: string;
|
|
29
|
+
readonly revision: Revision;
|
|
30
|
+
/** Includes every option or plugin setting that can change this parse's meaning. */
|
|
31
|
+
readonly grammar: GrammarIdentity;
|
|
32
|
+
readonly complete: true;
|
|
33
|
+
/** The shared front matter owner's body offset; zero if the parser read the full source.
|
|
34
|
+
* Token maps and recorded layout target offsets must be relative to that parsed body. */
|
|
35
|
+
readonly bodyOffset?: number;
|
|
36
|
+
readonly tokens: readonly MarkdownTokenInput[];
|
|
37
|
+
readonly env?: {
|
|
38
|
+
readonly references?: unknown;
|
|
39
|
+
readonly abbreviations?: unknown;
|
|
40
|
+
readonly footnotes?: {readonly list?: readonly {readonly label?: string; readonly count?: number}[]};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export interface ProjectedToken {
|
|
44
|
+
readonly type: string;
|
|
45
|
+
readonly tag: string;
|
|
46
|
+
readonly nesting: -1 | 0 | 1;
|
|
47
|
+
readonly level: number | null;
|
|
48
|
+
readonly block: boolean;
|
|
49
|
+
readonly hidden: boolean;
|
|
50
|
+
readonly content: string;
|
|
51
|
+
readonly markup: string;
|
|
52
|
+
readonly info: string;
|
|
53
|
+
readonly attrs: Json;
|
|
54
|
+
readonly meta: Json;
|
|
55
|
+
readonly span: TokenSpan | null;
|
|
56
|
+
readonly children: readonly ProjectedToken[] | null;
|
|
57
|
+
}
|
|
58
|
+
export interface MarkdownProjection {
|
|
59
|
+
readonly format: 'rapier-parsed/1';
|
|
60
|
+
readonly source: {
|
|
61
|
+
readonly revision: Revision;
|
|
62
|
+
readonly chars: number;
|
|
63
|
+
readonly hash: {readonly algorithm: 'sha256'; readonly encoding: 'utf16le'; readonly value: string};
|
|
64
|
+
};
|
|
65
|
+
readonly grammar: GrammarIdentity;
|
|
66
|
+
readonly body: Utf16Span;
|
|
67
|
+
readonly frontmatter: Utf16Span | null;
|
|
68
|
+
readonly tokens: readonly ProjectedToken[];
|
|
69
|
+
readonly definitions: Json;
|
|
70
|
+
readonly coverage: {readonly tokens: 'complete'; readonly tokenCount: number; readonly spans: 'complete' | 'partial'};
|
|
71
|
+
readonly omissions: readonly {readonly domain: string; readonly reason: string; readonly count?: number}[];
|
|
72
|
+
}
|
|
73
|
+
/** Project one completed parse. Does not parse again, normalize source, render or admit edits. */
|
|
74
|
+
export function projectMarkdown(parsed: ParsedMarkdown): MarkdownProjection;
|
|
75
|
+
/** Checks source, revision and grammar binding; does not authenticate an externally supplied projection. */
|
|
76
|
+
export function projectionMatches(projection: MarkdownProjection, current: {
|
|
77
|
+
readonly source: string; readonly revision: Revision; readonly grammar: GrammarIdentity;
|
|
78
|
+
}): boolean;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {sha256State} from './ledger/hash.mjs';
|
|
3
|
+
import {splitOpeningFrontmatter} from '../spec/frontmatter.mjs';
|
|
4
|
+
|
|
5
|
+
export function projectMarkdown(parsed) {
|
|
6
|
+
const {source, revision, grammar, tokens, env = {}, bodyOffset = 0, complete} = parsed || {};
|
|
7
|
+
requireSource(source, revision);
|
|
8
|
+
if (complete !== true || !Array.isArray(tokens)) throw new TypeError('A completed Markdown parse is required');
|
|
9
|
+
const front = splitOpeningFrontmatter(source);
|
|
10
|
+
if (!Number.isSafeInteger(bodyOffset) || bodyOffset < 0 || bodyOffset > source.length ||
|
|
11
|
+
bodyOffset !== 0 && bodyOffset !== front.bodyOffset) throw new TypeError('The parse must cover the document body');
|
|
12
|
+
const grammarCopy = copyGrammar(grammar), counts = new Map();
|
|
13
|
+
const omitted = (domain, reason) => {
|
|
14
|
+
const key = domain + ':' + reason;
|
|
15
|
+
const row = counts.get(key) || {domain, reason, count: 0};
|
|
16
|
+
row.count++; counts.set(key, row);
|
|
17
|
+
};
|
|
18
|
+
const lines = [bodyOffset], normalizedLines = [0];
|
|
19
|
+
let collapsed = 0;
|
|
20
|
+
for (const match of source.slice(bodyOffset).matchAll(/\r\n?|\n/g)) {
|
|
21
|
+
const end = match.index + match[0].length;
|
|
22
|
+
collapsed += match[0].length - 1;
|
|
23
|
+
lines.push(bodyOffset + end); normalizedLines.push(end - collapsed);
|
|
24
|
+
}
|
|
25
|
+
const originalOffset = offset => {
|
|
26
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > source.length - bodyOffset - collapsed)
|
|
27
|
+
throw new TypeError('A Markdown layout target has an invalid source offset');
|
|
28
|
+
let low = 0, high = normalizedLines.length;
|
|
29
|
+
while (low + 1 < high) {
|
|
30
|
+
const middle = (low + high) >>> 1;
|
|
31
|
+
if (normalizedLines[middle] <= offset) low = middle; else high = middle;
|
|
32
|
+
}
|
|
33
|
+
return lines[low] + offset - normalizedLines[low];
|
|
34
|
+
};
|
|
35
|
+
const metadata = token => {
|
|
36
|
+
const meta = copyJSON(token.meta ?? null, () => omitted('metadata', 'not-json'));
|
|
37
|
+
const target = meta?.mdLayoutSource;
|
|
38
|
+
if (!target) return meta;
|
|
39
|
+
const marker = target.marker ? Object.freeze({...target.marker, start: originalOffset(target.marker.start),
|
|
40
|
+
end: originalOffset(target.marker.end), unit: 'utf16'}) : null;
|
|
41
|
+
return Object.freeze({...meta, mdLayoutSource: Object.freeze({...target, start: originalOffset(target.start),
|
|
42
|
+
end: originalOffset(target.end), insert: target.insert == null ? null : originalOffset(target.insert), marker, unit: 'utf16'})});
|
|
43
|
+
};
|
|
44
|
+
const span = token => {
|
|
45
|
+
const map = token.map;
|
|
46
|
+
if (map == null) {
|
|
47
|
+
omitted('spans', token.block ? 'block-range-not-retained' : 'inline-range-not-retained');
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(map) || map.length !== 2 || map.some(at => !Number.isSafeInteger(at) || at < 0) ||
|
|
51
|
+
map[0] > map[1] || map[0] >= lines.length || map[1] > lines.length)
|
|
52
|
+
throw new TypeError('A Markdown token has an invalid line range');
|
|
53
|
+
if (map[0] === map[1]) { omitted('spans', token.block ? 'block-range-not-retained' : 'inline-range-not-retained'); return null; }
|
|
54
|
+
return Object.freeze({start: lines[map[0]], end: lines[map[1]] ?? source.length, unit: 'utf16', scope: 'lines'});
|
|
55
|
+
};
|
|
56
|
+
let count = 0;
|
|
57
|
+
const copyTokens = input => Object.freeze(input.map(token => {
|
|
58
|
+
if (!token || typeof token.type !== 'string' || ![-1, 0, 1].includes(token.nesting) ||
|
|
59
|
+
token.children != null && !Array.isArray(token.children)) throw new TypeError('Invalid Markdown token');
|
|
60
|
+
count++;
|
|
61
|
+
const scalar = key => {
|
|
62
|
+
if (token[key] == null) return '';
|
|
63
|
+
if (typeof token[key] !== 'string') throw new TypeError('Invalid Markdown token ' + key);
|
|
64
|
+
return token[key];
|
|
65
|
+
};
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
type: token.type, tag: scalar('tag'), nesting: token.nesting,
|
|
68
|
+
level: Number.isSafeInteger(token.level) && token.level >= 0 ? token.level : null,
|
|
69
|
+
block: token.block === true, hidden: token.hidden === true,
|
|
70
|
+
content: scalar('content'), markup: scalar('markup'), info: scalar('info'),
|
|
71
|
+
attrs: copyJSON(token.attrs ?? null, () => omitted('attributes', 'not-json')),
|
|
72
|
+
meta: metadata(token),
|
|
73
|
+
span: span(token), children: token.children ? copyTokens(token.children) : null,
|
|
74
|
+
});
|
|
75
|
+
}));
|
|
76
|
+
const projected = copyTokens(tokens);
|
|
77
|
+
const definitions = copyJSON({
|
|
78
|
+
references: env.references || {}, abbreviations: env.abbreviations || {},
|
|
79
|
+
footnotes: (env.footnotes?.list || []).map(note => ({label: note.label ?? null, count: note.count ?? 0})),
|
|
80
|
+
}, () => omitted('definitions', 'not-json'));
|
|
81
|
+
const omissions = [
|
|
82
|
+
...counts.values(),
|
|
83
|
+
{domain: 'source', reason: 'original-text-and-unrepresented-gaps-are-not-included'},
|
|
84
|
+
{domain: 'frontmatter', reason: 'metadata-is-not-interpreted'},
|
|
85
|
+
{domain: 'rendering', reason: 'html-dom-heading-ids-layout-and-decoded-assets-are-not-included'},
|
|
86
|
+
{domain: 'authority', reason: 'will-permissions-and-edit-admission-are-not-evaluated'},
|
|
87
|
+
{domain: 'environment', reason: 'only-reference-abbreviation-and-footnote-definitions-are-included'},
|
|
88
|
+
];
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
format: 'rapier-parsed/1',
|
|
91
|
+
source: Object.freeze({revision, chars: source.length,
|
|
92
|
+
hash: Object.freeze({algorithm: 'sha256', encoding: 'utf16le', value: sourceHash(source)})}),
|
|
93
|
+
grammar: grammarCopy,
|
|
94
|
+
body: Object.freeze({start: bodyOffset, end: source.length, unit: 'utf16'}),
|
|
95
|
+
frontmatter: bodyOffset ? Object.freeze({start: 0, end: bodyOffset, unit: 'utf16'}) : null,
|
|
96
|
+
tokens: projected, definitions,
|
|
97
|
+
coverage: Object.freeze({tokens: 'complete', tokenCount: count,
|
|
98
|
+
spans: counts.has('spans:block-range-not-retained') || counts.has('spans:inline-range-not-retained') ? 'partial' : 'complete'}),
|
|
99
|
+
omissions: Object.freeze(omissions.map(Object.freeze)),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function projectionMatches(projection, {source, revision, grammar} = {}) {
|
|
104
|
+
requireSource(source, revision);
|
|
105
|
+
const expected = copyGrammar(grammar);
|
|
106
|
+
if (!(projection?.format === 'rapier-parsed/1' && projection.source?.revision === revision &&
|
|
107
|
+
projection.source.chars === source.length && projection.source.hash?.algorithm === 'sha256' &&
|
|
108
|
+
projection.source.hash.encoding === 'utf16le' && projection.source.hash.value === sourceHash(source))) return false;
|
|
109
|
+
try { return JSON.stringify(copyGrammar(projection.grammar)) === JSON.stringify(expected); }
|
|
110
|
+
catch (_) { return false; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requireSource(source, revision) {
|
|
114
|
+
if (typeof source !== 'string' || !(typeof revision === 'string' && revision.length ||
|
|
115
|
+
Number.isSafeInteger(revision) && revision >= 0)) throw new TypeError('Exact source and its revision are required');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function copyGrammar(grammar) {
|
|
119
|
+
const copy = copyJSON(grammar, () => { throw new TypeError('Grammar identity and options must be JSON data'); });
|
|
120
|
+
if (!copy || typeof copy.id !== 'string' || !copy.id || !copy.options ||
|
|
121
|
+
typeof copy.options !== 'object' || Array.isArray(copy.options))
|
|
122
|
+
throw new TypeError('A grammar identity and its options are required');
|
|
123
|
+
return copy;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function copyJSON(value, omit, ancestors = new Set()) {
|
|
127
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value === 0 ? 0 : value;
|
|
128
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
129
|
+
if (!value || typeof value !== 'object' || ancestors.has(value) ||
|
|
130
|
+
!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value))) { omit(); return null; }
|
|
131
|
+
ancestors.add(value);
|
|
132
|
+
let result;
|
|
133
|
+
if (Array.isArray(value)) result = Array.from({length: value.length}, (_, index) => {
|
|
134
|
+
const property = Object.getOwnPropertyDescriptor(value, index);
|
|
135
|
+
if (!property || !Object.hasOwn(property, 'value')) { omit(); return null; }
|
|
136
|
+
return copyJSON(property.value, omit, ancestors);
|
|
137
|
+
});
|
|
138
|
+
else result = Object.fromEntries(Object.keys(value).sort().map(key => {
|
|
139
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
140
|
+
if (!Object.hasOwn(property, 'value')) { omit(); return [key, null]; }
|
|
141
|
+
return [key, copyJSON(property.value, omit, ancestors)];
|
|
142
|
+
}));
|
|
143
|
+
ancestors.delete(value);
|
|
144
|
+
return Object.freeze(result);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function sourceHash(source) {
|
|
148
|
+
const hash = sha256State(), bytes = new Uint8Array(32768);
|
|
149
|
+
for (let at = 0; at < source.length;) {
|
|
150
|
+
let used = 0;
|
|
151
|
+
while (at < source.length && used < bytes.length) {
|
|
152
|
+
const code = source.charCodeAt(at++); bytes[used++] = code & 255; bytes[used++] = code >>> 8;
|
|
153
|
+
}
|
|
154
|
+
hash.update(bytes.subarray(0, used));
|
|
155
|
+
}
|
|
156
|
+
return hash.finish();
|
|
157
|
+
}
|
|
@@ -90,9 +90,11 @@ function initMarkdownIt() {
|
|
|
90
90
|
md.renderer.rules.softbreak = () =>
|
|
91
91
|
sourceToken('\n', '\u2060', 'rapier-source-token--softbreak');
|
|
92
92
|
md.core.ruler.before('text_join', 'rapier_escape_token', state => {
|
|
93
|
-
for (
|
|
93
|
+
for (let at = 0; at < state.tokens.length; at++) {
|
|
94
|
+
const token = state.tokens[at];
|
|
94
95
|
if (token.type !== 'inline' || !token.children) continue;
|
|
95
|
-
for (
|
|
96
|
+
for (let next = 0; next < token.children.length; next++) {
|
|
97
|
+
const child = token.children[next];
|
|
96
98
|
if (child.type === 'text_special' && child.info === 'escape' && child.content.length === 1 && !/[\\*`[\]_]/.test(child.content)) child.type = 'rapier_escape';
|
|
97
99
|
}
|
|
98
100
|
}
|
|
@@ -251,7 +253,8 @@ function initMarkdownIt() {
|
|
|
251
253
|
|
|
252
254
|
md.core.ruler.push('rapier_setext_rule', function (state) {
|
|
253
255
|
let lines = null;
|
|
254
|
-
for (
|
|
256
|
+
for (let at = 0; at < state.tokens.length; at++) {
|
|
257
|
+
const token = state.tokens[at];
|
|
255
258
|
if (token.type !== 'heading_open') continue;
|
|
256
259
|
if (token.markup !== '=' && token.markup !== '-') continue;
|
|
257
260
|
if (!token.map) continue;
|
|
@@ -489,14 +492,14 @@ function _rapierInstallSemanticProbe(instance) {
|
|
|
489
492
|
const nextId = (targetEnv.__rapierInlineProbeSeq || 0) + 1;
|
|
490
493
|
targetEnv.__rapierInlineProbeSeq = nextId;
|
|
491
494
|
targetEnv.__rapierInlineProbeId = nextId;
|
|
492
|
-
|
|
493
|
-
Object.defineProperty(outTokens, '_rapierInlineProbeId', { value: nextId, configurable: true });
|
|
494
|
-
} catch (_) { outTokens._rapierInlineProbeId = nextId; }
|
|
495
|
+
const candidateStart = targetEnv.__rapierInlineCandidates?.length || 0;
|
|
495
496
|
try {
|
|
496
497
|
const result = originalParse.call(this, source, parser, targetEnv, outTokens);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
498
|
+
if ((targetEnv.__rapierInlineCandidates?.length || 0) > candidateStart) {
|
|
499
|
+
Object.defineProperty(outTokens, '_rapierInlineProbeId', { value: nextId, configurable: true });
|
|
500
|
+
for (const token of outTokens) {
|
|
501
|
+
Object.defineProperty(token, '_rapierInlineProbeId', { value: nextId, configurable: true });
|
|
502
|
+
}
|
|
500
503
|
}
|
|
501
504
|
return result;
|
|
502
505
|
} finally { targetEnv.__rapierInlineProbeId = priorId; }
|
|
@@ -504,6 +507,8 @@ function _rapierInstallSemanticProbe(instance) {
|
|
|
504
507
|
|
|
505
508
|
instance.inline.ruler.before('escape', 'rapier_semantic_probe', function (state, silent) {
|
|
506
509
|
if (silent || !state || !state.env) return false;
|
|
510
|
+
const opener = state.src.charCodeAt(state.pos);
|
|
511
|
+
if (opener !== 0x5b && opener !== 0x21) return false;
|
|
507
512
|
const inlineId = Number(state.env.__rapierInlineProbeId || 0);
|
|
508
513
|
if (!inlineId) return false;
|
|
509
514
|
const src = state.src;
|
|
@@ -561,6 +566,8 @@ function _rapierInstallSemanticProbe(instance) {
|
|
|
561
566
|
if (src.charCodeAt(after) === 0x28) {
|
|
562
567
|
let destinationPos = after + 1;
|
|
563
568
|
while (destinationPos < max && /[ \t\n]/.test(src.charAt(destinationPos))) destinationPos++;
|
|
569
|
+
const first = src.charCodeAt(destinationPos);
|
|
570
|
+
if (first !== 0x23 && first !== 0x3c && first !== 0x5c && first !== 0x26) return false;
|
|
564
571
|
const destination = state.md.helpers.parseLinkDestination(src, destinationPos, max);
|
|
565
572
|
if (!destination || !destination.ok || !String(destination.str || '').startsWith('#')) return false;
|
|
566
573
|
const rawDestination = String(destination.str || '');
|
|
@@ -184,7 +184,8 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
184
184
|
[side, settings?.geometry ? settings.geometry[side] / 20 : (side === 'top' ? 12 : 14) * 72 / 25.4]));
|
|
185
185
|
const bodyWidth = Math.floor((width - margins.left - margins.right) * 4 / 3);
|
|
186
186
|
const bodyHeight = Math.floor((height - margins.top - margins.bottom) * 4 / 3);
|
|
187
|
-
if (bodyWidth < 1 || bodyHeight < 1
|
|
187
|
+
if (bodyWidth < 1 || bodyHeight < 1) refuse('pdf_page_geometry_invalid');
|
|
188
|
+
const tiled = bodyWidth > CAPTURE_EDGE || bodyHeight > CAPTURE_EDGE || bodyWidth * bodyHeight > CAPTURE_PIXELS;
|
|
188
189
|
const frame = document.createElement('iframe');
|
|
189
190
|
let vectors, backdrop = null, nativeGlyphs, filmSession;
|
|
190
191
|
let rejectAbort;
|
|
@@ -319,7 +320,21 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
319
320
|
if (imageBytes > maxBytes) refuse('export_too_large');
|
|
320
321
|
pages.push({rgb: packed});
|
|
321
322
|
};
|
|
322
|
-
|
|
323
|
+
const tileWidth = Math.min(bodyWidth, CAPTURE_EDGE), tileHeight = Math.min(bodyHeight, CAPTURE_EDGE, Math.floor(CAPTURE_PIXELS / tileWidth));
|
|
324
|
+
for (let index = 0; tiled && index < count; index++) {
|
|
325
|
+
const tiles = [];
|
|
326
|
+
for (let y = 0; y < bodyHeight; y += tileHeight) for (let x = 0; x < bodyWidth; x += tileWidth) {
|
|
327
|
+
check();
|
|
328
|
+
const width = Math.min(tileWidth, bodyWidth - x), height = Math.min(tileHeight, bodyHeight - y);
|
|
329
|
+
const shot = await step(filmSession.raster({x: index * bodyWidth + x, y, width, height}));
|
|
330
|
+
if (shot?.width !== width || shot?.height !== height || !(shot.rgb instanceof Uint8Array)) refuse('pdf_render_unavailable');
|
|
331
|
+
await pack(shot.rgb);
|
|
332
|
+
shot.rgb = null;
|
|
333
|
+
tiles.push({...pages.pop(), x, y, width, height});
|
|
334
|
+
}
|
|
335
|
+
pages.push({tiles});
|
|
336
|
+
}
|
|
337
|
+
for (let first = 0; !tiled && first < count;) {
|
|
323
338
|
check();
|
|
324
339
|
const span = Math.min(share, count - first);
|
|
325
340
|
try {
|
|
@@ -698,9 +713,10 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
698
713
|
const marked = [];
|
|
699
714
|
parentTree.push(marked);
|
|
700
715
|
const held = pages[index], flate = !!held?.rgb;
|
|
701
|
-
const
|
|
716
|
+
const tiles = held?.tiles || [{...(flate ? held : {jpeg: held}), x: 0, y: 0, width: pixelWidth, height: pixelHeight}];
|
|
717
|
+
const pictures = tiles.map((tile, at) => '/Im0_' + at + ' ' + stream('/Type /XObject /Subtype /Image /Width ' + tile.width + ' /Height ' + tile.height +
|
|
718
|
+
' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /' + (tile.rgb ? 'FlateDecode' : 'DCTDecode'), tile.rgb || tile.jpeg) + ' 0 R'), states = [];
|
|
702
719
|
pages[index] = null;
|
|
703
|
-
const pictures = ['/Im0 ' + picture + ' 0 R'], states = [];
|
|
704
720
|
const emit = (drawing, left, top, clipWidth, clipHeight) => {
|
|
705
721
|
let out = `q\n.75 0 0 -.75 ${decimal(left)} ${decimal(top)} cm\n0 0 ${clipWidth} ${clipHeight} re W n\n1 0 0 1 ${decimal(drawing.x)} 0 cm\n`;
|
|
706
722
|
out += [drawing.clip.x, drawing.clip.y, drawing.clip.width, drawing.clip.height].map(decimal).join(' ') + ' re W n\n';
|
|
@@ -728,7 +744,8 @@ async function writeRasterPdf({pages, textPages, linkPages = [], vectorPages = [
|
|
|
728
744
|
};
|
|
729
745
|
let commands = '';
|
|
730
746
|
for (const drawing of backdrop || []) commands += emit(drawing, 0, height, Math.round(width * 4 / 3), Math.round(height * 4 / 3));
|
|
731
|
-
commands += '/Artifact BMC\
|
|
747
|
+
commands += '/Artifact BMC\n' + tiles.map((tile, at) => 'q\n' + [decimal(tile.width * .75), 0, 0, decimal(tile.height * .75), decimal(margins.left + tile.x * .75),
|
|
748
|
+
decimal(height - margins.top - (tile.y + tile.height) * .75)].join(' ') + ' cm\n/Im0_' + at + ' Do\nQ\n').join('') + 'EMC\n';
|
|
732
749
|
for (const drawing of vectorPages[index] || []) commands += emit(drawing, margins.left, height - margins.top, pixelWidth, pixelHeight);
|
|
733
750
|
const textAlphas = new Map();
|
|
734
751
|
for (const run of textPages[index]) {
|
|
@@ -119,7 +119,16 @@ const PAPERS = Object.freeze({
|
|
|
119
119
|
a4: Object.freeze({name: 'a4', width: 11906, height: 16838, css: 'A4'}),
|
|
120
120
|
a5: Object.freeze({name: 'a5', width: 8391, height: 11906, css: 'A5'}),
|
|
121
121
|
legal: Object.freeze({name: 'legal', width: 12240, height: 20160, css: 'legal'}),
|
|
122
|
+
tabloid: Object.freeze({name: 'tabloid', width: 15840, height: 24480, css: '11in 17in'}),
|
|
123
|
+
a3: Object.freeze({name: 'a3', width: 16838, height: 23811, css: 'A3'}),
|
|
122
124
|
});
|
|
125
|
+
function paperOf(value) {
|
|
126
|
+
const name = String(value || '').toLowerCase(), named = Object.hasOwn(PAPERS, name) ? PAPERS[name] : null;
|
|
127
|
+
if (named) return named;
|
|
128
|
+
const pixels = /^(\d{3,5})x(\d{3,5})px$/.exec(name), width = Number(pixels?.[1]), height = Number(pixels?.[2]);
|
|
129
|
+
return pixels && width <= 10000 && height <= 10000 ? Object.freeze({name, width: width * 15, height: height * 15, css: width + 'px ' + height + 'px'}) : null;
|
|
130
|
+
}
|
|
131
|
+
export const PAGE_SIZES = Object.freeze(['letter', 'legal', 'tabloid', 'a3', 'a4', 'a5', '1080x1080px', '1080x1350px', '1920x1080px', '1200x630px']);
|
|
123
132
|
const EMPTY = Object.freeze({
|
|
124
133
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
125
134
|
papersize: null, geometry: null, pagestyle: null, spacing: null,
|
|
@@ -334,7 +343,7 @@ export function readDocumentSettings(markdown) {
|
|
|
334
343
|
const monofont = documentFont(take('monofont'));
|
|
335
344
|
const defaultfont = documentFont(take('defaultfont'));
|
|
336
345
|
const linestretch = leading(take('linestretch'));
|
|
337
|
-
const papersize =
|
|
346
|
+
const papersize = paperOf(take('papersize'));
|
|
338
347
|
const geometry = take('geometry') != null ? marginOf(take('geometry')) : null;
|
|
339
348
|
const page = String(take('pagestyle') || '').toLowerCase();
|
|
340
349
|
return Object.freeze({
|
|
@@ -359,14 +368,14 @@ export function updateDocumentSettings(markdown, patch) {
|
|
|
359
368
|
text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
|
|
360
369
|
for (const [key, supplied] of Object.entries(patch || {})) {
|
|
361
370
|
const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
|
|
362
|
-
if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
371
|
+
if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry', 'papersize'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
363
372
|
const matches = fields.filter(item => item.key === key);
|
|
364
373
|
if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
|
|
365
374
|
const field = matches[0];
|
|
366
|
-
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(supplied) : leading(supplied);
|
|
375
|
+
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(supplied) : key === 'papersize' ? paperOf(supplied) : leading(supplied);
|
|
367
376
|
if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
|
|
368
|
-
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : key === 'geometry' ? unquote(supplied).trim() : String(value.factor);
|
|
369
|
-
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : key === 'geometry' ? ['top', 'right', 'bottom', 'left'].every(side => before.geometry[side] === value[side]) : before.linestretch.factor === value.factor)) continue;
|
|
377
|
+
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : key === 'geometry' ? unquote(supplied).trim() : key === 'papersize' ? value.name : String(value.factor);
|
|
378
|
+
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : key === 'geometry' ? ['top', 'right', 'bottom', 'left'].every(side => before.geometry[side] === value[side]) : key === 'papersize' ? before.papersize.name === value.name : before.linestretch.factor === value.factor)) continue;
|
|
370
379
|
if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
|
|
371
380
|
if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
|
|
372
381
|
if (field.opaque || field.invalid || field.multiline || field.block) refuse(key);
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -23,6 +23,7 @@ export function installMarkdownImages(md) {
|
|
|
23
23
|
const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
|
|
24
24
|
md.block.ruler.at('reference', (state, start, end, silent) => {
|
|
25
25
|
if (silent) return reference(state, start, end, silent);
|
|
26
|
+
if (state.src.charCodeAt(state.bMarks[start] + state.tShift[start]) !== 0x5B) return false;
|
|
26
27
|
const saved = state.env.references, local = Object.create(null);
|
|
27
28
|
state.env.references = local;
|
|
28
29
|
let accepted;
|
|
@@ -40,12 +41,12 @@ export function installMarkdownImages(md) {
|
|
|
40
41
|
md.inline.ruler.at('image', (state, silent) => {
|
|
41
42
|
const start = state.pos;
|
|
42
43
|
if (state.src[start] !== '!' || state.src[start + 1] !== '[') return false;
|
|
43
|
-
const close = state.md.helpers.parseLinkLabel(state, start + 1, false);
|
|
44
44
|
const before = state.tokens.length;
|
|
45
45
|
if (!image(state, silent)) return false;
|
|
46
46
|
if (!silent && state.tokens.length > before) {
|
|
47
47
|
const token = state.tokens[state.tokens.length - 1], source = state.src.slice(start, state.pos);
|
|
48
48
|
if (token.type === 'image') {
|
|
49
|
+
const close = state.md.helpers.parseLinkLabel(state, start + 1, false);
|
|
49
50
|
let reference = null;
|
|
50
51
|
if (close >= 0 && state.src[close + 1] !== '(') {
|
|
51
52
|
const explicit = state.src[close + 1] === '[' ? state.src.slice(close + 2, state.pos - 1) : '';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
const NEWLINES = /\r\n?/g, NULLS = /\0/g;
|
|
4
|
+
|
|
5
|
+
function lineScanner(Base, Token) {
|
|
6
|
+
function StateBlock(src, md, env, tokens) {
|
|
7
|
+
this.bMarks = []; this.eMarks = []; this.tShift = []; this.sCount = []; this.bsCount = [];
|
|
8
|
+
this.blkIndent = 0; this.line = 0; this.lineMax = 0; this.tight = false; this.listIndent = -1;
|
|
9
|
+
this.parentType = 'root'; this.level = 0; this.Token = Token;
|
|
10
|
+
this.src = src; this.md = md; this.env = env; this.tokens = tokens;
|
|
11
|
+
const length = src.length;
|
|
12
|
+
for (let start = 0; start < length;) {
|
|
13
|
+
let at = start, indent = 0, offset = 0;
|
|
14
|
+
for (; at < length; at++) {
|
|
15
|
+
const code = src.charCodeAt(at);
|
|
16
|
+
if (code === 0x20) offset++;
|
|
17
|
+
else if (code === 0x09) offset += 4 - offset % 4;
|
|
18
|
+
else break;
|
|
19
|
+
indent++;
|
|
20
|
+
}
|
|
21
|
+
if (at === length) break;
|
|
22
|
+
let end = src.indexOf('\n', at);
|
|
23
|
+
if (end < 0) end = length;
|
|
24
|
+
this.bMarks.push(start); this.eMarks.push(end); this.tShift.push(indent); this.sCount.push(offset); this.bsCount.push(0);
|
|
25
|
+
start = end + 1;
|
|
26
|
+
}
|
|
27
|
+
this.bMarks.push(length); this.eMarks.push(length); this.tShift.push(0); this.sCount.push(0); this.bsCount.push(0);
|
|
28
|
+
this.lineMax = this.bMarks.length - 1;
|
|
29
|
+
}
|
|
30
|
+
StateBlock.prototype = Base.prototype;
|
|
31
|
+
return StateBlock;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function backticks(state, silent) {
|
|
35
|
+
const start = state.pos, src = state.src;
|
|
36
|
+
if (src.charCodeAt(start) !== 0x60) return false;
|
|
37
|
+
const max = state.posMax;
|
|
38
|
+
let pos = start + 1;
|
|
39
|
+
while (pos < max && src.charCodeAt(pos) === 0x60) pos++;
|
|
40
|
+
const marker = src.slice(start, pos), length = marker.length, runs = state.backticks;
|
|
41
|
+
const scanned = state.backticksScanned;
|
|
42
|
+
if (!scanned || pos < scanned || (runs[length] ?? -1) >= pos) {
|
|
43
|
+
let matchStart, matchEnd = pos;
|
|
44
|
+
while ((matchStart = src.indexOf('`', matchEnd)) !== -1 && matchStart < max) {
|
|
45
|
+
matchEnd = matchStart + 1;
|
|
46
|
+
while (src.charCodeAt(matchEnd) === 0x60) matchEnd++;
|
|
47
|
+
if (matchEnd > max) break;
|
|
48
|
+
const run = matchEnd - matchStart;
|
|
49
|
+
if (run === length) {
|
|
50
|
+
if (!silent) {
|
|
51
|
+
const token = state.push('code_inline', 'code', 0);
|
|
52
|
+
token.markup = marker;
|
|
53
|
+
let content = src.slice(pos, matchStart).replace(/\n/g, ' ');
|
|
54
|
+
if (content.startsWith(' ') && content.endsWith(' ') && /[^ ]/.test(content)) content = content.slice(1, -1);
|
|
55
|
+
token.content = content;
|
|
56
|
+
}
|
|
57
|
+
state.pos = matchEnd;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (!(runs[run] >= matchStart)) runs[run] = matchStart;
|
|
61
|
+
}
|
|
62
|
+
if (matchStart === -1 && (!scanned || pos < scanned)) state.backticksScanned = pos;
|
|
63
|
+
}
|
|
64
|
+
if (!silent) state.pending += marker;
|
|
65
|
+
state.pos = pos;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const CLEAN_WEB_LINK = /^https?:\/\/([a-z0-9-]{1,63}(?:\.[a-z0-9-]{1,63})*)(?:\/[A-Za-z0-9\-._~/]*)?$/;
|
|
70
|
+
function cleanWebLink(url) {
|
|
71
|
+
const match = CLEAN_WEB_LINK.exec(url);
|
|
72
|
+
return match !== null && match[1].length <= 255;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function tuneMarkdownEngine(md) {
|
|
76
|
+
if (md.__rapierEngineTuned) return md;
|
|
77
|
+
md.__rapierEngineTuned = true;
|
|
78
|
+
md.core.ruler.at('normalize', state => {
|
|
79
|
+
let src = state.src;
|
|
80
|
+
if (src.indexOf('\r') >= 0) src = src.replace(NEWLINES, '\n');
|
|
81
|
+
if (src.indexOf('\0') >= 0) src = src.replace(NULLS, '�');
|
|
82
|
+
state.src = src;
|
|
83
|
+
});
|
|
84
|
+
const BaseBlock = md.block.State;
|
|
85
|
+
md.block.State = lineScanner(BaseBlock, new BaseBlock('', md, {}, []).Token);
|
|
86
|
+
md.inline.ruler.at('backticks', backticks);
|
|
87
|
+
const normalizeLink = md.normalizeLink;
|
|
88
|
+
md.normalizeLink = url => cleanWebLink(url) ? url : normalizeLink.call(md, url);
|
|
89
|
+
return md;
|
|
90
|
+
}
|
package/dist/spec/md-marks.mjs
CHANGED
|
@@ -251,8 +251,9 @@ export function createTypography({normalizeFontFamily, fontFamilyCss, FONT_STACK
|
|
|
251
251
|
if (md.__rapierTypography) return md;
|
|
252
252
|
md.__rapierTypography = true;
|
|
253
253
|
md.core.ruler.after('linkify', 'rapier-typography', state => {
|
|
254
|
-
for (
|
|
255
|
-
|
|
254
|
+
for (let at = 0; at < state.tokens.length; at++) {
|
|
255
|
+
const block = state.tokens[at];
|
|
256
|
+
if (block.type !== 'inline' || !block.children || block.content.indexOf('<') < 0) continue;
|
|
256
257
|
const stack = []; let depth = 0, overflow = 0;
|
|
257
258
|
for (const token of block.children) {
|
|
258
259
|
if (token.type !== 'html_inline') { depth += token.nesting || 0; continue; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.21",
|
|
4
4
|
"description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"types": "./dist/kit/markdown-it.d.mts",
|
|
45
45
|
"default": "./dist/kit/markdown-it.mjs"
|
|
46
46
|
},
|
|
47
|
+
"./parsed": {
|
|
48
|
+
"types": "./dist/kit/parsed.d.mts",
|
|
49
|
+
"default": "./dist/kit/parsed.mjs"
|
|
50
|
+
},
|
|
47
51
|
"./layout": "./dist/kit/layout.mjs",
|
|
48
52
|
"./marks": "./dist/kit/marks.mjs",
|
|
49
53
|
"./model": "./dist/kit/model.mjs",
|