rapier-markdown-kit 1.2.31 → 1.2.33

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.
@@ -308,10 +308,27 @@ export function createTypography() {
308
308
  return {key: keys.join('\u0000'), generation:keys[0], styles, parents, supported};
309
309
  }
310
310
 
311
+ function smallCapsAsPlanned(paragraph, text, inspection) {
312
+ if (paragraph.getAttribute('data-md-caps') !== 'small') return true;
313
+ const doc = paragraph.ownerDocument, style = inspection.styles.get(paragraph);
314
+ if (!style || !doc.body) return false;
315
+ const probe = caps => {
316
+ const span = doc.createElement('span');
317
+ span.style.cssText = 'position:absolute;visibility:hidden;white-space:pre;left:0;top:0;font-synthesis-small-caps:none;font-variant-caps:' + caps;
318
+ for (const name of ['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'fontStretch', 'letterSpacing', 'fontKerning', 'fontFeatureSettings'])
319
+ span.style[name] = style[name];
320
+ span.textContent = text.trim().slice(0, 80);
321
+ doc.body.append(span);
322
+ const width = span.getBoundingClientRect().width;
323
+ span.remove();
324
+ return width;
325
+ };
326
+ return Math.abs(probe('small-caps') - probe('normal')) < .01;
327
+ }
328
+
311
329
  function snapshot(paragraph, inspection) {
312
330
  const text = paragraph.textContent, language = paragraph.closest('[lang]')?.getAttribute('lang') ?? '';
313
- if (!inspection.supported || !text.trim() || paragraph.hasAttribute('data-md-lines') ||
314
- paragraph.getAttribute('data-md-caps') === 'small' || paragraph.closest('[data-rapier-published-ink]') ||
331
+ if (!inspection.supported || !text.trim() || !smallCapsAsPlanned(paragraph, text, inspection) || paragraph.closest('[data-rapier-published-ink]') ||
315
332
  /[^\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Script=Common}\p{Script=Inherited}]/u.test(text) ||
316
333
  /[\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/u.test(text)) return null;
317
334
  const hard = [], items = runs(paragraph, hard), atoms = [];
@@ -425,6 +442,24 @@ export function createTypography() {
425
442
  shape.box.height, shape.points.join(' ')].join(':')).join('|')};
426
443
  }
427
444
 
445
+ function dropMeasure(paragraph, view, width) {
446
+ if (!paragraph.hasAttribute('data-md-lines')) return null;
447
+ const letter = view.getComputedStyle(paragraph, '::first-letter');
448
+ const lines = Math.ceil(parseFloat(letter.initialLetter)), initial = initialRange(paragraph);
449
+ const advance = ['width', 'marginLeft', 'marginRight', 'paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth']
450
+ .reduce((sum, name) => sum + (parseFloat(letter[name]) || 0), 0);
451
+ const at = initial && point(runs(paragraph), initial.start);
452
+ if (!letter.initialLetter) return {refuse: true};
453
+ if (!(lines > 1)) return null;
454
+ if (!(advance > 0 && advance < width) || !at?.node.parentElement) return {refuse: true};
455
+ const style = view.getComputedStyle(at.node.parentElement), context = paragraph.ownerDocument.createElement('canvas').getContext('2d');
456
+ context.font = style.fontStyle + ' ' + style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
457
+ context.letterSpacing = style.letterSpacing;
458
+ const text = paragraph.textContent, word = text.slice(initial.start, initial.start + (/\s/u.exec(text.slice(initial.start))?.index ?? text.length));
459
+ const lead = context.measureText(word).width - context.measureText(word.slice(initial.text.length)).width;
460
+ return {lines, advance, lead};
461
+ }
462
+
428
463
  const topology = plan => plan && plan.lines.map(line => [line.start,line.end,line.next,line.kind,line.glyph].join(':')).join('|');
429
464
  function updateProjection(projection, plan) {
430
465
  for (const {node,property,line,sign} of projection.bindings) {
@@ -517,13 +552,6 @@ export function createTypography() {
517
552
  return Math.abs(inner - job.plan.lines.length * job.beside.lineHeight) < 1;
518
553
  }
519
554
 
520
- function editKey(paragraph, generation) {
521
- const page = paragraph.ownerDocument.documentElement;
522
- return [generation, paragraph.innerHTML, ...[...paragraph.attributes].map(item => item.name + '=' + item.value),
523
- ...[...page.attributes].map(item => item.name + '=' + item.value), paragraph.ownerDocument.body?.className,
524
- paragraph.ownerDocument.defaultView.innerWidth].join('\u0000');
525
- }
526
-
527
555
  function apply(root, {all=false, editing=false}={}) {
528
556
  const started = performance.now();
529
557
  const generation = captureFontAuthority(root?.ownerDocument || globalThis.document);
@@ -533,31 +561,40 @@ export function createTypography() {
533
561
  }
534
562
  const jobs=[];let pending=false,changed=0,released=0,preparedCount=0;
535
563
  for (const paragraph of paragraphs(root)) {
564
+ if (paragraph.dataset.rapierFlow==='true') continue;
536
565
  if (paragraph.closest('table,pre,code,h1,h2,h3,h4,h5,h6')) {clear(paragraph);continue;}
537
566
  if (paragraph.closest('.block-edit')) {
538
567
  if (!editing) continue;
539
- if (paragraph.getAttribute('data-md-align')!=='justify' || paragraph.getAttribute('data-md-hyphens')!=='auto') {
540
- if (paragraph.hasAttribute(mark)) clear(paragraph);
541
- protectInitial(paragraph);continue;
542
- }
568
+ if (paragraph.hasAttribute(mark)) clear(paragraph);
569
+ protectInitial(paragraph);
570
+ continue;
543
571
  }
544
572
  else if (paragraph.isContentEditable && !paragraph.closest('.block-read')) {clear(paragraph);continue;}
545
573
  if (!paragraph.isConnected || !all && paragraph.checkVisibility && !paragraph.checkVisibility({contentVisibilityAuto:true})) continue;
546
574
  const view=paragraph.ownerDocument.defaultView;
547
575
  if (!view || !paragraph.getClientRects().length) continue;
548
- if (editing && states.get(paragraph)?.editKey===editKey(paragraph,generation)) continue;
549
576
  const style=view.getComputedStyle(paragraph),box=paragraph.getBoundingClientRect(),prior=states.get(paragraph);
550
577
  if (!all && (box.bottom < -100 || box.top > view.innerHeight+100)) continue;
551
578
  const hanging=paragraph.getAttribute('data-md-hanging')==='on',size=dimensions(paragraph,style,box,hanging);
552
579
  if (!(size.width>0)) continue;
580
+ if (paragraph.getAttribute('data-md-align')!=='justify') {size.shrink=0;size.stretch=0;}
553
581
  const beside=floatMeasures(paragraph,style,box,size.width);
554
582
  if (beside?.refuse) {clear(paragraph);continue;}
555
583
  if (beside?.widths.some(width=>!(width>0))) {clear(paragraph);continue;}
556
584
  if (beside) size.widths=beside.widths;
585
+ const drop=dropMeasure(paragraph,view,size.width);
586
+ if (drop?.refuse) {clear(paragraph);continue;}
587
+ if (drop) {
588
+ const widths=[...(size.widths||[])];
589
+ while (widths.length<drop.lines) widths.push(size.width);
590
+ for (let line=0;line<drop.lines;line++) widths[line]=Math.floor((widths[line]-drop.advance+(line?0:drop.lead))*64)/64;
591
+ if (widths.some(value=>!(value>0))) {clear(paragraph);continue;}
592
+ size.widths=widths;
593
+ }
557
594
  const parents=prior?.parents || own(paragraph);
558
595
  const inspection=inspect(paragraph,parents,prior,style);
559
596
  const reusable=prior?.html===paragraph.innerHTML && prior.key===inspection.key && (!prior.geometry || scope(paragraph.ownerDocument).current(prior.geometry));
560
- const sizeKey=[size.width,size.indent,size.left,size.right,beside?.key||''].join('|');
597
+ const sizeKey=[size.width,size.indent,size.left,size.right,beside?.key||'',drop?drop.lines+'/'+drop.advance+'/'+drop.lead:''].join('|');
561
598
  if (refused.get(paragraph)===sizeKey+'\u0000'+paragraph.textContent) {if (paragraph.hasAttribute(mark)) clear(paragraph);continue;}
562
599
  if (reusable && !prior.pending && prior.sizeKey===sizeKey) continue;
563
600
  const fitWork=reusable && prior.sizeKey===sizeKey?prior.fitWork:null;
@@ -565,12 +602,12 @@ export function createTypography() {
565
602
  if (!all && jobs.length===4) {if (!reusable) clear(paragraph);pending=true;continue;}
566
603
  jobs.push({paragraph,view,style,inspection,size,sizeKey,hanging,beside,selected:selectionBefore(paragraph),
567
604
  cap:paragraph.hasAttribute('data-md-lines'),justified:paragraph.getAttribute('data-md-align')==='justify' && paragraph.getAttribute('data-md-hyphens')==='auto',
605
+ ragged:paragraph.getAttribute('data-md-align')!=='justify' && paragraph.getAttribute('data-md-hyphens')==='auto' && /^(start|left)$/.test(style.textAlign),
568
606
  saved:reusable?prior.saved:null,geometry:reusable?prior.geometry:null,prepared:reusable?prior.prepared:null,
569
607
  work:reusable?prior.work:null,fitWork,
570
608
  prior,reusable,needsClear:paragraph.hasAttribute(mark)});
571
609
  }
572
- if (editing && jobs.some(job=>job.selected && !job.reusable)) root.ownerDocument.getSelection()?.removeAllRanges();
573
- for (const job of jobs) if (job.needsClear && !job.saved) clear(job.paragraph,{keepSelection:!editing});
610
+ for (const job of jobs) if (job.needsClear && !job.saved) clear(job.paragraph);
574
611
  function stateFor(job) {
575
612
  return {properties:new Map(),moves:[],saved:job.saved,geometry:job.geometry,prepared:job.prepared,plan:job.plan,
576
613
  work:job.work,fitWork:job.fitWork,parents:job.inspection.parents,key:job.inspection.key,generation:job.inspection.generation,sizeKey:job.sizeKey};
@@ -584,12 +621,8 @@ export function createTypography() {
584
621
  for (let jobIndex = 0; jobIndex < jobs.length; jobIndex++) {
585
622
  if (!all && jobIndex && performance.now() - started >= 8) {pending=true;break;}
586
623
  const job = jobs[jobIndex];
587
- if (!job.saved && job.justified) {
588
- if (job.needsClear) {
589
- const elements=own(job.paragraph),parents=job.inspection.parents;
590
- if (!editing || !job.prior?.saved || elements.length!==parents.length || elements.some((element,index)=>element!==parents[index]))
591
- job.inspection=inspect(job.paragraph,elements,null,job.view.getComputedStyle(job.paragraph));
592
- }
624
+ if (!job.saved && (job.justified || job.ragged)) {
625
+ if (job.needsClear) job.inspection=inspect(job.paragraph,own(job.paragraph),null,job.view.getComputedStyle(job.paragraph));
593
626
  job.saved=snapshot(job.paragraph,job.inspection);
594
627
  if (job.saved) {
595
628
  job.work=scope(job.paragraph.ownerDocument).prepareSteps(job.paragraph,job.saved.inline,{policy:'paragraph',language:job.saved.language,
@@ -611,15 +644,15 @@ export function createTypography() {
611
644
  if (job.fitWork) {
612
645
  for (;;) {
613
646
  const step=job.fitWork.next();
614
- if (step.done) {job.plan=step.value;job.fitWork=null;break;}
647
+ if (step.done) {job.plan=job.ragged && step.value ? Object.freeze({...step.value,lines:Object.freeze(step.value.lines.map(line=>Object.freeze({...line,wordSpacing:0})))}) : step.value;job.fitWork=null;break;}
615
648
  if (!all && performance.now()-started>=8) break;
616
649
  }
617
650
  if (job.fitWork) {suspend(job);continue;}
618
651
  }
619
652
  job.projection=job.reusable && job.plan && job.prior?.projection?.topology===topology(job.plan) ? job.prior.projection : null;
620
- if (job.needsClear && job.saved && !job.projection) clear(job.paragraph,{forget:false,keepSelection:!editing});
621
- const {paragraph,view,hanging,cap,justified}=job;
622
- if (!cap && !hanging && !justified) {restoreSelection(paragraph,job.selected);continue;}
653
+ if (job.needsClear && job.saved && !job.projection) clear(job.paragraph,{forget:false});
654
+ const {paragraph,view,hanging,cap,justified,ragged}=job;
655
+ if (!cap && !hanging && !justified && !ragged) {restoreSelection(paragraph,job.selected);continue;}
623
656
  const state=stateFor(job);
624
657
  states.set(paragraph,state);live.add(paragraph);paragraph.setAttribute(mark,'');
625
658
  if (cap || hanging) protectInitial(paragraph);
@@ -631,6 +664,7 @@ export function createTypography() {
631
664
  }
632
665
  if (job.projection) {updateProjection(job.projection,job.plan);state.projection=job.projection;}
633
666
  else if (job.plan) state.projection=project(paragraph,job.saved,job.geometry,job.plan);
667
+ paragraph.toggleAttribute(mark+'-ragged',!!ragged && !!state.projection);
634
668
  if (state.projection && job.beside && !laidAsPlanned(paragraph,job)) {
635
669
  refused.set(paragraph,job.sizeKey+'\u0000'+paragraph.textContent);clear(paragraph);restoreSelection(paragraph,job.selected);continue;
636
670
  }
@@ -638,18 +672,103 @@ export function createTypography() {
638
672
  else if (hanging) hangPunctuation(paragraph,state,job.style,hangingEdge(paragraph,job.style,paragraph.getBoundingClientRect()));
639
673
  state.html=paragraph.innerHTML;
640
674
  restoreSelection(paragraph,job.selected);changed++;
641
- if (editing) state.editKey=editKey(paragraph,generation);
642
675
  }
643
676
  return {changed,released,pending,prepared:preparedCount};
644
677
  }
645
678
 
679
+ const slotPlans = new WeakMap();
680
+ function slotPlan(paragraph, items, options, input) {
681
+ const {width, top, obstacles, lineHeight, minWidth, justify, hyphenate} = input;
682
+ const round = value => Math.round(value * 100) / 100;
683
+ const key = [captureFontAuthority(paragraph.ownerDocument), width, lineHeight, minWidth, justify, hyphenate, options.language, options.wordBreak,
684
+ items.map(item => [item.text, item.font, item.letterSpacing, item.break, item.extraWidth].join('\u0001')).join('\u0002'),
685
+ obstacles.map(item => [item.x, item.y - top, item.width, item.height].map(round).join(',')).join(';')].join('\u0000');
686
+ const known = slotPlans.get(paragraph);
687
+ if (known?.key === key) return known.plan;
688
+ const plan = slotLines(paragraph, items, options, input);
689
+ slotPlans.set(paragraph, {key, plan});
690
+ return plan;
691
+ }
692
+ function slotLines(paragraph, items, options, {width, top, obstacles, lineHeight, minWidth, slotsForBand, justify, hyphenate}) {
693
+ if (!obstacles.length) return null;
694
+ const doc = paragraph.ownerDocument, words = scope(doc);
695
+ const source = items.map(item => ({...item, hyphenate}));
696
+ const geometry = words.prepare(paragraph, source, {policy: 'paragraph', language: options.language, whiteSpace: 'normal',
697
+ wordBreak: options.wordBreak, hanging: false, paragraphBreaks: [0, source.length]});
698
+ 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;
707
+ }
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;
754
+ }
755
+ fragments.push(fragment); previous = {itemIndex: at, end: to};
756
+ }
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};
763
+ }
764
+
646
765
  function retire(root, {documentChanged=false}={}) {
647
766
  clear(root);
648
767
  for (const paragraph of live) if (!paragraph.isConnected) clear(paragraph);
649
768
  scope(root?.ownerDocument || root || globalThis.document).retire({documentChanged});
650
769
  }
651
770
  const invalidateFonts = document => scope(document || globalThis.document).invalidateFonts();
652
- return {apply,clear,clearSelection,firstLineEnd,initialRange,protectInitial,invalidateFonts,retire};
771
+ return {apply,clear,clearSelection,firstLineEnd,initialRange,protectInitial,invalidateFonts,retire,slotPlan};
653
772
  }
654
773
 
655
774
  const typography = createTypography();
@@ -661,3 +780,4 @@ export const initialRange = typography.initialRange;
661
780
  export const protectInitial = typography.protectInitial;
662
781
  export const invalidateFonts = typography.invalidateFonts;
663
782
  export const retire = typography.retire;
783
+ export const slotPlan = typography.slotPlan;
@@ -216,6 +216,11 @@ export function documentFontUsage(markdown, parser, defaultFont) {
216
216
  for (const token of md.parse(splitOpeningFrontmatter(source).body, {})) {
217
217
  if (token.type === 'heading_open') blockFamily = sans;
218
218
  else if (token.type === 'heading_close' || token.type === 'paragraph_open') blockFamily = main;
219
+ else if (token.type === 'fence' && /^html(?:\s+\{[^{}]*\})?\s*$/i.test(token.info)) {
220
+ const text = plain(token.content);
221
+ add(blockFamily, text);
222
+ for (const name of htmlFontFamilies(token.content, md)) add(name, text);
223
+ }
219
224
  else if (token.type === 'fence' || token.type === 'code_block') add(mono, token.content);
220
225
  else if (token.type === 'html_block') {
221
226
  const text = plain(token.content);
@@ -128,10 +128,11 @@ function paperOf(value) {
128
128
  const pixels = /^(\d{3,5})x(\d{3,5})px$/.exec(name), width = Number(pixels?.[1]), height = Number(pixels?.[2]);
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
+ export const pageSizeOf = settings => settings?.papersize || PAPERS.letter;
131
132
  export const PAGE_SIZES = Object.freeze(['letter', 'legal', 'tabloid', 'a3', 'a4', 'a5', '1080x1080px', '1080x1350px', '1920x1080px', '1200x630px']);
132
133
  const EMPTY = Object.freeze({
133
134
  title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
134
- papersize: null, geometry: null, pagestyle: null, spacing: null,
135
+ papersize: null, geometry: null, pagestyle: null, spacing: null, template: null,
135
136
  });
136
137
 
137
138
  function unquote(value) {
@@ -351,6 +352,7 @@ export function readDocumentSettings(markdown) {
351
352
  fontsize, mainfont, sansfont, monofont, defaultfont, linestretch, papersize, geometry,
352
353
  spacing: normalizeDocumentSpacing(take('spacing')),
353
354
  pagestyle: page === 'plain' || page === 'empty' ? page : null,
355
+ template: String(take('template') || '').toLowerCase() === 'page' ? 'page' : null,
354
356
  });
355
357
  }
356
358
 
@@ -368,14 +370,14 @@ export function updateDocumentSettings(markdown, patch) {
368
370
  text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
369
371
  for (const [key, supplied] of Object.entries(patch || {})) {
370
372
  const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
371
- if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry', 'papersize'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
373
+ if (!font && !['fontsize', 'linestretch', 'spacing', 'geometry', 'papersize', 'pagestyle'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
372
374
  const matches = fields.filter(item => item.key === key);
373
375
  if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
374
376
  const field = matches[0];
375
- const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(supplied) : key === 'papersize' ? paperOf(supplied) : leading(supplied);
377
+ const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : key === 'geometry' ? marginOf(supplied) : key === 'papersize' ? paperOf(supplied) : key === 'pagestyle' ? (['plain', 'empty'].includes(supplied) ? {name: supplied} : null) : leading(supplied);
376
378
  if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
377
- const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : key === 'geometry' ? unquote(supplied).trim() : key === 'papersize' ? value.name : String(value.factor);
378
- if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : key === 'geometry' ? ['top', 'right', 'bottom', 'left'].every(side => before.geometry[side] === value[side]) : key === 'papersize' ? before.papersize.name === value.name : before.linestretch.factor === value.factor)) continue;
379
+ const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : key === 'geometry' ? unquote(supplied).trim() : key === 'papersize' || key === 'pagestyle' ? value.name : String(value.factor);
380
+ if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : key === 'spacing' ? JSON.stringify(before.spacing) === encoded : key === 'geometry' ? ['top', 'right', 'bottom', 'left'].every(side => before.geometry[side] === value[side]) : key === 'papersize' ? before.papersize.name === value.name : key === 'pagestyle' ? before.pagestyle === value.name : before.linestretch.factor === value.factor)) continue;
379
381
  if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
380
382
  if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
381
383
  if (field.opaque || field.invalid || field.multiline || field.block) refuse(key);
@@ -53,7 +53,7 @@ export function htmlFence(raw) {
53
53
  const tail = raw.slice(end), attached = /^(\r?\n)(<!--md-layout:v1[^\r\n]*-->)(?:\r?\n)?$/.exec(tail);
54
54
  const layout = attached && parseLayout(attached[2]);
55
55
  if (tail.trim() && !layout) return null;
56
- if (layout && ['align', 'first', 'indent', 'marker', 'hyphens', 'caps', 'hanging'].some(key => key in layout)) return null;
56
+ if (layout && ['align', 'first', 'indent', 'marker', 'hyphens', 'caps', 'hanging', 'lines', 'before', 'after', 'line'].some(key => key in layout)) return null;
57
57
  return {attrs, info: opening[3].trim(), body: rest.slice(0, found.index), infoStart: opening[1].length + mark.length,
58
58
  infoEnd: opening[0].replace(/\r?\n$/, '').length, source: raw, start: 0, end: raw.length, fenceEnd: end,
59
59
  layout: layout || HTML_FLOW, marker: attached ? {start: end + attached[1].length, end: end + attached[1].length + attached[2].length, text: attached[2]} : null};
@@ -545,24 +545,43 @@ export function mountHtmlBlocks(doc, options = {}) {
545
545
  value.width < 1 || value.viewportHeight < 1 || value.width > 4096 || value.viewportHeight > 4096) return;
546
546
  if (Math.abs(row.frame.clientWidth - value.width) > 1 || Math.abs(row.frame.clientHeight - value.viewportHeight) > 1) {message(row, 'snapshot'); return;}
547
547
  const stamp = ++row.generation, scale = Math.min(1, 1024 / Math.max(value.width, value.viewportHeight));
548
- const canvas = doc.createElement('canvas'); canvas.width = Math.max(1, Math.ceil(value.width * scale)); canvas.height = Math.max(1, Math.ceil(value.viewportHeight * scale));
549
- const snapshot = new win.Image();
548
+ const ratio = win.devicePixelRatio || 1, sharp = Math.min(ratio, 4096 / Math.max(value.width, value.viewportHeight), Math.sqrt(16777216 / (value.width * value.viewportHeight)));
549
+ const current = () => !dead && owned.get(row.image) === row && stamp === row.generation && keyOf(row.image) === row.key;
550
+ const draw = async factor => {
551
+ const canvas = doc.createElement('canvas'), snapshot = new win.Image();
552
+ canvas.width = Math.max(1, Math.ceil(value.width * factor)); canvas.height = Math.max(1, Math.ceil(value.viewportHeight * factor));
553
+ try {
554
+ snapshot.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="' + canvas.width + '" height="' + canvas.height + '" viewBox="0 0 ' + value.width + ' ' + value.viewportHeight + '"><foreignObject width="100%" height="100%">' + value.markup + '</foreignObject></svg>');
555
+ await Promise.race([snapshot.decode(), new Promise((_, reject) => win.setTimeout(() => reject(new Error('Snapshot decode timeout')), 2000))]);
556
+ if (!current()) return null;
557
+ const ctx = canvas.getContext('2d'); ctx.drawImage(snapshot, 0, 0); ctx.getImageData(0, 0, 1, 1);
558
+ return canvas.toDataURL('image/png');
559
+ } finally {snapshot.removeAttribute('src'); canvas.width = canvas.height = 0;}
560
+ };
550
561
  try {
551
- snapshot.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="' + canvas.width + '" height="' + canvas.height + '" viewBox="0 0 ' + value.width + ' ' + value.viewportHeight + '"><foreignObject width="100%" height="100%">' + value.markup + '</foreignObject></svg>');
552
- await Promise.race([snapshot.decode(), new Promise((_, reject) => win.setTimeout(() => reject(new Error('Snapshot decode timeout')), 2000))]);
553
- if (dead || owned.get(row.image) !== row || stamp !== row.generation || keyOf(row.image) !== row.key) return;
562
+ const first = await draw(scale);
563
+ if (first == null) return;
554
564
  if (Math.abs(row.frame.clientWidth - value.width) > 1 || Math.abs(row.frame.clientHeight - value.viewportHeight) > 1) {message(row, 'snapshot'); return;}
555
- const ctx = canvas.getContext('2d'); ctx.drawImage(snapshot, 0, 0); ctx.getImageData(0, 0, 1, 1);
556
- const first = canvas.toDataURL('image/png'); if (first.length > 4 * 1024 * 1024) throw new Error('Snapshot byte budget');
557
- row.image.dataset.rapierFirstFrame = first; row.image.src = first;
565
+ if (first.length > 4 * 1024 * 1024) throw new Error('Snapshot byte budget');
566
+ const shown = sharp > scale ? await draw(sharp).catch(() => first) : first;
567
+ if (shown == null) return;
568
+ row.ratio = ratio;
569
+ row.image.dataset.rapierFirstFrame = first; row.image.src = shown;
558
570
  row.snapshot = true;
559
571
  row.image.dataset.rapierLiveSurface = '';
560
572
  row.image.dataset.rapierContour = row.animated ? 'animation-box' : value.snapshotStatus === 'sampled' ? 'sampled' : 'unavailable-box';
561
573
  row.image.dataset.rapierNaturalWidth = String(value.width); row.image.dataset.rapierNaturalHeight = String(value.viewportHeight);
562
574
  row.image.dispatchEvent(new win.Event('rapier-html-resize', {bubbles: true})); schedule();
563
575
  } catch {if (owned.get(row.image) === row) {row.image.dataset.rapierContour = 'unavailable-box'; row.image.dataset.rapierSnapshot = 'unavailable';}}
564
- finally {snapshot.removeAttribute('src'); canvas.width = canvas.height = 0;}
565
576
  }
577
+ let density = null;
578
+ const redraw = () => {
579
+ density?.removeEventListener('change', redraw);
580
+ if (dead) return;
581
+ density = win.matchMedia('(resolution: ' + (win.devicePixelRatio || 1) + 'dppx)'); density.addEventListener('change', redraw);
582
+ for (const row of owned.values()) if (row.snapshot && row.ratio !== (win.devicePixelRatio || 1)) {row.generation++; message(row, 'snapshot');}
583
+ };
584
+ redraw();
566
585
  const changedVisibility = () => {for (const row of owned.values()) applyPlayback(row);};
567
586
  const hidden = event => {if (!event.persisted) {api.close(); return;} for (const row of owned.values()) {row.visible = false; applyPlayback(row);}};
568
587
  const shown = () => {scan(); for (const row of owned.values()) {const box = row.image.getBoundingClientRect();
@@ -580,7 +599,7 @@ export function mountHtmlBlocks(doc, options = {}) {
580
599
  snapshot: image.dataset.rapierSnapshot || null, contour: image.dataset.rapierContour || null, status: image.dataset.rapierPlayback || 'ready'} : null;},
581
600
  close() {dead = true; win.cancelAnimationFrame(pending); observer.disconnect(); resize.disconnect(); visibility?.disconnect();
582
601
  win.removeEventListener('message', receive); win.removeEventListener('resize', schedule); doc.removeEventListener('scroll', schedule, true);
583
- doc.removeEventListener('visibilitychange', changedVisibility); reduced.removeEventListener('change', changedMotion);
602
+ doc.removeEventListener('visibilitychange', changedVisibility); reduced.removeEventListener('change', changedMotion); density?.removeEventListener('change', redraw);
584
603
  for (const row of [...owned.values()]) release(row); sheet.remove(); local.clear();
585
604
  win.removeEventListener('pagehide', hidden); win.removeEventListener('pageshow', shown); if (doc.rapierObjects === api) delete doc.rapierObjects;}};
586
605
  win.addEventListener('pagehide', hidden); win.addEventListener('pageshow', shown); if (!options.root) doc.rapierObjects = api; scan(); return api;
@@ -1,6 +1,4 @@
1
1
  // SPDX-License-Identifier: MIT
2
- export function isTemplatePage(source) {
3
- const match = /^(?:)?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(String(source).slice(0, 65536));
4
- return !!match && /^template:[ \t]*page[ \t]*(?:#.*)?$/m.test(match[1]);
5
- }
2
+ import {readDocumentSettings} from './document-settings.mjs';
3
+ export const isTemplatePage = source => readDocumentSettings(source).template === 'page';
6
4
  export const isPageBorderLayout = layout => layout?.wrap === 'behind' && layout.lock === 'on';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rapier-markdown-kit",
3
- "version": "1.2.31",
3
+ "version": "1.2.33",
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
@@ -326,6 +326,23 @@
326
326
  position: relative;
327
327
  }
328
328
 
329
+ /* The line that holds the box hangs: its later lines start under the first line's words, not under the box. The box takes
330
+ 16px and a space of the first line (the 48px target less its overhangs), which the hang gives back. */
331
+ .md-render .task-list-item > p:has(> input[type="checkbox"]:first-child),
332
+ .md-render .task-list-item:has(> input[type="checkbox"]:first-child) {
333
+ /* The space the source keeps between the box and the words (`- [ ] words`), so the later lines meet the words. */
334
+ --md-task-space: .24em;
335
+ padding-inline-start: calc(16px + var(--md-space-1) + var(--md-task-space));
336
+ text-indent: calc(-16px - var(--md-space-1) - var(--md-task-space));
337
+ }
338
+ /* The hang is the line's alone: nothing inside it (a formula, a code chip) or after it takes the indent, and a tight item's
339
+ later blocks (a nested list, a paragraph) keep their own place. */
340
+ .md-render .task-list-item > p:has(> input[type="checkbox"]:first-child) *,
341
+ .md-render .task-list-item:has(> input[type="checkbox"]:first-child) * { text-indent: 0; }
342
+ .md-render .task-list-item:has(> input[type="checkbox"]:first-child) > :is(p, ul, ol, pre, blockquote, table, div, dl, details, figure, hr) {
343
+ margin-inline-start: calc(-16px - var(--md-space-1) - var(--md-task-space));
344
+ }
345
+
329
346
  .md-render .task-list-item input[type="checkbox"] {
330
347
  --md-task-box: clamp(14px, .92em, 24px);
331
348
  /* A form control does not inherit color on its own; the shell's own app sheet gives it one
@@ -401,6 +418,7 @@
401
418
  min-height:var(--md-ol-circle);
402
419
  padding-top:min(0.15 * var(--md-unit), 0.6 * max(0px, var(--md-ol-circle) - var(--md-item-line)));
403
420
  }
421
+ .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}
404
422
  .md-render ol>li::before{
405
423
  content:counter(rapier-ol);
406
424
  position:absolute;
@@ -424,16 +442,21 @@
424
442
 
425
443
  inset-inline-start:calc(calc(-1.75 * var(--md-unit)) - 1.5px + var(--md-marker, 0px));
426
444
  /* From just below this circle into the next one: at least to the next item's top (--md-item-gap is the gap between
427
- items the spacing settings give), and at least a quarter line, which the next circle covers. */
445
+ items the spacing settings give), and at least a quarter line, which the next circle covers. Where the circles
446
+ nearly touch (one-line items) there is no room for dots, so none are drawn: the line shows only when it spans at
447
+ least half a line (100% is this item's height). */
428
448
  top:calc(var(--md-ol-top) + 1.0875 * var(--md-ol-circle));
429
- bottom:calc(-1 * max(var(--md-item-gap, 0px), var(--md-line) / 4));
430
- border-inline-start:3px dotted var(--md-color-line);
449
+ --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));
450
+ height:clamp(0px, (var(--md-ol-dots) - var(--md-line) / 2) * 9999, var(--md-ol-dots));
451
+ /* Round dots, 3px across every 6px: a dotted border draws squares in Chrome. */
452
+ width:3px;
453
+ background:var(--md-ol-dot-line);
431
454
  z-index:0;
432
455
  }
433
456
  /* A heading between two parts of one numbered list (the second part's start follows the first's last number;
434
457
  _rapierMarkListContinuations marks them): the heading stands on the margin the item text stands on, and the dotted
435
458
  line runs on, out of the first part's last item, down the heading's own height, to the next circle. */
436
- .md-render ol[data-rapier-list-continues]>li:last-child::after{bottom:calc(-1 * var(--md-list-heading-before,var(--md-gap-heading-before)))}
459
+ .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)))}
437
460
  /* A wrapped picture set the next circle aside: the dotted line ends with this item, as at a list's last. */
438
461
  .md-render ol>li[data-rapier-wrap-step]::after{content:none!important}
439
462
  .md-render :is(h1,h2,h3,h4,h5,h6)[data-rapier-list-through]{position:relative;padding-inline-start:calc(2.75 * var(--md-unit))}
@@ -443,7 +466,8 @@
443
466
  inset-inline-start:calc(calc(1 * var(--md-unit)) - 1.5px);
444
467
  top:0;
445
468
  bottom:calc(-1 * var(--md-list-heading-after,var(--md-gap-heading-after)));
446
- border-inline-start:3px dotted var(--md-color-line);
469
+ width:3px;
470
+ background:var(--md-ol-dot-line);
447
471
  z-index:0;
448
472
  }
449
473
  .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)}