rapier-markdown-kit 1.2.11 → 1.2.13

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.
@@ -1,6 +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
3
  import {isWordSidecarBlock} from './word-sidecar.mjs';
4
+ import {sourceFacts, sourceView, sourceEdit} from '../kit/ledger/source.mjs';
4
5
 
5
6
  export const IMAGE_LIMITS = Object.freeze({bytes: 16 * 1024 * 1024, sourceChars: 64 * 1024 * 1024, dimension: 16384, pixels: 24000000, assets: 1024});
6
7
  const parsers = new WeakMap();
@@ -513,17 +514,107 @@ export function retireDeletedImageDefinitions(before, after, splices, factory) {
513
514
  } catch (_) { return []; }
514
515
  }
515
516
 
517
+ const omissionCache = new WeakMap();
518
+ function payloadFacts(text) {
519
+ let bad = 0, equals = 0;
520
+ for (let at = 0; at < text.length; at++) {
521
+ const code = text.charCodeAt(at);
522
+ if (code === 61) equals++;
523
+ else if (!(code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 43 || code === 47)) bad++;
524
+ }
525
+ return {bad, equals};
526
+ }
527
+ function payloadBytes(length, bad, equals, tail) {
528
+ return length % 4 === 0 && !bad && equals <= 2 && length > equals && tail === '='.repeat(equals)
529
+ ? length / 4 * 3 - equals : null;
530
+ }
531
+ function previousOmissions(view, kind) {
532
+ const changes = [];
533
+ let cursor = view, held;
534
+ while (!(held = omissionCache.get(cursor)?.[kind])) {
535
+ const after = cursor, change = sourceEdit(cursor);
536
+ if (!change || changes.length === 64 || !(cursor = change.before.deref())) return new Map();
537
+ changes.push({...change, after});
538
+ }
539
+ let rows = [...held.values()];
540
+ for (const change of changes.reverse()) {
541
+ const end = change.pos + change.removed, delta = change.inserted - change.removed;
542
+ rows = rows.flatMap(row => {
543
+ const permitted = kind === 'asset' ? /^[A-Za-z0-9+/=\\;&%#._~-]*$/
544
+ : row.profile === 'base64' ? /^[A-Za-z0-9+/=\s]*$/ : /^[A-Za-z0-9\-._~!$&*+;=:@/?%]*$/;
545
+ if (change.pos >= row.payloadStart && end <= row.end && permitted.test(change.text)) {
546
+ const next = {...row, end: row.end + delta, chars: row.chars + delta};
547
+ if (kind === 'asset') {
548
+ const was = payloadFacts(change.before.deref().slice(change.pos, end)), now = payloadFacts(change.text);
549
+ next.bad += now.bad - was.bad; next.equals += now.equals - was.equals;
550
+ next.bytes = payloadBytes(next.end - next.payloadStart, next.bad, next.equals,
551
+ next.equals <= 2 ? change.after.slice(next.end - next.equals, next.end) : '');
552
+ }
553
+ return [next];
554
+ }
555
+ if (change.pos > row.end) return [row];
556
+ if (end < row.start) return [{...row, start: row.start + delta, end: row.end + delta,
557
+ ...(row.payloadStart === undefined ? {} : {payloadStart: row.payloadStart + delta})}];
558
+ return [];
559
+ });
560
+ }
561
+ return new Map(rows.map(row => [row.start, row]));
562
+ }
563
+ function dataMatches(source, kind, envelopes = []) {
564
+ const measured = sourceFacts(source), view = sourceView(source), cached = omissionCache.get(view);
565
+ if (cached?.[kind]) return cached[kind];
566
+ const prior = previousOmissions(view, kind), rows = new Map();
567
+ const pattern = kind === 'asset' ? /data:(image\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\\;&%#._~-]*/iy
568
+ : /data:([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)(?:;[^,\s"'<>)]*)?,/iy;
569
+ let end = 0, envelopeIndex = 0;
570
+ for (const at of measured.markers) {
571
+ if (kind === 'asset' && at < end) continue;
572
+ while (envelopeIndex < envelopes.length && envelopes[envelopeIndex].end <= at) envelopeIndex++;
573
+ if (envelopes[envelopeIndex]?.start <= at) continue;
574
+ let row = prior.get(at);
575
+ if (!row) {
576
+ pattern.lastIndex = at;
577
+ const match = pattern.exec(source);
578
+ if (!match) continue;
579
+ if (kind === 'asset') {
580
+ const value = match[0], payload = value.slice(value.indexOf(',') + 1);
581
+ const {bad, equals} = payloadFacts(payload);
582
+ const bytes = payloadBytes(payload.length, bad, equals, equals <= 2 ? payload.slice(payload.length - equals) : '');
583
+ row = {start: at, end: pattern.lastIndex, chars: value.length,
584
+ domain: 'image_bytes', reason: 'embedded_asset', profile: 'embedded', type: match[1].toLowerCase(), bytes,
585
+ payloadStart: pattern.lastIndex - payload.length, bad, equals};
586
+ } else {
587
+ const payloadStart = pattern.lastIndex, base64 = /;base64,/i.test(match[0]);
588
+ let to = payloadStart;
589
+ const permitted = base64 ? /[A-Za-z0-9+/=\s]/ : /[A-Za-z0-9\-._~!$&*+;=:@/?%]/;
590
+ while (to < source.length && permitted.test(source[to])) to++;
591
+ row = {start: at, end: to, payloadStart, type: match[1], chars: to - at, profile: base64 ? 'base64' : 'data'};
592
+ }
593
+ Object.freeze(row);
594
+ }
595
+ rows.set(at, row); end = row.end;
596
+ }
597
+ omissionCache.set(view, {...cached, [kind]: rows});
598
+ return rows;
599
+ }
516
600
  export function assetOmissions(source) {
517
601
  if (typeof source !== 'string') return [];
518
- const pattern = /data:(image\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\\;&%#._~-]*/ig, spans = [];
519
- for (let match; (match = pattern.exec(source));) {
520
- const value = match[0], payload = value.slice(value.indexOf(',') + 1);
521
- const bytes = payload.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(payload)
522
- ? payload.length / 4 * 3 - (payload.endsWith('==') ? 2 : payload.endsWith('=') ? 1 : 0) : null;
523
- spans.push({start: match.index, end: pattern.lastIndex, chars: value.length,
524
- domain: 'image_bytes', reason: 'embedded_asset', profile: 'embedded', type: match[1].toLowerCase(), bytes});
602
+ return [...dataMatches(source, 'asset').values()].map(({payloadStart, bad, equals, ...row}) => row);
603
+ }
604
+
605
+ export function dataOmissions(source) {
606
+ const envelopes = assetOmissions(source), spans = [...envelopes];
607
+ let envelopeIndex = 0, end = 0;
608
+ for (const row of dataMatches(source, 'generic', envelopes).values()) {
609
+ if (row.start < end) continue;
610
+ while (envelopeIndex < envelopes.length && envelopes[envelopeIndex].end <= row.start) envelopeIndex++;
611
+ const envelope = envelopes[envelopeIndex];
612
+ if (envelope && envelope.start <= row.start) { end = envelope.end; continue; }
613
+ const {payloadStart, ...span} = row;
614
+ if (row.end > payloadStart) spans.push(span);
615
+ end = row.end;
525
616
  }
526
- return spans;
617
+ return spans.sort((a, b) => a.start - b.start);
527
618
  }
528
619
  export function isAssetBlock(raw) {
529
620
  if (isWordSidecarBlock(raw)) return true;
@@ -1,11 +1,22 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
 
3
- export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'marker', 'lock', 'hyphens', 'caps', 'hanging']);
3
+ export const fields = new Set(['align', 'width', 'lines', 'wrap', 'x', 'y', 'rotate', 'opacity', 'first', 'indent', 'marker', 'lock', 'hyphens', 'caps', 'hanging', 'before', 'after', 'line']);
4
+ export const SPACING_FIELDS = Object.freeze(['before', 'after', 'line']);
4
5
  export const alignments = new Set(['left', 'center', 'right', 'justify']);
5
6
  export const wraps = new Set(['around', 'box', 'behind', 'front']);
6
7
  const has = (value, key) => Object.hasOwn(value, key);
7
8
  const percent = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100;
8
9
  const lineCount = value => Number.isInteger(value) && value >= 1 && value <= 12;
10
+ const hundredths = value => Math.abs(value * 100 - Math.round(value * 100)) < 1e-9;
11
+ const spaceValue = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 720 && hundredths(value);
12
+ const linePoint = value => {
13
+ if (typeof value !== 'string') return null;
14
+ const match = /^(?:0|[1-9]\d{0,2})(?:\.\d{1,2})?pt(\+)?$/.exec(value);
15
+ if (!match) return null;
16
+ const points = Number(value.slice(0, match[1] ? -3 : -2));
17
+ return points > 0 && points <= 720 && hundredths(points) && value === decimal(points) + (match[1] ? 'pt+' : 'pt') ? {points, atLeast: !!match[1]} : null;
18
+ };
19
+ const lineValue = value => typeof value === 'number' ? Number.isFinite(value) && value >= .5 && value <= 4 && hundredths(value) : !!linePoint(value);
9
20
  export const INDENT_LIMIT = 64;
10
21
  export const indentLevel = value => typeof value === 'number' && Number.isFinite(value) && value !== 0 &&
11
22
  Math.abs(value) <= INDENT_LIMIT && Number(value.toFixed(6)) === value;
@@ -34,6 +45,8 @@ export function validLayout(value) {
34
45
  (!has(value, 'hyphens') || value.hyphens === 'auto') &&
35
46
  (!has(value, 'caps') || value.caps === 'small') &&
36
47
  (!has(value, 'hanging') || value.hanging === 'on') &&
48
+ (!has(value, 'before') || spaceValue(value.before)) && (!has(value, 'after') || spaceValue(value.after)) &&
49
+ (!has(value, 'line') || lineValue(value.line)) &&
37
50
  !(has(value, 'align') && (has(value, 'wrap') || has(value, 'x')));
38
51
  }
39
52
 
@@ -68,6 +81,15 @@ export function parseLayout(comment) {
68
81
  } else if (key === 'lines') {
69
82
  if (!/^[1-9]\d?$/.test(raw)) return null;
70
83
  value.lines = Number(raw);
84
+ } else if (key === 'before' || key === 'after') {
85
+ if (!/^(?:0|[1-9]\d{0,2})(?:\.\d{1,2})?pt$/.test(raw)) return null;
86
+ value[key] = Number(raw.slice(0, -2));
87
+ } else if (key === 'line') {
88
+ if (/^[0-4](?:\.\d{1,2})?$/.test(raw)) value.line = Number(raw);
89
+ else if (/^(?:0|[1-9]\d{0,2})(?:\.\d{1,2})?pt\+?$/.test(raw)) {
90
+ const plus = raw.endsWith('+');
91
+ value.line = decimal(Number(raw.slice(0, plus ? -3 : -2))) + (plus ? 'pt+' : 'pt');
92
+ } else return null;
71
93
  } else if (key === 'first' || key === 'indent' || key === 'marker') {
72
94
  if (!/^-?(?:0|[1-9]\d?)(?:\.\d{1,6})?$/.test(raw)) return null;
73
95
  value[key] = Number(raw);
@@ -94,19 +116,47 @@ export function textStyle(value, {portable = false, pointSize, indentOffset = 0}
94
116
  'calc(' + measure + ' * var(--md-text-body))');
95
117
  });
96
118
  if (portable && value.hyphens === 'auto') css.push('hyphens:auto');
119
+ if (has(value, 'line')) {
120
+ const point = linePoint(value.line);
121
+ const unit = point && 'calc(' + decimal(Number((point.points / 12).toFixed(6))) + ' * var(--md-unit))';
122
+ css.push('line-height:' + (!point ? decimal(value.line) : portable ? decimal(point.points) + 'pt' : point.atLeast ? 'max(1.2em, ' + unit + ')' : unit));
123
+ }
124
+ for (const [key, prop] of [['before', 'margin-top'], ['after', 'margin-bottom']])
125
+ if (has(value, key)) css.push(portable ? prop + ':' + decimal(value[key]) + 'pt' :
126
+ '--md-' + key + ':calc(' + decimal(Number((value[key] / 12).toFixed(6))) + ' * var(--md-unit))');
97
127
  return css.join(';');
98
128
  }
99
129
 
100
130
  export function textLayout(style, {listMarker = false, pointSize, offset = 0} = {}) {
131
+ const get = property => (typeof style?.getPropertyValue === 'function' ? style.getPropertyValue(property) :
132
+ style?.[property] ?? style?.[property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())] ?? '').trim();
101
133
  const value = {};
102
134
  for (const [key, property] of (listMarker ? [['marker', 'margin-inline-start']] : [['first', 'text-indent'], ['indent', 'margin-inline-start']])) {
103
- const raw = style?.getPropertyValue(property).trim() || '';
135
+ const raw = get(property);
104
136
  const match = /^(-?(?:0|[1-9]\d*)(?:\.\d{1,6})?)(em|pt)$/.exec(raw);
105
137
  const level = Number(((match ? Number(match[1]) / (match[2] === 'pt' ? pointSize : 1) / 2 : 0) +
106
138
  (key === 'first' ? 0 : offset)).toFixed(6));
107
139
  if (Number.isFinite(level) && level) value[key] = level;
108
140
  }
109
- if (!listMarker && style?.getPropertyValue('hyphens').trim() === 'auto') value.hyphens = 'auto';
141
+ if (!listMarker) {
142
+ if (get('hyphens') === 'auto') value.hyphens = 'auto';
143
+ for (const [key, property] of [['before', 'margin-top'], ['after', 'margin-bottom']]) {
144
+ const raw = get(property);
145
+ if (/^(?:0|[1-9]\d{0,2})(?:\.\d{1,2})?pt$/i.test(raw)) {
146
+ const points = Number(raw.slice(0, -2));
147
+ if (spaceValue(points)) value[key] = points;
148
+ }
149
+ }
150
+ const line = get('line-height');
151
+ if (/^[0-4](?:\.\d{1,2})?$/.test(line)) {
152
+ const factor = Number(line);
153
+ if (lineValue(factor)) value.line = factor;
154
+ } else if (/^(?:0|[1-9]\d{0,2})(?:\.\d{1,2})?pt\+?$/i.test(line)) {
155
+ const atLeast = line.endsWith('+'), points = Number(line.slice(0, atLeast ? -3 : -2));
156
+ const formatted = decimal(points) + (atLeast ? 'pt+' : 'pt');
157
+ if (lineValue(formatted)) value.line = formatted;
158
+ }
159
+ }
110
160
  return value;
111
161
  }
112
162
 
@@ -173,7 +223,8 @@ export function formatLayout(value) {
173
223
  if (has(value, key) && !(key === 'y' && value.y === 0) && !(key === 'rotate' && value.rotate === 0) &&
174
224
  !(key === 'opacity' && value.opacity === 100)) pairs.push(key + '=' +
175
225
  (key === 'width' || key === 'x' || key === 'opacity' ? decimal(value[key]) + '%' : key === 'y' ? decimal(value.y) + 'em' :
176
- key === 'rotate' ? decimal(value.rotate) + 'deg' : value[key]));
226
+ key === 'rotate' ? decimal(value.rotate) + 'deg' : key === 'before' || key === 'after' ? decimal(value[key]) + 'pt' :
227
+ key === 'line' ? (typeof value.line === 'number' ? decimal(value.line) : value.line) : value[key]));
177
228
  }
178
229
  return pairs.length ? '<!--md-layout:v1 ' + pairs.join(' ') + '-->' : '';
179
230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "description": "Read, render and write Self-contained Markdown: one .md file that carries its pictures, drawings and layout, and that any Markdown app can read. HTML renderer, stylesheet, text layout, Will/1 agent permissions, and an edit history with authorship and offline merge. MIT, no dependencies.",
5
5
  "keywords": [
6
6
  "markdown",
package/style.css CHANGED
@@ -219,6 +219,30 @@
219
219
  at admission; only renderer paint omissions are corrected here. */
220
220
  :where(.md-render) svg.rapier-native-flowchart{display:block;margin-inline:auto;height:auto}
221
221
  :where(.md-render) svg.rapier-diagram{display:block;margin-inline:auto;height:auto;text-rendering:optimizeLegibility}
222
+ /* The same token bridge serves the document, live preview and portable page. */
223
+ :where(.md-render) :is(.diagram-block,.diagram-live-preview){
224
+ --pele-bg:transparent;
225
+ --pele-fg:var(--color-text,var(--md-color-text));
226
+ --pele-line:var(--color-text,var(--md-color-text));
227
+ --pele-muted:var(--color-text-secondary,var(--md-color-text-secondary));
228
+ --pele-surface:var(--color-surface-2,var(--md-color-surface-2));
229
+ --pele-surface-alt:var(--color-surface-hover,var(--md-color-border-focus));
230
+ --pele-border:var(--color-border,var(--color-surface-hover,var(--md-color-border)));
231
+ --pele-accent:var(--color-accent,var(--md-color-accent));
232
+ --pele-font:var(--font-sans,var(--md-font-sans));
233
+ --pele-font-mono:var(--font-mono,var(--md-font-mono));
234
+ --pele-radius:0;
235
+ --pele-heading-weight:var(--md-fw-semibold);
236
+ --pele-title-weight:var(--md-fw-semibold);
237
+ --pele-series-1:var(--pele-accent);
238
+ --pele-series-2:#4f91f7;
239
+ --pele-series-3:#35c57a;
240
+ --pele-series-4:#e6b91e;
241
+ --pele-series-5:#9b6dea;
242
+ --pele-series-6:#f05a62;
243
+ --pele-series-7:color-mix(in srgb,var(--pele-accent) 50%,#4f91f7);
244
+ --pele-series-8:color-mix(in srgb,#9b6dea 50%,#f05a62);
245
+ }
222
246
 
223
247
  /* Radar gives these SVG labels a color declaration; SVG text needs a fill. */
224
248
  :where(.md-render) svg.rapier-diagram[aria-roledescription="radar"] :is(.radarTitle,.radarAxisLabel){fill:currentColor}
@@ -276,6 +300,8 @@
276
300
  .md-render h6{font-size:var(--md-text-body);font-weight:var(--md-fw-semibold);color:var(--md-heading-muted-color)}
277
301
  .md-render p{margin:0 0 var(--md-gap-paragraph)}
278
302
  .md-render p[data-md-picture]{margin-bottom:var(--md-gap-block)}
303
+ .md-render :is(p,h1,h2,h3,h4,h5,h6)[data-md-before]{--md-before:0px;margin-top:var(--md-before)}
304
+ .md-render :is(p,h1,h2,h3,h4,h5,h6)[data-md-after]{--md-after:0px;margin-bottom:var(--md-after)}
279
305
  .md-render ul,.md-render ol{margin:0 0 var(--md-gap-block);padding-inline-start:var(--md-space-5)}
280
306
  .md-render ul{list-style:none}
281
307
  .md-render ul>li{position:relative;--md-marker:0px}
@@ -426,11 +452,11 @@
426
452
  /* The house focus (rapier-app.css) in the document and on the exported page: the underline thickens, no ring. */
427
453
  .md-render a:focus-visible,.md-render ul.rapier-jump-list>li>a:focus-visible,.md-render .footnote-ref a:focus-visible,.md-render summary:focus-visible{outline:none;text-decoration:underline;text-decoration-thickness:3px;text-underline-offset:.2em}
428
454
 
455
+ /* The title over a table of contents ("Contents", "Table of contents", "Jump to"), as written and bigger than its
456
+ entries. */
429
457
  .md-render .rapier-jump-label{
430
458
  margin:0 0 calc(.5 * var(--md-unit));
431
- font:var(--md-fw-semibold) calc(.72 * var(--md-unit))/1.4 var(--md-font-mono);
432
- letter-spacing:var(--md-track-caps);
433
- text-transform:uppercase;
459
+ font:var(--md-fw-semibold) calc(1.3 * var(--md-unit))/1.3 var(--md-font-sans);
434
460
  color:var(--md-color-text);
435
461
  }
436
462
  .md-render .rapier-jump-label strong{font:inherit}
@@ -459,20 +485,22 @@
459
485
  top:var(--md-jump-circle-top);
460
486
  width:var(--md-jump-circle);height:var(--md-jump-circle);
461
487
  box-sizing:border-box;
462
- border:2px solid var(--md-color-accent-text);
488
+ border:2px solid var(--md-color-line);
463
489
  background:var(--md-color-bg);
464
490
  border-radius:50%;
465
491
  z-index:1;
466
492
  }
467
- /* The dotted line is a numbered list's (3px, the text's black or white): from below this circle to the next circle's
468
- center, which covers its end; none after the last item. --md-item-gap is the gap the spacing settings give. */
493
+ /* The circles and dots wear the text's black or white; only the underline is accent. Three
494
+ 3px dots, evenly spaced, from below this circle to the next circle's top; the line runs on under that circle's
495
+ center, which covers it. None after the last item. --md-item-gap is the gap the spacing settings give. */
469
496
  .md-render ul.rapier-jump-list>li:not(:last-child)::after{
470
497
  content:'';
471
498
  position:absolute;
472
499
  inset-inline-start:calc(var(--md-jump-marker-inline-start) + var(--md-jump-circle) / 2 - 1.5px);
473
500
  top:calc(var(--md-jump-circle-top) + var(--md-jump-circle));
474
501
  bottom:calc(-1 * (var(--md-item-gap, var(--md-jump-gap)) + var(--md-jump-circle-top) + var(--md-jump-circle) / 2));
475
- border-inline-start:3px dotted var(--md-color-line);
502
+ width:3px;
503
+ background:radial-gradient(circle closest-side,var(--md-color-line) 92%,transparent) top/3px calc((100% - var(--md-jump-circle) / 2) / 3) repeat-y;
476
504
  z-index:0;
477
505
  }
478
506
  .md-render ul.rapier-jump-list>li>a{