rapier-markdown-kit 1.2.6 → 1.2.8
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 +32 -30
- package/dist/agent/will.mjs +2 -1
- package/dist/images/playback.mjs +3 -2
- package/dist/kit/index.mjs +1 -1
- package/dist/kit/ledger/carried.d.mts +20 -0
- package/dist/kit/ledger.d.mts +88 -0
- package/dist/kit/ledger.mjs +18 -0
- package/dist/kit/markdown-it.d.mts +4 -0
- package/dist/kit/markdown-it.mjs +39 -0
- package/dist/kit/pdf-glyphs.mjs +59 -18
- package/dist/kit/pdf-source.d.mts +5 -0
- package/dist/kit/pdf-source.mjs +125 -0
- package/dist/kit/render-markdown.mjs +3 -2
- package/dist/kit/render-print.mjs +10 -5
- package/dist/kit/render-sanitize.mjs +28 -10
- package/dist/kit/render-styles.mjs +2 -2
- package/dist/kit/render.mjs +7 -5
- package/dist/kit/will.d.mts +42 -0
- package/dist/spec/document-settings.mjs +92 -6
- package/dist/spec/html-blocks.mjs +4 -2
- package/dist/spec/md-assets.mjs +27 -6
- package/dist/spec/md-layout.mjs +3 -3
- package/dist/spec/md-source.mjs +8 -4
- package/dist/spec/word-sidecar.mjs +7 -0
- package/package.json +21 -4
- package/style.css +10 -12
package/README.md
CHANGED
|
@@ -24,6 +24,28 @@ deliberately show a diff. The kit supplies the portable source and history those
|
|
|
24
24
|
npm install rapier-markdown-kit
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
Render a document with [markdown-it](https://github.com/markdown-it/markdown-it), then style it with
|
|
28
|
+
`rapier-markdown-kit/style.css` inside `<main class="md-render">`:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import MarkdownIt from 'markdown-it';
|
|
32
|
+
import {rapierMarkdown} from 'rapier-markdown-kit/markdown-it';
|
|
33
|
+
|
|
34
|
+
const html = new MarkdownIt({html: true}).use(rapierMarkdown).render('# Plan\n\nShips **Friday**.\n');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Keep a document's history with authorship, and merge two offline copies:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import {openLedger, replaceLedgerText, merge, authorship} from 'rapier-markdown-kit/rapier-ledger';
|
|
41
|
+
|
|
42
|
+
const base = openLedger('Draft\n');
|
|
43
|
+
const edited = replaceLedgerText(base, 'Draft, revised\n', {actor: {kind: 'human', id: 'ana'}});
|
|
44
|
+
console.log(merge(edited, base).text, authorship(edited).runs);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
TypeScript declarations ship for `markdown-it`, `will`, `rapier-ledger`, `ledger/carried` and `pdf`.
|
|
48
|
+
|
|
27
49
|
```js
|
|
28
50
|
import {parseLayout} from 'rapier-markdown-kit/layout'; // the md-layout comment a picture carries
|
|
29
51
|
import {typography, formatPageBreak} from 'rapier-markdown-kit/marks'; // inline typography and page breaks
|
|
@@ -46,6 +68,9 @@ as one HTML file on Android, Web and Windows.
|
|
|
46
68
|
|
|
47
69
|
## The shared renderer
|
|
48
70
|
|
|
71
|
+
To render Markdown in your own app, use `rapier-markdown-kit/markdown-it` (above). The render factories below are the
|
|
72
|
+
editor's own seam: each needs a `host` that supplies its DOM, Markdown, picture, style and font ports.
|
|
73
|
+
|
|
49
74
|
`rapier-markdown-kit/render` exports `createRenderer`, `createMarkdownRenderer`,
|
|
50
75
|
`createRenderStyles`, `createRenderSanitizer` and `createPrintRenderer`, plus the explicit font helpers
|
|
51
76
|
`exportFontCss`, `requireDocumentFonts`, `fontRequests`, `waitForFonts` and `waitForFontRequests`. These are the
|
|
@@ -139,6 +164,7 @@ family name share the host's font namespace. Print retains its existing factory
|
|
|
139
164
|
|
|
140
165
|
| Import | What it is |
|
|
141
166
|
| --- | --- |
|
|
167
|
+
| `rapier-markdown-kit/markdown-it` | `md.use(rapierMarkdown)`: the reference style's markup in markdown-it, in one line. |
|
|
142
168
|
| `rapier-markdown-kit/layout` | The `md-layout:v1` comment a picture carries: parse it, validate it, write it back. |
|
|
143
169
|
| `rapier-markdown-kit/marks` | Unified typography values, span writer and parser installer; preserved color/font comments, ink and page breaks. |
|
|
144
170
|
| `rapier-markdown-kit/model` | Occupancy profiles and the line planner: the wrapping itself. |
|
|
@@ -146,7 +172,8 @@ family name share the host's font namespace. Print retains its existing factory
|
|
|
146
172
|
| `rapier-markdown-kit/will` | The Will/1 grammar (a separate, optional standard): the markers around an agent's intent regions. |
|
|
147
173
|
| `rapier-markdown-kit/assets` | The picture appendix: the reference definitions a document's pictures live in. It never decodes a picture. |
|
|
148
174
|
| `rapier-markdown-kit/style.css` | The reference style: type, lists, tables, tasks, pictures and the document's other markup, scoped to `.md-render`. |
|
|
149
|
-
| `rapier-markdown-kit/rapier-ledger` | The MIT edit
|
|
175
|
+
| `rapier-markdown-kit/rapier-ledger` | The MIT edit ledger: `openLedger`, replay, authorship, selective Undo, offline `merge`, three-way `mergeText`, and the carried parts. |
|
|
176
|
+
| `rapier-markdown-kit/pdf` | `attachPdfSource`: attach the exact source to a PDF without rewriting its pages; `readPdfPageCount`. |
|
|
150
177
|
| `rapier-markdown-kit/ledger/carried` | Checked optional parts for pages and saved Markdown. |
|
|
151
178
|
| `rapier-markdown-kit` | Named exports from the layout, marks, model, typography, Will, assets and render modules, plus the `ledger` namespace. |
|
|
152
179
|
|
|
@@ -290,38 +317,13 @@ contract is [Reference style](https://github.com/jackskip22/rapier/blob/main/doc
|
|
|
290
317
|
<!-- reference-style-example -->
|
|
291
318
|
```js
|
|
292
319
|
import MarkdownIt from 'markdown-it';
|
|
293
|
-
import {
|
|
294
|
-
import {
|
|
295
|
-
import {
|
|
320
|
+
import {rapierMarkdown} from 'rapier-markdown-kit/markdown-it';
|
|
321
|
+
import {formatLayout} from 'rapier-markdown-kit/layout';
|
|
322
|
+
import {TEXT_COLOR_NAMES, typography, formatColorRun, formatPageBreak} from 'rapier-markdown-kit/marks';
|
|
296
323
|
import {readFile, writeFile} from 'node:fs/promises';
|
|
297
324
|
import {pathToFileURL} from 'node:url';
|
|
298
325
|
|
|
299
|
-
const md =
|
|
300
|
-
md.renderer.rules.table_open = (tokens, index, options, env, renderer) =>
|
|
301
|
-
'<div class="table-scroll-wrap">\n' + renderer.renderToken(tokens, index, options);
|
|
302
|
-
md.renderer.rules.table_close = (tokens, index, options, env, renderer) =>
|
|
303
|
-
renderer.renderToken(tokens, index, options) + '</div>\n';
|
|
304
|
-
md.core.ruler.after('inline', 'reference-layout', state => {
|
|
305
|
-
for (let i = 1; i < state.tokens.length; i++) {
|
|
306
|
-
const inline = state.tokens[i], owner = state.tokens[i - 1];
|
|
307
|
-
if (inline.type !== 'inline' || !['paragraph_open', 'heading_open'].includes(owner.type)) continue;
|
|
308
|
-
const children = inline.children, marker = children.at(-1);
|
|
309
|
-
const layout = marker?.type === 'html_inline' ? parseLayout(marker.content) : null;
|
|
310
|
-
if (!layout) continue;
|
|
311
|
-
children.pop();
|
|
312
|
-
if (layout.align) owner.attrSet('data-md-align', layout.align);
|
|
313
|
-
const visible = children.filter(token => token.type !== 'text' || token.content.trim());
|
|
314
|
-
if (visible.length === 1 && visible[0].type === 'image' && layout.width) {
|
|
315
|
-
visible[0].attrSet('style', imageStyle(layout));
|
|
316
|
-
visible[0].attrSet('data-md-image-width', String(layout.width));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
const htmlBlock = md.renderer.rules.html_block;
|
|
321
|
-
md.renderer.rules.html_block = (tokens, index, options, env, renderer) =>
|
|
322
|
-
isPageBreakBlock(tokens[index].content)
|
|
323
|
-
? '<div class="rapier-page-break" data-md-break="page" role="separator" aria-label="Page break"></div>\n'
|
|
324
|
-
: htmlBlock(tokens, index, options, env, renderer);
|
|
326
|
+
const md = new MarkdownIt({html: true, linkify: true}).use(rapierMarkdown);
|
|
325
327
|
|
|
326
328
|
export function renderReferenceDocument(source) {
|
|
327
329
|
return md.render(source);
|
package/dist/agent/will.mjs
CHANGED
|
@@ -10,7 +10,8 @@ const _RAPIER_WILL_UNSPACED_OPENER = '<!--will/';
|
|
|
10
10
|
const _RAPIER_WILL_UNSPACED_CLOSER = '<!--/will';
|
|
11
11
|
|
|
12
12
|
function hasWillMarkers(text) {
|
|
13
|
-
|
|
13
|
+
// Lines end at \r and \n only, as the parser splits them; /m would also start a line after U+2028 and U+2029.
|
|
14
|
+
return /(?:^|[\r\n])<!-- ?(?:will\/|\/will)/.test(String(text == null ? '' : text));
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function _rapierWillMarkerOf(content) {
|
package/dist/images/playback.mjs
CHANGED
|
@@ -73,8 +73,9 @@ export function createMediaPlayback(win, inspectGIF) {
|
|
|
73
73
|
if (canvas.height !== h) canvas.height = h;
|
|
74
74
|
const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, w, h); ctx.drawImage(frame, 0, 0);
|
|
75
75
|
if (!state.first && next === 0) state.first = canvas.toDataURL('image/png');
|
|
76
|
-
// A frame of 10 ms or less shows for 100 ms, as the browser draws an <img> (images/gif.mjs counts the same).
|
|
77
|
-
|
|
76
|
+
// A GIF frame of 10 ms or less shows for 100 ms, as the browser draws an <img> (images/gif.mjs counts the same).
|
|
77
|
+
// A JPEG XL frame keeps its own duration: its ticks are exact, and 100 frames a second is a real animation.
|
|
78
|
+
const delay = type !== 'image/gif' ? Math.max(8, frame.duration / 1000) : frame.duration > 10000 ? Math.max(20, frame.duration / 1000) : 100;
|
|
78
79
|
duration += delay; if (duration > 120000) throw new Error('Animation exceeds the duration budget');
|
|
79
80
|
state.status = 'ready'; state.frame = next; changed(state);
|
|
80
81
|
if (single) {paused = true; closeDecoder(); return;}
|
package/dist/kit/index.mjs
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The ledger and authorship carried inside the document: an optional HTML comment at the end of the file.
|
|
3
|
+
import type {LedgerEnvelope, AuthorshipEnvelope, Metadata} from '../ledger.mjs';
|
|
4
|
+
export interface DocumentParts { ledger?: LedgerEnvelope | null; authorship?: AuthorshipEnvelope | null; metadata?: Metadata; }
|
|
5
|
+
export interface ReadParts { text: string; ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null; }
|
|
6
|
+
/** The text with its parts appended as one EOF comment; the text alone when there are none. */
|
|
7
|
+
export function writeDocument(text: string, parts?: DocumentParts): string;
|
|
8
|
+
/** Splits a saved file into its exact text and verified parts; throws on a damaged or mismatched comment. */
|
|
9
|
+
export function readDocument(source: string): ReadParts;
|
|
10
|
+
export function writeParts(text: string, parts?: DocumentParts): string;
|
|
11
|
+
export function readParts(text: string, elements: unknown): {ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
|
|
12
|
+
export function validateParts(text: string, parts?: DocumentParts): {ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
|
|
13
|
+
export function takeParts(html: string, text?: string): {html: string; ledger: LedgerEnvelope | null; authorship: AuthorshipEnvelope | null};
|
|
14
|
+
export function encodeCarried(text: string): string;
|
|
15
|
+
export function decodeCarried(text: string): string;
|
|
16
|
+
export interface ComparisonBase { text: string; sha256: string; revision: string; name: string; by: string; at: string; }
|
|
17
|
+
export function readBase(value: Partial<ComparisonBase>): ComparisonBase;
|
|
18
|
+
export function writeBase(text: string, value: Partial<ComparisonBase>): string;
|
|
19
|
+
export function readBaseElements(elements: unknown): ComparisonBase | null;
|
|
20
|
+
export function takeBase(html: string): {html: string; base: ComparisonBase | null};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// rapier-markdown-kit/rapier-ledger: a document's edit history with authorship, Undo of any act and offline merge.
|
|
3
|
+
// Offsets count UTF-16 code units; splices run in their recorded order.
|
|
4
|
+
export type ActorKind = 'human' | 'agent' | 'system';
|
|
5
|
+
export interface Actor { kind: ActorKind; id: string; }
|
|
6
|
+
export type DocKind = 'markdown' | 'text' | 'code';
|
|
7
|
+
export interface Metadata { filename: string; docKind: DocKind; }
|
|
8
|
+
export interface Splice { pos: number; removed: string; inserted: string; }
|
|
9
|
+
export interface Transaction {
|
|
10
|
+
id: string; documentAuthority: string; baseRevision: number; revision: number; actor: Actor;
|
|
11
|
+
transport: string; operation: string; requestId: string | null; sourceTransactionId: string | null;
|
|
12
|
+
affectedBlockIds: string[]; parent: string | null; reverts: string | null; reapplies: string | null;
|
|
13
|
+
createdAt: string | null; turnId?: string; sourceTransactionIds?: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface LedgerRecord { transaction: Transaction; splices: Splice[]; beforeHash: string; afterHash: string; metadata?: Record<string, unknown>; authored?: unknown; }
|
|
16
|
+
/** rapier-ledger/2: plain JSON. */
|
|
17
|
+
export interface LedgerEnvelope {
|
|
18
|
+
format: 'rapier-ledger/2';
|
|
19
|
+
documentAuthority: string;
|
|
20
|
+
start: {text: string; revision: number; root: string; sha256: string; metadata: Metadata};
|
|
21
|
+
records: LedgerRecord[];
|
|
22
|
+
head: {revision: number; root: string; sha256: string; metadata: Metadata};
|
|
23
|
+
complete: boolean;
|
|
24
|
+
sha256: string;
|
|
25
|
+
}
|
|
26
|
+
/** Runs of [start, end, actor kind, actor id, createdAt] over the current text. */
|
|
27
|
+
export interface AuthorshipEnvelope { format: 'rapier-authorship/1'; sha256: string; runs: [number, number, ActorKind, string, string | number | null][]; }
|
|
28
|
+
export interface ReadLedger { ledger: LedgerEnvelope; text: string; metadata: Metadata; root: string; revision: number; complete: boolean; }
|
|
29
|
+
export interface LedgerInput { text: string; metadata: Metadata; records: LedgerRecord[]; documentAuthority?: string; revision?: number; root?: string; complete?: boolean; }
|
|
30
|
+
|
|
31
|
+
/** A new ledger with no history. Give every copy that may later merge the same documentAuthority. */
|
|
32
|
+
export function openLedger(text: string, options?: {filename?: string; docKind?: DocKind; documentAuthority?: string}): LedgerEnvelope;
|
|
33
|
+
export function exportLedger(input: LedgerInput): LedgerEnvelope;
|
|
34
|
+
/** Verifies the envelope (and, when given, that it describes this text and metadata); throws when it does not. */
|
|
35
|
+
export function readLedger(input: LedgerEnvelope, expectedText?: string, expectedMetadata?: Metadata): ReadLedger;
|
|
36
|
+
/** Records one replacement of the whole text as an act by `actor`. */
|
|
37
|
+
export function replaceLedgerText(input: LedgerEnvelope, text: string, options?: {actor?: Actor; operation?: string; at?: string}): LedgerEnvelope;
|
|
38
|
+
export function ledgerWithStart(input: LedgerEnvelope, text: string): LedgerEnvelope;
|
|
39
|
+
export function historyEnvelope(input: LedgerEnvelope, segmentIdentity?: unknown[], expectedText?: string, expectedMetadata?: Metadata): Record<string, unknown>;
|
|
40
|
+
/** The actor id an agent writes under, for a door and an agent name. */
|
|
41
|
+
export function agentActorId(door: string, agent: string): string;
|
|
42
|
+
export function textRoot(text: string): string;
|
|
43
|
+
export function rootAfter(root: string, splice: Splice): string;
|
|
44
|
+
export function sha256State(): {update(text: string): unknown; digest(): string};
|
|
45
|
+
|
|
46
|
+
export function authorship(input: LedgerEnvelope): AuthorshipEnvelope;
|
|
47
|
+
export function readAuthorship(input: AuthorshipEnvelope, text: string): AuthorshipEnvelope;
|
|
48
|
+
export function ledgerFromAuthorship(input: AuthorshipEnvelope, text: string, documentAuthority: string, metadata: Metadata): LedgerEnvelope;
|
|
49
|
+
|
|
50
|
+
export interface MergeConflict { kind: string; code?: string; actId?: string; [key: string]: unknown; }
|
|
51
|
+
export interface MergeResult {
|
|
52
|
+
text: string; metadata: Metadata; ledger: LedgerEnvelope;
|
|
53
|
+
/** False when a person must review: conflicts, or Will keep regions that differ. */
|
|
54
|
+
clean: boolean;
|
|
55
|
+
conflicts: MergeConflict[];
|
|
56
|
+
review: {required: boolean; base: string; proposed: string};
|
|
57
|
+
}
|
|
58
|
+
/** Merges two copies of one document (same documentAuthority) without a server. */
|
|
59
|
+
export function merge(first: LedgerEnvelope, second: LedgerEnvelope): MergeResult;
|
|
60
|
+
export function transportInterval(start: number, end: number, splices: Splice[]): {start: number; end: number} | null;
|
|
61
|
+
export function transportTouchedInterval(start: number, end: number, splices: Splice[]): {start: number; end: number};
|
|
62
|
+
|
|
63
|
+
export interface HistoryInput { source: string; metadata: Metadata; records: LedgerRecord[]; revision: number; earliestRevision: number; complete: boolean; }
|
|
64
|
+
export interface HistoryFailure { ok?: false; code: string; [key: string]: unknown; }
|
|
65
|
+
/** Reverses chosen acts while later work stays. */
|
|
66
|
+
export function selectiveUndo(input: HistoryInput, actIds: string[], options?: {actor?: Actor; splitSplice?: (...args: unknown[]) => unknown}): Record<string, unknown> | HistoryFailure;
|
|
67
|
+
export function replayHistory(input: HistoryInput): unknown;
|
|
68
|
+
export function sourceBefore(input: HistoryInput, actId: string): unknown;
|
|
69
|
+
export function historyProjection(input: HistoryInput, range?: {from?: number; to?: number; actId?: string}): unknown;
|
|
70
|
+
export function historyProjections(input: HistoryInput, ranges: unknown[]): unknown;
|
|
71
|
+
export function historyPlaces(input: HistoryInput, blocks: unknown[]): unknown;
|
|
72
|
+
export function groupHistoryActs(acts: unknown[]): unknown;
|
|
73
|
+
export function pictureHistorySplices(input: unknown, target: unknown, redo: boolean): Splice[] | null;
|
|
74
|
+
export function transposeSource(base: string, splices: Splice[], client: unknown, log: unknown): unknown;
|
|
75
|
+
export function authoredBasis(...args: unknown[]): unknown;
|
|
76
|
+
export function authoredPlacement(...args: unknown[]): unknown;
|
|
77
|
+
export function authoredHistory(base: string, records: LedgerRecord[], selected?: unknown): unknown;
|
|
78
|
+
export function transposeAuthored(base: string, records: LedgerRecord[], incoming: unknown, submitted: unknown): unknown;
|
|
79
|
+
|
|
80
|
+
export {writeDocument, readDocument, writeParts, readParts} from './ledger/carried.mjs';
|
|
81
|
+
|
|
82
|
+
export interface TextConflict { kind: 'text'; id: string; start: number; end: number; block: string; variants: string[]; }
|
|
83
|
+
export interface TextMerge { text: string; clean: boolean; conflicts: (TextConflict | {kind: string; advisory?: boolean})[]; }
|
|
84
|
+
/** Exact three-way text merge; a region both sides changed becomes a marked choice in the text. */
|
|
85
|
+
export function mergeText(base: string | null, ours: string, theirs: string, options?: Record<string, unknown>): TextMerge;
|
|
86
|
+
export function inspectTextConflicts(text: string, options?: {nested?: boolean}): TextConflict[];
|
|
87
|
+
export function resolveTextConflict(text: string, conflict: TextConflict | string, device: unknown): string;
|
|
88
|
+
export function mapTextConflictVariants(text: string, mapper: (variant: string) => string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The public edit ledger (rapier-markdown-kit/rapier-ledger): the history, its carried parts and three-way text merge.
|
|
3
|
+
// The editor binds kit/rapier-ledger.mjs, which also carries its private record helpers.
|
|
4
|
+
import {exportLedger} from './ledger/format.mjs';
|
|
5
|
+
export {exportLedger, replaceLedgerText, readLedger, ledgerWithStart, textRoot, rootAfter, historyEnvelope, agentActorId} from './ledger/format.mjs';
|
|
6
|
+
export {authorship, readAuthorship, ledgerFromAuthorship} from './ledger/authorship.mjs';
|
|
7
|
+
export {merge, transportInterval, transportTouchedInterval} from './ledger/merge.mjs';
|
|
8
|
+
export {replayHistory, sourceBefore, historyProjection, historyProjections, historyPlaces, selectiveUndo, groupHistoryActs, pictureHistorySplices} from './ledger/history.mjs';
|
|
9
|
+
export {sha256State} from './ledger/hash.mjs';
|
|
10
|
+
export {transposeSource, authoredBasis, authoredPlacement, authoredHistory, transposeAuthored} from './ledger/transport.mjs';
|
|
11
|
+
export {writeDocument, readDocument, writeParts, readParts} from './ledger/carried.mjs';
|
|
12
|
+
export {mergeText, resolveTextConflict, inspectTextConflicts, mapTextConflictVariants} from './ledger/text-merge.mjs';
|
|
13
|
+
|
|
14
|
+
// A new ledger for one text, with no history yet. documentAuthority names the document across its copies:
|
|
15
|
+
// pass the same one to every copy that may later merge.
|
|
16
|
+
export function openLedger(text, {filename = 'document.md', docKind = 'markdown', documentAuthority = globalThis.crypto.randomUUID()} = {}) {
|
|
17
|
+
return exportLedger({text, metadata: {filename, docKind}, records: [], documentAuthority, complete: true});
|
|
18
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// md.use(rapierMarkdown): Self-contained Markdown in markdown-it, with the reference style's markup.
|
|
3
|
+
interface MarkdownItLike { use(plugin: (md: any, ...params: any[]) => unknown, ...params: any[]): any; renderer: {rules: Record<string, unknown>}; core: {ruler: {after(after: string, name: string, rule: (state: any) => unknown): void}}; }
|
|
4
|
+
export function rapierMarkdown<T extends MarkdownItLike>(md: T): T;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// rapier-markdown-kit/markdown-it: md.use(rapierMarkdown) renders Self-contained Markdown with the reference style's markup
|
|
3
|
+
// (wrap the output in <main class="md-render"> with rapier-markdown-kit/style.css). Create markdown-it with {html: true}.
|
|
4
|
+
import {parseLayout, imageStyle} from './layout.mjs';
|
|
5
|
+
import {typography, isPageBreakBlock} from './marks.mjs';
|
|
6
|
+
import {installMarkdownImages} from './assets.mjs';
|
|
7
|
+
|
|
8
|
+
const PAGE_BREAK = '<div class="rapier-page-break" data-md-break="page" role="separator" aria-label="Page break"></div>\n';
|
|
9
|
+
|
|
10
|
+
export function rapierMarkdown(md) {
|
|
11
|
+
if (md.__rapierMarkdownInstalled) return md;
|
|
12
|
+
md.__rapierMarkdownInstalled = true;
|
|
13
|
+
typography.install(installMarkdownImages(md));
|
|
14
|
+
md.renderer.rules.table_open = (tokens, index, options, env, renderer) =>
|
|
15
|
+
'<div class="table-scroll-wrap">\n' + renderer.renderToken(tokens, index, options);
|
|
16
|
+
md.renderer.rules.table_close = (tokens, index, options, env, renderer) =>
|
|
17
|
+
renderer.renderToken(tokens, index, options) + '</div>\n';
|
|
18
|
+
// A paragraph's or heading's closing layout comment: its alignment, and a lone picture's width.
|
|
19
|
+
md.core.ruler.after('inline', 'rapier-layout', state => {
|
|
20
|
+
for (let i = 1; i < state.tokens.length; i++) {
|
|
21
|
+
const inline = state.tokens[i], owner = state.tokens[i - 1];
|
|
22
|
+
if (inline.type !== 'inline' || !['paragraph_open', 'heading_open'].includes(owner.type)) continue;
|
|
23
|
+
const children = inline.children, marker = children.at(-1);
|
|
24
|
+
const layout = marker?.type === 'html_inline' ? parseLayout(marker.content) : null;
|
|
25
|
+
if (!layout) continue;
|
|
26
|
+
children.pop();
|
|
27
|
+
if (layout.align) owner.attrSet('data-md-align', layout.align);
|
|
28
|
+
const visible = children.filter(token => token.type !== 'text' || token.content.trim());
|
|
29
|
+
if (visible.length === 1 && visible[0].type === 'image' && layout.width) {
|
|
30
|
+
visible[0].attrSet('style', imageStyle(layout));
|
|
31
|
+
visible[0].attrSet('data-md-image-width', String(layout.width));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const htmlBlock = md.renderer.rules.html_block || ((tokens, index) => tokens[index].content);
|
|
36
|
+
md.renderer.rules.html_block = (tokens, index, options, env, renderer) =>
|
|
37
|
+
isPageBreakBlock(tokens[index].content) ? PAGE_BREAK : htmlBlock(tokens, index, options, env, renderer);
|
|
38
|
+
return md;
|
|
39
|
+
}
|
package/dist/kit/pdf-glyphs.mjs
CHANGED
|
@@ -77,6 +77,14 @@ function shape(value, font, preserve, ligatures) {
|
|
|
77
77
|
const result = [];
|
|
78
78
|
for (let at = 0; at < value.length;) {
|
|
79
79
|
const code = value.codePointAt(at), character = String.fromCodePoint(code), start = at;
|
|
80
|
+
if (code === 0x00ad) {
|
|
81
|
+
at += character.length;
|
|
82
|
+
const own = font.glyph(code), hyphen = font.glyph(0x2d), glyph = own || hyphen;
|
|
83
|
+
// A discretionary hyphen with no glyph stays in the page paint. Do not skip the lookup.
|
|
84
|
+
if (!glyph || glyph >= font.glyphCount) return null;
|
|
85
|
+
result.push({start, end: at, text: own ? character : '-', glyph, soft: true});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
80
88
|
if (/[\p{Mark}\p{Format}]/u.test(character) || (/[\p{Letter}]/u.test(character) && !/\p{Script=Latin}/u.test(character)) || code === 0xfb01 || code === 0xfb02) return null;
|
|
81
89
|
at += character.length;
|
|
82
90
|
if (!preserve && /[ \t\r\n\f]/.test(character)) {while (at < value.length && /[ \t\r\n\f]/.test(value[at])) at++;}
|
|
@@ -97,7 +105,13 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
|
|
|
97
105
|
const document = main.ownerDocument, window = document.defaultView, origin = main.getBoundingClientRect();
|
|
98
106
|
const nodes = new Map(), hidden = [], canvas = document.createElement('canvas'); canvas.width = canvas.height = 1;
|
|
99
107
|
const context = canvas.getContext('2d', {willReadFrequently: true});
|
|
100
|
-
const restore = () => {
|
|
108
|
+
const restore = () => {
|
|
109
|
+
for (const row of hidden) {
|
|
110
|
+
if (row.wrap) {row.wrap.replaceWith(...row.wrap.childNodes); continue;}
|
|
111
|
+
if (row.value) row.node.style.setProperty('-webkit-text-fill-color', row.value, row.priority); else row.node.style.removeProperty('-webkit-text-fill-color');
|
|
112
|
+
}
|
|
113
|
+
canvas.width = canvas.height = 0;
|
|
114
|
+
};
|
|
101
115
|
if (!context) return {nodes, restore};
|
|
102
116
|
const colorCache = new Map(), styles = new Map(), styleOf = node => {if (!styles.has(node)) styles.set(node, window.getComputedStyle(node)); return styles.get(node);};
|
|
103
117
|
const color = value => {if (!colorCache.has(value)) {context.clearRect(0, 0, 1, 1); context.fillStyle = value; context.fillRect(0, 0, 1, 1); colorCache.set(value, Array.from(context.getImageData(0, 0, 1, 1).data, item => item / 255));} return colorCache.get(value);};
|
|
@@ -118,9 +132,10 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
|
|
|
118
132
|
if (++visited % 128 === 0) {await new Promise(resolve => setTimeout(resolve, 0)); check();}
|
|
119
133
|
// Leaving the existing element in place preserves selectors, counters, line breaking and inline marks.
|
|
120
134
|
// Keep preformatted blocks, their inline struts and syntax paint together.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
135
|
+
// A paragraph's own words beside a mark, a link or code are drawn too: each text node is placed and hidden on its own.
|
|
136
|
+
if (element.namespaceURI !== 'http://www.w3.org/1999/xhtml' || element.closest('svg,pre,.rapier-will-carrier')) continue;
|
|
137
|
+
const textNodes = Array.from(element.childNodes).filter(node => node.nodeType === 3 && node.data.trim());
|
|
138
|
+
if (!textNodes.length || !safe(element)) continue;
|
|
124
139
|
const language = (element.closest('[lang]')?.getAttribute('lang') || document.documentElement.lang || '').toLowerCase();
|
|
125
140
|
if (language && !/^en(?:-|$)/.test(language)) continue;
|
|
126
141
|
const style = styleOf(element), family = familyName(style.fontFamily.split(',')[0]), weight = Number(style.fontWeight), size = Number.parseFloat(style.fontSize);
|
|
@@ -134,25 +149,51 @@ export async function collectPdfGlyphs(main, {width, check, faces = []}) {
|
|
|
134
149
|
if (generated || altered || !Array.from(document.fonts).some(face => familyName(face.family) === family && face.status === 'loaded')) continue;
|
|
135
150
|
const asset = faces.find(face => face.family === family && face.weight === weight);
|
|
136
151
|
if (!asset) continue;
|
|
137
|
-
const font = readFont(asset, fontCache), preserve = /pre|break-spaces/.test(style.whiteSpace)
|
|
138
|
-
const shaped = shape(node.data, font, preserve, style.fontVariantLigatures === 'normal' && !(Number.parseFloat(style.letterSpacing) || 0));
|
|
139
|
-
if (!shaped) continue;
|
|
152
|
+
const font = readFont(asset, fontCache), preserve = /pre|break-spaces/.test(style.whiteSpace);
|
|
140
153
|
context.font = [style.fontStyle, style.fontWeight, style.fontSize, style.fontFamily].join(' ');
|
|
141
154
|
const metrics = context.measureText('Hg'), ascent = metrics.fontBoundingBoxAscent;
|
|
142
155
|
if (!Number.isFinite(ascent)) continue;
|
|
143
156
|
const rgba = color(style.webkitTextFillColor || style.color), range = document.createRange();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
157
|
+
for (const node of textNodes) {
|
|
158
|
+
const shaped = shape(node.data, font, preserve, style.fontVariantLigatures === 'normal' && !(Number.parseFloat(style.letterSpacing) || 0));
|
|
159
|
+
if (!shaped) continue;
|
|
160
|
+
let valid = true;
|
|
161
|
+
const placed = [];
|
|
162
|
+
for (const item of shaped) {
|
|
163
|
+
range.setStart(node, item.start); range.setEnd(node, item.end);
|
|
164
|
+
const ink = Array.from(range.getClientRects()).filter(rect => rect.height > 0);
|
|
165
|
+
// A break after a soft hyphen can leave the next letter with a phantom box on the
|
|
166
|
+
// hyphen's line. Keep the line that actually holds the ink.
|
|
167
|
+
const lines = [];
|
|
168
|
+
for (const rect of ink) {
|
|
169
|
+
const line = lines.find(row => Math.abs(row[0].top - rect.top) <= .25);
|
|
170
|
+
if (line) line.push(rect); else lines.push([rect]);
|
|
171
|
+
}
|
|
172
|
+
lines.sort((a, b) => b.reduce((sum, rect) => sum + rect.width, 0) - a.reduce((sum, rect) => sum + rect.width, 0));
|
|
173
|
+
const previous = shaped[shaped.indexOf(item) - 1];
|
|
174
|
+
if (lines.length > 1 && !item.soft && !previous?.soft) {valid = false; break;}
|
|
175
|
+
const rects = lines[0] || [];
|
|
176
|
+
const first = (item.soft ? rects.find(rect => rect.width > 0) : null) || rects[0];
|
|
177
|
+
// An unbroken soft hyphen has no ink. Leave it out of the vector run.
|
|
178
|
+
if (item.soft && !(first?.width > 0)) continue;
|
|
179
|
+
if (!first) {valid = false; break;}
|
|
180
|
+
const page = Math.max(0, Math.floor((first.left - origin.left + .01) / width));
|
|
181
|
+
Object.assign(item, {font, size, rgba, x: first.left - origin.left - page * width, baseline: first.top - origin.top + ascent});
|
|
182
|
+
placed.push(item);
|
|
183
|
+
}
|
|
184
|
+
if (!valid || !placed.length) continue;
|
|
185
|
+
nodes.set(node, placed);
|
|
186
|
+
if (element.localName === 'li' || element.children.length || textNodes.length > 1) {
|
|
187
|
+
// A list item's marker and a paragraph's marks are not this node's words: hide only the words.
|
|
188
|
+
const wrap = document.createElement('span');
|
|
189
|
+
node.replaceWith(wrap); wrap.append(node);
|
|
190
|
+
wrap.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
|
|
191
|
+
hidden.push({wrap});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
hidden.push({node: element, value: element.style.getPropertyValue('-webkit-text-fill-color'), priority: element.style.getPropertyPriority('-webkit-text-fill-color')});
|
|
195
|
+
element.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
|
|
151
196
|
}
|
|
152
|
-
if (!valid) continue;
|
|
153
|
-
nodes.set(node, shaped);
|
|
154
|
-
hidden.push({node: element, value: element.style.getPropertyValue('-webkit-text-fill-color'), priority: element.style.getPropertyPriority('-webkit-text-fill-color')});
|
|
155
|
-
element.style.setProperty('-webkit-text-fill-color', 'transparent', 'important');
|
|
156
197
|
}
|
|
157
198
|
check(); return {nodes, restore};
|
|
158
199
|
} catch (error) {restore(); throw error;}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Attach the exact source to a PDF Rapier wrote, without rewriting its pages (rapier-markdown-kit/pdf).
|
|
3
|
+
export function readPdfPageCount(input: Uint8Array | ArrayBuffer): number;
|
|
4
|
+
/** The PDF with `source` attached as an associated file; throws `pdf_source_invalid` for a PDF it cannot extend. */
|
|
5
|
+
export function attachPdfSource(input: Uint8Array | ArrayBuffer, options: {source: string; filename: string; maxBytes?: number; check?: () => void}): Promise<Uint8Array>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Add an associated source file to Rapier's fresh PDF without rewriting its pages.
|
|
3
|
+
function pdfStructure(input) {
|
|
4
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
5
|
+
const fail = () => { throw Object.assign(new Error('The PDF source could not be attached.'), {code: 'pdf_source_invalid'}); };
|
|
6
|
+
let text = '';
|
|
7
|
+
for (let at = 0; at < bytes.length; at += 32768) text += String.fromCharCode(...bytes.subarray(at, at + 32768));
|
|
8
|
+
if (!text.startsWith('%PDF-')) fail();
|
|
9
|
+
const end = /startxref\s+(\d+)\s+%%EOF\s*$/.exec(text), xref = Number(end?.[1]);
|
|
10
|
+
if (!end || !Number.isSafeInteger(xref) || text.slice(xref, xref + 4) !== 'xref') fail();
|
|
11
|
+
const offsets = new Map();
|
|
12
|
+
let at = xref + 4;
|
|
13
|
+
const space = () => { while (at < text.length && /[\s\0]/.test(text[at])) at++; };
|
|
14
|
+
for (;;) {
|
|
15
|
+
space(); if (text.startsWith('trailer', at)) { at += 7; break; }
|
|
16
|
+
const section = /^(\d+)\s+(\d+)\s+/.exec(text.slice(at)); if (!section) fail();
|
|
17
|
+
at += section[0].length;
|
|
18
|
+
const from = Number(section[1]), count = Number(section[2]);
|
|
19
|
+
if (!Number.isSafeInteger(from + count) || count > bytes.length / 18) fail();
|
|
20
|
+
for (let i = 0; i < count; i++) {
|
|
21
|
+
const entry = /^(\d{10})\s+(\d{5})\s+([nf])\s+/.exec(text.slice(at)); if (!entry) fail();
|
|
22
|
+
at += entry[0].length;
|
|
23
|
+
if (entry[3] === 'n') offsets.set(from + i, {at: Number(entry[1]), generation: Number(entry[2])});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const skip = pos => { for (;;) { while (pos < text.length && /[\s\0]/.test(text[pos])) pos++; if (text[pos] !== '%') return pos; while (pos < text.length && !/[\r\n]/.test(text[pos])) pos++; } };
|
|
27
|
+
const tokenEnd = pos => { while (pos < text.length && !/[\s\0()[\]<>/%]/.test(text[pos])) pos++; return pos; };
|
|
28
|
+
const valueEnd = (pos, depth = 0) => {
|
|
29
|
+
if (depth > 32) fail(); pos = skip(pos);
|
|
30
|
+
if (text.startsWith('<<', pos)) {
|
|
31
|
+
pos += 2;
|
|
32
|
+
for (;;) { pos = skip(pos); if (text.startsWith('>>', pos)) return pos + 2; if (text[pos] !== '/') fail(); pos = valueEnd(tokenEnd(pos + 1), depth + 1); }
|
|
33
|
+
}
|
|
34
|
+
if (text[pos] === '[') { pos++; for (;;) { pos = skip(pos); if (text[pos] === ']') return pos + 1; pos = valueEnd(pos, depth + 1); } }
|
|
35
|
+
if (text[pos] === '(') {
|
|
36
|
+
let nesting = 1; pos++;
|
|
37
|
+
while (pos < text.length && nesting) { const c = text[pos++]; if (c === '\\') pos++; else if (c === '(') nesting++; else if (c === ')') nesting--; }
|
|
38
|
+
if (nesting) fail(); return pos;
|
|
39
|
+
}
|
|
40
|
+
if (text[pos] === '<') { const close = text.indexOf('>', pos + 1); if (close < 0) fail(); return close + 1; }
|
|
41
|
+
if (text[pos] === '/') return tokenEnd(pos + 1);
|
|
42
|
+
const first = tokenEnd(pos); if (first === pos) fail();
|
|
43
|
+
const reference = /^\d+\s+\d+\s+R\b/.exec(text.slice(pos)); return reference ? pos + reference[0].length : first;
|
|
44
|
+
};
|
|
45
|
+
const dictionary = pos => {
|
|
46
|
+
pos = skip(pos); if (!text.startsWith('<<', pos)) fail();
|
|
47
|
+
const start = pos, fields = new Map(); pos += 2;
|
|
48
|
+
for (;;) {
|
|
49
|
+
pos = skip(pos); if (text.startsWith('>>', pos)) return {start, end: pos + 2, fields};
|
|
50
|
+
if (text[pos] !== '/') fail();
|
|
51
|
+
const keyStart = pos, keyEnd = tokenEnd(pos + 1), name = text.slice(pos + 1, keyEnd);
|
|
52
|
+
if (fields.has(name)) fail();
|
|
53
|
+
const from = skip(keyEnd), end = valueEnd(from);
|
|
54
|
+
fields.set(name, {start: keyStart, from, end, value: text.slice(from, end)}); pos = end;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const trailer = dictionary(at), rootRef = /^(\d+)\s+(\d+)\s+R$/.exec(trailer.fields.get('Root')?.value || '');
|
|
58
|
+
const size = Number(trailer.fields.get('Size')?.value), rootId = Number(rootRef?.[1]), generation = Number(rootRef?.[2]);
|
|
59
|
+
let highest = 0; for (const id of offsets.keys()) highest = Math.max(highest, id);
|
|
60
|
+
if (!rootRef || !Number.isSafeInteger(size) || size <= highest || trailer.fields.has('Encrypt') || trailer.fields.has('Prev')) fail();
|
|
61
|
+
const readDictionary = (id, gen) => {
|
|
62
|
+
const place = offsets.get(id); if (!place || place.generation !== gen) fail();
|
|
63
|
+
const header = new RegExp('^' + id + '\\s+' + gen + '\\s+obj\\b').exec(text.slice(place.at)); if (!header) fail();
|
|
64
|
+
return dictionary(place.at + header[0].length);
|
|
65
|
+
};
|
|
66
|
+
const root = readDictionary(rootId, generation);
|
|
67
|
+
return {bytes, text, offsets, root, rootId, generation, trailer, size, xref, readDictionary, dictionary, fail};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function readPdfPageCount(input) {
|
|
71
|
+
const {root, readDictionary, fail} = pdfStructure(input);
|
|
72
|
+
const reference = /^(\d+)\s+(\d+)\s+R$/.exec(root.fields.get('Pages')?.value || '');
|
|
73
|
+
if (!reference) fail();
|
|
74
|
+
const pages = readDictionary(Number(reference[1]), Number(reference[2])), count = Number(pages.fields.get('Count')?.value);
|
|
75
|
+
if (pages.fields.get('Type')?.value !== '/Pages' || !Number.isSafeInteger(count) || count < 1) fail();
|
|
76
|
+
return count;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function attachPdfSource(input, {source, filename, maxBytes = Infinity, check = () => {}}) {
|
|
80
|
+
const {bytes, text, offsets, root, rootId, generation, trailer, size, readDictionary, dictionary, fail} = pdfStructure(input);
|
|
81
|
+
if (typeof source !== 'string' || typeof filename !== 'string' || !filename) fail();
|
|
82
|
+
const ascii = value => Uint8Array.from(value, character => character.charCodeAt(0));
|
|
83
|
+
const encode = new TextEncoder(), hex = value => Array.from({length: value.length}, (_, i) => value.charCodeAt(i).toString(16).padStart(4, '0')).join('');
|
|
84
|
+
if (root.fields.get('Type')?.value !== '/Catalog' || root.fields.has('AF')) fail();
|
|
85
|
+
let names = '<<', remove = root.fields.get('Names');
|
|
86
|
+
if (remove) {
|
|
87
|
+
const ref = /^(\d+)\s+(\d+)\s+R$/.exec(remove.value);
|
|
88
|
+
const held = ref ? readDictionary(Number(ref[1]), Number(ref[2])) : dictionary(remove.from);
|
|
89
|
+
if (held.fields.has('EmbeddedFiles')) fail();
|
|
90
|
+
names = text.slice(held.start, held.end - 2);
|
|
91
|
+
}
|
|
92
|
+
const original = remove ? text.slice(root.start, remove.start) + text.slice(remove.end, root.end - 2) : text.slice(root.start, root.end - 2);
|
|
93
|
+
const payload = encode.encode(source); check();
|
|
94
|
+
const compressed = new Uint8Array(await new Response(new Blob([payload]).stream().pipeThrough(new CompressionStream('deflate'))).arrayBuffer()); check();
|
|
95
|
+
const fileId = size, streamId = size + 1, infoId = size + 2, totalSize = size + 3;
|
|
96
|
+
let infoBody = '<< /Producer (Rapier) >>';
|
|
97
|
+
const existingInfo = trailer.fields.get('Info');
|
|
98
|
+
if (existingInfo) {
|
|
99
|
+
const ref = /^(\d+)\s+(\d+)\s+R$/.exec(existingInfo.value);
|
|
100
|
+
const held = ref ? readDictionary(Number(ref[1]), Number(ref[2])) : dictionary(existingInfo.from);
|
|
101
|
+
const producer = held.fields.get('Producer');
|
|
102
|
+
const kept = producer ? text.slice(held.start, producer.start) + text.slice(producer.end, held.end - 2) : text.slice(held.start, held.end - 2);
|
|
103
|
+
infoBody = kept + ' /Producer (Rapier) >>';
|
|
104
|
+
}
|
|
105
|
+
const escaped = '(' + filename.replace(/[^\x20-\x7e]/g, '_').replace(/[\\()]/g, '\\$&') + ')';
|
|
106
|
+
const parts = [bytes, ascii('\n')], merged = new Map(offsets);
|
|
107
|
+
let length = bytes.length + 1;
|
|
108
|
+
const append = (id, gen, chunks) => { merged.set(id, {at: length, generation: gen}); const body = [ascii(id + ' ' + gen + ' obj\n'), ...chunks, ascii('\nendobj\n')]; for (const chunk of body) {parts.push(chunk); length += chunk.length;} };
|
|
109
|
+
append(rootId, generation, [ascii(original + ' /Names ' + names + ' /EmbeddedFiles << /Names [<feff' + hex(filename) + '> ' + fileId + ' 0 R] >> >> /AF [' + fileId + ' 0 R] >>')]);
|
|
110
|
+
append(fileId, 0, [ascii('<< /Type /Filespec /F ' + escaped + ' /UF <feff' + hex(filename) + '> /AFRelationship /Source /EF << /F ' + streamId + ' 0 R /UF ' + streamId + ' 0 R >> >>')]);
|
|
111
|
+
append(streamId, 0, [ascii('<< /Type /EmbeddedFile /Subtype /text#2Fmarkdown /Params << /Size ' + payload.length + ' >> /Filter /FlateDecode /Length ' + compressed.length + ' >>\nstream\n'), compressed, ascii('\nendstream')]);
|
|
112
|
+
append(infoId, 0, [ascii(infoBody)]);
|
|
113
|
+
let table = 'xref\n0 ' + totalSize + '\n0000000000 65535 f \n';
|
|
114
|
+
for (let id = 1; id < totalSize; id++) {
|
|
115
|
+
const place = merged.get(id);
|
|
116
|
+
table += place ? String(place.at).padStart(10, '0') + ' ' + String(place.generation).padStart(5, '0') + ' n \n' : '0000000000 00000 f \n';
|
|
117
|
+
}
|
|
118
|
+
const identity = trailer.fields.get('ID');
|
|
119
|
+
table += 'trailer\n<< /Size ' + totalSize + ' /Root ' + rootId + ' ' + generation + ' R /Info ' + infoId + ' 0 R' + (identity ? ' /ID ' + identity.value : '') + ' >>\nstartxref\n' + length + '\n%%EOF\n';
|
|
120
|
+
parts.push(ascii(table)); length += table.length;
|
|
121
|
+
if (length > maxBytes) throw Object.assign(new Error('The exported file exceeds the file limit.'), {code: 'export_too_large'});
|
|
122
|
+
const result = new Uint8Array(length); let at = 0;
|
|
123
|
+
for (const chunk of parts) { result.set(chunk, at); at += chunk.length; }
|
|
124
|
+
check(); return result;
|
|
125
|
+
}
|
|
@@ -410,7 +410,7 @@ function initMarkdownIt() {
|
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
|
|
413
|
-
const root = document.createElement('div');
|
|
413
|
+
const root = document.createElement('template').content.ownerDocument.createElement('div');
|
|
414
414
|
root.setAttribute('data-rapier-semantic-root', 'true');
|
|
415
415
|
|
|
416
416
|
if (metadata.docKind !== 'markdown') {
|
|
@@ -469,7 +469,8 @@ async function _rapierRenderSemanticRootAsync(canonical, metadata, options = {})
|
|
|
469
469
|
let {tokens, env} = parsed, labels = env.footnotes?.list?.map((item, index) => String(item?.label || ('inline-' + (index + 1))));
|
|
470
470
|
env = {...env, rapierImagePresentation: options.imagePresentation === true};
|
|
471
471
|
await pause(work, .3);
|
|
472
|
-
const
|
|
472
|
+
const inert = document.createElement('template').content.ownerDocument;
|
|
473
|
+
const root = inert.createElement('div'), part = inert.createElement('div'), boundary = htmlBoundary();
|
|
473
474
|
root.setAttribute('data-rapier-semantic-root', 'true');
|
|
474
475
|
let html = '', began = performance.now(), complete = false;
|
|
475
476
|
const append = () => {
|
|
@@ -5,6 +5,8 @@ import {fontRequests, waitForFonts} from './export-fonts.mjs';
|
|
|
5
5
|
import {apply as applyTypography} from '../layout/typography.mjs';
|
|
6
6
|
import {documentLanguage} from '../spec/document-settings.mjs';
|
|
7
7
|
import {applyPrintCapitals} from './print-capitals.mjs';
|
|
8
|
+
import {stripOneTerminator as _rapierWillStripOneTerminator} from '../agent/will.mjs';
|
|
9
|
+
import {quotedWillSpan} from '../spec/md-marks.mjs';
|
|
8
10
|
const RAPIER_WILL_SENTINEL_MARK = '';
|
|
9
11
|
// SPDX-License-Identifier: MIT
|
|
10
12
|
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
@@ -27,12 +29,11 @@ function _rapierWillSentinelPlan(canonical, kind) {
|
|
|
27
29
|
return { canonical: out + source.slice(cursor), markers, sentinel };
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
const _rapierWillStripOneTerminator = globalThis.RapierAgentWill.stripOneTerminator;
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
function _rapierPrintWillAdmit(canonical, plan) {
|
|
35
|
-
const quoted =
|
|
36
|
+
const quoted = quotedWillSpan(canonical, _rapierWillParse(canonical).blocks.map(block => [block.start, block.end]));
|
|
36
37
|
if (quoted && quoted.where === 'frontmatter') throw _rapierPrintWillLost('a marker stands in the front matter, which is not on the printed page', 'Take it out of the front matter');
|
|
37
38
|
if (quoted) throw _rapierPrintWillLost('a marker stands inside other content, where a reader of the PDF\'s text could not tell it from a carried marker', 'Take the quoted marker out');
|
|
38
39
|
for (const marker of plan.markers) {
|
|
@@ -42,8 +43,12 @@ function _rapierPrintWillAdmit(canonical, plan) {
|
|
|
42
43
|
if (new Set(plan.markers.flatMap(marker => Array.from(marker))).size > 65000) throw _rapierPrintWillLost('the markers use more distinct characters than a font can hold', 'Shorten them');
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function _rapierPrintWillFont(markers) {
|
|
46
|
-
const
|
|
46
|
+
function _rapierPrintWillFont(markers, advanceOverride) {
|
|
47
|
+
const metrics = _rapierPrintWillMetrics(markers);
|
|
48
|
+
const size = metrics.size;
|
|
49
|
+
// The page's carrier face keeps the short marker advance. Searchable text is
|
|
50
|
+
// positioned at 500 units, which is also /DW, so this copy can be asked to match it.
|
|
51
|
+
const advance = advanceOverride ?? metrics.advance;
|
|
47
52
|
const scalars = Array.from(new Set(markers.flatMap(marker => Array.from(marker, ch => ch.codePointAt(0)))));
|
|
48
53
|
const order = scalars.filter(code => code < 0x10000).sort((a, b) => a - b)
|
|
49
54
|
.concat(scalars.filter(code => code >= 0x10000).sort((a, b) => b - a));
|
|
@@ -296,7 +301,7 @@ async function _rapierWritePrintPdf({root, css, fontCss = '', canonical, bom = f
|
|
|
296
301
|
glyphs.add(character);
|
|
297
302
|
if (glyphs.size > 65000) refuse('pdf_text_glyph_limit');
|
|
298
303
|
}
|
|
299
|
-
const font = _rapierPrintWillFont(text);
|
|
304
|
+
const font = _rapierPrintWillFont(text, 500);
|
|
300
305
|
strip.style.width = count * bodyWidth + 'px';
|
|
301
306
|
strip.style.transform = 'none';
|
|
302
307
|
// One styled film is reused for every page window. Batches stay inside the capture edge
|