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