rapier-markdown-kit 1.1.79 → 1.1.81
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 +128 -8
- package/dist/kit/render-styles.mjs +5 -5
- package/dist/kit/render.mjs +41 -21
- package/dist/spec/md-assets.mjs +3 -2
- package/package.json +1 -1
- package/style.css +56 -63
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
const RAPIER_TABLE_CAPTION_PREFIX_RE = /^(?:Table:|:) /;
|
|
3
|
+
const RAPIER_PORTABLE_HIGHLIGHT_HEX = Object.freeze({
|
|
4
|
+
green: '#35c57a', red: '#f05a62', blue: '#4f91f7', yellow: '#e6b91e', purple: '#9b6dea',
|
|
5
|
+
});
|
|
6
|
+
const _RAPIER_RTL_SCRIPT = /[\p{Script=Hebrew}\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Samaritan}\p{Script=Mandaic}\p{Script=Adlam}\p{Script=Phoenician}\p{Script=Imperial_Aramaic}\p{Script=Palmyrene}\p{Script=Nabataean}\p{Script=Hatran}\p{Script=Old_South_Arabian}\p{Script=Old_North_Arabian}\p{Script=Mende_Kikakui}\p{Script=Old_Hungarian}\p{Script=Old_Sogdian}\p{Script=Sogdian}\p{Script=Elymaic}\p{Script=Chorasmian}\p{Script=Yezidi}\p{Script=Old_Uyghur}\p{Script=Hanifi_Rohingya}]/u;
|
|
3
7
|
// The document's renderer. DOM, codecs and host state are explicit inputs.
|
|
4
8
|
function createMarkdownRenderer(runtime) {
|
|
5
|
-
const {RAPIER_HIGHLIGHT_COLOR_BY_MARKER, RAPIER_MARKDOWN_SPEC, RAPIER_RENDERED_HEADING_SELECTOR, _rapierApplyMarkdownSpec,
|
|
9
|
+
const {Node, _rapierPrefixPortableAnchors, RAPIER_HIGHLIGHT_COLOR_BY_MARKER, RAPIER_MARKDOWN_SPEC, RAPIER_RENDERED_HEADING_SELECTOR, _rapierApplyMarkdownSpec, _rapierChromeOwnsId, _rapierCodeHtml, _rapierDeriveDarkColor, _rapierDormantHeadings, _rapierEmbedAssetSource, _rapierHeadingSlugBase, _rapierNextHeadingSlug, _rapierHighlightAdmitted, _rapierInstallMarkdownMath, _rapierMarkdownPreview, _rapierPlainLayout, _rapierProviders, _rapierRemoteContent, _rapierRemoteImagePlaceholder, _rapierRemoteSubresourceOrigin, _rapierRenderedAnchorOriginalIds, _rapierSourceCharEscaped, _rapierSplitOpeningFrontmatter, _rapierUiDiagram, _rapierUiMath, document, escapeRapierHtmlText, globalThis, sanitizeRapierHtml, window} = runtime;
|
|
6
10
|
let md = runtime.md;
|
|
7
11
|
function initMarkdownIt() {
|
|
8
12
|
if (typeof window.markdownit !== 'function') throw new Error('markdownit');
|
|
@@ -112,9 +116,10 @@ function initMarkdownIt() {
|
|
|
112
116
|
const layout = token.meta?.mdLayout?.imageOnly ? token.meta.mdLayout.marker : '';
|
|
113
117
|
const asset = globalThis.RapierImageAssets.dataImage(src), reference = token.meta?.mdImage?.reference;
|
|
114
118
|
// A connected host's own asset; a host without one (the server) supplies no port.
|
|
115
|
-
if (
|
|
119
|
+
if (!env.rapierPage && typeof globalThis.RapierEmbeddedImages?.imageHtml === 'function' &&
|
|
120
|
+
(_rapierEmbedAssetSource?.(src) || asset && (reference || asset.codec === 'image/jxl' || asset.codec === 'image/svg+xml'))) return globalThis.RapierEmbeddedImages.imageHtml(
|
|
116
121
|
reference, alt, title, size, layout, rawAlt, token.meta?.mdImage?.source, src);
|
|
117
|
-
if (!_rapierRemoteContent.allowed && _rapierRemoteSubresourceOrigin(src)) {
|
|
122
|
+
if (!env.rapierPage && !_rapierRemoteContent.allowed && _rapierRemoteSubresourceOrigin(src)) {
|
|
118
123
|
const html = _rapierRemoteImagePlaceholder(src, alt, title, size, layout, rawAlt);
|
|
119
124
|
return html.replace('<span ', '<span data-rapier-markdown-image="" ' +
|
|
120
125
|
(token.meta?.mdImage?.source ? 'data-rapier-image-source="' + md.utils.escapeHtml(encodeURIComponent(token.meta.mdImage.source)) + '" ' : ''));
|
|
@@ -246,7 +251,7 @@ function initMarkdownIt() {
|
|
|
246
251
|
token.attrSet('data-rapier-fence', token.markup || '```');
|
|
247
252
|
token.attrSet('data-rapier-fence-info', token.info == null ? '' : String(token.info));
|
|
248
253
|
const lang = String(token.info || '').trim().split(/\s+/)[0].toLowerCase();
|
|
249
|
-
if (lang !== 'mermaid') return _fenceDefault(tokens, idx, opts, env, self);
|
|
254
|
+
if (lang !== 'mermaid' || env.rapierPage) return _fenceDefault(tokens, idx, opts, env, self);
|
|
250
255
|
const src = String(token.content || '').replace(/\n$/, '');
|
|
251
256
|
const fence = md.utils.escapeHtml(token.markup || '```');
|
|
252
257
|
const info = token.info == null ? '' : String(token.info);
|
|
@@ -299,9 +304,10 @@ function initMarkdownIt() {
|
|
|
299
304
|
|
|
300
305
|
{
|
|
301
306
|
let _mathPromptedOnce = false;
|
|
302
|
-
const _renderMath = (src, displayMode) => {
|
|
307
|
+
const _renderMath = (src, displayMode, page) => {
|
|
303
308
|
const marker = displayMode ? '$$' : '$';
|
|
304
309
|
const source = marker + src + marker, literal = md.utils.escapeHtml(source);
|
|
310
|
+
if (page) return '<span data-rapier-math-source="">' + literal + '</span>';
|
|
305
311
|
const mathProvider = _rapierProviders.math;
|
|
306
312
|
if (!mathProvider || mathProvider.status !== 'ready' || typeof mathProvider.renderToString !== 'function') {
|
|
307
313
|
if (!_mathPromptedOnce) {
|
|
@@ -318,14 +324,14 @@ function initMarkdownIt() {
|
|
|
318
324
|
}
|
|
319
325
|
catch (_) { return '<span data-rapier-math-source="">' + literal + '</span>'; }
|
|
320
326
|
};
|
|
321
|
-
md.renderer.rules.math_inline = (tokens, idx) => _renderMath(tokens[idx].content, false);
|
|
322
|
-
md.renderer.rules.math_block = (tokens, idx) => wrapDisplayMath(_renderMath(tokens[idx].content, true)) + '\n';
|
|
327
|
+
md.renderer.rules.math_inline = (tokens, idx, _opts, env) => _renderMath(tokens[idx].content, false, env.rapierPage);
|
|
328
|
+
md.renderer.rules.math_block = (tokens, idx, _opts, env) => wrapDisplayMath(_renderMath(tokens[idx].content, true, env.rapierPage)) + '\n';
|
|
323
329
|
}
|
|
324
330
|
globalThis.RapierMarkdownLayout.installMarkdownLayout(md);
|
|
325
331
|
globalThis.RapierImageAssets.configureParser(md, _rapierSplitOpeningFrontmatter);
|
|
326
332
|
}
|
|
327
333
|
|
|
328
|
-
function _rapierRenderSemanticRoot(canonical, metadata) {
|
|
334
|
+
function _rapierRenderSemanticRoot(canonical, metadata, options = {}) {
|
|
329
335
|
const root = document.createElement('div');
|
|
330
336
|
root.setAttribute('data-rapier-semantic-root', 'true');
|
|
331
337
|
|
|
@@ -343,7 +349,7 @@ function _rapierRenderSemanticRoot(canonical, metadata) {
|
|
|
343
349
|
}
|
|
344
350
|
|
|
345
351
|
const markdown = _rapierSplitOpeningFrontmatter(canonical).body;
|
|
346
|
-
const env = {};
|
|
352
|
+
const env = options.page ? {rapierPage: true} : {};
|
|
347
353
|
let rendered = md
|
|
348
354
|
? md.render(markdown, env)
|
|
349
355
|
: '<pre><code>' + escapeRapierHtmlText(markdown) + '</code></pre>';
|
|
@@ -352,7 +358,7 @@ function _rapierRenderSemanticRoot(canonical, metadata) {
|
|
|
352
358
|
String(item && item.label || ('inline-' + (index + 1))));
|
|
353
359
|
rendered = _relabelFootnoteHtml(rendered, labels);
|
|
354
360
|
}
|
|
355
|
-
root.innerHTML = sanitizeRapierHtml(rendered, 'export');
|
|
361
|
+
root.innerHTML = sanitizeRapierHtml(rendered, options.page ? 'source' : 'export');
|
|
356
362
|
_rapierApplyBlockDirection(root);
|
|
357
363
|
|
|
358
364
|
_rapierAssignHeadingSlugs(root, RAPIER_RENDERED_HEADING_SELECTOR, false);
|
|
@@ -818,6 +824,477 @@ function wrapDisplayMath(html) {
|
|
|
818
824
|
return '<span class="math-display-wrap">' + html + '</span>';
|
|
819
825
|
}
|
|
820
826
|
|
|
821
|
-
|
|
827
|
+
function _rapierCalloutInfo(blockquote) {
|
|
828
|
+
const first = blockquote && blockquote.querySelector(':scope > p:first-child');
|
|
829
|
+
if (!first) return null;
|
|
830
|
+
const existing = first.querySelector('.callout__label');
|
|
831
|
+
if (existing) {
|
|
832
|
+
const type = String(existing.textContent || '').trim().toUpperCase();
|
|
833
|
+
return /^(NOTE|TIP|IMPORTANT|WARNING|CAUTION|DANGER|INFO)$/.test(type) ? { first, type, existing } : null;
|
|
834
|
+
}
|
|
835
|
+
const match = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|DANGER|INFO)\]/i.exec(first.textContent || '');
|
|
836
|
+
return match ? { first, type: match[1].toUpperCase(), existing: null } : null;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function _rapierRemoveCalloutMarker(first) {
|
|
840
|
+
if (!first) return;
|
|
841
|
+
first.innerHTML = first.innerHTML.replace(
|
|
842
|
+
/^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION|DANGER|INFO)\]\s*(?:<br\s*\/?>)?\s*/i,
|
|
843
|
+
''
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function _rapierMathSource(node) {
|
|
848
|
+
if (!node) return '';
|
|
849
|
+
const carrier = node.matches && node.matches('[data-math-src]')
|
|
850
|
+
? node
|
|
851
|
+
: node.querySelector && node.querySelector('[data-math-src]');
|
|
852
|
+
if (carrier) {
|
|
853
|
+
const encoded = carrier.getAttribute('data-math-src') || '';
|
|
854
|
+
try { return decodeURIComponent(encoded); } catch (_) { return encoded; }
|
|
855
|
+
}
|
|
856
|
+
const placeholder = node.matches && node.matches('.math-placeholder')
|
|
857
|
+
? node
|
|
858
|
+
: node.querySelector && node.querySelector('.math-placeholder');
|
|
859
|
+
return String((placeholder || node).textContent || '').trim();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function _rapierFlattenPortableDetails(root) {
|
|
863
|
+
const blockTags = new Set([
|
|
864
|
+
'ADDRESS','ARTICLE','ASIDE','BLOCKQUOTE','DIV','DL','FIELDSET','FIGURE',
|
|
865
|
+
'FOOTER','FORM','H1','H2','H3','H4','H5','H6','HEADER','HR','MAIN',
|
|
866
|
+
'NAV','OL','P','PRE','SECTION','TABLE','UL',
|
|
867
|
+
]);
|
|
868
|
+
Array.from(root.querySelectorAll('details')).reverse().forEach(details => {
|
|
869
|
+
const summary = Array.from(details.children).find(child => child.tagName === 'SUMMARY');
|
|
870
|
+
const section = document.createElement('section');
|
|
871
|
+
if (details.id) section.id = details.id;
|
|
872
|
+
if (summary && (summary.textContent.trim() || summary.children.length)) {
|
|
873
|
+
const heading = summary.querySelector('h1,h2,h3,h4,h5,h6');
|
|
874
|
+
const label = document.createElement(heading ? 'div' : 'p');
|
|
875
|
+
if (summary.id) label.id = summary.id;
|
|
876
|
+
const content = heading ? label : document.createElement('strong');
|
|
877
|
+
while (summary.firstChild) content.appendChild(summary.firstChild);
|
|
878
|
+
if (!heading) label.appendChild(content);
|
|
879
|
+
section.appendChild(label);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
let paragraph = null;
|
|
883
|
+
Array.from(details.childNodes).forEach(child => {
|
|
884
|
+
if (child === summary) return;
|
|
885
|
+
const isText = child.nodeType === Node.TEXT_NODE;
|
|
886
|
+
if (isText && !String(child.nodeValue || '').trim()) return;
|
|
887
|
+
const isBlock = child.nodeType === Node.ELEMENT_NODE && blockTags.has(child.tagName);
|
|
888
|
+
if (!isBlock) {
|
|
889
|
+
if (!paragraph) {
|
|
890
|
+
paragraph = document.createElement('p');
|
|
891
|
+
section.appendChild(paragraph);
|
|
892
|
+
}
|
|
893
|
+
paragraph.appendChild(child);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
paragraph = null;
|
|
897
|
+
section.appendChild(child);
|
|
898
|
+
});
|
|
899
|
+
details.replaceWith(section);
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function _rapierProjectPortableRoot(semanticRoot, options) {
|
|
904
|
+
const opts = options || {};
|
|
905
|
+
// This projection is HTML-shaped (width baked into a CSS `style`, `data-*` stripped) for the
|
|
906
|
+
// consumers that read HTML -- publishing/fragment export, TXT. DOCX is not an HTML consumer: its
|
|
907
|
+
// writer (interchange/docx.mjs) reads the layout comment itself, so stripping it first and
|
|
908
|
+
// handing back CSS only throws the fact away before the writer ever sees it. `keepLayoutData`
|
|
909
|
+
// leaves `data-rapier-image-layout` (width, align AND rotation) and the paragraph's
|
|
910
|
+
// `data-md-align` exactly as the semantic root already carries them, instead of the CSS-only
|
|
911
|
+
// projection, so the one existing layout representation reaches the writer.
|
|
912
|
+
const keepLayoutData = !!opts.keepLayoutData;
|
|
913
|
+
const root = semanticRoot.cloneNode(true), nativeElements = new Set();
|
|
914
|
+
// A source soft break is a reading space, never the editor's invisible token.
|
|
915
|
+
_rapierResolveSoftBreakTokens(root);
|
|
916
|
+
if (opts.keepDiagrams) for (const figure of root.querySelectorAll('.diagram-block[data-diagram-native]')) {
|
|
917
|
+
const svg = figure.querySelector('.diagram-cache > svg.rapier-native-flowchart');
|
|
918
|
+
if (figure.getAttribute('data-diagram-state') === 'ready' && svg) {
|
|
919
|
+
// A class in authored HTML grants no exemption: only the native materialization above does.
|
|
920
|
+
for (const element of [svg, ...svg.querySelectorAll('*')]) nativeElements.add(element);
|
|
921
|
+
figure.replaceWith(svg);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
root.querySelectorAll('blockquote').forEach(blockquote => {
|
|
926
|
+
const info = _rapierCalloutInfo(blockquote);
|
|
927
|
+
if (!info) return;
|
|
928
|
+
if (info.existing) info.existing.remove();
|
|
929
|
+
else _rapierRemoveCalloutMarker(info.first);
|
|
930
|
+
const label = document.createElement('strong');
|
|
931
|
+
label.textContent = info.type.charAt(0) + info.type.slice(1).toLowerCase() + ': ';
|
|
932
|
+
info.first.insertBefore(label, info.first.firstChild);
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
root.querySelectorAll('input[type="checkbox"]').forEach(input => {
|
|
936
|
+
const nextStartsWithSpace = input.nextSibling
|
|
937
|
+
&& input.nextSibling.nodeType === Node.TEXT_NODE
|
|
938
|
+
&& /^\s/.test(input.nextSibling.nodeValue || '');
|
|
939
|
+
input.replaceWith(document.createTextNode((input.checked ? '☒' : '☐') + (nextStartsWithSpace ? '' : ' ')));
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
root.querySelectorAll('.math-display-wrap').forEach(wrapper => {
|
|
943
|
+
const source = _rapierMathSource(wrapper);
|
|
944
|
+
const p = document.createElement('p');
|
|
945
|
+
const code = document.createElement('code');
|
|
946
|
+
code.textContent = source || '[equation]';
|
|
947
|
+
p.appendChild(code);
|
|
948
|
+
wrapper.replaceWith(p);
|
|
949
|
+
});
|
|
950
|
+
root.querySelectorAll('.math-rendered,.math-placeholder').forEach(math => {
|
|
951
|
+
const code = document.createElement('code');
|
|
952
|
+
code.textContent = _rapierMathSource(math) || '[equation]';
|
|
953
|
+
math.replaceWith(code);
|
|
954
|
+
});
|
|
955
|
+
root.querySelectorAll('math').forEach(math => {
|
|
956
|
+
const code = document.createElement('code');
|
|
957
|
+
code.textContent = String(math.textContent || '').trim() || '[equation]';
|
|
958
|
+
math.replaceWith(code);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
_rapierFlattenPortableDetails(root);
|
|
962
|
+
|
|
963
|
+
root.querySelectorAll('section.footnotes').forEach(section => {
|
|
964
|
+
section.querySelectorAll('.footnotes-sep').forEach(node => node.remove());
|
|
965
|
+
if (!section.querySelector(':scope > h1,:scope > h2,:scope > h3')) {
|
|
966
|
+
const heading = document.createElement('h2');
|
|
967
|
+
heading.textContent = 'Footnotes';
|
|
968
|
+
section.insertBefore(heading, section.firstChild);
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
root.querySelectorAll('.table-scroll-wrap,.math-display-wrap').forEach(_rapierUnwrapElement);
|
|
973
|
+
|
|
974
|
+
root.querySelectorAll('pre > code').forEach(_rapierNormalizeCodeElement);
|
|
975
|
+
|
|
976
|
+
root.querySelectorAll('svg').forEach(svg => {
|
|
977
|
+
if (nativeElements.has(svg)) return;
|
|
978
|
+
const title = svg.getAttribute('aria-label')
|
|
979
|
+
|| (svg.querySelector('title') && svg.querySelector('title').textContent)
|
|
980
|
+
|| '';
|
|
981
|
+
svg.replaceWith(document.createTextNode(title ? '[Image: ' + title.trim() + ']' : '[Image]'));
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
_rapierPrefixPortableAnchors(root, opts.baseName);
|
|
985
|
+
|
|
986
|
+
root.querySelectorAll('*').forEach(element => {
|
|
987
|
+
if (nativeElements.has(element)) return;
|
|
988
|
+
const lang = element.tagName === 'CODE' ? _rapierLanguageClass(element) : '';
|
|
989
|
+
const columnAlignment = /^(TH|TD)$/.test(element.tagName) && /^(left|center|right)$/.test(element.style.textAlign)
|
|
990
|
+
? element.style.textAlign : '';
|
|
991
|
+
const alignment = globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-md-layout'))?.align || columnAlignment;
|
|
992
|
+
const imageLayout = element.tagName === 'IMG' ? globalThis.RapierMarkdownLayout.parseLayoutAttribute(element.getAttribute('data-rapier-image-layout')) : null;
|
|
993
|
+
|
|
994
|
+
const colorHex = element.tagName === 'SPAN' ? element.getAttribute('data-md-color') : null;
|
|
995
|
+
|
|
996
|
+
const highlightHex = element.tagName === 'MARK'
|
|
997
|
+
? (RAPIER_PORTABLE_HIGHLIGHT_HEX[String(element.getAttribute('data-rapier-highlight') || '').toLowerCase()] || RAPIER_PORTABLE_HIGHLIGHT_HEX.yellow)
|
|
998
|
+
: null;
|
|
999
|
+
|
|
1000
|
+
const isTableCaption = element.tagName === 'P' && element.classList.contains('rapier-table-caption');
|
|
1001
|
+
|
|
1002
|
+
const isPageBreak = element.hasAttribute('data-md-break');
|
|
1003
|
+
Array.from(element.attributes).forEach(attribute => {
|
|
1004
|
+
const name = attribute.name.toLowerCase();
|
|
1005
|
+
if (keepLayoutData && (name === 'data-rapier-image-layout' || name === 'data-md-align')) return;
|
|
1006
|
+
if (name === 'class' || name === 'style' || name.startsWith('data-')
|
|
1007
|
+
|| name === 'contenteditable' || name === 'spellcheck'
|
|
1008
|
+
|| name === 'tabindex' || name === 'disabled' || name === 'aria-disabled') {
|
|
1009
|
+
element.removeAttribute(attribute.name);
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
if (lang) element.className = 'language-' + lang;
|
|
1013
|
+
if (alignment) element.style.textAlign = alignment;
|
|
1014
|
+
if (imageLayout && !keepLayoutData) {
|
|
1015
|
+
const pictureStyle = globalThis.RapierMarkdownLayout.imageStyle(imageLayout);
|
|
1016
|
+
if (imageLayout.width != null) element.removeAttribute('width');
|
|
1017
|
+
if (pictureStyle) element.style.cssText = pictureStyle;
|
|
1018
|
+
}
|
|
1019
|
+
if (colorHex && /^#[0-9a-f]{6}$/i.test(colorHex)) element.style.color = colorHex.toLowerCase();
|
|
1020
|
+
if (highlightHex) element.style.backgroundColor = highlightHex;
|
|
1021
|
+
if (isTableCaption) { element.style.fontStyle = 'italic'; element.style.color = '#555555'; }
|
|
1022
|
+
|
|
1023
|
+
if (isPageBreak) {
|
|
1024
|
+
const p = document.createElement('p');
|
|
1025
|
+
p.setAttribute('style', 'page-break-before: always');
|
|
1026
|
+
element.replaceWith(p);
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
const clean = document.createElement('div');
|
|
1031
|
+
clean.innerHTML = sanitizeRapierHtml(root.innerHTML, 'export');
|
|
1032
|
+
return clean;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function _rapierPortableHtml(root) {
|
|
1036
|
+
return sanitizeRapierHtml(root ? root.innerHTML : '', 'export').trim();
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function _rapierNormalizeInlineText(value) {
|
|
1040
|
+
return String(value || '')
|
|
1041
|
+
.replace(/\u00a0/g, ' ')
|
|
1042
|
+
.replace(/[\t\f\v ]+/g, ' ')
|
|
1043
|
+
.replace(/ *\n */g, '\n')
|
|
1044
|
+
.trim();
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// What a text file says where a picture stands: its words, and its address unless the picture is carried in the document itself.
|
|
1048
|
+
function _rapierPictureWords(alt, src) {
|
|
1049
|
+
if (/^data:image\//i.test(src)) return alt ? '[Image: ' + alt + ']' : '[Image]';
|
|
1050
|
+
if (src) return alt ? '[Image: ' + alt + ' — ' + src + ']' : '[Image: ' + src + ']';
|
|
1051
|
+
return alt ? '[Image: ' + alt + ']' : '[Image]';
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// A remote picture written as Markdown stands in the editor, until the person allows remote content, as a placeholder: its words, its origin and a
|
|
1055
|
+
// button. The text file says the picture as it does once the person has allowed it, its words and its address, never the placeholder's controls.
|
|
1056
|
+
function _rapierHeldPicturesAsWords(root) {
|
|
1057
|
+
const copy = root.cloneNode(true);
|
|
1058
|
+
copy.querySelectorAll('span[data-rapier-remote-src]').forEach(held => held.replaceWith(document.createTextNode(
|
|
1059
|
+
_rapierPictureWords(String(held.getAttribute('data-rapier-remote-alt') || '').trim(), String(held.getAttribute('data-rapier-remote-src') || '').trim()))));
|
|
1060
|
+
return copy;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function _rapierPortableInlineText(node, options) {
|
|
1064
|
+
const opts = options || {};
|
|
1065
|
+
if (!node) return '';
|
|
1066
|
+
if (node.nodeType === Node.TEXT_NODE) return node.nodeValue || '';
|
|
1067
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return '';
|
|
1068
|
+
const tag = node.tagName;
|
|
1069
|
+
if (tag === 'BR') return '\n';
|
|
1070
|
+
// A task's box is what the person sees of it.
|
|
1071
|
+
if (tag === 'INPUT') return /^checkbox$/i.test(node.type || '') ? (node.checked || node.hasAttribute('checked') ? '☑ ' : '☐ ') : '';
|
|
1072
|
+
if (tag === 'IMG') {
|
|
1073
|
+
if (opts.visible) return '';
|
|
1074
|
+
const alt = String(node.getAttribute('alt') || '').trim();
|
|
1075
|
+
const src = String(node.getAttribute('src') || '').trim();
|
|
1076
|
+
return _rapierPictureWords(alt, src);
|
|
1077
|
+
}
|
|
1078
|
+
if (tag === 'A') {
|
|
1079
|
+
const label = _rapierNormalizeInlineText(Array.from(node.childNodes)
|
|
1080
|
+
.map(child => _rapierPortableInlineText(child, opts)).join(''));
|
|
1081
|
+
const href = String(node.getAttribute('href') || '').trim();
|
|
1082
|
+
if (opts.visible || !href || href.charAt(0) === '#') return label;
|
|
1083
|
+
if (!label || label === href) return href;
|
|
1084
|
+
return label + ' (' + href + ')';
|
|
1085
|
+
}
|
|
1086
|
+
if (tag === 'CODE' && !opts.inPre) {
|
|
1087
|
+
const source = String(node.textContent || '');
|
|
1088
|
+
return opts.visible ? source : source ? '`' + source + '`' : '';
|
|
1089
|
+
}
|
|
1090
|
+
return Array.from(node.childNodes)
|
|
1091
|
+
.map(child => _rapierPortableInlineText(child, opts)).join('');
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function _rapierPortableListText(list, depth, options) {
|
|
1095
|
+
const opts = options || {};
|
|
1096
|
+
const level = Math.max(0, depth | 0);
|
|
1097
|
+
const ordered = list.tagName === 'OL';
|
|
1098
|
+
let counter = ordered ? (parseInt(list.getAttribute('start'), 10) || 1) : 0;
|
|
1099
|
+
const lines = [];
|
|
1100
|
+
Array.from(list.children).filter(child => child.tagName === 'LI').forEach(item => {
|
|
1101
|
+
const nested = Array.from(item.children).filter(child => child.tagName === 'UL' || child.tagName === 'OL');
|
|
1102
|
+
const content = Array.from(item.childNodes)
|
|
1103
|
+
.filter(child => !(child.nodeType === Node.ELEMENT_NODE && (child.tagName === 'UL' || child.tagName === 'OL')))
|
|
1104
|
+
.map(child => _rapierPortableInlineText(child, opts)).join('');
|
|
1105
|
+
const normalized = _rapierNormalizeInlineText(content);
|
|
1106
|
+
const indent = ' '.repeat(level);
|
|
1107
|
+
// A task shows its box where a bullet would be.
|
|
1108
|
+
const marker = /^[☐☑☒]/.test(normalized) ? '' : ordered ? (counter++) + '. ' : '• ';
|
|
1109
|
+
const parts = (normalized || '').split('\n');
|
|
1110
|
+
lines.push(indent + marker + (parts.shift() || ''));
|
|
1111
|
+
parts.forEach(part => lines.push(indent + ' ' + part));
|
|
1112
|
+
nested.forEach(child => {
|
|
1113
|
+
const nestedText = _rapierPortableListText(child, level + 1, opts);
|
|
1114
|
+
if (nestedText) lines.push(nestedText);
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
return lines.join('\n');
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// A table, or a part of one a selection clipped out (a head, a body, a row): its rows.
|
|
1121
|
+
function _rapierPortableTableText(table, options) {
|
|
1122
|
+
const opts = options || {};
|
|
1123
|
+
const rows = table.tagName === 'TR' ? [table] : Array.from(table.querySelectorAll('tr'));
|
|
1124
|
+
return rows.map(row => {
|
|
1125
|
+
return Array.from(row.children)
|
|
1126
|
+
.filter(cell => cell.tagName === 'TH' || cell.tagName === 'TD')
|
|
1127
|
+
.map(cell => _rapierNormalizeInlineText(_rapierPortableInlineText(cell, opts)))
|
|
1128
|
+
.join(opts.visible ? '\t' : ' | ');
|
|
1129
|
+
}).filter(Boolean).join('\n');
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function _rapierPortablePlainText(root, options) {
|
|
1133
|
+
const opts = options || {};
|
|
1134
|
+
const blocks = [];
|
|
1135
|
+
const blockTags = new Set([
|
|
1136
|
+
'ADDRESS','ARTICLE','ASIDE','BLOCKQUOTE','DETAILS','DIV','DL','FIELDSET','FIGURE','FOOTER',
|
|
1137
|
+
'FORM','H1','H2','H3','H4','H5','H6','HEADER','HR','MAIN','NAV','OL','P','PRE','SECTION','TABLE','UL'
|
|
1138
|
+
]);
|
|
1139
|
+
|
|
1140
|
+
const pushBlock = (text, preserve) => {
|
|
1141
|
+
let value = String(text == null ? '' : text).replace(/\r\n?/g, '\n');
|
|
1142
|
+
if (preserve) value = value.replace(/\n+$/, '');
|
|
1143
|
+
else value = value.replace(/[ \t]+$/gm, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
1144
|
+
if (value) blocks.push({ text: value, preserve: !!preserve });
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const collectChildren = parent => {
|
|
1148
|
+
let inline = [];
|
|
1149
|
+
const flush = () => {
|
|
1150
|
+
if (!inline.length) return;
|
|
1151
|
+
pushBlock(_rapierNormalizeInlineText(inline.map(node => _rapierPortableInlineText(node, opts)).join('')), false);
|
|
1152
|
+
inline = [];
|
|
1153
|
+
};
|
|
1154
|
+
const tablePart = node => node.nodeType === Node.ELEMENT_NODE && /^(THEAD|TBODY|TFOOT|TR)$/.test(node.tagName);
|
|
1155
|
+
const blankText = node => node.nodeType === Node.TEXT_NODE && !String(node.nodeValue || '').trim();
|
|
1156
|
+
const children = Array.from(parent.childNodes);
|
|
1157
|
+
const inTableRun = index => { for (let at = index - 1; at >= 0; at--) { if (!blankText(children[at])) return tablePart(children[at]); } return false; };
|
|
1158
|
+
children.forEach((child, index) => {
|
|
1159
|
+
if (tablePart(child)) {
|
|
1160
|
+
// The parts of a table a selection clipped out of it: one block of rows, whichever parts arrived.
|
|
1161
|
+
if (inTableRun(index)) return;
|
|
1162
|
+
flush();
|
|
1163
|
+
const parts = [];
|
|
1164
|
+
for (let at = index; at < children.length && (tablePart(children[at]) || blankText(children[at])); at++) {
|
|
1165
|
+
if (tablePart(children[at])) parts.push(_rapierPortableTableText(children[at], opts));
|
|
1166
|
+
}
|
|
1167
|
+
pushBlock(parts.filter(Boolean).join('\n'), false);
|
|
1168
|
+
} else if (blankText(child) && inTableRun(index)) {
|
|
1169
|
+
return;
|
|
1170
|
+
} else if (child.nodeType === Node.ELEMENT_NODE && blockTags.has(child.tagName)) {
|
|
1171
|
+
flush();
|
|
1172
|
+
collectBlock(child);
|
|
1173
|
+
} else if (opts.visible && child.nodeType === Node.ELEMENT_NODE && child.tagName === 'CODE' && String(child.textContent || '').includes('\n')) {
|
|
1174
|
+
// Lines of a code block taken without their block: the code as shown, every space kept.
|
|
1175
|
+
flush();
|
|
1176
|
+
pushBlock(String(child.textContent || ''), true);
|
|
1177
|
+
} else {
|
|
1178
|
+
inline.push(child);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
flush();
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
const collectBlock = node => {
|
|
1185
|
+
if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
|
|
1186
|
+
const tag = node.tagName;
|
|
1187
|
+
if (/^H[1-6]$/.test(tag) || tag === 'P') {
|
|
1188
|
+
const text = _rapierNormalizeInlineText(_rapierPortableInlineText(node, opts));
|
|
1189
|
+
// An empty paragraph (the line Enter makes) is a line of its own, as it is in Word's plain text: one newline more between its neighbours.
|
|
1190
|
+
if (tag === 'P' && !text && node.childNodes.length && !node.hasAttribute('data-md-break')) blocks.push({ text: '', blank: true });
|
|
1191
|
+
else pushBlock(text, false);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (tag === 'HR') { pushBlock('────────', false); return; }
|
|
1195
|
+
if (tag === 'PRE') {
|
|
1196
|
+
const code = node.querySelector(':scope > code') || node;
|
|
1197
|
+
const lang = _rapierLanguageClass(code);
|
|
1198
|
+
const source = String(code.textContent || '').replace(/\r\n?/g, '\n').replace(/\n$/, '');
|
|
1199
|
+
if (opts.visible) { pushBlock(source, true); return; }
|
|
1200
|
+
const label = lang ? lang.charAt(0).toUpperCase() + lang.slice(1) + ':\n\n' : '';
|
|
1201
|
+
pushBlock(label + source.split('\n').map(line => ' ' + line).join('\n'), true);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (tag === 'UL' || tag === 'OL') {
|
|
1205
|
+
pushBlock(_rapierPortableListText(node, 0, opts), false);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (tag === 'TABLE') {
|
|
1209
|
+
pushBlock(_rapierPortableTableText(node, opts), false);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (tag === 'BLOCKQUOTE') {
|
|
1213
|
+
const before = blocks.length;
|
|
1214
|
+
collectChildren(node);
|
|
1215
|
+
const quoted = blocks.splice(before).filter(block => !block.blank).map(block => block.text).join('\n\n');
|
|
1216
|
+
if (quoted) pushBlock(opts.visible ? quoted : quoted.split('\n').map(line => line ? '> ' + line : '>').join('\n'), true);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (tag === 'DL') {
|
|
1220
|
+
const children = Array.from(node.children);
|
|
1221
|
+
for (let i = 0; i < children.length; i++) {
|
|
1222
|
+
if (children[i].tagName !== 'DT') continue;
|
|
1223
|
+
const term = _rapierNormalizeInlineText(_rapierPortableInlineText(children[i], opts));
|
|
1224
|
+
const defs = [];
|
|
1225
|
+
while (children[i + 1] && children[i + 1].tagName === 'DD') {
|
|
1226
|
+
defs.push(_rapierNormalizeInlineText(_rapierPortableInlineText(children[++i], opts)));
|
|
1227
|
+
}
|
|
1228
|
+
pushBlock(term + (defs.length ? ' — ' + defs.join('\n ') : ''), false);
|
|
1229
|
+
}
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (tag === 'DETAILS') {
|
|
1233
|
+
const summary = node.querySelector(':scope > summary');
|
|
1234
|
+
if (summary) pushBlock(_rapierNormalizeInlineText(_rapierPortableInlineText(summary, opts)), false);
|
|
1235
|
+
Array.from(node.children).filter(child => child !== summary).forEach(collectBlock);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (tag === 'FIGURE') {
|
|
1239
|
+
const image = node.querySelector(':scope > img');
|
|
1240
|
+
if (image) pushBlock(_rapierPortableInlineText(image, opts), false);
|
|
1241
|
+
const caption = node.querySelector(':scope > figcaption');
|
|
1242
|
+
if (caption) pushBlock(_rapierNormalizeInlineText(_rapierPortableInlineText(caption, opts)), false);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
collectChildren(node);
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
collectChildren(root);
|
|
1249
|
+
// Each block stands a blank line from the next, and each empty paragraph between them adds one newline more.
|
|
1250
|
+
let joined = '', blanks = 0;
|
|
1251
|
+
for (const block of blocks) {
|
|
1252
|
+
if (block.blank) { blanks++; continue; }
|
|
1253
|
+
joined += (joined ? '\n\n' + '\n'.repeat(blanks) : '') + block.text;
|
|
1254
|
+
blanks = 0;
|
|
1255
|
+
}
|
|
1256
|
+
const text = joined.trim();
|
|
1257
|
+
return opts.finalNewline && text ? text + '\n' : text;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// The text file Export writes: a Markdown document's portable words and one final newline, any other document's own text.
|
|
1261
|
+
// The editor's Export and the door's txt both end here.
|
|
1262
|
+
function _rapierPlainTextFile(context) {
|
|
1263
|
+
return context.metadata.docKind === 'markdown'
|
|
1264
|
+
? _rapierPortablePlainText(_rapierProjectPortableRoot(_rapierHeldPicturesAsWords(context.semanticRoot), { baseName: context.baseName }), { finalNewline: true })
|
|
1265
|
+
: context.canonical;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function _rapierSourceTokenValue(node) {
|
|
1269
|
+
if (!node?.hasAttribute('data-rapier-source') || !node.hasAttribute('data-rapier-visible') ||
|
|
1270
|
+
Array.from(node.childNodes).some(child => child.nodeType !== Node.TEXT_NODE)) return null;
|
|
1271
|
+
try {
|
|
1272
|
+
return node.textContent === decodeURIComponent(node.getAttribute('data-rapier-visible'))
|
|
1273
|
+
? decodeURIComponent(node.getAttribute('data-rapier-source')) : null;
|
|
1274
|
+
} catch (_) { return null; }
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function _rapierResolveSoftBreakTokens(root) {
|
|
1278
|
+
root.querySelectorAll('.rapier-source-token--softbreak').forEach(token => {
|
|
1279
|
+
if (_rapierSourceTokenValue(token) === '\n') token.replaceWith(document.createTextNode(' '));
|
|
1280
|
+
else token.replaceWith(...token.childNodes);
|
|
1281
|
+
});
|
|
1282
|
+
return root;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function _rapierUnwrapElement(element) {
|
|
1286
|
+
while (element.firstChild) element.parentNode.insertBefore(element.firstChild, element);
|
|
1287
|
+
element.remove();
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function _rapierBidiStrong(cp) {
|
|
1291
|
+
const ch = String.fromCodePoint(cp);
|
|
1292
|
+
if (/\p{Nd}/u.test(ch)) return null;
|
|
1293
|
+
if (_RAPIER_RTL_SCRIPT.test(ch)) return 'R';
|
|
1294
|
+
if (/\p{L}/u.test(ch)) return 'L';
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return {_rapierCalloutInfo, _rapierRemoveCalloutMarker, _rapierMathSource, _rapierFlattenPortableDetails, _rapierProjectPortableRoot, _rapierPortableHtml, _rapierNormalizeInlineText, _rapierPortableInlineText, _rapierPortableListText, _rapierPortableTableText, _rapierPortablePlainText, _rapierPlainTextFile, _rapierHeldPicturesAsWords, _rapierSourceTokenValue, _rapierResolveSoftBreakTokens, _rapierUnwrapElement, _rapierBidiStrong, render: (source, metadata = {}) => { if (!md) initMarkdownIt(); return _rapierRenderSemanticRoot(source, metadata).innerHTML; }, initMarkdownIt, _rapierRenderSemanticRoot, _rapierInstallSemanticProbe, _rapierImageAltSize, _rapierImageAltSourceParts, _rapierImageAltText, _rapierImageSizeAttributes, _rapierRawHtmlFragment, _rapierApplyBlockDirection, _rapierFirstStrongDir, _rapierAssignHeadingSlugs, _rapierDisambiguateRenderedAnchors, _rapierDecodeHash, _rapierDecorateJumpLists, _rapierMarkFigureCaptions, _rapierMarkListContinuations, _rapierMarkTableCaptions, _rapierNormalizeCodeElement, _rapierLanguageClass, _relabelFootnoteHtml, _footnoteIdFromLabel, wrapDisplayMath, parser: () => md};
|
|
822
1299
|
}
|
|
823
|
-
export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, createMarkdownRenderer};
|
|
1300
|
+
export {RAPIER_TABLE_CAPTION_PREFIX_RE as tableCaptionPrefix, RAPIER_PORTABLE_HIGHLIGHT_HEX as portableHighlightHex, _RAPIER_RTL_SCRIPT as rtlScripts, createMarkdownRenderer};
|