md2gost 1.3.2 → 1.4.1

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,13 +970,13 @@ 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];
@@ -959,6 +989,8 @@ async function serializeDocx(doc, fout, workdir, assets) {
959
989
  indent: { firstLine: 0 },
960
990
  spacing: {
961
991
  line: 240,
992
+ lineRule: import_docx.LineRuleType.AT_LEAST,
993
+ // For LibreOffice to not shrink image
962
994
  ...!node.text && doc.img.spacing.after !== void 0 ? { after: doc.img.spacing.after * 20 } : {},
963
995
  ...doc.img.spacing.before !== void 0 ? { before: doc.img.spacing.before * 20 } : {}
964
996
  },
@@ -1004,10 +1036,10 @@ async function serializeDocx(doc, fout, workdir, assets) {
1004
1036
  case "externalDoc":
1005
1037
  const doc_path = getPath(node.path);
1006
1038
  if (!fs3.existsSync(doc_path))
1007
- throw new Error(`File not exist: ${node.path}`);
1039
+ throw new UserInputError(`File not exist: ${node.path}`);
1008
1040
  const ext = import_path2.default.extname(doc_path);
1009
1041
  if (ext != ".docx" && ext != ".pdf")
1010
- throw new Error(`File not .docx or .pdf: ${node.path}`);
1042
+ throw new UserInputError(`File not .docx or .pdf: ${node.path}`);
1011
1043
  return new import_docx.Paragraph({
1012
1044
  text: `!!(${doc_path})${JSON.stringify(node.dict)}`,
1013
1045
  indent: { firstLine: 0 },
@@ -1030,6 +1062,18 @@ async function serializeDocx(doc, fout, workdir, assets) {
1030
1062
  children,
1031
1063
  properties: {
1032
1064
  page: {
1065
+ // size: section.orientation == "landscape" ? {
1066
+ // orientation: PageOrientation.LANDSCAPE,
1067
+ // width: convertMillimetersToTwip(297),
1068
+ // height: convertMillimetersToTwip(210),
1069
+ // } : {
1070
+ // orientation: PageOrientation.PORTRAIT,
1071
+ // width: convertMillimetersToTwip(210),
1072
+ // height: convertMillimetersToTwip(297),
1073
+ // },
1074
+ size: {
1075
+ orientation: section.orientation == "landscape" ? import_docx.PageOrientation.LANDSCAPE : import_docx.PageOrientation.PORTRAIT
1076
+ },
1033
1077
  pageNumbers: !section.pageStart ? {} : {
1034
1078
  start: section.pageStart
1035
1079
  },
@@ -1047,7 +1091,7 @@ async function serializeDocx(doc, fout, workdir, assets) {
1047
1091
  new import_docx.Paragraph({
1048
1092
  alignment: import_docx.AlignmentType.CENTER,
1049
1093
  children: [new import_docx.TextRun({
1050
- children: section.pageStart ? [import_docx.PageNumber.CURRENT] : []
1094
+ children: section.displayPageNum ? [import_docx.PageNumber.CURRENT] : []
1051
1095
  })],
1052
1096
  indent: { firstLine: 0 },
1053
1097
  spacing: { line: 240 }
@@ -1103,7 +1147,26 @@ async function serializeDocx(doc, fout, workdir, assets) {
1103
1147
  let children = null;
1104
1148
  if (rune.type == "val" && rune.text == "pages") {
1105
1149
  rune.type = "text";
1106
- children = [import_docx.PageNumber.TOTAL_PAGES];
1150
+ children = doc.totalPagesOffset == 0 ? [import_docx.PageNumber.TOTAL_PAGES] : [
1151
+ // Complex Formula Field: { = { NUMPAGES } + 1 }
1152
+ // Outer field start: "="
1153
+ new RawXml("w:fldChar", { "w:fldCharType": "begin" }),
1154
+ new RawXml("w:instrText", { "xml:space": "preserve" }, " = "),
1155
+ // Inner field start: "NUMPAGES"
1156
+ new RawXml("w:fldChar", { "w:fldCharType": "begin" }),
1157
+ new RawXml("w:instrText", { "xml:space": "preserve" }, " NUMPAGES "),
1158
+ new RawXml("w:fldChar", { "w:fldCharType": "separate" }),
1159
+ new RawXml("w:fldChar", { "w:fldCharType": "end" }),
1160
+ // Operator
1161
+ new RawXml(
1162
+ "w:instrText",
1163
+ { "xml:space": "preserve" },
1164
+ doc.totalPagesOffset > 0 ? ` + ${doc.totalPagesOffset}` : ` - ${-doc.totalPagesOffset}`
1165
+ ),
1166
+ // Outer field close
1167
+ new RawXml("w:fldChar", { "w:fldCharType": "separate" }),
1168
+ new RawXml("w:fldChar", { "w:fldCharType": "end" })
1169
+ ];
1107
1170
  }
1108
1171
  return new import_docx.TextRun({
1109
1172
  ...children ? { children } : { text: rune.text },
@@ -1180,7 +1243,6 @@ function addListItemLevel(levels, level, startIndex, itemCount, ordered, alterna
1180
1243
  }
1181
1244
  const longList = startIndex + itemCount > 10;
1182
1245
  const left = alternativeStyle ? 0 : longList ? 20.5 : 17.5;
1183
- const hanging = longList && !alternativeStyle ? (0, import_docx.convertMillimetersToTwip)(8) : null;
1184
1246
  const text = ordered ? alternativeStyle ? `%${level + 1}.` : mark == "." ? `%${level + 1}.` : `%${level + 1})` : (
1185
1247
  // mark == "*" ? "\u2022" : "\u2012";
1186
1248
  mark == "*" ? "\xB7" : "\u2012"
@@ -1199,13 +1261,22 @@ function addListItemLevel(levels, level, startIndex, itemCount, ordered, alterna
1199
1261
  paragraph: {
1200
1262
  indent: {
1201
1263
  left: (0, import_docx.convertMillimetersToTwip)(left + 5 * indent),
1202
- ...alternativeStyle ? { firstLine: (0, import_docx.convertMillimetersToTwip)(12.5) } : {},
1203
- ...hanging ? { hanging } : {}
1264
+ hanging: (0, import_docx.convertMillimetersToTwip)(longList && !alternativeStyle ? 8 : 5),
1265
+ ...alternativeStyle ? { firstLine: (0, import_docx.convertMillimetersToTwip)(12.5) } : {}
1204
1266
  }
1205
1267
  }
1206
1268
  }
1207
1269
  });
1208
1270
  }
1271
+ var RawXml = class extends import_docx.XmlComponent {
1272
+ constructor(tag, attributes = {}, text = null) {
1273
+ super(tag);
1274
+ if (Object.keys(attributes).length > 0)
1275
+ this.root.push({ _attr: attributes });
1276
+ if (text)
1277
+ this.root.push(text);
1278
+ }
1279
+ };
1209
1280
  function renderCodeHighlighting(code, lang) {
1210
1281
  const grammar = import_prismjs.default.languages[lang.trim().toLowerCase()];
1211
1282
  if (!grammar) return null;
@@ -1826,14 +1897,22 @@ async function render({
1826
1897
  renderPDF = false,
1827
1898
  removeIntermediateDocx = false,
1828
1899
  disableMacros = false,
1900
+ useLibreOffice = false,
1901
+ allowExternalFiles = false,
1829
1902
  logwarn = console.warn,
1903
+ loginfo = console.info,
1830
1904
  logPS = (msg) => console.log(`PS: ${msg}`),
1831
- logPSError = (msg) => console.error(`PS: ${msg}`)
1905
+ logPSError = (msg) => console.error(`PS ERROR: ${msg}`),
1906
+ signal = null
1832
1907
  }) {
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...");
1908
+ signal?.throwIfAborted();
1834
1909
  const fin = import_path3.default.resolve(file);
1910
+ 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...");
1911
+ loginfo(`[!1] Parsing file ${fin}`);
1835
1912
  const doc = await parseMD(fin, logwarn);
1913
+ signal?.throwIfAborted();
1836
1914
  progress(10, "\u041A\u043E\u043B\u0434\u0443\u0435\u043C \u043D\u0430\u0434 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441\u043E\u043C...");
1915
+ loginfo("[!2] Processing document");
1837
1916
  enrichDoc(doc, logwarn);
1838
1917
  const runicDoc = runifyDoc(doc);
1839
1918
  alchemist(runicDoc, logwarn);
@@ -1848,64 +1927,107 @@ async function render({
1848
1927
  throw new Error(`\u0417\u0430\u043A\u0440\u043E\u0439\u0442\u0435 docx \u0444\u0430\u0439\u043B. Output file is busy or locked: ${ftmp}`);
1849
1928
  if (await checkIfFileIsBlocked(fout))
1850
1929
  throw new Error(`\u0417\u0430\u043A\u0440\u043E\u0439\u0442\u0435 docx \u0444\u0430\u0439\u043B. Output file is busy or locked: ${fout}`);
1930
+ signal?.throwIfAborted();
1851
1931
  progress(10, phrase_renderDocx());
1852
- await serializeDocx(runicDoc, ftmp, fdir, assets);
1932
+ loginfo("[!3] Serializing to docx");
1933
+ await serializeDocx(runicDoc, ftmp, fdir, assets, allowExternalFiles);
1853
1934
  let willRunMacros = !disableMacros && (renderPDF || hasReasonForRunningMacros(runicDoc));
1854
1935
  let limitedRender = false;
1855
- if (process.platform != "win32" && willRunMacros) {
1936
+ if (process.platform != "win32" && willRunMacros && !useLibreOffice) {
1856
1937
  limitedRender = true;
1857
1938
  willRunMacros = false;
1858
1939
  }
1859
1940
  if (willRunMacros) {
1941
+ signal?.throwIfAborted();
1860
1942
  progress(20, phrase_runMacros());
1943
+ loginfo("[!4] Starting macros");
1861
1944
  const tmpfolder = import_path3.default.join(pout ? pout.dir : fdir, ".md2gost_out");
1862
1945
  await import_promises3.default.rm(tmpfolder, { recursive: true, force: true });
1863
1946
  try {
1864
- const ok = await runDocxMacro(progress, logPS, logPSError, assets, fdir, ftmp, fout, renderPDF);
1947
+ const macroRunner = useLibreOffice ? runUnoScript : runDocxMacro;
1948
+ const ok = await macroRunner(progress, logPS, logPSError, assets, fdir, ftmp, fout, renderPDF, signal);
1865
1949
  if (!ok) {
1866
1950
  await import_promises3.default.rename(ftmp, fout);
1867
1951
  return { fout, err: "inPS" };
1868
1952
  }
1869
1953
  } catch (x) {
1954
+ if (signal?.aborted) {
1955
+ await import_promises3.default.unlink(ftmp).catch(() => {
1956
+ });
1957
+ throw x;
1958
+ }
1870
1959
  await import_promises3.default.rename(ftmp, fout);
1871
1960
  return { fout, err: "noPS", errS: x };
1872
1961
  }
1873
- await import_promises3.default.unlink(ftmp);
1962
+ if ((0, import_fs.existsSync)(ftmp))
1963
+ await import_promises3.default.unlink(ftmp);
1964
+ signal?.throwIfAborted();
1874
1965
  const errorTxt = import_path3.default.join(tmpfolder, "error.txt");
1875
1966
  if ((0, import_fs.existsSync)(errorTxt)) {
1876
1967
  const err = await import_promises3.default.readFile(errorTxt);
1877
1968
  await import_promises3.default.rm(tmpfolder, { recursive: true, force: true });
1878
1969
  return { fout, err: "vba", errS: `${err}` };
1879
1970
  }
1971
+ loginfo("[!5] Updating metadata");
1880
1972
  await updateMetadata(fout, doc);
1881
1973
  if (renderPDF) {
1974
+ signal?.throwIfAborted();
1882
1975
  progress(10, phrase_combine());
1976
+ loginfo("[!6] Merging pdfs");
1883
1977
  if (!(0, import_fs.existsSync)(tmpfolder))
1884
1978
  return { fout, err: "pdf" };
1885
1979
  try {
1886
1980
  const files = (await import_promises3.default.readdir(tmpfolder)).sort().map((f) => import_path3.default.join(tmpfolder, f));
1887
1981
  const pdf = pout ? import_path3.default.join(pout.dir, pout.base) : import_path3.default.join(fdir, fname + ".pdf");
1888
- await mergePDFs(files, pdf, doc);
1982
+ await mergePDFs(files, pdf, doc, signal || void 0);
1889
1983
  await import_promises3.default.rm(tmpfolder, { recursive: true, force: true });
1890
1984
  if (removeIntermediateDocx && (0, import_fs.existsSync)(fout)) await import_promises3.default.unlink(fout);
1891
1985
  return { fout: pdf };
1892
1986
  } catch (err) {
1987
+ if (signal?.aborted) throw err;
1893
1988
  return { fout, err: "pdf", errS: err };
1894
1989
  }
1895
1990
  }
1896
1991
  } else {
1897
1992
  await import_promises3.default.rename(ftmp, fout);
1898
1993
  }
1994
+ loginfo(`[!7] Rendered to ${fout}`);
1899
1995
  return { fout, err: limitedRender ? "noWin" : void 0 };
1900
1996
  }
1901
1997
  function hasReasonForRunningMacros(doc) {
1902
1998
  if (DISABLE_MACRO) return false;
1903
1999
  return doc.nodes.some((n) => n.type == "code" || n.type == "table");
1904
2000
  }
1905
- function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF) {
2001
+ function killProcessTree(child, wordPid) {
2002
+ if (!child.pid) return;
2003
+ if (process.platform === "win32") {
2004
+ if (wordPid) {
2005
+ (0, import_child_process.exec)(`taskkill /pid ${wordPid}`, () => {
2006
+ setTimeout(() => {
2007
+ (0, import_child_process.exec)(`taskkill /pid ${wordPid} /F`, () => {
2008
+ });
2009
+ setTimeout(() => {
2010
+ killProcessTree(child);
2011
+ }, 2e3);
2012
+ }, 2e3);
2013
+ });
2014
+ } else {
2015
+ (0, import_child_process.exec)(`taskkill /pid ${child.pid} /T /F`, () => {
2016
+ });
2017
+ }
2018
+ } else {
2019
+ try {
2020
+ process.kill(-child.pid, "SIGKILL");
2021
+ } catch {
2022
+ }
2023
+ }
2024
+ }
2025
+ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF, signal) {
1906
2026
  const script = import_path3.default.join(assets, "run.ps1");
1907
2027
  const templateMacro = import_path3.default.join(assets, "template.dotm");
1908
2028
  return new Promise((res, rej) => {
2029
+ if (signal?.aborted)
2030
+ return rej(signal.reason ?? new Error("Aborted before execution started"));
1909
2031
  const child = (0, import_child_process.spawn)("powershell", [
1910
2032
  "-NoProfile",
1911
2033
  "-ExecutionPolicy",
@@ -1919,10 +2041,28 @@ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF
1919
2041
  "-MacroTemplate",
1920
2042
  templateMacro,
1921
2043
  ...renderPDF ? ["-RenderPDF"] : []
1922
- ], { cwd });
2044
+ ], {
2045
+ cwd,
2046
+ detached: process.platform !== "win32"
2047
+ // Enables PGID on POSIX
2048
+ // detached: true,
2049
+ // shell: true,
2050
+ });
2051
+ let wordPid = null;
2052
+ let onAbort;
2053
+ if (signal) {
2054
+ onAbort = () => {
2055
+ logError("Operation aborted. Terminating process tree...");
2056
+ killProcessTree(child, wordPid);
2057
+ rej(signal.reason ?? new Error("Job timed out or aborted"));
2058
+ };
2059
+ signal.addEventListener("abort", onAbort, { once: true });
2060
+ }
1923
2061
  child.stdout.on("data", (data) => {
1924
- data = `${data}`;
1925
- const m = /\[\*(\d)\]/.exec(data);
2062
+ const raw = `${data}`;
2063
+ const pidMatch = /\[WordPID:(\d+)\]/.exec(raw);
2064
+ if (pidMatch) wordPid = parseInt(pidMatch[1], 10);
2065
+ const m = /\[\*(\d)\]/.exec(raw);
1926
2066
  if (m) progress(12, {
1927
2067
  // "1": "Fixing breaks (listings)",
1928
2068
  "1": render_fixingBreaks(),
@@ -1931,31 +2071,115 @@ function runDocxMacro(progress, log, logError, assets, cwd, fin, fout, renderPDF
1931
2071
  // "3": "Rendering to PDF",
1932
2072
  "3": render_renderPDF()
1933
2073
  }[m[1]] || "Make some work");
1934
- log(data);
2074
+ log(raw);
1935
2075
  });
1936
2076
  let ok = true;
1937
2077
  child.stderr.on("data", (data) => {
1938
2078
  ok = false;
1939
2079
  logError(`${data}`);
1940
2080
  });
1941
- child.on("close", () => res(ok));
1942
- child.on("error", rej);
2081
+ child.on("close", (code) => {
2082
+ if (signal && onAbort) {
2083
+ signal.removeEventListener("abort", onAbort);
2084
+ }
2085
+ res(ok && code === 0);
2086
+ });
2087
+ child.on("error", (err) => {
2088
+ if (signal && onAbort) {
2089
+ signal.removeEventListener("abort", onAbort);
2090
+ }
2091
+ rej(err);
2092
+ });
1943
2093
  });
1944
2094
  }
1945
- async function mergePDFs(files, fout, doc) {
2095
+ function getPythonExecutable() {
2096
+ if (process.platform === "win32") {
2097
+ const loPython = "C:\\Program Files\\LibreOffice\\program\\python.exe";
2098
+ if ((0, import_fs.existsSync)(loPython)) return loPython;
2099
+ }
2100
+ return process.env.UNO_PYTHON_PATH || "python3";
2101
+ }
2102
+ function runUnoScript(progress, log, logError, assets, cwd, fin, fout, renderPDF, signal) {
2103
+ const script = process.env.MACRO_ENGINE_PATH || import_path3.default.join(assets, "macro_engine.py");
2104
+ const pythonExec = getPythonExecutable();
2105
+ return new Promise((res, rej) => {
2106
+ if (signal?.aborted) {
2107
+ return rej(signal.reason ?? new Error("Aborted before execution started"));
2108
+ }
2109
+ const args = [
2110
+ script,
2111
+ "--input",
2112
+ fin,
2113
+ "--output",
2114
+ fout
2115
+ ];
2116
+ if (renderPDF) args.push("--render-pdf");
2117
+ const child = (0, import_child_process.spawn)(pythonExec, args, {
2118
+ cwd,
2119
+ detached: process.platform !== "win32"
2120
+ // Enables PGID on POSIX
2121
+ });
2122
+ let onAbort;
2123
+ if (signal) {
2124
+ onAbort = () => {
2125
+ logError("Operation aborted. Terminating process tree...");
2126
+ killProcessTree(child);
2127
+ rej(signal.reason ?? new Error("Job timed out or aborted"));
2128
+ };
2129
+ signal.addEventListener("abort", onAbort, { once: true });
2130
+ }
2131
+ child.stdout.on("data", (data) => {
2132
+ const lines = `${data}`.split("\n");
2133
+ for (const line of lines) {
2134
+ log(line.trimEnd());
2135
+ if (!line.trimEnd()) continue;
2136
+ const m = /\[\*(\d)\]/.exec(line);
2137
+ if (m) {
2138
+ const msg = {
2139
+ "1": render_fixingBreaks(),
2140
+ "2": render_fixingBreaks(),
2141
+ "3": render_renderPDF()
2142
+ }[m[1]] || "Processing document...";
2143
+ progress(12, msg);
2144
+ }
2145
+ }
2146
+ });
2147
+ let ok = true;
2148
+ child.stderr.on("data", (data) => {
2149
+ ok = false;
2150
+ logError(`${data}`);
2151
+ });
2152
+ child.on("close", (code) => {
2153
+ if (signal && onAbort) {
2154
+ signal.removeEventListener("abort", onAbort);
2155
+ }
2156
+ res(ok && code === 0);
2157
+ });
2158
+ child.on("error", (err) => {
2159
+ if (signal && onAbort) {
2160
+ signal.removeEventListener("abort", onAbort);
2161
+ }
2162
+ rej(err);
2163
+ });
2164
+ });
2165
+ }
2166
+ async function mergePDFs(files, fout, doc, signal) {
1946
2167
  const mergedPdf = await import_pdf_lib.PDFDocument.create();
1947
2168
  if (doc?.title) mergedPdf.setTitle(doc.title, { showInWindowTitleBar: true });
1948
2169
  if (doc?.author) mergedPdf.setAuthor(doc.author);
1949
2170
  if (doc?.ctime) mergedPdf.setCreationDate(doc.ctime);
1950
2171
  if (doc?.mtime) mergedPdf.setModificationDate(doc.mtime);
1951
2172
  for (const file of files) {
1952
- const bytes = await import_promises3.default.readFile(file);
2173
+ if (!file.endsWith(".pdf")) continue;
2174
+ const bytes = await import_promises3.default.readFile(file, { signal });
1953
2175
  const pdf = await import_pdf_lib.PDFDocument.load(bytes);
2176
+ signal?.throwIfAborted();
1954
2177
  const pages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
1955
2178
  pages.forEach((page) => mergedPdf.addPage(page));
2179
+ signal?.throwIfAborted();
1956
2180
  }
1957
2181
  const pdfBytes = await mergedPdf.save();
1958
- await import_promises3.default.writeFile(fout, pdfBytes);
2182
+ await import_promises3.default.writeFile(fout, pdfBytes, { signal });
1959
2183
  }
1960
2184
  async function updateMetadata(docfile, doc) {
1961
2185
  const content = await import_promises3.default.readFile(docfile);
@@ -2047,17 +2271,32 @@ async function renderMarkdown(config) {
2047
2271
  assert(config.format == "docx" || config.format == "pdf" || typeof config.format == "undefined", "Format must be either 'docx' or 'pdf', or undefined.");
2048
2272
  assert(typeof config.keepIntermediateDocx == "boolean" || typeof config.keepIntermediateDocx == "undefined", "keepIntermediateDocx must be a boolean or undefined.");
2049
2273
  assert(typeof config.disableMacros == "boolean" || typeof config.disableMacros == "undefined", "disableMacros must be a boolean or undefined.");
2274
+ assert(typeof config.useLibreOffice == "boolean" || typeof config.useLibreOffice == "undefined", "useLibreOffice must be a boolean or undefined.");
2275
+ assert(typeof config.allowExternalFiles == "boolean" || typeof config.allowExternalFiles == "undefined", "allowExternalFiles must be a boolean or undefined.");
2050
2276
  assert(typeof config.progress == "function" || typeof config.progress == "undefined", "Progress callback must be a function or undefined.");
2277
+ assert(config.abortSignal instanceof AbortSignal || typeof config.abortSignal == "undefined", "abortSignal must be an AbortSignal or undefined.");
2051
2278
  {
2052
2279
  const extraProps = getExtraProperties(
2053
2280
  config,
2054
- ["input", "output", "format", "keepIntermediateDocx", "disableMacros", "progress"]
2281
+ ["input", "output", "format", "keepIntermediateDocx", "disableMacros", "useLibreOffice", "progress", "logger", "abortSignal", "allowExternalFiles"]
2055
2282
  );
2056
2283
  assert(extraProps.length == 0, `Found unknown properties in config: ${extraProps.join(", ")}`);
2057
2284
  }
2285
+ assert(typeof config.logger == "object" && config.logger || typeof config.logger == "undefined", "Logger must be a object or undefined.");
2286
+ const logger = config.logger;
2287
+ if (logger) {
2288
+ const props = ["info", "warn", "error"];
2289
+ const extraProps = getExtraProperties(logger, props);
2290
+ assert(extraProps.length == 0, `Found unknown properties in config.logger: ${extraProps.join(", ")}`);
2291
+ props.forEach(
2292
+ (p) => assert(typeof logger[p] == "function", `config.logger.${p} must be a function.`)
2293
+ );
2294
+ }
2058
2295
  const renderPDF = config.format === "pdf" || !config.format && !!config.output?.endsWith(".pdf");
2059
2296
  const removeIntermediateDocx = !config.keepIntermediateDocx;
2060
2297
  const disableMacros = !!config.disableMacros;
2298
+ const useLibreOffice = !!config.useLibreOffice;
2299
+ const allowExternalFiles = !!config.allowExternalFiles;
2061
2300
  assert(!(disableMacros && renderPDF), "Macros must be enabled to render PDF output.");
2062
2301
  const pin = import_path4.default.parse(import_path4.default.resolve(config.input));
2063
2302
  const fname = pin.name.endsWith(".g") ? pin.name.slice(0, -2) : pin.name;
@@ -2072,10 +2311,10 @@ async function renderMarkdown(config) {
2072
2311
  const warnings = [];
2073
2312
  const logPS = [];
2074
2313
  let totalPercent = 0;
2075
- const handleProgress = config.progress ? (increment, message) => {
2314
+ const handleProgress = (increment, message) => {
2076
2315
  totalPercent = Math.min(totalPercent + increment, 99);
2077
- config.progress(totalPercent, message);
2078
- } : void 0;
2316
+ config.progress?.(totalPercent, message);
2317
+ };
2079
2318
  try {
2080
2319
  let throwMDE2 = function(code, msg) {
2081
2320
  throw new MDRenderError(code, msg, fout, warnings, logPS, errS ? { cause: errS } : void 0);
@@ -2089,13 +2328,31 @@ async function renderMarkdown(config) {
2089
2328
  renderPDF,
2090
2329
  removeIntermediateDocx,
2091
2330
  disableMacros,
2092
- logwarn: (msg) => warnings.push(msg),
2093
- logPS: (msg) => logPS.push(msg),
2094
- logPSError: (msg) => logPS.push(msg)
2331
+ useLibreOffice,
2332
+ allowExternalFiles,
2333
+ loginfo: logger?.info,
2334
+ logwarn: (msg) => {
2335
+ logger?.warn(msg);
2336
+ warnings.push(msg);
2337
+ },
2338
+ logPS: (msg) => {
2339
+ logger?.info(msg);
2340
+ logPS.push(msg);
2341
+ },
2342
+ logPSError: (msg) => {
2343
+ logger?.error(msg);
2344
+ logPS.push(msg);
2345
+ },
2346
+ signal: config.abortSignal
2095
2347
  });
2096
2348
  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.`);
2349
+ if (useLibreOffice) {
2350
+ 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.`);
2351
+ 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.`);
2352
+ } else {
2353
+ if (err == "noPS") throwMDE2("noPS", `Failed to initialize PowerShell. Ensure PowerShell is installed and explicitly added to your system's PATH environment variable.`);
2354
+ if (err == "inPS") throwMDE2("inPS", `An unexpected error occurred executing commands inside PowerShell. Verify that Microsoft Word is properly installed and licensed.`);
2355
+ }
2099
2356
  if (err == "vba") throwMDE2("vba", `Microsoft Word VBA Macro execution failed: ${errS}`);
2100
2357
  if (err == "pdf") throwMDE2("pdf", `Failed to export document to PDF.`);
2101
2358
  if (err == "noWin") throwMDE2("noWin", `Platform restriction: Extended formatting macros and PDF rendering are only supported natively on Windows systems.`);
@@ -2111,6 +2368,7 @@ async function renderMarkdown(config) {
2111
2368
  };
2112
2369
  } catch (x) {
2113
2370
  if (x instanceof MDRenderError) throw x;
2371
+ if (x instanceof UserInputError) throw new MDRenderError("userInput", x.message, null, warnings, logPS, { cause: x });
2114
2372
  const message = x instanceof Error ? x.message : String(x);
2115
2373
  throw new MDRenderError("unknown", `An unhandled internal error occurred during rendering: ${message}`, null, warnings, logPS, { cause: x });
2116
2374
  }
@@ -2123,6 +2381,7 @@ var MDRenderError = class _MDRenderError extends Error {
2123
2381
  /**
2124
2382
  * Code indicating the underlying cause of the failure:
2125
2383
  * - `unknown`: An unhandled or unexpected internal runtime error.
2384
+ * - `userInput`: Invalid user input or Markdown document structure.
2126
2385
  * - `noPS`: PowerShell could not be started or found in the system PATH.
2127
2386
  * - `inPS`: A failure occurred inside PowerShell; MS Word may be missing or corrupt.
2128
2387
  * - `vba`: An error occurred during the execution of internal Word VBA macros.