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.
@@ -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
- metadata: { embedType: 'youtube', videoId: ytMatch[1], url: embedUrl }
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: [![alt](thumb)](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(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"')
738
828
  .replace(/&#39;/g, '\'').replace(/&amp;/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,