rapier-markdown-kit 1.1.99 → 1.1.102
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -2
- package/README.md +42 -8
- package/dist/kit/export-fonts.mjs +403 -0
- package/dist/kit/ledger/data.mjs +1 -1
- package/dist/kit/ledger/format.mjs +12 -5
- package/dist/kit/ledger/hash.mjs +16 -22
- package/dist/kit/ledger/history.mjs +59 -276
- package/dist/kit/ledger/inverse-source.mjs +316 -0
- package/dist/kit/ledger/journal-records.mjs +116 -12
- package/dist/kit/ledger/merge.mjs +7 -4
- package/dist/kit/ledger/transport.mjs +123 -19
- package/dist/kit/pdf-glyphs.mjs +159 -0
- package/dist/kit/pdf-vectors.mjs +204 -0
- package/dist/kit/print-capitals.mjs +85 -0
- package/dist/kit/rapier-ledger.mjs +2 -2
- package/dist/kit/render-entry.mjs +1 -0
- package/dist/kit/render-ink.mjs +132 -0
- package/dist/kit/render-markdown.mjs +71 -17
- package/dist/kit/render-print.mjs +208 -60
- package/dist/kit/render-sanitize.mjs +23 -7
- package/dist/kit/render-styles.mjs +8 -12
- package/dist/kit/render.mjs +365 -133
- package/dist/layout/model.mjs +26 -20
- package/dist/layout/typography.mjs +364 -0
- package/dist/packages/rapier-diff/LICENSE +21 -0
- package/dist/packages/rapier-diff/index.mjs +610 -0
- package/dist/spec/document-fonts.mjs +244 -0
- package/dist/spec/document-settings.mjs +494 -0
- package/dist/spec/frontmatter.mjs +30 -0
- package/dist/spec/html-blocks.mjs +143 -0
- package/dist/spec/md-assets.mjs +77 -5
- package/dist/spec/md-layout.mjs +7 -3
- package/dist/spec/md-marks.mjs +114 -8
- package/package.json +1 -1
- package/style.css +110 -52
- package/dist/agent/diff.mjs +0 -52
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Embedded fonts are ordinary CommonMark reference definitions. Parsing owns spans, never bytes.
|
|
3
|
+
import {markdownParser, installMarkdownImages, normalizeLabel} from './md-assets.mjs';
|
|
4
|
+
import {splitOpeningFrontmatter} from './frontmatter.mjs';
|
|
5
|
+
import {readDocumentSettings, documentFont} from './document-settings.mjs';
|
|
6
|
+
import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
|
|
7
|
+
|
|
8
|
+
// Unquoted CSS generic families require no portable asset. Quoted names remain custom.
|
|
9
|
+
export const CSS_GENERIC_FONT_FAMILIES = Object.freeze(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',
|
|
10
|
+
'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong', 'initial', 'revert', 'revert-layer']);
|
|
11
|
+
|
|
12
|
+
// Also relocated into the parse worker. No decoding or external resource loading happens here.
|
|
13
|
+
export function fontDataUrl(url) {
|
|
14
|
+
if (typeof url !== 'string') return null;
|
|
15
|
+
const match = /^data:font\/(woff2|woff|ttf|otf);base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
|
|
16
|
+
if (!match || match[2].length % 4) return null;
|
|
17
|
+
const padding = match[2].endsWith('==') ? 2 : match[2].endsWith('=') ? 1 : 0;
|
|
18
|
+
const byteLength = match[2].length / 4 * 3 - padding;
|
|
19
|
+
return byteLength >= 12 ? {format: match[1].toLowerCase(), byteLength, data: match[2]} : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parserFor(parser) {
|
|
23
|
+
const md = installMarkdownImages(markdownParser(parser));
|
|
24
|
+
if (!md.__rapierFontsInstalled) {
|
|
25
|
+
const validate = md.validateLink;
|
|
26
|
+
md.validateLink = url => !!fontDataUrl(url) || validate(url);
|
|
27
|
+
md.__rapierFontsInstalled = true;
|
|
28
|
+
}
|
|
29
|
+
return md;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function faceOf(value) {
|
|
33
|
+
const family = normalizeFontFamily(value?.family), style = value?.style || 'normal';
|
|
34
|
+
const weight = value?.weight ?? 400, stretch = value?.stretch ?? 100;
|
|
35
|
+
if (!family || !['normal', 'italic', 'oblique'].includes(style) || !Number.isFinite(weight) || weight < 1 || weight > 1000 ||
|
|
36
|
+
!Number.isFinite(stretch) || stretch <= 0 || stretch > 1000) return null;
|
|
37
|
+
const result = {family, style, weight, stretch};
|
|
38
|
+
if (value.weightRange != null) {
|
|
39
|
+
const range = value.weightRange;
|
|
40
|
+
if (!Array.isArray(range) || range.length !== 2 || range.some(number => !Number.isFinite(number) || number < 1 || number > 1000) || range[0] > range[1]) return null;
|
|
41
|
+
result.weightRange = range.slice();
|
|
42
|
+
}
|
|
43
|
+
if (value.axes != null) {
|
|
44
|
+
if (!Array.isArray(value.axes) || value.axes.length > 64) return null;
|
|
45
|
+
const seen = new Set(), axes = [];
|
|
46
|
+
for (const axis of value.axes) {
|
|
47
|
+
if (!axis || !/^[ -~]{4}$/.test(axis.tag) || seen.has(axis.tag) ||
|
|
48
|
+
![axis.min, axis.default, axis.max].every(Number.isFinite) || axis.min > axis.default || axis.default > axis.max) return null;
|
|
49
|
+
seen.add(axis.tag);
|
|
50
|
+
axes.push({tag: axis.tag, min: axis.min, default: axis.default, max: axis.max, ...(typeof axis.name === 'string' ? {name: axis.name} : {})});
|
|
51
|
+
}
|
|
52
|
+
if (axes.length) result.axes = axes;
|
|
53
|
+
}
|
|
54
|
+
if (value.sourceId != null) {
|
|
55
|
+
if (!/^f[0-9a-f]{24}$/.test(value.sourceId)) return null;
|
|
56
|
+
result.sourceId = value.sourceId;
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function fontAssetKey(font) {
|
|
62
|
+
const face = faceOf(font);
|
|
63
|
+
if (!face) return '';
|
|
64
|
+
return [face.family.toLowerCase(), face.style, face.weight, face.stretch,
|
|
65
|
+
(face.axes || []).map(axis => [axis.tag, axis.min, axis.max].join(':')).sort().join(',')].join('/');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function readFontAssets(markdown, parser) {
|
|
69
|
+
const source = String(markdown ?? ''), assets = [], blocks = [], refused = [];
|
|
70
|
+
if (!/\[font:/i.test(source)) return {assets, blocks, refused, references: Object.create(null)};
|
|
71
|
+
const md = parserFor(parser), frame = splitOpeningFrontmatter(source), env = {}, tokens = [], starts = [frame.bodyOffset];
|
|
72
|
+
for (let at = frame.bodyOffset; at < source.length; at++) {
|
|
73
|
+
if (source[at] === '\r') { if (source[at + 1] === '\n') at++; starts.push(at + 1); }
|
|
74
|
+
else if (source[at] === '\n') starts.push(at + 1);
|
|
75
|
+
}
|
|
76
|
+
md.block.parse(frame.body.replace(/\r\n?/g, '\n').replace(/\0/g, '\ufffd'), md, env, tokens);
|
|
77
|
+
const labels = new Set(), faces = new Set();
|
|
78
|
+
for (const token of tokens) {
|
|
79
|
+
if (token.type !== 'reference_definition' || !token.map) continue;
|
|
80
|
+
const id = token.meta.label, first = !labels.has(id);
|
|
81
|
+
labels.add(id);
|
|
82
|
+
const definition = token.meta.mdImageDefinition || env.references?.[id];
|
|
83
|
+
const url = definition?.href, data = fontDataUrl(url);
|
|
84
|
+
if (!/^FONT:/i.test(id)) continue;
|
|
85
|
+
const start = starts[token.map[0]], next = starts[token.map[1]] ?? source.length;
|
|
86
|
+
let end = next;
|
|
87
|
+
if (source[end - 1] === '\n') end--;
|
|
88
|
+
if (source[end - 1] === '\r') end--;
|
|
89
|
+
const raw = source.slice(start, end), match = /^[ \t]{0,3}\[((?:\\.|[^\]\\\r\n])+)\]:/.exec(raw);
|
|
90
|
+
if (!match) continue;
|
|
91
|
+
const label = md.utils.unescapeAll(match[1]);
|
|
92
|
+
if (!data) { refused.push({name: label, reason: 'The font reference must contain an embedded font.'}); continue; }
|
|
93
|
+
let metadata = {family: label.slice(5)};
|
|
94
|
+
if (definition.title) {
|
|
95
|
+
try { metadata = JSON.parse(definition.title); }
|
|
96
|
+
catch (_) { refused.push({name: label, reason: 'The font reference has invalid metadata.'}); continue; }
|
|
97
|
+
}
|
|
98
|
+
const face = faceOf(metadata);
|
|
99
|
+
if (!face) { refused.push({name: label, reason: 'The font reference has invalid metadata.'}); continue; }
|
|
100
|
+
const key = fontAssetKey(face), active = first && !faces.has(key);
|
|
101
|
+
const row = {...face, ...data, url, label, id, key, source: raw, start, end, next, active, topLevel: token.level === 0};
|
|
102
|
+
blocks.push(row);
|
|
103
|
+
if (active) { assets.push(row); faces.add(key); }
|
|
104
|
+
}
|
|
105
|
+
return {assets, blocks, refused, references: env.references || Object.create(null)};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function fontLabel(face) {
|
|
109
|
+
const variants = face.style !== 'normal' || face.weight !== 400 || face.stretch !== 100 || face.axes?.length;
|
|
110
|
+
return 'font:' + face.family + (variants ? '/' + face.style + '/' + face.weight + (face.stretch !== 100 ? '/' + face.stretch : '')
|
|
111
|
+
+ (face.axes?.length ? '/' + face.axes.map(axis => axis.tag + '=' + axis.min + ':' + axis.max).join(',') : '') : '');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function serializeFontAsset(asset, label = null) {
|
|
115
|
+
const face = faceOf(asset), data = fontDataUrl(asset?.url);
|
|
116
|
+
if (!face || !data) throw new TypeError('font: use an embedded font with valid family metadata');
|
|
117
|
+
const escaped = escapeFontLabel(label || fontLabel(face));
|
|
118
|
+
const title = JSON.stringify(face).replace(/&/g, '&').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
119
|
+
return '[' + escaped + ']: ' + asset.url + ' "' + title + '"';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function escapeFontLabel(label) {
|
|
123
|
+
return String(label).replace(/[\\[\]]/g, '\\$&').replace(/&/g, '&');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Unchanged faces keep their original definition, whitespace and line ending. Only supplied font
|
|
127
|
+
// assets are replaced; pictures, other references and malformed authored source stay untouched.
|
|
128
|
+
export function writeFontAssets(markdown, assets, parser) {
|
|
129
|
+
const source = String(markdown ?? ''), md = parserFor(parser), index = readFontAssets(source, md), wanted = new Map();
|
|
130
|
+
for (const asset of assets || []) {
|
|
131
|
+
const key = fontAssetKey(asset);
|
|
132
|
+
if (!key || !fontDataUrl(asset?.url)) throw new TypeError('font: invalid embedded asset');
|
|
133
|
+
if (wanted.has(key) && wanted.get(key).url !== asset.url) throw new TypeError('font: one file owns each family face');
|
|
134
|
+
wanted.set(key, asset);
|
|
135
|
+
}
|
|
136
|
+
const edits = [], consumed = new Set(), labels = new Set(Object.keys(index.references));
|
|
137
|
+
const retired = index.assets.filter(row => !wanted.has(row.key));
|
|
138
|
+
for (const row of index.blocks) {
|
|
139
|
+
if (!row.active) {
|
|
140
|
+
if (retired.some(active => active.key === row.key || active.id === row.id)) edits.push({start: row.start, end: row.next, text: ''});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const asset = wanted.get(row.key);
|
|
144
|
+
if (!asset) { edits.push({start: row.start, end: row.next, text: ''}); continue; }
|
|
145
|
+
consumed.add(row.key);
|
|
146
|
+
if (asset.url === row.url && JSON.stringify(faceOf(asset)) === JSON.stringify(faceOf(row))) continue;
|
|
147
|
+
edits.push({start: row.start, end: row.end, text: serializeFontAsset(asset, row.label)});
|
|
148
|
+
}
|
|
149
|
+
let result = source;
|
|
150
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
151
|
+
const pending = [];
|
|
152
|
+
for (const [key, asset] of wanted) {
|
|
153
|
+
if (consumed.has(key)) continue;
|
|
154
|
+
const base = fontLabel(faceOf(asset));
|
|
155
|
+
let label = base, suffix = 2;
|
|
156
|
+
while (labels.has(normalizeLabel(escapeFontLabel(label)))) label = base + '/' + suffix++;
|
|
157
|
+
labels.add(normalizeLabel(escapeFontLabel(label)));
|
|
158
|
+
pending.push(serializeFontAsset(asset, label));
|
|
159
|
+
}
|
|
160
|
+
if (pending.length) {
|
|
161
|
+
const eol = /\r\n|\r|\n/.exec(source)?.[0] || '\n';
|
|
162
|
+
result += (result ? (/(?:\r\n|\r(?!\n)|\n)[ \t]*(?:\r\n|\r(?!\n)|\n)$/.test(result) ? '' : /[\r\n]$/.test(result) ? eol : eol + eol) : '') + pending.join(eol) + eol;
|
|
163
|
+
const written = readFontAssets(result, md).assets;
|
|
164
|
+
for (const [key, asset] of wanted) if (!written.some(row => row.key === key && row.url === asset.url)) throw new Error('font: close the code or HTML block before embedding a font');
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function documentFontsCss(assets) {
|
|
170
|
+
const rules = [];
|
|
171
|
+
for (const asset of assets || []) {
|
|
172
|
+
const face = faceOf(asset);
|
|
173
|
+
if (!face || !fontDataUrl(asset?.url)) continue;
|
|
174
|
+
const weight = face.axes?.find(axis => axis.tag === 'wght');
|
|
175
|
+
const width = face.axes?.find(axis => axis.tag === 'wdth');
|
|
176
|
+
const range = weight ? [weight.min, weight.max] : face.weightRange || [face.weight, face.weight];
|
|
177
|
+
const weightCss = range[0] === range[1] ? String(range[0]) : range.join(' ');
|
|
178
|
+
const stretchCss = width ? width.min + '% ' + width.max + '%' : face.stretch + '%';
|
|
179
|
+
rules.push('@font-face{font-family:' + fontFamilyCss(face.family) + ';font-style:' + face.style + ';font-weight:' + weightCss +
|
|
180
|
+
';font-stretch:' + stretchCss + ';src:url("' + asset.url + '");font-display:swap}');
|
|
181
|
+
}
|
|
182
|
+
return rules.join('');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function htmlFontFamilies(html, md) {
|
|
186
|
+
const families = new Set();
|
|
187
|
+
const decode = value => md.utils.unescapeAll(value.replace(/\\/g, '\\\\'));
|
|
188
|
+
for (const tag of html.matchAll(/<[A-Za-z][A-Za-z0-9:-]*(?:"[^"]*"|'[^']*'|[^'">])*>/g)) {
|
|
189
|
+
for (const attr of tag[0].matchAll(/\s(data-md-font|style)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi)) {
|
|
190
|
+
const value = decode(attr[2] ?? attr[3] ?? attr[4]), names = [];
|
|
191
|
+
if (attr[1].toLowerCase() === 'data-md-font') names.push(value);
|
|
192
|
+
else for (const declared of value.matchAll(/(?:^|;)\s*font-family\s*:\s*((?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^;])*)/gi)) {
|
|
193
|
+
const match = /^\s*(?:"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'|([^,!]+))/.exec(declared[1]);
|
|
194
|
+
if (!match) continue;
|
|
195
|
+
const name = (match[1] ?? match[2] ?? match[3]).replace(/\\(?:([0-9a-f]{1,6})\s?|([^\r\n\f]))/gi, (_whole, hex, escaped) => {
|
|
196
|
+
const code = hex ? parseInt(hex, 16) : 0;
|
|
197
|
+
return hex ? code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : '\ufffd' : escaped;
|
|
198
|
+
});
|
|
199
|
+
if (match[3] != null && CSS_GENERIC_FONT_FAMILIES.includes(name.trim().toLowerCase())) continue;
|
|
200
|
+
names.push(name);
|
|
201
|
+
}
|
|
202
|
+
for (const name of names) { const family = normalizeFontFamily(name); if (family) families.add(family); }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return families;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Tokens supply decoded characters: reference payloads, link destinations and Markdown punctuation
|
|
209
|
+
// never inflate a subset. Each face in a family receives the family's complete text for shaping.
|
|
210
|
+
export function documentFontUsage(markdown, parser, defaultFont) {
|
|
211
|
+
const source = String(markdown ?? ''), md = parserFor(parser), settings = readDocumentSettings(source), use = new Map();
|
|
212
|
+
const family = setting => setting?.stack === 'named' ? setting.family : null;
|
|
213
|
+
const inherited = defaultFont === undefined ? settings.defaultfont : documentFont(defaultFont);
|
|
214
|
+
const main = family(settings.mainfont || inherited || settings.sansfont), sans = family(settings.sansfont || settings.mainfont || inherited), mono = family(settings.monofont);
|
|
215
|
+
const add = (name, text) => { if (name && !Object.hasOwn(FONT_STACKS, name.toLowerCase()) && text) use.set(name, (use.get(name) || '') + text + '\n'); };
|
|
216
|
+
const plain = html => md.utils.unescapeAll(String(html).replace(/<!--[^]*?-->/g, '').replace(/<(script|style)\b[^>]*>[^]*?<\/\1\s*>/gi, '').replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\'));
|
|
217
|
+
let blockFamily = main;
|
|
218
|
+
for (const token of md.parse(splitOpeningFrontmatter(source).body, {})) {
|
|
219
|
+
if (token.type === 'heading_open') blockFamily = sans;
|
|
220
|
+
else if (token.type === 'heading_close' || token.type === 'paragraph_open') blockFamily = main;
|
|
221
|
+
else if (token.type === 'fence' || token.type === 'code_block') add(mono, token.content);
|
|
222
|
+
else if (token.type === 'html_block') {
|
|
223
|
+
const text = plain(token.content);
|
|
224
|
+
add(blockFamily, text);
|
|
225
|
+
for (const name of htmlFontFamilies(token.content, md)) add(name, text);
|
|
226
|
+
}
|
|
227
|
+
else if (token.type === 'inline') {
|
|
228
|
+
const stack = [blockFamily];
|
|
229
|
+
let html = '', text = '';
|
|
230
|
+
for (const child of token.children || []) {
|
|
231
|
+
if (child.type === 'html_inline') html += child.content;
|
|
232
|
+
if (['text', 'text_special', 'emoji', 'image', 'code_inline'].includes(child.type)) text += child.content;
|
|
233
|
+
const marked = child.type === 'rapier_font_open' ? normalizeFontFamily(child.attrGet('data-md-font')) : null;
|
|
234
|
+
if (marked) { stack.push(marked); continue; }
|
|
235
|
+
if (child.type === 'rapier_font_close') { if (stack.length > 1) stack.pop(); continue; }
|
|
236
|
+
if (child.type === 'code_inline') add(mono, child.content);
|
|
237
|
+
else if (['text', 'text_special', 'emoji', 'image'].includes(child.type)) add(stack.at(-1), child.content);
|
|
238
|
+
}
|
|
239
|
+
for (const name of htmlFontFamilies(html, md)) add(name, text);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
add(main, settings.title); add(main, settings.subtitle);
|
|
243
|
+
return use;
|
|
244
|
+
}
|