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.js CHANGED
@@ -54,6 +54,7 @@ var Doc = class {
54
54
  ctime;
55
55
  mtime;
56
56
  hyphenation = false;
57
+ totalPagesOffset = 0;
57
58
  headings = {
58
59
  h1: { size: 14, spacing: { before: 18, after: 4 }, indent_full: false, uppercase: false },
59
60
  h3: { size: 14, spacing: { before: 8, after: 4 }, indent_full: false, uppercase: false },
@@ -269,7 +270,7 @@ async function parseMD(file, logwarn = console.warn) {
269
270
  const node = { type: "code", lang: header[0], title, code: "" };
270
271
  text = "";
271
272
  while (lineI < lines.length) {
272
- let ln = lines[lineI++];
273
+ const ln = lines[lineI++];
273
274
  if (ln.startsWith("```")) break;
274
275
  text += "\n" + ln;
275
276
  }
@@ -277,9 +278,17 @@ async function parseMD(file, logwarn = console.warn) {
277
278
  return node;
278
279
  }
279
280
  function parseSection(text) {
281
+ text = text.toLowerCase();
280
282
  const match = /from\s+(\d+)/.exec(text);
281
283
  const num = match ? parseInt(match[1] || "") : NaN;
282
- return { type: "sectionBreak", pageStart: isFinite(num) ? num : null };
284
+ const unpaged = text.includes("unpaged");
285
+ const portrait = text.includes("portrait");
286
+ const landscape = text.includes("landscape");
287
+ return {
288
+ type: "sectionBreak",
289
+ pageStart: unpaged ? -1 : isFinite(num) ? num : null,
290
+ orientation: portrait ? "portrait" : landscape ? "landscape" : null
291
+ };
283
292
  }
284
293
  class RuleError extends Error {
285
294
  }
@@ -378,7 +387,7 @@ async function parseMD(file, logwarn = console.warn) {
378
387
  (prefix) => textl.startsWith(prefix) && !textl.slice(prefix.length)[0]?.trim()
379
388
  );
380
389
  const reRule = rulePrefix ? void 0 : reRules.map(({ re, f, n }) => ({ f, n, m: re.exec(textl) })).find(({ m }) => !!m);
381
- let applyRule = rulePrefix ? () => rules[rulePrefix](text.slice(rulePrefix.length).trim()) : reRule ? () => reRule.f(reRule.m) : void 0;
390
+ const applyRule = rulePrefix ? () => rules[rulePrefix](text.slice(rulePrefix.length).trim()) : reRule ? () => reRule.f(reRule.m) : void 0;
382
391
  if (applyRule) {
383
392
  try {
384
393
  applyRule();
@@ -497,7 +506,7 @@ function parseLine(line) {
497
506
  if (/^\s*---+\s*$/.test(line)) return { prefix: "---", text: "", level, parts: [] };
498
507
  const splited = line.trim().split(/\s/);
499
508
  let prefix = splited[0].toLowerCase();
500
- let text = splited.slice(1).join(" ");
509
+ const text = splited.slice(1).join(" ");
501
510
  let parts = [];
502
511
  if (prefix.startsWith("!")) {
503
512
  const m_img = /!\[(.*)\]\((.*)\)({(.*)})?/.exec(line);
@@ -536,7 +545,7 @@ function findDocs(nodes, logwarn = console.warn) {
536
545
  let dict = {};
537
546
  try {
538
547
  dict = JSON.parse(`{${m_doc[2]}}`);
539
- } catch (x) {
548
+ } catch {
540
549
  logwarn(`Cant parse doc dict: {${m_doc[2].replaceAll("\n", " ")}}`);
541
550
  }
542
551
  nodes.splice(i, 1, {
@@ -641,8 +650,8 @@ function runifyDoc(doc) {
641
650
  }
642
651
  var rainbowI = 0;
643
652
  function runifyText(text, rainbow = false) {
644
- 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) => {
645
- const m = /\[([^\]]*)\]\(([^\)]*)\)/.exec(p);
653
+ 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) => {
654
+ const m = /\[([^\]]*)\]\(([^)]*)\)/.exec(p);
646
655
  if (!m) return { text: p };
647
656
  return { text: m[1], link: m[2] };
648
657
  }).map((rune) => rune.text.split("\n").map((p, i) => ({
@@ -677,8 +686,8 @@ function runifyText(text, rainbow = false) {
677
686
  text: p,
678
687
  linebreak: i == 0 && rune.linebreak,
679
688
  mono: i % 2 == 1 && i != arr.length - 1
680
- }))).flat().map((rune) => rune.link ? [rune] : rune.text.split(/(\[!?[a-zA-Zа-яА-ЯёЁ_\d#]+\s*[+\-]?\s*\d*\])/g).map((p, i) => {
681
- const m = /\[(!?([a-zA-Zа-яА-ЯёЁ_\d]+|#)(\s*[-\+]\s*\d+)?)\]/.exec(p);
689
+ }))).flat().map((rune) => rune.link ? [rune] : rune.text.split(/(\[!?[a-zA-Zа-яА-ЯёЁ_\d#]+\s*[+-]?\s*\d*\])/g).map((p, i) => {
690
+ const m = /\[(!?([a-zA-Zа-яА-ЯёЁ_\d]+|#)(\s*[-+]\s*\d+)?)\]/.exec(p);
682
691
  const v = m && m[1];
683
692
  const isVal = v?.at(0) == "!";
684
693
  return {
@@ -806,23 +815,44 @@ var import_prism_yaml = require("prismjs/components/prism-yaml");
806
815
  var import_prism_markdown = require("prismjs/components/prism-markdown");
807
816
  var import_prism_docker = require("prismjs/components/prism-docker");
808
817
  var import_prism_nginx = require("prismjs/components/prism-nginx");
818
+
819
+ // src/errors.ts
820
+ var UserInputError = class extends Error {
821
+ constructor(message) {
822
+ super(message);
823
+ }
824
+ };
825
+
826
+ // src/serializer.ts
809
827
  var STYLE_list = "afc";
810
828
  var STYLE_table_title = "TableCaption";
811
829
  var STYLE_code_title = "ListingCaption";
812
830
  var STYLE_code = "ListingCode";
813
- async function serializeDocx(doc, fout, workdir, assets) {
831
+ async function serializeDocx(doc, fout, workdir, assets, allowExternalFiles) {
814
832
  const getPath = (fname) => {
833
+ if (process.platform != "win32") fname = fname.replaceAll("\\", "/");
815
834
  if (process.platform == "win32" && fname[0] == "/") fname = "." + fname;
816
- return import_path2.default.isAbsolute(fname) ? fname : import_path2.default.join(workdir, fname);
835
+ const targetPath = import_path2.default.isAbsolute(fname) ? import_path2.default.resolve(fname) : import_path2.default.resolve(workdir, fname);
836
+ if (!allowExternalFiles) {
837
+ const relative = import_path2.default.relative(workdir, targetPath);
838
+ const isOutside = relative.startsWith("..") || import_path2.default.isAbsolute(relative);
839
+ if (isOutside) throw new UserInputError(`Access denied: "${fname}" resolves outside the working directory.`);
840
+ }
841
+ return targetPath;
817
842
  };
818
843
  const sections = [];
819
844
  const numbering = [];
820
845
  const docSections = (function splitSections() {
821
- const sections2 = [{ pageStart: 1, nodes: [] }];
846
+ const sections2 = [{ displayPageNum: true, orientation: "portrait", pageStart: 1, nodes: [] }];
822
847
  for (let j = 0; j < doc.nodes.length; j++) {
823
848
  const node = doc.nodes[j];
824
849
  if (node.type == "sectionBreak")
825
- sections2.push({ pageStart: node.pageStart, nodes: [] });
850
+ sections2.push({
851
+ displayPageNum: node.pageStart == null ? sections2.at(-1)?.displayPageNum || true : node.pageStart >= 0,
852
+ orientation: node.orientation || sections2.at(-1)?.orientation || "portrait",
853
+ pageStart: node.pageStart,
854
+ nodes: []
855
+ });
826
856
  else
827
857
  sections2.at(-1)?.nodes.push(node);
828
858
  }
@@ -940,25 +970,29 @@ async function serializeDocx(doc, fout, workdir, assets) {
940
970
  case "image":
941
971
  const type = node.src.split(".").at(-1) || "";
942
972
  if (!["jpg", "png", "gif", "bmp", "svg"].includes(type))
943
- throw new Error(`Unsupported image format: "${type}", file: ${node.src}`);
973
+ throw new UserInputError(`Unsupported image format: "${type}", file: ${node.src}`);
944
974
  const img_path = getPath(node.src);
945
975
  if (!fs3.existsSync(img_path))
946
- throw new Error(`File not exist: ${node.src}`);
976
+ throw new UserInputError(`File not exist: ${node.src}`);
947
977
  const data = fs3.readFileSync(img_path);
948
978
  const dimensions = (0, import_image_size.imageSize)(data);
949
- const [MaxW, MaxH] = [600, 800];
979
+ const [MaxW, MaxH] = section.orientation == "landscape" ? [950, 600] : [600, 900];
950
980
  let [width, height] = [dimensions.width, dimensions.height];
951
981
  if (node.width && node.height) [width, height] = [node.width, node.height];
952
982
  if (node.width) [width, height] = [node.width, height / width * node.width];
953
983
  if (node.height) [width, height] = [width / height * node.height, node.height];
954
984
  if (height > MaxH) [width, height] = [width / height * MaxH, MaxH];
955
985
  if (width > MaxW) [width, height] = [MaxW, height / width * MaxW];
986
+ width = MaxW;
987
+ height = MaxH;
956
988
  return [
957
989
  new import_docx.Paragraph({
958
990
  alignment: "center",
959
991
  indent: { firstLine: 0 },
960
992
  spacing: {
961
993
  line: 240,
994
+ lineRule: import_docx.LineRuleType.AT_LEAST,
995
+ // For LibreOffice to not shrink image
962
996
  ...!node.text && doc.img.spacing.after !== void 0 ? { after: doc.img.spacing.after * 20 } : {},
963
997
  ...doc.img.spacing.before !== void 0 ? { before: doc.img.spacing.before * 20 } : {}
964
998
  },
@@ -1004,10 +1038,10 @@ async function serializeDocx(doc, fout, workdir, assets) {
1004
1038
  case "externalDoc":
1005
1039
  const doc_path = getPath(node.path);
1006
1040
  if (!fs3.existsSync(doc_path))
1007
- throw new Error(`File not exist: ${node.path}`);
1041
+ throw new UserInputError(`File not exist: ${node.path}`);
1008
1042
  const ext = import_path2.default.extname(doc_path);
1009
1043
  if (ext != ".docx" && ext != ".pdf")
1010
- throw new Error(`File not .docx or .pdf: ${node.path}`);
1044
+ throw new UserInputError(`File not .docx or .pdf: ${node.path}`);
1011
1045
  return new import_docx.Paragraph({
1012
1046
  text: `!!(${doc_path})${JSON.stringify(node.dict)}`,
1013
1047
  indent: { firstLine: 0 },
@@ -1030,6 +1064,18 @@ async function serializeDocx(doc, fout, workdir, assets) {
1030
1064
  children,
1031
1065
  properties: {
1032
1066
  page: {
1067
+ // size: section.orientation == "landscape" ? {
1068
+ // orientation: PageOrientation.LANDSCAPE,
1069
+ // width: convertMillimetersToTwip(297),
1070
+ // height: convertMillimetersToTwip(210),
1071
+ // } : {
1072
+ // orientation: PageOrientation.PORTRAIT,
1073
+ // width: convertMillimetersToTwip(210),
1074
+ // height: convertMillimetersToTwip(297),
1075
+ // },
1076
+ size: {
1077
+ orientation: section.orientation == "landscape" ? import_docx.PageOrientation.LANDSCAPE : import_docx.PageOrientation.PORTRAIT
1078
+ },
1033
1079
  pageNumbers: !section.pageStart ? {} : {
1034
1080
  start: section.pageStart
1035
1081
  },
@@ -1047,7 +1093,7 @@ async function serializeDocx(doc, fout, workdir, assets) {
1047
1093
  new import_docx.Paragraph({
1048
1094
  alignment: import_docx.AlignmentType.CENTER,
1049
1095
  children: [new import_docx.TextRun({
1050
- children: section.pageStart ? [import_docx.PageNumber.CURRENT] : []
1096
+ children: section.displayPageNum ? [import_docx.PageNumber.CURRENT] : []
1051
1097
  })],
1052
1098
  indent: { firstLine: 0 },
1053
1099
  spacing: { line: 240 }
@@ -1103,7 +1149,26 @@ async function serializeDocx(doc, fout, workdir, assets) {
1103
1149
  let children = null;
1104
1150
  if (rune.type == "val" && rune.text == "pages") {
1105
1151
  rune.type = "text";
1106
- children = [import_docx.PageNumber.TOTAL_PAGES];
1152
+ children = doc.totalPagesOffset == 0 ? [import_docx.PageNumber.TOTAL_PAGES] : [
1153
+ // Complex Formula Field: { = { NUMPAGES } + 1 }
1154
+ // Outer field start: "="
1155
+ new RawXml("w:fldChar", { "w:fldCharType": "begin" }),
1156
+ new RawXml("w:instrText", { "xml:space": "preserve" }, " = "),
1157
+ // Inner field start: "NUMPAGES"
1158
+ new RawXml("w:fldChar", { "w:fldCharType": "begin" }),
1159
+ new RawXml("w:instrText", { "xml:space": "preserve" }, " NUMPAGES "),
1160
+ new RawXml("w:fldChar", { "w:fldCharType": "separate" }),
1161
+ new RawXml("w:fldChar", { "w:fldCharType": "end" }),
1162
+ // Operator
1163
+ new RawXml(
1164
+ "w:instrText",
1165
+ { "xml:space": "preserve" },
1166
+ doc.totalPagesOffset > 0 ? ` + ${doc.totalPagesOffset}` : ` - ${-doc.totalPagesOffset}`
1167
+ ),
1168
+ // Outer field close
1169
+ new RawXml("w:fldChar", { "w:fldCharType": "separate" }),
1170
+ new RawXml("w:fldChar", { "w:fldCharType": "end" })
1171
+ ];
1107
1172
  }
1108
1173
  return new import_docx.TextRun({
1109
1174
  ...children ? { children } : { text: rune.text },
@@ -1180,7 +1245,6 @@ function addListItemLevel(levels, level, startIndex, itemCount, ordered, alterna
1180
1245
  }
1181
1246
  const longList = startIndex + itemCount > 10;
1182
1247
  const left = alternativeStyle ? 0 : longList ? 20.5 : 17.5;
1183
- const hanging = longList && !alternativeStyle ? (0, import_docx.convertMillimetersToTwip)(8) : null;
1184
1248
  const text = ordered ? alternativeStyle ? `%${level + 1}.` : mark == "." ? `%${level + 1}.` : `%${level + 1})` : (
1185
1249
  // mark == "*" ? "\u2022" : "\u2012";
1186
1250
  mark == "*" ? "\xB7" : "\u2012"
@@ -1199,13 +1263,22 @@ function addListItemLevel(levels, level, startIndex, itemCount, ordered, alterna
1199
1263
  paragraph: {
1200
1264
  indent: {
1201
1265
  left: (0, import_docx.convertMillimetersToTwip)(left + 5 * indent),
1202
- ...alternativeStyle ? { firstLine: (0, import_docx.convertMillimetersToTwip)(12.5) } : {},
1203
- ...hanging ? { hanging } : {}
1266
+ hanging: (0, import_docx.convertMillimetersToTwip)(longList && !alternativeStyle ? 8 : 5),
1267
+ ...alternativeStyle ? { firstLine: (0, import_docx.convertMillimetersToTwip)(12.5) } : {}
1204
1268
  }
1205
1269
  }
1206
1270
  }
1207
1271
  });
1208
1272
  }
1273
+ var RawXml = class extends import_docx.XmlComponent {
1274
+ constructor(tag, attributes = {}, text = null) {
1275
+ super(tag);
1276
+ if (Object.keys(attributes).length > 0)
1277
+ this.root.push({ _attr: attributes });
1278
+ if (text)
1279
+ this.root.push(text);
1280
+ }
1281
+ };
1209
1282
  function renderCodeHighlighting(code, lang) {
1210
1283
  const grammar = import_prismjs.default.languages[lang.trim().toLowerCase()];
1211
1284
  if (!grammar) return null;
@@ -1696,7 +1769,7 @@ function alchemist(doc, logwarn = console.warn) {
1696
1769
  }
1697
1770
  if (rune.type != "ref") return;
1698
1771
  const type = node.type;
1699
- const m = /^(.*?)([-\+]\d+)?$/.exec(rune.text.replaceAll(/\s+/g, ""));
1772
+ const m = /^(.*?)([-+]\d+)?$/.exec(rune.text.replaceAll(/\s+/g, ""));
1700
1773
  const tag = m?.[1] || "";
1701
1774
  const math = m?.[2] || "";
1702
1775
  const mathSubstarct = math.at(0) == "-";
@@ -1826,14 +1899,22 @@ async function render({
1826
1899
  renderPDF = false,
1827
1900
  removeIntermediateDocx = false,
1828
1901
  disableMacros = false,
1902
+ useLibreOffice = false,
1903
+ allowExternalFiles = false,
1829
1904
  logwarn = console.warn,
1905
+ loginfo = console.info,
1830
1906
  logPS = (msg) => console.log(`PS: ${msg}`),
1831
- logPSError = (msg) => console.error(`PS: ${msg}`)
1907
+ logPSError = (msg) => console.error(`PS ERROR: ${msg}`),
1908
+ signal = null
1832
1909
  }) {
1833
- 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...");
1910
+ signal?.throwIfAborted();
1834
1911
  const fin = import_path3.default.resolve(file);
1912
+ 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...");
1913
+ loginfo(`[!1] Parsing file ${fin}`);
1835
1914
  const doc = await parseMD(fin, logwarn);
1915
+ signal?.throwIfAborted();
1836
1916
  progress(10, "\u041A\u043E\u043B\u0434\u0443\u0435\u043C \u043D\u0430\u0434 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441\u043E\u043C...");
1917
+ loginfo("[!2] Processing document");
1837
1918
  enrichDoc(doc, logwarn);
1838
1919
  const runicDoc = runifyDoc(doc);
1839
1920
  alchemist(runicDoc, logwarn);
@@ -1848,64 +1929,107 @@ async function render({
1848
1929
  throw new Error(`\u0417\u0430\u043A\u0440\u043E\u0439\u0442\u0435 docx \u0444\u0430\u0439\u043B. Output file is busy or locked: ${ftmp}`);
1849
1930
  if (await checkIfFileIsBlocked(fout))
1850
1931
  throw new Error(`\u0417\u0430\u043A\u0440\u043E\u0439\u0442\u0435 docx \u0444\u0430\u0439\u043B. Output file is busy or locked: ${fout}`);
1932
+ signal?.throwIfAborted();
1851
1933
  progress(10, phrase_renderDocx());
1852
- await serializeDocx(runicDoc, ftmp, fdir, assets);
1934
+ loginfo("[!3] Serializing to docx");
1935
+ await serializeDocx(runicDoc, ftmp, fdir, assets, allowExternalFiles);
1853
1936
  let willRunMacros = !disableMacros && (renderPDF || hasReasonForRunningMacros(runicDoc));
1854
1937
  let limitedRender = false;
1855
- if (process.platform != "win32" && willRunMacros) {
1938
+ if (process.platform != "win32" && willRunMacros && !useLibreOffice) {
1856
1939
  limitedRender = true;
1857
1940
  willRunMacros = false;
1858
1941
  }
1859
1942
  if (willRunMacros) {
1943
+ signal?.throwIfAborted();
1860
1944
  progress(20, phrase_runMacros());
1945
+ loginfo("[!4] Starting macros");
1861
1946
  const tmpfolder = import_path3.default.join(pout ? pout.dir : fdir, ".md2gost_out");
1862
1947
  await import_promises3.default.rm(tmpfolder, { recursive: true, force: true });
1863
1948
  try {
1864
- const ok = await runDocxMacro(progress, logPS, logPSError, assets, fdir, ftmp, fout, renderPDF);
1949
+ const macroRunner = useLibreOffice ? runUnoScript : runDocxMacro;
1950
+ const ok = await macroRunner(progress, logPS, logPSError, assets, fdir, ftmp, fout, renderPDF, signal);
1865
1951
  if (!ok) {
1866
1952
  await import_promises3.default.rename(ftmp, fout);
1867
1953
  return { fout, err: "inPS" };
1868
1954
  }
1869
1955
  } catch (x) {
1956
+ if (signal?.aborted) {
1957
+ await import_promises3.default.unlink(ftmp).catch(() => {
1958
+ });
1959
+ throw x;
1960
+ }
1870
1961
  await import_promises3.default.rename(ftmp, fout);
1871
1962
  return { fout, err: "noPS", errS: x };
1872
1963
  }
1873
- await import_promises3.default.unlink(ftmp);
1964
+ if ((0, import_fs.existsSync)(ftmp))
1965
+ await import_promises3.default.unlink(ftmp);
1966
+ signal?.throwIfAborted();
1874
1967
  const errorTxt = import_path3.default.join(tmpfolder, "error.txt");
1875
1968
  if ((0, import_fs.existsSync)(errorTxt)) {
1876
1969
  const err = await import_promises3.default.readFile(errorTxt);
1877
1970
  await import_promises3.default.rm(tmpfolder, { recursive: true, force: true });
1878
1971
  return { fout, err: "vba", errS: `${err}` };
1879
1972
  }
1973
+ loginfo("[!5] Updating metadata");
1880
1974
  await updateMetadata(fout, doc);
1881
1975
  if (renderPDF) {
1976
+ signal?.throwIfAborted();
1882
1977
  progress(10, phrase_combine());
1978
+ loginfo("[!6] Merging pdfs");
1883
1979
  if (!(0, import_fs.existsSync)(tmpfolder))
1884
1980
  return { fout, err: "pdf" };
1885
1981
  try {
1886
1982
  const files = (await import_promises3.default.readdir(tmpfolder)).sort().map((f) => import_path3.default.join(tmpfolder, f));
1887
1983
  const pdf = pout ? import_path3.default.join(pout.dir, pout.base) : import_path3.default.join(fdir, fname + ".pdf");
1888
- await mergePDFs(files, pdf, doc);
1984
+ await mergePDFs(files, pdf, doc, signal || void 0);
1889
1985
  await import_promises3.default.rm(tmpfolder, { recursive: true, force: true });
1890
1986
  if (removeIntermediateDocx && (0, import_fs.existsSync)(fout)) await import_promises3.default.unlink(fout);
1891
1987
  return { fout: pdf };
1892
1988
  } catch (err) {
1989
+ if (signal?.aborted) throw err;
1893
1990
  return { fout, err: "pdf", errS: err };
1894
1991
  }
1895
1992
  }
1896
1993
  } else {
1897
1994
  await import_promises3.default.rename(ftmp, fout);
1898
1995
  }
1996
+ loginfo(`[!7] Rendered to ${fout}`);
1899
1997
  return { fout, err: limitedRender ? "noWin" : void 0 };
1900
1998
  }
1901
1999
  function hasReasonForRunningMacros(doc) {
1902
2000
  if (DISABLE_MACRO) return false;
1903
2001
  return doc.nodes.some((n) => n.type == "code" || n.type == "table");
1904
2002
  }
1905
- function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF) {
2003
+ function killProcessTree(child, wordPid) {
2004
+ if (!child.pid) return;
2005
+ if (process.platform === "win32") {
2006
+ if (wordPid) {
2007
+ (0, import_child_process.exec)(`taskkill /pid ${wordPid}`, () => {
2008
+ setTimeout(() => {
2009
+ (0, import_child_process.exec)(`taskkill /pid ${wordPid} /F`, () => {
2010
+ });
2011
+ setTimeout(() => {
2012
+ killProcessTree(child);
2013
+ }, 2e3);
2014
+ }, 2e3);
2015
+ });
2016
+ } else {
2017
+ (0, import_child_process.exec)(`taskkill /pid ${child.pid} /T /F`, () => {
2018
+ });
2019
+ }
2020
+ } else {
2021
+ try {
2022
+ process.kill(-child.pid, "SIGKILL");
2023
+ } catch {
2024
+ }
2025
+ }
2026
+ }
2027
+ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF, signal) {
1906
2028
  const script = import_path3.default.join(assets, "run.ps1");
1907
2029
  const templateMacro = import_path3.default.join(assets, "template.dotm");
1908
2030
  return new Promise((res, rej) => {
2031
+ if (signal?.aborted)
2032
+ return rej(signal.reason ?? new Error("Aborted before execution started"));
1909
2033
  const child = (0, import_child_process.spawn)("powershell", [
1910
2034
  "-NoProfile",
1911
2035
  "-ExecutionPolicy",
@@ -1919,10 +2043,28 @@ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF
1919
2043
  "-MacroTemplate",
1920
2044
  templateMacro,
1921
2045
  ...renderPDF ? ["-RenderPDF"] : []
1922
- ], { cwd });
2046
+ ], {
2047
+ cwd,
2048
+ detached: process.platform !== "win32"
2049
+ // Enables PGID on POSIX
2050
+ // detached: true,
2051
+ // shell: true,
2052
+ });
2053
+ let wordPid = null;
2054
+ let onAbort;
2055
+ if (signal) {
2056
+ onAbort = () => {
2057
+ logError("Operation aborted. Terminating process tree...");
2058
+ killProcessTree(child, wordPid);
2059
+ rej(signal.reason ?? new Error("Job timed out or aborted"));
2060
+ };
2061
+ signal.addEventListener("abort", onAbort, { once: true });
2062
+ }
1923
2063
  child.stdout.on("data", (data) => {
1924
- data = `${data}`;
1925
- const m = /\[\*(\d)\]/.exec(data);
2064
+ const raw = `${data}`;
2065
+ const pidMatch = /\[WordPID:(\d+)\]/.exec(raw);
2066
+ if (pidMatch) wordPid = parseInt(pidMatch[1], 10);
2067
+ const m = /\[\*(\d)\]/.exec(raw);
1926
2068
  if (m) progress(12, {
1927
2069
  // "1": "Fixing breaks (listings)",
1928
2070
  "1": render_fixingBreaks(),
@@ -1931,31 +2073,115 @@ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF
1931
2073
  // "3": "Rendering to PDF",
1932
2074
  "3": render_renderPDF()
1933
2075
  }[m[1]] || "Make some work");
1934
- log(data);
2076
+ log(raw);
2077
+ });
2078
+ let ok = true;
2079
+ child.stderr.on("data", (data) => {
2080
+ ok = false;
2081
+ logError(`${data}`);
2082
+ });
2083
+ child.on("close", (code) => {
2084
+ if (signal && onAbort) {
2085
+ signal.removeEventListener("abort", onAbort);
2086
+ }
2087
+ res(ok && code === 0);
2088
+ });
2089
+ child.on("error", (err) => {
2090
+ if (signal && onAbort) {
2091
+ signal.removeEventListener("abort", onAbort);
2092
+ }
2093
+ rej(err);
2094
+ });
2095
+ });
2096
+ }
2097
+ function getPythonExecutable() {
2098
+ if (process.platform === "win32") {
2099
+ const loPython = "C:\\Program Files\\LibreOffice\\program\\python.exe";
2100
+ if ((0, import_fs.existsSync)(loPython)) return loPython;
2101
+ }
2102
+ return process.env.UNO_PYTHON_PATH || "python3";
2103
+ }
2104
+ function runUnoScript(progress, log, logError, assets, cwd, fin, fout, renderPDF, signal) {
2105
+ const script = process.env.MACRO_ENGINE_PATH || import_path3.default.join(assets, "macro_engine.py");
2106
+ const pythonExec = getPythonExecutable();
2107
+ return new Promise((res, rej) => {
2108
+ if (signal?.aborted) {
2109
+ return rej(signal.reason ?? new Error("Aborted before execution started"));
2110
+ }
2111
+ const args = [
2112
+ script,
2113
+ "--input",
2114
+ fin,
2115
+ "--output",
2116
+ fout
2117
+ ];
2118
+ if (renderPDF) args.push("--render-pdf");
2119
+ const child = (0, import_child_process.spawn)(pythonExec, args, {
2120
+ cwd,
2121
+ detached: process.platform !== "win32"
2122
+ // Enables PGID on POSIX
2123
+ });
2124
+ let onAbort;
2125
+ if (signal) {
2126
+ onAbort = () => {
2127
+ logError("Operation aborted. Terminating process tree...");
2128
+ killProcessTree(child);
2129
+ rej(signal.reason ?? new Error("Job timed out or aborted"));
2130
+ };
2131
+ signal.addEventListener("abort", onAbort, { once: true });
2132
+ }
2133
+ child.stdout.on("data", (data) => {
2134
+ const lines = `${data}`.split("\n");
2135
+ for (const line of lines) {
2136
+ log(line.trimEnd());
2137
+ if (!line.trimEnd()) continue;
2138
+ const m = /\[\*(\d)\]/.exec(line);
2139
+ if (m) {
2140
+ const msg = {
2141
+ "1": render_fixingBreaks(),
2142
+ "2": render_fixingBreaks(),
2143
+ "3": render_renderPDF()
2144
+ }[m[1]] || "Processing document...";
2145
+ progress(12, msg);
2146
+ }
2147
+ }
1935
2148
  });
1936
2149
  let ok = true;
1937
2150
  child.stderr.on("data", (data) => {
1938
2151
  ok = false;
1939
2152
  logError(`${data}`);
1940
2153
  });
1941
- child.on("close", () => res(ok));
1942
- child.on("error", rej);
2154
+ child.on("close", (code) => {
2155
+ if (signal && onAbort) {
2156
+ signal.removeEventListener("abort", onAbort);
2157
+ }
2158
+ res(ok && code === 0);
2159
+ });
2160
+ child.on("error", (err) => {
2161
+ if (signal && onAbort) {
2162
+ signal.removeEventListener("abort", onAbort);
2163
+ }
2164
+ rej(err);
2165
+ });
1943
2166
  });
1944
2167
  }
1945
- async function mergePDFs(files, fout, doc) {
2168
+ async function mergePDFs(files, fout, doc, signal) {
1946
2169
  const mergedPdf = await import_pdf_lib.PDFDocument.create();
1947
2170
  if (doc?.title) mergedPdf.setTitle(doc.title, { showInWindowTitleBar: true });
1948
2171
  if (doc?.author) mergedPdf.setAuthor(doc.author);
1949
2172
  if (doc?.ctime) mergedPdf.setCreationDate(doc.ctime);
1950
2173
  if (doc?.mtime) mergedPdf.setModificationDate(doc.mtime);
1951
2174
  for (const file of files) {
1952
- const bytes = await import_promises3.default.readFile(file);
2175
+ if (!file.endsWith(".pdf")) continue;
2176
+ const bytes = await import_promises3.default.readFile(file, { signal });
1953
2177
  const pdf = await import_pdf_lib.PDFDocument.load(bytes);
2178
+ signal?.throwIfAborted();
1954
2179
  const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
1955
2180
  pages.forEach((page) => mergedPdf.addPage(page));
2181
+ signal?.throwIfAborted();
1956
2182
  }
1957
2183
  const pdfBytes = await mergedPdf.save();
1958
- await import_promises3.default.writeFile(fout, pdfBytes);
2184
+ await import_promises3.default.writeFile(fout, pdfBytes, { signal });
1959
2185
  }
1960
2186
  async function updateMetadata(docfile, doc) {
1961
2187
  const content = await import_promises3.default.readFile(docfile);
@@ -2047,25 +2273,50 @@ async function renderMarkdown(config) {
2047
2273
  assert(config.format == "docx" || config.format == "pdf" || typeof config.format == "undefined", "Format must be either 'docx' or 'pdf', or undefined.");
2048
2274
  assert(typeof config.keepIntermediateDocx == "boolean" || typeof config.keepIntermediateDocx == "undefined", "keepIntermediateDocx must be a boolean or undefined.");
2049
2275
  assert(typeof config.disableMacros == "boolean" || typeof config.disableMacros == "undefined", "disableMacros must be a boolean or undefined.");
2276
+ assert(typeof config.useLibreOffice == "boolean" || typeof config.useLibreOffice == "undefined", "useLibreOffice must be a boolean or undefined.");
2277
+ assert(typeof config.allowExternalFiles == "boolean" || typeof config.allowExternalFiles == "undefined", "allowExternalFiles must be a boolean or undefined.");
2050
2278
  assert(typeof config.progress == "function" || typeof config.progress == "undefined", "Progress callback must be a function or undefined.");
2279
+ assert(config.abortSignal instanceof AbortSignal || typeof config.abortSignal == "undefined", "abortSignal must be an AbortSignal or undefined.");
2051
2280
  {
2052
2281
  const extraProps = getExtraProperties(
2053
2282
  config,
2054
- ["input", "output", "format", "keepIntermediateDocx", "disableMacros", "progress"]
2283
+ ["input", "output", "format", "keepIntermediateDocx", "disableMacros", "useLibreOffice", "progress", "logger", "abortSignal", "allowExternalFiles"]
2055
2284
  );
2056
2285
  assert(extraProps.length == 0, `Found unknown properties in config: ${extraProps.join(", ")}`);
2057
2286
  }
2287
+ assert(typeof config.logger == "object" && config.logger || typeof config.logger == "undefined", "Logger must be a object or undefined.");
2288
+ const logger = config.logger;
2289
+ if (logger) {
2290
+ const props = ["info", "warn", "error"];
2291
+ const extraProps = getExtraProperties(logger, props);
2292
+ assert(extraProps.length == 0, `Found unknown properties in config.logger: ${extraProps.join(", ")}`);
2293
+ props.forEach(
2294
+ (p) => assert(typeof logger[p] == "function", `config.logger.${p} must be a function.`)
2295
+ );
2296
+ }
2058
2297
  const renderPDF = config.format === "pdf" || !config.format && !!config.output?.endsWith(".pdf");
2059
2298
  const removeIntermediateDocx = !config.keepIntermediateDocx;
2060
2299
  const disableMacros = !!config.disableMacros;
2300
+ const useLibreOffice = !!config.useLibreOffice;
2301
+ const allowExternalFiles = !!config.allowExternalFiles;
2061
2302
  assert(!(disableMacros && renderPDF), "Macros must be enabled to render PDF output.");
2303
+ const pin = import_path4.default.parse(import_path4.default.resolve(config.input));
2304
+ const fname = pin.name.endsWith(".g") ? pin.name.slice(0, -2) : pin.name;
2305
+ let resolvedOutput = config.output;
2306
+ if (resolvedOutput) {
2307
+ const isDir = resolvedOutput.endsWith("/") || resolvedOutput.endsWith("\\") || !import_path4.default.extname(resolvedOutput);
2308
+ if (isDir) {
2309
+ const ext = renderPDF ? ".pdf" : ".docx";
2310
+ resolvedOutput = import_path4.default.join(resolvedOutput, fname + ext);
2311
+ }
2312
+ }
2062
2313
  const warnings = [];
2063
2314
  const logPS = [];
2064
2315
  let totalPercent = 0;
2065
- const handleProgress = config.progress ? (increment, message) => {
2316
+ const handleProgress = (increment, message) => {
2066
2317
  totalPercent = Math.min(totalPercent + increment, 99);
2067
- config.progress(totalPercent, message);
2068
- } : void 0;
2318
+ config.progress?.(totalPercent, message);
2319
+ };
2069
2320
  try {
2070
2321
  let throwMDE2 = function(code, msg) {
2071
2322
  throw new MDRenderError(code, msg, fout, warnings, logPS, errS ? { cause: errS } : void 0);
@@ -2075,26 +2326,41 @@ async function renderMarkdown(config) {
2075
2326
  progress: handleProgress,
2076
2327
  assets: getAssetsDir(),
2077
2328
  file: config.input,
2078
- output: config.output,
2329
+ output: resolvedOutput,
2079
2330
  renderPDF,
2080
2331
  removeIntermediateDocx,
2081
2332
  disableMacros,
2082
- logwarn: (msg) => warnings.push(msg),
2083
- logPS: (msg) => logPS.push(msg),
2084
- logPSError: (msg) => logPS.push(msg)
2333
+ useLibreOffice,
2334
+ allowExternalFiles,
2335
+ loginfo: logger?.info,
2336
+ logwarn: (msg) => {
2337
+ logger?.warn(msg);
2338
+ warnings.push(msg);
2339
+ },
2340
+ logPS: (msg) => {
2341
+ logger?.info(msg);
2342
+ logPS.push(msg);
2343
+ },
2344
+ logPSError: (msg) => {
2345
+ logger?.error(msg);
2346
+ logPS.push(msg);
2347
+ },
2348
+ signal: config.abortSignal
2085
2349
  });
2086
2350
  config.progress?.(100, "Done!");
2087
- ;
2088
- if (err == "noPS") throwMDE2("noPS", `Failed to initialize PowerShell. Ensure PowerShell is installed and explicitly added to your system's PATH environment variable.`);
2089
- if (err == "inPS") throwMDE2("inPS", `An unexpected error occurred executing commands inside PowerShell. Verify that Microsoft Word is properly installed and licensed.`);
2351
+ if (useLibreOffice) {
2352
+ 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.`);
2353
+ 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.`);
2354
+ } else {
2355
+ if (err == "noPS") throwMDE2("noPS", `Failed to initialize PowerShell. Ensure PowerShell is installed and explicitly added to your system's PATH environment variable.`);
2356
+ if (err == "inPS") throwMDE2("inPS", `An unexpected error occurred executing commands inside PowerShell. Verify that Microsoft Word is properly installed and licensed.`);
2357
+ }
2090
2358
  if (err == "vba") throwMDE2("vba", `Microsoft Word VBA Macro execution failed: ${errS}`);
2091
2359
  if (err == "pdf") throwMDE2("pdf", `Failed to export document to PDF.`);
2092
2360
  if (err == "noWin") throwMDE2("noWin", `Platform restriction: Extended formatting macros and PDF rendering are only supported natively on Windows systems.`);
2093
2361
  let intermediateDocxPath;
2094
- if (renderPDF) {
2095
- const pin = import_path4.default.parse(import_path4.default.resolve(config.input));
2096
- const fname = pin.name.endsWith(".g") ? pin.name.slice(0, -2) : pin.name;
2097
- const pout = config.output ? import_path4.default.parse(import_path4.default.resolve(config.output)) : void 0;
2362
+ if (renderPDF && !removeIntermediateDocx) {
2363
+ const pout = resolvedOutput ? import_path4.default.parse(import_path4.default.resolve(resolvedOutput)) : void 0;
2098
2364
  intermediateDocxPath = pout ? import_path4.default.join(pout.dir, pout.name + ".docx") : import_path4.default.join(pin.dir, fname + ".docx");
2099
2365
  }
2100
2366
  return {
@@ -2104,6 +2370,7 @@ async function renderMarkdown(config) {
2104
2370
  };
2105
2371
  } catch (x) {
2106
2372
  if (x instanceof MDRenderError) throw x;
2373
+ if (x instanceof UserInputError) throw new MDRenderError("userInput", x.message, null, warnings, logPS, { cause: x });
2107
2374
  const message = x instanceof Error ? x.message : String(x);
2108
2375
  throw new MDRenderError("unknown", `An unhandled internal error occurred during rendering: ${message}`, null, warnings, logPS, { cause: x });
2109
2376
  }
@@ -2116,6 +2383,7 @@ var MDRenderError = class _MDRenderError extends Error {
2116
2383
  /**
2117
2384
  * Code indicating the underlying cause of the failure:
2118
2385
  * - `unknown`: An unhandled or unexpected internal runtime error.
2386
+ * - `userInput`: Invalid user input or Markdown document structure.
2119
2387
  * - `noPS`: PowerShell could not be started or found in the system PATH.
2120
2388
  * - `inPS`: A failure occurred inside PowerShell; MS Word may be missing or corrupt.
2121
2389
  * - `vba`: An error occurred during the execution of internal Word VBA macros.