md2gost 1.3.1 → 1.4.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/dist/index.mjs CHANGED
@@ -19,6 +19,7 @@ var Doc = class {
19
19
  ctime;
20
20
  mtime;
21
21
  hyphenation = false;
22
+ totalPagesOffset = 0;
22
23
  headings = {
23
24
  h1: { size: 14, spacing: { before: 18, after: 4 }, indent_full: false, uppercase: false },
24
25
  h3: { size: 14, spacing: { before: 8, after: 4 }, indent_full: false, uppercase: false },
@@ -234,7 +235,7 @@ async function parseMD(file, logwarn = console.warn) {
234
235
  const node = { type: "code", lang: header[0], title, code: "" };
235
236
  text = "";
236
237
  while (lineI < lines.length) {
237
- let ln = lines[lineI++];
238
+ const ln = lines[lineI++];
238
239
  if (ln.startsWith("```")) break;
239
240
  text += "\n" + ln;
240
241
  }
@@ -242,9 +243,17 @@ async function parseMD(file, logwarn = console.warn) {
242
243
  return node;
243
244
  }
244
245
  function parseSection(text) {
246
+ text = text.toLowerCase();
245
247
  const match = /from\s+(\d+)/.exec(text);
246
248
  const num = match ? parseInt(match[1] || "") : NaN;
247
- return { type: "sectionBreak", pageStart: isFinite(num) ? num : null };
249
+ const unpaged = text.includes("unpaged");
250
+ const portrait = text.includes("portrait");
251
+ const landscape = text.includes("landscape");
252
+ return {
253
+ type: "sectionBreak",
254
+ pageStart: unpaged ? -1 : isFinite(num) ? num : null,
255
+ orientation: portrait ? "portrait" : landscape ? "landscape" : null
256
+ };
248
257
  }
249
258
  class RuleError extends Error {
250
259
  }
@@ -343,7 +352,7 @@ async function parseMD(file, logwarn = console.warn) {
343
352
  (prefix) => textl.startsWith(prefix) && !textl.slice(prefix.length)[0]?.trim()
344
353
  );
345
354
  const reRule = rulePrefix ? void 0 : reRules.map(({ re, f, n }) => ({ f, n, m: re.exec(textl) })).find(({ m }) => !!m);
346
- let applyRule = rulePrefix ? () => rules[rulePrefix](text.slice(rulePrefix.length).trim()) : reRule ? () => reRule.f(reRule.m) : void 0;
355
+ const applyRule = rulePrefix ? () => rules[rulePrefix](text.slice(rulePrefix.length).trim()) : reRule ? () => reRule.f(reRule.m) : void 0;
347
356
  if (applyRule) {
348
357
  try {
349
358
  applyRule();
@@ -462,7 +471,7 @@ function parseLine(line) {
462
471
  if (/^\s*---+\s*$/.test(line)) return { prefix: "---", text: "", level, parts: [] };
463
472
  const splited = line.trim().split(/\s/);
464
473
  let prefix = splited[0].toLowerCase();
465
- let text = splited.slice(1).join(" ");
474
+ const text = splited.slice(1).join(" ");
466
475
  let parts = [];
467
476
  if (prefix.startsWith("!")) {
468
477
  const m_img = /!\[(.*)\]\((.*)\)({(.*)})?/.exec(line);
@@ -501,7 +510,7 @@ function findDocs(nodes, logwarn = console.warn) {
501
510
  let dict = {};
502
511
  try {
503
512
  dict = JSON.parse(`{${m_doc[2]}}`);
504
- } catch (x) {
513
+ } catch {
505
514
  logwarn(`Cant parse doc dict: {${m_doc[2].replaceAll("\n", " ")}}`);
506
515
  }
507
516
  nodes.splice(i, 1, {
@@ -606,8 +615,8 @@ function runifyDoc(doc) {
606
615
  }
607
616
  var rainbowI = 0;
608
617
  function runifyText(text, rainbow = false) {
609
- return replaceAmpCodes(text).replaceAll("\n", "&Tab;\n").replaceAll(/\s*<br>\s*/g, "\n").replaceAll("\u2014", "-").replaceAll(" - ", " \u2013 ").replaceAll(/(^|[^\p{L}\d_])"([\p{L}\d_])/gu, "$1\xAB$2").replaceAll(/([\p{L}\d_])"([^\p{L}\d_]|$)/gu, "$1\xBB$2").replaceAll(/(^|\s)(\*+)($|\s)/g, (sub) => sub.replaceAll("*", "&Star;")).split(/(\[[^\]]*\]\([^\)]*\))/g).map((p) => {
610
- const m = /\[([^\]]*)\]\(([^\)]*)\)/.exec(p);
618
+ return replaceAmpCodes(text).replaceAll("\n", "&Tab;\n").replaceAll(/\s*<br>\s*/g, "\n").replaceAll("\u2014", "-").replaceAll(" - ", " \u2013 ").replaceAll(/(^|[^\p{L}\d_])"([\p{L}\d_])/gu, "$1\xAB$2").replaceAll(/([\p{L}\d_])"([^\p{L}\d_]|$)/gu, "$1\xBB$2").replaceAll(/(^|\s)(\*+)($|\s)/g, (sub) => sub.replaceAll("*", "&Star;")).split(/(\[[^\]]*\]\([^)]*\))/g).map((p) => {
619
+ const m = /\[([^\]]*)\]\(([^)]*)\)/.exec(p);
611
620
  if (!m) return { text: p };
612
621
  return { text: m[1], link: m[2] };
613
622
  }).map((rune) => rune.text.split("\n").map((p, i) => ({
@@ -642,8 +651,8 @@ function runifyText(text, rainbow = false) {
642
651
  text: p,
643
652
  linebreak: i == 0 && rune.linebreak,
644
653
  mono: i % 2 == 1 && i != arr.length - 1
645
- }))).flat().map((rune) => rune.link ? [rune] : rune.text.split(/(\[!?[a-zA-Zа-яА-ЯёЁ_\d#]+\s*[+\-]?\s*\d*\])/g).map((p, i) => {
646
- const m = /\[(!?([a-zA-Zа-яА-ЯёЁ_\d]+|#)(\s*[-\+]\s*\d+)?)\]/.exec(p);
654
+ }))).flat().map((rune) => rune.link ? [rune] : rune.text.split(/(\[!?[a-zA-Zа-яА-ЯёЁ_\d#]+\s*[+-]?\s*\d*\])/g).map((p, i) => {
655
+ const m = /\[(!?([a-zA-Zа-яА-ЯёЁ_\d]+|#)(\s*[-+]\s*\d+)?)\]/.exec(p);
647
656
  const v = m && m[1];
648
657
  const isVal = v?.at(0) == "!";
649
658
  return {
@@ -741,7 +750,7 @@ function replaceAmpCodes(text) {
741
750
 
742
751
  // src/serializer.ts
743
752
  import * as fs3 from "fs";
744
- import { AlignmentType, Document, Footer, convertMillimetersToTwip, Packer, PageBreak, PageNumber, Paragraph, TableOfContents, TextRun, LevelFormat, Table, TableRow, TableCell, ImageRun, ExternalHyperlink, InternalHyperlink, Bookmark } from "docx";
753
+ import { AlignmentType, Document, Footer, convertMillimetersToTwip, Packer, PageBreak, PageNumber, Paragraph, TableOfContents, TextRun, LevelFormat, Table, TableRow, TableCell, ImageRun, ExternalHyperlink, InternalHyperlink, Bookmark, XmlComponent, LineRuleType, PageOrientation } from "docx";
745
754
  import { imageSize } from "image-size";
746
755
  import path2 from "path";
747
756
  import Prism from "prismjs";
@@ -771,23 +780,44 @@ import "prismjs/components/prism-yaml";
771
780
  import "prismjs/components/prism-markdown";
772
781
  import "prismjs/components/prism-docker";
773
782
  import "prismjs/components/prism-nginx";
783
+
784
+ // src/errors.ts
785
+ var UserInputError = class extends Error {
786
+ constructor(message) {
787
+ super(message);
788
+ }
789
+ };
790
+
791
+ // src/serializer.ts
774
792
  var STYLE_list = "afc";
775
793
  var STYLE_table_title = "TableCaption";
776
794
  var STYLE_code_title = "ListingCaption";
777
795
  var STYLE_code = "ListingCode";
778
- async function serializeDocx(doc, fout, workdir, assets) {
796
+ async function serializeDocx(doc, fout, workdir, assets, allowExternalFiles) {
779
797
  const getPath = (fname) => {
798
+ if (process.platform != "win32") fname = fname.replaceAll("\\", "/");
780
799
  if (process.platform == "win32" && fname[0] == "/") fname = "." + fname;
781
- return path2.isAbsolute(fname) ? fname : path2.join(workdir, fname);
800
+ const targetPath = path2.isAbsolute(fname) ? path2.resolve(fname) : path2.resolve(workdir, fname);
801
+ if (!allowExternalFiles) {
802
+ const relative = path2.relative(workdir, targetPath);
803
+ const isOutside = relative.startsWith("..") || path2.isAbsolute(relative);
804
+ if (isOutside) throw new UserInputError(`Access denied: "${fname}" resolves outside the working directory.`);
805
+ }
806
+ return targetPath;
782
807
  };
783
808
  const sections = [];
784
809
  const numbering = [];
785
810
  const docSections = (function splitSections() {
786
- const sections2 = [{ pageStart: 1, nodes: [] }];
811
+ const sections2 = [{ displayPageNum: true, orientation: "portrait", pageStart: 1, nodes: [] }];
787
812
  for (let j = 0; j < doc.nodes.length; j++) {
788
813
  const node = doc.nodes[j];
789
814
  if (node.type == "sectionBreak")
790
- sections2.push({ pageStart: node.pageStart, nodes: [] });
815
+ sections2.push({
816
+ displayPageNum: node.pageStart == null ? sections2.at(-1)?.displayPageNum || true : node.pageStart >= 0,
817
+ orientation: node.orientation || sections2.at(-1)?.orientation || "portrait",
818
+ pageStart: node.pageStart,
819
+ nodes: []
820
+ });
791
821
  else
792
822
  sections2.at(-1)?.nodes.push(node);
793
823
  }
@@ -905,25 +935,29 @@ async function serializeDocx(doc, fout, workdir, assets) {
905
935
  case "image":
906
936
  const type = node.src.split(".").at(-1) || "";
907
937
  if (!["jpg", "png", "gif", "bmp", "svg"].includes(type))
908
- throw new Error(`Unsupported image format: "${type}", file: ${node.src}`);
938
+ throw new UserInputError(`Unsupported image format: "${type}", file: ${node.src}`);
909
939
  const img_path = getPath(node.src);
910
940
  if (!fs3.existsSync(img_path))
911
- throw new Error(`File not exist: ${node.src}`);
941
+ throw new UserInputError(`File not exist: ${node.src}`);
912
942
  const data = fs3.readFileSync(img_path);
913
943
  const dimensions = imageSize(data);
914
- const [MaxW, MaxH] = [600, 800];
944
+ const [MaxW, MaxH] = section.orientation == "landscape" ? [950, 600] : [600, 900];
915
945
  let [width, height] = [dimensions.width, dimensions.height];
916
946
  if (node.width && node.height) [width, height] = [node.width, node.height];
917
947
  if (node.width) [width, height] = [node.width, height / width * node.width];
918
948
  if (node.height) [width, height] = [width / height * node.height, node.height];
919
949
  if (height > MaxH) [width, height] = [width / height * MaxH, MaxH];
920
950
  if (width > MaxW) [width, height] = [MaxW, height / width * MaxW];
951
+ width = MaxW;
952
+ height = MaxH;
921
953
  return [
922
954
  new Paragraph({
923
955
  alignment: "center",
924
956
  indent: { firstLine: 0 },
925
957
  spacing: {
926
958
  line: 240,
959
+ lineRule: LineRuleType.AT_LEAST,
960
+ // For LibreOffice to not shrink image
927
961
  ...!node.text && doc.img.spacing.after !== void 0 ? { after: doc.img.spacing.after * 20 } : {},
928
962
  ...doc.img.spacing.before !== void 0 ? { before: doc.img.spacing.before * 20 } : {}
929
963
  },
@@ -969,10 +1003,10 @@ async function serializeDocx(doc, fout, workdir, assets) {
969
1003
  case "externalDoc":
970
1004
  const doc_path = getPath(node.path);
971
1005
  if (!fs3.existsSync(doc_path))
972
- throw new Error(`File not exist: ${node.path}`);
1006
+ throw new UserInputError(`File not exist: ${node.path}`);
973
1007
  const ext = path2.extname(doc_path);
974
1008
  if (ext != ".docx" && ext != ".pdf")
975
- throw new Error(`File not .docx or .pdf: ${node.path}`);
1009
+ throw new UserInputError(`File not .docx or .pdf: ${node.path}`);
976
1010
  return new Paragraph({
977
1011
  text: `!!(${doc_path})${JSON.stringify(node.dict)}`,
978
1012
  indent: { firstLine: 0 },
@@ -995,6 +1029,18 @@ async function serializeDocx(doc, fout, workdir, assets) {
995
1029
  children,
996
1030
  properties: {
997
1031
  page: {
1032
+ // size: section.orientation == "landscape" ? {
1033
+ // orientation: PageOrientation.LANDSCAPE,
1034
+ // width: convertMillimetersToTwip(297),
1035
+ // height: convertMillimetersToTwip(210),
1036
+ // } : {
1037
+ // orientation: PageOrientation.PORTRAIT,
1038
+ // width: convertMillimetersToTwip(210),
1039
+ // height: convertMillimetersToTwip(297),
1040
+ // },
1041
+ size: {
1042
+ orientation: section.orientation == "landscape" ? PageOrientation.LANDSCAPE : PageOrientation.PORTRAIT
1043
+ },
998
1044
  pageNumbers: !section.pageStart ? {} : {
999
1045
  start: section.pageStart
1000
1046
  },
@@ -1012,7 +1058,7 @@ async function serializeDocx(doc, fout, workdir, assets) {
1012
1058
  new Paragraph({
1013
1059
  alignment: AlignmentType.CENTER,
1014
1060
  children: [new TextRun({
1015
- children: section.pageStart ? [PageNumber.CURRENT] : []
1061
+ children: section.displayPageNum ? [PageNumber.CURRENT] : []
1016
1062
  })],
1017
1063
  indent: { firstLine: 0 },
1018
1064
  spacing: { line: 240 }
@@ -1068,7 +1114,26 @@ async function serializeDocx(doc, fout, workdir, assets) {
1068
1114
  let children = null;
1069
1115
  if (rune.type == "val" && rune.text == "pages") {
1070
1116
  rune.type = "text";
1071
- children = [PageNumber.TOTAL_PAGES];
1117
+ children = doc.totalPagesOffset == 0 ? [PageNumber.TOTAL_PAGES] : [
1118
+ // Complex Formula Field: { = { NUMPAGES } + 1 }
1119
+ // Outer field start: "="
1120
+ new RawXml("w:fldChar", { "w:fldCharType": "begin" }),
1121
+ new RawXml("w:instrText", { "xml:space": "preserve" }, " = "),
1122
+ // Inner field start: "NUMPAGES"
1123
+ new RawXml("w:fldChar", { "w:fldCharType": "begin" }),
1124
+ new RawXml("w:instrText", { "xml:space": "preserve" }, " NUMPAGES "),
1125
+ new RawXml("w:fldChar", { "w:fldCharType": "separate" }),
1126
+ new RawXml("w:fldChar", { "w:fldCharType": "end" }),
1127
+ // Operator
1128
+ new RawXml(
1129
+ "w:instrText",
1130
+ { "xml:space": "preserve" },
1131
+ doc.totalPagesOffset > 0 ? ` + ${doc.totalPagesOffset}` : ` - ${-doc.totalPagesOffset}`
1132
+ ),
1133
+ // Outer field close
1134
+ new RawXml("w:fldChar", { "w:fldCharType": "separate" }),
1135
+ new RawXml("w:fldChar", { "w:fldCharType": "end" })
1136
+ ];
1072
1137
  }
1073
1138
  return new TextRun({
1074
1139
  ...children ? { children } : { text: rune.text },
@@ -1145,7 +1210,6 @@ function addListItemLevel(levels, level, startIndex, itemCount, ordered, alterna
1145
1210
  }
1146
1211
  const longList = startIndex + itemCount > 10;
1147
1212
  const left = alternativeStyle ? 0 : longList ? 20.5 : 17.5;
1148
- const hanging = longList && !alternativeStyle ? convertMillimetersToTwip(8) : null;
1149
1213
  const text = ordered ? alternativeStyle ? `%${level + 1}.` : mark == "." ? `%${level + 1}.` : `%${level + 1})` : (
1150
1214
  // mark == "*" ? "\u2022" : "\u2012";
1151
1215
  mark == "*" ? "\xB7" : "\u2012"
@@ -1164,13 +1228,22 @@ function addListItemLevel(levels, level, startIndex, itemCount, ordered, alterna
1164
1228
  paragraph: {
1165
1229
  indent: {
1166
1230
  left: convertMillimetersToTwip(left + 5 * indent),
1167
- ...alternativeStyle ? { firstLine: convertMillimetersToTwip(12.5) } : {},
1168
- ...hanging ? { hanging } : {}
1231
+ hanging: convertMillimetersToTwip(longList && !alternativeStyle ? 8 : 5),
1232
+ ...alternativeStyle ? { firstLine: convertMillimetersToTwip(12.5) } : {}
1169
1233
  }
1170
1234
  }
1171
1235
  }
1172
1236
  });
1173
1237
  }
1238
+ var RawXml = class extends XmlComponent {
1239
+ constructor(tag, attributes = {}, text = null) {
1240
+ super(tag);
1241
+ if (Object.keys(attributes).length > 0)
1242
+ this.root.push({ _attr: attributes });
1243
+ if (text)
1244
+ this.root.push(text);
1245
+ }
1246
+ };
1174
1247
  function renderCodeHighlighting(code, lang) {
1175
1248
  const grammar = Prism.languages[lang.trim().toLowerCase()];
1176
1249
  if (!grammar) return null;
@@ -1509,7 +1582,7 @@ function enrichDoc(doc, logwarn = console.warn) {
1509
1582
  }
1510
1583
 
1511
1584
  // src/main.ts
1512
- import { spawn } from "child_process";
1585
+ import { exec, spawn } from "child_process";
1513
1586
  import fs4 from "fs/promises";
1514
1587
  import { existsSync as existsSync2 } from "fs";
1515
1588
  import { PDFDocument } from "pdf-lib";
@@ -1661,7 +1734,7 @@ function alchemist(doc, logwarn = console.warn) {
1661
1734
  }
1662
1735
  if (rune.type != "ref") return;
1663
1736
  const type = node.type;
1664
- const m = /^(.*?)([-\+]\d+)?$/.exec(rune.text.replaceAll(/\s+/g, ""));
1737
+ const m = /^(.*?)([-+]\d+)?$/.exec(rune.text.replaceAll(/\s+/g, ""));
1665
1738
  const tag = m?.[1] || "";
1666
1739
  const math = m?.[2] || "";
1667
1740
  const mathSubstarct = math.at(0) == "-";
@@ -1791,14 +1864,22 @@ async function render({
1791
1864
  renderPDF = false,
1792
1865
  removeIntermediateDocx = false,
1793
1866
  disableMacros = false,
1867
+ useLibreOffice = false,
1868
+ allowExternalFiles = false,
1794
1869
  logwarn = console.warn,
1870
+ loginfo = console.info,
1795
1871
  logPS = (msg) => console.log(`PS: ${msg}`),
1796
- logPSError = (msg) => console.error(`PS: ${msg}`)
1872
+ logPSError = (msg) => console.error(`PS ERROR: ${msg}`),
1873
+ signal = null
1797
1874
  }) {
1798
- progress(10, "\u041F\u044B\u0442\u0430\u0435\u043C\u0441\u044F \u043F\u043E\u043D\u044F\u0442\u044C, \u0447\u0442\u043E \u0432\u044B \u0442\u0443\u0442 \u043D\u0430\u043F\u0438\u0441\u0430\u043B\u0438...");
1875
+ signal?.throwIfAborted();
1799
1876
  const fin = path3.resolve(file);
1877
+ progress(10, "\u041F\u044B\u0442\u0430\u0435\u043C\u0441\u044F \u043F\u043E\u043D\u044F\u0442\u044C, \u0447\u0442\u043E \u0432\u044B \u0442\u0443\u0442 \u043D\u0430\u043F\u0438\u0441\u0430\u043B\u0438...");
1878
+ loginfo(`[!1] Parsing file ${fin}`);
1800
1879
  const doc = await parseMD(fin, logwarn);
1880
+ signal?.throwIfAborted();
1801
1881
  progress(10, "\u041A\u043E\u043B\u0434\u0443\u0435\u043C \u043D\u0430\u0434 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441\u043E\u043C...");
1882
+ loginfo("[!2] Processing document");
1802
1883
  enrichDoc(doc, logwarn);
1803
1884
  const runicDoc = runifyDoc(doc);
1804
1885
  alchemist(runicDoc, logwarn);
@@ -1813,64 +1894,107 @@ async function render({
1813
1894
  throw new Error(`\u0417\u0430\u043A\u0440\u043E\u0439\u0442\u0435 docx \u0444\u0430\u0439\u043B. Output file is busy or locked: ${ftmp}`);
1814
1895
  if (await checkIfFileIsBlocked(fout))
1815
1896
  throw new Error(`\u0417\u0430\u043A\u0440\u043E\u0439\u0442\u0435 docx \u0444\u0430\u0439\u043B. Output file is busy or locked: ${fout}`);
1897
+ signal?.throwIfAborted();
1816
1898
  progress(10, phrase_renderDocx());
1817
- await serializeDocx(runicDoc, ftmp, fdir, assets);
1899
+ loginfo("[!3] Serializing to docx");
1900
+ await serializeDocx(runicDoc, ftmp, fdir, assets, allowExternalFiles);
1818
1901
  let willRunMacros = !disableMacros && (renderPDF || hasReasonForRunningMacros(runicDoc));
1819
1902
  let limitedRender = false;
1820
- if (process.platform != "win32" && willRunMacros) {
1903
+ if (process.platform != "win32" && willRunMacros && !useLibreOffice) {
1821
1904
  limitedRender = true;
1822
1905
  willRunMacros = false;
1823
1906
  }
1824
1907
  if (willRunMacros) {
1908
+ signal?.throwIfAborted();
1825
1909
  progress(20, phrase_runMacros());
1910
+ loginfo("[!4] Starting macros");
1826
1911
  const tmpfolder = path3.join(pout ? pout.dir : fdir, ".md2gost_out");
1827
1912
  await fs4.rm(tmpfolder, { recursive: true, force: true });
1828
1913
  try {
1829
- const ok = await runDocxMacro(progress, logPS, logPSError, assets, fdir, ftmp, fout, renderPDF);
1914
+ const macroRunner = useLibreOffice ? runUnoScript : runDocxMacro;
1915
+ const ok = await macroRunner(progress, logPS, logPSError, assets, fdir, ftmp, fout, renderPDF, signal);
1830
1916
  if (!ok) {
1831
1917
  await fs4.rename(ftmp, fout);
1832
1918
  return { fout, err: "inPS" };
1833
1919
  }
1834
1920
  } catch (x) {
1921
+ if (signal?.aborted) {
1922
+ await fs4.unlink(ftmp).catch(() => {
1923
+ });
1924
+ throw x;
1925
+ }
1835
1926
  await fs4.rename(ftmp, fout);
1836
1927
  return { fout, err: "noPS", errS: x };
1837
1928
  }
1838
- await fs4.unlink(ftmp);
1929
+ if (existsSync2(ftmp))
1930
+ await fs4.unlink(ftmp);
1931
+ signal?.throwIfAborted();
1839
1932
  const errorTxt = path3.join(tmpfolder, "error.txt");
1840
1933
  if (existsSync2(errorTxt)) {
1841
1934
  const err = await fs4.readFile(errorTxt);
1842
1935
  await fs4.rm(tmpfolder, { recursive: true, force: true });
1843
1936
  return { fout, err: "vba", errS: `${err}` };
1844
1937
  }
1938
+ loginfo("[!5] Updating metadata");
1845
1939
  await updateMetadata(fout, doc);
1846
1940
  if (renderPDF) {
1941
+ signal?.throwIfAborted();
1847
1942
  progress(10, phrase_combine());
1943
+ loginfo("[!6] Merging pdfs");
1848
1944
  if (!existsSync2(tmpfolder))
1849
1945
  return { fout, err: "pdf" };
1850
1946
  try {
1851
1947
  const files = (await fs4.readdir(tmpfolder)).sort().map((f) => path3.join(tmpfolder, f));
1852
1948
  const pdf = pout ? path3.join(pout.dir, pout.base) : path3.join(fdir, fname + ".pdf");
1853
- await mergePDFs(files, pdf, doc);
1949
+ await mergePDFs(files, pdf, doc, signal || void 0);
1854
1950
  await fs4.rm(tmpfolder, { recursive: true, force: true });
1855
1951
  if (removeIntermediateDocx && existsSync2(fout)) await fs4.unlink(fout);
1856
1952
  return { fout: pdf };
1857
1953
  } catch (err) {
1954
+ if (signal?.aborted) throw err;
1858
1955
  return { fout, err: "pdf", errS: err };
1859
1956
  }
1860
1957
  }
1861
1958
  } else {
1862
1959
  await fs4.rename(ftmp, fout);
1863
1960
  }
1961
+ loginfo(`[!7] Rendered to ${fout}`);
1864
1962
  return { fout, err: limitedRender ? "noWin" : void 0 };
1865
1963
  }
1866
1964
  function hasReasonForRunningMacros(doc) {
1867
1965
  if (DISABLE_MACRO) return false;
1868
1966
  return doc.nodes.some((n) => n.type == "code" || n.type == "table");
1869
1967
  }
1870
- function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF) {
1968
+ function killProcessTree(child, wordPid) {
1969
+ if (!child.pid) return;
1970
+ if (process.platform === "win32") {
1971
+ if (wordPid) {
1972
+ exec(`taskkill /pid ${wordPid}`, () => {
1973
+ setTimeout(() => {
1974
+ exec(`taskkill /pid ${wordPid} /F`, () => {
1975
+ });
1976
+ setTimeout(() => {
1977
+ killProcessTree(child);
1978
+ }, 2e3);
1979
+ }, 2e3);
1980
+ });
1981
+ } else {
1982
+ exec(`taskkill /pid ${child.pid} /T /F`, () => {
1983
+ });
1984
+ }
1985
+ } else {
1986
+ try {
1987
+ process.kill(-child.pid, "SIGKILL");
1988
+ } catch {
1989
+ }
1990
+ }
1991
+ }
1992
+ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF, signal) {
1871
1993
  const script = path3.join(assets, "run.ps1");
1872
1994
  const templateMacro = path3.join(assets, "template.dotm");
1873
1995
  return new Promise((res, rej) => {
1996
+ if (signal?.aborted)
1997
+ return rej(signal.reason ?? new Error("Aborted before execution started"));
1874
1998
  const child = spawn("powershell", [
1875
1999
  "-NoProfile",
1876
2000
  "-ExecutionPolicy",
@@ -1884,10 +2008,28 @@ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF
1884
2008
  "-MacroTemplate",
1885
2009
  templateMacro,
1886
2010
  ...renderPDF ? ["-RenderPDF"] : []
1887
- ], { cwd });
2011
+ ], {
2012
+ cwd,
2013
+ detached: process.platform !== "win32"
2014
+ // Enables PGID on POSIX
2015
+ // detached: true,
2016
+ // shell: true,
2017
+ });
2018
+ let wordPid = null;
2019
+ let onAbort;
2020
+ if (signal) {
2021
+ onAbort = () => {
2022
+ logError("Operation aborted. Terminating process tree...");
2023
+ killProcessTree(child, wordPid);
2024
+ rej(signal.reason ?? new Error("Job timed out or aborted"));
2025
+ };
2026
+ signal.addEventListener("abort", onAbort, { once: true });
2027
+ }
1888
2028
  child.stdout.on("data", (data) => {
1889
- data = `${data}`;
1890
- const m = /\[\*(\d)\]/.exec(data);
2029
+ const raw = `${data}`;
2030
+ const pidMatch = /\[WordPID:(\d+)\]/.exec(raw);
2031
+ if (pidMatch) wordPid = parseInt(pidMatch[1], 10);
2032
+ const m = /\[\*(\d)\]/.exec(raw);
1891
2033
  if (m) progress(12, {
1892
2034
  // "1": "Fixing breaks (listings)",
1893
2035
  "1": render_fixingBreaks(),
@@ -1896,31 +2038,115 @@ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF
1896
2038
  // "3": "Rendering to PDF",
1897
2039
  "3": render_renderPDF()
1898
2040
  }[m[1]] || "Make some work");
1899
- log(data);
2041
+ log(raw);
2042
+ });
2043
+ let ok = true;
2044
+ child.stderr.on("data", (data) => {
2045
+ ok = false;
2046
+ logError(`${data}`);
2047
+ });
2048
+ child.on("close", (code) => {
2049
+ if (signal && onAbort) {
2050
+ signal.removeEventListener("abort", onAbort);
2051
+ }
2052
+ res(ok && code === 0);
2053
+ });
2054
+ child.on("error", (err) => {
2055
+ if (signal && onAbort) {
2056
+ signal.removeEventListener("abort", onAbort);
2057
+ }
2058
+ rej(err);
2059
+ });
2060
+ });
2061
+ }
2062
+ function getPythonExecutable() {
2063
+ if (process.platform === "win32") {
2064
+ const loPython = "C:\\Program Files\\LibreOffice\\program\\python.exe";
2065
+ if (existsSync2(loPython)) return loPython;
2066
+ }
2067
+ return process.env.UNO_PYTHON_PATH || "python3";
2068
+ }
2069
+ function runUnoScript(progress, log, logError, assets, cwd, fin, fout, renderPDF, signal) {
2070
+ const script = process.env.MACRO_ENGINE_PATH || path3.join(assets, "macro_engine.py");
2071
+ const pythonExec = getPythonExecutable();
2072
+ return new Promise((res, rej) => {
2073
+ if (signal?.aborted) {
2074
+ return rej(signal.reason ?? new Error("Aborted before execution started"));
2075
+ }
2076
+ const args = [
2077
+ script,
2078
+ "--input",
2079
+ fin,
2080
+ "--output",
2081
+ fout
2082
+ ];
2083
+ if (renderPDF) args.push("--render-pdf");
2084
+ const child = spawn(pythonExec, args, {
2085
+ cwd,
2086
+ detached: process.platform !== "win32"
2087
+ // Enables PGID on POSIX
2088
+ });
2089
+ let onAbort;
2090
+ if (signal) {
2091
+ onAbort = () => {
2092
+ logError("Operation aborted. Terminating process tree...");
2093
+ killProcessTree(child);
2094
+ rej(signal.reason ?? new Error("Job timed out or aborted"));
2095
+ };
2096
+ signal.addEventListener("abort", onAbort, { once: true });
2097
+ }
2098
+ child.stdout.on("data", (data) => {
2099
+ const lines = `${data}`.split("\n");
2100
+ for (const line of lines) {
2101
+ log(line.trimEnd());
2102
+ if (!line.trimEnd()) continue;
2103
+ const m = /\[\*(\d)\]/.exec(line);
2104
+ if (m) {
2105
+ const msg = {
2106
+ "1": render_fixingBreaks(),
2107
+ "2": render_fixingBreaks(),
2108
+ "3": render_renderPDF()
2109
+ }[m[1]] || "Processing document...";
2110
+ progress(12, msg);
2111
+ }
2112
+ }
1900
2113
  });
1901
2114
  let ok = true;
1902
2115
  child.stderr.on("data", (data) => {
1903
2116
  ok = false;
1904
2117
  logError(`${data}`);
1905
2118
  });
1906
- child.on("close", () => res(ok));
1907
- child.on("error", rej);
2119
+ child.on("close", (code) => {
2120
+ if (signal && onAbort) {
2121
+ signal.removeEventListener("abort", onAbort);
2122
+ }
2123
+ res(ok && code === 0);
2124
+ });
2125
+ child.on("error", (err) => {
2126
+ if (signal && onAbort) {
2127
+ signal.removeEventListener("abort", onAbort);
2128
+ }
2129
+ rej(err);
2130
+ });
1908
2131
  });
1909
2132
  }
1910
- async function mergePDFs(files, fout, doc) {
2133
+ async function mergePDFs(files, fout, doc, signal) {
1911
2134
  const mergedPdf = await PDFDocument.create();
1912
2135
  if (doc?.title) mergedPdf.setTitle(doc.title, { showInWindowTitleBar: true });
1913
2136
  if (doc?.author) mergedPdf.setAuthor(doc.author);
1914
2137
  if (doc?.ctime) mergedPdf.setCreationDate(doc.ctime);
1915
2138
  if (doc?.mtime) mergedPdf.setModificationDate(doc.mtime);
1916
2139
  for (const file of files) {
1917
- const bytes = await fs4.readFile(file);
2140
+ if (!file.endsWith(".pdf")) continue;
2141
+ const bytes = await fs4.readFile(file, { signal });
1918
2142
  const pdf = await PDFDocument.load(bytes);
2143
+ signal?.throwIfAborted();
1919
2144
  const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
1920
2145
  pages.forEach((page) => mergedPdf.addPage(page));
2146
+ signal?.throwIfAborted();
1921
2147
  }
1922
2148
  const pdfBytes = await mergedPdf.save();
1923
- await fs4.writeFile(fout, pdfBytes);
2149
+ await fs4.writeFile(fout, pdfBytes, { signal });
1924
2150
  }
1925
2151
  async function updateMetadata(docfile, doc) {
1926
2152
  const content = await fs4.readFile(docfile);
@@ -2012,25 +2238,50 @@ async function renderMarkdown(config) {
2012
2238
  assert(config.format == "docx" || config.format == "pdf" || typeof config.format == "undefined", "Format must be either 'docx' or 'pdf', or undefined.");
2013
2239
  assert(typeof config.keepIntermediateDocx == "boolean" || typeof config.keepIntermediateDocx == "undefined", "keepIntermediateDocx must be a boolean or undefined.");
2014
2240
  assert(typeof config.disableMacros == "boolean" || typeof config.disableMacros == "undefined", "disableMacros must be a boolean or undefined.");
2241
+ assert(typeof config.useLibreOffice == "boolean" || typeof config.useLibreOffice == "undefined", "useLibreOffice must be a boolean or undefined.");
2242
+ assert(typeof config.allowExternalFiles == "boolean" || typeof config.allowExternalFiles == "undefined", "allowExternalFiles must be a boolean or undefined.");
2015
2243
  assert(typeof config.progress == "function" || typeof config.progress == "undefined", "Progress callback must be a function or undefined.");
2244
+ assert(config.abortSignal instanceof AbortSignal || typeof config.abortSignal == "undefined", "abortSignal must be an AbortSignal or undefined.");
2016
2245
  {
2017
2246
  const extraProps = getExtraProperties(
2018
2247
  config,
2019
- ["input", "output", "format", "keepIntermediateDocx", "disableMacros", "progress"]
2248
+ ["input", "output", "format", "keepIntermediateDocx", "disableMacros", "useLibreOffice", "progress", "logger", "abortSignal", "allowExternalFiles"]
2020
2249
  );
2021
2250
  assert(extraProps.length == 0, `Found unknown properties in config: ${extraProps.join(", ")}`);
2022
2251
  }
2252
+ assert(typeof config.logger == "object" && config.logger || typeof config.logger == "undefined", "Logger must be a object or undefined.");
2253
+ const logger = config.logger;
2254
+ if (logger) {
2255
+ const props = ["info", "warn", "error"];
2256
+ const extraProps = getExtraProperties(logger, props);
2257
+ assert(extraProps.length == 0, `Found unknown properties in config.logger: ${extraProps.join(", ")}`);
2258
+ props.forEach(
2259
+ (p) => assert(typeof logger[p] == "function", `config.logger.${p} must be a function.`)
2260
+ );
2261
+ }
2023
2262
  const renderPDF = config.format === "pdf" || !config.format && !!config.output?.endsWith(".pdf");
2024
2263
  const removeIntermediateDocx = !config.keepIntermediateDocx;
2025
2264
  const disableMacros = !!config.disableMacros;
2265
+ const useLibreOffice = !!config.useLibreOffice;
2266
+ const allowExternalFiles = !!config.allowExternalFiles;
2026
2267
  assert(!(disableMacros && renderPDF), "Macros must be enabled to render PDF output.");
2268
+ const pin = path4.parse(path4.resolve(config.input));
2269
+ const fname = pin.name.endsWith(".g") ? pin.name.slice(0, -2) : pin.name;
2270
+ let resolvedOutput = config.output;
2271
+ if (resolvedOutput) {
2272
+ const isDir = resolvedOutput.endsWith("/") || resolvedOutput.endsWith("\\") || !path4.extname(resolvedOutput);
2273
+ if (isDir) {
2274
+ const ext = renderPDF ? ".pdf" : ".docx";
2275
+ resolvedOutput = path4.join(resolvedOutput, fname + ext);
2276
+ }
2277
+ }
2027
2278
  const warnings = [];
2028
2279
  const logPS = [];
2029
2280
  let totalPercent = 0;
2030
- const handleProgress = config.progress ? (increment, message) => {
2281
+ const handleProgress = (increment, message) => {
2031
2282
  totalPercent = Math.min(totalPercent + increment, 99);
2032
- config.progress(totalPercent, message);
2033
- } : void 0;
2283
+ config.progress?.(totalPercent, message);
2284
+ };
2034
2285
  try {
2035
2286
  let throwMDE2 = function(code, msg) {
2036
2287
  throw new MDRenderError(code, msg, fout, warnings, logPS, errS ? { cause: errS } : void 0);
@@ -2040,26 +2291,41 @@ async function renderMarkdown(config) {
2040
2291
  progress: handleProgress,
2041
2292
  assets: getAssetsDir(),
2042
2293
  file: config.input,
2043
- output: config.output,
2294
+ output: resolvedOutput,
2044
2295
  renderPDF,
2045
2296
  removeIntermediateDocx,
2046
2297
  disableMacros,
2047
- logwarn: (msg) => warnings.push(msg),
2048
- logPS: (msg) => logPS.push(msg),
2049
- logPSError: (msg) => logPS.push(msg)
2298
+ useLibreOffice,
2299
+ allowExternalFiles,
2300
+ loginfo: logger?.info,
2301
+ logwarn: (msg) => {
2302
+ logger?.warn(msg);
2303
+ warnings.push(msg);
2304
+ },
2305
+ logPS: (msg) => {
2306
+ logger?.info(msg);
2307
+ logPS.push(msg);
2308
+ },
2309
+ logPSError: (msg) => {
2310
+ logger?.error(msg);
2311
+ logPS.push(msg);
2312
+ },
2313
+ signal: config.abortSignal
2050
2314
  });
2051
2315
  config.progress?.(100, "Done!");
2052
- ;
2053
- if (err == "noPS") throwMDE2("noPS", `Failed to initialize PowerShell. Ensure PowerShell is installed and explicitly added to your system's PATH environment variable.`);
2054
- if (err == "inPS") throwMDE2("inPS", `An unexpected error occurred executing commands inside PowerShell. Verify that Microsoft Word is properly installed and licensed.`);
2316
+ if (useLibreOffice) {
2317
+ if (err == "noPS") throwMDE2("noPS", `Failed to run the LibreOffice binary (soffice). Ensure LibreOffice is installed and added to your system's PATH environment variable.`);
2318
+ if (err == "inPS") throwMDE2("inPS", `An unexpected error occurred executing the LibreOffice conversion command. Verify that LibreOffice is properly installed, up to date, and has permission to access the target files.`);
2319
+ } else {
2320
+ if (err == "noPS") throwMDE2("noPS", `Failed to initialize PowerShell. Ensure PowerShell is installed and explicitly added to your system's PATH environment variable.`);
2321
+ if (err == "inPS") throwMDE2("inPS", `An unexpected error occurred executing commands inside PowerShell. Verify that Microsoft Word is properly installed and licensed.`);
2322
+ }
2055
2323
  if (err == "vba") throwMDE2("vba", `Microsoft Word VBA Macro execution failed: ${errS}`);
2056
2324
  if (err == "pdf") throwMDE2("pdf", `Failed to export document to PDF.`);
2057
2325
  if (err == "noWin") throwMDE2("noWin", `Platform restriction: Extended formatting macros and PDF rendering are only supported natively on Windows systems.`);
2058
2326
  let intermediateDocxPath;
2059
- if (renderPDF) {
2060
- const pin = path4.parse(path4.resolve(config.input));
2061
- const fname = pin.name.endsWith(".g") ? pin.name.slice(0, -2) : pin.name;
2062
- const pout = config.output ? path4.parse(path4.resolve(config.output)) : void 0;
2327
+ if (renderPDF && !removeIntermediateDocx) {
2328
+ const pout = resolvedOutput ? path4.parse(path4.resolve(resolvedOutput)) : void 0;
2063
2329
  intermediateDocxPath = pout ? path4.join(pout.dir, pout.name + ".docx") : path4.join(pin.dir, fname + ".docx");
2064
2330
  }
2065
2331
  return {
@@ -2069,6 +2335,7 @@ async function renderMarkdown(config) {
2069
2335
  };
2070
2336
  } catch (x) {
2071
2337
  if (x instanceof MDRenderError) throw x;
2338
+ if (x instanceof UserInputError) throw new MDRenderError("userInput", x.message, null, warnings, logPS, { cause: x });
2072
2339
  const message = x instanceof Error ? x.message : String(x);
2073
2340
  throw new MDRenderError("unknown", `An unhandled internal error occurred during rendering: ${message}`, null, warnings, logPS, { cause: x });
2074
2341
  }
@@ -2081,6 +2348,7 @@ var MDRenderError = class _MDRenderError extends Error {
2081
2348
  /**
2082
2349
  * Code indicating the underlying cause of the failure:
2083
2350
  * - `unknown`: An unhandled or unexpected internal runtime error.
2351
+ * - `userInput`: Invalid user input or Markdown document structure.
2084
2352
  * - `noPS`: PowerShell could not be started or found in the system PATH.
2085
2353
  * - `inPS`: A failure occurred inside PowerShell; MS Word may be missing or corrupt.
2086
2354
  * - `vba`: An error occurred during the execution of internal Word VBA macros.