@dunkinfrunkin/mdcat 0.1.7 → 0.1.9
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/README.md +4 -1
- package/package.json +2 -1
- package/src/cli.js +22 -4
- package/src/docx.js +515 -0
package/README.md
CHANGED
|
@@ -17,9 +17,12 @@ npm install -g @dunkinfrunkin/mdcat
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
|
-
#
|
|
20
|
+
# npm (global)
|
|
21
21
|
npm install -g @dunkinfrunkin/mdcat
|
|
22
22
|
|
|
23
|
+
# Homebrew
|
|
24
|
+
brew install dunkinfrunkin/tap/mdcat
|
|
25
|
+
|
|
23
26
|
# Zero-install
|
|
24
27
|
npx @dunkinfrunkin/mdcat README.md
|
|
25
28
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dunkinfrunkin/mdcat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "View markdown files beautifully in your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"chalk": "^5.6.2",
|
|
18
18
|
"cli-highlight": "^2.1.11",
|
|
19
|
+
"docx": "^9.6.0",
|
|
19
20
|
"marked": "^14.0.0",
|
|
20
21
|
"wrap-ansi": "^10.0.0"
|
|
21
22
|
},
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { execFileSync } from "child_process";
|
|
|
6
6
|
import { marked } from "marked";
|
|
7
7
|
import { renderTokens } from "./render.js";
|
|
8
8
|
import { launch } from "./tui.js";
|
|
9
|
+
import { toDocx } from "./docx.js";
|
|
9
10
|
|
|
10
11
|
marked.use({ gfm: true });
|
|
11
12
|
|
|
@@ -27,6 +28,7 @@ if (args[0] === "--help" || args[0] === "-h") {
|
|
|
27
28
|
console.log(`${bold("Usage:")}`);
|
|
28
29
|
console.log(` mdcat ${dim("<file.md>")}`);
|
|
29
30
|
console.log(` mdcat ${dim("--web <file.md>")} ${dim("# open in browser")}`);
|
|
31
|
+
console.log(` mdcat ${dim("--doc <file.md>")} ${dim("# export to .docx")}`);
|
|
30
32
|
console.log(` cat file.md ${dim("|")} mdcat\n`);
|
|
31
33
|
console.log(`${bold("Keys:")}`);
|
|
32
34
|
console.log(` ${blue("/")} search ${blue("n/N")} next/prev match`);
|
|
@@ -102,16 +104,30 @@ function runTUI(title, content) {
|
|
|
102
104
|
launch(title, lines);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
// --web
|
|
107
|
+
// --web / --doc flags
|
|
106
108
|
const webMode = args[0] === "--web" || args[0] === "-w";
|
|
107
|
-
const
|
|
109
|
+
const docMode = args[0] === "--doc" || args[0] === "-d";
|
|
110
|
+
const fileArgs = (webMode || docMode) ? args.slice(1) : args;
|
|
111
|
+
|
|
112
|
+
async function exportDocx(title, content) {
|
|
113
|
+
const tokens = marked.lexer(content);
|
|
114
|
+
const buf = await toDocx(tokens, title);
|
|
115
|
+
const outName = title.replace(/\.md$/i, "") + ".docx";
|
|
116
|
+
const outPath = resolve(outName);
|
|
117
|
+
writeFileSync(outPath, buf);
|
|
118
|
+
console.log(`${CAT} ${bold("mdcat")} ${dim("→")} ${blue(outPath)}`);
|
|
119
|
+
}
|
|
108
120
|
|
|
109
121
|
// Piped input
|
|
110
122
|
if (!process.stdin.isTTY && fileArgs.length === 0) {
|
|
111
123
|
let input = "";
|
|
112
124
|
process.stdin.setEncoding("utf8");
|
|
113
125
|
process.stdin.on("data", (chunk) => (input += chunk));
|
|
114
|
-
process.stdin.on("end", () =>
|
|
126
|
+
process.stdin.on("end", () => {
|
|
127
|
+
if (docMode) exportDocx("stdin", input);
|
|
128
|
+
else if (webMode) openInBrowser("stdin", input);
|
|
129
|
+
else runTUI("stdin", input);
|
|
130
|
+
});
|
|
115
131
|
} else if (fileArgs.length === 0) {
|
|
116
132
|
console.error("Usage: mdcat <file.md>");
|
|
117
133
|
process.exit(1);
|
|
@@ -125,5 +141,7 @@ if (!process.stdin.isTTY && fileArgs.length === 0) {
|
|
|
125
141
|
process.exit(1);
|
|
126
142
|
}
|
|
127
143
|
const title = basename(filePath);
|
|
128
|
-
|
|
144
|
+
if (docMode) exportDocx(title, content);
|
|
145
|
+
else if (webMode) openInBrowser(title, content);
|
|
146
|
+
else runTUI(title, content);
|
|
129
147
|
}
|
package/src/docx.js
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Document, Packer, Paragraph, TextRun, HeadingLevel,
|
|
3
|
+
Table, TableRow, TableCell, WidthType, BorderStyle,
|
|
4
|
+
AlignmentType, ExternalHyperlink, TabStopPosition, TabStopType,
|
|
5
|
+
ShadingType, TableLayoutType, Footer, Header,
|
|
6
|
+
} from "docx";
|
|
7
|
+
|
|
8
|
+
// ─── Colors (Word-native blues/grays) ──────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const COLOR = {
|
|
11
|
+
heading1: "1F3864", // dark navy
|
|
12
|
+
heading2: "2E74B5", // medium blue
|
|
13
|
+
heading3: "2E74B5",
|
|
14
|
+
heading4: "404040",
|
|
15
|
+
link: "2E74B5",
|
|
16
|
+
code: "D63384", // magenta for inline code
|
|
17
|
+
codeBg: "F6F8FA",
|
|
18
|
+
tableHeader:"2E74B5",
|
|
19
|
+
tableHeaderBg: "D9E2F3",
|
|
20
|
+
tableAltBg: "F2F2F2",
|
|
21
|
+
tableBorder:"BFBFBF",
|
|
22
|
+
blockquoteBorder: "2E74B5",
|
|
23
|
+
blockquoteText: "595959",
|
|
24
|
+
taskGreen: "2D8A4E",
|
|
25
|
+
taskGray: "999999",
|
|
26
|
+
body: "333333",
|
|
27
|
+
muted: "808080",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ─── Inline token → TextRun[] / ExternalHyperlink[] ─────────────────────────
|
|
31
|
+
|
|
32
|
+
function inlineRuns(tokens, opts = {}) {
|
|
33
|
+
if (!tokens?.length) return [];
|
|
34
|
+
const runs = [];
|
|
35
|
+
|
|
36
|
+
for (const tok of tokens) {
|
|
37
|
+
switch (tok.type) {
|
|
38
|
+
case "text":
|
|
39
|
+
if (tok.tokens) {
|
|
40
|
+
runs.push(...inlineRuns(tok.tokens, opts));
|
|
41
|
+
} else {
|
|
42
|
+
runs.push(new TextRun({ text: htmlDecode(tok.text ?? ""), color: COLOR.body, ...opts }));
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
case "strong":
|
|
46
|
+
runs.push(...inlineRuns(tok.tokens, { ...opts, bold: true }));
|
|
47
|
+
break;
|
|
48
|
+
case "em":
|
|
49
|
+
runs.push(...inlineRuns(tok.tokens, { ...opts, italics: true }));
|
|
50
|
+
break;
|
|
51
|
+
case "del":
|
|
52
|
+
runs.push(...inlineRuns(tok.tokens, { ...opts, strike: true, color: COLOR.muted }));
|
|
53
|
+
break;
|
|
54
|
+
case "codespan":
|
|
55
|
+
runs.push(new TextRun({
|
|
56
|
+
text: tok.text ?? "",
|
|
57
|
+
font: "Consolas",
|
|
58
|
+
size: 20,
|
|
59
|
+
color: COLOR.code,
|
|
60
|
+
shading: { type: ShadingType.CLEAR, fill: COLOR.codeBg },
|
|
61
|
+
...opts,
|
|
62
|
+
}));
|
|
63
|
+
break;
|
|
64
|
+
case "link": {
|
|
65
|
+
// Recursively get inline runs for the link label to preserve bold/italic/etc
|
|
66
|
+
const labelRuns = tok.tokens?.length
|
|
67
|
+
? inlineRuns(tok.tokens, { ...opts, color: COLOR.link, underline: { type: "single" } })
|
|
68
|
+
: [new TextRun({ text: tok.href ?? "", color: COLOR.link, underline: { type: "single" }, ...opts })];
|
|
69
|
+
runs.push(new ExternalHyperlink({
|
|
70
|
+
children: labelRuns,
|
|
71
|
+
link: tok.href ?? "",
|
|
72
|
+
}));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "image":
|
|
76
|
+
runs.push(new TextRun({
|
|
77
|
+
text: `[${tok.text || tok.alt || "image"}]`,
|
|
78
|
+
italics: true,
|
|
79
|
+
color: COLOR.muted,
|
|
80
|
+
...opts,
|
|
81
|
+
}));
|
|
82
|
+
break;
|
|
83
|
+
case "br":
|
|
84
|
+
runs.push(new TextRun({ break: 1 }));
|
|
85
|
+
break;
|
|
86
|
+
case "escape":
|
|
87
|
+
runs.push(new TextRun({ text: tok.text ?? "", color: COLOR.body, ...opts }));
|
|
88
|
+
break;
|
|
89
|
+
default:
|
|
90
|
+
if (tok.raw) runs.push(new TextRun({ text: tok.raw, color: COLOR.body, ...opts }));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return runs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── HTML entity decoder ────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function htmlDecode(s) {
|
|
101
|
+
return s
|
|
102
|
+
.replace(/&/g, "&")
|
|
103
|
+
.replace(/</g, "<")
|
|
104
|
+
.replace(/>/g, ">")
|
|
105
|
+
.replace(/"/g, '"')
|
|
106
|
+
.replace(/'/g, "'")
|
|
107
|
+
.replace(/ /g, " ");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Block token → Paragraph[] ──────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function blockToParagraphs(tok) {
|
|
113
|
+
const out = [];
|
|
114
|
+
|
|
115
|
+
switch (tok.type) {
|
|
116
|
+
case "heading": {
|
|
117
|
+
const headingMap = {
|
|
118
|
+
1: HeadingLevel.HEADING_1,
|
|
119
|
+
2: HeadingLevel.HEADING_2,
|
|
120
|
+
3: HeadingLevel.HEADING_3,
|
|
121
|
+
4: HeadingLevel.HEADING_4,
|
|
122
|
+
5: HeadingLevel.HEADING_5,
|
|
123
|
+
6: HeadingLevel.HEADING_6,
|
|
124
|
+
};
|
|
125
|
+
const colorMap = {
|
|
126
|
+
1: COLOR.heading1,
|
|
127
|
+
2: COLOR.heading2,
|
|
128
|
+
3: COLOR.heading3,
|
|
129
|
+
4: COLOR.heading4,
|
|
130
|
+
5: COLOR.heading4,
|
|
131
|
+
6: COLOR.muted,
|
|
132
|
+
};
|
|
133
|
+
const runs = inlineRuns(tok.tokens).map(r => {
|
|
134
|
+
// Override color for heading runs
|
|
135
|
+
if (r instanceof TextRun) {
|
|
136
|
+
return new TextRun({
|
|
137
|
+
...extractRunProps(r),
|
|
138
|
+
color: colorMap[tok.depth] ?? COLOR.body,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return r;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
out.push(new Paragraph({
|
|
145
|
+
heading: headingMap[tok.depth] ?? HeadingLevel.HEADING_1,
|
|
146
|
+
children: runs,
|
|
147
|
+
spacing: { before: 240, after: 120 },
|
|
148
|
+
...(tok.depth <= 2 ? {
|
|
149
|
+
border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "D9D9D9", space: 4 } },
|
|
150
|
+
} : {}),
|
|
151
|
+
}));
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
case "paragraph":
|
|
156
|
+
out.push(new Paragraph({
|
|
157
|
+
children: inlineRuns(tok.tokens),
|
|
158
|
+
spacing: { after: 160, line: 300 },
|
|
159
|
+
}));
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "code": {
|
|
163
|
+
const lang = (tok.lang ?? "").split(/\s/)[0].trim();
|
|
164
|
+
const lines = (tok.text ?? "").split("\n");
|
|
165
|
+
|
|
166
|
+
// Language label
|
|
167
|
+
if (lang) {
|
|
168
|
+
out.push(new Paragraph({
|
|
169
|
+
children: [new TextRun({
|
|
170
|
+
text: lang.toUpperCase(),
|
|
171
|
+
font: "Consolas",
|
|
172
|
+
size: 16,
|
|
173
|
+
color: COLOR.muted,
|
|
174
|
+
bold: true,
|
|
175
|
+
})],
|
|
176
|
+
spacing: { before: 200, after: 40 },
|
|
177
|
+
}));
|
|
178
|
+
} else {
|
|
179
|
+
out.push(new Paragraph({ spacing: { before: 120 } }));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Code lines in a shaded block
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
out.push(new Paragraph({
|
|
185
|
+
children: [new TextRun({
|
|
186
|
+
text: lines[i] || " ",
|
|
187
|
+
font: "Consolas",
|
|
188
|
+
size: 19,
|
|
189
|
+
color: COLOR.body,
|
|
190
|
+
})],
|
|
191
|
+
shading: { type: ShadingType.CLEAR, fill: COLOR.codeBg },
|
|
192
|
+
spacing: { after: 0, line: 260 },
|
|
193
|
+
indent: { left: 240, right: 240 },
|
|
194
|
+
...(i === 0 ? { border: { top: { style: BorderStyle.SINGLE, size: 1, color: "E1E4E8", space: 4 } } } : {}),
|
|
195
|
+
...(i === lines.length - 1 ? { border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "E1E4E8", space: 4 } } } : {}),
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
out.push(new Paragraph({ spacing: { after: 160 } }));
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case "blockquote": {
|
|
203
|
+
for (const t of tok.tokens ?? []) {
|
|
204
|
+
if (t.tokens) {
|
|
205
|
+
const children = inlineRuns(t.tokens).map(r => {
|
|
206
|
+
if (r instanceof TextRun) {
|
|
207
|
+
return new TextRun({ ...extractRunProps(r), color: COLOR.blockquoteText, italics: true });
|
|
208
|
+
}
|
|
209
|
+
return r;
|
|
210
|
+
});
|
|
211
|
+
out.push(new Paragraph({
|
|
212
|
+
children,
|
|
213
|
+
indent: { left: 480 },
|
|
214
|
+
border: { left: { style: BorderStyle.SINGLE, size: 12, color: COLOR.blockquoteBorder, space: 12 } },
|
|
215
|
+
spacing: { after: 80, line: 280 },
|
|
216
|
+
}));
|
|
217
|
+
} else if (t.type === "paragraph" && t.tokens) {
|
|
218
|
+
const children = inlineRuns(t.tokens).map(r => {
|
|
219
|
+
if (r instanceof TextRun) {
|
|
220
|
+
return new TextRun({ ...extractRunProps(r), color: COLOR.blockquoteText, italics: true });
|
|
221
|
+
}
|
|
222
|
+
return r;
|
|
223
|
+
});
|
|
224
|
+
out.push(new Paragraph({
|
|
225
|
+
children,
|
|
226
|
+
indent: { left: 480 },
|
|
227
|
+
border: { left: { style: BorderStyle.SINGLE, size: 12, color: COLOR.blockquoteBorder, space: 12 } },
|
|
228
|
+
spacing: { after: 80, line: 280 },
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
out.push(new Paragraph({ spacing: { after: 80 } }));
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
case "list": {
|
|
237
|
+
const items = tok.items ?? [];
|
|
238
|
+
items.forEach((item, i) => {
|
|
239
|
+
const children = [];
|
|
240
|
+
|
|
241
|
+
if (item.task) {
|
|
242
|
+
const checked = item.checked;
|
|
243
|
+
children.push(new TextRun({
|
|
244
|
+
text: checked ? "✓ " : "○ ",
|
|
245
|
+
font: "Segoe UI Symbol",
|
|
246
|
+
color: checked ? COLOR.taskGreen : COLOR.taskGray,
|
|
247
|
+
bold: checked,
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const t of item.tokens) {
|
|
252
|
+
if (t.type === "text") {
|
|
253
|
+
const taskDone = item.task && item.checked;
|
|
254
|
+
children.push(...inlineRuns(
|
|
255
|
+
t.tokens ?? [{ type: "text", text: t.text ?? "" }],
|
|
256
|
+
taskDone ? { strike: true, color: COLOR.muted } : {},
|
|
257
|
+
));
|
|
258
|
+
} else if (t.type === "paragraph") {
|
|
259
|
+
const taskDone = item.task && item.checked;
|
|
260
|
+
children.push(...inlineRuns(
|
|
261
|
+
t.tokens,
|
|
262
|
+
taskDone ? { strike: true, color: COLOR.muted } : {},
|
|
263
|
+
));
|
|
264
|
+
} else if (t.type === "list") {
|
|
265
|
+
// handled below
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const bullet = tok.ordered
|
|
270
|
+
? new TextRun({ text: `${(tok.start ?? 1) + i}. `, color: COLOR.heading2, bold: true })
|
|
271
|
+
: new TextRun({ text: item.task ? "" : "• ", color: COLOR.heading2 });
|
|
272
|
+
|
|
273
|
+
out.push(new Paragraph({
|
|
274
|
+
children: [bullet, ...children],
|
|
275
|
+
indent: { left: 360 },
|
|
276
|
+
spacing: { after: 60, line: 276 },
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
// Nested lists
|
|
280
|
+
for (const t of item.tokens) {
|
|
281
|
+
if (t.type === "list") {
|
|
282
|
+
const nested = blockToParagraphs(t);
|
|
283
|
+
for (const p of nested) {
|
|
284
|
+
out.push(new Paragraph({
|
|
285
|
+
...extractParaProps(p),
|
|
286
|
+
indent: { left: 720 },
|
|
287
|
+
spacing: { after: 60, line: 276 },
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
out.push(new Paragraph({ spacing: { after: 80 } }));
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case "table": {
|
|
298
|
+
const headers = tok.header ?? [];
|
|
299
|
+
const rows = tok.rows ?? [];
|
|
300
|
+
const aligns = tok.align ?? [];
|
|
301
|
+
|
|
302
|
+
const alignMap = {
|
|
303
|
+
left: AlignmentType.LEFT,
|
|
304
|
+
right: AlignmentType.RIGHT,
|
|
305
|
+
center: AlignmentType.CENTER,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const cellBorder = {
|
|
309
|
+
top: { style: BorderStyle.SINGLE, size: 1, color: COLOR.tableBorder },
|
|
310
|
+
bottom: { style: BorderStyle.SINGLE, size: 1, color: COLOR.tableBorder },
|
|
311
|
+
left: { style: BorderStyle.SINGLE, size: 1, color: COLOR.tableBorder },
|
|
312
|
+
right: { style: BorderStyle.SINGLE, size: 1, color: COLOR.tableBorder },
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Compute column widths from content
|
|
316
|
+
const colWidths = headers.map((h, i) => {
|
|
317
|
+
const hLen = (h.tokens ?? []).map(t => t.text ?? t.raw ?? "").join("").length;
|
|
318
|
+
const maxCell = rows.reduce((m, r) => {
|
|
319
|
+
const cLen = (r[i]?.tokens ?? []).map(t => t.text ?? t.raw ?? "").join("").length;
|
|
320
|
+
return Math.max(m, cLen);
|
|
321
|
+
}, 0);
|
|
322
|
+
return Math.max(hLen, maxCell, 3);
|
|
323
|
+
});
|
|
324
|
+
const totalW = colWidths.reduce((s, w) => s + w, 0) || 1;
|
|
325
|
+
const colPcts = colWidths.map(w => Math.max(8, Math.round((w / totalW) * 100)));
|
|
326
|
+
|
|
327
|
+
// Header row
|
|
328
|
+
const headerCells = headers.map((h, i) => new TableCell({
|
|
329
|
+
children: [new Paragraph({
|
|
330
|
+
children: inlineRuns(h.tokens ?? []).map(r => {
|
|
331
|
+
if (r instanceof TextRun) {
|
|
332
|
+
return new TextRun({ ...extractRunProps(r), bold: true, color: "FFFFFF" });
|
|
333
|
+
}
|
|
334
|
+
return r;
|
|
335
|
+
}),
|
|
336
|
+
alignment: alignMap[aligns[i]] ?? AlignmentType.LEFT,
|
|
337
|
+
spacing: { before: 40, after: 40 },
|
|
338
|
+
})],
|
|
339
|
+
width: { size: colPcts[i], type: WidthType.PERCENTAGE },
|
|
340
|
+
shading: { type: ShadingType.CLEAR, fill: COLOR.tableHeader },
|
|
341
|
+
borders: cellBorder,
|
|
342
|
+
margins: { top: 40, bottom: 40, left: 80, right: 80 },
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
const tableRows = [new TableRow({ children: headerCells, tableHeader: true })];
|
|
346
|
+
|
|
347
|
+
// Data rows with alternating shading
|
|
348
|
+
rows.forEach((row, rowIdx) => {
|
|
349
|
+
const cells = row.map((cell, i) => new TableCell({
|
|
350
|
+
children: [new Paragraph({
|
|
351
|
+
children: inlineRuns(cell.tokens ?? []),
|
|
352
|
+
alignment: alignMap[aligns[i]] ?? AlignmentType.LEFT,
|
|
353
|
+
spacing: { before: 20, after: 20 },
|
|
354
|
+
})],
|
|
355
|
+
width: { size: colPcts[i], type: WidthType.PERCENTAGE },
|
|
356
|
+
shading: rowIdx % 2 === 1
|
|
357
|
+
? { type: ShadingType.CLEAR, fill: COLOR.tableAltBg }
|
|
358
|
+
: undefined,
|
|
359
|
+
borders: cellBorder,
|
|
360
|
+
margins: { top: 20, bottom: 20, left: 80, right: 80 },
|
|
361
|
+
}));
|
|
362
|
+
tableRows.push(new TableRow({ children: cells }));
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
out.push(new Table({
|
|
366
|
+
rows: tableRows,
|
|
367
|
+
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
368
|
+
layout: TableLayoutType.FIXED,
|
|
369
|
+
columnWidths: colPcts.map(p => Math.round(p * 90)),
|
|
370
|
+
}));
|
|
371
|
+
out.push(new Paragraph({ spacing: { after: 200 } }));
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case "hr":
|
|
376
|
+
out.push(new Paragraph({
|
|
377
|
+
border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "D9D9D9" } },
|
|
378
|
+
spacing: { before: 240, after: 240 },
|
|
379
|
+
}));
|
|
380
|
+
break;
|
|
381
|
+
|
|
382
|
+
case "space":
|
|
383
|
+
case "html":
|
|
384
|
+
case "def":
|
|
385
|
+
break;
|
|
386
|
+
|
|
387
|
+
default:
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ─── Helpers to extract properties from existing objects ─────────────────────
|
|
395
|
+
|
|
396
|
+
function extractRunProps(run) {
|
|
397
|
+
// TextRun stores props internally; we reconstruct from the XML root
|
|
398
|
+
const props = {};
|
|
399
|
+
const root = run.root;
|
|
400
|
+
if (!root) return props;
|
|
401
|
+
|
|
402
|
+
// Walk the internal representation to pull out text and formatting
|
|
403
|
+
// This is a simplified extraction — we pass through common props
|
|
404
|
+
for (const child of root) {
|
|
405
|
+
if (typeof child === "string") continue;
|
|
406
|
+
if (child?.rootKey === "w:t") {
|
|
407
|
+
props.text = child.root?.[1] ?? "";
|
|
408
|
+
}
|
|
409
|
+
if (child?.rootKey === "w:rPr") {
|
|
410
|
+
for (const pr of child.root ?? []) {
|
|
411
|
+
if (pr?.rootKey === "w:b") props.bold = true;
|
|
412
|
+
if (pr?.rootKey === "w:i") props.italics = true;
|
|
413
|
+
if (pr?.rootKey === "w:strike") props.strike = true;
|
|
414
|
+
if (pr?.rootKey === "w:color") {
|
|
415
|
+
const val = pr.root?.find(a => a?.rootKey === "w:val" || a?._attr?.["w:val"]);
|
|
416
|
+
if (val?._attr?.["w:val"]) props.color = val._attr["w:val"];
|
|
417
|
+
}
|
|
418
|
+
if (pr?.rootKey === "w:rFonts") {
|
|
419
|
+
const val = pr.root?.find(a => a?._attr?.["w:ascii"]);
|
|
420
|
+
if (val?._attr?.["w:ascii"]) props.font = val._attr["w:ascii"];
|
|
421
|
+
}
|
|
422
|
+
if (pr?.rootKey === "w:sz") {
|
|
423
|
+
const val = pr.root?.find(a => a?._attr?.["w:val"]);
|
|
424
|
+
if (val?._attr?.["w:val"]) props.size = parseInt(val._attr["w:val"]);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return props;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function extractParaProps(para) {
|
|
433
|
+
// Return a minimal representation for re-creating paragraphs
|
|
434
|
+
return {
|
|
435
|
+
children: para.root?.[1]?.root?.filter(r => r instanceof TextRun || r instanceof ExternalHyperlink) ?? [],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Convert marked tokens to a .docx buffer.
|
|
443
|
+
*/
|
|
444
|
+
export async function toDocx(tokens, title = "Document") {
|
|
445
|
+
const children = tokens.flatMap(tok => blockToParagraphs(tok));
|
|
446
|
+
|
|
447
|
+
const doc = new Document({
|
|
448
|
+
title,
|
|
449
|
+
styles: {
|
|
450
|
+
default: {
|
|
451
|
+
document: {
|
|
452
|
+
run: { size: 24, font: "Calibri", color: COLOR.body },
|
|
453
|
+
paragraph: { spacing: { after: 160, line: 300 } },
|
|
454
|
+
},
|
|
455
|
+
heading1: {
|
|
456
|
+
run: { size: 36, font: "Calibri", bold: true, color: COLOR.heading1 },
|
|
457
|
+
paragraph: { spacing: { before: 360, after: 120 } },
|
|
458
|
+
},
|
|
459
|
+
heading2: {
|
|
460
|
+
run: { size: 30, font: "Calibri", bold: true, color: COLOR.heading2 },
|
|
461
|
+
paragraph: { spacing: { before: 280, after: 100 } },
|
|
462
|
+
},
|
|
463
|
+
heading3: {
|
|
464
|
+
run: { size: 26, font: "Calibri", bold: true, color: COLOR.heading3 },
|
|
465
|
+
paragraph: { spacing: { before: 240, after: 80 } },
|
|
466
|
+
},
|
|
467
|
+
heading4: {
|
|
468
|
+
run: { size: 24, font: "Calibri", bold: true, color: COLOR.heading4 },
|
|
469
|
+
paragraph: { spacing: { before: 200, after: 80 } },
|
|
470
|
+
},
|
|
471
|
+
heading5: {
|
|
472
|
+
run: { size: 22, font: "Calibri", italics: true, color: COLOR.heading4 },
|
|
473
|
+
paragraph: { spacing: { before: 160, after: 60 } },
|
|
474
|
+
},
|
|
475
|
+
heading6: {
|
|
476
|
+
run: { size: 22, font: "Calibri", color: COLOR.muted },
|
|
477
|
+
paragraph: { spacing: { before: 160, after: 60 } },
|
|
478
|
+
},
|
|
479
|
+
hyperlink: {
|
|
480
|
+
run: { color: COLOR.link, underline: { type: "single" } },
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
sections: [{
|
|
485
|
+
properties: {
|
|
486
|
+
page: {
|
|
487
|
+
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
footers: {
|
|
491
|
+
default: new Footer({
|
|
492
|
+
children: [new Paragraph({
|
|
493
|
+
alignment: AlignmentType.CENTER,
|
|
494
|
+
children: [
|
|
495
|
+
new TextRun({ text: "Created with ", color: COLOR.muted, size: 16, font: "Calibri" }),
|
|
496
|
+
new ExternalHyperlink({
|
|
497
|
+
children: [new TextRun({
|
|
498
|
+
text: "mdcat",
|
|
499
|
+
color: COLOR.link,
|
|
500
|
+
underline: { type: "single" },
|
|
501
|
+
size: 16,
|
|
502
|
+
font: "Calibri",
|
|
503
|
+
})],
|
|
504
|
+
link: "https://mdcat.frankchan.dev",
|
|
505
|
+
}),
|
|
506
|
+
],
|
|
507
|
+
})],
|
|
508
|
+
}),
|
|
509
|
+
},
|
|
510
|
+
children,
|
|
511
|
+
}],
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
return await Packer.toBuffer(doc);
|
|
515
|
+
}
|