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/CHANGELOG.md +18 -0
- package/README.md +35 -9
- package/assets/run.ps1 +15 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.js +300 -41
- package/dist/index.js.map +2 -2
- package/dist/index.mjs +302 -43
- package/dist/index.mjs.map +3 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
1168
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
], {
|
|
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
|
-
|
|
1890
|
-
const
|
|
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(
|
|
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", () =>
|
|
1907
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2279
|
+
const handleProgress = (increment, message) => {
|
|
2041
2280
|
totalPercent = Math.min(totalPercent + increment, 99);
|
|
2042
|
-
config.progress(totalPercent, message);
|
|
2043
|
-
}
|
|
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
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
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 (
|
|
2063
|
-
|
|
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.
|