rapier-markdown-kit 1.2.6 → 1.2.8
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 +32 -30
- package/dist/agent/will.mjs +2 -1
- package/dist/images/playback.mjs +3 -2
- package/dist/kit/index.mjs +1 -1
- package/dist/kit/ledger/carried.d.mts +20 -0
- package/dist/kit/ledger.d.mts +88 -0
- package/dist/kit/ledger.mjs +18 -0
- package/dist/kit/markdown-it.d.mts +4 -0
- package/dist/kit/markdown-it.mjs +39 -0
- package/dist/kit/pdf-glyphs.mjs +59 -18
- package/dist/kit/pdf-source.d.mts +5 -0
- package/dist/kit/pdf-source.mjs +125 -0
- package/dist/kit/render-markdown.mjs +3 -2
- package/dist/kit/render-print.mjs +10 -5
- package/dist/kit/render-sanitize.mjs +28 -10
- package/dist/kit/render-styles.mjs +2 -2
- package/dist/kit/render.mjs +7 -5
- package/dist/kit/will.d.mts +42 -0
- package/dist/spec/document-settings.mjs +92 -6
- package/dist/spec/html-blocks.mjs +4 -2
- package/dist/spec/md-assets.mjs +27 -6
- package/dist/spec/md-layout.mjs +3 -3
- package/dist/spec/md-source.mjs +8 -4
- package/dist/spec/word-sidecar.mjs +7 -0
- package/package.json +21 -4
- package/style.css +10 -12
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
import {typography} from '../spec/md-marks.mjs';
|
|
3
3
|
const RAPIER_SANITIZE_FORBID_TAGS = Object.freeze(['form', 'style', 'dialog', 'template', 'iframe']);
|
|
4
|
-
|
|
4
|
+
// Invokers (DOMPurify 3.4.16 allows them) act on another element by id, and a document shares the page with the editor's own
|
|
5
|
+
// controls: a document's button could open or close them.
|
|
6
|
+
const RAPIER_SANITIZE_FORBID_ATTR = Object.freeze(['action', 'formaction', 'form', 'for', 'command', 'commandfor', 'popover', 'popovertarget', 'popovertargetaction']);
|
|
5
7
|
// The document profiles use one allowlist in a browser and in an inert server parser.
|
|
6
8
|
const RAPIER_SANITIZE_PROFILES = Object.freeze({
|
|
7
9
|
htmlTags: Object.freeze(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),
|
|
8
10
|
svgTags: Object.freeze(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),
|
|
9
11
|
mathTags: Object.freeze(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),
|
|
10
|
-
htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),
|
|
12
|
+
htmlAttributes: Object.freeze(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","command","commandfor","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),
|
|
11
13
|
svgAttributes: Object.freeze(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dominant-baseline","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","pointer-events","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-orientation","text-rendering","textlength","type","u1","u2","unicode","values","vector-effect","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),
|
|
12
14
|
mathAttributes: Object.freeze(["accent","accentunder","align","bevelled","close","columnalign","columnlines","columnspacing","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lquote","lspace","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),
|
|
13
15
|
xmlAttributes: Object.freeze(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),
|
|
@@ -89,8 +91,10 @@ function prepareMathSource(input, DOMParser) {
|
|
|
89
91
|
// An inert parser and a string sanitizer use the same profile as DOMPurify. Neither port may
|
|
90
92
|
// execute document scripts or fetch resources. HTML5 parsing precedes filtering so foreign
|
|
91
93
|
// content, entities and malformed table markup cross the same boundary as a browser's parser.
|
|
92
|
-
function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl}) {
|
|
93
|
-
|
|
94
|
+
function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId = id => /^rapier-(?!anchor-)/.test(id)}) {
|
|
95
|
+
// chromeOwnsId defaults to _rapierChromeOwnsId's prefix rule: every page Rapier writes carries rapier- elements
|
|
96
|
+
// (rapier-document, rapier-ledger). The factories are serialized into written pages, so neither reads a module constant.
|
|
97
|
+
if (![parseFragment, serialize, sanitizeHtml, safeRasterDataUrl, chromeOwnsId].every(port => typeof port === 'function'))
|
|
94
98
|
throw new TypeError('The inert HTML sanitizer requires every parser and filtering port');
|
|
95
99
|
const uri = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
|
96
100
|
const uriSafe = new Set(['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);
|
|
@@ -270,7 +274,7 @@ function _rapierInstallSanitizeHooks() {
|
|
|
270
274
|
if (!/^data:/i.test(compact)) return;
|
|
271
275
|
const tag = node?.nodeName === 'IMG' ? 'img'
|
|
272
276
|
: node?.namespaceURI === DIAGRAM_LABEL_SVG_NAMESPACE && node.localName === 'image' ? 'image' : '';
|
|
273
|
-
const rasterAllowed = (/^(?:render|export|raw|source)$/.test(_rapierSanitizeRuntime.context)
|
|
277
|
+
const rasterAllowed = (/^(?:paste|render|export|raw|source)$/.test(_rapierSanitizeRuntime.context)
|
|
274
278
|
|| _rapierSanitizeRuntime.context === 'diagram' && tag === 'image')
|
|
275
279
|
&& _rapierImageDataAttributeAllowed(tag, data.attrName.toLowerCase(), compact, _rapierSafeRasterDataUrl);
|
|
276
280
|
if (!rasterAllowed) data.keepAttr = false;
|
|
@@ -309,14 +313,17 @@ function _rapierInstallSanitizeHooks() {
|
|
|
309
313
|
// know: a marker's style is only `mso-list:Ignore`. The paste carries both as data-* first, where they stay,
|
|
310
314
|
// for the conversion that reads them (_rapierPasteWordLists).
|
|
311
315
|
DOMPurify.addHook('uponSanitizeElement', node => {
|
|
312
|
-
if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|SPAN)$/.test(node.nodeName)) return;
|
|
316
|
+
if (_rapierSanitizeRuntime.context !== 'paste' || !node || !/^(?:P|LI|H[1-6]|SPAN)$/.test(node.nodeName)) return;
|
|
313
317
|
// Only the style names them: a page cannot bring these attributes of its own.
|
|
314
318
|
node.removeAttribute('data-rapier-word-level');
|
|
315
319
|
node.removeAttribute('data-rapier-word-marker');
|
|
316
320
|
const style = node.getAttribute('style');
|
|
317
321
|
if (!style || !/mso-list/i.test(style)) return;
|
|
318
|
-
|
|
319
|
-
const
|
|
322
|
+
// Word's list reading is the editor's (AGPL) grammar; a host without it keeps the paste's paragraphs as they are.
|
|
323
|
+
const spec = globalThis.RapierMarkdownSpec;
|
|
324
|
+
if (!spec) return;
|
|
325
|
+
const {wordListLevel, wordListIsMarker} = spec;
|
|
326
|
+
const level = /^(?:P|LI|H[1-6])$/.test(node.nodeName) ? wordListLevel(style) : 0;
|
|
320
327
|
if (level) node.setAttribute('data-rapier-word-level', String(level));
|
|
321
328
|
else if (node.nodeName === 'SPAN' && wordListIsMarker(style)) node.setAttribute('data-rapier-word-marker', '1');
|
|
322
329
|
});
|
|
@@ -444,6 +451,17 @@ function _rapierChromeOwnsId(id) {
|
|
|
444
451
|
return _rapierSanitizeRuntime.chromeIds.has(String(id)) || /^rapier-(?!anchor-)/.test(id);
|
|
445
452
|
}
|
|
446
453
|
|
|
454
|
+
// A link whose words or address carry a bidi control, or whose host is spelled in punycode, can show a destination it does
|
|
455
|
+
// not have. Opening one names its real address first; the answer is that address, or '' when the link opens at once.
|
|
456
|
+
const RAPIER_BIDI_CONTROL = /[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
|
|
457
|
+
function _rapierLinkDestinationToConfirm(href, text = '') {
|
|
458
|
+
let url, address = String(href || '');
|
|
459
|
+
try { url = new URL(address); } catch (_) { return ''; }
|
|
460
|
+
try { address = decodeURIComponent(address); } catch (_) {}
|
|
461
|
+
return RAPIER_BIDI_CONTROL.test(String(text)) || RAPIER_BIDI_CONTROL.test(address) ||
|
|
462
|
+
url.hostname.split('.').some(label => /^xn--/i.test(label)) ? url.href : '';
|
|
463
|
+
}
|
|
464
|
+
|
|
447
465
|
function _rapierSafeRasterDataUrl(value) {
|
|
448
466
|
const match = RAPIER_RASTER_DATA_URL_RE.exec(value);
|
|
449
467
|
if (!match) return false;
|
|
@@ -526,7 +544,7 @@ function _rapierDropRemoteRules(rules, owner) {
|
|
|
526
544
|
}
|
|
527
545
|
|
|
528
546
|
function _rapierVerifyRasterBytes(bytes, mime) {
|
|
529
|
-
if (mime === 'image/jxl') return globalThis.RapierImageAssets
|
|
547
|
+
if (mime === 'image/jxl') return globalThis.RapierImageAssets?.isJxl(bytes) === true; // without the editor's codec a JPEG XL picture is refused
|
|
530
548
|
|
|
531
549
|
// declaration, doctype or generator/license comment before <svg itself, of whatever length its
|
|
532
550
|
|
|
@@ -540,6 +558,6 @@ function _rapierVerifyRasterBytes(bytes, mime) {
|
|
|
540
558
|
|
|
541
559
|
return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml,
|
|
542
560
|
sanitizeRapierFragment: typeof DOMPurify?.sanitize === 'function' ? sanitizeRapierFragment : undefined,
|
|
543
|
-
diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
|
|
561
|
+
diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierLinkDestinationToConfirm, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
|
|
544
562
|
}
|
|
545
563
|
export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, createInertHtmlSanitizer, RAPIER_SANITIZE_OPTIONS as options, prepareMathSource};
|
|
@@ -54,7 +54,7 @@ function _rapierDocumentPrintCss(hostSelector) {
|
|
|
54
54
|
@page{size:auto;margin:12mm 14mm 14mm}
|
|
55
55
|
@media print{
|
|
56
56
|
${p('.md-render')}{--md-color-line:#000000!important}
|
|
57
|
-
${list('.md-render h1','.md-render h2','.md-render h3','.md-render h4','.md-render h5','.md-render h6')}{break-after:avoid
|
|
57
|
+
${list('.md-render h1','.md-render h2','.md-render h3','.md-render h4','.md-render h5','.md-render h6')}{page-break-after:avoid;break-after:avoid-page;color:#111}
|
|
58
58
|
${list('.md-render p','.md-render li','.md-render blockquote')}{orphans:3;widows:3}
|
|
59
59
|
${p('.md-render img')}{max-width:100%!important;height:auto!important;break-inside:avoid-page}
|
|
60
60
|
${list('.md-render pre','.artifact-flat')}{white-space:pre-wrap!important;overflow-wrap:anywhere!important;word-break:break-word!important;overflow:visible!important;max-height:none!important}
|
|
@@ -66,7 +66,7 @@ function _rapierDocumentPrintCss(hostSelector) {
|
|
|
66
66
|
${list('.md-render th','.md-render td')}{overflow-wrap:anywhere;word-break:normal}
|
|
67
67
|
${p('.md-render thead')}{display:table-header-group}
|
|
68
68
|
${p('.md-render tfoot')}{display:table-footer-group}
|
|
69
|
-
${p('.md-render tr')}{break-inside:avoid
|
|
69
|
+
${p('.md-render tr')}{page-break-inside:avoid;break-inside:avoid-page}
|
|
70
70
|
${p('.md-render details>*:not(summary)')}{display:block!important}
|
|
71
71
|
${p('.md-render details')}{break-inside:auto}
|
|
72
72
|
${p('.md-render a')}{color:#111!important;text-decoration:underline}
|
package/dist/kit/render.mjs
CHANGED
|
@@ -161,7 +161,9 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
161
161
|
+ (printWidth > 0 ? '.rapier-page.md-render{width:' + printWidth + 'px!important}' : '') + '}' : '';
|
|
162
162
|
const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
|
|
163
163
|
let css = _rapierArtifactStyles(theme, includeFonts, !!opts.print || completePage, false, opts.print && opts.kind === 'standalone' ? '' : null)
|
|
164
|
-
|
|
164
|
+
// Print already floats pictures. Tables, code, figures and rules clear them
|
|
165
|
+
// through the same clearing rule a complete page uses; print keeps its own page flow.
|
|
166
|
+
+ (completePage || opts.print ? '\n\n' + _rapierSharedPageFallbackCss(!completePage) : '')
|
|
165
167
|
+ (opts.extraCss ? '\n\n' + opts.extraCss : '')
|
|
166
168
|
+ (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
|
|
167
169
|
+ (settingsCss ? '\n' + settingsCss : '')
|
|
@@ -671,13 +673,13 @@ async function _rapierReadSharedDocument(text, filename) {
|
|
|
671
673
|
return {source, filename: name, kind, bom, ...parts, comparisonBase};
|
|
672
674
|
}
|
|
673
675
|
|
|
674
|
-
function _rapierSharedPageFallbackCss() {
|
|
675
|
-
|
|
676
|
+
function _rapierSharedPageFallbackCss(clearOnly = false) {
|
|
677
|
+
const clear = '.rapier-page>:is(pre,table,hr,figure,.table-scroll-wrap,.math-display-wrap){clear:both}\n';
|
|
678
|
+
return clearOnly ? clear : `
|
|
676
679
|
.rapier-page{display:flow-root}
|
|
677
680
|
.rapier-page>:first-child{margin-top:0}
|
|
678
681
|
.md-render h1{display:flow-root}
|
|
679
|
-
|
|
680
|
-
`;
|
|
682
|
+
` + clear;
|
|
681
683
|
}
|
|
682
684
|
|
|
683
685
|
function _rapierShareWrapKind(element) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Will/1: permissions a Markdown document gives agents, as HTML comments on their own lines.
|
|
3
|
+
// <!-- will/1 keep --> ... <!-- /will --> keep: no change; append: add after the region's text only; edit: any change
|
|
4
|
+
export type WillLaw = 'keep' | 'append' | 'edit';
|
|
5
|
+
export type WillFault = 'malformed_marker' | 'unpaired_marker';
|
|
6
|
+
/** A paired region; `start` to `end` is its text between the marker lines (UTF-16 offsets). */
|
|
7
|
+
export interface WillRegion {
|
|
8
|
+
index: number; law: WillLaw; intent: string | null;
|
|
9
|
+
openerLine: number; openerStart: number; openerEnd: number; openerContentEnd: number;
|
|
10
|
+
closerLine: number; closerStart: number; closerEnd: number;
|
|
11
|
+
start: number; end: number;
|
|
12
|
+
}
|
|
13
|
+
export interface WillMarker { kind: 'open' | 'close' | 'near'; law: WillLaw | ''; intent: string | null; content: string; fault?: WillFault; line?: number; start?: number; end?: number; contentEnd?: number; }
|
|
14
|
+
export interface Will {
|
|
15
|
+
/** True when the text has any marker line. */
|
|
16
|
+
present: boolean;
|
|
17
|
+
/** The first fault, or ''. A document with a fault is all keep. */
|
|
18
|
+
fault: WillFault | '';
|
|
19
|
+
faults: {mode: WillFault; line: number; start: number; end: number}[];
|
|
20
|
+
/** Every marker line, as [start, end) with its terminator. */
|
|
21
|
+
blocks: {start: number; end: number}[];
|
|
22
|
+
markers: WillMarker[];
|
|
23
|
+
regions: WillRegion[];
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
/** A quick test for any marker line. */
|
|
27
|
+
export function hasWillMarkers(text: string): boolean;
|
|
28
|
+
export function parseWill(text: string): Will;
|
|
29
|
+
/** The law over [start, end): 'keep' when a fault, a marker or a keep region is touched; 'append' inside an append region; else 'edit'. */
|
|
30
|
+
export function willGovern(will: Will, start: number, end: number): WillLaw;
|
|
31
|
+
/** The regions [start, end) touches (an empty range: the regions it stands in). */
|
|
32
|
+
export function willRegionsIn(will: Will, start: number, end: number): WillRegion[];
|
|
33
|
+
/** The region or marker line [start, end) touches, or null. */
|
|
34
|
+
export function willTouchesMarker(will: Will, start: number, end: number): WillRegion | {law: 'keep'} | null;
|
|
35
|
+
/** The intent words of the one region wholly containing [start, end), or ''. */
|
|
36
|
+
export function willIntentOf(will: Will, start: number, end: number): string;
|
|
37
|
+
/** Reads one line as a marker, or null. */
|
|
38
|
+
export function willMarkerOf(line: string): WillMarker | null;
|
|
39
|
+
/** The width of a line terminator ending at `last` (char code), with `prior` before it: 0, 1 or 2. */
|
|
40
|
+
export function terminatorWidth(last: number, prior: number): number;
|
|
41
|
+
/** The text without one trailing line terminator. */
|
|
42
|
+
export function stripOneTerminator(text: string): string;
|
|
@@ -10,6 +10,91 @@ import {normalizeFontFamily, fontFamilyCss, FONT_STACKS} from './md-marks.mjs';
|
|
|
10
10
|
const NUMBER = /^(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
11
11
|
export const DOCUMENT_LEADING = 1.75 / 1.1;
|
|
12
12
|
|
|
13
|
+
// Before/after are points; line is a multiple of the element's own type size.
|
|
14
|
+
// Presets and native exports read this same table. Explicit measures do not grow with font size.
|
|
15
|
+
export const SPACING_TYPES = Object.freeze(['paragraph', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
16
|
+
'list', 'item', 'table', 'picture', 'drawing', 'quote', 'code', 'rule', 'footnote', 'toc']);
|
|
17
|
+
export const SPACING_PRESETS = Object.freeze({compact: .7, standard: 1, roomy: 1.5});
|
|
18
|
+
const SPACING_SELECTORS = Object.freeze({paragraph: 'p', h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6',
|
|
19
|
+
list: ':is(ul,ol)', item: 'li', table: ':is(table,.table-scroll-wrap)', picture: 'p[data-md-picture]',
|
|
20
|
+
drawing: ':is(.diagram-block,p[data-md-picture]:has(img[data-md-svg],img[src^="data:image/svg+xml"]))', quote: 'blockquote',
|
|
21
|
+
code: 'pre', rule: 'hr:not(.footnotes-sep):not([data-md-break])', footnote: '.footnotes', toc: '.rapier-jump-list'});
|
|
22
|
+
const HEADING_LINE = [1.75 / 2.2, 1.5 / 1.75, 1.2 / 1.5, 1 / 1.25, .9 / 1.1, .82];
|
|
23
|
+
|
|
24
|
+
export function normalizeDocumentSpacing(value) {
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
if (Object.hasOwn(SPACING_PRESETS, value)) value = {preset: value};
|
|
27
|
+
else { try { value = JSON.parse(value); } catch (_) { return null; } }
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
32
|
+
if (key === 'preset') {
|
|
33
|
+
if (!Object.hasOwn(SPACING_PRESETS, entry)) return null;
|
|
34
|
+
result.preset = entry; continue;
|
|
35
|
+
}
|
|
36
|
+
if (!SPACING_TYPES.includes(key) || !entry || typeof entry !== 'object' || Array.isArray(entry)) return null;
|
|
37
|
+
const row = {};
|
|
38
|
+
for (const [field, number] of Object.entries(entry)) {
|
|
39
|
+
if (!['before', 'after', 'line'].includes(field) || typeof number !== 'number' || !Number.isFinite(number) ||
|
|
40
|
+
number < (field === 'line' ? .5 : 0) || number > (field === 'line' ? 4 : 720)) return null;
|
|
41
|
+
row[field] = number;
|
|
42
|
+
}
|
|
43
|
+
result[key] = Object.freeze(row);
|
|
44
|
+
}
|
|
45
|
+
return Object.freeze(result);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function documentSpacing(settings, kind = 'paragraph') {
|
|
49
|
+
const type = SPACING_TYPES.includes(kind) ? kind : 'paragraph', spacing = settings?.spacing;
|
|
50
|
+
const preset = spacing?.preset || 'standard', scale = SPACING_PRESETS[preset];
|
|
51
|
+
const size = (settings?.fontsize?.unit || 1.1) / 1.1;
|
|
52
|
+
const heading = /^h([1-6])$/.exec(type), leading = settings?.linestretch?.factor ??
|
|
53
|
+
(preset === 'compact' ? 1.3 : preset === 'roomy' ? 1.85 : DOCUMENT_LEADING);
|
|
54
|
+
const before = heading ? 15 : type === 'footnote' ? 9 : 0;
|
|
55
|
+
const after = heading ? 3 : type === 'paragraph' ? 6 : type === 'item' ? 3 : type === 'footnote' ? 0 : 9;
|
|
56
|
+
const line = heading ? leading * HEADING_LINE[Number(heading[1]) - 1] : type === 'code' ? leading * .75 / .7386 : leading;
|
|
57
|
+
if (settings?.word) return {before: (heading ? 12 : type === 'footnote' ? 9 : 0) * scale,
|
|
58
|
+
after: (heading ? 6 : type === 'paragraph' ? 8 : type === 'item' || type === 'footnote' ? 0 : 9) * scale,
|
|
59
|
+
line: leading, ...spacing?.[type]};
|
|
60
|
+
return {before: before * scale * size, after: after * scale * size, line, ...spacing?.[type]};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Word's widely supported Calibri/11 pt Normal template. Explicit document settings always win.
|
|
64
|
+
export function wordDocumentSettings(settings) {
|
|
65
|
+
return {...settings, word: true, fontsize: settings?.fontsize || fontSize('11pt'),
|
|
66
|
+
mainfont: settings?.mainfont || documentFont('Calibri'), sansfont: settings?.sansfont || documentFont('Calibri'),
|
|
67
|
+
linestretch: settings?.linestretch || leading('1.08')};
|
|
68
|
+
}
|
|
69
|
+
export function documentHeadingSize(settings, level) {
|
|
70
|
+
const body = (settings?.fontsize?.unit || 1.1) * 12;
|
|
71
|
+
return body * (settings?.word ? [16/11,13/11,12/11,1,1,1] : [2.2,1.75,1.5,1.25,1.1,1])[level - 1];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function documentSpacingCss(settings, scope = '.md-render', defaults = false) {
|
|
75
|
+
if (!defaults && !settings?.spacing && !settings?.linestretch && !settings?.fontsize) return '';
|
|
76
|
+
const length = points => 'calc(' + Number((points / 12).toFixed(8)) + ' * var(--md-unit))';
|
|
77
|
+
let css = '';
|
|
78
|
+
for (const type of SPACING_TYPES) {
|
|
79
|
+
const row = documentSpacing(settings, type), selector = scope + ' ' + SPACING_SELECTORS[type];
|
|
80
|
+
css += selector + '{margin-block:' + length(row.before) + ' ' + length(row.after) + ';line-height:' + row.line + '}';
|
|
81
|
+
if (settings?.word && /^h[1-6]$/.test(type)) css += selector + '{font-size:' + length(documentHeadingSize(settings, Number(type[1]))) + '}';
|
|
82
|
+
}
|
|
83
|
+
// Compound blocks own the outside gap; the last child does not add it again.
|
|
84
|
+
css += scope + ' :is(li,td,th,blockquote)>:last-child{margin-bottom:0}' +
|
|
85
|
+
scope + ' li:last-child{margin-bottom:0}' + scope + ' li>:is(ul,ol){margin-bottom:0}' +
|
|
86
|
+
scope + ' .table-scroll-wrap>table{margin-block:0}' +
|
|
87
|
+
scope + ' pre{font-size:calc(var(--md-text-body) * .7386)}' +
|
|
88
|
+
scope + ' pre code{line-height:inherit}' + scope + ' .footnotes{padding-top:0}' +
|
|
89
|
+
scope + ' .footnotes-list{margin-block:0}' + scope + ' blockquote:not(.callout){padding-block:0}';
|
|
90
|
+
const item = documentSpacing(settings, 'item');
|
|
91
|
+
css += scope + ' ul.rapier-jump-list>li{padding-block:0;margin-block:' + length(item.before) + ' ' + length(item.after) + '}' +
|
|
92
|
+
scope + ' ul.rapier-jump-list>li:last-child{margin-bottom:0}';
|
|
93
|
+
for (const [type, selector] of [['item','li>p'], ['table',':is(td,th)>p'], ['quote','blockquote>p'], ['footnote','.footnotes p'], ['toc','.rapier-jump-list li']])
|
|
94
|
+
css += scope + ' ' + selector + '{line-height:' + documentSpacing(settings, type).line + '}';
|
|
95
|
+
return css;
|
|
96
|
+
}
|
|
97
|
+
|
|
13
98
|
function measure(value, positive = false) {
|
|
14
99
|
const text = unquote(value);
|
|
15
100
|
const number = NUMBER.test(text) ? Number(text) : NaN;
|
|
@@ -40,7 +125,7 @@ const PAPERS = Object.freeze({
|
|
|
40
125
|
});
|
|
41
126
|
const EMPTY = Object.freeze({
|
|
42
127
|
title: null, subtitle: null, lang: null, fontsize: null, mainfont: null, sansfont: null, monofont: null, defaultfont: null, linestretch: null,
|
|
43
|
-
papersize: null, geometry: null, pagestyle: null,
|
|
128
|
+
papersize: null, geometry: null, pagestyle: null, spacing: null,
|
|
44
129
|
});
|
|
45
130
|
|
|
46
131
|
function unquote(value) {
|
|
@@ -257,6 +342,7 @@ export function readDocumentSettings(markdown) {
|
|
|
257
342
|
return Object.freeze({
|
|
258
343
|
title: take('title') || null, subtitle: take('subtitle') || null, lang: languageTag(take('lang')),
|
|
259
344
|
fontsize, mainfont, sansfont, monofont, defaultfont, linestretch, papersize, geometry,
|
|
345
|
+
spacing: normalizeDocumentSpacing(take('spacing')),
|
|
260
346
|
pagestyle: page === 'plain' || page === 'empty' ? page : null,
|
|
261
347
|
});
|
|
262
348
|
}
|
|
@@ -276,14 +362,14 @@ export function updateDocumentSettings(markdown, patch) {
|
|
|
276
362
|
text: (source[field.start - 1] === ':' ? ' ' : '') + text + (source[field.end] === '#' ? ' ' : '')});
|
|
277
363
|
for (const [key, supplied] of Object.entries(patch || {})) {
|
|
278
364
|
const font = ['mainfont', 'sansfont', 'monofont', 'defaultfont'].includes(key);
|
|
279
|
-
if (!font && !['fontsize', 'linestretch'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
365
|
+
if (!font && !['fontsize', 'linestretch', 'spacing'].includes(key)) throw new TypeError('Unknown document setting: ' + key);
|
|
280
366
|
const matches = fields.filter(item => item.key === key);
|
|
281
367
|
if (matches.length > 1) throw new RangeError('Duplicate document setting: ' + key);
|
|
282
368
|
const field = matches[0];
|
|
283
|
-
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : leading(supplied);
|
|
369
|
+
const value = supplied === null ? null : font ? documentFont(supplied) : key === 'fontsize' ? fontSize(supplied) : key === 'spacing' ? normalizeDocumentSpacing(supplied) : leading(supplied);
|
|
284
370
|
if (supplied !== null && !value) throw new TypeError('Invalid document setting: ' + key);
|
|
285
|
-
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : String(value.factor);
|
|
286
|
-
if (value !== null && before[key] && (font ? before[key].family === value.family : key === 'fontsize' ? same(before.fontsize.unit, value.unit) : before.linestretch.factor === value.factor)) continue;
|
|
371
|
+
const encoded = value === null ? '' : font ? JSON.stringify(value.family) : key === 'fontsize' ? value.step : key === 'spacing' ? JSON.stringify(value) : String(value.factor);
|
|
372
|
+
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 : before.linestretch.factor === value.factor)) continue;
|
|
287
373
|
if (key === 'fontsize' && value !== null && fontSize(encoded)?.unit !== value.unit) refuse(key);
|
|
288
374
|
if (!field) { if (value !== null) append.push(key + ': ' + encoded + eol); continue; }
|
|
289
375
|
if (field.opaque || field.invalid || field.multiline || field.block) refuse(key);
|
|
@@ -327,7 +413,7 @@ export function documentSettingsStyle(settings, defaultFont) {
|
|
|
327
413
|
if (sans) decls.push('--md-font-sans:' + (sans.css || fontFamilyCss(sans.family)));
|
|
328
414
|
if (main) decls.push('font-family:' + (main.css || fontFamilyCss(main.family)));
|
|
329
415
|
if (mono) decls.push('--md-font-mono:' + (mono.css || fontFamilyCss(mono.family)));
|
|
330
|
-
return decls.length ? '.md-render{' + decls.join(';') + '}' : '';
|
|
416
|
+
return (decls.length ? '.md-render{' + decls.join(';') + '}' : '') + documentSpacingCss(settings);
|
|
331
417
|
}
|
|
332
418
|
|
|
333
419
|
// Null when the page keys do not change the print sheet. The view never asks.
|
|
@@ -474,6 +474,7 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
474
474
|
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};}
|
|
475
475
|
if (match[1].toLowerCase() === 'image/svg+xml') {
|
|
476
476
|
const svgText = new win.TextDecoder('utf-8', {fatal: true}).decode(bytes);
|
|
477
|
+
if (/<!\s*(?:DOCTYPE|ENTITY)\b/i.test(svgText)) throw new Error('SVG declarations are unsupported');
|
|
477
478
|
const parsed = new win.DOMParser().parseFromString(svgText, 'image/svg+xml');
|
|
478
479
|
if (parsed.querySelector('parsererror') || parsed.documentElement.localName !== 'svg') throw new Error('Invalid SVG image');
|
|
479
480
|
const svg = doc.importNode(parsed.documentElement, true);
|
|
@@ -618,8 +619,9 @@ export function mountHtmlBlocks(doc, options = {}) {
|
|
|
618
619
|
|
|
619
620
|
// Cold export uses the same opaque renderer, independently of the visible editor
|
|
620
621
|
// and its caches. The caller owns original bytes; only detached display nodes change.
|
|
621
|
-
export async function materializeObjects(root, {signal, width = 640, imageInfo, poster, bestEffort = false, issues = []} = {}) {
|
|
622
|
-
|
|
622
|
+
export async function materializeObjects(root, {signal, width = 640, imageInfo, poster, bestEffort = false, issues = [], document: live} = {}) {
|
|
623
|
+
// An export root is parsed in an inert document (no window, no body); the caller names the page whose sandboxed frames draw its objects.
|
|
624
|
+
const doc = root.ownerDocument.defaultView ? root.ownerDocument : live, win = doc.defaultView;
|
|
623
625
|
const images = [...root.querySelectorAll('img[data-rapier-html],img[data-rapier-animation]')];
|
|
624
626
|
if (images.length > 64) throw new Error('Object export count budget');
|
|
625
627
|
for (const image of images) {
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Pure text and markdown-it tokens: no DOM, no byte decoding, no globalThis.Rapier* (MIT kit; decoders stay in images/assets.mjs).
|
|
3
|
+
import {isWordSidecarBlock} from './word-sidecar.mjs';
|
|
3
4
|
|
|
4
5
|
export const IMAGE_LIMITS = Object.freeze({bytes: 16 * 1024 * 1024, sourceChars: 64 * 1024 * 1024, dimension: 16384, pixels: 24000000, assets: 1024});
|
|
5
6
|
const parsers = new WeakMap();
|
|
@@ -93,6 +94,11 @@ const unheldLines = new Proxy(Array.prototype, {
|
|
|
93
94
|
// lines begins (undefined when the source ends before it). `env` is the whole pass's. The tables hold at most
|
|
94
95
|
// the lines between two visits: BLOCK_WINDOW of them, or as many as one block's rules read.
|
|
95
96
|
export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul = false) {
|
|
97
|
+
for (const window of blockWindows(source, bodyOffset, parser, env, keepNul)) visit(window);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The same tokenizer can pause between complete top-level windows; no grammar rule is suspended.
|
|
101
|
+
export function* blockWindows(source, bodyOffset, parser, env, keepNul = false) {
|
|
96
102
|
const lineEnds = source.slice(bodyOffset).replace(/\r\n?/g, '\n'), body = keepNul ? lineEnds : lineEnds.replace(/\0/g, '\ufffd');
|
|
97
103
|
// The body is the source with every CRLF one unit shorter: count the pairs before a body offset, from the last
|
|
98
104
|
// offset asked.
|
|
@@ -105,7 +111,7 @@ export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul
|
|
|
105
111
|
const tokens = [];
|
|
106
112
|
let origin = 0, first = 0, state;
|
|
107
113
|
// A line begins after the newline that ends the line before it. No rule writes eMarks.
|
|
108
|
-
const report = () =>
|
|
114
|
+
const report = () => ({tokens, firstLine: first, lineStart: line => {
|
|
109
115
|
if (!line) return sourceOffset(origin);
|
|
110
116
|
const end = Object.hasOwn(state.eMarks, line - 1) ? state.eMarks[line - 1] : body.length;
|
|
111
117
|
return end < body.length ? sourceOffset(end + 1) : undefined;
|
|
@@ -115,7 +121,7 @@ export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul
|
|
|
115
121
|
if (lines <= BLOCK_WINDOW) {
|
|
116
122
|
state = new parser.block.State(body, parser, env, tokens);
|
|
117
123
|
parser.block.tokenize(state, state.line, state.lineMax);
|
|
118
|
-
|
|
124
|
+
yield report(); return;
|
|
119
125
|
}
|
|
120
126
|
// The parser's own count of a last line that no newline ends: it does not count one of only spaces.
|
|
121
127
|
state = new parser.block.State(body.slice(body.lastIndexOf('\n') + 1), parser, env, tokens);
|
|
@@ -180,14 +186,14 @@ export function eachBlockWindow(source, bodyOffset, parser, env, visit, keepNul
|
|
|
180
186
|
else if (!parser.utils.isSpace(code)) break;
|
|
181
187
|
}
|
|
182
188
|
frontier = origin; ended = false; betweenBlocks = false;
|
|
183
|
-
state.line = 0; state.lineMax = lines - first;
|
|
189
|
+
state.line = 0; state.lineMax = lines - first; state.rapierFirstLine = first;
|
|
184
190
|
const outer = unheldLine;
|
|
185
191
|
unheldLine = answer;
|
|
186
192
|
let finished = true;
|
|
187
193
|
try { parser.block.tokenize(state, 0, state.lineMax); }
|
|
188
194
|
catch (error) { if (error !== WINDOW_END) throw error; finished = false; }
|
|
189
195
|
finally { unheldLine = outer; }
|
|
190
|
-
report();
|
|
196
|
+
yield report();
|
|
191
197
|
if (finished) return;
|
|
192
198
|
tokens.length = 0;
|
|
193
199
|
origin = state.eMarks[state.line - 1] + 1; first += state.line;
|
|
@@ -209,6 +215,19 @@ export function dataImage(url) {
|
|
|
209
215
|
|
|
210
216
|
// Markdown-it owns recognition, nesting, duplicate precedence and escapes; block parse only.
|
|
211
217
|
export function parseAssets(source, factory = configured || globalThis.markdownit, bodyOffset = 0) {
|
|
218
|
+
const steps = assetSteps(source, factory, bodyOffset);
|
|
219
|
+
let next = steps.next();
|
|
220
|
+
while (!next.done) next = steps.next();
|
|
221
|
+
return next.value;
|
|
222
|
+
}
|
|
223
|
+
export async function parseAssetsWork(source, factory, bodyOffset, work) {
|
|
224
|
+
const steps = assetSteps(source, factory, bodyOffset);
|
|
225
|
+
work.signal?.throwIfAborted();
|
|
226
|
+
let next = steps.next();
|
|
227
|
+
while (!next.done) { await work.yield(); work.signal?.throwIfAborted(); next = steps.next(); }
|
|
228
|
+
return next.value;
|
|
229
|
+
}
|
|
230
|
+
function* assetSteps(source, factory = configured || globalThis.markdownit, bodyOffset = 0) {
|
|
212
231
|
if (typeof source !== 'string') return fail('image_source_invalid');
|
|
213
232
|
if (source.length > IMAGE_LIMITS.sourceChars) return fail('image_source_limit');
|
|
214
233
|
if (!source.includes(']:')) return {assets: new Map(), blocks: [], duplicateLabels: new Set(), appendixStart: source.length, references: Object.create(null)};
|
|
@@ -218,7 +237,7 @@ export function parseAssets(source, factory = configured || globalThis.markdowni
|
|
|
218
237
|
if (definition) return definition.index;
|
|
219
238
|
const parser = parserFor(factory), env = {};
|
|
220
239
|
const assets = new Map(), blocks = [], seen = new Set(), duplicateLabels = new Set();
|
|
221
|
-
|
|
240
|
+
for (const {tokens, lineStart} of blockWindows(source, bodyOffset, parser, env)) {
|
|
222
241
|
for (const token of tokens) {
|
|
223
242
|
if (token.type !== 'reference_definition' || !token.map) continue;
|
|
224
243
|
const id = token.meta.label, definition = token.meta.mdImageDefinition, first = !seen.has(id);
|
|
@@ -241,7 +260,8 @@ export function parseAssets(source, factory = configured || globalThis.markdowni
|
|
|
241
260
|
blocks.push(row);
|
|
242
261
|
if (first) assets.set(id, row);
|
|
243
262
|
}
|
|
244
|
-
|
|
263
|
+
yield;
|
|
264
|
+
}
|
|
245
265
|
const references = env.references || Object.create(null);
|
|
246
266
|
if (assets.size > IMAGE_LIMITS.assets) for (const asset of assets.values()) asset.status = 'asset_count_limit';
|
|
247
267
|
let appendixStart = source.length;
|
|
@@ -562,6 +582,7 @@ export function assetOmissions(source) {
|
|
|
562
582
|
return spans;
|
|
563
583
|
}
|
|
564
584
|
export function isAssetBlock(raw) {
|
|
585
|
+
if (isWordSidecarBlock(raw)) return true;
|
|
565
586
|
if (typeof raw !== 'string' || !/data:image\//i.test(raw)) return false;
|
|
566
587
|
const parsed = parseAssets(raw);
|
|
567
588
|
if (!parsed.blocks.length) return false;
|
package/dist/spec/md-layout.mjs
CHANGED
|
@@ -169,9 +169,9 @@ export function wrapTextBlock(element) {
|
|
|
169
169
|
|
|
170
170
|
// The one wrap-owner rule (layout/browser.js, kit/render.mjs, editor/share.js): text after, else before, skipping pictures,
|
|
171
171
|
// metadata and empty paragraphs; any other block is a barrier in that direction.
|
|
172
|
-
export function wrapNeighbour(node, classify) {
|
|
173
|
-
for (const
|
|
174
|
-
for (let sibling = step(node); sibling; sibling = step(sibling)) {
|
|
172
|
+
export function wrapNeighbour(node, classify, step = (n, direction) => n && n[direction > 0 ? 'nextElementSibling' : 'previousElementSibling']) {
|
|
173
|
+
for (const direction of [1, -1]) {
|
|
174
|
+
for (let sibling = step(node, direction); sibling; sibling = step(sibling, direction)) {
|
|
175
175
|
const kind = classify(sibling);
|
|
176
176
|
if (kind && typeof kind === 'object') return kind.stop;
|
|
177
177
|
if (kind === 'metadata') continue;
|
package/dist/spec/md-source.mjs
CHANGED
|
@@ -115,9 +115,13 @@ export function markdownSourcePositions(source, parser) {
|
|
|
115
115
|
|
|
116
116
|
// Capture only table-cell provenance while the parser still owns container-adjusted row
|
|
117
117
|
// bounds. Looking for cell text in a whole row aliases literal pipes and quote/list markers.
|
|
118
|
-
export function markdownTableSources(source, parser, environment, lineStarts = null) {
|
|
118
|
+
export function markdownTableSources(source, parser, environment, lineStarts = null, parse = null) {
|
|
119
119
|
const cells = new WeakMap();
|
|
120
|
-
|
|
120
|
+
const read = owner => {
|
|
121
|
+
if (parse) return parse(owner, source, environment).then(tokens => ({tokens, cells}));
|
|
122
|
+
return {tokens: owner.parse(source, environment), cells};
|
|
123
|
+
};
|
|
124
|
+
if (!source.includes('|')) return read(parser);
|
|
121
125
|
const lines = lineStarts || [0];
|
|
122
126
|
if (!lineStarts) for (const row of source.matchAll(/\r\n?|\n/g)) lines.push(row.index + row[0].length);
|
|
123
127
|
const owner = Object.create(parser), block = Object.create(parser.block), ruler = Object.create(parser.block.ruler);
|
|
@@ -131,14 +135,14 @@ export function markdownTableSources(source, parser, environment, lineStarts = n
|
|
|
131
135
|
if (token.type === 'tr_open') {
|
|
132
136
|
const row = token.map[0], from = state.bMarks[row] + state.tShift[row];
|
|
133
137
|
const line = state.src.lastIndexOf('\n', from - 1) + 1;
|
|
134
|
-
cell = tableSource(state.src.slice(from, state.eMarks[row]), lines[row] + from - line);
|
|
138
|
+
cell = tableSource(state.src.slice(from, state.eMarks[row]), lines[row + (state.rapierFirstLine || 0)] + from - line);
|
|
135
139
|
} else if (token.type === 'inline' && cell) cells.set(token, cell(token.content));
|
|
136
140
|
else if (token.type === 'tr_close') cell = null;
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
return matched;
|
|
140
144
|
});
|
|
141
|
-
return
|
|
145
|
+
return read(owner);
|
|
142
146
|
}
|
|
143
147
|
|
|
144
148
|
function tableSource(row, base) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// An inert document asset, kept in Source and excluded from the reading surface.
|
|
3
|
+
// This is recognition only: interchange/docx.mjs admits the bounded JSON and native XML.
|
|
4
|
+
export const WORD_SIDECAR_OPEN = '<!-- rapier-word:v1\n';
|
|
5
|
+
export function isWordSidecarBlock(raw) {
|
|
6
|
+
return typeof raw === 'string' && /^<!-- rapier-word:v1\r?\n[^\r\n]*\r?\n-->[ \t\r\n]*$/.test(raw);
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
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",
|
|
@@ -40,17 +40,34 @@
|
|
|
40
40
|
"main": "./dist/kit/index.mjs",
|
|
41
41
|
"exports": {
|
|
42
42
|
".": "./dist/kit/index.mjs",
|
|
43
|
+
"./markdown-it": {
|
|
44
|
+
"types": "./dist/kit/markdown-it.d.mts",
|
|
45
|
+
"default": "./dist/kit/markdown-it.mjs"
|
|
46
|
+
},
|
|
43
47
|
"./layout": "./dist/kit/layout.mjs",
|
|
44
48
|
"./marks": "./dist/kit/marks.mjs",
|
|
45
49
|
"./model": "./dist/kit/model.mjs",
|
|
46
50
|
"./typography": "./dist/kit/typography.mjs",
|
|
47
|
-
"./will":
|
|
51
|
+
"./will": {
|
|
52
|
+
"types": "./dist/kit/will.d.mts",
|
|
53
|
+
"default": "./dist/kit/will.mjs"
|
|
54
|
+
},
|
|
48
55
|
"./assets": "./dist/kit/assets.mjs",
|
|
49
56
|
"./style.css": "./style.css",
|
|
50
57
|
"./package.json": "./package.json",
|
|
51
58
|
"./render": "./dist/kit/render-entry.mjs",
|
|
52
|
-
"./rapier-ledger":
|
|
53
|
-
|
|
59
|
+
"./rapier-ledger": {
|
|
60
|
+
"types": "./dist/kit/ledger.d.mts",
|
|
61
|
+
"default": "./dist/kit/ledger.mjs"
|
|
62
|
+
},
|
|
63
|
+
"./pdf": {
|
|
64
|
+
"types": "./dist/kit/pdf-source.d.mts",
|
|
65
|
+
"default": "./dist/kit/pdf-source.mjs"
|
|
66
|
+
},
|
|
67
|
+
"./ledger/carried": {
|
|
68
|
+
"types": "./dist/kit/ledger/carried.d.mts",
|
|
69
|
+
"default": "./dist/kit/ledger/carried.mjs"
|
|
70
|
+
}
|
|
54
71
|
},
|
|
55
72
|
"repository": {
|
|
56
73
|
"type": "git",
|