md2gost 1.3.2 → 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 },
@@ -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
  }
@@ -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;
@@ -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);
1935
2077
  });
1936
2078
  let ok = true;
1937
2079
  child.stderr.on("data", (data) => {
1938
2080
  ok = false;
1939
2081
  logError(`${data}`);
1940
2082
  });
1941
- child.on("close", () => res(ok));
1942
- child.on("error", rej);
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
+ });
1943
2095
  });
1944
2096
  }
1945
- async function mergePDFs(files, fout, doc) {
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
+ }
2148
+ });
2149
+ let ok = true;
2150
+ child.stderr.on("data", (data) => {
2151
+ ok = false;
2152
+ logError(`${data}`);
2153
+ });
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
+ });
2166
+ });
2167
+ }
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,17 +2273,32 @@ 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.");
2062
2303
  const pin = import_path4.default.parse(import_path4.default.resolve(config.input));
2063
2304
  const fname = pin.name.endsWith(".g") ? pin.name.slice(0, -2) : pin.name;
@@ -2072,10 +2313,10 @@ async function renderMarkdown(config) {
2072
2313
  const warnings = [];
2073
2314
  const logPS = [];
2074
2315
  let totalPercent = 0;
2075
- const handleProgress = config.progress ? (increment, message) => {
2316
+ const handleProgress = (increment, message) => {
2076
2317
  totalPercent = Math.min(totalPercent + increment, 99);
2077
- config.progress(totalPercent, message);
2078
- } : void 0;
2318
+ config.progress?.(totalPercent, message);
2319
+ };
2079
2320
  try {
2080
2321
  let throwMDE2 = function(code, msg) {
2081
2322
  throw new MDRenderError(code, msg, fout, warnings, logPS, errS ? { cause: errS } : void 0);
@@ -2089,13 +2330,31 @@ async function renderMarkdown(config) {
2089
2330
  renderPDF,
2090
2331
  removeIntermediateDocx,
2091
2332
  disableMacros,
2092
- logwarn: (msg) => warnings.push(msg),
2093
- logPS: (msg) => logPS.push(msg),
2094
- 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
2095
2349
  });
2096
2350
  config.progress?.(100, "Done!");
2097
- if (err == "noPS") throwMDE2("noPS", `Failed to initialize PowerShell. Ensure PowerShell is installed and explicitly added to your system's PATH environment variable.`);
2098
- 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
+ }
2099
2358
  if (err == "vba") throwMDE2("vba", `Microsoft Word VBA Macro execution failed: ${errS}`);
2100
2359
  if (err == "pdf") throwMDE2("pdf", `Failed to export document to PDF.`);
2101
2360
  if (err == "noWin") throwMDE2("noWin", `Platform restriction: Extended formatting macros and PDF rendering are only supported natively on Windows systems.`);
@@ -2111,6 +2370,7 @@ async function renderMarkdown(config) {
2111
2370
  };
2112
2371
  } catch (x) {
2113
2372
  if (x instanceof MDRenderError) throw x;
2373
+ if (x instanceof UserInputError) throw new MDRenderError("userInput", x.message, null, warnings, logPS, { cause: x });
2114
2374
  const message = x instanceof Error ? x.message : String(x);
2115
2375
  throw new MDRenderError("unknown", `An unhandled internal error occurred during rendering: ${message}`, null, warnings, logPS, { cause: x });
2116
2376
  }
@@ -2123,6 +2383,7 @@ var MDRenderError = class _MDRenderError extends Error {
2123
2383
  /**
2124
2384
  * Code indicating the underlying cause of the failure:
2125
2385
  * - `unknown`: An unhandled or unexpected internal runtime error.
2386
+ * - `userInput`: Invalid user input or Markdown document structure.
2126
2387
  * - `noPS`: PowerShell could not be started or found in the system PATH.
2127
2388
  * - `inPS`: A failure occurred inside PowerShell; MS Word may be missing or corrupt.
2128
2389
  * - `vba`: An error occurred during the execution of internal Word VBA macros.