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.
- package/README.md +2 -2
- package/dist/kit/ledger/carried.mjs +14 -2
- package/dist/kit/ledger/text-merge.mjs +163 -26
- package/dist/kit/markdown-it.mjs +19 -5
- package/dist/kit/pdf-glyphs.mjs +2 -2
- package/dist/kit/pdf-vectors.mjs +29 -9
- package/dist/kit/render-markdown.mjs +8 -1
- package/dist/kit/render-print.mjs +67 -26
- package/dist/kit/render.mjs +36 -19
- package/dist/layout/hyphenation.mjs +15 -3
- package/dist/layout/model.mjs +9 -1
- package/dist/layout/paragraph-fit.mjs +3 -3
- package/dist/layout/typography.mjs +149 -29
- package/dist/spec/document-fonts.mjs +5 -0
- package/dist/spec/document-settings.mjs +7 -5
- package/dist/spec/html-blocks.mjs +30 -11
- package/dist/spec/template-page.mjs +2 -4
- package/package.json +1 -1
- package/style.css +29 -5
|
@@ -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.
|
|
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.
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
549
|
-
const
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
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
|
-
|
|
3
|
-
|
|
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.
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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)}
|