printdown 1.1.1 → 1.2.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.
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  defaultThemeCss,
3
3
  getFontDir
4
- } from "./chunk-GEQQR3U7.js";
4
+ } from "./chunk-QYKBYAYW.js";
5
5
 
6
6
  // src/index.ts
7
- import fs3 from "fs";
8
- import path3 from "path";
7
+ import fs4 from "fs";
8
+ import path4 from "path";
9
9
 
10
10
  // src/markdown/index.ts
11
11
  import MarkdownIt from "markdown-it";
@@ -595,7 +595,7 @@ function centerPlugin(md) {
595
595
  }
596
596
 
597
597
  // src/markdown/plugins/heading.ts
598
- var NO_BORDER_ATTR_REGEX = /\s*(?:\{(?:\.|\s)*(?:no-border|no-underline|no-bottom-border|plain|-)\s*\}|\[(?:no-border|no-underline)\]|<!--\s*(?:no-border|no-underline)\s*-->)\s*$/i;
598
+ var HEADING_ATTR_REGEX = /\s*(?:\{(?:\.|\s)*(?:no-border|no-underline|no-bottom-border|plain|-|page-break|pagebreak|break-before)(?:\s+(?:\.|\s)*(?:no-border|no-underline|no-bottom-border|plain|-|page-break|pagebreak|break-before))*\s*\}|\[(?:no-border|no-underline|page-break|pagebreak|break-before)\]|<!--\s*(?:no-border|no-underline|page-break|pagebreak|break-before)\s*-->)\s*$/i;
599
599
  function addClass2(token, className) {
600
600
  const classIndex = token.attrIndex("class");
601
601
  if (classIndex < 0) {
@@ -615,28 +615,35 @@ function addClass2(token, className) {
615
615
  }
616
616
  }
617
617
  function headingPlugin(md) {
618
- md.core.ruler.after("inline", "heading_no_border", (state) => {
618
+ md.core.ruler.after("inline", "heading_attributes", (state) => {
619
619
  const tokens = state.tokens;
620
620
  for (let i = 0; i < tokens.length; i++) {
621
621
  if (tokens[i].type === "heading_open") {
622
622
  const inlineToken = tokens[i + 1];
623
623
  if (inlineToken && inlineToken.type === "inline") {
624
- if (NO_BORDER_ATTR_REGEX.test(inlineToken.content)) {
625
- addClass2(tokens[i], "printdown-no-border printdown-no-underline");
626
- inlineToken.content = inlineToken.content.replace(NO_BORDER_ATTR_REGEX, "");
624
+ const match = inlineToken.content.match(HEADING_ATTR_REGEX);
625
+ if (match) {
626
+ const attrStr = match[0].toLowerCase();
627
+ if (attrStr.includes("no-border") || attrStr.includes("no-underline") || attrStr.includes("plain") || attrStr.includes("{-}")) {
628
+ addClass2(tokens[i], "printdown-no-border printdown-no-underline");
629
+ }
630
+ if (attrStr.includes("page-break") || attrStr.includes("pagebreak") || attrStr.includes("break-before")) {
631
+ addClass2(tokens[i], "printdown-page-break");
632
+ }
633
+ inlineToken.content = inlineToken.content.replace(HEADING_ATTR_REGEX, "");
627
634
  if (inlineToken.children && inlineToken.children.length > 0) {
628
635
  for (let j = inlineToken.children.length - 1; j >= 0; j--) {
629
636
  const child = inlineToken.children[j];
630
637
  if (child.type === "text") {
631
- if (NO_BORDER_ATTR_REGEX.test(child.content)) {
632
- child.content = child.content.replace(NO_BORDER_ATTR_REGEX, "");
638
+ if (HEADING_ATTR_REGEX.test(child.content)) {
639
+ child.content = child.content.replace(HEADING_ATTR_REGEX, "");
633
640
  if (child.content === "") {
634
641
  inlineToken.children.splice(j, 1);
635
642
  }
636
643
  break;
637
644
  }
638
645
  } else if (child.type === "html_inline") {
639
- if (child.content.includes("no-border") || child.content.includes("no-underline")) {
646
+ if (child.content.includes("no-border") || child.content.includes("no-underline") || child.content.includes("page-break") || child.content.includes("pagebreak") || child.content.includes("break-before")) {
640
647
  inlineToken.children.splice(j, 1);
641
648
  break;
642
649
  }
@@ -647,26 +654,441 @@ function headingPlugin(md) {
647
654
  }
648
655
  } else if (tokens[i].type === "html_block") {
649
656
  tokens[i].content = tokens[i].content.replace(
650
- /<(h[1-6])(\s+[^>]*)?(?:\s+(?:no-border|no-underline))\s*([^>]*)>/gi,
657
+ /<(h[1-6])(\s+[^>]*)?(?:\s+(?:no-border|no-underline|page-break|pagebreak|break-before))\s*([^>]*)>/gi,
651
658
  (match, tag, before, after) => {
652
659
  const combinedAttrs = `${before || ""} ${after || ""}`.trim();
660
+ const classesToAdd = [];
661
+ if (match.includes("no-border") || match.includes("no-underline")) {
662
+ classesToAdd.push("printdown-no-border printdown-no-underline");
663
+ }
664
+ if (match.includes("page-break") || match.includes("pagebreak") || match.includes("break-before")) {
665
+ classesToAdd.push("printdown-page-break");
666
+ }
667
+ const classString = classesToAdd.join(" ");
653
668
  if (combinedAttrs.includes('class="') || combinedAttrs.includes("class='")) {
654
669
  return `<${tag} ${combinedAttrs.replace(
655
670
  /class=(["'])(.*?)\1/,
656
- "class=$1$2 printdown-no-border printdown-no-underline$1"
671
+ `class=$1$2 ${classString}$1`
657
672
  )}>`;
658
673
  }
659
- return `<${tag} class="printdown-no-border printdown-no-underline" ${combinedAttrs}>`.replace(
660
- /\s+>/,
661
- ">"
662
- );
674
+ return `<${tag} class="${classString}" ${combinedAttrs}>`.replace(/\s+>/, ">");
675
+ }
676
+ );
677
+ }
678
+ }
679
+ });
680
+ }
681
+
682
+ // src/markdown/plugins/page-break.ts
683
+ var PAGE_BREAK_LINE_REGEX = /^(?:<!--\s*(?:pagebreak|page-break|newpage|break)\s*-->|\\(?:pagebreak|newpage)|\[(?:pagebreak|page-break|newpage)\]|<(?:pagebreak|page-break|newpage)(?:\s*\/?)>)$/i;
684
+ var PAGE_BREAK_MATCH_REGEX = /(?:<!--\s*(?:pagebreak|page-break|newpage|break)\s*-->|\\(?:pagebreak|newpage)|\[(?:pagebreak|page-break|newpage)\]|<(?:pagebreak|page-break|newpage)(?:\s*\/?)>)/gi;
685
+ function pageBreakPlugin(md) {
686
+ function blockPageBreakRule(state, startLine, endLine, silent) {
687
+ const startPos = state.bMarks[startLine] + state.tShift[startLine];
688
+ const maxPos = state.eMarks[startLine];
689
+ const lineText = state.src.slice(startPos, maxPos).trim();
690
+ if (PAGE_BREAK_LINE_REGEX.test(lineText)) {
691
+ if (silent) return true;
692
+ const token = state.push("html_block", "", 0);
693
+ token.content = '<div class="printdown-page-break"></div>\n';
694
+ token.map = [startLine, startLine + 1];
695
+ state.line = startLine + 1;
696
+ return true;
697
+ }
698
+ if (/^:::\s*(?:pagebreak|page-break|newpage)\s*$/i.test(lineText)) {
699
+ if (silent) return true;
700
+ let nextLine = startLine + 1;
701
+ while (nextLine < endLine) {
702
+ const pos = state.bMarks[nextLine] + state.tShift[nextLine];
703
+ const max = state.eMarks[nextLine];
704
+ const curLine = state.src.slice(pos, max).trim();
705
+ if (/^:::\s*$/.test(curLine)) {
706
+ break;
707
+ }
708
+ nextLine++;
709
+ }
710
+ const token = state.push("html_block", "", 0);
711
+ token.content = '<div class="printdown-page-break"></div>\n';
712
+ token.map = [startLine, nextLine + 1];
713
+ state.line = nextLine < endLine ? nextLine + 1 : nextLine;
714
+ return true;
715
+ }
716
+ return false;
717
+ }
718
+ md.block.ruler.before("hr", "page_break", blockPageBreakRule);
719
+ md.core.ruler.after("inline", "page_break_tags", (state) => {
720
+ const tokens = state.tokens;
721
+ for (let i = 0; i < tokens.length; i++) {
722
+ const token = tokens[i];
723
+ if (token.type === "html_block") {
724
+ if (PAGE_BREAK_MATCH_REGEX.test(token.content)) {
725
+ token.content = token.content.replace(
726
+ PAGE_BREAK_MATCH_REGEX,
727
+ '<div class="printdown-page-break"></div>'
728
+ );
729
+ }
730
+ } else if (token.type === "paragraph_open") {
731
+ const inlineToken = tokens[i + 1];
732
+ if (inlineToken && inlineToken.type === "inline") {
733
+ const content = inlineToken.content.trim();
734
+ if (PAGE_BREAK_LINE_REGEX.test(content)) {
735
+ tokens[i].type = "html_block";
736
+ tokens[i].tag = "";
737
+ tokens[i].content = '<div class="printdown-page-break"></div>\n';
738
+ tokens[i].children = null;
739
+ tokens.splice(i + 1, 2);
740
+ }
741
+ }
742
+ } else if (token.type === "inline" && token.children) {
743
+ for (let j = 0; j < token.children.length; j++) {
744
+ const child = token.children[j];
745
+ if (child.type === "html_inline" || child.type === "text") {
746
+ if (PAGE_BREAK_MATCH_REGEX.test(child.content)) {
747
+ child.type = "html_inline";
748
+ child.content = child.content.replace(
749
+ PAGE_BREAK_MATCH_REGEX,
750
+ '<div class="printdown-page-break"></div>'
751
+ );
752
+ }
753
+ }
754
+ }
755
+ }
756
+ }
757
+ });
758
+ }
759
+
760
+ // src/markdown/plugins/cover.ts
761
+ var COVER_CONTAINER_START_REGEX = /^:::\s*cover(?:\s+([a-zA-Z0-9_-]+))?\s*$/i;
762
+ var COVER_TAG_OPEN_REGEX = /^<cover(?:\s+variant=["']?([a-zA-Z0-9_-]+)["']?)?(?:\s+class=["']?([^"'>]*)["']?)?[^>]*>$/i;
763
+ var COVER_TAG_CLOSE_REGEX = /^<\/cover>$/i;
764
+ function coverPlugin(md) {
765
+ function blockCoverRule(state, startLine, endLine, silent) {
766
+ const startPos = state.bMarks[startLine] + state.tShift[startLine];
767
+ const maxPos = state.eMarks[startLine];
768
+ const lineText = state.src.slice(startPos, maxPos).trim();
769
+ if (/^:::\s*(?:cover-meta|cover-footer)\s*$/i.test(lineText)) {
770
+ if (silent) return true;
771
+ let nextLine = startLine + 1;
772
+ let foundEnd = false;
773
+ while (nextLine < endLine) {
774
+ const pos = state.bMarks[nextLine] + state.tShift[nextLine];
775
+ const max = state.eMarks[nextLine];
776
+ const curLine = state.src.slice(pos, max).trim();
777
+ if (/^:::\s*$/.test(curLine) || /^:::\s*(?:cover-meta|cover-footer)\s*$/i.test(curLine)) {
778
+ foundEnd = true;
779
+ break;
780
+ }
781
+ nextLine++;
782
+ }
783
+ const tokenOpen = state.push("cover_meta_open", "div", 1);
784
+ tokenOpen.attrs = [["class", "printdown-cover-meta"]];
785
+ tokenOpen.block = true;
786
+ const oldParentType = state.parentType;
787
+ state.parentType = "root";
788
+ state.md.block.tokenize(state, startLine + 1, nextLine);
789
+ state.parentType = oldParentType;
790
+ const tokenClose = state.push("cover_meta_close", "div", -1);
791
+ tokenClose.block = true;
792
+ state.line = foundEnd ? nextLine + 1 : nextLine;
793
+ return true;
794
+ }
795
+ const coverMatch = lineText.match(COVER_CONTAINER_START_REGEX);
796
+ if (coverMatch) {
797
+ if (silent) return true;
798
+ const variant = coverMatch[1] ? coverMatch[1].toLowerCase() : "default";
799
+ let nextLine = startLine + 1;
800
+ let foundEnd = false;
801
+ let depth = 1;
802
+ while (nextLine < endLine) {
803
+ const pos = state.bMarks[nextLine] + state.tShift[nextLine];
804
+ const max = state.eMarks[nextLine];
805
+ const curLine = state.src.slice(pos, max).trim();
806
+ if (/^:::\s*[a-zA-Z0-9_-]+\s*$/i.test(curLine)) {
807
+ depth++;
808
+ } else if (/^:::\s*$/i.test(curLine)) {
809
+ depth--;
810
+ if (depth === 0) {
811
+ foundEnd = true;
812
+ break;
813
+ }
814
+ } else if (/^:::\s*cover\s*$/i.test(curLine)) {
815
+ depth--;
816
+ if (depth <= 0) {
817
+ foundEnd = true;
818
+ break;
819
+ }
820
+ }
821
+ nextLine++;
822
+ }
823
+ const tokenOpen = state.push("cover_open", "div", 1);
824
+ const classNames = ["printdown-cover", `printdown-cover-${variant}`];
825
+ tokenOpen.attrs = [["class", classNames.join(" ")]];
826
+ tokenOpen.block = true;
827
+ const oldParentType = state.parentType;
828
+ state.parentType = "root";
829
+ state.md.block.tokenize(state, startLine + 1, nextLine);
830
+ state.parentType = oldParentType;
831
+ const tokenClose = state.push("cover_close", "div", -1);
832
+ tokenClose.block = true;
833
+ state.line = foundEnd ? nextLine + 1 : nextLine;
834
+ return true;
835
+ }
836
+ return false;
837
+ }
838
+ md.block.ruler.before("fence", "cover_block", blockCoverRule);
839
+ md.core.ruler.after("inline", "cover_tags", (state) => {
840
+ const tokens = state.tokens;
841
+ function processToken(token) {
842
+ if (token.type === "html_inline") {
843
+ const trimmed = token.content.trim();
844
+ const openMatch = trimmed.match(COVER_TAG_OPEN_REGEX);
845
+ if (openMatch) {
846
+ const variant = openMatch[1] ? openMatch[1].toLowerCase() : "default";
847
+ const extraClass = openMatch[2] ? ` ${openMatch[2]}` : "";
848
+ token.content = `<div class="printdown-cover printdown-cover-${variant}${extraClass}">`;
849
+ } else if (COVER_TAG_CLOSE_REGEX.test(trimmed)) {
850
+ token.content = "</div>";
851
+ }
852
+ } else if (token.type === "html_block") {
853
+ token.content = token.content.replace(
854
+ /<cover(?:\s+variant=["']?([a-zA-Z0-9_-]+)["']?)?(?:\s+class=["']?([^"'>]*)["']?)?[^>]*>([\s\S]*?)<\/cover>/gi,
855
+ (_, variant, customClass, body) => {
856
+ const v = variant ? variant.toLowerCase() : "default";
857
+ const extra = customClass ? ` ${customClass}` : "";
858
+ const rendered = md.render(body.trim()).trim();
859
+ return `<div class="printdown-cover printdown-cover-${v}${extra}">
860
+ ${rendered}
861
+ </div>`;
663
862
  }
664
863
  );
665
864
  }
666
865
  }
866
+ for (let i = 0; i < tokens.length; i++) {
867
+ processToken(tokens[i]);
868
+ if (tokens[i].children) {
869
+ for (const child of tokens[i].children) {
870
+ processToken(child);
871
+ }
872
+ }
873
+ }
667
874
  });
668
875
  }
669
876
 
877
+ // src/markdown/plugins/math.ts
878
+ import katex from "katex";
879
+ function mathPlugin(md, options = {}) {
880
+ const defaultKatexOptions = {
881
+ throwOnError: false,
882
+ output: "htmlAndMathml",
883
+ ...options.katexOptions
884
+ };
885
+ function inlineMathRule(state, silent) {
886
+ const start = state.pos;
887
+ const max = state.posMax;
888
+ const src = state.src;
889
+ if (start > 0 && src.charCodeAt(start - 1) === 92) {
890
+ return false;
891
+ }
892
+ const firstChar = src.charCodeAt(start);
893
+ if (firstChar === 92 && src.charCodeAt(start + 1) === 40) {
894
+ let matchEnd = -1;
895
+ let pos = start + 2;
896
+ while (pos < max - 1) {
897
+ if (src.charCodeAt(pos) === 92 && src.charCodeAt(pos + 1) === 41) {
898
+ matchEnd = pos;
899
+ break;
900
+ }
901
+ pos++;
902
+ }
903
+ if (matchEnd === -1) return false;
904
+ if (!silent) {
905
+ const content = src.slice(start + 2, matchEnd).trim();
906
+ const token = state.push("math_inline", "math", 0);
907
+ token.content = content;
908
+ token.meta = { displayMode: false };
909
+ }
910
+ state.pos = matchEnd + 2;
911
+ return true;
912
+ }
913
+ if (firstChar === 36) {
914
+ const isDouble = src.charCodeAt(start + 1) === 36;
915
+ const delimiterLength = isDouble ? 2 : 1;
916
+ if (!isDouble) {
917
+ const nextChar = src.charCodeAt(start + 1);
918
+ if (nextChar === 32 || nextChar === 9 || nextChar === 10 || nextChar === 13 || isNaN(nextChar)) {
919
+ return false;
920
+ }
921
+ }
922
+ let matchEnd = -1;
923
+ let pos = start + delimiterLength;
924
+ while (pos < max) {
925
+ if (src.charCodeAt(pos) === 92) {
926
+ pos += 2;
927
+ continue;
928
+ }
929
+ if (isDouble) {
930
+ if (src.charCodeAt(pos) === 36 && src.charCodeAt(pos + 1) === 36) {
931
+ matchEnd = pos;
932
+ break;
933
+ }
934
+ } else {
935
+ if (src.charCodeAt(pos) === 36) {
936
+ const prevChar = src.charCodeAt(pos - 1);
937
+ if (prevChar !== 32 && prevChar !== 9 && prevChar !== 10 && prevChar !== 13) {
938
+ const nextChar = src.charCodeAt(pos + 1);
939
+ if (isNaN(nextChar) || nextChar < 48 || nextChar > 57) {
940
+ matchEnd = pos;
941
+ break;
942
+ }
943
+ }
944
+ }
945
+ }
946
+ pos++;
947
+ }
948
+ if (matchEnd === -1) return false;
949
+ const content = src.slice(start + delimiterLength, matchEnd).trim();
950
+ if (content.length === 0) return false;
951
+ if (!silent) {
952
+ const token = state.push(isDouble ? "math_block" : "math_inline", "math", 0);
953
+ token.content = content;
954
+ token.meta = { displayMode: isDouble };
955
+ }
956
+ state.pos = matchEnd + delimiterLength;
957
+ return true;
958
+ }
959
+ return false;
960
+ }
961
+ function blockMathRule(state, startLine, endLine, silent) {
962
+ const startPos = state.bMarks[startLine] + state.tShift[startLine];
963
+ const maxPos = state.eMarks[startLine];
964
+ const lineText = state.src.slice(startPos, maxPos).trim();
965
+ const containerMatch = lineText.match(/^:::\s*(math|latex|tex|katex)\s*$/i);
966
+ if (containerMatch) {
967
+ if (silent) return true;
968
+ let nextLine = startLine + 1;
969
+ let foundEnd = false;
970
+ while (nextLine < endLine) {
971
+ const pos = state.bMarks[nextLine] + state.tShift[nextLine];
972
+ const max = state.eMarks[nextLine];
973
+ const curLine = state.src.slice(pos, max).trim();
974
+ if (/^:::\s*$/.test(curLine) || /^:::\s*(math|latex|tex|katex)\s*$/i.test(curLine)) {
975
+ foundEnd = true;
976
+ break;
977
+ }
978
+ nextLine++;
979
+ }
980
+ const contentLines = [];
981
+ for (let l = startLine + 1; l < nextLine; l++) {
982
+ const pos = state.bMarks[l] + state.tShift[l];
983
+ const max = state.eMarks[l];
984
+ contentLines.push(state.src.slice(pos, max));
985
+ }
986
+ const token = state.push("math_block", "math", 0);
987
+ token.block = true;
988
+ token.content = contentLines.join("\n").trim();
989
+ token.meta = { displayMode: true };
990
+ state.line = foundEnd ? nextLine + 1 : nextLine;
991
+ return true;
992
+ }
993
+ if (lineText.startsWith("$$")) {
994
+ if (lineText.length > 2 && lineText.endsWith("$$")) {
995
+ if (silent) return true;
996
+ const mathContent = lineText.slice(2, -2).trim();
997
+ const token2 = state.push("math_block", "math", 0);
998
+ token2.block = true;
999
+ token2.content = mathContent;
1000
+ token2.meta = { displayMode: true };
1001
+ state.line = startLine + 1;
1002
+ return true;
1003
+ }
1004
+ if (silent) return true;
1005
+ const firstLineContent = lineText.slice(2).trim();
1006
+ const contentLines = [];
1007
+ if (firstLineContent) {
1008
+ contentLines.push(firstLineContent);
1009
+ }
1010
+ let nextLine = startLine + 1;
1011
+ let foundEnd = false;
1012
+ while (nextLine < endLine) {
1013
+ const pos = state.bMarks[nextLine] + state.tShift[nextLine];
1014
+ const max = state.eMarks[nextLine];
1015
+ const curLine = state.src.slice(pos, max).trim();
1016
+ if (curLine.endsWith("$$")) {
1017
+ foundEnd = true;
1018
+ const lastLineContent = curLine.slice(0, -2).trim();
1019
+ if (lastLineContent) {
1020
+ contentLines.push(lastLineContent);
1021
+ }
1022
+ break;
1023
+ }
1024
+ contentLines.push(state.src.slice(pos, max));
1025
+ nextLine++;
1026
+ }
1027
+ const token = state.push("math_block", "math", 0);
1028
+ token.block = true;
1029
+ token.content = contentLines.join("\n").trim();
1030
+ token.meta = { displayMode: true };
1031
+ state.line = foundEnd ? nextLine + 1 : nextLine;
1032
+ return true;
1033
+ }
1034
+ return false;
1035
+ }
1036
+ md.inline.ruler.before("escape", "math_inline", inlineMathRule);
1037
+ md.block.ruler.before("fence", "math_block", blockMathRule);
1038
+ const originalFence = md.renderer.rules.fence;
1039
+ md.renderer.rules.fence = (tokens, idx, opt, env, self) => {
1040
+ const token = tokens[idx];
1041
+ const info = token.info ? token.info.trim().toLowerCase() : "";
1042
+ if (info === "math" || info === "latex" || info === "tex" || info === "katex") {
1043
+ try {
1044
+ const rendered = katex.renderToString(token.content.trim(), {
1045
+ ...defaultKatexOptions,
1046
+ displayMode: true
1047
+ });
1048
+ return `<div class="printdown-math-block katex-display">${rendered}</div>
1049
+ `;
1050
+ } catch (err) {
1051
+ const errMessage = err instanceof Error ? err.message : String(err);
1052
+ return `<div class="printdown-math-block katex-display"><span class="katex-error">${errMessage}</span></div>
1053
+ `;
1054
+ }
1055
+ }
1056
+ if (originalFence) {
1057
+ return originalFence(tokens, idx, opt, env, self);
1058
+ }
1059
+ return self.renderToken(tokens, idx, opt);
1060
+ };
1061
+ md.renderer.rules.math_inline = (tokens, idx) => {
1062
+ const token = tokens[idx];
1063
+ try {
1064
+ const rendered = katex.renderToString(token.content, {
1065
+ ...defaultKatexOptions,
1066
+ displayMode: false
1067
+ });
1068
+ return `<span class="printdown-math-inline">${rendered}</span>`;
1069
+ } catch (err) {
1070
+ const errMessage = err instanceof Error ? err.message : String(err);
1071
+ return `<span class="printdown-math-inline katex-error">${errMessage}</span>`;
1072
+ }
1073
+ };
1074
+ md.renderer.rules.math_block = (tokens, idx) => {
1075
+ const token = tokens[idx];
1076
+ const displayMode = token.meta?.displayMode !== false;
1077
+ try {
1078
+ const rendered = katex.renderToString(token.content, {
1079
+ ...defaultKatexOptions,
1080
+ displayMode
1081
+ });
1082
+ return `<div class="printdown-math-block katex-display">${rendered}</div>
1083
+ `;
1084
+ } catch (err) {
1085
+ const errMessage = err instanceof Error ? err.message : String(err);
1086
+ return `<div class="printdown-math-block katex-display"><span class="katex-error">${errMessage}</span></div>
1087
+ `;
1088
+ }
1089
+ };
1090
+ }
1091
+
670
1092
  // src/markdown/index.ts
671
1093
  var DEFAULT_LANGS = [
672
1094
  "javascript",
@@ -743,14 +1165,22 @@ async function createMarkdownRenderer(options = {}) {
743
1165
  md.use(kbdPlugin);
744
1166
  md.use(centerPlugin);
745
1167
  md.use(headingPlugin);
1168
+ md.use(pageBreakPlugin);
1169
+ md.use(coverPlugin);
1170
+ md.use(mathPlugin, { katexOptions: options.katex });
746
1171
  const highlighter = await getHighlighter(options.shiki);
747
1172
  const theme = options.shiki?.theme || DEFAULT_THEME;
748
1173
  const defaultFence = md.renderer.rules.fence;
749
1174
  md.renderer.rules.fence = (tokens, idx, fenceOptions, env, self) => {
750
1175
  const token = tokens[idx];
751
1176
  const info = token.info ? token.info.trim() : "";
752
- const lang = info ? info.split(/\s+/)[0] : "text";
1177
+ const lang = info ? info.split(/\s+/)[0].toLowerCase() : "text";
753
1178
  const code = token.content;
1179
+ if (lang === "math" || lang === "latex" || lang === "tex" || lang === "katex") {
1180
+ if (defaultFence) {
1181
+ return defaultFence(tokens, idx, fenceOptions, env, self);
1182
+ }
1183
+ }
754
1184
  try {
755
1185
  const loadedLangs = highlighter.getLoadedLanguages();
756
1186
  const targetLang = loadedLangs.includes(lang) ? lang : "text";
@@ -781,11 +1211,79 @@ async function createMarkdownRenderer(options = {}) {
781
1211
  import fs from "fs";
782
1212
  import path from "path";
783
1213
  import { chromium } from "playwright";
1214
+
1215
+ // src/renderer/header-footer.ts
1216
+ var DEFAULT_FONT_STYLE = 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 8pt; color: #64748b; width: 100%; padding: 0 18mm; box-sizing: border-box; -webkit-print-color-adjust: exact;';
1217
+ function replacePlaceholdersWithHtml(str) {
1218
+ return str.replace(/\{page\}/gi, '<span class="pageNumber"></span>').replace(/\{(?:total|totalPages)\}/gi, '<span class="totalPages"></span>').replace(/\{date\}/gi, '<span class="date"></span>').replace(/\{title\}/gi, '<span class="title"></span>');
1219
+ }
1220
+ function buildHeaderFooterHtml(content, type) {
1221
+ const text = typeof content === "boolean" ? type === "header" ? "{title}" : "{page} / {total}" : content.trim();
1222
+ const parts = text.split("|").map((p) => replacePlaceholdersWithHtml(p.trim()));
1223
+ let innerHtml;
1224
+ if (parts.length === 1) {
1225
+ const isCentered = type === "footer" || text.includes("{page}") || text.includes("{total}") || text.includes('<span class="pageNumber">');
1226
+ innerHtml = `<div style="display: flex; justify-content: ${isCentered ? "center" : "space-between"}; width: 100%; align-items: center;"><span>${parts[0]}</span></div>`;
1227
+ } else if (parts.length === 2) {
1228
+ innerHtml = `<div style="display: flex; justify-content: space-between; width: 100%; align-items: center;"><span>${parts[0]}</span><span>${parts[1]}</span></div>`;
1229
+ } else {
1230
+ innerHtml = `<div style="display: flex; justify-content: space-between; width: 100%; align-items: center;"><span style="flex: 1; text-align: left;">${parts[0]}</span><span style="flex: 1; text-align: center;">${parts[1]}</span><span style="flex: 1; text-align: right;">${parts[2]}</span></div>`;
1231
+ }
1232
+ return `<div style="${DEFAULT_FONT_STYLE}">${innerHtml}</div>`;
1233
+ }
1234
+ function resolveHeaderFooterPdfOptions(options) {
1235
+ const hasHeader = Boolean(options.header || options.headerTemplate);
1236
+ const hasPageNumber = Boolean(options.pageNumber);
1237
+ const hasFooter = Boolean(options.footer || options.footerTemplate || hasPageNumber);
1238
+ const shouldDisplay = options.displayHeaderFooter ?? (hasHeader || hasFooter);
1239
+ if (!shouldDisplay) {
1240
+ return {};
1241
+ }
1242
+ let headerTemplate = options.headerTemplate;
1243
+ if (!headerTemplate && options.header) {
1244
+ headerTemplate = buildHeaderFooterHtml(options.header, "header");
1245
+ }
1246
+ if (!headerTemplate) {
1247
+ headerTemplate = '<span style="font-size: 0px;"></span>';
1248
+ }
1249
+ let footerTemplate = options.footerTemplate;
1250
+ if (!footerTemplate) {
1251
+ if (options.footer) {
1252
+ footerTemplate = buildHeaderFooterHtml(options.footer, "footer");
1253
+ } else if (options.pageNumber) {
1254
+ const pageFormat = typeof options.pageNumber === "string" ? options.pageNumber : "{page} / {total}";
1255
+ footerTemplate = buildHeaderFooterHtml(pageFormat, "footer");
1256
+ }
1257
+ }
1258
+ if (!footerTemplate) {
1259
+ footerTemplate = '<span style="font-size: 0px;"></span>';
1260
+ }
1261
+ return {
1262
+ displayHeaderFooter: true,
1263
+ headerTemplate,
1264
+ footerTemplate,
1265
+ margin: {
1266
+ top: "20mm",
1267
+ bottom: "20mm",
1268
+ left: "18mm",
1269
+ right: "18mm"
1270
+ }
1271
+ };
1272
+ }
1273
+ function formatHeaderFooterCanvasText(content, pageIndex, totalPages, docTitle) {
1274
+ const text = typeof content === "boolean" ? "{page} / {total}" : content;
1275
+ const today = (/* @__PURE__ */ new Date()).toLocaleDateString();
1276
+ const title = docTitle || "";
1277
+ return text.replace(/\{page\}/gi, String(pageIndex)).replace(/\{(?:total|totalPages)\}/gi, String(totalPages)).replace(/\{date\}/gi, today).replace(/\{title\}/gi, title);
1278
+ }
1279
+
1280
+ // src/renderer/index.ts
784
1281
  async function setupFontRouting(context) {
785
1282
  const interDir = getFontDir("@fontsource-variable/inter/index.css");
786
1283
  const notoDir = getFontDir("@fontsource-variable/noto-sans-jp/index.css");
787
1284
  const jbDir = getFontDir("@fontsource-variable/jetbrains-mono/index.css");
788
1285
  const genDir = getFontDir("gen-interface-jp/500.css");
1286
+ const katexDir = getFontDir("katex/dist/katex.min.css");
789
1287
  await context.route("https://printdown.local/fonts/**", async (route) => {
790
1288
  const reqUrl = route.request().url();
791
1289
  let filePath = "";
@@ -813,13 +1311,20 @@ async function setupFontRouting(context) {
813
1311
  "w",
814
1312
  reqUrl.replace("https://printdown.local/fonts/gen-interface-jp/w/", "")
815
1313
  );
1314
+ } else if (katexDir && reqUrl.startsWith("https://printdown.local/fonts/katex/")) {
1315
+ filePath = path.join(
1316
+ katexDir,
1317
+ "fonts",
1318
+ reqUrl.replace("https://printdown.local/fonts/katex/", "")
1319
+ );
816
1320
  }
817
1321
  if (filePath && fs.existsSync(filePath)) {
818
1322
  try {
819
1323
  const body = fs.readFileSync(filePath);
1324
+ const contentType = filePath.endsWith(".woff2") ? "font/woff2" : filePath.endsWith(".woff") ? "font/woff" : filePath.endsWith(".ttf") ? "font/ttf" : "font/woff2";
820
1325
  await route.fulfill({
821
1326
  status: 200,
822
- contentType: "font/woff2",
1327
+ contentType,
823
1328
  body
824
1329
  });
825
1330
  return;
@@ -906,9 +1411,11 @@ var BrowserRenderer = class {
906
1411
  );
907
1412
  try {
908
1413
  if (format === "pdf") {
1414
+ const headerFooterOptions = resolveHeaderFooterPdfOptions(options);
909
1415
  const pdfBuffer = await page.pdf({
910
1416
  printBackground: true,
911
1417
  preferCSSPageSize: true,
1418
+ ...headerFooterOptions,
912
1419
  ...options.pdf
913
1420
  });
914
1421
  return Buffer.from(pdfBuffer);
@@ -962,8 +1469,14 @@ var BrowserRenderer = class {
962
1469
  const deviceScaleFactor = options.scale ?? options.deviceScaleFactor ?? 2;
963
1470
  const pagesHtml = html.includes("</head>") ? html.replace(
964
1471
  "</head>",
965
- "<style>.printdown { padding-top: 0 !important; padding-bottom: 0 !important; }</style></head>"
966
- ) : html + "<style>.printdown { padding-top: 0 !important; padding-bottom: 0 !important; }</style>";
1472
+ `<style>
1473
+ .printdown { padding-top: 0 !important; padding-bottom: 0 !important; }
1474
+ .printdown .printdown-cover, .printdown cover { min-height: ${contentHeightPerPage}px !important; height: ${contentHeightPerPage}px !important; margin: 0 !important; }
1475
+ </style></head>`
1476
+ ) : html + `<style>
1477
+ .printdown { padding-top: 0 !important; padding-bottom: 0 !important; }
1478
+ .printdown .printdown-cover, .printdown cover { min-height: ${contentHeightPerPage}px !important; height: ${contentHeightPerPage}px !important; margin: 0 !important; }
1479
+ </style>`;
967
1480
  const { browser, context, page, isLocalBrowser } = await this.preparePage(
968
1481
  pagesHtml,
969
1482
  width,
@@ -972,10 +1485,112 @@ var BrowserRenderer = class {
972
1485
  );
973
1486
  let composePage = null;
974
1487
  try {
1488
+ const pageSlices = await page.evaluate((maxPageH) => {
1489
+ const totalH = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
1490
+ const selectors = [
1491
+ "h1",
1492
+ "h2",
1493
+ "h3",
1494
+ "h4",
1495
+ "h5",
1496
+ "h6",
1497
+ "p",
1498
+ "ul > li",
1499
+ "ol > li",
1500
+ ".printdown-task-list-item",
1501
+ "blockquote",
1502
+ "table",
1503
+ "tr",
1504
+ "pre",
1505
+ ".printdown-cover",
1506
+ "cover",
1507
+ ".printdown-page-break",
1508
+ ".page-break",
1509
+ ".break-before",
1510
+ ".break-after"
1511
+ ];
1512
+ const elements = Array.from(document.querySelectorAll(selectors.join(",")));
1513
+ const items = [];
1514
+ for (const el of elements) {
1515
+ const rect = el.getBoundingClientRect();
1516
+ const top = rect.top + window.scrollY;
1517
+ const bottom = rect.bottom + window.scrollY;
1518
+ const isForcedBreak = el.classList.contains("printdown-page-break") || el.classList.contains("page-break") || el.classList.contains("break-before") || window.getComputedStyle(el).breakBefore === "page" || window.getComputedStyle(el).pageBreakBefore === "always";
1519
+ const isForcedAfterBreak = el.classList.contains("printdown-cover") || el.tagName.toLowerCase() === "cover" || el.classList.contains("break-after") || el.classList.contains("printdown-break-after") || window.getComputedStyle(el).breakAfter === "page" || window.getComputedStyle(el).pageBreakAfter === "always";
1520
+ if (rect.height > 0 || isForcedBreak || isForcedAfterBreak) {
1521
+ items.push({ top, bottom, isForcedBreak, isForcedAfterBreak });
1522
+ }
1523
+ }
1524
+ items.sort((a, b) => a.top - b.top);
1525
+ const headings = Array.from(
1526
+ document.querySelectorAll("h1, h2, h3, h4, h5, h6")
1527
+ ).map((h) => {
1528
+ const rect = h.getBoundingClientRect();
1529
+ return {
1530
+ top: rect.top + window.scrollY,
1531
+ bottom: rect.bottom + window.scrollY
1532
+ };
1533
+ });
1534
+ const slices = [];
1535
+ let currentY = 0;
1536
+ while (currentY < totalH - 5) {
1537
+ const idealBottom = currentY + maxPageH;
1538
+ if (idealBottom >= totalH) {
1539
+ slices.push({ y: currentY, h: totalH - currentY });
1540
+ break;
1541
+ }
1542
+ const forcedAfterBreak = items.find(
1543
+ (item) => item.isForcedAfterBreak && item.bottom > currentY && item.top >= currentY - 5
1544
+ );
1545
+ if (forcedAfterBreak && forcedAfterBreak.bottom > currentY) {
1546
+ slices.push({ y: currentY, h: Math.max(20, forcedAfterBreak.bottom - currentY) });
1547
+ currentY = forcedAfterBreak.bottom;
1548
+ continue;
1549
+ }
1550
+ const forcedBreak = items.find(
1551
+ (item) => item.isForcedBreak && item.top > currentY + 10 && item.top <= idealBottom
1552
+ );
1553
+ if (forcedBreak) {
1554
+ slices.push({ y: currentY, h: Math.max(20, forcedBreak.top - currentY) });
1555
+ currentY = forcedBreak.top;
1556
+ continue;
1557
+ }
1558
+ let bestBreakY = idealBottom;
1559
+ let foundGoodBreak = false;
1560
+ const orphanHeading = headings.find(
1561
+ (h) => h.top > currentY + 50 && h.top <= idealBottom && idealBottom - h.top < 80
1562
+ );
1563
+ if (orphanHeading) {
1564
+ bestBreakY = orphanHeading.top;
1565
+ foundGoodBreak = true;
1566
+ } else {
1567
+ for (let i = items.length - 1; i >= 0; i--) {
1568
+ const item = items[i];
1569
+ if (item.top > currentY + 40 && item.top <= idealBottom) {
1570
+ if (item.bottom > idealBottom) {
1571
+ bestBreakY = item.top;
1572
+ foundGoodBreak = true;
1573
+ break;
1574
+ } else if (idealBottom - item.bottom <= 50) {
1575
+ bestBreakY = item.bottom;
1576
+ foundGoodBreak = true;
1577
+ break;
1578
+ }
1579
+ }
1580
+ }
1581
+ }
1582
+ if (!foundGoodBreak || bestBreakY <= currentY + 50) {
1583
+ bestBreakY = idealBottom;
1584
+ }
1585
+ slices.push({ y: currentY, h: bestBreakY - currentY });
1586
+ currentY = bestBreakY;
1587
+ }
1588
+ return slices.length > 0 ? slices : [{ y: 0, h: totalH }];
1589
+ }, contentHeightPerPage);
975
1590
  const totalContentHeight = await page.evaluate(
976
1591
  () => Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)
977
1592
  );
978
- const pageCount = Math.max(1, Math.ceil(totalContentHeight / contentHeightPerPage));
1593
+ const pageCount = pageSlices.length;
979
1594
  await page.setViewportSize({
980
1595
  width,
981
1596
  height: Math.max(totalContentHeight + 100, pageHeight)
@@ -1003,10 +1618,35 @@ var BrowserRenderer = class {
1003
1618
  const pageBuffers = [];
1004
1619
  const imageType = format === "jpeg" ? "jpeg" : format === "webp" ? "webp" : "png";
1005
1620
  for (let i = 0; i < pageCount; i++) {
1006
- const srcY = i * contentHeightPerPage;
1007
- const srcH = Math.min(contentHeightPerPage, totalContentHeight - srcY);
1621
+ const slice = pageSlices[i];
1622
+ const srcY = slice.y;
1623
+ const srcH = slice.h;
1624
+ const headerText = options.header ? formatHeaderFooterCanvasText(
1625
+ options.header === true ? options.title || "" : options.header,
1626
+ i + 1,
1627
+ pageCount,
1628
+ options.title
1629
+ ) : "";
1630
+ const footerContent = options.footer || options.pageNumber;
1631
+ const footerText = footerContent ? formatHeaderFooterCanvasText(
1632
+ typeof footerContent === "boolean" ? "{page} / {total}" : footerContent,
1633
+ i + 1,
1634
+ pageCount,
1635
+ options.title
1636
+ ) : "";
1008
1637
  await composePage.evaluate(
1009
- async ({ base64, width: width2, marginTop: marginTop2, srcY: srcY2, srcH: srcH2, scale }) => {
1638
+ async ({
1639
+ base64,
1640
+ width: width2,
1641
+ pageHeight: pageHeight2,
1642
+ marginTop: marginTop2,
1643
+ marginBottom: marginBottom2,
1644
+ srcY: srcY2,
1645
+ srcH: srcH2,
1646
+ scale,
1647
+ headerText: headerText2,
1648
+ footerText: footerText2
1649
+ }) => {
1010
1650
  const canvas = document.getElementById("canvas");
1011
1651
  const ctx = canvas.getContext("2d");
1012
1652
  if (!ctx) return;
@@ -1028,14 +1668,66 @@ var BrowserRenderer = class {
1028
1668
  width2 * scale,
1029
1669
  srcH2 * scale
1030
1670
  );
1671
+ if (headerText2) {
1672
+ ctx.font = `${9 * scale}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
1673
+ ctx.fillStyle = "#64748b";
1674
+ ctx.textBaseline = "middle";
1675
+ const parts = headerText2.split("|").map((s) => s.trim());
1676
+ const paddingX = 18 / 210 * width2 * scale;
1677
+ const y = marginTop2 * scale / 2;
1678
+ if (parts.length === 1) {
1679
+ ctx.textAlign = "center";
1680
+ ctx.fillText(parts[0], width2 * scale / 2, y);
1681
+ } else if (parts.length === 2) {
1682
+ ctx.textAlign = "left";
1683
+ ctx.fillText(parts[0], paddingX, y);
1684
+ ctx.textAlign = "right";
1685
+ ctx.fillText(parts[1], width2 * scale - paddingX, y);
1686
+ } else {
1687
+ ctx.textAlign = "left";
1688
+ ctx.fillText(parts[0], paddingX, y);
1689
+ ctx.textAlign = "center";
1690
+ ctx.fillText(parts[1], width2 * scale / 2, y);
1691
+ ctx.textAlign = "right";
1692
+ ctx.fillText(parts[2], width2 * scale - paddingX, y);
1693
+ }
1694
+ }
1695
+ if (footerText2) {
1696
+ ctx.font = `${9 * scale}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
1697
+ ctx.fillStyle = "#64748b";
1698
+ ctx.textBaseline = "middle";
1699
+ const parts = footerText2.split("|").map((s) => s.trim());
1700
+ const paddingX = 18 / 210 * width2 * scale;
1701
+ const y = (pageHeight2 - marginBottom2 / 2) * scale;
1702
+ if (parts.length === 1) {
1703
+ ctx.textAlign = "center";
1704
+ ctx.fillText(parts[0], width2 * scale / 2, y);
1705
+ } else if (parts.length === 2) {
1706
+ ctx.textAlign = "left";
1707
+ ctx.fillText(parts[0], paddingX, y);
1708
+ ctx.textAlign = "right";
1709
+ ctx.fillText(parts[1], width2 * scale - paddingX, y);
1710
+ } else {
1711
+ ctx.textAlign = "left";
1712
+ ctx.fillText(parts[0], paddingX, y);
1713
+ ctx.textAlign = "center";
1714
+ ctx.fillText(parts[1], width2 * scale / 2, y);
1715
+ ctx.textAlign = "right";
1716
+ ctx.fillText(parts[2], width2 * scale - paddingX, y);
1717
+ }
1718
+ }
1031
1719
  },
1032
1720
  {
1033
1721
  base64: base64Img,
1034
1722
  width,
1723
+ pageHeight,
1035
1724
  marginTop,
1725
+ marginBottom,
1036
1726
  srcY,
1037
1727
  srcH,
1038
- scale: deviceScaleFactor
1728
+ scale: deviceScaleFactor,
1729
+ headerText,
1730
+ footerText
1039
1731
  }
1040
1732
  );
1041
1733
  const screenshotBuffer = await composePage.screenshot({
@@ -1072,8 +1764,106 @@ async function renderHtmlToPageBuffers(html, format, options = {}) {
1072
1764
  }
1073
1765
 
1074
1766
  // src/renderer/html.ts
1767
+ import fs3 from "fs";
1768
+ import path3 from "path";
1769
+
1770
+ // src/renderer/images.ts
1075
1771
  import fs2 from "fs";
1076
1772
  import path2 from "path";
1773
+ import url from "url";
1774
+ var MIME_TYPES = {
1775
+ ".png": "image/png",
1776
+ ".jpg": "image/jpeg",
1777
+ ".jpeg": "image/jpeg",
1778
+ ".webp": "image/webp",
1779
+ ".gif": "image/gif",
1780
+ ".svg": "image/svg+xml",
1781
+ ".ico": "image/x-icon",
1782
+ ".avif": "image/avif",
1783
+ ".bmp": "image/bmp",
1784
+ ".tiff": "image/tiff",
1785
+ ".tif": "image/tiff"
1786
+ };
1787
+ function resolveLocalImageToDataUri(src, baseUrl) {
1788
+ if (!src || typeof src !== "string") return null;
1789
+ const trimmed = src.trim();
1790
+ if (trimmed.startsWith("data:") || trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("//") || trimmed.startsWith("blob:")) {
1791
+ return null;
1792
+ }
1793
+ let rawPath = trimmed;
1794
+ if (rawPath.startsWith("file://")) {
1795
+ try {
1796
+ rawPath = url.fileURLToPath(rawPath);
1797
+ } catch {
1798
+ rawPath = rawPath.replace(/^file:\/\//, "");
1799
+ }
1800
+ }
1801
+ const [cleanPath] = rawPath.split(/[?#]/);
1802
+ let decodedPath;
1803
+ try {
1804
+ decodedPath = decodeURIComponent(cleanPath);
1805
+ } catch {
1806
+ decodedPath = cleanPath;
1807
+ }
1808
+ const candidates = [];
1809
+ if (path2.isAbsolute(decodedPath)) {
1810
+ candidates.push(decodedPath);
1811
+ } else {
1812
+ if (baseUrl) {
1813
+ const baseDir = fs2.existsSync(baseUrl) && fs2.statSync(baseUrl).isFile() ? path2.dirname(baseUrl) : baseUrl;
1814
+ candidates.push(path2.resolve(baseDir, decodedPath));
1815
+ }
1816
+ candidates.push(path2.resolve(process.cwd(), decodedPath));
1817
+ }
1818
+ for (const candidate of candidates) {
1819
+ try {
1820
+ if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
1821
+ const fileBuffer = fs2.readFileSync(candidate);
1822
+ const ext = path2.extname(candidate).toLowerCase();
1823
+ const mimeType = MIME_TYPES[ext] || "image/png";
1824
+ return `data:${mimeType};base64,${fileBuffer.toString("base64")}`;
1825
+ }
1826
+ } catch {
1827
+ }
1828
+ }
1829
+ return null;
1830
+ }
1831
+ function inlineLocalImagesInHtml(html, baseUrl) {
1832
+ if (!html) return html;
1833
+ let result = html.replace(
1834
+ /(<img\b[^>]*?\bsrc\s*=\s*)(["']?)([^"'\s>]+)\2([^>]*>)/gi,
1835
+ (match, prefix, _quote, src, suffix) => {
1836
+ const dataUri = resolveLocalImageToDataUri(src, baseUrl);
1837
+ if (dataUri) {
1838
+ return `${prefix}"${dataUri}"${suffix}`;
1839
+ }
1840
+ return match;
1841
+ }
1842
+ );
1843
+ result = result.replace(
1844
+ /(<image\b[^>]*?\b(?:href|xlink:href)\s*=\s*)(["']?)([^"'\s>]+)\2([^>]*>)/gi,
1845
+ (match, prefix, _quote, src, suffix) => {
1846
+ const dataUri = resolveLocalImageToDataUri(src, baseUrl);
1847
+ if (dataUri) {
1848
+ return `${prefix}"${dataUri}"${suffix}`;
1849
+ }
1850
+ return match;
1851
+ }
1852
+ );
1853
+ result = result.replace(/url\(\s*(["']?)([^"')]+)\1\s*\)/gi, (match, _quote, urlStr) => {
1854
+ if (urlStr.startsWith("https://printdown.local/fonts/") || urlStr.startsWith("data:") || urlStr.startsWith("http://") || urlStr.startsWith("https://")) {
1855
+ return match;
1856
+ }
1857
+ const dataUri = resolveLocalImageToDataUri(urlStr, baseUrl);
1858
+ if (dataUri) {
1859
+ return `url("${dataUri}")`;
1860
+ }
1861
+ return match;
1862
+ });
1863
+ return result;
1864
+ }
1865
+
1866
+ // src/renderer/html.ts
1077
1867
  function resolveCss(cssInput, baseUrl) {
1078
1868
  const inputs = Array.isArray(cssInput) ? cssInput : [cssInput];
1079
1869
  const resolvedCssParts = [];
@@ -1081,22 +1871,22 @@ function resolveCss(cssInput, baseUrl) {
1081
1871
  if (!input || typeof input !== "string") continue;
1082
1872
  let isFile = false;
1083
1873
  let filePath = input;
1084
- if (baseUrl && !path2.isAbsolute(input)) {
1085
- const candidate = path2.resolve(baseUrl, input);
1086
- if (fs2.existsSync(candidate) && fs2.statSync(candidate).isFile()) {
1874
+ if (baseUrl && !path3.isAbsolute(input)) {
1875
+ const candidate = path3.resolve(baseUrl, input);
1876
+ if (fs3.existsSync(candidate) && fs3.statSync(candidate).isFile()) {
1087
1877
  isFile = true;
1088
1878
  filePath = candidate;
1089
1879
  }
1090
1880
  }
1091
- if (!isFile && (fs2.existsSync(input) || input.endsWith(".css"))) {
1092
- if (fs2.existsSync(input) && fs2.statSync(input).isFile()) {
1881
+ if (!isFile && (fs3.existsSync(input) || input.endsWith(".css"))) {
1882
+ if (fs3.existsSync(input) && fs3.statSync(input).isFile()) {
1093
1883
  isFile = true;
1094
- filePath = path2.resolve(input);
1884
+ filePath = path3.resolve(input);
1095
1885
  }
1096
1886
  }
1097
1887
  if (isFile) {
1098
1888
  try {
1099
- const fileContent = fs2.readFileSync(filePath, "utf-8");
1889
+ const fileContent = fs3.readFileSync(filePath, "utf-8");
1100
1890
  resolvedCssParts.push(fileContent);
1101
1891
  } catch (err) {
1102
1892
  console.warn(`[printdown] Failed to read CSS file "${filePath}":`, err);
@@ -1120,7 +1910,9 @@ function buildHtmlDocument(bodyHtml, options = {}) {
1120
1910
  themeCss = defaultThemeCss;
1121
1911
  }
1122
1912
  }
1123
- const userCss = options.css ? resolveCss(options.css, options.baseUrl) : "";
1913
+ const userCssRaw = options.css ? resolveCss(options.css, options.baseUrl) : "";
1914
+ const userCss = userCssRaw ? inlineLocalImagesInHtml(userCssRaw, options.baseUrl) : "";
1915
+ const processedBodyHtml = inlineLocalImagesInHtml(bodyHtml, options.baseUrl);
1124
1916
  const baseTag = options.baseUrl ? `<base href="${options.baseUrl.endsWith("/") ? options.baseUrl : options.baseUrl + "/"}">` : "";
1125
1917
  return `<!DOCTYPE html>
1126
1918
  <html lang="ja">
@@ -1134,7 +1926,7 @@ function buildHtmlDocument(bodyHtml, options = {}) {
1134
1926
  </head>
1135
1927
  <body>
1136
1928
  <article class="printdown">
1137
- ${bodyHtml}
1929
+ ${processedBodyHtml}
1138
1930
  </article>
1139
1931
  </body>
1140
1932
  </html>`;
@@ -1146,7 +1938,7 @@ function inferFormat(output, explicitFormat) {
1146
1938
  return explicitFormat;
1147
1939
  }
1148
1940
  if (output) {
1149
- const ext = path3.extname(output).toLowerCase();
1941
+ const ext = path4.extname(output).toLowerCase();
1150
1942
  if (ext === ".png") return "png";
1151
1943
  if (ext === ".jpg" || ext === ".jpeg") return "jpeg";
1152
1944
  if (ext === ".webp") return "webp";
@@ -1160,14 +1952,18 @@ function resolvePageOutputFilePath(outputPath, pageIndex, totalPages) {
1160
1952
  return outputPath.replace(/%d/g, String(pageNumber));
1161
1953
  }
1162
1954
  if (totalPages === 1 && !outputPath.includes("-1")) {
1163
- const parsed2 = path3.parse(outputPath);
1164
- return path3.join(parsed2.dir, `${parsed2.name}-${pageNumber}${parsed2.ext}`);
1955
+ const parsed2 = path4.parse(outputPath);
1956
+ return path4.join(parsed2.dir, `${parsed2.name}-${pageNumber}${parsed2.ext}`);
1165
1957
  }
1166
- const parsed = path3.parse(outputPath);
1167
- return path3.join(parsed.dir, `${parsed.name}-${pageNumber}${parsed.ext}`);
1958
+ const parsed = path4.parse(outputPath);
1959
+ return path4.join(parsed.dir, `${parsed.name}-${pageNumber}${parsed.ext}`);
1168
1960
  }
1169
1961
  async function renderToHtml(markdown, options = {}) {
1170
- const md = await createMarkdownRenderer(options.markdown);
1962
+ const markdownOptions = {
1963
+ ...options.markdown,
1964
+ ...options.katex ? { katex: options.katex } : {}
1965
+ };
1966
+ const md = await createMarkdownRenderer(markdownOptions);
1171
1967
  const bodyHtml = md.render(markdown);
1172
1968
  return buildHtmlDocument(bodyHtml, options);
1173
1969
  }
@@ -1176,27 +1972,27 @@ async function renderPages(markdown, options = {}) {
1176
1972
  const html = await renderToHtml(markdown, options);
1177
1973
  const buffers = await renderHtmlToPageBuffers(html, format, options);
1178
1974
  if (options.output) {
1179
- const resolvedPath = path3.resolve(options.output);
1180
- const outputDir = path3.dirname(resolvedPath);
1181
- if (!fs3.existsSync(outputDir)) {
1182
- fs3.mkdirSync(outputDir, { recursive: true });
1975
+ const resolvedPath = path4.resolve(options.output);
1976
+ const outputDir = path4.dirname(resolvedPath);
1977
+ if (!fs4.existsSync(outputDir)) {
1978
+ fs4.mkdirSync(outputDir, { recursive: true });
1183
1979
  }
1184
1980
  if (format === "pdf" && buffers.length === 1) {
1185
- fs3.writeFileSync(resolvedPath, buffers[0]);
1981
+ fs4.writeFileSync(resolvedPath, buffers[0]);
1186
1982
  } else {
1187
1983
  buffers.forEach((buf, idx) => {
1188
1984
  const filePath = resolvePageOutputFilePath(resolvedPath, idx, buffers.length);
1189
- fs3.writeFileSync(filePath, buf);
1985
+ fs4.writeFileSync(filePath, buf);
1190
1986
  });
1191
1987
  }
1192
1988
  }
1193
1989
  return buffers;
1194
1990
  }
1195
1991
  async function renderFilePages(filePath, options = {}) {
1196
- const absolutePath = path3.resolve(filePath);
1992
+ const absolutePath = path4.resolve(filePath);
1197
1993
  const encoding = options.encoding ?? "utf-8";
1198
- const markdown = fs3.readFileSync(absolutePath, encoding);
1199
- const fileDir = path3.dirname(absolutePath);
1994
+ const markdown = fs4.readFileSync(absolutePath, encoding);
1995
+ const fileDir = path4.dirname(absolutePath);
1200
1996
  const baseUrl = options.baseUrl ?? fileDir;
1201
1997
  return renderPages(markdown, {
1202
1998
  baseUrl,
@@ -1211,20 +2007,20 @@ async function render(markdown, options = {}) {
1211
2007
  const html = await renderToHtml(markdown, options);
1212
2008
  const buffer = await renderHtmlToBuffer(html, format, options);
1213
2009
  if (options.output) {
1214
- const outputPath = path3.resolve(options.output);
1215
- const outputDir = path3.dirname(outputPath);
1216
- if (!fs3.existsSync(outputDir)) {
1217
- fs3.mkdirSync(outputDir, { recursive: true });
2010
+ const outputPath = path4.resolve(options.output);
2011
+ const outputDir = path4.dirname(outputPath);
2012
+ if (!fs4.existsSync(outputDir)) {
2013
+ fs4.mkdirSync(outputDir, { recursive: true });
1218
2014
  }
1219
- fs3.writeFileSync(outputPath, buffer);
2015
+ fs4.writeFileSync(outputPath, buffer);
1220
2016
  }
1221
2017
  return buffer;
1222
2018
  }
1223
2019
  async function renderFile(filePath, options = {}) {
1224
- const absolutePath = path3.resolve(filePath);
2020
+ const absolutePath = path4.resolve(filePath);
1225
2021
  const encoding = options.encoding ?? "utf-8";
1226
- const markdown = fs3.readFileSync(absolutePath, encoding);
1227
- const fileDir = path3.dirname(absolutePath);
2022
+ const markdown = fs4.readFileSync(absolutePath, encoding);
2023
+ const fileDir = path4.dirname(absolutePath);
1228
2024
  const baseUrl = options.baseUrl ?? fileDir;
1229
2025
  if (options.pages) {
1230
2026
  return renderPages(markdown, {
@@ -1239,13 +2035,19 @@ async function renderFile(filePath, options = {}) {
1239
2035
  }
1240
2036
 
1241
2037
  export {
2038
+ mathPlugin,
1242
2039
  createMarkdownRenderer,
2040
+ buildHeaderFooterHtml,
2041
+ resolveHeaderFooterPdfOptions,
2042
+ formatHeaderFooterCanvasText,
1243
2043
  BrowserRenderer,
1244
2044
  renderHtmlToPageBuffers,
2045
+ resolveLocalImageToDataUri,
2046
+ inlineLocalImagesInHtml,
1245
2047
  renderToHtml,
1246
2048
  renderPages,
1247
2049
  renderFilePages,
1248
2050
  render,
1249
2051
  renderFile
1250
2052
  };
1251
- //# sourceMappingURL=chunk-ZB6NLLOD.js.map
2053
+ //# sourceMappingURL=chunk-BLKOSAAA.js.map