rapier-html 1.2.33 → 1.2.42
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/BUILD.json +21 -21
- package/README.md +22 -13
- package/dist/ledger/data.mjs +57 -4
- package/dist/ledger/document-settings.mjs +1 -0
- package/dist/ledger/format.mjs +114 -47
- package/dist/ledger/hash.mjs +43 -2
- package/dist/ledger/history.mjs +6 -0
- package/dist/ledger/inverse-source.mjs +15 -2
- package/dist/ledger/journal-records.mjs +32 -9
- package/dist/ledger/md-assets.mjs +709 -0
- package/dist/ledger/will.mjs +239 -0
- package/dist/ledger/word-sidecar.mjs +5 -0
- package/dist/page.d.mts +2 -0
- package/dist/rapier.html +85 -84
- package/dist/wrap.mjs +11 -4
- package/package.json +1 -1
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Pure text and markdown-it tokens: no DOM, no byte decoding, no globalThis.Rapier* (MIT kit; decoders stay in images/assets.mjs).
|
|
3
|
+
import {isWordSidecarBlock} from './word-sidecar.mjs';
|
|
4
|
+
import {sourceFacts, sourceView, sourceEdit} from './source.mjs';
|
|
5
|
+
|
|
6
|
+
export const IMAGE_LIMITS = Object.freeze({bytes: 16 * 1024 * 1024, sourceChars: 64 * 1024 * 1024, dimension: 16384, pixels: 24000000, assets: 1024});
|
|
7
|
+
const parsers = new WeakMap();
|
|
8
|
+
const cached = [];
|
|
9
|
+
let preparedDefinitions = [];
|
|
10
|
+
let configured = null, splitSource = null;
|
|
11
|
+
export function configureParser(parser, split) { configured = parser; splitSource = split; cached.length = 0; preparedDefinitions = []; }
|
|
12
|
+
function fail(reason) { throw new Error(reason); }
|
|
13
|
+
export const normalizeLabel = label => String(label).trim().replace(/\s+/g, ' ').toLowerCase().toUpperCase();
|
|
14
|
+
export const assetTitle = value => String(value || '').replace(/[\r\n]/g, ' ').replace(/\0/g, '\ufffd');
|
|
15
|
+
|
|
16
|
+
export function installMarkdownImages(md) {
|
|
17
|
+
if (md.__rapierImagesInstalled) return md;
|
|
18
|
+
md.__rapierImagesInstalled = true;
|
|
19
|
+
const validate = md.validateLink;
|
|
20
|
+
md.validateLink = url => !!dataImage(url) || validate(url);
|
|
21
|
+
const normalize = md.normalizeLink;
|
|
22
|
+
md.normalizeLink = url => dataImage(url) ? url : normalize(url);
|
|
23
|
+
const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
|
|
24
|
+
md.block.ruler.at('reference', (state, start, end, silent) => {
|
|
25
|
+
if (silent) return reference(state, start, end, silent);
|
|
26
|
+
if (state.src.charCodeAt(state.bMarks[start] + state.tShift[start]) !== 0x5B) return false;
|
|
27
|
+
const saved = state.env.references, local = Object.create(null);
|
|
28
|
+
state.env.references = local;
|
|
29
|
+
let accepted;
|
|
30
|
+
try { accepted = reference(state, start, end, false); }
|
|
31
|
+
finally { state.env.references = saved; }
|
|
32
|
+
if (accepted) {
|
|
33
|
+
const token = state.tokens[state.tokens.length - 1], label = token.meta.label;
|
|
34
|
+
token.meta.mdImageDefinition = local[label];
|
|
35
|
+
const refs = state.env.references ||= Object.create(null);
|
|
36
|
+
if (!Object.hasOwn(refs, label)) refs[label] = local[label];
|
|
37
|
+
}
|
|
38
|
+
return accepted;
|
|
39
|
+
});
|
|
40
|
+
const image = md.inline.ruler.__rules__.find(row => row.name === 'image').fn;
|
|
41
|
+
md.inline.ruler.at('image', (state, silent) => {
|
|
42
|
+
const start = state.pos;
|
|
43
|
+
if (state.src[start] !== '!' || state.src[start + 1] !== '[') return false;
|
|
44
|
+
const before = state.tokens.length;
|
|
45
|
+
if (!image(state, silent)) return false;
|
|
46
|
+
if (!silent && state.tokens.length > before) {
|
|
47
|
+
const token = state.tokens[state.tokens.length - 1], source = state.src.slice(start, state.pos);
|
|
48
|
+
if (token.type === 'image') {
|
|
49
|
+
const close = state.md.helpers.parseLinkLabel(state, start + 1, false);
|
|
50
|
+
let reference = null;
|
|
51
|
+
if (close >= 0 && state.src[close + 1] !== '(') {
|
|
52
|
+
const explicit = state.src[close + 1] === '[' ? state.src.slice(close + 2, state.pos - 1) : '';
|
|
53
|
+
reference = md.utils.normalizeReference(explicit || state.src.slice(start + 2, close));
|
|
54
|
+
}
|
|
55
|
+
(token.meta ||= {}).mdImage = {source, reference};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
return md;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parserFor(factory = configured || globalThis.markdownit) {
|
|
64
|
+
if (factory?.block && factory?.utils) return factory;
|
|
65
|
+
if (configured && factory === globalThis.markdownit) return configured;
|
|
66
|
+
if (typeof factory !== 'function') return fail('markdown_parser_unavailable');
|
|
67
|
+
let parser = parsers.get(factory);
|
|
68
|
+
if (!parser) { parser = installMarkdownImages(factory({html: true, maxNesting: 20})); parsers.set(factory, parser); }
|
|
69
|
+
return parser;
|
|
70
|
+
}
|
|
71
|
+
export {parserFor as markdownParser};
|
|
72
|
+
export const markdownBodyOffset = source => splitSource ? splitSource(source).bodyOffset : 0;
|
|
73
|
+
|
|
74
|
+
const BLOCK_WINDOW = 1024, TABLE_STEP = 128, WINDOW_END = Symbol('block window end');
|
|
75
|
+
const LINE_TABLES = ['bMarks', 'eMarks', 'tShift', 'sCount', 'bsCount'];
|
|
76
|
+
const lineIndex = key => typeof key === 'string' && /^(?:0|[1-9]\d*)$/.test(key);
|
|
77
|
+
let unheldLine = null;
|
|
78
|
+
const unheldLines = new Proxy(Array.prototype, {
|
|
79
|
+
get: (target, key, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key)) : Reflect.get(target, key, table),
|
|
80
|
+
set: (target, key, value, table) => unheldLine && lineIndex(key) ? unheldLine(table, Number(key), true, value) : Reflect.set(target, key, value, table),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul = false) {
|
|
84
|
+
for (const window of blockWindows(source, bodyOffset, parser, env, keepNul)) visit(window);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function* blockWindows(source, bodyOffset, parser, env, keepNul = false) {
|
|
88
|
+
const lineEnds = source.slice(bodyOffset).replace(/\r\n?/g, '\n'), body = keepNul ? lineEnds : lineEnds.replace(/\0/g, '\ufffd');
|
|
89
|
+
let pair = source.indexOf('\r\n', bodyOffset), shift = bodyOffset;
|
|
90
|
+
const sourceOffset = at => {
|
|
91
|
+
while (pair >= 0 && at + shift > pair) { shift++; pair = source.indexOf('\r\n', pair + 2); }
|
|
92
|
+
for (let last; shift > bodyOffset && at + shift - 1 <= (last = source.lastIndexOf('\r\n', (pair < 0 ? source.length : pair) - 1)); pair = last) shift--;
|
|
93
|
+
return at + shift;
|
|
94
|
+
};
|
|
95
|
+
const tokens = [];
|
|
96
|
+
let origin = 0, first = 0, state;
|
|
97
|
+
const report = () => ({tokens, firstLine: first, lineStart: line => {
|
|
98
|
+
if (!line) return sourceOffset(origin);
|
|
99
|
+
const end = Object.hasOwn(state.eMarks, line - 1) ? state.eMarks[line - 1] : body.length;
|
|
100
|
+
return end < body.length ? sourceOffset(end + 1) : undefined;
|
|
101
|
+
}});
|
|
102
|
+
let lines = 0;
|
|
103
|
+
for (let at = 0; at < body.length; at++) if (body.charCodeAt(at) === 10) lines++;
|
|
104
|
+
if (lines <= BLOCK_WINDOW) {
|
|
105
|
+
state = new parser.block.State(body, parser, env, tokens);
|
|
106
|
+
state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
|
|
107
|
+
parser.block.tokenize(state, state.line, state.lineMax);
|
|
108
|
+
yield report(); return;
|
|
109
|
+
}
|
|
110
|
+
state = new parser.block.State(body.slice(body.lastIndexOf('\n') + 1), parser, env, tokens);
|
|
111
|
+
state.rapierSourceSlice = (from, to) => source.slice(sourceOffset(from), sourceOffset(to));
|
|
112
|
+
lines += state.lineMax;
|
|
113
|
+
state.src = body;
|
|
114
|
+
const tables = new WeakMap();
|
|
115
|
+
for (const name of LINE_TABLES) { state[name].length = 0; tables.set(Object.setPrototypeOf(state[name], unheldLines), name); }
|
|
116
|
+
let frontier = 0, ended = false, betweenBlocks = false;
|
|
117
|
+
const fill = line => {
|
|
118
|
+
while (!ended && line >= state.bMarks.length) {
|
|
119
|
+
const held = state.bMarks.length;
|
|
120
|
+
let until = frontier;
|
|
121
|
+
for (let more = held ? Math.max(TABLE_STEP, held - BLOCK_WINDOW) : BLOCK_WINDOW; more && until < body.length; more--) {
|
|
122
|
+
const end = body.indexOf('\n', until);
|
|
123
|
+
until = end < 0 ? body.length : end + 1;
|
|
124
|
+
}
|
|
125
|
+
const scanned = new parser.block.State(body.slice(frontier, until), parser, env, []);
|
|
126
|
+
ended = until === body.length;
|
|
127
|
+
const added = ended ? scanned.lineMax + 1 : scanned.lineMax;
|
|
128
|
+
for (const name of LINE_TABLES) {
|
|
129
|
+
const old = state[name], next = scanned[name], offset = name === 'bMarks' || name === 'eMarks' ? frontier : 0, table = [];
|
|
130
|
+
for (let at = 0; at < held; at++) table.push(old[at]);
|
|
131
|
+
for (let at = 0; at < added; at++) table.push(next[at] + offset);
|
|
132
|
+
old.length = 0;
|
|
133
|
+
tables.set(table, name);
|
|
134
|
+
state[name] = Object.setPrototypeOf(table, unheldLines);
|
|
135
|
+
}
|
|
136
|
+
frontier = until;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const answer = (table, line, write, value) => {
|
|
140
|
+
const name = tables.get(table);
|
|
141
|
+
if (line >= state[name].length) {
|
|
142
|
+
if (betweenBlocks) throw WINDOW_END;
|
|
143
|
+
fill(line);
|
|
144
|
+
}
|
|
145
|
+
const current = state[name], held = Object.hasOwn(current, line);
|
|
146
|
+
if (!write) return held ? current[line] : undefined;
|
|
147
|
+
if (held) current[line] = value;
|
|
148
|
+
else Object.defineProperty(current, line, {value, writable: true, enumerable: true, configurable: true});
|
|
149
|
+
return true;
|
|
150
|
+
};
|
|
151
|
+
const skipEmptyLines = state.skipEmptyLines;
|
|
152
|
+
state.skipEmptyLines = function (line) {
|
|
153
|
+
if (this.level || line !== this.line || !line) return skipEmptyLines.call(this, line);
|
|
154
|
+
if (line >= BLOCK_WINDOW) throw WINDOW_END;
|
|
155
|
+
betweenBlocks = true;
|
|
156
|
+
line = skipEmptyLines.call(this, line);
|
|
157
|
+
betweenBlocks = false;
|
|
158
|
+
return line;
|
|
159
|
+
};
|
|
160
|
+
for (;;) {
|
|
161
|
+
for (let at = origin; at < body.length; at++) {
|
|
162
|
+
const code = body.charCodeAt(at);
|
|
163
|
+
if (code === 10) { origin = at + 1; first++; }
|
|
164
|
+
else if (!parser.utils.isSpace(code)) break;
|
|
165
|
+
}
|
|
166
|
+
frontier = origin; ended = false; betweenBlocks = false;
|
|
167
|
+
state.line = 0; state.lineMax = lines - first; state.rapierFirstLine = first;
|
|
168
|
+
const outer = unheldLine;
|
|
169
|
+
unheldLine = answer;
|
|
170
|
+
let finished = true;
|
|
171
|
+
try { parser.block.tokenize(state, 0, state.lineMax); }
|
|
172
|
+
catch (error) { if (error !== WINDOW_END) throw error; finished = false; }
|
|
173
|
+
finally { unheldLine = outer; }
|
|
174
|
+
yield report();
|
|
175
|
+
if (finished) return;
|
|
176
|
+
tokens.length = 0;
|
|
177
|
+
origin = state.eMarks[state.line - 1] + 1; first += state.line;
|
|
178
|
+
for (const name of LINE_TABLES) state[name].length = 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const PERCENT_PAYLOAD = Array.from({length: 128}, (_, code) => code >= 48 && code <= 57 || code >= 65 && code <= 90 ||
|
|
183
|
+
code >= 97 && code <= 122 || '-._~:/?@+$,;='.includes(String.fromCharCode(code)));
|
|
184
|
+
const isHexUnit = code => code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
|
|
185
|
+
let lastScan = null;
|
|
186
|
+
export function dataImage(url) {
|
|
187
|
+
if (typeof url !== 'string' || url.length > IMAGE_LIMITS.sourceChars) return null;
|
|
188
|
+
if (url.length > 4096) {
|
|
189
|
+
if (lastScan?.url !== url) lastScan = {url, image: scanDataImage(url)};
|
|
190
|
+
return lastScan.image && {...lastScan.image};
|
|
191
|
+
}
|
|
192
|
+
return scanDataImage(url);
|
|
193
|
+
}
|
|
194
|
+
function scanDataImage(url) {
|
|
195
|
+
const prepared = preparedDefinitions.find(row => row.image.url === url)?.image;
|
|
196
|
+
if (prepared) return {codec: prepared.codec, byteLength: prepared.byteLength, payloadStart: url.indexOf(',') + 1,
|
|
197
|
+
...(prepared.encoding ? {encoding: prepared.encoding} : {})};
|
|
198
|
+
if (/^data:image\/svg\+xml,/i.test(url)) {
|
|
199
|
+
const payloadStart = 19;
|
|
200
|
+
let byteLength = 0;
|
|
201
|
+
for (let at = payloadStart; at < url.length; at++, byteLength++) {
|
|
202
|
+
const code = url.charCodeAt(at);
|
|
203
|
+
if (code === 37) {
|
|
204
|
+
if (!isHexUnit(url.charCodeAt(at + 1)) || !isHexUnit(url.charCodeAt(at + 2))) return null;
|
|
205
|
+
at += 2;
|
|
206
|
+
} else if (!(code < 128 && PERCENT_PAYLOAD[code])) return null;
|
|
207
|
+
}
|
|
208
|
+
return byteLength && byteLength <= IMAGE_LIMITS.bytes
|
|
209
|
+
? {codec: 'image/svg+xml', byteLength, payloadStart, encoding: 'percent'} : null;
|
|
210
|
+
}
|
|
211
|
+
const match = /^data:(image\/(?:jxl|png|jpeg|webp|gif|svg\+xml));base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
|
|
212
|
+
if (!match || match[2].length % 4) return null;
|
|
213
|
+
const padding = match[2].endsWith('==') ? 2 : match[2].endsWith('=') ? 1 : 0;
|
|
214
|
+
const byteLength = match[2].length / 4 * 3 - padding;
|
|
215
|
+
if (!byteLength || byteLength > IMAGE_LIMITS.bytes) return null;
|
|
216
|
+
return {codec: match[1].toLowerCase(), byteLength, payloadStart: match[0].length - match[2].length};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function parseAssets(source, factory = configured || globalThis.markdownit, bodyOffset = 0) {
|
|
220
|
+
const steps = assetSteps(source, factory, bodyOffset);
|
|
221
|
+
let next = steps.next();
|
|
222
|
+
while (!next.done) next = steps.next();
|
|
223
|
+
return next.value;
|
|
224
|
+
}
|
|
225
|
+
export async function parseAssetsWork(source, factory, bodyOffset, work) {
|
|
226
|
+
const steps = assetSteps(source, factory, bodyOffset);
|
|
227
|
+
work.signal?.throwIfAborted();
|
|
228
|
+
let next = steps.next();
|
|
229
|
+
while (!next.done) { await work.yield(); work.signal?.throwIfAborted(); next = steps.next(); }
|
|
230
|
+
return next.value;
|
|
231
|
+
}
|
|
232
|
+
function* assetSteps(source, factory = configured || globalThis.markdownit, bodyOffset = 0) {
|
|
233
|
+
if (typeof source !== 'string') return fail('image_source_invalid');
|
|
234
|
+
if (source.length > IMAGE_LIMITS.sourceChars) return fail('image_source_limit');
|
|
235
|
+
if (!source.includes(']:')) return {assets: new Map(), blocks: [], duplicateLabels: new Set(), appendixStart: source.length, references: Object.create(null)};
|
|
236
|
+
const prior = cached.find(row => row.source === source && row.factory === factory && row.bodyOffset === bodyOffset);
|
|
237
|
+
if (prior) return prior.index;
|
|
238
|
+
const definition = !bodyOffset && preparedDefinitions.find(row => row.source === source && row.factory === factory);
|
|
239
|
+
if (definition) return definition.index;
|
|
240
|
+
const parser = parserFor(factory), env = {};
|
|
241
|
+
const assets = new Map(), blocks = [], seen = new Set(), duplicateLabels = new Set();
|
|
242
|
+
for (const {tokens, lineStart} of blockWindows(source, bodyOffset, parser, env)) {
|
|
243
|
+
for (const token of tokens) {
|
|
244
|
+
if (token.type !== 'reference_definition' || !token.map) continue;
|
|
245
|
+
const id = token.meta.label, definition = token.meta.mdImageDefinition, first = !seen.has(id);
|
|
246
|
+
if (!first) duplicateLabels.add(id);
|
|
247
|
+
seen.add(id);
|
|
248
|
+
const info = dataImage(definition?.href);
|
|
249
|
+
if (!info) continue;
|
|
250
|
+
const start = lineStart(token.map[0]), stop = lineStart(token.map[1]) ?? source.length;
|
|
251
|
+
let end = stop;
|
|
252
|
+
if (source[end - 1] === '\n') end--;
|
|
253
|
+
if (source[end - 1] === '\r') end--;
|
|
254
|
+
const raw = source.slice(start, end), found = raw.indexOf(definition.href);
|
|
255
|
+
const repeated = found >= 0 ? raw.indexOf(definition.href, found + 1) : -1;
|
|
256
|
+
const labelContainsUrl = definition.href.length <= id.length && id.includes(parser.utils.normalizeReference(definition.href));
|
|
257
|
+
const urlStart = found >= 0 && repeated < 0 && !labelContainsUrl ? start + found : null;
|
|
258
|
+
const row = {id, label: id, source: raw, url: definition.href, title: definition.title || '', start, end,
|
|
259
|
+
...info, urlStart, urlEnd: urlStart == null ? null : urlStart + definition.href.length,
|
|
260
|
+
payloadStart: urlStart == null ? null : urlStart + info.payloadStart, payloadEnd: urlStart == null ? null : urlStart + definition.href.length, status: 'unverified', active: first, topLevel: token.level === 0};
|
|
261
|
+
blocks.push(row);
|
|
262
|
+
if (first) assets.set(id, row);
|
|
263
|
+
}
|
|
264
|
+
yield;
|
|
265
|
+
}
|
|
266
|
+
const references = env.references || Object.create(null);
|
|
267
|
+
if (assets.size > IMAGE_LIMITS.assets) for (const asset of assets.values()) asset.status = 'asset_count_limit';
|
|
268
|
+
let appendixStart = source.length;
|
|
269
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
270
|
+
const block = blocks[index];
|
|
271
|
+
if (!block.active || !block.topLevel || !/^[ \t\r\n]*$/.test(source.slice(block.end, appendixStart))) break;
|
|
272
|
+
appendixStart = block.start;
|
|
273
|
+
}
|
|
274
|
+
const index = {assets, blocks, duplicateLabels, appendixStart, references};
|
|
275
|
+
cached.unshift({source, factory, bodyOffset, index});
|
|
276
|
+
if (cached.length > 3) cached.pop();
|
|
277
|
+
return index;
|
|
278
|
+
}
|
|
279
|
+
export function documentAssets(source, factory) { return parseAssets(source, factory, markdownBodyOffset(source)); }
|
|
280
|
+
|
|
281
|
+
export function packAssetIndex(index, offset = 0) {
|
|
282
|
+
const positions = ['start', 'end', 'urlStart', 'urlEnd', 'payloadStart', 'payloadEnd'];
|
|
283
|
+
const blocks = index.blocks.map(row => {
|
|
284
|
+
const packed = {...row};
|
|
285
|
+
delete packed.source;
|
|
286
|
+
if (Number.isSafeInteger(row.urlStart) && Number.isSafeInteger(row.urlEnd)) delete packed.url;
|
|
287
|
+
for (const key of positions) if (Number.isSafeInteger(packed[key])) packed[key] -= offset;
|
|
288
|
+
return packed;
|
|
289
|
+
});
|
|
290
|
+
const references = Object.create(null);
|
|
291
|
+
for (const [label, definition] of Object.entries(index.references)) {
|
|
292
|
+
const image = index.assets.get(label);
|
|
293
|
+
references[label] = image?.url === definition.href
|
|
294
|
+
? {row: index.blocks.indexOf(image), title: definition.title}
|
|
295
|
+
: {...definition};
|
|
296
|
+
}
|
|
297
|
+
return {blocks, references, duplicateLabels: [...index.duplicateLabels], appendixStart: index.appendixStart - offset};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, factory = configured || globalThis.markdownit} = {}) {
|
|
301
|
+
const positions = ['start', 'end', 'urlStart', 'urlEnd', 'payloadStart', 'payloadEnd'];
|
|
302
|
+
const blocks = packet.blocks.map(packed => {
|
|
303
|
+
const row = {...packed};
|
|
304
|
+
for (const key of positions) if (Number.isSafeInteger(row[key])) row[key] += offset;
|
|
305
|
+
row.source = source.slice(row.start, row.end);
|
|
306
|
+
if (Number.isSafeInteger(row.urlStart) && Number.isSafeInteger(row.urlEnd)) row.url = source.slice(row.urlStart, row.urlEnd);
|
|
307
|
+
return row;
|
|
308
|
+
});
|
|
309
|
+
const references = Object.create(null), assets = new Map();
|
|
310
|
+
for (const row of blocks) if (row.active) assets.set(row.id, row);
|
|
311
|
+
for (const [label, definition] of Object.entries(packet.references)) references[label] = Number.isSafeInteger(definition.row)
|
|
312
|
+
? {href: blocks[definition.row].url, title: definition.title} : {...definition};
|
|
313
|
+
const index = {assets, blocks, duplicateLabels: new Set(packet.duplicateLabels), appendixStart: packet.appendixStart + offset, references};
|
|
314
|
+
cached.unshift({source, factory, bodyOffset, index});
|
|
315
|
+
if (cached.length > 3) cached.pop();
|
|
316
|
+
preparedDefinitions = blocks.filter(row => row.topLevel).map(row => {
|
|
317
|
+
const local = {...row, start: 0, end: row.source.length, active: true, status: 'unverified'};
|
|
318
|
+
for (const key of positions.slice(2)) if (Number.isSafeInteger(local[key])) local[key] -= row.start;
|
|
319
|
+
const image = Object.freeze({url: row.url, codec: row.codec, byteLength: row.byteLength,
|
|
320
|
+
...(row.encoding ? {encoding: row.encoding} : {})});
|
|
321
|
+
return {source: row.source, factory, image, index: {assets: new Map([[row.id, local]]), blocks: [local],
|
|
322
|
+
references: Object.assign(Object.create(null), {[row.id]: {href: row.url, title: row.title}}),
|
|
323
|
+
duplicateLabels: new Set(), appendixStart: 0}};
|
|
324
|
+
});
|
|
325
|
+
return index;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function projectImageDefinitions(source, index) {
|
|
329
|
+
const records = [], parts = [];
|
|
330
|
+
let at = 0, length = 0;
|
|
331
|
+
for (const row of index.blocks) {
|
|
332
|
+
if (!Number.isSafeInteger(row.payloadStart) || !Number.isSafeInteger(row.payloadEnd) || row.payloadStart < at) continue;
|
|
333
|
+
const text = source.slice(at, row.payloadStart);
|
|
334
|
+
parts.push(text, 'AA=='); length += text.length;
|
|
335
|
+
records.push({start: length, end: length + 4, id: row.id, codec: row.codec,
|
|
336
|
+
payload: source.slice(row.payloadStart, row.payloadEnd), url: row.url});
|
|
337
|
+
length += 4; at = row.payloadEnd;
|
|
338
|
+
}
|
|
339
|
+
parts.push(source.slice(at));
|
|
340
|
+
const references = Object.create(null);
|
|
341
|
+
for (const [label, definition] of Object.entries(index.references)) {
|
|
342
|
+
const image = index.assets.get(label);
|
|
343
|
+
references[label] = {href: image?.url === definition.href ? image.url.slice(0, image.url.indexOf(',') + 1) + 'AA==' : definition.href,
|
|
344
|
+
title: definition.title};
|
|
345
|
+
}
|
|
346
|
+
return {source: records.length ? parts.join('') : source, records, references};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function blockwiseAssets(source, rows, parse = raw => parseAssets(raw), bodyOffset = markdownBodyOffset(source)) {
|
|
350
|
+
if (typeof source !== 'string' || !Array.isArray(rows)) return null;
|
|
351
|
+
const blocks = [], references = Object.create(null), duplicateLabels = new Set(), seen = new Set(), assets = new Map();
|
|
352
|
+
let cursor = bodyOffset;
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
const raw = String(row.raw || ''), start = row.start;
|
|
355
|
+
if (!(start >= cursor) || source.slice(cursor, start).includes(']:') || source.slice(start, start + raw.length) !== raw) return null;
|
|
356
|
+
cursor = start + raw.length;
|
|
357
|
+
if (!raw.includes(']:')) continue;
|
|
358
|
+
const index = parse(raw, row);
|
|
359
|
+
const labels = Object.keys(index.references);
|
|
360
|
+
for (const label of index.duplicateLabels) duplicateLabels.add(label);
|
|
361
|
+
for (const label of labels) {
|
|
362
|
+
if (seen.has(label)) duplicateLabels.add(label);
|
|
363
|
+
if (!Object.hasOwn(references, label)) references[label] = index.references[label];
|
|
364
|
+
}
|
|
365
|
+
for (const block of index.blocks) {
|
|
366
|
+
const active = block.active && !seen.has(block.id);
|
|
367
|
+
const shifted = {...block, start: block.start + start, end: block.end + start, status: 'unverified', active};
|
|
368
|
+
for (const key of ['urlStart', 'urlEnd', 'payloadStart', 'payloadEnd']) if (block[key] != null) shifted[key] = block[key] + start;
|
|
369
|
+
blocks.push(shifted);
|
|
370
|
+
if (active) assets.set(block.id, shifted);
|
|
371
|
+
}
|
|
372
|
+
for (const label of labels) seen.add(label);
|
|
373
|
+
}
|
|
374
|
+
if (source.slice(cursor).includes(']:')) return null;
|
|
375
|
+
if (assets.size > IMAGE_LIMITS.assets) for (const asset of assets.values()) asset.status = 'asset_count_limit';
|
|
376
|
+
let appendixStart = source.length;
|
|
377
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
378
|
+
const block = blocks[index];
|
|
379
|
+
if (!block.active || !block.topLevel || !/^[ \t\r\n]*$/.test(source.slice(block.end, appendixStart))) break;
|
|
380
|
+
appendixStart = block.start;
|
|
381
|
+
}
|
|
382
|
+
return {assets, blocks, duplicateLabels, appendixStart, references};
|
|
383
|
+
}
|
|
384
|
+
export function imageEnvironment(source, factory) { return {references: Object.assign(Object.create(null), documentAssets(source, factory).references)}; }
|
|
385
|
+
|
|
386
|
+
function imageUses(source, factory, env = {}) {
|
|
387
|
+
const parser = parserFor(factory), offset = markdownBodyOffset(source), starts = [offset], normalized = [0];
|
|
388
|
+
let position = 0;
|
|
389
|
+
for (let at = offset; at < source.length; at++, position++) {
|
|
390
|
+
if (source[at] === '\r') { if (source[at + 1] === '\n') at++; }
|
|
391
|
+
else if (source[at] !== '\n') continue;
|
|
392
|
+
starts.push(at + 1); normalized.push(position + 1);
|
|
393
|
+
}
|
|
394
|
+
const originalOffset = at => {
|
|
395
|
+
let low = 0, high = normalized.length;
|
|
396
|
+
while (low + 1 < high) {
|
|
397
|
+
const mid = (low + high) >>> 1;
|
|
398
|
+
if (normalized[mid] <= at) low = mid; else high = mid;
|
|
399
|
+
}
|
|
400
|
+
return starts[low] + at - normalized[low];
|
|
401
|
+
};
|
|
402
|
+
const body = source.slice(offset).replace(/\r\n?/g, '\n').replace(/\0/g, '\ufffd');
|
|
403
|
+
const tokens = parser.parse(body, env), urls = new Set(), images = [], html = [];
|
|
404
|
+
const visit = rows => {
|
|
405
|
+
for (const token of rows || []) {
|
|
406
|
+
if (token.type === 'image') urls.add(token.attrGet('src'));
|
|
407
|
+
if (token.type === 'link_open') urls.add(token.attrGet('href'));
|
|
408
|
+
if (token.type === 'html_inline' || token.type === 'html_block') html.push(parser.utils.unescapeAll(token.content));
|
|
409
|
+
if (token.children) visit(token.children);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
visit(tokens);
|
|
413
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
414
|
+
const token = tokens[index], map = token.map || tokens[index - 1]?.map;
|
|
415
|
+
if (token.type !== 'inline' || !map) continue;
|
|
416
|
+
const groups = new Map();
|
|
417
|
+
for (const image of token.children || []) {
|
|
418
|
+
const meta = image.type === 'image' && image.meta?.mdImage;
|
|
419
|
+
if (!meta?.source) continue;
|
|
420
|
+
if (!groups.has(meta.source)) groups.set(meta.source, []);
|
|
421
|
+
groups.get(meta.source).push({id: meta.reference, url: image.attrGet('src')});
|
|
422
|
+
}
|
|
423
|
+
const start = normalized[map[0]], end = normalized[map[1]] ?? body.length;
|
|
424
|
+
if (!Number.isSafeInteger(start) || end <= start) continue;
|
|
425
|
+
for (const [raw, rows] of groups) {
|
|
426
|
+
const found = [];
|
|
427
|
+
for (let at = body.indexOf(raw, start); at >= 0 && at + raw.length <= end; at = body.indexOf(raw, at + raw.length)) found.push(at);
|
|
428
|
+
if (found.length !== rows.length) continue;
|
|
429
|
+
found.forEach((at, index) => images.push({...rows[index], start: originalOffset(at), end: originalOffset(at + raw.length)}));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const simple = tokens.length === 3 && tokens[1].type === 'inline' && tokens[1].level === 1 &&
|
|
433
|
+
images.length === (tokens[1].children || []).filter(row => row.type === 'image').length;
|
|
434
|
+
return {urls, images, simple, html: html.join('\n').toUpperCase()};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function referenceOccurs(source, id, factory) {
|
|
438
|
+
try { return imageUses(source, factory).images.some(image => image.id === id); }
|
|
439
|
+
catch (_) { return true; }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function occurrenceChange(image, splices) {
|
|
443
|
+
let {start, end} = image, changed = false;
|
|
444
|
+
for (const row of splices) {
|
|
445
|
+
const stop = row.pos + row.removed.length;
|
|
446
|
+
if (row.pos <= start && stop >= end && stop > row.pos) return true;
|
|
447
|
+
const shift = row.inserted.length - row.removed.length;
|
|
448
|
+
if (stop <= start) { start += shift; end += shift; continue; }
|
|
449
|
+
if (row.pos > end || row.pos === end && row.removed.length) continue;
|
|
450
|
+
if (row.pos < start || stop > end) return null;
|
|
451
|
+
changed = true; end += shift;
|
|
452
|
+
}
|
|
453
|
+
return changed ? {start, end} : null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function mayRetireImageDefinitions(source, splices, factory, hint) {
|
|
457
|
+
const store = typeof source === 'string' ? null : source;
|
|
458
|
+
if (store ? !store.contains(']:') || !store.contains('data:image/', true)
|
|
459
|
+
: !source.includes(']:') || !/data:image\//i.test(source)) return false;
|
|
460
|
+
const original = (at, right, count) => {
|
|
461
|
+
for (let index = count - 1; index >= 0; index--) {
|
|
462
|
+
const row = splices[index], end = row.pos + row.inserted.length;
|
|
463
|
+
if (at >= end) at += row.removed.length - row.inserted.length;
|
|
464
|
+
else if (at > row.pos) at = row.pos + (right ? row.removed.length : 0);
|
|
465
|
+
}
|
|
466
|
+
return at;
|
|
467
|
+
};
|
|
468
|
+
const possible = splices.some((row, index) => {
|
|
469
|
+
if (!row.removed && !row.inserted) return false;
|
|
470
|
+
const start = original(row.pos, false, index), end = original(row.pos + row.removed.length, true, index);
|
|
471
|
+
if (store) {
|
|
472
|
+
if (!store.contains('![')) return false;
|
|
473
|
+
const from = Math.max(0, start - 8192);
|
|
474
|
+
const local = store.readSlice(from, Math.min(store.length, end + 4));
|
|
475
|
+
const image = local.lastIndexOf('![', end - from);
|
|
476
|
+
const separator = Math.max(local.lastIndexOf('\n\n', start - from),
|
|
477
|
+
local.lastIndexOf('\r\n\r\n', start - from), local.lastIndexOf('\r\r', start - from));
|
|
478
|
+
if (image >= 0) return image + from >= start || separator < image;
|
|
479
|
+
const exclude = left => {
|
|
480
|
+
if (hint && splices.length === 1) hint.excluded = {start: left, end};
|
|
481
|
+
return false;
|
|
482
|
+
};
|
|
483
|
+
if (separator >= 0 || from === 0) return exclude(separator >= 0 ? from + separator + 4 : 0);
|
|
484
|
+
let cursor = from;
|
|
485
|
+
while (cursor > 0) {
|
|
486
|
+
const sliceFrom = Math.max(0, cursor - 8192);
|
|
487
|
+
const chunk = store.readSlice(sliceFrom, Math.min(store.length, cursor + 3));
|
|
488
|
+
const limit = cursor - sliceFrom;
|
|
489
|
+
const sep = Math.max(chunk.lastIndexOf('\n\n', limit), chunk.lastIndexOf('\r\n\r\n', limit),
|
|
490
|
+
chunk.lastIndexOf('\r\r', limit));
|
|
491
|
+
const imageAt = chunk.lastIndexOf('![', limit);
|
|
492
|
+
if (imageAt >= 0 && (sep < 0 || imageAt > sep)) return true;
|
|
493
|
+
if (sep >= 0 || sliceFrom === 0) return exclude(sep >= 0 ? sliceFrom + sep + 4 : 0);
|
|
494
|
+
cursor = sliceFrom;
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
const image = source.lastIndexOf('![', end);
|
|
499
|
+
return image >= 0 && (image >= start || Math.max(source.lastIndexOf('\n\n', start),
|
|
500
|
+
source.lastIndexOf('\r\n\r\n', start), source.lastIndexOf('\r\r', start)) < image);
|
|
501
|
+
});
|
|
502
|
+
if (store) return possible && mayRetireImageDefinitions(store.read(), splices, factory);
|
|
503
|
+
if (!possible || splices.length !== 1) return possible;
|
|
504
|
+
const row = splices[0];
|
|
505
|
+
if (row.removed.includes('![') || /[\r\n]/.test(row.removed) || /[\r\n]/.test(row.inserted)) return true;
|
|
506
|
+
const start = row.pos ? Math.max(source.lastIndexOf('\n', row.pos - 1), source.lastIndexOf('\r', row.pos - 1)) + 1 : 0;
|
|
507
|
+
let end = source.length;
|
|
508
|
+
for (const eol of ['\r', '\n']) { const at = source.indexOf(eol, row.pos); if (at >= 0) end = Math.min(end, at); }
|
|
509
|
+
if (end - start > 8192 || row.pos + row.removed.length > end ||
|
|
510
|
+
start !== markdownBodyOffset(source) && !/(?:\n\n|\r\n\r\n|\r\r)$/.test(source.slice(Math.max(0, start - 4), start)) ||
|
|
511
|
+
end !== source.length && !/^(?:\n\n|\r\n\r\n|\r\r)/.test(source.slice(end, end + 4))) return true;
|
|
512
|
+
try {
|
|
513
|
+
const local = imageUses(source.slice(start, end), factory, imageEnvironment(source, factory));
|
|
514
|
+
return !local.simple || local.images.some(image => occurrenceChange({...image,
|
|
515
|
+
start: start + image.start, end: start + image.end}, splices));
|
|
516
|
+
} catch (_) { return true; }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function retireDeletedImageDefinitions(before, after, splices, factory) {
|
|
520
|
+
if (before === after || !mayRetireImageDefinitions(before, splices, factory)) return [];
|
|
521
|
+
try {
|
|
522
|
+
const prior = documentAssets(before, factory), current = documentAssets(after, factory);
|
|
523
|
+
if (!prior.assets.size || !current.assets.size) return [];
|
|
524
|
+
const changes = imageUses(before, factory).images.filter(image => image.id)
|
|
525
|
+
.map(image => ({image, change: occurrenceChange(image, splices)})).filter(row => row.change);
|
|
526
|
+
if (!changes.length) return [];
|
|
527
|
+
const now = imageUses(after, factory);
|
|
528
|
+
const deleted = new Set(changes.filter(({image, change}) => change === true || now.images.some(row =>
|
|
529
|
+
row.start === change.start && row.end === change.end && row.url !== image.url)).map(row => row.image.id));
|
|
530
|
+
if (!deleted.size) return [];
|
|
531
|
+
let remaining = '', at = 0;
|
|
532
|
+
for (const row of current.blocks) { remaining += after.slice(at, row.start); at = row.end; }
|
|
533
|
+
remaining = normalizeLabel(remaining + after.slice(at)).replace(/\[\s+/g, '[');
|
|
534
|
+
const retired = current.blocks.filter(row => {
|
|
535
|
+
const old = prior.assets.get(row.id);
|
|
536
|
+
return deleted.has(row.id) && row.active && row.topLevel && old?.topLevel &&
|
|
537
|
+
row.status === 'unverified' && old.source === row.source && !now.urls.has(row.url) &&
|
|
538
|
+
!prior.duplicateLabels.has(row.id) && !current.duplicateLabels.has(row.id) &&
|
|
539
|
+
!remaining.includes('[' + row.id) && !now.html.includes(row.id) && !now.html.includes(row.url.toUpperCase());
|
|
540
|
+
});
|
|
541
|
+
return retired.sort((a, b) => b.start - a.start).map(row => {
|
|
542
|
+
const gap = /(?:\r\n|\r(?!\n)|(?<!\r)\n){2}$/.exec(after.slice(Math.max(0, row.start - 5), row.start));
|
|
543
|
+
let tail = after.slice(row.end);
|
|
544
|
+
const last = /^(?:\r\n|\n|\r)?$/.test(tail);
|
|
545
|
+
for (const other of retired) if (other.start >= row.end) tail = tail.replace(other.source, '');
|
|
546
|
+
const whole = gap && !/\S/.test(tail) && row.start - gap[0].length > markdownBodyOffset(after);
|
|
547
|
+
if (whole) {
|
|
548
|
+
const [first, second] = gap[0].match(/\r\n|\r|\n/g), padding = first === second ? gap[0] : second;
|
|
549
|
+
return {pos: row.start - padding.length, removed: padding + row.source + (last ? after.slice(row.end) : ''), inserted: ''};
|
|
550
|
+
}
|
|
551
|
+
const between = gap && /^(?:\r\n|\r(?!\n)|(?<!\r)\n){2}/.test(tail) && row.start - gap[0].length >= markdownBodyOffset(after);
|
|
552
|
+
return between ? {pos: row.start - gap[0].length, removed: gap[0] + row.source, inserted: ''} : {pos: row.start, removed: row.source, inserted: ''};
|
|
553
|
+
});
|
|
554
|
+
} catch (_) { return []; }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const omissionCache = new WeakMap();
|
|
558
|
+
function payloadFacts(text) {
|
|
559
|
+
let bad = 0, equals = 0;
|
|
560
|
+
for (let at = 0; at < text.length; at++) {
|
|
561
|
+
const code = text.charCodeAt(at);
|
|
562
|
+
if (code === 61) equals++;
|
|
563
|
+
else if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) bad++;
|
|
564
|
+
}
|
|
565
|
+
return {bad, equals};
|
|
566
|
+
}
|
|
567
|
+
function payloadBytes(length, bad, equals, tail) {
|
|
568
|
+
return length % 4 === 0 && !bad && equals <= 2 && length > equals && tail === '='.repeat(equals)
|
|
569
|
+
? length / 4 * 3 - equals : null;
|
|
570
|
+
}
|
|
571
|
+
function previousOmissions(view, kind) {
|
|
572
|
+
const changes = [];
|
|
573
|
+
let cursor = view, held;
|
|
574
|
+
while (!(held = omissionCache.get(cursor)?.[kind])) {
|
|
575
|
+
const after = cursor, change = sourceEdit(cursor);
|
|
576
|
+
if (!change || changes.length === 64 || !(cursor = change.before.deref())) return new Map();
|
|
577
|
+
changes.push({...change, after});
|
|
578
|
+
}
|
|
579
|
+
let rows = [...held.values()];
|
|
580
|
+
for (const change of changes.reverse()) {
|
|
581
|
+
const end = change.pos + change.removed, delta = change.inserted - change.removed;
|
|
582
|
+
rows = rows.flatMap(row => {
|
|
583
|
+
const permitted = kind === 'asset' ? /^[A-Za-z0-9+/=\\;&%#._~-]*$/
|
|
584
|
+
: row.profile === 'base64' ? /^[A-Za-z0-9+/=\s]*$/ : /^[A-Za-z0-9\-._~!$&*+;=:@/?%]*$/;
|
|
585
|
+
if (!row.percent && change.pos >= row.payloadStart && end <= row.end && permitted.test(change.text)) {
|
|
586
|
+
const next = {...row, end: row.end + delta, chars: row.chars + delta};
|
|
587
|
+
if (kind === 'asset') {
|
|
588
|
+
const was = payloadFacts(change.before.deref().slice(change.pos, end)), now = payloadFacts(change.text);
|
|
589
|
+
next.bad += now.bad - was.bad; next.equals += now.equals - was.equals;
|
|
590
|
+
next.bytes = payloadBytes(next.end - next.payloadStart, next.bad, next.equals,
|
|
591
|
+
next.equals <= 2 ? change.after.slice(next.end - next.equals, next.end) : '');
|
|
592
|
+
}
|
|
593
|
+
return [next];
|
|
594
|
+
}
|
|
595
|
+
if (change.pos > row.end) return [row];
|
|
596
|
+
if (end < row.start) return [{...row, start: row.start + delta, end: row.end + delta,
|
|
597
|
+
...(row.payloadStart === undefined ? {} : {payloadStart: row.payloadStart + delta})}];
|
|
598
|
+
return [];
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return new Map(rows.map(row => [row.start, row]));
|
|
602
|
+
}
|
|
603
|
+
function dataMatches(source, kind, envelopes = []) {
|
|
604
|
+
const measured = sourceFacts(source), view = sourceView(source), cached = omissionCache.get(view);
|
|
605
|
+
if (cached?.[kind]) return cached[kind];
|
|
606
|
+
const prior = previousOmissions(view, kind), rows = new Map();
|
|
607
|
+
const pattern = kind === 'asset' ? /data:(image\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\\;&%#._~-]*/iy
|
|
608
|
+
: /data:([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)(?:;[^,\s"'<>)]*)?,/iy;
|
|
609
|
+
const svgPattern = /data:(image\/svg\+xml),[A-Za-z0-9%\-._~:/?@+$,;=]*/iy;
|
|
610
|
+
let end = 0, envelopeIndex = 0;
|
|
611
|
+
for (const at of measured.markers) {
|
|
612
|
+
if (kind === 'asset' && at < end) continue;
|
|
613
|
+
while (envelopeIndex < envelopes.length && envelopes[envelopeIndex].end <= at) envelopeIndex++;
|
|
614
|
+
if (envelopes[envelopeIndex]?.start <= at) continue;
|
|
615
|
+
let row = prior.get(at);
|
|
616
|
+
if (!row) {
|
|
617
|
+
pattern.lastIndex = at;
|
|
618
|
+
let match = pattern.exec(source), percent = false;
|
|
619
|
+
if (!match && kind === 'asset') {
|
|
620
|
+
svgPattern.lastIndex = at; match = svgPattern.exec(source);
|
|
621
|
+
if (match && !dataImage(match[0])) match = null;
|
|
622
|
+
percent = !!match;
|
|
623
|
+
if (percent) pattern.lastIndex = svgPattern.lastIndex;
|
|
624
|
+
}
|
|
625
|
+
if (!match) continue;
|
|
626
|
+
if (kind === 'asset') {
|
|
627
|
+
const value = match[0], payload = value.slice(value.indexOf(',') + 1);
|
|
628
|
+
const {bad, equals} = payloadFacts(payload);
|
|
629
|
+
const bytes = percent ? dataImage(value)?.byteLength ?? null : payloadBytes(payload.length, bad, equals, equals <= 2 ? payload.slice(payload.length - equals) : '');
|
|
630
|
+
row = {start: at, end: pattern.lastIndex, chars: value.length,
|
|
631
|
+
domain: 'image_bytes', reason: 'embedded_asset', profile: 'embedded', type: match[1].toLowerCase(), bytes,
|
|
632
|
+
payloadStart: pattern.lastIndex - payload.length, bad, equals, ...(percent ? {percent} : {})};
|
|
633
|
+
} else {
|
|
634
|
+
const payloadStart = pattern.lastIndex, base64 = /;base64,/i.test(match[0]);
|
|
635
|
+
let to = payloadStart;
|
|
636
|
+
const permitted = base64 ? /[A-Za-z0-9+/=\s]/ : /[A-Za-z0-9\-._~!$&*+;=:@/?%]/;
|
|
637
|
+
while (to < source.length && permitted.test(source[to])) to++;
|
|
638
|
+
row = {start: at, end: to, payloadStart, type: match[1], chars: to - at, profile: base64 ? 'base64' : 'data'};
|
|
639
|
+
}
|
|
640
|
+
Object.freeze(row);
|
|
641
|
+
}
|
|
642
|
+
rows.set(at, row); end = row.end;
|
|
643
|
+
}
|
|
644
|
+
omissionCache.set(view, {...cached, [kind]: rows});
|
|
645
|
+
return rows;
|
|
646
|
+
}
|
|
647
|
+
export function assetOmissions(source) {
|
|
648
|
+
if (typeof source !== 'string') return [];
|
|
649
|
+
return [...dataMatches(source, 'asset').values()].map(({payloadStart, bad, equals, percent, ...row}) => row);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function dataOmissions(source) {
|
|
653
|
+
const envelopes = assetOmissions(source), spans = [...envelopes];
|
|
654
|
+
let envelopeIndex = 0, end = 0;
|
|
655
|
+
for (const row of dataMatches(source, 'generic', envelopes).values()) {
|
|
656
|
+
if (row.start < end) continue;
|
|
657
|
+
while (envelopeIndex < envelopes.length && envelopes[envelopeIndex].end <= row.start) envelopeIndex++;
|
|
658
|
+
const envelope = envelopes[envelopeIndex];
|
|
659
|
+
if (envelope && envelope.start <= row.start) { end = envelope.end; continue; }
|
|
660
|
+
const {payloadStart, ...span} = row;
|
|
661
|
+
if (row.end > payloadStart) spans.push(span);
|
|
662
|
+
end = row.end;
|
|
663
|
+
}
|
|
664
|
+
return spans.sort((a, b) => a.start - b.start);
|
|
665
|
+
}
|
|
666
|
+
export function isAssetBlock(raw) {
|
|
667
|
+
if (isWordSidecarBlock(raw)) return true;
|
|
668
|
+
if (typeof raw !== 'string' || !/data:image\//i.test(raw)) return false;
|
|
669
|
+
const parsed = parseAssets(raw);
|
|
670
|
+
if (!parsed.blocks.length) return false;
|
|
671
|
+
let at = 0;
|
|
672
|
+
for (const block of parsed.blocks) {
|
|
673
|
+
if (!block.active || !block.topLevel || !/^[ \t\r\n]*$/.test(raw.slice(at, block.start))) return false;
|
|
674
|
+
at = block.end;
|
|
675
|
+
}
|
|
676
|
+
return /^[ \t\r\n]*$/.test(raw.slice(at));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export function escapeImageAlt(value) {
|
|
680
|
+
return String(value ?? '').replace(/[\r\n]+/g, ' ')
|
|
681
|
+
.replace(/[\\[\]*_`~^+$=<&]/g, '\\$&').replace(/\|(?=[1-9]\d{1,3}$)/, '\\|');
|
|
682
|
+
}
|
|
683
|
+
export function serializeAsset(asset) {
|
|
684
|
+
if (!asset || !/^[a-z0-9-]+$/i.test(asset.label) || !dataImage(asset.url)) return fail('image_metadata_invalid');
|
|
685
|
+
const value = assetTitle(asset.title), title = value ? ' "' + value.replace(/&/g, '&').replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"' : '';
|
|
686
|
+
return '[' + asset.label + ']: ' + asset.url + title;
|
|
687
|
+
}
|
|
688
|
+
export function appendAssetText(source, asset, lineEnding, {preserveLabel = false} = {}) {
|
|
689
|
+
if (typeof source !== 'string' || !asset || normalizeLabel(asset.label) !== asset.id) return fail('image_source_invalid');
|
|
690
|
+
const parsed = documentAssets(source), title = assetTitle(asset.title);
|
|
691
|
+
const existing = !preserveLabel && [...parsed.assets.values()].find(row => row.url === asset.url && row.title === title);
|
|
692
|
+
if (existing) return {source, id: existing.id, reference: existing.label, added: false, suffix: ''};
|
|
693
|
+
const previous = parsed.references[asset.id];
|
|
694
|
+
const block = serializeAsset(asset);
|
|
695
|
+
if (previous) {
|
|
696
|
+
if (previous.href !== asset.url || (previous.title || '') !== title) return fail('image_label_conflict');
|
|
697
|
+
return {source, id: asset.id, reference: asset.label, added: false, suffix: ''};
|
|
698
|
+
}
|
|
699
|
+
if (parsed.assets.size >= IMAGE_LIMITS.assets) return fail('image_asset_count_limit');
|
|
700
|
+
const eol = lineEnding || /\r\n|\n|\r/.exec(source)?.[0] || '\n', tail = /(?:\r\n?|\n)[ \t]*$/.exec(source);
|
|
701
|
+
const separator = !source || tail && /[\r\n][ \t]*$/.test(source.slice(0, tail.index)) ? '' : tail ? eol : eol + eol;
|
|
702
|
+
const suffix = separator + block + eol, nextSource = source + suffix;
|
|
703
|
+
if (nextSource.length > IMAGE_LIMITS.sourceChars) return fail('image_source_limit');
|
|
704
|
+
if (documentAssets(nextSource).references[asset.id]?.href !== asset.url) return fail('image_appendix_not_available');
|
|
705
|
+
return {source: nextSource, id: asset.id, reference: asset.label, added: true, suffix};
|
|
706
|
+
}
|
|
707
|
+
export async function appendAsset(source, asset) { return appendAssetText(source, asset); }
|
|
708
|
+
|
|
709
|
+
export const imageOnlySourceBlock = source => /^(?:!\[[^\]\n]*\]\(data:image\/(?:(?:png|jpeg|webp|gif|jxl|svg\+xml);base64,[A-Za-z0-9+\/=]+|svg\+xml,[A-Za-z0-9%\-._~:/?@+$,;=]+)(?: "[^"\n]*")?\)(?:[ \t]*<!--md-layout:v1 [^>\r\n]{1,180}-->)?\s*)+$/.test(source);
|