bridgerator 1.1.4 → 1.1.5

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.
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "bridgerator",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "bridgerator",
9
- "version": "1.1.4",
9
+ "version": "1.1.5",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "^1.29.0",
13
- "@wonderwhy-er/desktop-commander": "0.2.52",
13
+ "bridgerator-engine": "npm:@wonderwhy-er/desktop-commander@0.2.52",
14
14
  "ws": "^8.20.0"
15
15
  },
16
16
  "bin": {
@@ -1477,69 +1477,6 @@
1477
1477
  "win32"
1478
1478
  ]
1479
1479
  },
1480
- "node_modules/@wonderwhy-er/desktop-commander": {
1481
- "version": "0.2.52",
1482
- "resolved": "https://registry.npmjs.org/@wonderwhy-er/desktop-commander/-/desktop-commander-0.2.52.tgz",
1483
- "integrity": "sha512-VNeKfaBR6TN/8MlP/ziTpjNEeHrGOFmmzQjrwzr52kQalBJoNNISWwaSQElPFc6+I17NOOM354KbSdS/aDrqGA==",
1484
- "hasInstallScript": true,
1485
- "license": "MIT",
1486
- "dependencies": {
1487
- "@modelcontextprotocol/sdk": "^1.9.0",
1488
- "@opendocsg/pdf2md": "^0.2.2",
1489
- "@supabase/supabase-js": "^2.89.0",
1490
- "@tiptap/core": "^3.22.3",
1491
- "@tiptap/extension-image": "^3.22.3",
1492
- "@tiptap/extension-table": "^3.22.4",
1493
- "@tiptap/extension-table-cell": "^3.22.4",
1494
- "@tiptap/extension-table-header": "^3.22.4",
1495
- "@tiptap/extension-table-row": "^3.22.4",
1496
- "@tiptap/pm": "^3.22.3",
1497
- "@tiptap/starter-kit": "^3.22.3",
1498
- "@vscode/ripgrep": "^1.15.9",
1499
- "cross-fetch": "^4.1.0",
1500
- "exceljs": "^4.4.0",
1501
- "fastest-levenshtein": "^1.0.16",
1502
- "file-type": "^21.1.1",
1503
- "glob": "^10.3.10",
1504
- "highlight.js": "^11.11.1",
1505
- "isbinaryfile": "^5.0.4",
1506
- "markdown-it": "^14.1.0",
1507
- "md-to-pdf": "^5.2.5",
1508
- "open": "^10.2.0",
1509
- "pdf-lib": "^1.17.1",
1510
- "pizzip": "^3.2.0",
1511
- "proper-lockfile": "^4.1.2",
1512
- "remark": "^15.0.1",
1513
- "remark-gfm": "^4.0.1",
1514
- "remark-parse": "^11.0.0",
1515
- "sharp": "^0.34.5",
1516
- "tiptap-markdown": "^0.9.0",
1517
- "unified": "^11.0.5",
1518
- "unpdf": "^1.4.0",
1519
- "zod": "^3.24.1",
1520
- "zod-to-json-schema": "^3.23.5"
1521
- },
1522
- "bin": {
1523
- "desktop-commander": "dist/index.js",
1524
- "remove": "dist/uninstall-claude-server.js",
1525
- "setup": "dist/setup-claude-server.js"
1526
- },
1527
- "engines": {
1528
- "node": ">=18.0.0"
1529
- },
1530
- "optionalDependencies": {
1531
- "caffeinate": "^1.0.1"
1532
- }
1533
- },
1534
- "node_modules/@wonderwhy-er/desktop-commander/node_modules/zod": {
1535
- "version": "3.25.76",
1536
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
1537
- "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
1538
- "license": "MIT",
1539
- "funding": {
1540
- "url": "https://github.com/sponsors/colinhacks"
1541
- }
1542
- },
1543
1480
  "node_modules/accepts": {
1544
1481
  "version": "2.0.0",
1545
1482
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
@@ -1986,6 +1923,70 @@
1986
1923
  "node": ">=8"
1987
1924
  }
1988
1925
  },
1926
+ "node_modules/bridgerator-engine": {
1927
+ "name": "@wonderwhy-er/desktop-commander",
1928
+ "version": "0.2.52",
1929
+ "resolved": "https://registry.npmjs.org/@wonderwhy-er/desktop-commander/-/desktop-commander-0.2.52.tgz",
1930
+ "integrity": "sha512-VNeKfaBR6TN/8MlP/ziTpjNEeHrGOFmmzQjrwzr52kQalBJoNNISWwaSQElPFc6+I17NOOM354KbSdS/aDrqGA==",
1931
+ "hasInstallScript": true,
1932
+ "license": "MIT",
1933
+ "dependencies": {
1934
+ "@modelcontextprotocol/sdk": "^1.9.0",
1935
+ "@opendocsg/pdf2md": "^0.2.2",
1936
+ "@supabase/supabase-js": "^2.89.0",
1937
+ "@tiptap/core": "^3.22.3",
1938
+ "@tiptap/extension-image": "^3.22.3",
1939
+ "@tiptap/extension-table": "^3.22.4",
1940
+ "@tiptap/extension-table-cell": "^3.22.4",
1941
+ "@tiptap/extension-table-header": "^3.22.4",
1942
+ "@tiptap/extension-table-row": "^3.22.4",
1943
+ "@tiptap/pm": "^3.22.3",
1944
+ "@tiptap/starter-kit": "^3.22.3",
1945
+ "@vscode/ripgrep": "^1.15.9",
1946
+ "cross-fetch": "^4.1.0",
1947
+ "exceljs": "^4.4.0",
1948
+ "fastest-levenshtein": "^1.0.16",
1949
+ "file-type": "^21.1.1",
1950
+ "glob": "^10.3.10",
1951
+ "highlight.js": "^11.11.1",
1952
+ "isbinaryfile": "^5.0.4",
1953
+ "markdown-it": "^14.1.0",
1954
+ "md-to-pdf": "^5.2.5",
1955
+ "open": "^10.2.0",
1956
+ "pdf-lib": "^1.17.1",
1957
+ "pizzip": "^3.2.0",
1958
+ "proper-lockfile": "^4.1.2",
1959
+ "remark": "^15.0.1",
1960
+ "remark-gfm": "^4.0.1",
1961
+ "remark-parse": "^11.0.0",
1962
+ "sharp": "^0.34.5",
1963
+ "tiptap-markdown": "^0.9.0",
1964
+ "unified": "^11.0.5",
1965
+ "unpdf": "^1.4.0",
1966
+ "zod": "^3.24.1",
1967
+ "zod-to-json-schema": "^3.23.5"
1968
+ },
1969
+ "bin": {
1970
+ "desktop-commander": "dist/index.js",
1971
+ "remove": "dist/uninstall-claude-server.js",
1972
+ "setup": "dist/setup-claude-server.js"
1973
+ },
1974
+ "engines": {
1975
+ "node": ">=18.0.0"
1976
+ },
1977
+ "optionalDependencies": {
1978
+ "caffeinate": "^1.0.1"
1979
+ }
1980
+ },
1981
+ "node_modules/bridgerator-engine/node_modules/zod": {
1982
+ "version": "3.25.76",
1983
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
1984
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
1985
+ "license": "MIT",
1986
+ "funding": {
1987
+ "url": "https://github.com/sponsors/colinhacks"
1988
+ }
1989
+ },
1989
1990
  "node_modules/buffer": {
1990
1991
  "version": "5.7.1",
1991
1992
  "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bridgerator",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Connect ChatGPT to your Windows PC: files, search and terminal through an outbound-only bridge with folder limits. No inbound ports, no admin rights.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@modelcontextprotocol/sdk": "^1.29.0",
31
- "@wonderwhy-er/desktop-commander": "0.2.52",
31
+ "bridgerator-engine": "npm:@wonderwhy-er/desktop-commander@0.2.52",
32
32
  "ws": "^8.20.0"
33
33
  },
34
34
  "homepage": "https://bridgerator.recondeck.com",
package/src/bridge.mjs CHANGED
@@ -59,7 +59,7 @@ export function createLimiter(max, waitMs) {
59
59
  };
60
60
  }
61
61
  const BUSY_WAIT_MS = 30000;
62
- const DC_ENTRY = createRequire(import.meta.url).resolve('@wonderwhy-er/desktop-commander/dist/index.js');
62
+ const DC_ENTRY = createRequire(import.meta.url).resolve('bridgerator-engine/dist/index.js');
63
63
  const ISOLATE = new URL('./dc-isolate.mjs', import.meta.url).href;
64
64
 
65
65
  /** Drive roots of this PC (C:\\ … Z:\\): what the Read only preset makes read-only when there is no folder list. */
@@ -25,7 +25,7 @@ const PATCHES = [
25
25
  { file: /[\\/]dist[\\/]terminal-manager\.js$/, from: "args: ['-Command', command],", to: "args: ['-Command', '[Console]::OutputEncoding=[Text.Encoding]::UTF8;' + command]," },
26
26
  { file: /[\\/]dist[\\/]index\.js$/, from: ' ensureChromeAvailable();\n', to: ' // bridgerator: no Chrome pre-download\n' },
27
27
  ];
28
- const ENGINE = /[\\/]@wonderwhy-er[\\/]desktop-commander[\\/]dist[\\/]/;
28
+ const ENGINE = /[\\/]bridgerator-engine[\\/]dist[\\/]/; // the bundled engine (Desktop Commander, MIT) installed under our own name (npm alias, owner 26.10)
29
29
 
30
30
  export async function load(url, context, nextLoad) {
31
31
  const r = await nextLoad(url, context);
package/src/docs-tool.mjs CHANGED
@@ -1,494 +1,494 @@
1
- // LOGIC: see critical-logic/bridge.md ("Extra tools" → create_document). New Word / Excel / PDF files
2
- // from markdown and tables, without Office or a browser: libraries the bundled engine already ships
3
- // (markdown-it, jszip, exceljs, pdf-lib; see lib()) + system fonts (pdf-font.mjs). A file is never overwritten:
4
- // an existing name gets " (2)". Paths go through the same folder rules as file writes (guard).
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import { guard, fail, json, needAction, str, freeName } from './tool-lib.mjs';
8
- import { loadSystemFonts, subsetTtf, MINI_FONTKIT, FontError } from './pdf-font.mjs';
9
- import { createRequire } from 'node:module';
10
- import { pathToFileURL } from 'node:url';
11
-
12
- // The engine's libraries (not our own dependencies): found at the top of node_modules when npm hoists
13
- // them, else inside the engine's own node_modules (a version clash keeps them there).
14
- export async function lib(name) {
15
- try { return await import(name); } catch (e) {
16
- if (e?.code !== 'ERR_MODULE_NOT_FOUND') throw e;
17
- const req = createRequire(createRequire(import.meta.url).resolve('@wonderwhy-er/desktop-commander/dist/index.js'));
18
- return import(pathToFileURL(req.resolve(name)).href);
19
- }
20
- }
21
-
22
- const ACTIONS = ['docx', 'xlsx', 'pdf'];
23
- const MAX_MD = 2_000_000, MAX_CELLS = 100_000, MAX_SHEETS = 50, MAX_CELL_TEXT = 32767, MAX_PAGES = 2000;
24
- const CELL = { type: ['string', 'number', 'boolean', 'null'] };
25
-
26
- export const defs = [{
27
- name: 'create_document',
28
- title: 'Create document',
29
- description: 'Creates a new Word (.docx), Excel (.xlsx) or PDF file on this PC from markdown text and/or tables, without Office. An existing file is never overwritten: the new one gets a free name like "Report (2).docx".',
30
- inputSchema: {
31
- type: 'object', additionalProperties: false, required: ['action', 'path'],
32
- properties: {
33
- action: { type: 'string', enum: ACTIONS, description: 'File type to create' },
34
- path: { type: 'string', description: 'Full path of the new file, e.g. C:\\Users\\Name\\Documents\\Report.docx' },
35
- title: { type: 'string', maxLength: 300, description: 'Document title (shown at the top and in the file properties)' },
36
- markdown: { type: 'string', description: 'Content: # headings, paragraphs, **bold**, *italic*, `code`, - and 1. lists, | tables |, ``` code blocks. In xlsx each table becomes a sheet.' },
37
- sheets: {
38
- type: 'array', maxItems: MAX_SHEETS,
39
- description: 'Tables. Numbers stay numbers, dates like 2026-10-20 become dates, text starting with "=" is a formula (xlsx). In docx/pdf they follow the text.',
40
- items: {
41
- type: 'object', additionalProperties: false, required: ['rows'],
42
- properties: {
43
- name: { type: 'string', maxLength: 31 },
44
- header: { type: 'boolean', description: 'First row is a header (default true)' },
45
- rows: { type: 'array', items: { type: 'array', items: CELL } },
46
- },
47
- },
48
- },
49
- },
50
- },
51
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
52
- }];
53
-
54
- // ---- input → a small document model -----------------------------------------------------------
55
- // blocks: heading {level, runs} · para {runs, quote} · item {level, ordered, n, group, runs, cont} ·
56
- // table {rows: [[runs]], header, name?} · code {text} · hr. runs: [{ text, b, i, code }]
57
- const CTRL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\ufffe\uffff]/g;
58
- const cleanText = (s) => String(s).replace(CTRL, '').replace(/\t/g, ' ');
59
- const plain = (runs) => runs.map((r) => r.text).join('');
60
-
61
- function inlineRuns(tok) {
62
- const runs = [];
63
- let b = 0, i = 0, href = null, linkStart = 0;
64
- for (const c of tok?.children || []) {
65
- const push = (text, extra = {}) => { if (text) runs.push({ text: cleanText(text), b: b > 0, i: i > 0, ...extra }); };
66
- switch (c.type) {
67
- case 'text': push(c.content); break;
68
- case 'code_inline': push(c.content, { code: true }); break;
69
- case 'strong_open': b++; break;
70
- case 'strong_close': b--; break;
71
- case 'em_open': i++; break;
72
- case 'em_close': i--; break;
73
- case 'softbreak': push(' '); break;
74
- case 'hardbreak': runs.push({ text: '\n' }); break;
75
- case 'link_open': href = c.attrGet('href'); linkStart = runs.length; break;
76
- case 'link_close': {
77
- const shown = plain(runs.slice(linkStart));
78
- if (href && shown !== href && !/^mailto:/i.test(href)) push(` (${href})`);
79
- href = null; break;
80
- }
81
- case 'image': push(c.content || c.attrGet('alt') || ''); break;
82
- default: if (c.content) push(c.content);
83
- }
84
- }
85
- return runs;
86
- }
87
-
88
- export async function parseMarkdown(src) {
89
- const MarkdownIt = (await lib('markdown-it')).default;
90
- const md = new MarkdownIt('default', { html: false, linkify: false, typographer: false });
91
- const toks = md.parse(String(src || ''), {});
92
- const blocks = [];
93
- const lists = [];
94
- let group = 0, quote = 0, table = null, row = null, item = null, heading = 0;
95
- for (const t of toks) {
96
- switch (t.type) {
97
- case 'heading_open': heading = Number(t.tag.slice(1)); break;
98
- case 'heading_close': heading = 0; break;
99
- case 'blockquote_open': quote++; break;
100
- case 'blockquote_close': quote--; break;
101
- case 'bullet_list_open': lists.push({ ordered: false, n: 0 }); break;
102
- case 'ordered_list_open': lists.push({ ordered: true, n: Number(t.attrGet('start') || 1) - 1, group: ++group }); break;
103
- case 'bullet_list_close': case 'ordered_list_close': lists.pop(); break;
104
- case 'list_item_open': { const l = lists[lists.length - 1]; l.n++; item = { first: true }; break; }
105
- case 'list_item_close': item = null; break;
106
- case 'table_open': table = { rows: [], header: false }; break;
107
- case 'table_close': blocks.push({ type: 'table', ...table }); table = null; break;
108
- case 'thead_open': table.header = true; break;
109
- case 'tr_open': row = []; break;
110
- case 'tr_close': table.rows.push(row); row = null; break;
111
- case 'fence': case 'code_block': blocks.push({ type: 'code', text: cleanText(t.content.replace(/\n$/, '')) }); break;
112
- case 'hr': blocks.push({ type: 'hr' }); break;
113
- case 'inline': {
114
- const runs = inlineRuns(t);
115
- if (row) { row.push(runs); break; }
116
- if (heading) { blocks.push({ type: 'heading', level: heading, runs }); break; }
117
- if (item && lists.length) {
118
- const l = lists[lists.length - 1];
119
- blocks.push({ type: 'item', level: lists.length - 1, ordered: l.ordered, n: l.n, group: l.group || 0, runs, cont: !item.first, quote });
120
- item.first = false;
121
- break;
122
- }
123
- blocks.push({ type: 'para', runs, quote });
124
- break;
125
- }
126
- default: break;
127
- }
128
- }
129
- return blocks;
130
- }
131
-
132
- const ISO = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
133
- /** "2026-10-20" / "2026-10-20T14:30" → a Date with that wall-clock time in UTC (Excel has no zones), else null. */
134
- export function isoDate(s) {
135
- const m = ISO.exec(s);
136
- if (!m) return null;
137
- const [y, mo, d, h = 0, mi = 0, se = 0] = m.slice(1).map((x) => (x === undefined ? 0 : Number(x)));
138
- const dt = new Date(Date.UTC(y, mo - 1, d, h, mi, se));
139
- if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d || h > 23 || mi > 59 || se > 59) return null;
140
- return { date: dt, time: m[4] !== undefined };
141
- }
142
-
143
- function checkSheets(sheets) {
144
- if (sheets === undefined) return [];
145
- if (!Array.isArray(sheets)) fail('BAD_ARGS', 'sheets must be a list of tables.');
146
- if (sheets.length > MAX_SHEETS) fail('TOO_BIG', `At most ${MAX_SHEETS} sheets at once.`);
147
- let cells = 0;
148
- return sheets.map((s, i) => {
149
- if (!s || typeof s !== 'object' || !Array.isArray(s.rows)) fail('BAD_ARGS', `sheets[${i}].rows must be a list of rows.`);
150
- if (s.name !== undefined && typeof s.name !== 'string') fail('BAD_ARGS', `sheets[${i}].name must be text.`);
151
- for (const r of s.rows) {
152
- if (!Array.isArray(r)) fail('BAD_ARGS', `sheets[${i}]: every row must be a list of cells.`);
153
- cells += r.length;
154
- if (cells > MAX_CELLS) fail('TOO_BIG', `Too many cells (more than ${MAX_CELLS.toLocaleString('en')}).`, 'Split the data into several files.');
155
- for (const c of r) {
156
- if (c !== null && !['string', 'number', 'boolean'].includes(typeof c)) fail('BAD_ARGS', `sheets[${i}]: a cell must be text, a number, true/false or null.`);
157
- if (typeof c === 'number' && !Number.isFinite(c)) fail('BAD_ARGS', `sheets[${i}]: numbers must be finite.`);
158
- if (typeof c === 'string' && c.length > MAX_CELL_TEXT) fail('TOO_BIG', `A cell is longer than ${MAX_CELL_TEXT} characters (the Excel limit).`);
159
- }
160
- }
161
- return { name: s.name, header: s.header !== false, rows: s.rows };
162
- });
163
- }
164
- const cellText = (c) => (c === null || c === undefined ? '' : String(c));
165
- const sheetBlocks = (sheets) => sheets.flatMap((s) => [
166
- ...(s.name ? [{ type: 'heading', level: 3, runs: [{ text: cleanText(s.name) }] }] : []),
167
- { type: 'table', header: s.header, rows: s.rows.map((r) => r.map((c) => [{ text: cleanText(cellText(c)) }])) },
168
- ]);
169
-
170
- // ---- DOCX --------------------------------------------------------------------------------------
171
- const x = (s) => String(s).replace(/[<>&"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c]));
172
- function wRuns(runs) {
173
- return runs.map((r) => {
174
- if (r.text === '\n') return '<w:r><w:br/></w:r>';
175
- // Element order inside rPr is fixed by the OOXML schema (Word refuses files that break it).
176
- const pr = `${r.code ? '<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>' : ''}${r.b ? '<w:b/>' : ''}${r.i ? '<w:i/>' : ''}${r.code ? '<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/>' : ''}`;
177
- return `<w:r>${pr ? `<w:rPr>${pr}</w:rPr>` : ''}<w:t xml:space="preserve">${x(r.text)}</w:t></w:r>`;
178
- }).join('');
179
- }
180
- const wP = (runs, ppr = '') => `<w:p>${ppr ? `<w:pPr>${ppr}</w:pPr>` : ''}${wRuns(runs)}</w:p>`;
181
- const TEXT_W = 9638; // A4 minus 2 cm margins, twips
182
-
183
- function docxBody(title, blocks) {
184
- const out = [];
185
- const nums = new Map(); // ordered group → { numId, level }
186
- if (title) out.push(wP([{ text: title }], '<w:pStyle w:val="Title"/>'));
187
- for (const bl of blocks) {
188
- if (bl.type === 'heading') out.push(wP(bl.runs, `<w:pStyle w:val="Heading${Math.min(bl.level, 3)}"/>`));
189
- else if (bl.type === 'para') out.push(wP(bl.runs, bl.quote ? '<w:pStyle w:val="Quote"/>' : ''));
190
- else if (bl.type === 'item') {
191
- const ind = 720 * (bl.level + 1);
192
- if (bl.cont) { out.push(wP(bl.runs, `<w:ind w:left="${ind}"/>`)); continue; }
193
- let numId = 1;
194
- if (bl.ordered) { if (!nums.has(bl.group)) nums.set(bl.group, { numId: nums.size + 2, level: bl.level, start: bl.n }); numId = nums.get(bl.group).numId; }
195
- out.push(wP(bl.runs, `<w:pStyle w:val="ListParagraph"/><w:numPr><w:ilvl w:val="${Math.min(bl.level, 8)}"/><w:numId w:val="${numId}"/></w:numPr>`));
196
- } else if (bl.type === 'code') for (const line of bl.text.split('\n')) out.push(wP([{ text: line }], '<w:pStyle w:val="Code"/>'));
197
- else if (bl.type === 'hr') out.push('<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="999999"/></w:pBdr></w:pPr></w:p>');
198
- else if (bl.type === 'table') {
199
- const cols = Math.max(1, ...bl.rows.map((r) => r.length));
200
- const w = Math.floor(TEXT_W / cols);
201
- const rows = bl.rows.map((r, ri) => {
202
- const head = bl.header && ri === 0;
203
- const cells = Array.from({ length: cols }, (_, ci) => {
204
- const runs = (r[ci] || []).map((run) => (head ? { ...run, b: true } : run));
205
- return `<w:tc><w:tcPr><w:tcW w:w="${w}" w:type="dxa"/>${head ? '<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/>' : ''}</w:tcPr>${wP(runs, '<w:spacing w:after="0"/>')}</w:tc>`;
206
- }).join('');
207
- return `<w:tr>${head ? '<w:trPr><w:tblHeader/></w:trPr>' : ''}${cells}</w:tr>`;
208
- }).join('');
209
- out.push(`<w:tbl><w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="5000" w:type="pct"/></w:tblPr><w:tblGrid>${`<w:gridCol w:w="${w}"/>`.repeat(cols)}</w:tblGrid>${rows}</w:tbl>`, '<w:p/>');
210
- }
211
- }
212
- return { body: out.join(''), nums: [...nums.values()] };
213
- }
214
-
215
- const W_NS = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"';
216
- const STYLES = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
217
- <w:styles ${W_NS}><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:eastAsia="Calibri" w:cs="Calibri"/><w:sz w:val="22"/><w:szCs w:val="22"/><w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="120" w:line="264" w:lineRule="auto"/></w:pPr></w:pPrDefault></w:docDefaults>
218
- <w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style>
219
- <w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="240"/></w:pPr><w:rPr><w:sz w:val="48"/><w:szCs w:val="48"/></w:rPr></w:style>
220
- ${[1, 2, 3].map((n) => `<w:style w:type="paragraph" w:styleId="Heading${n}"><w:name w:val="heading ${n}"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="${[0, 360, 280, 200][n]}" w:after="80"/><w:outlineLvl w:val="${n - 1}"/></w:pPr><w:rPr><w:b/><w:color w:val="1F3864"/><w:sz w:val="${[0, 36, 30, 26][n]}"/><w:szCs w:val="${[0, 36, 30, 26][n]}"/></w:rPr></w:style>`).join('\n')}
221
- <w:style w:type="paragraph" w:styleId="ListParagraph"><w:name w:val="List Paragraph"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="40"/><w:contextualSpacing/></w:pPr></w:style>
222
- <w:style w:type="paragraph" w:styleId="Quote"><w:name w:val="Quote"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:pBdr><w:left w:val="single" w:sz="12" w:space="8" w:color="BFBFBF"/></w:pBdr><w:ind w:left="567"/></w:pPr><w:rPr><w:i/><w:color w:val="595959"/></w:rPr></w:style>
223
- <w:style w:type="paragraph" w:styleId="Code"><w:name w:val="Code"/><w:basedOn w:val="Normal"/><w:pPr><w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/><w:spacing w:after="0" w:line="240" w:lineRule="auto"/></w:pPr><w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/><w:sz w:val="19"/><w:szCs w:val="19"/></w:rPr></w:style>
224
- <w:style w:type="table" w:default="1" w:styleId="TableNormal"><w:name w:val="Normal Table"/><w:tblPr><w:tblInd w:w="0" w:type="dxa"/><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="108" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="108" w:type="dxa"/></w:tblCellMar></w:tblPr></w:style>
225
- <w:style w:type="table" w:styleId="TableGrid"><w:name w:val="Table Grid"/><w:basedOn w:val="TableNormal"/><w:tblPr><w:tblBorders>${['top', 'left', 'bottom', 'right', 'insideH', 'insideV'].map((s) => `<w:${s} w:val="single" w:sz="4" w:space="0" w:color="A6A6A6"/>`).join('')}</w:tblBorders></w:tblPr></w:style>
226
- </w:styles>`;
227
- const lvls = (fmt) => Array.from({ length: 9 }, (_, l) => `<w:lvl w:ilvl="${l}"><w:start w:val="1"/><w:numFmt w:val="${fmt}"/><w:lvlText w:val="${fmt === 'bullet' ? ['•', '◦', '▪'][l % 3] : `%${l + 1}.`}"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="${720 * (l + 1)}" w:hanging="360"/></w:pPr></w:lvl>`).join('');
228
- const numbering = (nums) => `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
229
- <w:numbering ${W_NS}><w:abstractNum w:abstractNumId="0"><w:multiLevelType w:val="hybridMultilevel"/>${lvls('bullet')}</w:abstractNum><w:abstractNum w:abstractNumId="1"><w:multiLevelType w:val="hybridMultilevel"/>${lvls('decimal')}</w:abstractNum><w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>${nums.map((n) => `<w:num w:numId="${n.numId}"><w:abstractNumId w:val="1"/><w:lvlOverride w:ilvl="${Math.min(n.level, 8)}"><w:startOverride w:val="${Math.max(1, n.start)}"/></w:lvlOverride></w:num>`).join('')}</w:numbering>`;
230
-
231
- export async function buildDocx({ title, blocks }) {
232
- const JSZip = (await lib('jszip')).default;
233
- const { body, nums } = docxBody(title, blocks);
234
- const now = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
235
- const zip0 = new JSZip();
236
- // No folder entries in the package (Office's own files have none).
237
- const zip = { file: (n, d) => zip0.file(n, d, { createFolders: false }), generateAsync: (o) => zip0.generateAsync(o) };
238
- zip.file('[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>');
239
- zip.file('_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>');
240
- zip.file('word/_rels/document.xml.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/></Relationships>');
241
- zip.file('docProps/core.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${title ? `<dc:title>${x(title)}</dc:title>` : ''}<dc:creator>Bridgerator</dc:creator><dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`);
242
- zip.file('word/styles.xml', STYLES);
243
- zip.file('word/numbering.xml', numbering(nums));
244
- zip.file('word/document.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:document ${W_NS}><w:body>${body || '<w:p/>'}<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1134" w:right="1134" w:bottom="1134" w:left="1134" w:header="567" w:footer="567" w:gutter="0"/></w:sectPr></w:body></w:document>`);
245
- return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
246
- }
247
-
248
- // ---- XLSX --------------------------------------------------------------------------------------
249
- const NUM = /^-?(0|[1-9]\d{0,14})(\.\d+)?$/;
250
- function sheetName(n, i, used) {
251
- let s = cleanText(n || `Sheet${i + 1}`).replace(/[[\]:*?/\\]/g, ' ').replace(/^'+|'+$/g, '').trim().slice(0, 31) || `Sheet${i + 1}`;
252
- let k = 2;
253
- const base = s;
254
- while (used.has(s.toLowerCase())) s = `${base.slice(0, 27)} (${k++})`;
255
- used.add(s.toLowerCase());
256
- return s;
257
- }
258
- export async function buildXlsx({ title, sheets }) {
259
- const ExcelJS = (await lib('exceljs')).default;
260
- const wb = new ExcelJS.Workbook();
261
- wb.creator = 'Bridgerator';
262
- if (title) wb.title = title;
263
- wb.calcProperties = { fullCalcOnLoad: true };
264
- const used = new Set();
265
- sheets.forEach((s, si) => {
266
- const ws = wb.addWorksheet(sheetName(s.name, si, used));
267
- const widths = [];
268
- s.rows.forEach((r, ri) => {
269
- const row = ws.getRow(ri + 1);
270
- r.forEach((c, ci) => {
271
- const cell = row.getCell(ci + 1);
272
- let shown = cellText(c);
273
- if (typeof c === 'string') {
274
- const d = !(s.header && ri === 0) && isoDate(c.trim());
275
- if (s.textOnly) {
276
- if (NUM.test(c.trim()) && !(s.header && ri === 0)) cell.value = Number(c.trim());
277
- else if (d) { cell.value = d.date; cell.numFmt = d.time ? 'yyyy-mm-dd hh:mm' : 'yyyy-mm-dd'; } else cell.value = cleanText(c);
278
- } else if (c.startsWith('=') && c.length > 1) { cell.value = { formula: c.slice(1) }; shown = ''; }
279
- else if (d) { cell.value = d.date; cell.numFmt = d.time ? 'yyyy-mm-dd hh:mm' : 'yyyy-mm-dd'; } else cell.value = cleanText(c);
280
- } else if (c !== null && c !== undefined) cell.value = c;
281
- widths[ci] = Math.max(widths[ci] || 0, Math.min(60, shown.length));
282
- });
283
- if (s.header && ri === 0) row.font = { bold: true };
284
- });
285
- widths.forEach((w, i) => { ws.getColumn(i + 1).width = Math.max(8, w + 2); });
286
- if (s.header && s.rows.length > 1) ws.views = [{ state: 'frozen', ySplit: 1 }];
287
- });
288
- return Buffer.from(await wb.xlsx.writeBuffer());
289
- }
290
-
291
- // ---- PDF ---------------------------------------------------------------------------------------
292
- const A4 = [595.28, 841.89], M = 56, BODY = 11;
293
- const keyOf = (r) => (r.code ? 'm' : r.b && r.i ? 'bi' : r.b ? 'b' : r.i ? 'i' : 'r');
294
-
295
- /** Lays the blocks out into pages of draw items with the fonts' metrics (pass 1, no PDF yet). */
296
- export function layoutPdf({ title, blocks }, fonts) {
297
- const [W, H] = A4, width = W - 2 * M;
298
- const pages = [];
299
- let page, y;
300
- const newPage = () => { page = []; pages.push(page); y = H - M; if (pages.length > MAX_PAGES) fail('TOO_BIG', `The PDF would have more than ${MAX_PAGES} pages.`, 'Split the content into several files.'); };
301
- newPage();
302
- const f = (k) => fonts[k] || fonts.r;
303
- const has = (k, ch) => f(k).cmap.has(ch.codePointAt(0));
304
- const fix = (k, t) => Array.from(t, (ch) => (has(k, ch) || ch === ' ' || ch === ' ' ? ch : (has('r', ch) ? ch : '?'))).join('');
305
- // A no-break space (code-block indentation) measures as a space when the font has no glyph for it.
306
- const wOf = (k, t, size) => { const ft = f(k); let s = 0; for (const ch of t) { const c = ch.codePointAt(0); const g = ft.cmap.get(c) || (c === 0xa0 ? ft.cmap.get(32) : 0) || 0; s += ft.advance(g); } return (s * size) / ft.unitsPerEm; };
307
- const asc = (size) => (fonts.r.ascent / fonts.r.unitsPerEm) * size;
308
- const lh = (size) => ((fonts.r.ascent - fonts.r.descent) / fonts.r.unitsPerEm) * size * 1.08;
309
-
310
- /** runs → lines of segments [{ text, k, dx }] within maxW. */
311
- function wrap(runs, size, maxW) {
312
- const lines = [];
313
- let line = [], lw = 0;
314
- const flush = () => { while (line.length && !line[line.length - 1].text.trim()) { lw -= line[line.length - 1].w; line.pop(); } lines.push(line); line = []; lw = 0; };
315
- const add = (text, k) => {
316
- const w = wOf(k, text, size);
317
- const last = line[line.length - 1];
318
- if (last && last.k === k) { last.text += text; last.w += w; } else line.push({ text, k, w });
319
- lw += w;
320
- };
321
- for (const r of runs) {
322
- if (r.text === '\n') { flush(); continue; }
323
- const k = keyOf(r);
324
- // Breaks at white space except the no-break space (it keeps code indentation and runs of spaces).
325
- for (const piece of fix(k, r.text).split(/([^\S ]+)/)) {
326
- if (!piece) continue;
327
- if (/^[^\S ]+$/.test(piece)) { if (line.length) add(' ', k); continue; }
328
- const w = wOf(k, piece, size);
329
- if (lw + w <= maxW) { add(piece, k); continue; }
330
- if (line.length) flush();
331
- if (w <= maxW) { add(piece, k); continue; }
332
- let rest = piece;
333
- while (rest) {
334
- let n = 1;
335
- const chars = Array.from(rest);
336
- while (n < chars.length && wOf(k, chars.slice(0, n + 1).join(''), size) <= maxW - lw) n++;
337
- add(chars.slice(0, n).join(''), k);
338
- rest = chars.slice(n).join('');
339
- if (rest) flush();
340
- }
341
- }
342
- }
343
- if (line.length || !lines.length) flush();
344
- return lines.map((l) => { let dx = 0; return l.map((s) => { const o = { text: s.text, k: s.k, dx }; dx += s.w; return o; }); });
345
- }
346
- const need = (h) => { if (y - h < M) newPage(); };
347
- const text = (segs, x, top, size, color) => { for (const s of segs) if (s.text.trim()) page.push({ t: 'text', text: s.text, k: s.k, x: x + s.dx, y: top - asc(size), size, color }); };
348
- function paragraph(runs, { size = BODY, indent = 0, color, before = 0, after = 6, marker, bar } = {}) {
349
- const lines = wrap(runs, size, width - indent);
350
- const h = lh(size);
351
- y -= before;
352
- lines.forEach((segs, i) => {
353
- need(h);
354
- if (i === 0 && marker) text([{ text: marker, k: 'r', dx: 0 }], M + indent - wOf('r', marker, size) - 6, y, size, color);
355
- if (bar) page.push({ t: 'rect', x: M + indent - 10, y: y - h, w: 2, h, color: [0.75, 0.75, 0.75] });
356
- text(segs, M + indent, y, size, color);
357
- y -= h;
358
- });
359
- y -= after;
360
- }
361
- function table(bl) {
362
- const size = 10, pad = 4, h = lh(size);
363
- const cols = Math.max(1, ...bl.rows.map((r) => r.length));
364
- const runsAt = (r, c, ri) => (r[c] || []).map((x) => (bl.header && ri === 0 ? { ...x, b: true } : x));
365
- const nat = Array(cols).fill(0);
366
- bl.rows.forEach((r, ri) => { for (let c = 0; c < cols; c++) nat[c] = Math.max(nat[c], runsAt(r, c, ri).reduce((s, x) => s + wOf(keyOf(x), fix(keyOf(x), x.text), size), 0)); });
367
- for (let c = 0; c < cols; c++) nat[c] += 2 * pad + 1;
368
- let cw;
369
- const total = nat.reduce((a, b) => a + b, 0);
370
- if (total <= width) cw = nat.map((n) => n + (width - total) / cols);
371
- else {
372
- const fair = width / cols;
373
- cw = nat.map((n) => Math.min(n, fair));
374
- const left = width - cw.reduce((a, b) => a + b, 0);
375
- const want = nat.map((n, i) => n - cw[i]), sumWant = want.reduce((a, b) => a + b, 0);
376
- if (sumWant > 0) cw = cw.map((w, i) => w + (left * want[i]) / sumWant);
377
- }
378
- const maxLines = Math.max(1, Math.floor((A4[1] - 2 * M - 2 * pad - (bl.header ? h + 2 * pad : 0)) / h) - 1);
379
- const rowsLaid = bl.rows.map((r, ri) => {
380
- const cells = cw.map((w, c) => wrap(runsAt(r, c, ri), size, w - 2 * pad).slice(0, maxLines));
381
- return { cells, height: Math.max(...cells.map((l) => l.length)) * h + 2 * pad, head: bl.header && ri === 0 };
382
- });
383
- const drawRow = (row) => {
384
- let x = M;
385
- row.cells.forEach((lines, c) => {
386
- page.push({ t: 'rect', x, y: y - row.height, w: cw[c], h: row.height, border: true, fill: row.head ? [0.95, 0.95, 0.95] : null });
387
- lines.forEach((segs, i) => text(segs, x + pad, y - pad - i * h, size));
388
- x += cw[c];
389
- });
390
- y -= row.height;
391
- };
392
- y -= 4;
393
- rowsLaid.forEach((row, i) => {
394
- if (y - row.height < M) { newPage(); if (bl.header && i > 0) drawRow(rowsLaid[0]); }
395
- drawRow(row);
396
- });
397
- y -= 10;
398
- }
399
- if (title) paragraph([{ text: cleanText(title), b: true }], { size: 22, after: 12 });
400
- for (const bl of blocks) {
401
- if (bl.type === 'heading') paragraph(bl.runs.map((r) => ({ ...r, b: true })), { size: [0, 18, 15, 13, 12, 12, 12][bl.level] || 12, before: 8, after: 4 });
402
- else if (bl.type === 'para') paragraph(bl.runs, bl.quote ? { indent: 14 * bl.quote, color: [0.35, 0.35, 0.35], bar: true } : {});
403
- else if (bl.type === 'item') {
404
- const indent = 18 * (bl.level + 1);
405
- const bullet = has('r', '•') ? '•' : '-';
406
- paragraph(bl.runs, { indent, after: 2, marker: bl.cont ? undefined : (bl.ordered ? `${bl.n}.` : bullet) });
407
- } else if (bl.type === 'code') {
408
- const size = 9.5, h = lh(size);
409
- y -= 2;
410
- for (const raw of bl.text.split('\n')) {
411
- for (const segs of wrap([{ text: raw.replace(/ /g, '\u00a0') || '\u00a0', code: true }], size, width - 8)) {
412
- need(h);
413
- page.push({ t: 'rect', x: M, y: y - h, w: width, h, fill: [0.95, 0.95, 0.95] });
414
- text(segs.map((s) => ({ ...s, text: s.text.replace(/\u00a0/g, ' ') })), M + 4, y, size);
415
- y -= h;
416
- }
417
- }
418
- y -= 8;
419
- } else if (bl.type === 'hr') { need(12); page.push({ t: 'line', x1: M, x2: M + width, y: y - 6 }); y -= 12; }
420
- else if (bl.type === 'table') table(bl);
421
- }
422
- if (pages.length > 1) pages.forEach((p, i) => { const s = `${i + 1} / ${pages.length}`; p.push({ t: 'text', text: s, k: 'r', x: W - M - wOf('r', s, 8), y: M / 2, size: 8, color: [0.5, 0.5, 0.5] }); });
423
- return pages;
424
- }
425
-
426
- export async function buildPdf({ title, blocks }, { fonts, fontDir } = {}) {
427
- let fs0;
428
- try { fs0 = fonts || loadSystemFonts(fontDir); } catch (e) { if (e instanceof FontError) fail('NO_FONT', 'No usable system font for PDF was found on this PC.', 'Create a .docx instead, or check that C:\\Windows\\Fonts has Segoe UI or Arial.'); throw e; }
429
- const pages = layoutPdf({ title, blocks }, fs0);
430
- const { PDFDocument, rgb } = await lib('pdf-lib');
431
- const doc = await PDFDocument.create();
432
- doc.registerFontkit(MINI_FONTKIT);
433
- doc.setCreator('Bridgerator'); doc.setProducer('Bridgerator');
434
- if (title) doc.setTitle(title);
435
- const usedBy = {};
436
- for (const p of pages) for (const it of p) if (it.t === 'text') { const set = (usedBy[it.k] ||= new Set()); for (const ch of it.text) set.add(ch.codePointAt(0)); }
437
- const embedded = {};
438
- for (const [k, set] of Object.entries(usedBy)) embedded[k] = await doc.embedFont(subsetTtf(fs0[k] || fs0.r, set), { subset: false });
439
- const grey = rgb(0.65, 0.65, 0.65);
440
- for (const items of pages) {
441
- const page = doc.addPage(A4);
442
- for (const it of items) {
443
- if (it.t === 'rect') page.drawRectangle({ x: it.x, y: it.y, width: it.w, height: it.h, ...(it.fill ? { color: rgb(...it.fill) } : {}), ...(it.color ? { color: rgb(...it.color) } : {}), ...(it.border ? { borderColor: grey, borderWidth: 0.5 } : {}) });
444
- else if (it.t === 'line') page.drawLine({ start: { x: it.x1, y: it.y }, end: { x: it.x2, y: it.y }, thickness: 0.7, color: grey });
445
- }
446
- for (const it of items) if (it.t === 'text') page.drawText(it.text, { x: it.x, y: it.y, size: it.size, font: embedded[it.k], ...(it.color ? { color: rgb(...it.color) } : {}) });
447
- }
448
- return { bytes: Buffer.from(await doc.save()), pages: pages.length };
449
- }
450
-
451
- // ---- the tool ----------------------------------------------------------------------------------
452
- export async function handle(name, args, ctx) {
453
- const action = needAction(args, ACTIONS);
454
- const want = str(args, 'path', { required: true, max: 1000 });
455
- if (path.extname(want).toLowerCase() !== `.${action}`) fail('BAD_PATH', `The file name must end with .${action} for action "${action}".`, `Use a path like ...\\Report.${action}.`);
456
- // One line without control characters (a control character makes the docx / xlsx XML invalid).
457
- const title = str(args, 'title', { max: 300 }) === undefined ? undefined : cleanText(args.title).replace(/\s+/g, ' ').trim() || undefined;
458
- const markdown = str(args, 'markdown', { max: MAX_MD });
459
- const sheets = checkSheets(args.sheets);
460
- if (!markdown?.trim() && !sheets.length && !title) fail('EMPTY', 'There is nothing to put in the document.', 'Pass markdown text and/or sheets.');
461
- const blocks = markdown ? await parseMarkdown(markdown) : [];
462
- let bytes, extra = {};
463
- if (action === 'xlsx') {
464
- let all = sheets;
465
- if (!all.length) {
466
- // Markdown tables → one sheet each, named by the heading above them.
467
- let lastHead = '';
468
- all = [];
469
- for (const b of blocks) {
470
- if (b.type === 'heading') lastHead = plain(b.runs);
471
- if (b.type === 'table') all.push({ name: lastHead || undefined, header: b.header, rows: b.rows.map((r) => r.map(plain)), textOnly: true });
472
- }
473
- if (!all.length) fail('NO_TABLE', 'An Excel file needs a table.', 'Pass sheets: [{ name, rows: [[...]] }] or a markdown table.');
474
- }
475
- bytes = await buildXlsx({ title, sheets: all });
476
- extra = { sheets: all.length };
477
- } else {
478
- const model = { title, blocks: [...blocks, ...sheetBlocks(sheets)] };
479
- if (action === 'docx') bytes = await buildDocx(model);
480
- else { const r = await buildPdf(model, { fonts: ctx?.fonts, fontDir: ctx?.fontDir }); bytes = r.bytes; extra = { pages: r.pages }; }
481
- }
482
- let target = freeName(guard(ctx, want, { write: true }));
483
- if (target !== path.resolve(want)) guard(ctx, target, { write: true });
484
- // Same confirmation as a file write ("Writing files" category), when it is on.
485
- const stop = await ctx?.gate?.gate?.('write_file', { path: target });
486
- if (stop) return stop;
487
- fs.mkdirSync(path.dirname(target), { recursive: true });
488
- try { fs.writeFileSync(target, bytes, { flag: 'wx' }); } catch (e) {
489
- if (e.code !== 'EEXIST') throw e;
490
- target = freeName(target); // created by someone else in between
491
- fs.writeFileSync(target, bytes, { flag: 'wx' });
492
- }
493
- return json({ created: target, bytes: bytes.length, ...extra, ...(target !== path.resolve(want) ? { note: 'A file with the requested name already existed; it was left as is.' } : {}) });
494
- }
1
+ // LOGIC: see critical-logic/bridge.md ("Extra tools" → create_document). New Word / Excel / PDF files
2
+ // from markdown and tables, without Office or a browser: libraries the bundled engine already ships
3
+ // (markdown-it, jszip, exceljs, pdf-lib; see lib()) + system fonts (pdf-font.mjs). A file is never overwritten:
4
+ // an existing name gets " (2)". Paths go through the same folder rules as file writes (guard).
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { guard, fail, json, needAction, str, freeName } from './tool-lib.mjs';
8
+ import { loadSystemFonts, subsetTtf, MINI_FONTKIT, FontError } from './pdf-font.mjs';
9
+ import { createRequire } from 'node:module';
10
+ import { pathToFileURL } from 'node:url';
11
+
12
+ // The engine's libraries (not our own dependencies): found at the top of node_modules when npm hoists
13
+ // them, else inside the engine's own node_modules (a version clash keeps them there).
14
+ export async function lib(name) {
15
+ try { return await import(name); } catch (e) {
16
+ if (e?.code !== 'ERR_MODULE_NOT_FOUND') throw e;
17
+ const req = createRequire(createRequire(import.meta.url).resolve('bridgerator-engine/dist/index.js'));
18
+ return import(pathToFileURL(req.resolve(name)).href);
19
+ }
20
+ }
21
+
22
+ const ACTIONS = ['docx', 'xlsx', 'pdf'];
23
+ const MAX_MD = 2_000_000, MAX_CELLS = 100_000, MAX_SHEETS = 50, MAX_CELL_TEXT = 32767, MAX_PAGES = 2000;
24
+ const CELL = { type: ['string', 'number', 'boolean', 'null'] };
25
+
26
+ export const defs = [{
27
+ name: 'create_document',
28
+ title: 'Create document',
29
+ description: 'Creates a new Word (.docx), Excel (.xlsx) or PDF file on this PC from markdown text and/or tables, without Office. An existing file is never overwritten: the new one gets a free name like "Report (2).docx".',
30
+ inputSchema: {
31
+ type: 'object', additionalProperties: false, required: ['action', 'path'],
32
+ properties: {
33
+ action: { type: 'string', enum: ACTIONS, description: 'File type to create' },
34
+ path: { type: 'string', description: 'Full path of the new file, e.g. C:\\Users\\Name\\Documents\\Report.docx' },
35
+ title: { type: 'string', maxLength: 300, description: 'Document title (shown at the top and in the file properties)' },
36
+ markdown: { type: 'string', description: 'Content: # headings, paragraphs, **bold**, *italic*, `code`, - and 1. lists, | tables |, ``` code blocks. In xlsx each table becomes a sheet.' },
37
+ sheets: {
38
+ type: 'array', maxItems: MAX_SHEETS,
39
+ description: 'Tables. Numbers stay numbers, dates like 2026-10-20 become dates, text starting with "=" is a formula (xlsx). In docx/pdf they follow the text.',
40
+ items: {
41
+ type: 'object', additionalProperties: false, required: ['rows'],
42
+ properties: {
43
+ name: { type: 'string', maxLength: 31 },
44
+ header: { type: 'boolean', description: 'First row is a header (default true)' },
45
+ rows: { type: 'array', items: { type: 'array', items: CELL } },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
52
+ }];
53
+
54
+ // ---- input → a small document model -----------------------------------------------------------
55
+ // blocks: heading {level, runs} · para {runs, quote} · item {level, ordered, n, group, runs, cont} ·
56
+ // table {rows: [[runs]], header, name?} · code {text} · hr. runs: [{ text, b, i, code }]
57
+ const CTRL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\ufffe\uffff]/g;
58
+ const cleanText = (s) => String(s).replace(CTRL, '').replace(/\t/g, ' ');
59
+ const plain = (runs) => runs.map((r) => r.text).join('');
60
+
61
+ function inlineRuns(tok) {
62
+ const runs = [];
63
+ let b = 0, i = 0, href = null, linkStart = 0;
64
+ for (const c of tok?.children || []) {
65
+ const push = (text, extra = {}) => { if (text) runs.push({ text: cleanText(text), b: b > 0, i: i > 0, ...extra }); };
66
+ switch (c.type) {
67
+ case 'text': push(c.content); break;
68
+ case 'code_inline': push(c.content, { code: true }); break;
69
+ case 'strong_open': b++; break;
70
+ case 'strong_close': b--; break;
71
+ case 'em_open': i++; break;
72
+ case 'em_close': i--; break;
73
+ case 'softbreak': push(' '); break;
74
+ case 'hardbreak': runs.push({ text: '\n' }); break;
75
+ case 'link_open': href = c.attrGet('href'); linkStart = runs.length; break;
76
+ case 'link_close': {
77
+ const shown = plain(runs.slice(linkStart));
78
+ if (href && shown !== href && !/^mailto:/i.test(href)) push(` (${href})`);
79
+ href = null; break;
80
+ }
81
+ case 'image': push(c.content || c.attrGet('alt') || ''); break;
82
+ default: if (c.content) push(c.content);
83
+ }
84
+ }
85
+ return runs;
86
+ }
87
+
88
+ export async function parseMarkdown(src) {
89
+ const MarkdownIt = (await lib('markdown-it')).default;
90
+ const md = new MarkdownIt('default', { html: false, linkify: false, typographer: false });
91
+ const toks = md.parse(String(src || ''), {});
92
+ const blocks = [];
93
+ const lists = [];
94
+ let group = 0, quote = 0, table = null, row = null, item = null, heading = 0;
95
+ for (const t of toks) {
96
+ switch (t.type) {
97
+ case 'heading_open': heading = Number(t.tag.slice(1)); break;
98
+ case 'heading_close': heading = 0; break;
99
+ case 'blockquote_open': quote++; break;
100
+ case 'blockquote_close': quote--; break;
101
+ case 'bullet_list_open': lists.push({ ordered: false, n: 0 }); break;
102
+ case 'ordered_list_open': lists.push({ ordered: true, n: Number(t.attrGet('start') || 1) - 1, group: ++group }); break;
103
+ case 'bullet_list_close': case 'ordered_list_close': lists.pop(); break;
104
+ case 'list_item_open': { const l = lists[lists.length - 1]; l.n++; item = { first: true }; break; }
105
+ case 'list_item_close': item = null; break;
106
+ case 'table_open': table = { rows: [], header: false }; break;
107
+ case 'table_close': blocks.push({ type: 'table', ...table }); table = null; break;
108
+ case 'thead_open': table.header = true; break;
109
+ case 'tr_open': row = []; break;
110
+ case 'tr_close': table.rows.push(row); row = null; break;
111
+ case 'fence': case 'code_block': blocks.push({ type: 'code', text: cleanText(t.content.replace(/\n$/, '')) }); break;
112
+ case 'hr': blocks.push({ type: 'hr' }); break;
113
+ case 'inline': {
114
+ const runs = inlineRuns(t);
115
+ if (row) { row.push(runs); break; }
116
+ if (heading) { blocks.push({ type: 'heading', level: heading, runs }); break; }
117
+ if (item && lists.length) {
118
+ const l = lists[lists.length - 1];
119
+ blocks.push({ type: 'item', level: lists.length - 1, ordered: l.ordered, n: l.n, group: l.group || 0, runs, cont: !item.first, quote });
120
+ item.first = false;
121
+ break;
122
+ }
123
+ blocks.push({ type: 'para', runs, quote });
124
+ break;
125
+ }
126
+ default: break;
127
+ }
128
+ }
129
+ return blocks;
130
+ }
131
+
132
+ const ISO = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
133
+ /** "2026-10-20" / "2026-10-20T14:30" → a Date with that wall-clock time in UTC (Excel has no zones), else null. */
134
+ export function isoDate(s) {
135
+ const m = ISO.exec(s);
136
+ if (!m) return null;
137
+ const [y, mo, d, h = 0, mi = 0, se = 0] = m.slice(1).map((x) => (x === undefined ? 0 : Number(x)));
138
+ const dt = new Date(Date.UTC(y, mo - 1, d, h, mi, se));
139
+ if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d || h > 23 || mi > 59 || se > 59) return null;
140
+ return { date: dt, time: m[4] !== undefined };
141
+ }
142
+
143
+ function checkSheets(sheets) {
144
+ if (sheets === undefined) return [];
145
+ if (!Array.isArray(sheets)) fail('BAD_ARGS', 'sheets must be a list of tables.');
146
+ if (sheets.length > MAX_SHEETS) fail('TOO_BIG', `At most ${MAX_SHEETS} sheets at once.`);
147
+ let cells = 0;
148
+ return sheets.map((s, i) => {
149
+ if (!s || typeof s !== 'object' || !Array.isArray(s.rows)) fail('BAD_ARGS', `sheets[${i}].rows must be a list of rows.`);
150
+ if (s.name !== undefined && typeof s.name !== 'string') fail('BAD_ARGS', `sheets[${i}].name must be text.`);
151
+ for (const r of s.rows) {
152
+ if (!Array.isArray(r)) fail('BAD_ARGS', `sheets[${i}]: every row must be a list of cells.`);
153
+ cells += r.length;
154
+ if (cells > MAX_CELLS) fail('TOO_BIG', `Too many cells (more than ${MAX_CELLS.toLocaleString('en')}).`, 'Split the data into several files.');
155
+ for (const c of r) {
156
+ if (c !== null && !['string', 'number', 'boolean'].includes(typeof c)) fail('BAD_ARGS', `sheets[${i}]: a cell must be text, a number, true/false or null.`);
157
+ if (typeof c === 'number' && !Number.isFinite(c)) fail('BAD_ARGS', `sheets[${i}]: numbers must be finite.`);
158
+ if (typeof c === 'string' && c.length > MAX_CELL_TEXT) fail('TOO_BIG', `A cell is longer than ${MAX_CELL_TEXT} characters (the Excel limit).`);
159
+ }
160
+ }
161
+ return { name: s.name, header: s.header !== false, rows: s.rows };
162
+ });
163
+ }
164
+ const cellText = (c) => (c === null || c === undefined ? '' : String(c));
165
+ const sheetBlocks = (sheets) => sheets.flatMap((s) => [
166
+ ...(s.name ? [{ type: 'heading', level: 3, runs: [{ text: cleanText(s.name) }] }] : []),
167
+ { type: 'table', header: s.header, rows: s.rows.map((r) => r.map((c) => [{ text: cleanText(cellText(c)) }])) },
168
+ ]);
169
+
170
+ // ---- DOCX --------------------------------------------------------------------------------------
171
+ const x = (s) => String(s).replace(/[<>&"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c]));
172
+ function wRuns(runs) {
173
+ return runs.map((r) => {
174
+ if (r.text === '\n') return '<w:r><w:br/></w:r>';
175
+ // Element order inside rPr is fixed by the OOXML schema (Word refuses files that break it).
176
+ const pr = `${r.code ? '<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/>' : ''}${r.b ? '<w:b/>' : ''}${r.i ? '<w:i/>' : ''}${r.code ? '<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/>' : ''}`;
177
+ return `<w:r>${pr ? `<w:rPr>${pr}</w:rPr>` : ''}<w:t xml:space="preserve">${x(r.text)}</w:t></w:r>`;
178
+ }).join('');
179
+ }
180
+ const wP = (runs, ppr = '') => `<w:p>${ppr ? `<w:pPr>${ppr}</w:pPr>` : ''}${wRuns(runs)}</w:p>`;
181
+ const TEXT_W = 9638; // A4 minus 2 cm margins, twips
182
+
183
+ function docxBody(title, blocks) {
184
+ const out = [];
185
+ const nums = new Map(); // ordered group → { numId, level }
186
+ if (title) out.push(wP([{ text: title }], '<w:pStyle w:val="Title"/>'));
187
+ for (const bl of blocks) {
188
+ if (bl.type === 'heading') out.push(wP(bl.runs, `<w:pStyle w:val="Heading${Math.min(bl.level, 3)}"/>`));
189
+ else if (bl.type === 'para') out.push(wP(bl.runs, bl.quote ? '<w:pStyle w:val="Quote"/>' : ''));
190
+ else if (bl.type === 'item') {
191
+ const ind = 720 * (bl.level + 1);
192
+ if (bl.cont) { out.push(wP(bl.runs, `<w:ind w:left="${ind}"/>`)); continue; }
193
+ let numId = 1;
194
+ if (bl.ordered) { if (!nums.has(bl.group)) nums.set(bl.group, { numId: nums.size + 2, level: bl.level, start: bl.n }); numId = nums.get(bl.group).numId; }
195
+ out.push(wP(bl.runs, `<w:pStyle w:val="ListParagraph"/><w:numPr><w:ilvl w:val="${Math.min(bl.level, 8)}"/><w:numId w:val="${numId}"/></w:numPr>`));
196
+ } else if (bl.type === 'code') for (const line of bl.text.split('\n')) out.push(wP([{ text: line }], '<w:pStyle w:val="Code"/>'));
197
+ else if (bl.type === 'hr') out.push('<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="999999"/></w:pBdr></w:pPr></w:p>');
198
+ else if (bl.type === 'table') {
199
+ const cols = Math.max(1, ...bl.rows.map((r) => r.length));
200
+ const w = Math.floor(TEXT_W / cols);
201
+ const rows = bl.rows.map((r, ri) => {
202
+ const head = bl.header && ri === 0;
203
+ const cells = Array.from({ length: cols }, (_, ci) => {
204
+ const runs = (r[ci] || []).map((run) => (head ? { ...run, b: true } : run));
205
+ return `<w:tc><w:tcPr><w:tcW w:w="${w}" w:type="dxa"/>${head ? '<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/>' : ''}</w:tcPr>${wP(runs, '<w:spacing w:after="0"/>')}</w:tc>`;
206
+ }).join('');
207
+ return `<w:tr>${head ? '<w:trPr><w:tblHeader/></w:trPr>' : ''}${cells}</w:tr>`;
208
+ }).join('');
209
+ out.push(`<w:tbl><w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="5000" w:type="pct"/></w:tblPr><w:tblGrid>${`<w:gridCol w:w="${w}"/>`.repeat(cols)}</w:tblGrid>${rows}</w:tbl>`, '<w:p/>');
210
+ }
211
+ }
212
+ return { body: out.join(''), nums: [...nums.values()] };
213
+ }
214
+
215
+ const W_NS = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"';
216
+ const STYLES = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
217
+ <w:styles ${W_NS}><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:eastAsia="Calibri" w:cs="Calibri"/><w:sz w:val="22"/><w:szCs w:val="22"/><w:lang w:val="en-US" w:eastAsia="en-US" w:bidi="ar-SA"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="120" w:line="264" w:lineRule="auto"/></w:pPr></w:pPrDefault></w:docDefaults>
218
+ <w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style>
219
+ <w:style w:type="paragraph" w:styleId="Title"><w:name w:val="Title"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="240"/></w:pPr><w:rPr><w:sz w:val="48"/><w:szCs w:val="48"/></w:rPr></w:style>
220
+ ${[1, 2, 3].map((n) => `<w:style w:type="paragraph" w:styleId="Heading${n}"><w:name w:val="heading ${n}"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:keepNext/><w:spacing w:before="${[0, 360, 280, 200][n]}" w:after="80"/><w:outlineLvl w:val="${n - 1}"/></w:pPr><w:rPr><w:b/><w:color w:val="1F3864"/><w:sz w:val="${[0, 36, 30, 26][n]}"/><w:szCs w:val="${[0, 36, 30, 26][n]}"/></w:rPr></w:style>`).join('\n')}
221
+ <w:style w:type="paragraph" w:styleId="ListParagraph"><w:name w:val="List Paragraph"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:after="40"/><w:contextualSpacing/></w:pPr></w:style>
222
+ <w:style w:type="paragraph" w:styleId="Quote"><w:name w:val="Quote"/><w:basedOn w:val="Normal"/><w:qFormat/><w:pPr><w:pBdr><w:left w:val="single" w:sz="12" w:space="8" w:color="BFBFBF"/></w:pBdr><w:ind w:left="567"/></w:pPr><w:rPr><w:i/><w:color w:val="595959"/></w:rPr></w:style>
223
+ <w:style w:type="paragraph" w:styleId="Code"><w:name w:val="Code"/><w:basedOn w:val="Normal"/><w:pPr><w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/><w:spacing w:after="0" w:line="240" w:lineRule="auto"/></w:pPr><w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/><w:sz w:val="19"/><w:szCs w:val="19"/></w:rPr></w:style>
224
+ <w:style w:type="table" w:default="1" w:styleId="TableNormal"><w:name w:val="Normal Table"/><w:tblPr><w:tblInd w:w="0" w:type="dxa"/><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="108" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="108" w:type="dxa"/></w:tblCellMar></w:tblPr></w:style>
225
+ <w:style w:type="table" w:styleId="TableGrid"><w:name w:val="Table Grid"/><w:basedOn w:val="TableNormal"/><w:tblPr><w:tblBorders>${['top', 'left', 'bottom', 'right', 'insideH', 'insideV'].map((s) => `<w:${s} w:val="single" w:sz="4" w:space="0" w:color="A6A6A6"/>`).join('')}</w:tblBorders></w:tblPr></w:style>
226
+ </w:styles>`;
227
+ const lvls = (fmt) => Array.from({ length: 9 }, (_, l) => `<w:lvl w:ilvl="${l}"><w:start w:val="1"/><w:numFmt w:val="${fmt}"/><w:lvlText w:val="${fmt === 'bullet' ? ['•', '◦', '▪'][l % 3] : `%${l + 1}.`}"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="${720 * (l + 1)}" w:hanging="360"/></w:pPr></w:lvl>`).join('');
228
+ const numbering = (nums) => `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
229
+ <w:numbering ${W_NS}><w:abstractNum w:abstractNumId="0"><w:multiLevelType w:val="hybridMultilevel"/>${lvls('bullet')}</w:abstractNum><w:abstractNum w:abstractNumId="1"><w:multiLevelType w:val="hybridMultilevel"/>${lvls('decimal')}</w:abstractNum><w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>${nums.map((n) => `<w:num w:numId="${n.numId}"><w:abstractNumId w:val="1"/><w:lvlOverride w:ilvl="${Math.min(n.level, 8)}"><w:startOverride w:val="${Math.max(1, n.start)}"/></w:lvlOverride></w:num>`).join('')}</w:numbering>`;
230
+
231
+ export async function buildDocx({ title, blocks }) {
232
+ const JSZip = (await lib('jszip')).default;
233
+ const { body, nums } = docxBody(title, blocks);
234
+ const now = new Date().toISOString().replace(/\.\d+Z$/, 'Z');
235
+ const zip0 = new JSZip();
236
+ // No folder entries in the package (Office's own files have none).
237
+ const zip = { file: (n, d) => zip0.file(n, d, { createFolders: false }), generateAsync: (o) => zip0.generateAsync(o) };
238
+ zip.file('[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>');
239
+ zip.file('_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>');
240
+ zip.file('word/_rels/document.xml.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/></Relationships>');
241
+ zip.file('docProps/core.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${title ? `<dc:title>${x(title)}</dc:title>` : ''}<dc:creator>Bridgerator</dc:creator><dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`);
242
+ zip.file('word/styles.xml', STYLES);
243
+ zip.file('word/numbering.xml', numbering(nums));
244
+ zip.file('word/document.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:document ${W_NS}><w:body>${body || '<w:p/>'}<w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1134" w:right="1134" w:bottom="1134" w:left="1134" w:header="567" w:footer="567" w:gutter="0"/></w:sectPr></w:body></w:document>`);
245
+ return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
246
+ }
247
+
248
+ // ---- XLSX --------------------------------------------------------------------------------------
249
+ const NUM = /^-?(0|[1-9]\d{0,14})(\.\d+)?$/;
250
+ function sheetName(n, i, used) {
251
+ let s = cleanText(n || `Sheet${i + 1}`).replace(/[[\]:*?/\\]/g, ' ').replace(/^'+|'+$/g, '').trim().slice(0, 31) || `Sheet${i + 1}`;
252
+ let k = 2;
253
+ const base = s;
254
+ while (used.has(s.toLowerCase())) s = `${base.slice(0, 27)} (${k++})`;
255
+ used.add(s.toLowerCase());
256
+ return s;
257
+ }
258
+ export async function buildXlsx({ title, sheets }) {
259
+ const ExcelJS = (await lib('exceljs')).default;
260
+ const wb = new ExcelJS.Workbook();
261
+ wb.creator = 'Bridgerator';
262
+ if (title) wb.title = title;
263
+ wb.calcProperties = { fullCalcOnLoad: true };
264
+ const used = new Set();
265
+ sheets.forEach((s, si) => {
266
+ const ws = wb.addWorksheet(sheetName(s.name, si, used));
267
+ const widths = [];
268
+ s.rows.forEach((r, ri) => {
269
+ const row = ws.getRow(ri + 1);
270
+ r.forEach((c, ci) => {
271
+ const cell = row.getCell(ci + 1);
272
+ let shown = cellText(c);
273
+ if (typeof c === 'string') {
274
+ const d = !(s.header && ri === 0) && isoDate(c.trim());
275
+ if (s.textOnly) {
276
+ if (NUM.test(c.trim()) && !(s.header && ri === 0)) cell.value = Number(c.trim());
277
+ else if (d) { cell.value = d.date; cell.numFmt = d.time ? 'yyyy-mm-dd hh:mm' : 'yyyy-mm-dd'; } else cell.value = cleanText(c);
278
+ } else if (c.startsWith('=') && c.length > 1) { cell.value = { formula: c.slice(1) }; shown = ''; }
279
+ else if (d) { cell.value = d.date; cell.numFmt = d.time ? 'yyyy-mm-dd hh:mm' : 'yyyy-mm-dd'; } else cell.value = cleanText(c);
280
+ } else if (c !== null && c !== undefined) cell.value = c;
281
+ widths[ci] = Math.max(widths[ci] || 0, Math.min(60, shown.length));
282
+ });
283
+ if (s.header && ri === 0) row.font = { bold: true };
284
+ });
285
+ widths.forEach((w, i) => { ws.getColumn(i + 1).width = Math.max(8, w + 2); });
286
+ if (s.header && s.rows.length > 1) ws.views = [{ state: 'frozen', ySplit: 1 }];
287
+ });
288
+ return Buffer.from(await wb.xlsx.writeBuffer());
289
+ }
290
+
291
+ // ---- PDF ---------------------------------------------------------------------------------------
292
+ const A4 = [595.28, 841.89], M = 56, BODY = 11;
293
+ const keyOf = (r) => (r.code ? 'm' : r.b && r.i ? 'bi' : r.b ? 'b' : r.i ? 'i' : 'r');
294
+
295
+ /** Lays the blocks out into pages of draw items with the fonts' metrics (pass 1, no PDF yet). */
296
+ export function layoutPdf({ title, blocks }, fonts) {
297
+ const [W, H] = A4, width = W - 2 * M;
298
+ const pages = [];
299
+ let page, y;
300
+ const newPage = () => { page = []; pages.push(page); y = H - M; if (pages.length > MAX_PAGES) fail('TOO_BIG', `The PDF would have more than ${MAX_PAGES} pages.`, 'Split the content into several files.'); };
301
+ newPage();
302
+ const f = (k) => fonts[k] || fonts.r;
303
+ const has = (k, ch) => f(k).cmap.has(ch.codePointAt(0));
304
+ const fix = (k, t) => Array.from(t, (ch) => (has(k, ch) || ch === ' ' || ch === ' ' ? ch : (has('r', ch) ? ch : '?'))).join('');
305
+ // A no-break space (code-block indentation) measures as a space when the font has no glyph for it.
306
+ const wOf = (k, t, size) => { const ft = f(k); let s = 0; for (const ch of t) { const c = ch.codePointAt(0); const g = ft.cmap.get(c) || (c === 0xa0 ? ft.cmap.get(32) : 0) || 0; s += ft.advance(g); } return (s * size) / ft.unitsPerEm; };
307
+ const asc = (size) => (fonts.r.ascent / fonts.r.unitsPerEm) * size;
308
+ const lh = (size) => ((fonts.r.ascent - fonts.r.descent) / fonts.r.unitsPerEm) * size * 1.08;
309
+
310
+ /** runs → lines of segments [{ text, k, dx }] within maxW. */
311
+ function wrap(runs, size, maxW) {
312
+ const lines = [];
313
+ let line = [], lw = 0;
314
+ const flush = () => { while (line.length && !line[line.length - 1].text.trim()) { lw -= line[line.length - 1].w; line.pop(); } lines.push(line); line = []; lw = 0; };
315
+ const add = (text, k) => {
316
+ const w = wOf(k, text, size);
317
+ const last = line[line.length - 1];
318
+ if (last && last.k === k) { last.text += text; last.w += w; } else line.push({ text, k, w });
319
+ lw += w;
320
+ };
321
+ for (const r of runs) {
322
+ if (r.text === '\n') { flush(); continue; }
323
+ const k = keyOf(r);
324
+ // Breaks at white space except the no-break space (it keeps code indentation and runs of spaces).
325
+ for (const piece of fix(k, r.text).split(/([^\S ]+)/)) {
326
+ if (!piece) continue;
327
+ if (/^[^\S ]+$/.test(piece)) { if (line.length) add(' ', k); continue; }
328
+ const w = wOf(k, piece, size);
329
+ if (lw + w <= maxW) { add(piece, k); continue; }
330
+ if (line.length) flush();
331
+ if (w <= maxW) { add(piece, k); continue; }
332
+ let rest = piece;
333
+ while (rest) {
334
+ let n = 1;
335
+ const chars = Array.from(rest);
336
+ while (n < chars.length && wOf(k, chars.slice(0, n + 1).join(''), size) <= maxW - lw) n++;
337
+ add(chars.slice(0, n).join(''), k);
338
+ rest = chars.slice(n).join('');
339
+ if (rest) flush();
340
+ }
341
+ }
342
+ }
343
+ if (line.length || !lines.length) flush();
344
+ return lines.map((l) => { let dx = 0; return l.map((s) => { const o = { text: s.text, k: s.k, dx }; dx += s.w; return o; }); });
345
+ }
346
+ const need = (h) => { if (y - h < M) newPage(); };
347
+ const text = (segs, x, top, size, color) => { for (const s of segs) if (s.text.trim()) page.push({ t: 'text', text: s.text, k: s.k, x: x + s.dx, y: top - asc(size), size, color }); };
348
+ function paragraph(runs, { size = BODY, indent = 0, color, before = 0, after = 6, marker, bar } = {}) {
349
+ const lines = wrap(runs, size, width - indent);
350
+ const h = lh(size);
351
+ y -= before;
352
+ lines.forEach((segs, i) => {
353
+ need(h);
354
+ if (i === 0 && marker) text([{ text: marker, k: 'r', dx: 0 }], M + indent - wOf('r', marker, size) - 6, y, size, color);
355
+ if (bar) page.push({ t: 'rect', x: M + indent - 10, y: y - h, w: 2, h, color: [0.75, 0.75, 0.75] });
356
+ text(segs, M + indent, y, size, color);
357
+ y -= h;
358
+ });
359
+ y -= after;
360
+ }
361
+ function table(bl) {
362
+ const size = 10, pad = 4, h = lh(size);
363
+ const cols = Math.max(1, ...bl.rows.map((r) => r.length));
364
+ const runsAt = (r, c, ri) => (r[c] || []).map((x) => (bl.header && ri === 0 ? { ...x, b: true } : x));
365
+ const nat = Array(cols).fill(0);
366
+ bl.rows.forEach((r, ri) => { for (let c = 0; c < cols; c++) nat[c] = Math.max(nat[c], runsAt(r, c, ri).reduce((s, x) => s + wOf(keyOf(x), fix(keyOf(x), x.text), size), 0)); });
367
+ for (let c = 0; c < cols; c++) nat[c] += 2 * pad + 1;
368
+ let cw;
369
+ const total = nat.reduce((a, b) => a + b, 0);
370
+ if (total <= width) cw = nat.map((n) => n + (width - total) / cols);
371
+ else {
372
+ const fair = width / cols;
373
+ cw = nat.map((n) => Math.min(n, fair));
374
+ const left = width - cw.reduce((a, b) => a + b, 0);
375
+ const want = nat.map((n, i) => n - cw[i]), sumWant = want.reduce((a, b) => a + b, 0);
376
+ if (sumWant > 0) cw = cw.map((w, i) => w + (left * want[i]) / sumWant);
377
+ }
378
+ const maxLines = Math.max(1, Math.floor((A4[1] - 2 * M - 2 * pad - (bl.header ? h + 2 * pad : 0)) / h) - 1);
379
+ const rowsLaid = bl.rows.map((r, ri) => {
380
+ const cells = cw.map((w, c) => wrap(runsAt(r, c, ri), size, w - 2 * pad).slice(0, maxLines));
381
+ return { cells, height: Math.max(...cells.map((l) => l.length)) * h + 2 * pad, head: bl.header && ri === 0 };
382
+ });
383
+ const drawRow = (row) => {
384
+ let x = M;
385
+ row.cells.forEach((lines, c) => {
386
+ page.push({ t: 'rect', x, y: y - row.height, w: cw[c], h: row.height, border: true, fill: row.head ? [0.95, 0.95, 0.95] : null });
387
+ lines.forEach((segs, i) => text(segs, x + pad, y - pad - i * h, size));
388
+ x += cw[c];
389
+ });
390
+ y -= row.height;
391
+ };
392
+ y -= 4;
393
+ rowsLaid.forEach((row, i) => {
394
+ if (y - row.height < M) { newPage(); if (bl.header && i > 0) drawRow(rowsLaid[0]); }
395
+ drawRow(row);
396
+ });
397
+ y -= 10;
398
+ }
399
+ if (title) paragraph([{ text: cleanText(title), b: true }], { size: 22, after: 12 });
400
+ for (const bl of blocks) {
401
+ if (bl.type === 'heading') paragraph(bl.runs.map((r) => ({ ...r, b: true })), { size: [0, 18, 15, 13, 12, 12, 12][bl.level] || 12, before: 8, after: 4 });
402
+ else if (bl.type === 'para') paragraph(bl.runs, bl.quote ? { indent: 14 * bl.quote, color: [0.35, 0.35, 0.35], bar: true } : {});
403
+ else if (bl.type === 'item') {
404
+ const indent = 18 * (bl.level + 1);
405
+ const bullet = has('r', '•') ? '•' : '-';
406
+ paragraph(bl.runs, { indent, after: 2, marker: bl.cont ? undefined : (bl.ordered ? `${bl.n}.` : bullet) });
407
+ } else if (bl.type === 'code') {
408
+ const size = 9.5, h = lh(size);
409
+ y -= 2;
410
+ for (const raw of bl.text.split('\n')) {
411
+ for (const segs of wrap([{ text: raw.replace(/ /g, '\u00a0') || '\u00a0', code: true }], size, width - 8)) {
412
+ need(h);
413
+ page.push({ t: 'rect', x: M, y: y - h, w: width, h, fill: [0.95, 0.95, 0.95] });
414
+ text(segs.map((s) => ({ ...s, text: s.text.replace(/\u00a0/g, ' ') })), M + 4, y, size);
415
+ y -= h;
416
+ }
417
+ }
418
+ y -= 8;
419
+ } else if (bl.type === 'hr') { need(12); page.push({ t: 'line', x1: M, x2: M + width, y: y - 6 }); y -= 12; }
420
+ else if (bl.type === 'table') table(bl);
421
+ }
422
+ if (pages.length > 1) pages.forEach((p, i) => { const s = `${i + 1} / ${pages.length}`; p.push({ t: 'text', text: s, k: 'r', x: W - M - wOf('r', s, 8), y: M / 2, size: 8, color: [0.5, 0.5, 0.5] }); });
423
+ return pages;
424
+ }
425
+
426
+ export async function buildPdf({ title, blocks }, { fonts, fontDir } = {}) {
427
+ let fs0;
428
+ try { fs0 = fonts || loadSystemFonts(fontDir); } catch (e) { if (e instanceof FontError) fail('NO_FONT', 'No usable system font for PDF was found on this PC.', 'Create a .docx instead, or check that C:\\Windows\\Fonts has Segoe UI or Arial.'); throw e; }
429
+ const pages = layoutPdf({ title, blocks }, fs0);
430
+ const { PDFDocument, rgb } = await lib('pdf-lib');
431
+ const doc = await PDFDocument.create();
432
+ doc.registerFontkit(MINI_FONTKIT);
433
+ doc.setCreator('Bridgerator'); doc.setProducer('Bridgerator');
434
+ if (title) doc.setTitle(title);
435
+ const usedBy = {};
436
+ for (const p of pages) for (const it of p) if (it.t === 'text') { const set = (usedBy[it.k] ||= new Set()); for (const ch of it.text) set.add(ch.codePointAt(0)); }
437
+ const embedded = {};
438
+ for (const [k, set] of Object.entries(usedBy)) embedded[k] = await doc.embedFont(subsetTtf(fs0[k] || fs0.r, set), { subset: false });
439
+ const grey = rgb(0.65, 0.65, 0.65);
440
+ for (const items of pages) {
441
+ const page = doc.addPage(A4);
442
+ for (const it of items) {
443
+ if (it.t === 'rect') page.drawRectangle({ x: it.x, y: it.y, width: it.w, height: it.h, ...(it.fill ? { color: rgb(...it.fill) } : {}), ...(it.color ? { color: rgb(...it.color) } : {}), ...(it.border ? { borderColor: grey, borderWidth: 0.5 } : {}) });
444
+ else if (it.t === 'line') page.drawLine({ start: { x: it.x1, y: it.y }, end: { x: it.x2, y: it.y }, thickness: 0.7, color: grey });
445
+ }
446
+ for (const it of items) if (it.t === 'text') page.drawText(it.text, { x: it.x, y: it.y, size: it.size, font: embedded[it.k], ...(it.color ? { color: rgb(...it.color) } : {}) });
447
+ }
448
+ return { bytes: Buffer.from(await doc.save()), pages: pages.length };
449
+ }
450
+
451
+ // ---- the tool ----------------------------------------------------------------------------------
452
+ export async function handle(name, args, ctx) {
453
+ const action = needAction(args, ACTIONS);
454
+ const want = str(args, 'path', { required: true, max: 1000 });
455
+ if (path.extname(want).toLowerCase() !== `.${action}`) fail('BAD_PATH', `The file name must end with .${action} for action "${action}".`, `Use a path like ...\\Report.${action}.`);
456
+ // One line without control characters (a control character makes the docx / xlsx XML invalid).
457
+ const title = str(args, 'title', { max: 300 }) === undefined ? undefined : cleanText(args.title).replace(/\s+/g, ' ').trim() || undefined;
458
+ const markdown = str(args, 'markdown', { max: MAX_MD });
459
+ const sheets = checkSheets(args.sheets);
460
+ if (!markdown?.trim() && !sheets.length && !title) fail('EMPTY', 'There is nothing to put in the document.', 'Pass markdown text and/or sheets.');
461
+ const blocks = markdown ? await parseMarkdown(markdown) : [];
462
+ let bytes, extra = {};
463
+ if (action === 'xlsx') {
464
+ let all = sheets;
465
+ if (!all.length) {
466
+ // Markdown tables → one sheet each, named by the heading above them.
467
+ let lastHead = '';
468
+ all = [];
469
+ for (const b of blocks) {
470
+ if (b.type === 'heading') lastHead = plain(b.runs);
471
+ if (b.type === 'table') all.push({ name: lastHead || undefined, header: b.header, rows: b.rows.map((r) => r.map(plain)), textOnly: true });
472
+ }
473
+ if (!all.length) fail('NO_TABLE', 'An Excel file needs a table.', 'Pass sheets: [{ name, rows: [[...]] }] or a markdown table.');
474
+ }
475
+ bytes = await buildXlsx({ title, sheets: all });
476
+ extra = { sheets: all.length };
477
+ } else {
478
+ const model = { title, blocks: [...blocks, ...sheetBlocks(sheets)] };
479
+ if (action === 'docx') bytes = await buildDocx(model);
480
+ else { const r = await buildPdf(model, { fonts: ctx?.fonts, fontDir: ctx?.fontDir }); bytes = r.bytes; extra = { pages: r.pages }; }
481
+ }
482
+ let target = freeName(guard(ctx, want, { write: true }));
483
+ if (target !== path.resolve(want)) guard(ctx, target, { write: true });
484
+ // Same confirmation as a file write ("Writing files" category), when it is on.
485
+ const stop = await ctx?.gate?.gate?.('write_file', { path: target });
486
+ if (stop) return stop;
487
+ fs.mkdirSync(path.dirname(target), { recursive: true });
488
+ try { fs.writeFileSync(target, bytes, { flag: 'wx' }); } catch (e) {
489
+ if (e.code !== 'EEXIST') throw e;
490
+ target = freeName(target); // created by someone else in between
491
+ fs.writeFileSync(target, bytes, { flag: 'wx' });
492
+ }
493
+ return json({ created: target, bytes: bytes.length, ...extra, ...(target !== path.resolve(want) ? { note: 'A file with the requested name already existed; it was left as is.' } : {}) });
494
+ }