rapier-markdown-kit 1.2.32 → 1.2.40

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 (45) hide show
  1. package/README.md +16 -10
  2. package/dist/agent/vendor/pretext/NOTICE +2 -2
  3. package/dist/agent/vendor/pretext/SOURCE.json +62 -78
  4. package/dist/agent/vendor/pretext/analysis.js +24 -6
  5. package/dist/agent/vendor/pretext/gecko-line-breaks.js +1 -1
  6. package/dist/agent/vendor/pretext/generated/engine-break-data.js +1 -1
  7. package/dist/agent/vendor/pretext/generated/webkit-generic-families.js +1 -1
  8. package/dist/agent/vendor/pretext/graphemes.js +1 -1
  9. package/dist/agent/vendor/pretext/han-kerning.js +1 -1
  10. package/dist/agent/vendor/pretext/layout.js +14 -11
  11. package/dist/agent/vendor/pretext/line-break.js +170 -195
  12. package/dist/agent/vendor/pretext/line-breaks.js +1 -7
  13. package/dist/agent/vendor/pretext/line-text.js +2 -2
  14. package/dist/agent/vendor/pretext/measurement.js +454 -113
  15. package/dist/agent/vendor/pretext/prepare.js +164 -244
  16. package/dist/agent/vendor/pretext/rich-inline.js +57 -37
  17. package/dist/agent/will.mjs +73 -0
  18. package/dist/kit/ledger/data.mjs +2 -2
  19. package/dist/kit/ledger/format.mjs +86 -42
  20. package/dist/kit/ledger/hash.mjs +43 -2
  21. package/dist/kit/ledger/history.mjs +6 -0
  22. package/dist/kit/ledger/inverse-source.mjs +6 -2
  23. package/dist/kit/ledger/journal-records.mjs +4 -2
  24. package/dist/kit/ledger/merge.mjs +3 -17
  25. package/dist/kit/ledger/text-merge.mjs +191 -28
  26. package/dist/kit/rapier-ledger.mjs +2 -2
  27. package/dist/kit/render-markdown.mjs +133 -10
  28. package/dist/kit/render-print.mjs +3 -3
  29. package/dist/kit/render-sanitize.mjs +66 -23
  30. package/dist/kit/render.mjs +10 -10
  31. package/dist/kit/wiki.mjs +34 -0
  32. package/dist/kit/will.d.mts +3 -0
  33. package/dist/layout/hyphenation.mjs +6 -4
  34. package/dist/layout/line-plan.mjs +96 -0
  35. package/dist/layout/model.mjs +2 -1
  36. package/dist/layout/paragraph-fit.mjs +4 -4
  37. package/dist/layout/prepared-paragraph.mjs +19 -2
  38. package/dist/layout/typography.mjs +57 -63
  39. package/dist/spec/document-fonts.mjs +1 -0
  40. package/dist/spec/document-settings.mjs +1 -0
  41. package/dist/spec/html-blocks.mjs +10 -7
  42. package/dist/spec/md-assets.mjs +52 -12
  43. package/package.json +1 -1
  44. package/style.css +37 -28
  45. package/dist/agent/vendor/pretext/entry-geometry.js +0 -183
@@ -464,6 +464,7 @@ export function createTypography() {
464
464
  function updateProjection(projection, plan) {
465
465
  for (const {node,property,line,sign} of projection.bindings) {
466
466
  const value = sign * plan.lines[line][property] + 'px';
467
+ if (node.hasAttribute(boundary)) { if (node.style.getPropertyValue('--md-fit-protrude') !== value) node.style.setProperty('--md-fit-protrude', value); continue; }
467
468
  const name = property === 'opening' ? 'marginInlineStart' : property === 'closing' ? 'marginInlineEnd' : 'wordSpacing';
468
469
  if (node.style[name] !== value) node.style[name] = value;
469
470
  }
@@ -487,7 +488,7 @@ export function createTypography() {
487
488
  add(line.end);
488
489
  }
489
490
  if (line.kind === 'space') operations.push({start:line.end,end:line.next,boundary:true});
490
- else if (line.kind !== 'end') inserts.push({offset:line.end,glyph:line.glyph});
491
+ else if (line.kind !== 'end') inserts.push({offset:line.end,glyph:line.glyph,line:lineIndex,protrudes:line.closing>0});
491
492
  const first = words[line.firstWord], last = words[line.lastWord];
492
493
  if (first.opening && line.start === first.start) operations.push({start:first.start,end:first.openEnd,opening:true,line:lineIndex});
493
494
  if (last.closing && line.end === last.end) operations.push({start:last.closeStart,end:last.end,closing:true,line:lineIndex});
@@ -538,6 +539,7 @@ export function createTypography() {
538
539
  const at=ends.get(insert.offset);
539
540
  const marker=paragraph.ownerDocument.createElement('span');marker.setAttribute(boundary,insert.glyph?'hyphen':'');
540
541
  if (insert.glyph) marker.style.setProperty('--md-fit-hyphen',JSON.stringify(insert.glyph));
542
+ if (insert.protrudes) bindings.push({node:marker,property:'closing',line:insert.line,sign:-1});
541
543
  at.after(marker);
542
544
  }
543
545
  const projection = {bindings,topology:topology(plan)};
@@ -689,77 +691,69 @@ export function createTypography() {
689
691
  slotPlans.set(paragraph, {key, plan});
690
692
  return plan;
691
693
  }
692
- function slotLines(paragraph, items, options, {width, top, obstacles, lineHeight, minWidth, slotsForBand, justify, hyphenate}) {
694
+ function slotLines(paragraph, items, options, {width, top, obstacles, lineHeight, minWidth, slotsForBand, slotSequence, columnsFilled, justify, hyphenate}) {
693
695
  if (!obstacles.length) return null;
694
696
  const doc = paragraph.ownerDocument, words = scope(doc);
695
697
  const source = items.map(item => ({...item, hyphenate}));
696
698
  const geometry = words.prepare(paragraph, source, {policy: 'paragraph', language: options.language, whiteSpace: 'normal',
697
699
  wordBreak: options.wordBreak, hanging: false, paragraphBreaks: [0, source.length]});
698
700
  if (!geometry) return null;
699
- const sequence = [], bottom = Math.max(...obstacles.map(item => item.y + item.height)) - top;
700
- let y = 0, rows = 0;
701
- while (y < bottom && rows < 512 && sequence.length < 1024) {
702
- const bands = slotsForBand(width, obstacles, top + y, lineHeight, Math.min(minWidth, width));
703
- if (!bands) return null;
704
- if (bands.length) {
705
- for (const band of bands) sequence.push({x: band.x, y, width: band.width});
706
- y += lineHeight; rows++; continue;
701
+ const build = order => {
702
+ const found = slotSequence(width, top, obstacles, lineHeight, minWidth, slotsForBand, 'ltr', order);
703
+ if (!found?.multiple) return null;
704
+ const sequence = found.slots, y = found.end;
705
+ const prepared = fitter.prepare(geometry);
706
+ let slots = sequence, plan = null;
707
+ for (let pass = 0; prepared && pass < 8; pass++) {
708
+ const measures = slots.map(slot => Math.floor(slot.width * 64) / 64);
709
+ if (!measures.length) return null;
710
+ plan = words.fit(paragraph, geometry, known => fitter.solveSteps(prepared, width, {widths: measures, shrink: justify ? undefined : 0}, known));
711
+ if (!plan) return null;
712
+ const wide = new Set();
713
+ plan.lines.forEach((line, index) => {
714
+ if (index >= slots.length || line.firstWord !== line.lastWord || line.kind === 'soft' || line.kind === 'auto') return;
715
+ const word = geometry.segments[line.segment].words[line.firstWord];
716
+ if (line.start === word.start && line.end === word.end && word.width > measures[index] + .5) wide.add(slots[index]);
717
+ });
718
+ if (!wide.size) break;
719
+ slots = slots.filter(slot => !wide.has(slot)); plan = null;
707
720
  }
708
- let next = Infinity;
709
- for (const obstacle of obstacles) if (obstacle.x < width && obstacle.x + obstacle.width > 0 &&
710
- obstacle.y < top + y + lineHeight && obstacle.y + obstacle.height > top + y) next = Math.min(next, obstacle.y + obstacle.height - top);
711
- if (!Number.isFinite(next) || next <= y) return null;
712
- y = next;
713
- }
714
- const prepared = fitter.prepare(geometry);
715
- let slots = sequence, plan = null;
716
- for (let pass = 0; prepared && pass < 8; pass++) {
717
- const measures = slots.map(slot => Math.floor(slot.width * 64) / 64);
718
- if (!measures.length) return null;
719
- plan = words.fit(paragraph, geometry, known => fitter.solveSteps(prepared, width, {widths: measures, shrink: justify ? undefined : 0}, known));
720
- if (!plan) return null;
721
- const wide = new Set();
722
- plan.lines.forEach((line, index) => {
723
- if (index >= slots.length || line.firstWord !== line.lastWord || line.kind === 'soft' || line.kind === 'auto') return;
724
- const word = geometry.segments[line.segment].words[line.firstWord];
725
- if (line.start === word.start && line.end === word.end && word.width > measures[index] + .5) wide.add(slots[index]);
726
- });
727
- if (!wide.size) break;
728
- slots = slots.filter(slot => !wide.has(slot)); plan = null;
729
- }
730
- const perRow = new Map();
731
- for (const slot of slots) perRow.set(slot.y, (perRow.get(slot.y) || 0) + 1);
732
- if (!plan || ![...perRow.values()].some(count => count > 1)) return null;
733
- const lines = [], text = geometry.text, offsets = geometry.offsets, space = /^[\t\n\f\r ]*$/;
734
- for (let index = 0; index < plan.lines.length; index++) {
735
- const line = plan.lines[index], slot = sequence[index] || {x: 0, y: y + (index - sequence.length) * lineHeight, width};
736
- const spacing = justify ? line.wordSpacing : 0, fragments = [];
737
- const segment = geometry.segments[line.segment].words.slice(line.firstWord, line.lastWord + 1);
738
- let at = 0, previous = null;
739
- while (at < source.length - 1 && offsets[at + 1] <= line.start) at++;
740
- for (; at < source.length && offsets[at] < line.end; at++) {
741
- let from = Math.max(line.start, offsets[at]), to = Math.min(line.end, offsets[at + 1]);
742
- if (previous) while (from < to && /[\t\n\f\r ]/.test(text[from])) from++;
743
- if (offsets[at + 1] < line.end) while (to > from && /[\t\n\f\r ]/.test(text[to - 1])) to--;
744
- if (to <= from) continue;
745
- const fragment = {itemIndex: at, sourceStart: from - offsets[at], sourceEnd: to - offsets[at], derive: true, occupiedWidth: 0, gapItemIndex: at, gapBefore: 0};
746
- if (previous) {
747
- fragment.gapItemIndex = previous.itemIndex;
748
- if (previous.end < from) {
749
- if (!space.test(text.slice(previous.end, from))) return null;
750
- const before = segment.find(word => word.end === previous.end);
751
- if (!before?.gap) return null;
752
- fragment.gapBefore = before.gap.width + spacing;
753
- } else fragment.gapBefore = 0;
721
+ const perRow = new Map();
722
+ for (const slot of slots) perRow.set(slot.y, (perRow.get(slot.y) || 0) + 1);
723
+ if (!plan || ![...perRow.values()].some(count => count > 1)) return null;
724
+ const lines = [], text = geometry.text, offsets = geometry.offsets, space = /^[\t\n\f\r ]*$/;
725
+ for (let index = 0; index < plan.lines.length; index++) {
726
+ const line = plan.lines[index], slot = sequence[index] || {x: 0, y: y + (index - sequence.length) * lineHeight, width};
727
+ const spacing = justify ? line.wordSpacing : 0, fragments = [];
728
+ const segment = geometry.segments[line.segment].words.slice(line.firstWord, line.lastWord + 1);
729
+ let at = 0, previous = null;
730
+ while (at < source.length - 1 && offsets[at + 1] <= line.start) at++;
731
+ for (; at < source.length && offsets[at] < line.end; at++) {
732
+ let from = Math.max(line.start, offsets[at]), to = Math.min(line.end, offsets[at + 1]);
733
+ if (previous) while (from < to && /[\t\n\f\r ]/.test(text[from])) from++;
734
+ if (offsets[at + 1] < line.end) while (to > from && /[\t\n\f\r ]/.test(text[to - 1])) to--;
735
+ if (to <= from) continue;
736
+ const fragment = {itemIndex: at, sourceStart: from - offsets[at], sourceEnd: to - offsets[at], derive: true, occupiedWidth: 0, gapItemIndex: at, gapBefore: 0};
737
+ if (previous) {
738
+ fragment.gapItemIndex = previous.itemIndex;
739
+ if (previous.end < from) {
740
+ if (!space.test(text.slice(previous.end, from))) return null;
741
+ const before = segment.find(word => word.end === previous.end);
742
+ if (!before?.gap) return null;
743
+ fragment.gapBefore = before.gap.width + spacing;
744
+ } else fragment.gapBefore = 0;
745
+ }
746
+ fragments.push(fragment); previous = {itemIndex: at, end: to};
754
747
  }
755
- fragments.push(fragment); previous = {itemIndex: at, end: to};
748
+ if (!fragments.length) return null;
749
+ if (line.kind === 'soft' || line.kind === 'auto') fragments.at(-1).glyph = line.glyph || '-';
750
+ lines.push({x: slot.x, y: slot.y, width: slot.width, fragments, wordSpacing: spacing});
756
751
  }
757
- if (!fragments.length) return null;
758
- if (line.kind === 'soft' || line.kind === 'auto') fragments.at(-1).glyph = line.glyph || '-';
759
- lines.push({x: slot.x, y: slot.y, width: slot.width, fragments, wordSpacing: spacing});
760
- }
761
- const last = lines.at(-1);
762
- return {lines, height: last ? Math.max(lineHeight, last.y + lineHeight) : lineHeight};
752
+ return {plan: {lines, height: Math.max(lineHeight, ...lines.map(line => line.y + lineHeight))}, filled: columnsFilled(slots, slots.slice(0, plan.lines.length))};
753
+ };
754
+ const columns = build('columns');
755
+ if (!columns || columns.filled) return columns?.plan || null;
756
+ return build('rows')?.plan || columns.plan;
763
757
  }
764
758
 
765
759
  function retire(root, {documentChanged=false}={}) {
@@ -211,6 +211,7 @@ export function documentFontUsage(markdown, parser, defaultFont) {
211
211
  const inherited = defaultFont === undefined ? settings.defaultfont : documentFont(defaultFont);
212
212
  const main = family(settings.mainfont || inherited || settings.sansfont), sans = family(settings.sansfont || settings.mainfont || inherited), mono = family(settings.monofont);
213
213
  const add = (name, text) => { if (name && !Object.hasOwn(FONT_STACKS, name.toLowerCase()) && text) use.set(name, (use.get(name) || '') + text + '\n'); };
214
+ if (!main && !sans && !mono && !source.includes('<')) return use;
214
215
  const plain = html => md.utils.unescapeAll(String(html).replace(/<!--[^]*?-->/g, '').replace(/<(script|style)\b[^>]*>[^]*?<\/\1\s*>/gi, '').replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\'));
215
216
  let blockFamily = main;
216
217
  for (const token of md.parse(splitOpeningFrontmatter(source).body, {})) {
@@ -129,6 +129,7 @@ function paperOf(value) {
129
129
  return pixels && width <= 10000 && height <= 10000 ? Object.freeze({name, width: width * 15, height: height * 15, css: width + 'px ' + height + 'px'}) : null;
130
130
  }
131
131
  export const pageSizeOf = settings => settings?.papersize || PAPERS.letter;
132
+ export const paperForLanguage = language => /(?:^|[-_])(?:US|CA|MX|PH|CL|CO|VE|CR|GT)$/i.test(String(language || '')) ? PAPERS.letter : PAPERS.a4;
132
133
  export const PAGE_SIZES = Object.freeze(['letter', 'legal', 'tabloid', 'a3', 'a4', 'a5', '1080x1080px', '1080x1350px', '1920x1080px', '1200x630px']);
133
134
  const EMPTY = Object.freeze({
134
135
  title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
@@ -425,7 +425,7 @@ export function mountHtmlBlocks(doc, options = {}) {
425
425
  const name = attr.name.toLowerCase(), value = attr.value;
426
426
  if (name.startsWith('on') || name.startsWith('data-rapier-') || ['nonce','srcdoc','is','autofocus','form','formaction','action','target','download','ping','xmlns:xlink'].includes(name)) el.removeAttribute(attr.name);
427
427
  if (['src','srcset','href'].includes(attr.localName.toLowerCase()) && !(name.endsWith('href') && /^#[A-Za-z][\w.-]*$/.test(value) ||
428
- (name === 'src' && tag === 'img' || name.endsWith('href') && tag === 'image') && /^data:image\/(?:png|jpeg|webp|gif|jxl|svg\+xml);/i.test(value))) el.removeAttribute(attr.name);
428
+ (name === 'src' && tag === 'img' || name.endsWith('href') && tag === 'image') && /^data:image\/(?:(?:png|jpeg|webp|gif|jxl|svg\+xml);|svg\+xml,)/i.test(value))) el.removeAttribute(attr.name);
429
429
  }
430
430
  if (tag.startsWith('animate') || tag === 'set') {
431
431
  const attribute = (el.getAttribute('attributeName') || '').trim().toLowerCase().split(':').pop();
@@ -437,13 +437,16 @@ export function mountHtmlBlocks(doc, options = {}) {
437
437
  const name = tag === 'img' ? 'src' : el.hasAttribute('href') ? 'href' : 'xlink:href', original = el.getAttribute(name);
438
438
  if (original) {
439
439
  const match = /^data:(image\/(?:png|jpeg|webp|gif|jxl|svg\+xml));(?:charset=[^;,]+;)?base64,([A-Za-z0-9+/]+={0,2})$/i.exec(original);
440
- if (!match || original.length > 24 * 1024 * 1024) throw new Error('Image requires a bounded base64 data URL');
441
- const url = 'data:' + match[1].toLowerCase() + ';base64,' + match[2];
442
- const bytes = Uint8Array.from(win.atob(match[2]), c => c.charCodeAt(0));
443
- const info = match[1].toLowerCase() === 'image/gif' ? media.inspectGIF(bytes) : imageInfo(url) || admitted[url];
444
- if (!info && match[1].toLowerCase() !== 'image/svg+xml') {el.removeAttribute(name); continue;}
440
+ const percent = /^data:image\/svg\+xml,/i.test(original);
441
+ if ((!match && !percent) || original.length > 24 * 1024 * 1024) throw new Error('Image requires a bounded base64 data URL');
442
+ const mime = percent ? 'image/svg+xml' : match[1].toLowerCase();
443
+ const url = percent ? original : 'data:' + mime + ';base64,' + match[2];
444
+ const bytes = percent ? new win.TextEncoder().encode(decodeURIComponent(original.slice(19)))
445
+ : Uint8Array.from(win.atob(match[2]), c => c.charCodeAt(0));
446
+ const info = mime === 'image/gif' ? media.inspectGIF(bytes) : imageInfo(url) || admitted[url];
447
+ if (!info && mime !== 'image/svg+xml') {el.removeAttribute(name); continue;}
445
448
  if (info) {pixels += info.width * info.height; if (pixels > 24000000) throw new Error('HTML image pixel budget'); admitted[url] = {width: info.width, height: info.height, animated: !!info.animated};}
446
- if (match[1].toLowerCase() === 'image/svg+xml') {
449
+ if (mime === 'image/svg+xml') {
447
450
  const svgText = new win.TextDecoder('utf-8', {fatal: true}).decode(bytes);
448
451
  if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svgText)) throw new Error('SVG declarations are unsupported');
449
452
  const parsed = new win.DOMParser().parseFromString(svgText, 'image/svg+xml');
@@ -17,9 +17,9 @@ export function installMarkdownImages(md) {
17
17
  if (md.__rapierImagesInstalled) return md;
18
18
  md.__rapierImagesInstalled = true;
19
19
  const validate = md.validateLink;
20
- md.validateLink = url => /^data:image\/(?:jxl|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) || validate(url);
20
+ md.validateLink = url => !!dataImage(url) || validate(url);
21
21
  const normalize = md.normalizeLink;
22
- md.normalizeLink = url => /^data:image\/(?:jxl|png|jpeg|webp|gif|svg\+xml);base64,[A-Za-z0-9+/]+={0,2}$/i.test(url) ? url : normalize(url);
22
+ md.normalizeLink = url => dataImage(url) ? url : normalize(url);
23
23
  const reference = md.block.ruler.__rules__.find(row => row.name === 'reference').fn;
24
24
  md.block.ruler.at('reference', (state, start, end, silent) => {
25
25
  if (silent) return reference(state, start, end, silent);
@@ -179,10 +179,35 @@ export function* blockWindows(source, bodyOffset, parser, env, keepNul = false)
179
179
  }
180
180
  }
181
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;
182
186
  export function dataImage(url) {
183
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) {
184
195
  const prepared = preparedDefinitions.find(row => row.image.url === url)?.image;
185
- if (prepared) return {codec: prepared.codec, byteLength: prepared.byteLength, payloadStart: url.indexOf(',') + 1};
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
+ }
186
211
  const match = /^data:(image\/(?:jxl|png|jpeg|webp|gif|svg\+xml));base64,([A-Za-z0-9+/]+={0,2})$/i.exec(url);
187
212
  if (!match || match[2].length % 4) return null;
188
213
  const padding = match[2].endsWith('==') ? 2 : match[2].endsWith('=') ? 1 : 0;
@@ -291,7 +316,8 @@ export function restoreAssetIndex(source, packet, {offset = 0, bodyOffset = 0, f
291
316
  preparedDefinitions = blocks.filter(row => row.topLevel).map(row => {
292
317
  const local = {...row, start: 0, end: row.source.length, active: true, status: 'unverified'};
293
318
  for (const key of positions.slice(2)) if (Number.isSafeInteger(local[key])) local[key] -= row.start;
294
- const image = Object.freeze({url: row.url, codec: row.codec, byteLength: row.byteLength});
319
+ const image = Object.freeze({url: row.url, codec: row.codec, byteLength: row.byteLength,
320
+ ...(row.encoding ? {encoding: row.encoding} : {})});
295
321
  return {source: row.source, factory, image, index: {assets: new Map([[row.id, local]]), blocks: [local],
296
322
  references: Object.assign(Object.create(null), {[row.id]: {href: row.url, title: row.title}}),
297
323
  duplicateLabels: new Set(), appendixStart: 0}};
@@ -314,7 +340,7 @@ export function projectImageDefinitions(source, index) {
314
340
  const references = Object.create(null);
315
341
  for (const [label, definition] of Object.entries(index.references)) {
316
342
  const image = index.assets.get(label);
317
- references[label] = {href: image?.url === definition.href ? 'data:' + image.codec + ';base64,AA==' : definition.href,
343
+ references[label] = {href: image?.url === definition.href ? image.url.slice(0, image.url.indexOf(',') + 1) + 'AA==' : definition.href,
318
344
  title: definition.title};
319
345
  }
320
346
  return {source: records.length ? parts.join('') : source, records, references};
@@ -513,12 +539,17 @@ export function retireDeletedImageDefinitions(before, after, splices, factory) {
513
539
  !remaining.includes('[' + row.id) && !now.html.includes(row.id) && !now.html.includes(row.url.toUpperCase());
514
540
  });
515
541
  return retired.sort((a, b) => b.start - a.start).map(row => {
516
- const gap = /(?:\r\n\r\n|\n\n|\r\r)$/.exec(after.slice(Math.max(0, row.start - 4), row.start));
542
+ const gap = /(?:\r\n|\r(?!\n)|(?<!\r)\n){2}$/.exec(after.slice(Math.max(0, row.start - 5), row.start));
517
543
  let tail = after.slice(row.end);
518
544
  const last = /^(?:\r\n|\n|\r)?$/.test(tail);
519
545
  for (const other of retired) if (other.start >= row.end) tail = tail.replace(other.source, '');
520
546
  const whole = gap && !/\S/.test(tail) && row.start - gap[0].length > markdownBodyOffset(after);
521
- return whole ? {pos: row.start - gap[0].length, removed: gap[0] + row.source + (last ? after.slice(row.end) : ''), inserted: ''} : {pos: row.start, removed: row.source, inserted: ''};
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: ''};
522
553
  });
523
554
  } catch (_) { return []; }
524
555
  }
@@ -551,7 +582,7 @@ function previousOmissions(view, kind) {
551
582
  rows = rows.flatMap(row => {
552
583
  const permitted = kind === 'asset' ? /^[A-Za-z0-9+/=\\;&%#._~-]*$/
553
584
  : row.profile === 'base64' ? /^[A-Za-z0-9+/=\s]*$/ : /^[A-Za-z0-9\-._~!$&*+;=:@/?%]*$/;
554
- if (change.pos >= row.payloadStart && end <= row.end && permitted.test(change.text)) {
585
+ if (!row.percent && change.pos >= row.payloadStart && end <= row.end && permitted.test(change.text)) {
555
586
  const next = {...row, end: row.end + delta, chars: row.chars + delta};
556
587
  if (kind === 'asset') {
557
588
  const was = payloadFacts(change.before.deref().slice(change.pos, end)), now = payloadFacts(change.text);
@@ -575,6 +606,7 @@ function dataMatches(source, kind, envelopes = []) {
575
606
  const prior = previousOmissions(view, kind), rows = new Map();
576
607
  const pattern = kind === 'asset' ? /data:(image\/[a-z0-9.+-]+);base64,[A-Za-z0-9+/=\\;&%#._~-]*/iy
577
608
  : /data:([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)(?:;[^,\s"'<>)]*)?,/iy;
609
+ const svgPattern = /data:(image\/svg\+xml),[A-Za-z0-9%\-._~:/?@+$,;=]*/iy;
578
610
  let end = 0, envelopeIndex = 0;
579
611
  for (const at of measured.markers) {
580
612
  if (kind === 'asset' && at < end) continue;
@@ -583,15 +615,21 @@ function dataMatches(source, kind, envelopes = []) {
583
615
  let row = prior.get(at);
584
616
  if (!row) {
585
617
  pattern.lastIndex = at;
586
- const match = pattern.exec(source);
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
+ }
587
625
  if (!match) continue;
588
626
  if (kind === 'asset') {
589
627
  const value = match[0], payload = value.slice(value.indexOf(',') + 1);
590
628
  const {bad, equals} = payloadFacts(payload);
591
- const bytes = payloadBytes(payload.length, bad, equals, equals <= 2 ? payload.slice(payload.length - equals) : '');
629
+ const bytes = percent ? dataImage(value)?.byteLength ?? null : payloadBytes(payload.length, bad, equals, equals <= 2 ? payload.slice(payload.length - equals) : '');
592
630
  row = {start: at, end: pattern.lastIndex, chars: value.length,
593
631
  domain: 'image_bytes', reason: 'embedded_asset', profile: 'embedded', type: match[1].toLowerCase(), bytes,
594
- payloadStart: pattern.lastIndex - payload.length, bad, equals};
632
+ payloadStart: pattern.lastIndex - payload.length, bad, equals, ...(percent ? {percent} : {})};
595
633
  } else {
596
634
  const payloadStart = pattern.lastIndex, base64 = /;base64,/i.test(match[0]);
597
635
  let to = payloadStart;
@@ -608,7 +646,7 @@ function dataMatches(source, kind, envelopes = []) {
608
646
  }
609
647
  export function assetOmissions(source) {
610
648
  if (typeof source !== 'string') return [];
611
- return [...dataMatches(source, 'asset').values()].map(({payloadStart, bad, equals, ...row}) => row);
649
+ return [...dataMatches(source, 'asset').values()].map(({payloadStart, bad, equals, percent, ...row}) => row);
612
650
  }
613
651
 
614
652
  export function dataOmissions(source) {
@@ -667,3 +705,5 @@ export function appendAssetText(source, asset, lineEnding, {preserveLabel = fals
667
705
  return {source: nextSource, id: asset.id, reference: asset.label, added: true, suffix};
668
706
  }
669
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.32",
3
+ "version": "1.2.40",
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
@@ -142,7 +142,7 @@
142
142
  .md-render p[data-rapier-typography-fit]{text-align:justify!important;text-align-last:start;text-wrap:nowrap;hyphens:none;hanging-punctuation:none}
143
143
  .md-render p[data-rapier-typography-fit] :is(a,span,em,i,strong,b,del,s,mark,code,kbd,samp,u,ins,abbr){text-wrap:nowrap;hyphens:none;hanging-punctuation:none}
144
144
  .md-render [data-rapier-typography-separator]{font-size:0;line-height:0;word-spacing:0!important;letter-spacing:0!important}
145
- .md-render [data-rapier-typography-break="hyphen"]::before{content:var(--md-fit-hyphen,'-')}
145
+ .md-render [data-rapier-typography-break="hyphen"]::before{content:var(--md-fit-hyphen,'-');margin-inline-end:var(--md-fit-protrude,0px)}
146
146
  .md-render p[data-rapier-typography-ragged]{text-align:start!important}
147
147
  .md-render p[data-md-lines]:not([data-rapier-typography-protected-initial])::first-letter{
148
148
  initial-letter:var(--md-drop-lines,normal);
@@ -219,29 +219,31 @@
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. */
222
+ /* The same token bridge serves the document, live preview and portable page. Pele reads --pele-* names; the
223
+ adapter (_presentPele in shell/plugin-loader.js) points them at these --md-pele-* names, and the app's own sheet
224
+ lets its theme (--color-*, --font-*) lead them in the editor. */
223
225
  :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);
226
+ --md-pele-bg:transparent;
227
+ --md-pele-fg:var(--md-color-text);
228
+ --md-pele-line:var(--md-color-text);
229
+ --md-pele-muted:var(--md-color-text-secondary);
230
+ --md-pele-surface:var(--md-color-surface-2);
231
+ --md-pele-surface-alt:var(--md-color-border-focus);
232
+ --md-pele-border:var(--md-color-border);
233
+ --md-pele-accent:var(--md-color-accent);
234
+ --md-pele-font:var(--md-font-sans);
235
+ --md-pele-font-mono:var(--md-font-mono);
236
+ --md-pele-radius:0;
237
+ --md-pele-heading-weight:var(--md-fw-semibold);
238
+ --md-pele-title-weight:var(--md-fw-semibold);
239
+ --md-pele-series-1:var(--md-pele-accent);
240
+ --md-pele-series-2:#4f91f7;
241
+ --md-pele-series-3:#35c57a;
242
+ --md-pele-series-4:#e6b91e;
243
+ --md-pele-series-5:#9b6dea;
244
+ --md-pele-series-6:#f05a62;
245
+ --md-pele-series-7:color-mix(in srgb,var(--md-pele-accent) 50%,#4f91f7);
246
+ --md-pele-series-8:color-mix(in srgb,#9b6dea 50%,#f05a62);
245
247
  }
246
248
 
247
249
  /* Radar gives these SVG labels a color declaration; SVG text needs a fill. */
@@ -418,6 +420,7 @@
418
420
  min-height:var(--md-ol-circle);
419
421
  padding-top:min(0.15 * var(--md-unit), 0.6 * max(0px, var(--md-ol-circle) - var(--md-item-line)));
420
422
  }
423
+ .md-render{--md-ol-dot-line:radial-gradient(circle closest-side, var(--md-color-line) calc(100% - 0.5px), transparent 100%) center top / 3px 6px repeat-y}
421
424
  .md-render ol>li::before{
422
425
  content:counter(rapier-ol);
423
426
  position:absolute;
@@ -441,16 +444,21 @@
441
444
 
442
445
  inset-inline-start:calc(calc(-1.75 * var(--md-unit)) - 1.5px + var(--md-marker, 0px));
443
446
  /* From just below this circle into the next one: at least to the next item's top (--md-item-gap is the gap between
444
- items the spacing settings give), and at least a quarter line, which the next circle covers. */
447
+ items the spacing settings give), and at least a quarter line, which the next circle covers. Where the circles
448
+ nearly touch (one-line items) there is no room for dots, so none are drawn: the line shows only when it spans at
449
+ least half a line (100% is this item's height). */
445
450
  top:calc(var(--md-ol-top) + 1.0875 * var(--md-ol-circle));
446
- bottom:calc(-1 * max(var(--md-item-gap, 0px), var(--md-line) / 4));
447
- border-inline-start:3px dotted var(--md-color-line);
451
+ --md-ol-dots:calc(100% - var(--md-ol-top) - 1.0875 * var(--md-ol-circle) + max(var(--md-item-gap, 0px), var(--md-line) / 4));
452
+ height:clamp(0px, (var(--md-ol-dots) - var(--md-line) / 2) * 9999, var(--md-ol-dots));
453
+ /* Round dots, 3px across every 6px: a dotted border draws squares in Chrome. */
454
+ width:3px;
455
+ background:var(--md-ol-dot-line);
448
456
  z-index:0;
449
457
  }
450
458
  /* A heading between two parts of one numbered list (the second part's start follows the first's last number;
451
459
  _rapierMarkListContinuations marks them): the heading stands on the margin the item text stands on, and the dotted
452
460
  line runs on, out of the first part's last item, down the heading's own height, to the next circle. */
453
- .md-render ol[data-rapier-list-continues]>li:last-child::after{bottom:calc(-1 * var(--md-list-heading-before,var(--md-gap-heading-before)))}
461
+ .md-render ol[data-rapier-list-continues]>li:last-child::after{height:auto;bottom:calc(-1 * var(--md-list-heading-before,var(--md-gap-heading-before)))}
454
462
  /* A wrapped picture set the next circle aside: the dotted line ends with this item, as at a list's last. */
455
463
  .md-render ol>li[data-rapier-wrap-step]::after{content:none!important}
456
464
  .md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:calc(2.75 * var(--md-unit))}
@@ -460,7 +468,8 @@
460
468
  inset-inline-start:calc(calc(1 * var(--md-unit)) - 1.5px);
461
469
  top:0;
462
470
  bottom:calc(-1 * var(--md-list-heading-after,var(--md-gap-heading-after)));
463
- border-inline-start:3px dotted var(--md-color-line);
471
+ width:3px;
472
+ background:var(--md-ol-dot-line);
464
473
  z-index:0;
465
474
  }
466
475
  .md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]:has(+:is(h1,h2,h3,h4,h5,h6)){--md-list-heading-after:var(--md-gap-heading-before)}