rapier-html 1.2.45 → 1.2.48

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.
Files changed (55) hide show
  1. package/BUILD.json +16 -16
  2. package/README.md +13 -12
  3. package/dist/ledger/LICENSE +12 -5
  4. package/dist/ledger/authored-origin.mjs +2 -2
  5. package/dist/ledger/authorship.mjs +5 -5
  6. package/dist/ledger/carried.mjs +21 -12
  7. package/dist/ledger/diff/concerns.mjs +3 -3
  8. package/dist/ledger/diff/diff.mjs +6 -6
  9. package/dist/ledger/diff/index.mjs +2 -2
  10. package/dist/ledger/diff/merge.mjs +3 -3
  11. package/dist/ledger/diff/moves.mjs +1 -1
  12. package/dist/ledger/diff/walk.mjs +1 -1
  13. package/dist/ledger/document-fonts.mjs +296 -0
  14. package/dist/ledger/document-settings.mjs +2 -2
  15. package/dist/ledger/format.mjs +14 -12
  16. package/dist/ledger/history.mjs +7 -7
  17. package/dist/ledger/inverse-source.mjs +5 -5
  18. package/dist/ledger/journal-records.mjs +3 -3
  19. package/dist/ledger/math/delimiters.mjs +38 -0
  20. package/dist/ledger/math/markdown-rules.mjs +103 -0
  21. package/dist/ledger/md-assets.mjs +3 -2
  22. package/dist/ledger/md-comments.mjs +384 -0
  23. package/dist/ledger/md-engine.mjs +157 -0
  24. package/dist/ledger/md-marks.mjs +10 -1
  25. package/dist/ledger/md-parser-config.mjs +463 -0
  26. package/dist/ledger/md-parser-runtime.mjs +23 -0
  27. package/dist/ledger/md-source.mjs +1 -1
  28. package/dist/ledger/source.mjs +31 -2
  29. package/dist/ledger/template-page.mjs +1 -1
  30. package/dist/ledger/transport.mjs +66 -7
  31. package/dist/ledger/vendor/NOTICE.txt +60 -0
  32. package/dist/ledger/vendor/SOURCES.json +253 -0
  33. package/dist/ledger/vendor/entities/decode-codepoint.mjs +27 -0
  34. package/dist/ledger/vendor/entities.mjs +499 -0
  35. package/dist/ledger/vendor/linkify-it.mjs +427 -0
  36. package/dist/ledger/vendor/markdown-it.mjs +4027 -0
  37. package/dist/ledger/vendor/markdown-licenses/entities-LICENSE.txt +11 -0
  38. package/dist/ledger/vendor/markdown-licenses/linkify-it-LICENSE.txt +22 -0
  39. package/dist/ledger/vendor/markdown-licenses/markdown-it-LICENSE.txt +22 -0
  40. package/dist/ledger/vendor/markdown-licenses/mdurl-LICENSE.txt +45 -0
  41. package/dist/ledger/vendor/markdown-licenses/punycode.js-LICENSE-MIT.txt +20 -0
  42. package/dist/ledger/vendor/markdown-licenses/uc.micro-LICENSE.txt +20 -0
  43. package/dist/ledger/vendor/markdown-plugins.mjs +1344 -0
  44. package/dist/ledger/vendor/markdown-runtime.mjs +5 -0
  45. package/dist/ledger/vendor/mdurl/index.mjs +11 -0
  46. package/dist/ledger/vendor/mdurl/lib/decode.mjs +107 -0
  47. package/dist/ledger/vendor/mdurl/lib/encode.mjs +77 -0
  48. package/dist/ledger/vendor/mdurl/lib/format.mjs +20 -0
  49. package/dist/ledger/vendor/mdurl/lib/parse.mjs +231 -0
  50. package/dist/ledger/vendor/punycode.mjs +282 -0
  51. package/dist/ledger/vendor/uc.micro.mjs +49 -0
  52. package/dist/page.mjs +11 -3
  53. package/dist/rapier.html +113 -75
  54. package/dist/wrap.mjs +10 -5
  55. package/package.json +1 -1
@@ -1,13 +1,13 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {_rapierPrepareHistoryRecords, _rapierValidMetadata, _rapierTransformMetadata} from './journal-records.mjs';
3
- import {pictureHistorySplices} from './history.mjs';
4
- import {HistorySource} from './source.mjs';
5
- import {authoredHistory} from './transport.mjs';
6
- import {textRoot, rootAfter, sha256, sha256PartsAsync, createSHA256} from './hash.mjs';
7
- import {canonicalJSON, canonicalParts, canonicalRowsParts, ownRow, ownLedger, isOwnedLedger, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from './data.mjs';
8
- import {diff} from './diff/diff.mjs';
9
- import {minimalHistorySplice} from './inverse-source.mjs';
10
- import {evaluateWill} from './will.mjs';
2
+ import {_rapierPrepareHistoryRecords, _rapierValidMetadata, _rapierTransformMetadata} from "./journal-records.mjs";
3
+ import {pictureHistorySplices} from "./history.mjs";
4
+ import {HistorySource} from "./source.mjs";
5
+ import {authoredHistory} from "./transport.mjs";
6
+ import {textRoot, rootAfter, sha256, sha256PartsAsync, createSHA256} from "./hash.mjs";
7
+ import {canonicalJSON, canonicalParts, canonicalRowsParts, ownRow, ownLedger, isOwnedLedger, plainData, documentText, utf8Length, exactKeys, nonnegative, fault} from "./data.mjs";
8
+ import {diff} from "./diff/diff.mjs";
9
+ import {minimalHistorySplice} from "./inverse-source.mjs";
10
+ import {evaluateWill} from "./will.mjs";
11
11
  export {textRoot, rootAfter};
12
12
  const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
13
13
  function* checkedRecords(ledger, prepared, from = null) {
@@ -170,6 +170,7 @@ const expandedRows = new WeakMap();
170
170
  const sameSplices = (a, b) => a === b || a.length === b.length &&
171
171
  a.every((row, i) => row.pos === b[i].pos && row.removed === b[i].removed && row.inserted === b[i].inserted);
172
172
  const sameEffect = (a, b) => a === b || !!a && !!b && canonicalJSON(a) === canonicalJSON(b);
173
+ const RECENT_TEXT_LIMIT = 1 << 19;
173
174
  let recentLedgers = [];
174
175
  function* produceLedger({text, metadata, records, documentAuthority, revision, root, complete = false, earlier = records} = {}, native = false) {
175
176
  if (!_rapierValidMetadata(metadata)) throw fault('document metadata required');
@@ -239,9 +240,10 @@ function* produceLedger({text, metadata, records, documentAuthority, revision, r
239
240
  const replayedMetadata = prepared.replayMetadata(startMetadata);
240
241
  if (!replayedMetadata || canonicalJSON(replayedMetadata) !== canonicalJSON(metadata)) throw fault('metadata does not describe this document');
241
242
  const ledger = {...payload, sha256: yield* checksum(envelopeParts(payload), native)};
242
- recentLedgers = [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata), headBytes, startText, startMetadata: {...startMetadata},
243
- startFacts, startBytes: bytes, documentAuthority, complete: payload.complete, startRoot, startRevision, chainRoot: payload.head.root,
244
- chainRevision: payload.head.revision}, ...recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority)].slice(0, 4);
243
+ const others = recentLedgers.filter(row => row.rows[0] !== expanded[0] && row.documentAuthority === documentAuthority);
244
+ recentLedgers = text.length + startText.length > RECENT_TEXT_LIMIT ? [] : [{rows: expanded, headText: text, headMetadata: canonicalJSON(metadata),
245
+ headBytes, startText, startMetadata: {...startMetadata}, startFacts, startBytes: bytes, documentAuthority, complete: payload.complete,
246
+ startRoot, startRevision, chainRoot: payload.head.root, chainRevision: payload.head.revision}, ...others].slice(0, 4);
245
247
  ownLedger(ledger);
246
248
  proofs.set(ledger, {shell: Array.from(envelopeParts({...ledger, start: {...ledger.start, text: ''}})).join(''), startText,
247
249
  text, metadata: {...metadata}, root, revision, prepared: _rapierPrepareHistoryRecords(ledger.records),
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {_rapierTransformSplices, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierPrepareHistoryRecords} from './journal-records.mjs';
3
- import {projectInverseSource} from './inverse-source.mjs';
4
- import {sourceView, HistorySource} from './source.mjs';
5
- import {textRootParts, rootAfter} from './hash.mjs';
6
- import {authoredHistory, authoredPlacements} from './transport.mjs';
7
- import {plainData, canonicalJSON} from './data.mjs';
8
- import {evaluateWill} from './will.mjs';
2
+ import {_rapierTransformSplices, _rapierValidMetadata, _rapierTransformMetadata, _rapierMetadataDelta, _rapierHistoryEffects, _rapierMetadataState, _rapierPrepareHistoryRecords} from "./journal-records.mjs";
3
+ import {projectInverseSource} from "./inverse-source.mjs";
4
+ import {sourceView, HistorySource} from "./source.mjs";
5
+ import {textRootParts, rootAfter} from "./hash.mjs";
6
+ import {authoredHistory, authoredPlacements} from "./transport.mjs";
7
+ import {plainData, canonicalJSON} from "./data.mjs";
8
+ import {evaluateWill} from "./will.mjs";
9
9
 
10
10
  const historyFailure = reason => ({ok: false, reason});
11
11
  const historyInteger = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
@@ -1,9 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {_rapierTransformSplices, _rapierHistoryEffects} from './journal-records.mjs';
3
- import {HistorySource} from './source.mjs';
4
- import {authoredPlacements} from './authored-origin.mjs';
5
- import {canonicalJSON} from './data.mjs';
6
- import {diff} from './diff/index.mjs';
2
+ import {_rapierTransformSplices, _rapierHistoryEffects} from "./journal-records.mjs";
3
+ import {HistorySource} from "./source.mjs";
4
+ import {authoredPlacements} from "./authored-origin.mjs";
5
+ import {canonicalJSON} from "./data.mjs";
6
+ import {diff} from "./diff/index.mjs";
7
7
 
8
8
  const historyFailure = reason => ({ok: false, reason});
9
9
  const historyBoundary = (source, at) => Number.isSafeInteger(at) && at >= 0 && !Object.is(at, -0) && at <= source.length &&
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- import {fields, scalar, plainPrototype} from './data.mjs';
3
- import {transformText} from './source.mjs';
4
- import {dataImage} from './md-assets.mjs';
2
+ import {fields, scalar, plainPrototype} from "./data.mjs";
3
+ import {transformText} from "./source.mjs";
4
+ import {dataImage} from "./md-assets.mjs";
5
5
 
6
6
  const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
7
7
  const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;
@@ -0,0 +1,38 @@
1
+ // SPDX-License-Identifier: MIT
2
+ export function createMathMatcher(source, {singleDollar = false, brackets = true} = {}) {
3
+ if (typeof source !== 'string') throw new TypeError('Math boundaries require source text.');
4
+ const markers = new Map(), next = Object.create(null);
5
+ let slashes = 0, line = 0;
6
+ for (let at = 0; at < source.length;) {
7
+ const char = source[at];
8
+ if (char === '\\') { slashes++; at++; continue; }
9
+ if (char === '\n' || char === '\r') line++;
10
+ if (char === '$') {
11
+ let end = at + 1;
12
+ while (source[end] === '$') end++;
13
+ const count = end - at;
14
+ if (!(slashes & 1) && (count === 2 || count === 1 && singleDollar))
15
+ markers.set(at, {at, marker: count === 2 ? '$$' : '$', line});
16
+ at = end;
17
+ } else {
18
+ if (brackets && slashes & 1 && '()[]'.includes(char))
19
+ markers.set(at - 1, {at: at - 1, marker: '\\' + char, line});
20
+ at++;
21
+ }
22
+ slashes = 0;
23
+ }
24
+ for (const entry of [...markers.values()].reverse()) {
25
+ const closing = entry.marker === '\\(' ? '\\)' : entry.marker === '\\[' ? '\\]' : entry.marker;
26
+ entry.close = next[closing];
27
+ next[entry.marker] = entry;
28
+ }
29
+ return function match(start, limit = source.length) {
30
+ const open = markers.get(start), close = open?.close;
31
+ if (!close || open.marker === '\\)' || open.marker === '\\]' || close.at + close.marker.length > limit) return null;
32
+ const bodyStart = start + open.marker.length, bodyEnd = close.at;
33
+ if (open.marker === '$' && (open.line !== close.line || bodyStart === bodyEnd ||
34
+ /\s/.test(source[bodyStart]) || /\s/.test(source[bodyEnd - 1]) || /[0-9]/.test(source[bodyEnd + 1] || ''))) return null;
35
+ return {start, end: bodyEnd + close.marker.length, bodyStart, bodyEnd,
36
+ open: open.marker, close: close.marker, displayMode: open.marker === '$$' || open.marker === '\\['};
37
+ };
38
+ }
@@ -0,0 +1,103 @@
1
+ // SPDX-License-Identifier: MIT
2
+ import {createMathMatcher} from "./delimiters.mjs";
3
+
4
+ export function installMathRules(md, {singleDollar = false, brackets = true, ruleName = 'rapier_math', inLink = false} = {}) {
5
+ const authored = new WeakMap();
6
+ md.core.ruler.before('normalize', ruleName + '_source', state => {
7
+ authored.set(state.tokens, {source: state.src});
8
+ });
9
+ md.core.ruler.after('block', ruleName + '_source_release', state => authored.delete(state.tokens));
10
+ const tokenize = md.block.tokenize;
11
+ md.block.tokenize = function(state, startLine, endLine) {
12
+ const first = state.tokens.length;
13
+ try { return tokenize.call(this, state, startLine, endLine); }
14
+ finally {
15
+ for (let index = first; index < state.tokens.length; index++) {
16
+ const token = state.tokens[index];
17
+ if (token.type !== 'fence' || !token.map || token.meta?.mathSource !== undefined ||
18
+ String(token.info || '').trim().split(/\s+/)[0].toLowerCase() !== 'math') continue;
19
+ const held = authored.get(state.tokens), source = held?.source ?? state.src;
20
+ let value;
21
+ if (!state.rapierSourceSlice && source === state.src) value = state.getLines(token.map[0], token.map[1], state.blkIndent, false);
22
+ else {
23
+ if (!state.rapierSourceSlice) {
24
+ const starts = text => [0, ...Array.from(text.matchAll(/\r\n?|\n/g), match => match.index + match[0].length)];
25
+ held.rawLines ||= starts(source);
26
+ held.lines ||= starts(state.src);
27
+ }
28
+ const view = Object.create(state), parts = [];
29
+ view.bMarks = []; view.eMarks = [];
30
+ for (let line = token.map[0]; line < token.map[1]; line++) {
31
+ const shift = state.rapierSourceSlice ? 0 : held.rawLines[line] - held.lines[line];
32
+ view.src = state.rapierSourceSlice
33
+ ? state.rapierSourceSlice(state.bMarks[line], state.eMarks[line])
34
+ : source.slice(state.bMarks[line] + shift, state.eMarks[line] + shift);
35
+ view.bMarks[line] = 0; view.eMarks[line] = view.src.length;
36
+ parts.push(view.getLines(line, line + 1, state.blkIndent, false));
37
+ if (line + 1 < token.map[1]) parts.push(state.rapierSourceSlice
38
+ ? state.rapierSourceSlice(state.eMarks[line], state.eMarks[line] + 1)
39
+ : source.slice(state.eMarks[line] + shift, held.rawLines[line + 1]));
40
+ }
41
+ value = parts.join('');
42
+ }
43
+ token.meta = {...token.meta, mathSource: value};
44
+ }
45
+ }
46
+ };
47
+ const renderText = md.renderer.renderInlineAsText;
48
+ md.renderer.renderInlineAsText = function(tokens, options, env) {
49
+ return renderText.call(this, tokens.map(token =>
50
+ token.type === 'math_inline' || token.type === 'math_block'
51
+ ? {...token, type: 'text', content: token.meta?.mathSource ?? token.content}
52
+ : token), options, env);
53
+ };
54
+ const matchers = new WeakMap();
55
+ function match(state, at, limit) {
56
+ let entry = matchers.get(state);
57
+ if (!entry || entry.source !== state.src) {
58
+ entry = {source: state.src, match: createMathMatcher(state.src, {singleDollar, brackets})};
59
+ matchers.set(state, entry);
60
+ }
61
+ return entry.match(at, limit);
62
+ }
63
+ md.inline.ruler.before('escape', ruleName, (state, silent) => {
64
+ if (!inLink && state.linkLevel > 0) return false;
65
+ if (state.src[state.pos] !== '$' && state.src[state.pos] !== '\\') return false;
66
+ const span = match(state, state.pos, state.posMax);
67
+ if (!span) return false;
68
+ if (!silent) {
69
+ const token = state.push(span.displayMode ? 'math_block' : 'math_inline', '', 0);
70
+ token.markup = span.open;
71
+ token.content = state.src.slice(span.bodyStart, span.bodyEnd);
72
+ token.meta = {mathSource: state.src.slice(span.start, span.end)};
73
+ }
74
+ state.pos = span.end;
75
+ return true;
76
+ });
77
+ md.block.ruler.before('fence', ruleName, (state, startLine, endLine, silent) => {
78
+ if (state.sCount[startLine] - state.blkIndent >= 4) return false;
79
+ const start = state.bMarks[startLine] + state.tShift[startLine];
80
+ if (state.src.slice(start, start + 2) !== '$$' && (!brackets || state.src.slice(start, start + 2) !== '\\[')) return false;
81
+ const span = match(state, start, state.eMarks[endLine - 1]);
82
+ if (!span?.displayMode) return false;
83
+ let last = startLine;
84
+ while (last < endLine && state.eMarks[last] < span.end) last++;
85
+ if (last >= endLine || span.bodyEnd < state.bMarks[last] + state.tShift[last] ||
86
+ state.src.slice(span.end, state.eMarks[last]).trim()) return false;
87
+ if (silent) return true;
88
+ const parts = [];
89
+ for (let line = startLine; line <= last; line++) {
90
+ const from = line === startLine ? span.bodyStart : state.bMarks[line] + state.tShift[line];
91
+ const to = line === last ? span.bodyEnd : state.eMarks[line];
92
+ parts.push(state.src.slice(from, to));
93
+ }
94
+ const token = state.push('math_block', 'math', 0);
95
+ token.block = true;
96
+ token.markup = span.open;
97
+ token.content = parts.join('\n');
98
+ token.meta = {mathSource: span.open + token.content + span.close};
99
+ token.map = [startLine, last + 1];
100
+ state.line = last + 1;
101
+ return true;
102
+ }, {alt: ['paragraph', 'reference', 'blockquote', 'list']});
103
+ }
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
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';
3
+ import {isWordSidecarBlock} from "./word-sidecar.mjs";
4
+ import {sourceFacts, sourceView, sourceEdit} from "./source.mjs";
5
5
 
6
6
  export const IMAGE_LIMITS = Object.freeze({bytes: 16 * 1024 * 1024, sourceChars: 64 * 1024 * 1024, dimension: 16384, pixels: 24000000, assets: 1024});
7
7
  const parsers = new WeakMap();
@@ -9,6 +9,7 @@ const cached = [];
9
9
  let preparedDefinitions = [];
10
10
  let configured = null, splitSource = null;
11
11
  export function configureParser(parser, split) { configured = parser; splitSource = split; cached.length = 0; preparedDefinitions = []; }
12
+ export const configuredMarkdownParser = () => configured;
12
13
  function fail(reason) { throw new Error(reason); }
13
14
  export const normalizeLabel = label => String(label).trim().replace(/\s+/g, ' ').toLowerCase().toUpperCase();
14
15
  export const assetTitle = value => String(value || '').replace(/[\r\n]/g, ' ').replace(/\0/g, '\ufffd');
@@ -0,0 +1,384 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2026 Jack Skipworth. See LICENSE-MIT.
3
+ import {ensureMarkdownParser} from "./md-parser-runtime.mjs";
4
+ import {markdownParser, markdownBodyOffset, documentAssets, parseAssets, projectImageDefinitions, eachBlockWindow} from "./md-assets.mjs";
5
+ import {_rapierTransformSplices as transformSplices} from "./journal-records.mjs";
6
+
7
+ export const MARKER = '<!-- md-comments:v1 ';
8
+ const int = value => Number.isSafeInteger(value) && value >= 0;
9
+ const plain = value => !!value && typeof value === 'object' && !Array.isArray(value);
10
+ const exactKeys = (value, required, optional = []) => plain(value) && required.every(key => Object.hasOwn(value, key)) &&
11
+ Object.keys(value).every(key => required.includes(key) || optional.includes(key));
12
+ const id = value => typeof value === 'string' && value.length <= 128 && /^[A-Za-z0-9_-]+$/.test(value);
13
+ const text = value => typeof value === 'string' && !/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\uD800-\uDFFF]/u.test(value);
14
+
15
+ const FIRST = 16777619, SECOND = 2246822519;
16
+ export const emptyHash = () => ({length: 0, first: 0, second: 0, firstPower: 1, secondPower: 1});
17
+
18
+ export function hashText(text) {
19
+ let first = 0, second = 0, firstPower = 1, secondPower = 1;
20
+ for (let at = 0; at < text.length; at++) {
21
+ const code = text.charCodeAt(at) + 1;
22
+ first = (Math.imul(first, FIRST) + code) >>> 0;
23
+ second = (Math.imul(second, SECOND) + code) >>> 0;
24
+ firstPower = Math.imul(firstPower, FIRST) >>> 0;
25
+ secondPower = Math.imul(secondPower, SECOND) >>> 0;
26
+ }
27
+ return {length: text.length, first, second, firstPower, secondPower};
28
+ }
29
+
30
+ export function foldHash(left, right) {
31
+ return {length: left.length + right.length,
32
+ first: (Math.imul(left.first, right.firstPower) + right.first) >>> 0,
33
+ second: (Math.imul(left.second, right.secondPower) + right.second) >>> 0,
34
+ firstPower: Math.imul(left.firstPower, right.firstPower) >>> 0,
35
+ secondPower: Math.imul(left.secondPower, right.secondPower) >>> 0};
36
+ }
37
+
38
+ export const digest = hash => 'p1:' + hash.length + ':' + hash.first + ':' + hash.second;
39
+ export const commentBodyDigest = text => digest(hashText(text));
40
+ export function validCommentTimestamp(value) {
41
+ return value === null || Number.isSafeInteger(value) && value >= -62167219200000 && value <= 253402300799999;
42
+ }
43
+
44
+ export function validCommentDate(value) {
45
+ if (typeof value !== 'string') return false;
46
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))?$/.exec(value);
47
+ if (!match) return false;
48
+ const [, year, month, day, hour, minute, second, zoneHour, zoneMinute] = match.map((part, index) => index ? Number(part) : part);
49
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
50
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
51
+ return month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1] && hour <= 23 && minute <= 59 && second <= 59 &&
52
+ (match[7] === undefined || zoneHour <= 14 && zoneMinute <= 59 && (zoneHour !== 14 || zoneMinute === 0));
53
+ }
54
+
55
+ export function commentDigest(value) {
56
+ let fnv = 0x811c9dc5, a = 1, b = 0;
57
+ for (let offset = 0; offset < value.length; offset += 4096) {
58
+ const end = Math.min(value.length, offset + 4096);
59
+ for (let index = offset; index < end; index++) {
60
+ const code = value.charCodeAt(index);
61
+ fnv = Math.imul(fnv ^ code, 16777619); a += code; b += a;
62
+ }
63
+ a %= 65521; b %= 65521;
64
+ }
65
+ return `${value.length}:${fnv >>> 0}:${((b << 16) | a) >>> 0}`;
66
+ }
67
+
68
+ export function validAnchor(anchor) {
69
+ if (!exactKeys(anchor, ['kind', 'status'], ['start', 'end', 'exact', 'quote', 'objectId', 'reason'])) return false;
70
+ if (!['document', 'text', 'image', 'drawing'].includes(anchor.kind) || !['attached', 'stale'].includes(anchor.status)) return false;
71
+ if (anchor.kind !== 'document' && (!int(anchor.start) || !int(anchor.end) || anchor.end < anchor.start || !text(anchor.exact) || !text(anchor.quote))) return false;
72
+ return (anchor.objectId === undefined || anchor.kind === 'drawing' && id(anchor.objectId)) &&
73
+ (anchor.reason === undefined || text(anchor.reason));
74
+ }
75
+
76
+ export function validData(data) {
77
+ if (!exactKeys(data, ['body', 'threads']) || !text(data.body) || !Array.isArray(data.threads)) return false;
78
+ const threadIds = new Set(), messageIds = new Set();
79
+ return data.threads.every(thread => {
80
+ if (!exactKeys(thread, ['id', 'anchor', 'resolved', 'messages']) || !id(thread.id) || threadIds.has(thread.id) ||
81
+ !validAnchor(thread.anchor) || typeof thread.resolved !== 'boolean' || !Array.isArray(thread.messages) || !thread.messages.length) return false;
82
+ threadIds.add(thread.id);
83
+ const localMessageIds = new Set(thread.messages.map(message => message?.id));
84
+ const validMessages = thread.messages.every((message, index) => {
85
+ if (!exactKeys(message, ['id', 'text', 'author', 'createdAt'], ['recipient', 'date', 'replyTo']) || !id(message.id) || messageIds.has(message.id) ||
86
+ !text(message.text) || !validCommentTimestamp(message.createdAt) ||
87
+ message.date !== undefined && !validCommentDate(message.date) ||
88
+ !exactKeys(message.author, ['kind', 'name']) ||
89
+ !['human', 'agent', 'system'].includes(message.author.kind) || !text(message.author.name) ||
90
+ (message.recipient !== undefined && !text(message.recipient)) || (message.replyTo !== undefined &&
91
+ (!index || !id(message.replyTo) || message.replyTo === message.id || !localMessageIds.has(message.replyTo)))) return false;
92
+ messageIds.add(message.id); return true;
93
+ });
94
+ if (!validMessages) return false;
95
+ const parents = new Map(thread.messages.map(message => [message.id, message.replyTo])), finished = new Set();
96
+ for (const message of thread.messages) {
97
+ const seen = new Set(); let at = message.id;
98
+ while (at !== undefined && !finished.has(at)) {
99
+ if (seen.has(at)) return false;
100
+ seen.add(at); at = parents.get(at);
101
+ }
102
+ for (const id of seen) finished.add(id);
103
+ }
104
+ return true;
105
+ });
106
+ }
107
+
108
+ export function commentProjectionParts(raw) {
109
+ const opening = /^(<!-- md-comments:v1 [^\r\n]*? -->)(?=\r|\n|$)/.exec(raw)?.[1];
110
+ if (!opening) return null;
111
+ let data;
112
+ try { data = JSON.parse(opening.slice(MARKER.length, -4)); } catch { return null; }
113
+ if (!validData(data)) return null;
114
+ if (opening !== raw) return null;
115
+ return {parts: [opening]};
116
+ }
117
+
118
+ export function commentSourceRange(anchor, parsed) {
119
+ if (anchor.kind === 'document' || anchor.status !== 'attached') return null;
120
+ const at = (value, start) => parsed.record && (value > parsed.record.start || start && value === parsed.record.start) ? value + parsed.record.raw.length : value;
121
+ return {start: at(anchor.start, true), end: at(anchor.end, false)};
122
+ }
123
+
124
+ export function commentAnchor(kind, start, end, source, parsed, objectId) {
125
+ if (kind === 'document') return {kind, status: 'attached'};
126
+ const record = parsed.record;
127
+ if (!int(start) || !int(end) || end <= start || end > source.length ||
128
+ record && start < record.end && record.start < end) throw new TypeError('comment_anchor_invalid');
129
+ const at = value => record && value >= record.end ? value - record.raw.length : value;
130
+ const selected = source.slice(start, end);
131
+ return {kind, status: 'attached', start: at(start), end: at(end), exact: commentDigest(selected),
132
+ quote: Array.from(selected).slice(0, 256).join(''), ...(objectId ? {objectId} : {})};
133
+ }
134
+
135
+ export function serializeComments(threads, body) {
136
+ return encodeCommentRecord(threads, commentBodyDigest(body));
137
+ }
138
+
139
+ export function transport(anchor, row) {
140
+ if (anchor.kind === 'document' || anchor.status !== 'attached' &&
141
+ !(anchor.kind === 'drawing' && anchor.reason === 'object_missing')) return anchor;
142
+ const end = row.pos + row.removed.length, delta = row.inserted.length - row.removed.length;
143
+ if (end <= anchor.start) return {...anchor, start: anchor.start + delta, end: anchor.end + delta};
144
+ if (row.pos >= anchor.end) return anchor;
145
+ if (anchor.kind === 'drawing' && row.pos === anchor.start && end === anchor.end && row.inserted) {
146
+ return {...anchor, end: anchor.start + row.inserted.length, exact: commentDigest(row.inserted),
147
+ quote: Array.from(row.inserted).slice(0, 256).join('')};
148
+ }
149
+ return {...anchor, status: 'stale', reason: row.inserted ? 'target_changed' : 'target_deleted'};
150
+ }
151
+
152
+ function carryDefinitions(source, blocks, splices) {
153
+ return blocks.flatMap(block => {
154
+ let {start, end} = block;
155
+ for (const row of splices) {
156
+ const stop = row.pos + row.removed.length;
157
+ if (row.pos <= end && stop >= start) return [];
158
+ if (stop < start) { start += row.inserted.length - row.removed.length; end += row.inserted.length - row.removed.length; }
159
+ }
160
+ if (source.slice(start, end) !== block.source) return [];
161
+ const moved = {...block, start, end};
162
+ for (const key of ['urlStart', 'urlEnd', 'payloadStart', 'payloadEnd']) if (block[key] != null) moved[key] += start - block.start;
163
+ return [moved];
164
+ });
165
+ }
166
+
167
+ export function encodeCommentValue(data) {
168
+ return JSON.stringify(data).replace(/"(?:[^"\\]|\\.)*"/g, value => value.replace(/[<>&-]/g,
169
+ character => '\\u' + character.charCodeAt(0).toString(16).padStart(4, '0')));
170
+ }
171
+
172
+ export function encodeCommentRecord(threads, digest) {
173
+ const data = {body: digest, threads};
174
+ if (!validData(data)) throw new TypeError('comments_data_invalid');
175
+ return MARKER + encodeCommentValue(data) + ' -->';
176
+ }
177
+
178
+ export function freezeCommentRecord(parsed) {
179
+ for (const thread of parsed.threads) {
180
+ Object.freeze(thread.anchor);
181
+ for (const message of thread.messages) { Object.freeze(message.author); Object.freeze(message); }
182
+ Object.freeze(thread.messages); Object.freeze(thread);
183
+ }
184
+ Object.freeze(parsed.threads);
185
+ if (parsed.record) Object.freeze(parsed.record);
186
+ return Object.freeze(parsed);
187
+ }
188
+
189
+ export function createCommentSourceOwner({readDrawing} = {}) {
190
+ const memos = [], MEMOS = 3;
191
+ const remembered = source => memos.find(row => row.source === source)?.parsed;
192
+ const grammar = new WeakMap();
193
+
194
+ function scanComments(source, verifyBody = true, definitions, scopeOffset) {
195
+ const empty = {record: null, threads: [], body: source, current: true};
196
+ if (scopeOffset === undefined && !source.includes('md-comments:')) return empty;
197
+ ensureMarkdownParser();
198
+ if (!definitions) {
199
+ try { definitions = documentAssets(source).blocks; } catch (_) { definitions = []; }
200
+ }
201
+ const offset = scopeOffset ?? markdownBodyOffset(source);
202
+ definitions = definitions.filter(row => row.start >= offset);
203
+ const projected = projectImageDefinitions(source, {blocks: definitions, references: {}});
204
+ const payloadEnds = [], payloadUnits = [];
205
+ for (const row of projected.records) { payloadEnds.push(row.end); payloadUnits.push((payloadUnits.at(-1) || 0) + row.payload.length - 4); }
206
+ const sourceOffset = at => {
207
+ let low = 0, high = payloadEnds.length;
208
+ while (low < high) { const mid = (low + high) >>> 1; if (payloadEnds[mid] <= at) low = mid + 1; else high = mid; }
209
+ return low ? at + payloadUnits[low - 1] : at;
210
+ };
211
+ const parser = markdownParser(), env = {};
212
+ const records = [], blocks = [], referenceRows = [], regions = [], firstDefinitions = [];
213
+ const known = new Map(definitions.map(row => [row.start, row]));
214
+ eachBlockWindow(projected.source, offset, parser, env, ({tokens, lineStart}) => {
215
+ const lineOffset = line => { const at = lineStart(line); return at === undefined ? source.length : sourceOffset(at); };
216
+ for (const token of tokens) {
217
+ const isDefinition = token.type === 'reference_definition';
218
+ if (!token.map || !isDefinition && token.level !== 0) continue;
219
+ const start = lineOffset(token.map[0]), stop = lineOffset(token.map[1]);
220
+ if (isDefinition) {
221
+ let end = stop;
222
+ if (source[end - 1] === '\n') end--;
223
+ if (source[end - 1] === '\r') end--;
224
+ const previous = known.get(start), definition = token.meta.mdImageDefinition;
225
+ let exactReference = definition;
226
+ const projectedURL = previous?.payloadStart != null
227
+ ? previous.url.slice(0, previous.payloadStart - previous.urlStart) + 'AA==' : previous?.url;
228
+ if (previous?.end === end && previous.id === token.meta.label && definition?.href === projectedURL) {
229
+ blocks.push({...previous, topLevel: token.level === 0});
230
+ exactReference = {...definition, href: previous.url};
231
+ if (env.references?.[previous.id] === definition) firstDefinitions.push([previous.id, exactReference]);
232
+ } else {
233
+ for (const row of parseAssets(source.slice(start, end)).blocks) {
234
+ const moved = {...row, start: row.start + start, end: row.end + start};
235
+ for (const key of ['urlStart', 'urlEnd', 'payloadStart', 'payloadEnd']) if (row[key] != null) moved[key] += start;
236
+ blocks.push(moved);
237
+ }
238
+ }
239
+ referenceRows.push({start, label: token.meta.label, definition: exactReference});
240
+ }
241
+ if (token.level !== 0) continue;
242
+ if (token.map[1] > token.map[0]) regions.push({start, stop, type: token.type});
243
+ const match = token.type === 'html_block' && /^(<!-- md-comments:[^\r\n]*? -->)(?:\r\n|\n|\r)?$/.exec(source.slice(start, stop));
244
+ if (match) records.push({start, end: start + match[1].length, raw: match[1]});
245
+ }
246
+ }, true);
247
+ const references = Object.assign(Object.create(null), env.references);
248
+ for (const [label, definition] of firstDefinitions) references[label] = {...definition};
249
+ if (!regions.length || regions[0].start > 0) regions.unshift({start: 0, stop: offset, type: 'trivia'});
250
+ let recordAt = 0, rowAt = 0;
251
+ const scopes = regions.map((region, index) => {
252
+ const next = regions[index + 1]?.start ?? source.length, defined = [];
253
+ while (recordAt < records.length && records[recordAt].start < region.start) recordAt++;
254
+ for (; rowAt < referenceRows.length && referenceRows[rowAt].start < next; rowAt++)
255
+ if (referenceRows[rowAt].start >= region.start) defined.push([referenceRows[rowAt].label, referenceRows[rowAt].definition]);
256
+ return {length: next - region.start, parsedLength: region.stop - region.start, type: region.type,
257
+ record: records[recordAt]?.start === region.start ? records[recordAt].raw : null, references: defined};
258
+ });
259
+ const result = value => { grammar.set(value, {blocks, references, scopes, offset}); return value; };
260
+ if (!records.length) return result(empty);
261
+ if (records.length !== 1) return result({...empty, reason: 'comments_ambiguous'});
262
+ const record = records[0];
263
+ if (!record.raw.startsWith(MARKER)) return result({...empty, reason: 'comments_format_unknown'});
264
+ let data;
265
+ try { data = JSON.parse(record.raw.slice(MARKER.length, -4)); } catch { return result({...empty, reason: 'comments_malformed'}); }
266
+ if (!validData(data)) return result({...empty, reason: 'comments_malformed'});
267
+ const body = source.slice(0, record.start) + source.slice(record.end);
268
+ return result({record, threads: data.threads, body, current: verifyBody && data.body === commentBodyDigest(body)});
269
+ }
270
+
271
+ function remember(source, parsed, context = grammar.get(parsed)) {
272
+ freezeCommentRecord(parsed);
273
+ if (context) grammar.set(parsed, context);
274
+ const stale = memos.findIndex(row => row.source === source);
275
+ if (stale >= 0) memos.splice(stale, 1);
276
+ memos.unshift({source, parsed});
277
+ if (memos.length > MEMOS) memos.pop();
278
+ return parsed;
279
+ }
280
+
281
+ function parseComments(source) {
282
+ return remembered(source) || remember(source, scanComments(source));
283
+ }
284
+
285
+ function objectPresent(source, anchor, parsed, context) {
286
+ if (anchor.kind !== 'drawing' && anchor.kind !== 'image') return true;
287
+ const range = commentSourceRange(anchor, parsed), occurrence = source.slice(range.start, range.end);
288
+ const parser = markdownParser(), assets = context || grammar.get(parsed) || grammar.get(remembered(source)) || documentAssets(source), tokens = [];
289
+ parser.inline.parse(occurrence, parser, {references: assets.references}, tokens);
290
+ if (tokens.length !== 1 || tokens[0].type !== 'image' || tokens[0].meta?.mdImage?.source !== occurrence) return false;
291
+ if (anchor.kind === 'image') return true;
292
+ const url = tokens[0].attrGet('src');
293
+ if (!/^data:image\/svg\+xml(?:;base64)?,/i.test(url || '')) return false;
294
+ let recipe;
295
+ try { recipe = readDrawing?.(url); } catch { return false; }
296
+ return !!recipe && (!anchor.objectId || recipe.shapes.some(shape => shape.id === anchor.objectId));
297
+ }
298
+
299
+ function commentThreads(source, parsed = parseComments(source)) {
300
+ return parsed.threads.map(thread => {
301
+ let anchor = {...thread.anchor};
302
+ if (anchor.kind !== 'document' && anchor.status === 'attached') {
303
+ const reason = !parsed.current ? 'source_changed_without_history' :
304
+ commentDigest(parsed.body.slice(anchor.start, anchor.end)) !== anchor.exact ? 'target_changed' :
305
+ !objectPresent(source, anchor, parsed) ? 'object_missing' : '';
306
+ if (reason) anchor = {...anchor, status: 'stale', reason};
307
+ }
308
+ return {...thread, anchor, messages: thread.messages.slice()};
309
+ });
310
+ }
311
+
312
+ function writeComments(source, threads, parsed = parseComments(source)) {
313
+ if (parsed.reason) throw new TypeError(parsed.reason);
314
+ if (parsed.record) return {pos: parsed.record.start, removed: parsed.record.raw, inserted: serializeComments(threads, parsed.body)};
315
+ const eol = /\r\n|\n|\r/.exec(source)?.[0] || '\n';
316
+ const prefix = !source || /(?:\r\n|\n|\r){2}$/.test(source) ? '' : /[\r\n]$/.test(source) ? eol : eol + eol;
317
+ const body = source + prefix + eol;
318
+ const inserted = prefix + serializeComments(threads, body) + eol;
319
+ const next = parseComments(source + inserted);
320
+ if (!next.record || next.reason) throw new TypeError('comments_appendix_unavailable');
321
+ return {pos: source.length, removed: '', inserted};
322
+ }
323
+
324
+ function commentSplices(before, splices) {
325
+ if (!before.includes(MARKER)) return [];
326
+ const parsed = parseComments(before);
327
+ if (!parsed.record || parsed.reason) return [];
328
+ if (!readDrawing && parsed.threads.some(thread => thread.anchor.kind === 'drawing')) return [];
329
+ let position = parsed.record.start, end = parsed.record.end;
330
+ const bodyRows = [];
331
+ for (const row of splices) {
332
+ const stop = row.pos + row.removed.length;
333
+ if (row.pos < end && stop > position || row.pos > position && row.pos < end) return [];
334
+ bodyRows.push({...row, pos: row.pos >= end ? row.pos - parsed.record.raw.length : row.pos});
335
+ if (stop <= position) { position += row.inserted.length - row.removed.length; end += row.inserted.length - row.removed.length; }
336
+ }
337
+ const after = transformSplices(before, splices);
338
+ if (after == null) return [];
339
+ const next = scanComments(after, false, carryDefinitions(after, grammar.get(parsed)?.blocks || [], splices));
340
+ if (!next.record || next.reason || next.record.start !== position || next.record.raw !== parsed.record.raw) return [];
341
+ const threads = commentThreads(before, parsed).map(thread => ({...thread,
342
+ anchor: bodyRows.reduce(transport, thread.anchor)}));
343
+ for (const thread of threads) if (thread.anchor.status === 'attached' && !objectPresent(after, thread.anchor, next)) {
344
+ thread.anchor = {...thread.anchor, status: 'stale', reason: 'object_missing'};
345
+ }
346
+ const row = writeComments(after, threads, next);
347
+ if (row.inserted !== row.removed) {
348
+ const final = after.slice(0, row.pos) + row.inserted + after.slice(row.pos + row.removed.length);
349
+ const context = grammar.get(next);
350
+ remember(final, {...next, current: true, threads,
351
+ record: {...next.record, raw: row.inserted, end: row.pos + row.inserted.length}},
352
+ context && {...context, blocks: carryDefinitions(final, context.blocks, [row])});
353
+ }
354
+ return row.inserted === row.removed ? [] : [row];
355
+ }
356
+
357
+ function derivedCommentEdits(base, edits) {
358
+ const parsed = parseComments(base), submitted = transformSplices(base, edits);
359
+ if (parsed.reason || submitted === null) return null;
360
+ if (!parsed.record) return parseComments(submitted).record ? null : {edits, restore: []};
361
+ let start = parsed.record.start, end = parsed.record.end;
362
+ const body = [];
363
+ for (const row of edits) {
364
+ const stop = row.pos + row.removed.length, delta = row.inserted.length - row.removed.length;
365
+ if (row.pos < end && stop > start || row.pos > start && row.pos < end) {
366
+ if (row.pos < start || stop > end) return null;
367
+ end += delta;
368
+ } else {
369
+ body.push({...row, pos: row.pos >= end ? row.pos - (end - start) + parsed.record.raw.length : row.pos});
370
+ if (stop <= start) { start += delta; end += delta; }
371
+ }
372
+ }
373
+ const clean = transformSplices(base, body), derived = commentSplices(base, body);
374
+ if (clean === null || transformSplices(clean, derived) !== submitted) return null;
375
+ const restore = derived.slice().reverse().map(row => ({pos: row.pos, removed: row.inserted, inserted: row.removed}));
376
+ return {edits: body, restore};
377
+ }
378
+
379
+ return Object.freeze({scanComments, parseComments, commentThreads, writeComments, commentSplices,
380
+ derivedCommentEdits, objectPresent, context: parsed => grammar.get(parsed)});
381
+ }
382
+
383
+ const portable = createCommentSourceOwner();
384
+ export const {parseComments, commentSplices, derivedCommentEdits} = portable;