officeparser 7.7.0 → 7.8.0
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 +3 -2
- package/dist/defaults.js +2 -0
- package/dist/generators/HtmlGenerator.js +19 -3
- package/dist/generators/MarkdownGenerator.d.ts +1 -0
- package/dist/generators/MarkdownGenerator.js +58 -14
- package/dist/officeparser.browser.d.ts +52 -2
- package/dist/officeparser.browser.iife.js +167 -161
- package/dist/officeparser.browser.mjs +167 -161
- package/dist/officeparser.browser.slim.d.ts +52 -2
- package/dist/officeparser.browser.slim.iife.js +158 -152
- package/dist/officeparser.browser.slim.mjs +158 -152
- package/dist/parsers/HtmlParser.js +37 -2
- package/dist/parsers/MarkdownParser.js +113 -3
- package/dist/sbom.cdx.json +92 -92
- package/dist/types.d.ts +52 -2
- package/package.json +1 -1
|
@@ -558,6 +558,32 @@ const parseHtml = async (buffer, config) => {
|
|
|
558
558
|
}
|
|
559
559
|
return kids;
|
|
560
560
|
};
|
|
561
|
+
// Gated generic-iframe embed (HtmlGenerator's `gatedEmbeds` shape): an inert
|
|
562
|
+
// click-to-load placeholder that never auto-loads its src. Read it back to the same
|
|
563
|
+
// `embed` node unconditionally - capturing metadata is safe (the src is scheme-checked
|
|
564
|
+
// again on any re-emit); it is the trusted, already-gated counterpart to a raw <iframe>.
|
|
565
|
+
if (tagName === 'div' && node.attributes?.['data-embed-gated'] !== undefined) {
|
|
566
|
+
const gatedSrc = decodeEntities(node.attributes?.['data-embed-src'] || '');
|
|
567
|
+
if (!gatedSrc)
|
|
568
|
+
return null;
|
|
569
|
+
const gatedAlignAttr = node.attributes?.['data-embed-align'];
|
|
570
|
+
const gatedAlign = ['left', 'center', 'right'].includes(gatedAlignAttr) ? gatedAlignAttr : undefined;
|
|
571
|
+
const gatedNode = {
|
|
572
|
+
type: 'embed',
|
|
573
|
+
text: gatedSrc,
|
|
574
|
+
metadata: {
|
|
575
|
+
embedType: 'iframe',
|
|
576
|
+
url: gatedSrc,
|
|
577
|
+
width: node.attributes?.['data-embed-width'],
|
|
578
|
+
height: node.attributes?.['data-embed-height'],
|
|
579
|
+
align: gatedAlign,
|
|
580
|
+
label: node.attributes?.['data-embed-label']
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
if (config.includeRawContent)
|
|
584
|
+
gatedNode.rawContent = '<div data-embed-gated>...</div>';
|
|
585
|
+
return gatedNode;
|
|
586
|
+
}
|
|
561
587
|
// YouTube embeds: attribute-driven editors render
|
|
562
588
|
// <div data-youtube-video="ID" data-width="…" data-align="…">…<iframe…></div>.
|
|
563
589
|
// Recognise both the wrapper div and a bare iframe so externally-authored HTML
|
|
@@ -578,7 +604,8 @@ const parseHtml = async (buffer, config) => {
|
|
|
578
604
|
videoId,
|
|
579
605
|
url: embedUrl,
|
|
580
606
|
width,
|
|
581
|
-
align: embedAlign
|
|
607
|
+
align: embedAlign,
|
|
608
|
+
label: node.attributes?.['data-embed-label']
|
|
582
609
|
}
|
|
583
610
|
};
|
|
584
611
|
if (config.includeRawContent)
|
|
@@ -593,7 +620,15 @@ const parseHtml = async (buffer, config) => {
|
|
|
593
620
|
const embedNode = {
|
|
594
621
|
type: 'embed',
|
|
595
622
|
text: embedUrl,
|
|
596
|
-
|
|
623
|
+
// Carry the iframe's own width/height so a YouTube iframe's dimensions are not
|
|
624
|
+
// dropped (metadata is unified across embedTypes; align has no source here).
|
|
625
|
+
metadata: {
|
|
626
|
+
embedType: 'youtube',
|
|
627
|
+
videoId: ytMatch[1],
|
|
628
|
+
url: embedUrl,
|
|
629
|
+
width: node.attributes?.width,
|
|
630
|
+
height: node.attributes?.height
|
|
631
|
+
}
|
|
597
632
|
};
|
|
598
633
|
if (config.includeRawContent)
|
|
599
634
|
embedNode.rawContent = '<iframe>...</iframe>';
|
|
@@ -262,6 +262,37 @@ const parseMarkdown = async (buffer, config) => {
|
|
|
262
262
|
}
|
|
263
263
|
return result;
|
|
264
264
|
};
|
|
265
|
+
// Attribute list for an embed leaf directive `{id=... src=... width=... height=... align=...}`.
|
|
266
|
+
// Superset of parseAttributeList (adds id/src/height); space-separated `k=v` tokens.
|
|
267
|
+
const parseEmbedDirectiveAttrs = (attrStr) => {
|
|
268
|
+
const result = {};
|
|
269
|
+
for (const token of attrStr.trim().split(/\s+/).filter(Boolean)) {
|
|
270
|
+
const kv = token.match(/^([a-zA-Z-]+)=(.+)$/);
|
|
271
|
+
if (!kv)
|
|
272
|
+
continue;
|
|
273
|
+
const [, key, val] = kv;
|
|
274
|
+
if (key === 'id')
|
|
275
|
+
result.id = val;
|
|
276
|
+
else if (key === 'src')
|
|
277
|
+
result.src = val;
|
|
278
|
+
else if (key === 'width')
|
|
279
|
+
result.width = val;
|
|
280
|
+
else if (key === 'height')
|
|
281
|
+
result.height = val;
|
|
282
|
+
else if (key === 'align' && ['left', 'center', 'right'].includes(val))
|
|
283
|
+
result.align = val;
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
};
|
|
287
|
+
// Extracts a YouTube video id from any of its URL shapes (watch?v=, youtu.be/, /embed/,
|
|
288
|
+
// img.youtube.com/vi/). Returns undefined for a non-YouTube URL. Used only by the opt-in
|
|
289
|
+
// folk-form import (embedFolkForms).
|
|
290
|
+
const extractYoutubeId = (url) => {
|
|
291
|
+
if (!url || !/(?:youtu\.be|youtube(?:-nocookie)?\.com)/.test(url))
|
|
292
|
+
return undefined;
|
|
293
|
+
const m = url.match(/(?:youtu\.be\/|\/embed\/|[?&]v=|\/vi\/)([A-Za-z0-9_-]+)/);
|
|
294
|
+
return m ? m[1] : undefined;
|
|
295
|
+
};
|
|
265
296
|
const parseInline = (text, currentFormatting = {}) => {
|
|
266
297
|
const nodes = [];
|
|
267
298
|
const plainText = (t) => ({ type: 'text', text: t, formatting: Object.keys(currentFormatting).length > 0 ? { ...currentFormatting } : undefined });
|
|
@@ -697,6 +728,63 @@ const parseMarkdown = async (buffer, config) => {
|
|
|
697
728
|
alignment = (alignMatch[1] || alignMatch[2]).toLowerCase();
|
|
698
729
|
block = alignMatch[3];
|
|
699
730
|
}
|
|
731
|
+
// Embed leaf directive (remark-directive family): `::youtube[Label]{id=... width=... align=...}`
|
|
732
|
+
// or `::embed[Label]{src=... width=... height=... align=...}`. Only these two names are
|
|
733
|
+
// recognised; any other `::name` stays literal text (no catch-all). `::youtube` renders from
|
|
734
|
+
// a validated id via a fixed template, so it is unconditional; `::embed` carries an arbitrary
|
|
735
|
+
// src, so it is gated behind `preserveIframes` (the trust input) exactly like a raw <iframe>,
|
|
736
|
+
// and stays literal text otherwise. New input only; nothing that parsed before changes.
|
|
737
|
+
const embedDirectiveMatch = block.match(/^::(youtube|embed)(?:\[([^\]]*)\])?\{([^}]*)\}$/);
|
|
738
|
+
if (embedDirectiveMatch) {
|
|
739
|
+
const kind = embedDirectiveMatch[1];
|
|
740
|
+
const label = (embedDirectiveMatch[2] || '').trim() || undefined;
|
|
741
|
+
const attrs = parseEmbedDirectiveAttrs(embedDirectiveMatch[3]);
|
|
742
|
+
if (kind === 'youtube' && attrs.id) {
|
|
743
|
+
const embedUrl = `https://www.youtube.com/watch?v=${attrs.id}`;
|
|
744
|
+
content.push({
|
|
745
|
+
type: 'embed',
|
|
746
|
+
text: embedUrl,
|
|
747
|
+
metadata: { embedType: 'youtube', videoId: attrs.id, url: embedUrl, width: attrs.width, align: attrs.align, label }
|
|
748
|
+
});
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (kind === 'embed' && attrs.src && (0, sanitize_js_1.iframeAllowed)(attrs.src, config.htmlParserConfig?.preserveIframes)) {
|
|
752
|
+
content.push({
|
|
753
|
+
type: 'embed',
|
|
754
|
+
text: attrs.src,
|
|
755
|
+
metadata: { embedType: 'iframe', url: attrs.src, width: attrs.width, height: attrs.height, align: attrs.align, label }
|
|
756
|
+
});
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
// Recognised name but not a usable/allowed directive: fall through so the line becomes
|
|
760
|
+
// ordinary text rather than being dropped.
|
|
761
|
+
}
|
|
762
|
+
// Ambiguous "folk" embed forms, imported only under the opt-in (embedFolkForms), since
|
|
763
|
+
// auto-upgrading an image/link to an embed is a heuristic that could mangle a genuine image
|
|
764
|
+
// link. Both become a safe youtube embed (rendered from the validated id). A standalone line
|
|
765
|
+
// only; anything not matching falls through to ordinary image/link parsing.
|
|
766
|
+
if (config.htmlParserConfig?.embedFolkForms) {
|
|
767
|
+
// Clickable thumbnail: [](watch), youtube when either URL is a youtube link.
|
|
768
|
+
const thumbMatch = block.match(/^\[!\[([^\]]*)\]\(([^)\s]+)\)\]\(([^)\s]+)\)$/);
|
|
769
|
+
if (thumbMatch) {
|
|
770
|
+
const fid = extractYoutubeId(thumbMatch[2]) || extractYoutubeId(thumbMatch[3]);
|
|
771
|
+
if (fid) {
|
|
772
|
+
const embedUrl = `https://www.youtube.com/watch?v=${fid}`;
|
|
773
|
+
content.push({ type: 'embed', text: embedUrl, metadata: { embedType: 'youtube', videoId: fid, url: embedUrl, label: thumbMatch[1].trim() || undefined } });
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
// Obsidian-style: a standalone image whose URL is a youtube link.
|
|
778
|
+
const obsMatch = block.match(/^!\[([^\]]*)\]\(([^)\s]+)\)$/);
|
|
779
|
+
if (obsMatch) {
|
|
780
|
+
const fid = extractYoutubeId(obsMatch[2]);
|
|
781
|
+
if (fid) {
|
|
782
|
+
const embedUrl = `https://www.youtube.com/watch?v=${fid}`;
|
|
783
|
+
content.push({ type: 'embed', text: embedUrl, metadata: { embedType: 'youtube', videoId: fid, url: embedUrl, label: obsMatch[1].trim() || undefined } });
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
700
788
|
// YouTube embed fallback: MarkdownGenerator's 'embed' case emits a single-line
|
|
701
789
|
// <div data-youtube-video="ID" data-width="…" data-align="…"></div> when fallbackToHtml
|
|
702
790
|
// is on; recognise it here so a saved-then-reopened .md keeps the video.
|
|
@@ -706,6 +794,7 @@ const parseMarkdown = async (buffer, config) => {
|
|
|
706
794
|
const attrsStr = youtubeMatch[2];
|
|
707
795
|
const widthMatch = attrsStr.match(/data-width="([^"]*)"/i);
|
|
708
796
|
const youtubeAlignMatch = attrsStr.match(/data-align="([^"]*)"/i);
|
|
797
|
+
const youtubeLabelMatch = attrsStr.match(/data-embed-label="([^"]*)"/i);
|
|
709
798
|
const embedAlign = youtubeAlignMatch && ['left', 'center', 'right'].includes(youtubeAlignMatch[1]) ? youtubeAlignMatch[1] : undefined;
|
|
710
799
|
const embedUrl = videoId ? `https://www.youtube.com/watch?v=${videoId}` : undefined;
|
|
711
800
|
content.push({
|
|
@@ -718,7 +807,8 @@ const parseMarkdown = async (buffer, config) => {
|
|
|
718
807
|
videoId,
|
|
719
808
|
url: embedUrl,
|
|
720
809
|
width: widthMatch?.[1],
|
|
721
|
-
align: embedAlign
|
|
810
|
+
align: embedAlign,
|
|
811
|
+
label: youtubeLabelMatch?.[1]
|
|
722
812
|
}
|
|
723
813
|
});
|
|
724
814
|
continue;
|
|
@@ -737,9 +827,29 @@ const parseMarkdown = async (buffer, config) => {
|
|
|
737
827
|
.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
738
828
|
.replace(/'/g, '\'').replace(/&/g, '&');
|
|
739
829
|
const src = decodeAttr(attrsStr.match(/\bsrc="([^"]*)"/i)?.[1]);
|
|
830
|
+
const width = attrsStr.match(/\bwidth="([^"]*)"/i)?.[1];
|
|
831
|
+
const height = attrsStr.match(/\bheight="([^"]*)"/i)?.[1];
|
|
832
|
+
// A YouTube iframe is recognised the same way the HTML parser does it (host + id
|
|
833
|
+
// capture), BEFORE and INDEPENDENT of the preserveIframes gate, so the same iframe
|
|
834
|
+
// yields the same 'youtube' embed whichever parser sees it. Only a generic (non-YouTube)
|
|
835
|
+
// iframe is gated behind preserveIframes and kept as an 'iframe' embed.
|
|
836
|
+
const ytMatch = src && /youtube(?:-nocookie)?\.com/.test(src) ? src.match(/(?:embed\/|v=)([^&?/\s]+)/) : null;
|
|
837
|
+
if (ytMatch) {
|
|
838
|
+
const embedUrl = `https://www.youtube.com/watch?v=${ytMatch[1]}`;
|
|
839
|
+
content.push({
|
|
840
|
+
type: 'embed',
|
|
841
|
+
text: embedUrl,
|
|
842
|
+
metadata: {
|
|
843
|
+
embedType: 'youtube',
|
|
844
|
+
videoId: ytMatch[1],
|
|
845
|
+
url: embedUrl,
|
|
846
|
+
width: width !== undefined ? decodeAttr(width) : undefined,
|
|
847
|
+
height: height !== undefined ? decodeAttr(height) : undefined
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
740
852
|
if (src && (0, sanitize_js_1.iframeAllowed)(src, config.htmlParserConfig?.preserveIframes)) {
|
|
741
|
-
const width = attrsStr.match(/\bwidth="([^"]*)"/i)?.[1];
|
|
742
|
-
const height = attrsStr.match(/\bheight="([^"]*)"/i)?.[1];
|
|
743
853
|
content.push({
|
|
744
854
|
type: 'embed',
|
|
745
855
|
text: src,
|