rapier-markdown-kit 1.1.78 → 1.1.80
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/dist/kit/render-markdown.mjs +489 -12
- package/dist/kit/render-print.mjs +264 -3
- package/dist/kit/render-sanitize.mjs +252 -13
- package/dist/kit/render-styles.mjs +5 -5
- package/dist/kit/render.mjs +33 -11
- package/dist/spec/md-assets.mjs +3 -2
- package/package.json +1 -1
- package/style.css +73 -187
|
@@ -1,52 +1,263 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
const RAPIER_SANITIZE_FORBID_TAGS = Object.freeze(['form', 'style', 'dialog', 'template', 'iframe']);
|
|
3
3
|
const RAPIER_SANITIZE_FORBID_ATTR = Object.freeze(['action', 'formaction', 'form', 'for']);
|
|
4
|
+
// The document profiles use one allowlist in a browser and in an inert server parser.
|
|
5
|
+
const RAPIER_SANITIZE_PROFILES = Object.freeze({
|
|
6
|
+
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"]),
|
|
7
|
+
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"]),
|
|
8
|
+
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"]),
|
|
9
|
+
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"]),
|
|
10
|
+
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"]),
|
|
11
|
+
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"]),
|
|
12
|
+
xmlAttributes: Object.freeze(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),
|
|
13
|
+
});
|
|
14
|
+
const RAPIER_SANITIZE_DROP_CONTENTS = Object.freeze('annotation-xml.audio.colgroup.desc.foreignobject.head.iframe.math.mi.mn.mo.ms.mtext.noembed.noframes.noscript.plaintext.script.selectedcontent.style.svg.template.thead.title.video.xmp'.split('.'));
|
|
15
|
+
function _rapierSanitizeProfile(...kinds) {
|
|
16
|
+
return {ALLOWED_TAGS: Object.freeze(['#text', ...new Set(kinds.flatMap(kind => RAPIER_SANITIZE_PROFILES[kind + 'Tags']))]),
|
|
17
|
+
ALLOWED_ATTR: Object.freeze([...new Set(kinds.flatMap(kind => RAPIER_SANITIZE_PROFILES[kind + 'Attributes']).concat(kinds.some(kind => kind !== 'html') ? RAPIER_SANITIZE_PROFILES.xmlAttributes : []))]),
|
|
18
|
+
FORBID_CONTENTS: RAPIER_SANITIZE_DROP_CONTENTS};
|
|
19
|
+
}
|
|
4
20
|
const RAPIER_SANITIZE_OPTIONS = Object.freeze({
|
|
5
21
|
render: Object.freeze({
|
|
6
|
-
|
|
22
|
+
..._rapierSanitizeProfile('html', 'svg', 'math'),
|
|
7
23
|
FORBID_TAGS: RAPIER_SANITIZE_FORBID_TAGS,
|
|
8
24
|
FORBID_ATTR: RAPIER_SANITIZE_FORBID_ATTR,
|
|
9
25
|
}),
|
|
10
26
|
paste: Object.freeze({
|
|
11
|
-
|
|
27
|
+
..._rapierSanitizeProfile('html'),
|
|
12
28
|
FORBID_TAGS: RAPIER_SANITIZE_FORBID_TAGS,
|
|
13
29
|
FORBID_ATTR: RAPIER_SANITIZE_FORBID_ATTR,
|
|
14
30
|
}),
|
|
15
31
|
snippet: Object.freeze({
|
|
16
|
-
|
|
32
|
+
..._rapierSanitizeProfile('html'),
|
|
17
33
|
FORBID_TAGS: RAPIER_SANITIZE_FORBID_TAGS,
|
|
18
34
|
FORBID_ATTR: Object.freeze([...'style']),
|
|
19
35
|
}),
|
|
20
36
|
diagram: Object.freeze({
|
|
21
|
-
|
|
37
|
+
..._rapierSanitizeProfile('html', 'svg'),
|
|
22
38
|
FORBID_TAGS: Object.freeze(['form', 'dialog', 'template', 'iframe', 'script', 'foreignobject']),
|
|
23
39
|
FORBID_ATTR: RAPIER_SANITIZE_FORBID_ATTR,
|
|
24
40
|
}),
|
|
25
41
|
});
|
|
26
42
|
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
const DIAGRAM_LABEL_HTML = new Set('div span p br strong em b i s u del code sub sup'.split(' '));
|
|
44
|
+
const DIAGRAM_LABEL_MATH = new Set(('math mrow mi mn mo mtext mspace ms msub msup msubsup mfrac msqrt mroot mstyle merror '
|
|
45
|
+
+ 'mpadded mphantom mfenced menclose munder mover munderover mtable mtr mtd mlabeledtr mmultiscripts mprescripts none semantics').split(' '));
|
|
46
|
+
const DIAGRAM_LABEL_MATH_ATTRIBUTES = new Set(('display mathvariant mathsize mathcolor mathbackground dir accent accentunder '
|
|
47
|
+
+ 'columnalign columnlines columnspacing columnspan columnwidth depth displaystyle equalcolumns equalrows fence frame framespacing '
|
|
48
|
+
+ 'height largeop linebreak linethickness lspace maxsize minsize movablelimits notation rowalign rowlines rowspacing rowspan rspace '
|
|
49
|
+
+ 'scriptlevel scriptminsize scriptsizemultiplier separator stretchy subscriptshift superscriptshift valign voffset width').split(' '));
|
|
50
|
+
const DIAGRAM_LABEL_CLASSES = new Set('nodeLabel edgeLabel labelBkg markdown-node-label katex messageText'.split(' '));
|
|
51
|
+
const DIAGRAM_LABEL_CSS = new Set(('color fill stroke background-color font-family font-size font-style font-weight font-variant '
|
|
52
|
+
+ 'font-variant-ligatures font-feature-settings line-height letter-spacing word-spacing text-align vertical-align white-space '
|
|
53
|
+
+ 'word-break overflow-wrap text-wrap text-transform text-decoration text-decoration-line text-decoration-style text-decoration-color '
|
|
54
|
+
+ 'text-decoration-thickness text-underline-offset align-items align-content justify-content justify-items flex-direction flex-wrap '
|
|
55
|
+
+ 'width height min-width min-height max-width max-height margin margin-top margin-right margin-bottom margin-left '
|
|
56
|
+
+ 'padding padding-top padding-right padding-bottom padding-left').split(' '));
|
|
57
|
+
const DIAGRAM_LABEL_SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
58
|
+
const DIAGRAM_LABEL_HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
59
|
+
const DIAGRAM_LABEL_MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
|
|
60
|
+
|
|
61
|
+
// Snapshot current math text before a Markdown writer collapses whitespace. Rendered SVG
|
|
62
|
+
// glyphs have no textContent: put their retained source in this inert copy so the writer's
|
|
63
|
+
// blank-node rule cannot discard an equation or its containing paragraph/list item.
|
|
29
64
|
function prepareMathSource(input, DOMParser) {
|
|
30
65
|
const html = typeof input === 'string';
|
|
31
|
-
const MARK = 'span[data-rapier-math-source]';
|
|
32
|
-
if (html ? !/data-rapier-math-source/i.test(input) : typeof input?.cloneNode !== 'function' ||
|
|
66
|
+
const MARK = 'span[data-rapier-math-source],span.math-rendered[data-math-src]';
|
|
67
|
+
if (html ? !/data-(?:rapier-math-source|math-src)/i.test(input) : typeof input?.cloneNode !== 'function' ||
|
|
33
68
|
!(input.matches?.(MARK) || input.querySelector?.(MARK))) return input;
|
|
34
69
|
const root = html ? new DOMParser().parseFromString(
|
|
35
70
|
'<x-turndown id="turndown-root">' + input + '</x-turndown>', 'text/html').getElementById('turndown-root') : input.cloneNode(true);
|
|
36
71
|
const nodes = Array.from(root.querySelectorAll(MARK));
|
|
37
|
-
const sourceRoot = root.nodeName === 'SPAN' && root.
|
|
72
|
+
const sourceRoot = root.nodeName === 'SPAN' && root.matches(MARK);
|
|
38
73
|
if (sourceRoot) nodes.push(root);
|
|
39
74
|
for (const node of nodes) {
|
|
75
|
+
if (node.classList.contains('math-rendered') && node.hasAttribute('data-math-src')) {
|
|
76
|
+
try { node.textContent = decodeURIComponent(node.getAttribute('data-math-src')); } catch (_) {}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
40
79
|
try { node.setAttribute('data-rapier-math-source', encodeURIComponent(node.textContent || '')); }
|
|
41
80
|
catch (_) { node.removeAttribute('data-rapier-math-source'); }
|
|
42
81
|
}
|
|
43
82
|
return sourceRoot ? root.outerHTML : root;
|
|
44
83
|
}
|
|
45
84
|
|
|
85
|
+
// An inert parser and a string sanitizer use the same profile as DOMPurify. Neither port may
|
|
86
|
+
// execute document scripts or fetch resources. HTML5 parsing precedes filtering so foreign
|
|
87
|
+
// content, entities and malformed table markup cross the same boundary as a browser's parser.
|
|
88
|
+
function createInertHtmlSanitizer({parseFragment, serialize, sanitizeHtml, safeRasterDataUrl}) {
|
|
89
|
+
if (![parseFragment, serialize, sanitizeHtml, safeRasterDataUrl].every(port => typeof port === 'function'))
|
|
90
|
+
throw new TypeError('The inert HTML sanitizer requires every parser and filtering port');
|
|
91
|
+
const uri = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i;
|
|
92
|
+
const uriSafe = new Set(['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);
|
|
93
|
+
const schemes = ['http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp', 'matrix'];
|
|
94
|
+
const htmlNamespace = 'http://www.w3.org/1999/xhtml', svgNamespace = 'http://www.w3.org/2000/svg', mathNamespace = 'http://www.w3.org/1998/Math/MathML';
|
|
95
|
+
const svgTags = new Set(RAPIER_SANITIZE_PROFILES.svgTags), mathTags = new Set(RAPIER_SANITIZE_PROFILES.mathTags);
|
|
96
|
+
const mathIntegration = new Set(['mi', 'mo', 'mn', 'ms', 'mtext']), htmlIntegration = new Set(['annotation-xml']);
|
|
97
|
+
const sharedHtmlSvg = new Set(['title', 'style', 'font', 'a', 'script']);
|
|
98
|
+
// DOMPurify's default namespace admission applies in addition to its tag profile. In
|
|
99
|
+
// particular a MathML name parsed as HTML, or HTML inside SVG desc, is not admitted.
|
|
100
|
+
const validNamespace = (node, parent) => {
|
|
101
|
+
const tag = node.tagName.toLowerCase(), parentTag = (parent.tagName || 'template').toLowerCase();
|
|
102
|
+
const namespace = node.namespaceURI, parentNamespace = parent.namespaceURI || htmlNamespace;
|
|
103
|
+
if (namespace === svgNamespace) return parentNamespace === htmlNamespace ? tag === 'svg'
|
|
104
|
+
: parentNamespace === mathNamespace ? tag === 'svg' && (parentTag === 'annotation-xml' || mathIntegration.has(parentTag)) : svgTags.has(tag);
|
|
105
|
+
if (namespace === mathNamespace) return parentNamespace === htmlNamespace ? tag === 'math'
|
|
106
|
+
: parentNamespace === svgNamespace ? tag === 'math' && htmlIntegration.has(parentTag) : mathTags.has(tag);
|
|
107
|
+
return namespace === htmlNamespace && !(parentNamespace === svgNamespace && !htmlIntegration.has(parentTag))
|
|
108
|
+
&& !(parentNamespace === mathNamespace && !mathIntegration.has(parentTag))
|
|
109
|
+
&& !mathTags.has(tag) && (sharedHtmlSvg.has(tag) || !svgTags.has(tag));
|
|
110
|
+
};
|
|
111
|
+
return function sanitize(value, context = 'render') {
|
|
112
|
+
const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
|
|
113
|
+
const options = RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
|
|
114
|
+
const forbiddenTags = new Set(options.FORBID_TAGS || []), forbiddenAttributes = new Set(options.FORBID_ATTR || []);
|
|
115
|
+
const tags = new Set(options.ALLOWED_TAGS.filter(tag => tag !== '#text' && !forbiddenTags.has(tag)));
|
|
116
|
+
const attributes = new Set(options.ALLOWED_ATTR);
|
|
117
|
+
const tree = parseFragment(String(value == null ? '' : value));
|
|
118
|
+
const dropContents = new Set(RAPIER_SANITIZE_DROP_CONTENTS);
|
|
119
|
+
const admitTree = parent => {
|
|
120
|
+
for (let index = 0; index < (parent.childNodes || []).length;) {
|
|
121
|
+
const node = parent.childNodes[index], tag = node.tagName?.toLowerCase();
|
|
122
|
+
if (tag && !tags.has(tag)) {
|
|
123
|
+
const children = dropContents.has(tag) ? [] : node.childNodes || [];
|
|
124
|
+
for (const child of children) child.parentNode = parent;
|
|
125
|
+
parent.childNodes.splice(index, 1, ...children);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (tag && !validNamespace(node, parent) || node.nodeName === '#comment') {
|
|
129
|
+
parent.childNodes.splice(index, 1); continue;
|
|
130
|
+
}
|
|
131
|
+
admitTree(node); index++;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
admitTree(tree);
|
|
135
|
+
const admitNames = node => {
|
|
136
|
+
if (node.tagName && tags.has(node.tagName.toLowerCase())) tags.add(node.tagName);
|
|
137
|
+
for (const child of node.childNodes || []) admitNames(child);
|
|
138
|
+
};
|
|
139
|
+
admitNames(tree);
|
|
140
|
+
const cleaned = sanitizeHtml(serialize(tree), {
|
|
141
|
+
allowedTags: [...tags], allowedAttributes: false,
|
|
142
|
+
disallowedTagsMode: 'discard', nonTextTags: RAPIER_SANITIZE_DROP_CONTENTS,
|
|
143
|
+
allowedSchemes: schemes, allowedSchemesByTag: {img: [...schemes, 'data']},
|
|
144
|
+
allowedSchemesAppliedToAttributes: ['href', 'src', 'xlink:href', 'cite', 'background', 'poster'],
|
|
145
|
+
parseStyleAttributes: false,
|
|
146
|
+
parser: {lowerCaseTags: false, lowerCaseAttributeNames: false, decodeEntities: true},
|
|
147
|
+
transformTags: {'*': (tagName, input) => {
|
|
148
|
+
const attribs = Object.create(null), tag = tagName.toLowerCase();
|
|
149
|
+
for (const [name, inputValue] of Object.entries(input)) {
|
|
150
|
+
const key = name.toLowerCase();
|
|
151
|
+
const value = name === 'value' ? inputValue : inputValue.trim();
|
|
152
|
+
if (forbiddenAttributes.has(key) || key.startsWith('on')) continue;
|
|
153
|
+
if (/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i.test(value)
|
|
154
|
+
|| key === 'attributename' && value.includes('href')) continue;
|
|
155
|
+
const data = /^data-[\-\w.\u00b7-\uffff]+$/.test(key), aria = /^aria-[\-\w]+$/.test(key);
|
|
156
|
+
const pageBreak = key === 'contenteditable' && value === 'false' && tag === 'div' && input['data-md-break'] === 'page';
|
|
157
|
+
if (!(data && selected !== 'raw') && !aria && !pageBreak && !attributes.has(key)) continue;
|
|
158
|
+
const compact = value.replace(/[\u0000-\u0020\u00a0\u1680\u180e\u2000-\u2029\u205f\u3000]/g, '');
|
|
159
|
+
if (!data && !aria && !pageBreak && !uriSafe.has(key) && compact) {
|
|
160
|
+
if (/^data:/i.test(compact)) {
|
|
161
|
+
if (key !== 'src' || tag !== 'img' || !safeRasterDataUrl(compact)) continue;
|
|
162
|
+
} else if (!uri.test(compact)) continue;
|
|
163
|
+
}
|
|
164
|
+
attribs[name] = value;
|
|
165
|
+
}
|
|
166
|
+
if ((tag === 'a' || tag === 'area') && attribs.target?.trim().toLowerCase() === '_blank') attribs.rel = 'noopener noreferrer';
|
|
167
|
+
return {tagName, attribs};
|
|
168
|
+
}},
|
|
169
|
+
});
|
|
170
|
+
return serialize(parseFragment(cleaned));
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
46
174
|
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
47
175
|
function createRenderSanitizer(runtime) {
|
|
48
|
-
const {CSSStyleSheet, DOMPurify, RAPIER_RASTER_DATA_URL_RE, URL, _rapierCssDeclaration, _rapierDropRemoteDeclarations, _rapierRemoteContent, _rapierRuleDescriptorIsRemote, _rapierSanitizeRuntime,
|
|
176
|
+
const {CSSStyleSheet, DOMPurify, RAPIER_RASTER_DATA_URL_RE, URL, _rapierCssDeclaration, _rapierDropRemoteDeclarations, _rapierRemoteContent, _rapierRuleDescriptorIsRemote, _rapierSanitizeRuntime, document, globalThis, location} = runtime;
|
|
49
177
|
|
|
178
|
+
// Only the diagram reader and its styled export admit static foreignObject labels. The
|
|
179
|
+
// ordinary SVG reader still rejects HTML subdocuments. The export class selects scope;
|
|
180
|
+
// every descendant is checked again, so a copied class never confers authority.
|
|
181
|
+
function diagramLabelRoot(node) {
|
|
182
|
+
for (let current = node; current?.nodeType === 1; current = current.parentElement) {
|
|
183
|
+
if (current.localName?.toLowerCase() === 'foreignobject') return current;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function diagramLabelStyle(property, value) {
|
|
189
|
+
property = String(property).toLowerCase();
|
|
190
|
+
value = String(value).trim();
|
|
191
|
+
if (property === 'display') return /^(?:none|block|inline|inline-block|table|table-cell|flex|inline-flex)$/.test(value);
|
|
192
|
+
if (/^overflow(?:-[xy])?$/.test(property)) return /^(?:hidden|clip)$/.test(value);
|
|
193
|
+
if (property === 'contain') return value === 'content';
|
|
194
|
+
if (property === 'isolation') return value === 'isolate';
|
|
195
|
+
if (!DIAGRAM_LABEL_CSS.has(property)) return false;
|
|
196
|
+
const declaration = document.createElement('span').style;
|
|
197
|
+
declaration.setProperty(property, value);
|
|
198
|
+
const parsed = declaration.getPropertyValue(property);
|
|
199
|
+
if (!parsed || /(?:url|image-set|element|paint|attr)\s*\(/i.test(parsed)) return false;
|
|
200
|
+
// SVG paint accepts resource references; a custom property must not smuggle one in.
|
|
201
|
+
return !/^(?:fill|stroke)$/.test(property) || !/var\s*\(/i.test(parsed);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function diagramLabelElement(node) {
|
|
205
|
+
if (node.nodeType !== 1) return;
|
|
206
|
+
const label = diagramLabelRoot(node);
|
|
207
|
+
if (!label) return;
|
|
208
|
+
if (node === label) {
|
|
209
|
+
const svg = node.parentElement?.closest('svg');
|
|
210
|
+
if (node.namespaceURI !== DIAGRAM_LABEL_SVG_NAMESPACE || !svg || diagramLabelRoot(node.parentElement) ||
|
|
211
|
+
(_rapierSanitizeRuntime.context !== 'diagram' && !svg.classList.contains('rapier-diagram'))) node.remove();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const name = node.localName?.toLowerCase();
|
|
215
|
+
if (!(node.namespaceURI === DIAGRAM_LABEL_HTML_NAMESPACE && DIAGRAM_LABEL_HTML.has(name)) &&
|
|
216
|
+
!(node.namespaceURI === DIAGRAM_LABEL_MATH_NAMESPACE && DIAGRAM_LABEL_MATH.has(name))) node.remove();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function diagramLabelAttribute(node, data) {
|
|
220
|
+
const label = diagramLabelRoot(node);
|
|
221
|
+
if (!label) return;
|
|
222
|
+
data.forceKeepAttr = false;
|
|
223
|
+
const name = String(data.attrName).toLowerCase();
|
|
224
|
+
const value = String(data.attrValue);
|
|
225
|
+
if (name === 'style') {
|
|
226
|
+
const declaration = document.createElement('span').style;
|
|
227
|
+
declaration.cssText = value;
|
|
228
|
+
for (const property of Array.from(declaration)) {
|
|
229
|
+
if (!diagramLabelStyle(property, declaration.getPropertyValue(property))) declaration.removeProperty(property);
|
|
230
|
+
}
|
|
231
|
+
data.attrValue = declaration.cssText;
|
|
232
|
+
data.keepAttr = !!data.attrValue;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (name === 'class') {
|
|
236
|
+
data.attrValue = value.split(/\s+/).filter(token => DIAGRAM_LABEL_CLASSES.has(token)).join(' ');
|
|
237
|
+
data.keepAttr = !!data.attrValue;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (name === 'xmlns') {
|
|
241
|
+
data.keepAttr = value === node.namespaceURI;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (node === label) {
|
|
245
|
+
data.keepAttr = name === 'transform' || /^(?:x|y|width|height)$/.test(name) &&
|
|
246
|
+
/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?(?:px|%)?$/i.test(value) && Number.isFinite(parseFloat(value)) &&
|
|
247
|
+
(!/^(?:width|height)$/.test(name) || parseFloat(value) >= 0);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
data.keepAttr = node.namespaceURI === DIAGRAM_LABEL_MATH_NAMESPACE
|
|
251
|
+
? DIAGRAM_LABEL_MATH_ATTRIBUTES.has(name)
|
|
252
|
+
: name === 'dir' && /^(?:ltr|rtl|auto)$/.test(value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function diagramLabelContained(node) {
|
|
256
|
+
const label = diagramLabelRoot(node);
|
|
257
|
+
if (!label) return;
|
|
258
|
+
if (node === label) node.style.setProperty('overflow', 'hidden', 'important');
|
|
259
|
+
else if (node.parentElement === label) node.style.setProperty('contain', 'content', 'important');
|
|
260
|
+
}
|
|
50
261
|
|
|
51
262
|
function _rapierInstallSanitizeHooks() {
|
|
52
263
|
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
|
|
@@ -148,11 +359,27 @@ function sanitizeRapierHtml(value, context = 'render') {
|
|
|
148
359
|
// (the hook) without an id the chrome owns. 'source' is Markdown source HTML, written and never shown: the render
|
|
149
360
|
// profile's admission, and the block on remote content stays the reader's (a remote picture's URL is the person's words).
|
|
150
361
|
const selected = context === 'raw' || context === 'export' || context === 'source' || RAPIER_SANITIZE_OPTIONS[context] ? context : 'render';
|
|
151
|
-
|
|
362
|
+
let options = selected === 'raw' ? { ...RAPIER_SANITIZE_OPTIONS.render, ALLOW_DATA_ATTR: false } : RAPIER_SANITIZE_OPTIONS[selected] || RAPIER_SANITIZE_OPTIONS.render;
|
|
363
|
+
const labels = selected === 'diagram' || selected === 'export';
|
|
364
|
+
if (labels) {
|
|
365
|
+
// DOMPurify's USE_PROFILES replaces explicit allowlists. Add MathML to the
|
|
366
|
+
// selected profile without discarding its admitted HTML and SVG vocabulary.
|
|
367
|
+
options = {...options, ALLOWED_TAGS: [...new Set([...options.ALLOWED_TAGS, ...RAPIER_SANITIZE_PROFILES.mathTags])],
|
|
368
|
+
ALLOWED_ATTR: [...new Set([...options.ALLOWED_ATTR, ...RAPIER_SANITIZE_PROFILES.mathAttributes])], ADD_TAGS: ['foreignobject'],
|
|
369
|
+
HTML_INTEGRATION_POINTS: {foreignobject: true}, FORBID_TAGS: options.FORBID_TAGS.filter(tag => tag !== 'foreignobject')};
|
|
370
|
+
DOMPurify.addHook('uponSanitizeElement', diagramLabelElement);
|
|
371
|
+
DOMPurify.addHook('uponSanitizeAttribute', diagramLabelAttribute);
|
|
372
|
+
DOMPurify.addHook('afterSanitizeAttributes', diagramLabelContained);
|
|
373
|
+
}
|
|
152
374
|
_rapierSanitizeRuntime.context = selected;
|
|
153
375
|
try {
|
|
154
376
|
return DOMPurify.sanitize(value == null ? '' : String(value), options);
|
|
155
377
|
} finally {
|
|
378
|
+
if (labels) {
|
|
379
|
+
DOMPurify.removeHook('uponSanitizeElement', diagramLabelElement);
|
|
380
|
+
DOMPurify.removeHook('uponSanitizeAttribute', diagramLabelAttribute);
|
|
381
|
+
DOMPurify.removeHook('afterSanitizeAttributes', diagramLabelContained);
|
|
382
|
+
}
|
|
156
383
|
_rapierSanitizeRuntime.context = 'render';
|
|
157
384
|
}
|
|
158
385
|
}
|
|
@@ -237,6 +464,18 @@ function _rapierDropRemoteRules(rules, owner) {
|
|
|
237
464
|
}
|
|
238
465
|
}
|
|
239
466
|
|
|
240
|
-
|
|
467
|
+
function _rapierVerifyRasterBytes(bytes, mime) {
|
|
468
|
+
if (mime === 'image/jxl') return globalThis.RapierImageAssets.isJxl(bytes);
|
|
469
|
+
|
|
470
|
+
// declaration, doctype or generator/license comment before <svg itself, of whatever length its
|
|
471
|
+
|
|
472
|
+
if (mime === 'image/svg+xml') return /<svg[\s>]/i.test(new TextDecoder('utf-8', {fatal: false}).decode(bytes));
|
|
473
|
+
if (mime === 'image/png') return bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 && bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a;
|
|
474
|
+
if (mime === 'image/jpeg') return bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff;
|
|
475
|
+
if (mime === 'image/webp') return bytes.length >= 12 && String.fromCharCode(...bytes.slice(0, 4)) === 'RIFF' && String.fromCharCode(...bytes.slice(8, 12)) === 'WEBP';
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return {_rapierVerifyRasterBytes, _rapierInstallSanitizeHooks, sanitizeRapierHtml, diagramLabelStyle, escapeRapierHtmlText, _rapierChromeOwnsId, _rapierSafeRasterDataUrl, _rapierCssPresentationIsRemote, _rapierRemoteSubresourceOrigin, _rapierStyleWithoutRemoteUrls, _rapierStylesheetWithoutRemoteUrls, _rapierDropRemoteRules};
|
|
241
480
|
}
|
|
242
|
-
export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, RAPIER_SANITIZE_OPTIONS as options, prepareMathSource};
|
|
481
|
+
export {RAPIER_SANITIZE_FORBID_TAGS as forbidTags, RAPIER_SANITIZE_FORBID_ATTR as forbidAttributes, createRenderSanitizer, createInertHtmlSanitizer, RAPIER_SANITIZE_OPTIONS as options, prepareMathSource};
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
3
3
|
function createRenderStyles(runtime) {
|
|
4
4
|
const {_rapierArtifactAccent, _rapierStyleText} = runtime;
|
|
5
|
-
function _rapierArtifactThemeCss() {
|
|
5
|
+
function _rapierArtifactThemeCss(houseDefaults = false) {
|
|
6
6
|
// Geometry and the selected accent belong to this page; palette and type belong to the
|
|
7
7
|
// MIT reference sheet. Only the optional code lexer still reads the app token names.
|
|
8
|
-
const accent = _rapierArtifactAccent();
|
|
8
|
+
const accent = houseDefaults ? '' : ':root,.md-render{--md-color-accent:' + _rapierArtifactAccent() + '}';
|
|
9
9
|
return `
|
|
10
|
-
|
|
10
|
+
${accent}
|
|
11
11
|
html[data-rapier-theme="dark"]{color-scheme:dark}
|
|
12
12
|
html[data-rapier-theme="light"]{color-scheme:light}
|
|
13
13
|
html[data-rapier-theme="system"]{color-scheme:light dark}
|
|
@@ -71,11 +71,11 @@ function _rapierDocumentPrintCss(hostSelector) {
|
|
|
71
71
|
}`;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
function _rapierArtifactStyles(theme, includeFonts, printMode) {
|
|
74
|
+
function _rapierArtifactStyles(theme, includeFonts, printMode, houseDefaults = false) {
|
|
75
75
|
const parts = [];
|
|
76
76
|
if (includeFonts) parts.push(_rapierStyleText('rapier-font-style'));
|
|
77
77
|
parts.push(_rapierStyleText('rapier-content-style'));
|
|
78
|
-
parts.push(_rapierArtifactThemeCss());
|
|
78
|
+
parts.push(_rapierArtifactThemeCss(houseDefaults));
|
|
79
79
|
// Where the faces do not ride (the written page's policy carries no font-src), the reader's own sans is
|
|
80
80
|
// matched to Geist's metrics: one family per system face, src:local() only (fetched by nothing, so the
|
|
81
81
|
// policy is untouched), size-adjust from Geist's weighted average advance over the face's own (Geist 467
|
package/dist/kit/render.mjs
CHANGED
|
@@ -32,7 +32,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
32
32
|
|
|
33
33
|
// The semantic export root is detached and has never entered the live visibility queue.
|
|
34
34
|
// Await the same materialization owner before any projection clones or serializes it.
|
|
35
|
-
for (const diagram of context.semanticRoot.querySelectorAll('.diagram-block[data-diagram-src]')) {
|
|
35
|
+
for (const diagram of opts.kind === 'page' ? [] : context.semanticRoot.querySelectorAll('.diagram-block[data-diagram-src]')) {
|
|
36
36
|
await _rapierFillDiagram(diagram, true);
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -41,21 +41,23 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
41
41
|
const styledRoot = _rapierProjectStyledRoot(context.semanticRoot, {
|
|
42
42
|
metadata,
|
|
43
43
|
print: !!opts.print,
|
|
44
|
+
sourceCode: opts.kind === 'page',
|
|
44
45
|
baseName: base,
|
|
45
46
|
});
|
|
46
47
|
// _rapierProjectStyledRoot already returns the sanitized projection.
|
|
47
48
|
// Print never emits the layout reflow script (see below), so a print artifact has no use for
|
|
48
49
|
// this annotation -- skip it there rather than pay the cost for numbers nothing will read.
|
|
49
|
-
if (!opts.print) _rapierAnnotateExportBoxPolygons(styledRoot);
|
|
50
|
+
if (!opts.print && opts.kind !== 'page') _rapierAnnotateExportBoxPolygons(styledRoot);
|
|
50
51
|
// Share adds its nearest-side no-script float and long-code cap here. Offline-image
|
|
51
52
|
// admission above and the page policy below belong to this common writer, not its callers.
|
|
52
53
|
if (typeof opts.afterRoot === 'function') opts.afterRoot(styledRoot);
|
|
53
54
|
const carrier = opts.kind === 'standalone' ? await _rapierSharedSourceCarrier(context, styledRoot) : '';
|
|
54
|
-
const bodyHtml = styledRoot.innerHTML;
|
|
55
|
+
const bodyHtml = opts.kind === 'page' ? _rapierPageHtml(styledRoot) : styledRoot.innerHTML;
|
|
55
56
|
const theme = opts.print
|
|
56
57
|
? 'light'
|
|
58
|
+
: opts.kind === 'page' ? 'system'
|
|
57
59
|
: _rapierArtifactPreference('theme', ['system', 'light', 'dark']);
|
|
58
|
-
const highlights = _rapierArtifactPreference('highlights', ['accent', 'standard']);
|
|
60
|
+
const highlights = opts.kind === 'page' ? 'standard' : _rapierArtifactPreference('highlights', ['accent', 'standard']);
|
|
59
61
|
const versionMeta = document.querySelector('meta[name="rapier-version"]');
|
|
60
62
|
const version = versionMeta && versionMeta.content ? versionMeta.content.trim() : '0.0.0';
|
|
61
63
|
const bodyClass = theme === 'light' ? 'light' : '';
|
|
@@ -79,7 +81,7 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
79
81
|
const settings = _rapierDocumentSettingsOf(context.canonical);
|
|
80
82
|
const settingsCss = _rapierDocumentSettingsCss(settings);
|
|
81
83
|
const pageTitle = settings && settings.title ? globalThis.RapierMarkdownSpec.documentTitle(settings) : metadata.filename;
|
|
82
|
-
const css = _rapierArtifactStyles(theme, !!opts.print, !!opts.print || opts.kind === 'standalone')
|
|
84
|
+
const css = _rapierArtifactStyles(theme, !!opts.print, !!opts.print || opts.kind === 'standalone', opts.kind === 'page')
|
|
83
85
|
+ (opts.extraCss ? '\n\n' + opts.extraCss : '')
|
|
84
86
|
+ (willFont ? '\n\n@font-face{font-family:' + willFont.family + ';src:url(' + await _rapierBlobDataUrl(new Blob([willFont.bytes], { type: 'font/ttf' })) + ') format("truetype")}' : '')
|
|
85
87
|
+ (settingsCss ? '\n' + settingsCss : '');
|
|
@@ -90,11 +92,11 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
90
92
|
// The reflow script is written first, because whether it exists decides the policy. A page that carries no script says so: script-src 'none' refuses every
|
|
91
93
|
// script outright. Declaring a nonce a page has no use for is a widening, small but real -- anything that could inject markup into the file could read the
|
|
92
94
|
// nonce out of it and be admitted.
|
|
93
|
-
const layoutScript = !opts.print ? _rapierArtifactLayoutScript(styledRoot, nonce) : '';
|
|
95
|
+
const layoutScript = !opts.print && opts.kind !== 'page' ? _rapierArtifactLayoutScript(styledRoot, nonce) : '';
|
|
94
96
|
// The lexer for the page's code, under the same nonce, only when a block earned it: the CPU
|
|
95
97
|
// spans are the first paint, and where the reader's browser has WebGPU the lexer repaints them.
|
|
96
|
-
const lexerScript = !opts.print ? _rapierArtifactLexerScript(styledRoot, nonce) : '';
|
|
97
|
-
const inkScript = !opts.print ? _rapierArtifactInkScript(styledRoot, nonce) : '';
|
|
98
|
+
const lexerScript = !opts.print && opts.kind !== 'page' ? _rapierArtifactLexerScript(styledRoot, nonce) : '';
|
|
99
|
+
const inkScript = !opts.print && opts.kind !== 'page' ? _rapierArtifactInkScript(styledRoot, nonce) : '';
|
|
98
100
|
const page = '<!DOCTYPE html>\n'
|
|
99
101
|
+ '<html lang="en" data-rapier-theme="' + theme + '" data-highlights="' + highlights + '">\n'
|
|
100
102
|
+ '<head>\n<meta charset="UTF-8">\n' + (carrier ? RapierPageReturnAddress.PAGE_SEED + '\n' : '')
|
|
@@ -116,12 +118,31 @@ async function _rapierBuildArtifact(options, providedContext) {
|
|
|
116
118
|
html: page,
|
|
117
119
|
filename: base + '.html',
|
|
118
120
|
bodyHtml,
|
|
121
|
+
css,
|
|
119
122
|
docClass,
|
|
120
123
|
metadata,
|
|
121
124
|
documentTitle: settings && settings.title ? pageTitle : '',
|
|
122
125
|
};
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// Page files have one byte representation across native and inert DOM hosts. Attribute
|
|
129
|
+
// insertion order and a host's optional entity escapes are not document content.
|
|
130
|
+
function _rapierPageHtml(root) {
|
|
131
|
+
const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
132
|
+
const children = node => Array.from(node.childNodes || [], write).join('');
|
|
133
|
+
function write(node) {
|
|
134
|
+
if (node.nodeType === 3) return escapeRapierHtmlText(node.nodeValue || '');
|
|
135
|
+
if (node.nodeType !== 1) return '';
|
|
136
|
+
// This writer emits text/html; the HTML reader restores SVG's camel-cased names.
|
|
137
|
+
const tag = node.localName.toLowerCase();
|
|
138
|
+
const attributes = Array.from(node.attributes).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
139
|
+
.map(attribute => ' ' + attribute.name + '="' + escapeRapierHtmlText(attribute.value) + '"').join('');
|
|
140
|
+
const open = '<' + tag + attributes + '>';
|
|
141
|
+
return voidTags.has(tag) ? open : open + children(node) + '</' + tag + '>';
|
|
142
|
+
}
|
|
143
|
+
return children(root);
|
|
144
|
+
}
|
|
145
|
+
|
|
125
146
|
function _rapierProjectStyledRoot(semanticRoot, options) {
|
|
126
147
|
const opts = options || {};
|
|
127
148
|
const root = semanticRoot.cloneNode(true);
|
|
@@ -138,11 +159,12 @@ function _rapierProjectStyledRoot(semanticRoot, options) {
|
|
|
138
159
|
if (code) {
|
|
139
160
|
const lang = _rapierLanguageClass(code) || String(metadata.codeLang || '');
|
|
140
161
|
code.classList.add('artifact-code');
|
|
141
|
-
if (metadata.docKind === 'code') { _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang)); _rapierArtifactMarkLexed(code, lang); }
|
|
162
|
+
if (metadata.docKind === 'code' && !opts.sourceCode) { _rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang)); _rapierArtifactMarkLexed(code, lang); }
|
|
142
163
|
if (lang) code.classList.add('language-' + String(lang).replace(/[^a-z0-9_-]/gi, ''));
|
|
143
164
|
}
|
|
144
165
|
} else {
|
|
145
166
|
root.querySelectorAll('pre > code').forEach(code => {
|
|
167
|
+
if (opts.sourceCode) return;
|
|
146
168
|
const lang = _rapierLanguageClass(code) || 'text';
|
|
147
169
|
_rapierPaintCode(code, _rapierArtifactHighlight(code.textContent || '', lang));
|
|
148
170
|
_rapierArtifactMarkLexed(code, lang);
|
|
@@ -157,7 +179,7 @@ function _rapierProjectStyledRoot(semanticRoot, options) {
|
|
|
157
179
|
});
|
|
158
180
|
|
|
159
181
|
root.querySelectorAll('math[display="block"]').forEach(math => {
|
|
160
|
-
if (math.closest('.math-display-wrap')) return;
|
|
182
|
+
if (math.closest('svg') || math.closest('.math-display-wrap')) return;
|
|
161
183
|
const wrap = document.createElement('span');
|
|
162
184
|
wrap.className = 'math-display-wrap';
|
|
163
185
|
math.parentNode.insertBefore(wrap, math);
|
|
@@ -184,7 +206,7 @@ function _rapierProjectStyledRoot(semanticRoot, options) {
|
|
|
184
206
|
_rapierPrefixPortableAnchors(root, opts.baseName || metadata.filename || 'document');
|
|
185
207
|
|
|
186
208
|
const clean = document.createElement('div');
|
|
187
|
-
clean.innerHTML = sanitizeRapierHtml(root.innerHTML, 'export');
|
|
209
|
+
clean.innerHTML = sanitizeRapierHtml(root.innerHTML, opts.sourceCode ? 'source' : 'export');
|
|
188
210
|
return clean;
|
|
189
211
|
}
|
|
190
212
|
|
package/dist/spec/md-assets.mjs
CHANGED
|
@@ -370,7 +370,8 @@ export function serializeAsset(asset) {
|
|
|
370
370
|
return '[' + asset.label + ']: ' + asset.url + title;
|
|
371
371
|
}
|
|
372
372
|
// Pure: re-derived at commit against the moved document; appends only at the text's end.
|
|
373
|
-
|
|
373
|
+
// A replay supplies the retained definition's line ending instead of choosing one from later prose.
|
|
374
|
+
export function appendAssetText(source, asset, lineEnding) {
|
|
374
375
|
if (typeof source !== 'string' || !asset || normalizeLabel(asset.label) !== asset.id) return fail('image_source_invalid');
|
|
375
376
|
const parsed = documentAssets(source), title = assetTitle(asset.title);
|
|
376
377
|
const existing = [...parsed.assets.values()].find(row => row.url === asset.url && row.title === title);
|
|
@@ -382,7 +383,7 @@ export function appendAssetText(source, asset) {
|
|
|
382
383
|
return {source, id: asset.id, reference: asset.label, added: false, suffix: ''};
|
|
383
384
|
}
|
|
384
385
|
if (parsed.assets.size >= IMAGE_LIMITS.assets) return fail('image_asset_count_limit');
|
|
385
|
-
const eol = /\r\n|\n|\r/.exec(source)?.[0] || '\n', tail = /(?:\r\n?|\n)[ \t]*$/.exec(source);
|
|
386
|
+
const eol = lineEnding || /\r\n|\n|\r/.exec(source)?.[0] || '\n', tail = /(?:\r\n?|\n)[ \t]*$/.exec(source);
|
|
386
387
|
const separator = !source || tail && /[\r\n][ \t]*$/.test(source.slice(0, tail.index)) ? '' : tail ? eol : eol + eol;
|
|
387
388
|
const suffix = separator + block + eol, nextSource = source + suffix;
|
|
388
389
|
if (nextSource.length > IMAGE_LIMITS.sourceChars) return fail('image_source_limit');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rapier-markdown-kit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.80",
|
|
4
4
|
"description": "Read, render, write and style Self-contained Markdown, and carry its optional edit ledger: the shared HTML writer, semantic renderer, stylesheet, layout, marks, pictures, Will/1, replay, authorship and offline merge. MIT, no npm dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|