create-pmos 0.178.0 → 0.180.0

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.
Files changed (59) hide show
  1. package/package.json +1 -1
  2. package/payload/KIT-VERSION +1 -1
  3. package/payload/planning/evals/eval-calibration.md +1 -1
  4. package/payload/planning/okrs/health-budgets.md +1 -1
  5. package/payload/state/discoveries.md +1 -1
  6. package/workspace/c3-report-setup-test.js +190 -0
  7. package/workspace/c3-reports-test.js +30 -97
  8. package/workspace/guide/console-calendar.md +17 -2
  9. package/workspace/guide/document-sheet.md +14 -0
  10. package/workspace/guide/index.md +3 -2
  11. package/workspace/guide/reports-dashboards.md +17 -28
  12. package/workspace/guide/reports-page.md +10 -5
  13. package/workspace/guide/reports-set-up.md +56 -0
  14. package/workspace/guide/reports.md +13 -1
  15. package/workspace/guide/settings.md +11 -2
  16. package/workspace/lib/doc-sync.js +28 -6
  17. package/workspace/lib/docs-cli.js +7 -11
  18. package/workspace/lib/four-panel.js +1 -1
  19. package/workspace/lib/headless.js +21 -2
  20. package/workspace/lib/notes-series.js +357 -0
  21. package/workspace/lib/notes.js +53 -4
  22. package/workspace/lib/report-checks.js +103 -0
  23. package/workspace/lib/report-cli.js +89 -4
  24. package/workspace/lib/report-export.js +347 -0
  25. package/workspace/lib/report-finder.js +156 -0
  26. package/workspace/lib/report-needs.js +1 -1
  27. package/workspace/lib/report-setups.js +743 -0
  28. package/workspace/lib/route-modules.js +1 -0
  29. package/workspace/lib/routes-c3-reports.js +35 -17
  30. package/workspace/lib/routes-doc-health.js +15 -1
  31. package/workspace/lib/routes-notes.js +27 -2
  32. package/workspace/lib/routes-report-setups.js +368 -0
  33. package/workspace/lib/schedule-logic.js +5 -2
  34. package/workspace/package-lock.json +2 -2
  35. package/workspace/package.json +1 -1
  36. package/workspace/public/pm/c3-demo-rep2.js +15 -0
  37. package/workspace/public/pm/c3-demo-rs.js +281 -0
  38. package/workspace/public/pm/c3-demo.js +1 -1
  39. package/workspace/public/pm/c3-doc-formats.js +152 -0
  40. package/workspace/public/pm/c3-doc-sheet.js +115 -25
  41. package/workspace/public/pm/c3-notes.js +299 -34
  42. package/workspace/public/pm/c3-quiet.js +2 -2
  43. package/workspace/public/pm/c3-report-print.css +20 -0
  44. package/workspace/public/pm/c3-report-setup.css +62 -0
  45. package/workspace/public/pm/c3-report-setup.js +260 -0
  46. package/workspace/public/pm/c3-report-store.js +208 -0
  47. package/workspace/public/pm/c3-reports.css +4 -0
  48. package/workspace/public/pm/c3-reports.js +47 -181
  49. package/workspace/public/pm/c3-settings.js +4 -3
  50. package/workspace/public/pm/c3-shell.js +15 -31
  51. package/workspace/public/pm/console3.css +13 -0
  52. package/workspace/run-tests.js +25 -1
  53. package/workspace/server.js +66 -5
  54. package/workspace/skills/doc-sync.skill +17 -8
  55. package/workspace/skills/report-find.skill +75 -0
  56. package/workspace/skills/report-setup.skill +67 -0
  57. package/workspace/test-files.json +12 -0
  58. package/workspace/test-lists/c3-notes-series.json +5 -0
  59. package/workspace/test-lists/report-workspace-ui.json +5 -0
@@ -1,10 +1,14 @@
1
1
  'use strict';
2
2
  /* report-cli.js — `pmos report …`: draft a cadence report, issue it, list the reports (pm-os-absorb Run 5, PRD R22,
3
- * R24, R25; D86 points 2 and 3).
3
+ * R24, R25; D86 points 2 and 3), and the report workspace's verbs (initiative report-workspace).
4
4
  *
5
5
  * pmos report draft <cadence> [<slot>] [--now <time>] [--redraft] [--json] [--root <dir>]
6
6
  * pmos report issue <cadence> <slot> [--root <dir>]
7
7
  * pmos report list [<cadence>] [--json] [--root <dir>]
8
+ * pmos report propose --json <file|-> a report-setup or report-find run files its proposal or finding (agent-allowed)
9
+ * pmos report definitions [--json] the report definitions (read-only)
10
+ * pmos report setups [--json] the recurring reports (read-only)
11
+ * pmos report finder <weekly|off> the PM's: how often PMOS looks for reports you send (also: finder source <name> <on|off>)
8
12
  *
9
13
  * `draft` is the script with no model (R22.1): lib/report-draft.js folds the ledgers since the last issue and writes
10
14
  * every number into `<state>/reports/<cadence>/<slot>.md`, with one empty note marker per block. It refuses a slot that
@@ -16,6 +20,11 @@
16
20
  * written. Then lib/issues.js appendFromCli records the path and the git blob sha, which freezes the report. Nothing
17
21
  * is sent: print the file or copy it as markdown.
18
22
  *
23
+ * `propose` is the ONLY write a background report run has (lib/headless.js kinds report-setup and report-find). The server starts the run with
24
+ * PMOS_REPORT_RUN (`setup:<ask>` or `find:<run>`) and PMOS_REPORT_FILE (the ledger), which the model cannot change: a setup run files the
25
+ * proposal of its own ask and nothing else; a find run files findings and nothing else. It answers `filed`, `duplicate` or `dismissed` (the PM
26
+ * said Not a report for that pattern). A proposal is data; only a PM press on the page sets a report up (lib/routes-report-setups.js).
27
+ *
19
28
  * Who writes is decided by the call path, never by an argument (lib/actor.js). The verb never calls the server.
20
29
  */
21
30
  const fs = require('fs');
@@ -25,9 +34,10 @@ const reportDraft = require('./report-draft.js');
25
34
  const issues = require('./issues.js');
26
35
  const guard = require('./pm-guard.js');
27
36
  const actorLib = require('./actor.js');
37
+ const RS = require('./report-setups.js');
28
38
  const { parseArgs } = require('./schedule-cli');
29
39
 
30
- const COMMANDS = { report: ['draft', 'issue', 'list'] };
40
+ const COMMANDS = { report: ['draft', 'issue', 'list', 'propose', 'definitions', 'setups', 'finder'] };
31
41
  const USAGE = [
32
42
  'usage: pmos report draft|issue|list — cadence reports (for example the biweekly)',
33
43
  ' pmos report draft <cadence> [<slot>] [--now <time>] [--redraft]',
@@ -37,11 +47,20 @@ const USAGE = [
37
47
  ' Freeze the draft. Only the PM can do this, at a terminal, by typing <cadence>-<slot>. Nothing is sent.',
38
48
  ' pmos report list [<cadence>]',
39
49
  ' Show the drafts and the issued reports.',
50
+ ' pmos report propose --json <file|->',
51
+ ' A report-setup or report-find run files its proposal or finding (JSON, from a file or standard input). It answers filed, duplicate',
52
+ ' or dismissed. A proposal is data: nothing is set up until the PM presses Set up on the Reports page.',
53
+ ' pmos report definitions',
54
+ ' Show the report definitions (read-only).',
55
+ ' pmos report setups',
56
+ ' Show the recurring reports (read-only).',
57
+ ' pmos report finder <weekly|off> | pmos report finder source <calendar|slack|mail|drive> <on|off>',
58
+ ' Only the PM can change how PMOS looks for the reports you send (default weekly; mail is off until you turn it on).',
40
59
  ' every command: [--json] [--root <dir>]. The cadence is the report skill id without "report-", for example biweekly.',
41
60
  ];
42
61
  const BOOL = new Set(['json', 'redraft', 'help']);
43
62
  const VALUE = new Set(['now', 'root']);
44
- const ALLOWED = { draft: ['json', 'redraft', 'now', 'root'], issue: ['root'], list: ['json', 'root'] };
63
+ const ALLOWED = { draft: ['json', 'redraft', 'now', 'root'], issue: ['root'], list: ['json', 'root'], propose: ['json', 'root'], definitions: ['json', 'root'], setups: ['json', 'root'], finder: ['json', 'root'] };
45
64
  const CADENCE_RE = /^[a-z][a-z0-9-]{0,39}$/;
46
65
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
47
66
 
@@ -62,7 +81,7 @@ function repoRoot(cwd) {
62
81
  }
63
82
  const isDate = (s) => typeof s === 'string' && DATE_RE.test(s) && new Date(s + 'T00:00:00Z').toISOString().slice(0, 10) === s;
64
83
 
65
- async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, ask, now } = {}) {
84
+ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isTTY, env, ask, now, stdin } = {}) {
66
85
  const args = argv[0] === 'report' ? argv.slice(1) : argv.slice();
67
86
  const verb = args[0];
68
87
  const { pos, flags } = parse(args.slice(1));
@@ -110,6 +129,8 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
110
129
  return 0;
111
130
  }
112
131
 
132
+ if (verb === 'definitions' || verb === 'setups' || verb === 'propose' || verb === 'finder') return reportWorkspace(verb, { pos, flags, root, json, say, fail, cwd, isTTY, env, stdin, now });
133
+
113
134
  const [cadence, slot] = pos;
114
135
  if (!cadence || !CADENCE_RE.test(cadence) || pos.length > 2) { say(`usage: pmos report ${verb} <cadence> ${verb === 'issue' ? '<slot>' : '[<slot>]'}`); return 2; }
115
136
  if (slot !== undefined && !isDate(slot)) return fail('the slot must be a date, YYYY-MM-DD', 2);
@@ -149,4 +170,68 @@ async function run(argv, { say = (s) => console.log(s), cwd = process.cwd(), isT
149
170
  return 0;
150
171
  }
151
172
 
173
+ /** The report workspace's verbs. `propose` is the one an agent run may call; `finder` is the PM's. */
174
+ function reportWorkspace(verb, { pos, flags, root, json, say, fail, isTTY, env, stdin, now }) {
175
+ const E = env === undefined ? process.env : env;
176
+ const stamp = now == null ? undefined : now;
177
+ if (verb === 'definitions') {
178
+ const defs = RS.definitionsOf(root);
179
+ if (json) { say(JSON.stringify({ ok: true, definitions: defs })); return 0; }
180
+ if (!defs.length) say('No report definition yet. Set one up on the Reports page, or ask PMOS there.');
181
+ for (const d of defs) say(`${d.id} ${d.output || 'no output'} ${d.audience || 'no audience'}${d.errors.length ? ` ! ${d.errors.join('; ')}` : ''} ${d.path}`);
182
+ return 0;
183
+ }
184
+ if (verb === 'setups') {
185
+ const st = RS.stateOf(root);
186
+ const setups = [...st.setups.values()].map((s) => ({ id: s.id, name: s.name, audience: s.audience, source: s.source, output: s.output, schedule: s.schedule, label: RS.scheduleLabel(s.schedule), status: s.status }));
187
+ if (json) { say(JSON.stringify({ ok: true, setups })); return 0; }
188
+ if (!setups.length) say('No recurring report is set up yet.');
189
+ for (const s of setups) say(`${s.id} ${s.status} ${s.label} ${s.output} ${s.source} ${s.name}`);
190
+ return 0;
191
+ }
192
+ const who = actorLib.fromCli({ ...(isTTY === undefined ? {} : { isTTY }), ...(env === undefined ? {} : { env }) });
193
+ if (verb === 'finder') {
194
+ // The PM's: an agent session cannot turn the finder or a source on or off.
195
+ if (!actorLib.isPm(who)) return fail(`only the PM can change the report finder, at an interactive terminal${actorLib.isAgent(who) && who.by !== 'agent:unknown' ? '; an agent session cannot' : ''}`);
196
+ const file = RS.ledgerFile(root);
197
+ let r;
198
+ if (pos[0] === 'source') {
199
+ if (!RS.FINDER_SOURCES.includes(pos[1]) || !['on', 'off'].includes(pos[2])) { say('usage: pmos report finder source <calendar|slack|mail|drive> <on|off>'); return 2; }
200
+ r = RS.setFinder(file, { sources: { [pos[1]]: pos[2] === 'on' }, by: who.by, now: stamp });
201
+ } else {
202
+ if (!RS.FINDER_CADENCES.includes(pos[0]) || pos.length !== 1) { say('usage: pmos report finder <weekly|off>'); return 2; }
203
+ r = RS.setFinder(file, { cadence: pos[0], by: who.by, now: stamp });
204
+ }
205
+ if (!r.ok) return fail(r.error);
206
+ if (json) { say(JSON.stringify({ ok: true, finder: r.finder })); return 0; }
207
+ say(`✓ The report finder is ${r.finder.cadence}. Sources on: ${RS.FINDER_SOURCES.filter((k) => r.finder.sources[k]).join(', ') || 'none'}.`);
208
+ return 0;
209
+ }
210
+ // propose
211
+ const run = String(E && E.PMOS_REPORT_RUN || '');
212
+ const m = /^(setup|find):([A-Za-z0-9._-]{1,40})$/.exec(run);
213
+ if (!m) return fail('pmos report propose is for a report-setup or report-find run. The PM sets a report up on the Reports page');
214
+ let file = E && typeof E.PMOS_REPORT_FILE === 'string' && E.PMOS_REPORT_FILE && path.basename(E.PMOS_REPORT_FILE) === RS.LEDGER ? E.PMOS_REPORT_FILE : null;
215
+ if (!file) file = RS.ledgerFile(root);
216
+ const src = pos[0];
217
+ if (!src) { say('usage: pmos report propose --json <file|->'); return 2; }
218
+ let text;
219
+ try { text = src === '-' ? (typeof stdin === 'string' ? stdin : fs.readFileSync(0, 'utf8')) : fs.readFileSync(path.resolve(src), 'utf8'); }
220
+ catch { return fail(`could not read ${src === '-' ? 'standard input' : src}`); }
221
+ let body;
222
+ try { body = JSON.parse(text); } catch { return fail('the proposal is not JSON'); }
223
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return fail('the proposal is a JSON object');
224
+ const done = (result, extra = {}) => { if (json) say(JSON.stringify({ ok: true, result, ...extra })); else say(result + (extra.id ? ` ${extra.id}` : '')); return 0; };
225
+ if (m[1] === 'setup') {
226
+ const r = RS.fileProposal(file, { ask: m[2], proposal: body.proposal && typeof body.proposal === 'object' ? body.proposal : body, by: who.by, now: stamp });
227
+ if (!r.ok) return fail(r.error);
228
+ return done('filed', { ask: m[2] });
229
+ }
230
+ const r = RS.fileFinding(file, body, { run: m[2], by: who.by, now: stamp });
231
+ if (r.result === 'duplicate') return done('duplicate');
232
+ if (r.result === 'dismissed') return fail('dismissed');
233
+ if (!r.ok) return fail(r.error);
234
+ return done('filed', { id: r.id });
235
+ }
236
+
152
237
  module.exports = { run, COMMANDS, USAGE };
@@ -0,0 +1,347 @@
1
+ 'use strict';
2
+ /* report-export.js — a report document as a file on this machine: .docx, .pptx, .xlsx or .csv (initiative report-workspace, PRD section 6).
3
+ *
4
+ * exportOf(markdown, fmt) → { ok, fmt, name, mime, buffer } | { ok: false, error } fmt: docx | pptx | xlsx | csv
5
+ * parseReport(markdown) → { title, subtitle, blocks: [{ t: 'h', level, text } | { t: 'p', text } | { t: 'li', text } | { t: 'table', rows }] }
6
+ * sectionsOf(report) → [{ heading, blocks }] the blocks under each level-2 heading
7
+ * docx(report) · pptx(report) · xlsx(report) · csv(report) → Buffer (csv: string)
8
+ * zip(entries) · unzip(buffer) → Buffer · [{ name, data }] a minimal zip writer and reader
9
+ *
10
+ * NO DEPENDENCY, NO NETWORK. The Office files are ZIP packages of small XML parts, built here from the report's markdown.
11
+ * `zip` writes stored or deflated entries (node's zlib), a CRC-32, and the central directory; `unzip` reads them back so a test
12
+ * can open what was written. The files carry the report's words and tables only: no macro, no link to the outside, no embedded
13
+ * object. Nothing is uploaded; the route that serves the file (lib/routes-report-setups.js) writes it to the response.
14
+ *
15
+ * WHAT THE MARKDOWN BECOMES. The frontmatter gives the title. A level-2 heading starts a section; a level-3 heading is a
16
+ * sub-heading in it. A pipe table becomes rows. A `- ` line is a bullet, any other line a paragraph. A note marker, the slide data
17
+ * comment and any other HTML comment are dropped: they are for PMOS, not for the reader.
18
+ * .docx headings, paragraphs and bullets .pptx a title slide, then one slide per section (title and bullets)
19
+ * .xlsx one sheet: a table is its rows; otherwise one row for each line, with its section beside it .csv the same rows
20
+ */
21
+ const zlib = require('zlib');
22
+
23
+ // ── the zip ────────────────────────────────────────────────────────────────────────────────────────
24
+ const CRC_TABLE = (() => {
25
+ const t = new Int32Array(256);
26
+ for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c; }
27
+ return t;
28
+ })();
29
+ function crc32(buf) {
30
+ let c = -1;
31
+ for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
32
+ return (c ^ -1) >>> 0;
33
+ }
34
+ const DOS_TIME = 0;
35
+ const DOS_DATE = (46 << 9) | (10 << 5) | 12; // 2026-10-12: a fixed stamp, so the same report gives the same bytes
36
+
37
+ /** entries: [{ name, data: string | Buffer }]. An entry is deflated when that makes it smaller, else stored. */
38
+ function zip(entries) {
39
+ const locals = []; const central = [];
40
+ let offset = 0;
41
+ for (const e of entries) {
42
+ const name = Buffer.from(String(e.name), 'utf8');
43
+ const raw = Buffer.isBuffer(e.data) ? e.data : Buffer.from(String(e.data), 'utf8');
44
+ const deflated = zlib.deflateRawSync(raw);
45
+ const useDeflate = deflated.length < raw.length;
46
+ const body = useDeflate ? deflated : raw;
47
+ const method = useDeflate ? 8 : 0;
48
+ const crc = crc32(raw);
49
+ const lh = Buffer.alloc(30);
50
+ lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt16LE(0x0800, 6); lh.writeUInt16LE(method, 8);
51
+ lh.writeUInt16LE(DOS_TIME, 10); lh.writeUInt16LE(DOS_DATE, 12); lh.writeUInt32LE(crc, 14);
52
+ lh.writeUInt32LE(body.length, 18); lh.writeUInt32LE(raw.length, 22); lh.writeUInt16LE(name.length, 26); lh.writeUInt16LE(0, 28);
53
+ locals.push(lh, name, body);
54
+ const ch = Buffer.alloc(46);
55
+ ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(20, 4); ch.writeUInt16LE(20, 6); ch.writeUInt16LE(0x0800, 8); ch.writeUInt16LE(method, 10);
56
+ ch.writeUInt16LE(DOS_TIME, 12); ch.writeUInt16LE(DOS_DATE, 14); ch.writeUInt32LE(crc, 16);
57
+ ch.writeUInt32LE(body.length, 20); ch.writeUInt32LE(raw.length, 24); ch.writeUInt16LE(name.length, 28);
58
+ ch.writeUInt16LE(0, 30); ch.writeUInt16LE(0, 32); ch.writeUInt16LE(0, 34); ch.writeUInt16LE(0, 36); ch.writeUInt32LE(0, 38); ch.writeUInt32LE(offset, 42);
59
+ central.push(ch, name);
60
+ offset += lh.length + name.length + body.length;
61
+ }
62
+ const cd = Buffer.concat(central);
63
+ const end = Buffer.alloc(22);
64
+ end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(0, 4); end.writeUInt16LE(0, 6);
65
+ end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10); end.writeUInt32LE(cd.length, 12); end.writeUInt32LE(offset, 16); end.writeUInt16LE(0, 20);
66
+ return Buffer.concat([...locals, cd, end]);
67
+ }
68
+
69
+ /** Read the entries of a zip this module (or any plain zip) wrote. Throws on a damaged archive or a bad CRC. */
70
+ function unzip(buf) {
71
+ let eocd = -1;
72
+ for (let i = buf.length - 22; i >= 0 && i >= buf.length - 65557; i--) if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
73
+ if (eocd < 0) throw new Error('not a zip: no end record');
74
+ const count = buf.readUInt16LE(eocd + 10);
75
+ let p = buf.readUInt32LE(eocd + 16);
76
+ const out = [];
77
+ for (let n = 0; n < count; n++) {
78
+ if (buf.readUInt32LE(p) !== 0x02014b50) throw new Error('not a zip: bad central directory');
79
+ const method = buf.readUInt16LE(p + 10); const crc = buf.readUInt32LE(p + 16); const csize = buf.readUInt32LE(p + 20);
80
+ const nlen = buf.readUInt16LE(p + 28); const xlen = buf.readUInt16LE(p + 30); const clen = buf.readUInt16LE(p + 32); const lho = buf.readUInt32LE(p + 42);
81
+ const name = buf.toString('utf8', p + 46, p + 46 + nlen);
82
+ if (buf.readUInt32LE(lho) !== 0x04034b50) throw new Error('not a zip: bad local header for ' + name);
83
+ const start = lho + 30 + buf.readUInt16LE(lho + 26) + buf.readUInt16LE(lho + 28);
84
+ const body = buf.subarray(start, start + csize);
85
+ const data = method === 8 ? zlib.inflateRawSync(body) : Buffer.from(body);
86
+ if (crc32(data) !== crc) throw new Error('bad CRC for ' + name);
87
+ out.push({ name, data });
88
+ p += 46 + nlen + xlen + clen;
89
+ }
90
+ return out;
91
+ }
92
+
93
+ // ── the markdown ───────────────────────────────────────────────────────────────────────────────────
94
+ const xml = (s) => String(s == null ? '' : s).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
95
+ /** Plain words: no emphasis marks, no code ticks, a link as its text. */
96
+ const plain = (s) => String(s == null ? '' : s).replace(/\*\*([^*]+)\*\*/g, '$1').replace(/__([^_]+)__/g, '$1').replace(/`([^`]*)`/g, '$1')
97
+ .replace(/!?\[([^\]]*)\]\(([^)]*)\)/g, (m, t, u) => (t || u)).replace(/\\\|/g, '|').replace(/\s+/g, ' ').trim();
98
+
99
+ function frontmatterOf(text) {
100
+ const m = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
101
+ const map = {};
102
+ if (!m) return { map, body: text };
103
+ for (const line of m[1].split(/\r?\n/)) { const kv = /^([A-Za-z_][\w-]*):\s*(.*?)\s*$/.exec(line); if (kv) map[kv[1]] = kv[2].replace(/^["']|["']$/g, ''); }
104
+ return { map, body: text.slice(m[0].length) };
105
+ }
106
+
107
+ function parseReport(markdown) {
108
+ const { map, body } = frontmatterOf(String(markdown == null ? '' : markdown).replace(/\r\n?/g, '\n'));
109
+ const clean = body.replace(/<!--[\s\S]*?-->/g, '');
110
+ const blocks = [];
111
+ let table = null;
112
+ const flush = () => { if (table) { blocks.push({ t: 'table', rows: table }); table = null; } };
113
+ for (const raw of clean.split('\n')) {
114
+ const line = raw.replace(/\s+$/, '');
115
+ if (!line.trim()) { flush(); continue; }
116
+ if (/^\s*\|/.test(line)) {
117
+ const cells = line.trim().replace(/^\||\|$/g, '').split(/(?<!\\)\|/).map((c) => plain(c));
118
+ if (cells.every((c) => /^:?-{2,}:?$/.test(c))) continue; // the separator row
119
+ (table || (table = [])).push(cells);
120
+ continue;
121
+ }
122
+ flush();
123
+ const h = /^(#{1,3})\s+(.*)$/.exec(line);
124
+ if (h) { blocks.push({ t: 'h', level: h[1].length, text: plain(h[2]) }); continue; }
125
+ const li = /^\s*(?:[-*+]|\d+[.)])\s+(.*)$/.exec(line);
126
+ if (li) { const t = plain(li[1]); if (t) blocks.push({ t: 'li', text: t }); continue; }
127
+ const p = plain(line);
128
+ if (p) blocks.push({ t: 'p', text: p });
129
+ }
130
+ flush();
131
+ const first = blocks.find((b) => b.t === 'h' && b.level === 1);
132
+ const title = plain(map.title || (first && first.text) || 'Report');
133
+ const rest = first ? blocks.filter((b) => b !== first) : blocks;
134
+ return { title, subtitle: map.slot ? String(map.slot) : '', blocks: rest };
135
+ }
136
+
137
+ /** The blocks under each level-2 heading; blocks before the first one go under the title. */
138
+ function sectionsOf(report) {
139
+ const out = [];
140
+ let cur = null;
141
+ for (const b of report.blocks) {
142
+ if (b.t === 'h' && b.level === 2) { cur = { heading: b.text, blocks: [] }; out.push(cur); continue; }
143
+ if (!cur) { cur = { heading: report.title, blocks: [] }; out.push(cur); }
144
+ cur.blocks.push(b);
145
+ }
146
+ return out;
147
+ }
148
+
149
+ // ── .docx ──────────────────────────────────────────────────────────────────────────────────────────
150
+ const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
151
+ const NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
152
+ const NS_PKG_REL = 'http://schemas.openxmlformats.org/package/2006/relationships';
153
+ const NS_CT = 'http://schemas.openxmlformats.org/package/2006/content-types';
154
+ const DECL = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n';
155
+
156
+ function docx(report) {
157
+ const para = (text, { style, bold, size, indent, hanging } = {}) => {
158
+ const ppr = (style ? `<w:pStyle w:val="${style}"/>` : '') + (indent ? `<w:ind w:left="${indent}" w:hanging="${hanging || 0}"/>` : '');
159
+ const rpr = (bold ? '<w:b/>' : '') + (size ? `<w:sz w:val="${size}"/>` : '');
160
+ return `<w:p>${ppr ? `<w:pPr>${ppr}</w:pPr>` : ''}<w:r>${rpr ? `<w:rPr>${rpr}</w:rPr>` : ''}<w:t xml:space="preserve">${xml(text)}</w:t></w:r></w:p>`;
161
+ };
162
+ const cell = (text, head) => `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>${para(text, { bold: head })}</w:tc>`;
163
+ const table = (rows) => `<w:tbl><w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/><w:tblBorders>`
164
+ + ['top', 'left', 'bottom', 'right', 'insideH', 'insideV'].map((s) => `<w:${s} w:val="single" w:sz="4" w:space="0" w:color="999999"/>`).join('')
165
+ + `</w:tblBorders></w:tblPr>${rows.map((r, i) => `<w:tr>${r.map((c) => cell(c, i === 0)).join('')}</w:tr>`).join('')}</w:tbl>${para('')}`;
166
+ const body = [para(report.title, { style: 'Title' })];
167
+ if (report.subtitle) body.push(para(report.subtitle, { style: 'Subtitle' }));
168
+ for (const b of report.blocks) {
169
+ if (b.t === 'h') body.push(para(b.text, { style: 'Heading' + Math.min(3, b.level) }));
170
+ else if (b.t === 'li') body.push(para('• ' + b.text, { indent: 540, hanging: 270 }));
171
+ else if (b.t === 'table') body.push(table(b.rows.length ? b.rows : [['']]));
172
+ else body.push(para(b.text));
173
+ }
174
+ const document = `${DECL}<w:document xmlns:w="${NS_W}" xmlns:r="${NS_R}"><w:body>${body.join('')}<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>`;
175
+ const style = (id, name, size, bold, extra = '') => `<w:style w:type="paragraph" w:styleId="${id}"><w:name w:val="${name}"/><w:basedOn w:val="Normal"/><w:next w:val="Normal"/><w:qFormat/><w:pPr><w:spacing w:before="240" w:after="80"/>${extra}</w:pPr><w:rPr>${bold ? '<w:b/>' : ''}<w:sz w:val="${size}"/></w:rPr></w:style>`;
176
+ const styles = `${DECL}<w:styles xmlns:w="${NS_W}"><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/><w:sz w:val="22"/></w:rPr></w:rPrDefault><w:pPrDefault><w:pPr><w:spacing w:after="100"/></w:pPr></w:pPrDefault></w:docDefaults>`
177
+ + '<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style>'
178
+ + style('Title', 'Title', 44, true) + style('Subtitle', 'Subtitle', 26, false)
179
+ + style('Heading1', 'heading 1', 32, true) + style('Heading2', 'heading 2', 28, true) + style('Heading3', 'heading 3', 24, true)
180
+ + '<w:style w:type="table" w:styleId="TableGrid"><w:name w:val="Table Grid"/><w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders></w:tblPr></w:style>'
181
+ + '</w:styles>';
182
+ return zip([
183
+ { name: '[Content_Types].xml', data: `${DECL}<Types xmlns="${NS_CT}"><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="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>` },
184
+ { name: '_rels/.rels', data: `${DECL}<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${NS_R}/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>` },
185
+ { name: 'docProps/core.xml', data: coreProps(report.title) },
186
+ { name: 'word/document.xml', data: document },
187
+ { name: 'word/styles.xml', data: styles },
188
+ { name: 'word/_rels/document.xml.rels', data: `${DECL}<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${NS_R}/styles" Target="styles.xml"/></Relationships>` },
189
+ ]);
190
+ }
191
+ function coreProps(title) {
192
+ return `${DECL}<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"><dc:title>${xml(title)}</dc:title><dc:creator>PMOS</dc:creator></cp:coreProperties>`;
193
+ }
194
+
195
+ // ── .pptx ──────────────────────────────────────────────────────────────────────────────────────────
196
+ const NS_A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
197
+ const NS_P = 'http://schemas.openxmlformats.org/presentationml/2006/main';
198
+ const EMU_W = 12192000; const EMU_H = 6858000; // 16:9
199
+ const PER_SLIDE = 9;
200
+
201
+ /** The lines a section puts on slides: bullets and paragraphs as they are, a table row as "a | b | c", a sub-heading as a bold-less line. */
202
+ function slideLines(blocks) {
203
+ const out = [];
204
+ for (const b of blocks) {
205
+ if (b.t === 'table') for (const r of b.rows) out.push(r.join(' | '));
206
+ else if (b.t === 'h') out.push(b.text);
207
+ else out.push(b.text);
208
+ }
209
+ return out;
210
+ }
211
+
212
+ function pptx(report) {
213
+ const slides = []; // { title, lines, kind }
214
+ slides.push({ kind: 'title', title: report.title, lines: report.subtitle ? [report.subtitle] : [] });
215
+ for (const s of sectionsOf(report)) {
216
+ const lines = slideLines(s.blocks);
217
+ if (!lines.length) { slides.push({ kind: 'body', title: s.heading, lines: [] }); continue; }
218
+ for (let i = 0; i < lines.length; i += PER_SLIDE) slides.push({ kind: 'body', title: i ? s.heading + ' (continued)' : s.heading, lines: lines.slice(i, i + PER_SLIDE) });
219
+ }
220
+ const sp = (id, name, ph, x, y, cx, cy, paras) => `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${name}"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="${ph}"${ph === 'body' ? ' idx="1"' : ''}/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/>${paras}</p:txBody></p:sp>`;
221
+ const run = (t, sz, b) => `<a:r><a:rPr lang="en-GB" sz="${sz}"${b ? ' b="1"' : ''} dirty="0"/><a:t>${xml(t)}</a:t></a:r>`;
222
+ const slideXml = (s) => {
223
+ const title = sp(2, 'Title', s.kind === 'title' ? 'ctrTitle' : 'title', s.kind === 'title' ? 914400 : 609600, s.kind === 'title' ? 2286000 : 365760, s.kind === 'title' ? 10363200 : 10972800, s.kind === 'title' ? 1371600 : 914400,
224
+ `<a:p>${run(s.title, s.kind === 'title' ? 4400 : 3200, true)}</a:p>`);
225
+ const bodyParas = s.lines.map((l) => (s.kind === 'title' ? `<a:p>${run(l, 2400, false)}</a:p>` : `<a:p><a:pPr marL="285750" indent="-285750"><a:buFont typeface="Arial"/><a:buChar char="&#8226;"/></a:pPr>${run(l, 1800, false)}</a:p>`)).join('');
226
+ const body = s.lines.length ? sp(3, 'Content', s.kind === 'title' ? 'subTitle' : 'body', s.kind === 'title' ? 914400 : 609600, s.kind === 'title' ? 3810000 : 1371600, s.kind === 'title' ? 10363200 : 10972800, s.kind === 'title' ? 914400 : 4846320, bodyParas) : '';
227
+ return `${DECL}<p:sld xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>${title}${body}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`;
228
+ };
229
+ const layoutXml = `${DECL}<p:sldLayout xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}" type="obj" preserve="1"><p:cSld name="Title and Content"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>`
230
+ + sp(2, 'Title 1', 'title', 609600, 365760, 10972800, 914400, '<a:p><a:endParaRPr lang="en-GB"/></a:p>')
231
+ + sp(3, 'Content Placeholder 2', 'body', 609600, 1371600, 10972800, 4846320, '<a:p><a:endParaRPr lang="en-GB"/></a:p>')
232
+ + '</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>';
233
+ const masterXml = `${DECL}<p:sldMaster xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>`
234
+ + sp(2, 'Title Placeholder 1', 'title', 609600, 365760, 10972800, 914400, '<a:p><a:endParaRPr lang="en-GB"/></a:p>')
235
+ + sp(3, 'Text Placeholder 2', 'body', 609600, 1371600, 10972800, 4846320, '<a:p><a:endParaRPr lang="en-GB"/></a:p>')
236
+ + '</p:spTree></p:cSld><p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>'
237
+ + '<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst><p:txStyles><p:titleStyle><a:lvl1pPr algn="l"><a:defRPr sz="3200" b="1"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mj-lt"/></a:defRPr></a:lvl1pPr></p:titleStyle>'
238
+ + '<p:bodyStyle><a:lvl1pPr marL="285750" indent="-285750"><a:defRPr sz="1800"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill><a:latin typeface="+mn-lt"/></a:defRPr></a:lvl1pPr></p:bodyStyle><p:otherStyle><a:defPPr><a:defRPr lang="en-GB"/></a:defPPr></p:otherStyle></p:txStyles></p:sldMaster>';
239
+ const themeXml = `${DECL}<a:theme xmlns:a="${NS_A}" name="PMOS"><a:themeElements><a:clrScheme name="PMOS"><a:dk1><a:srgbClr val="1F2328"/></a:dk1><a:lt1><a:srgbClr val="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="44546A"/></a:dk2><a:lt2><a:srgbClr val="E7E6E6"/></a:lt2><a:accent1><a:srgbClr val="2F5FD0"/></a:accent1><a:accent2><a:srgbClr val="7A8CA5"/></a:accent2><a:accent3><a:srgbClr val="2E8B57"/></a:accent3><a:accent4><a:srgbClr val="C58A00"/></a:accent4><a:accent5><a:srgbClr val="B3412F"/></a:accent5><a:accent6><a:srgbClr val="6B5BB0"/></a:accent6><a:hlink><a:srgbClr val="2F5FD0"/></a:hlink><a:folHlink><a:srgbClr val="6B5BB0"/></a:folHlink></a:clrScheme>`
240
+ + '<a:fontScheme name="PMOS"><a:majorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme>'
241
+ + '<a:fmtScheme name="PMOS"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst>'
242
+ + '<a:lnStyleLst><a:ln w="9525"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="28575"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst>'
243
+ + '<a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>'
244
+ + '<a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>';
245
+ const n = slides.length;
246
+ const presXml = `${DECL}<p:presentation xmlns:a="${NS_A}" xmlns:r="${NS_R}" xmlns:p="${NS_P}"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst><p:sldIdLst>`
247
+ + slides.map((s, i) => `<p:sldId id="${256 + i}" r:id="rId${i + 2}"/>`).join('') + `</p:sldIdLst><p:sldSz cx="${EMU_W}" cy="${EMU_H}"/><p:notesSz cx="6858000" cy="9144000"/></p:presentation>`;
248
+ const rel = (id, type, target) => `<Relationship Id="${id}" Type="${NS_R}/${type}" Target="${target}"/>`;
249
+ const entries = [
250
+ { name: '[Content_Types].xml', data: `${DECL}<Types xmlns="${NS_CT}"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/><Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/><Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/><Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>`
251
+ + slides.map((s, i) => `<Override PartName="/ppt/slides/slide${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>`).join('') + '</Types>' },
252
+ { name: '_rels/.rels', data: `${DECL}<Relationships xmlns="${NS_PKG_REL}">${rel('rId1', 'officeDocument', 'ppt/presentation.xml')}<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>` },
253
+ { name: 'docProps/core.xml', data: coreProps(report.title) },
254
+ { name: 'ppt/presentation.xml', data: presXml },
255
+ { name: 'ppt/_rels/presentation.xml.rels', data: `${DECL}<Relationships xmlns="${NS_PKG_REL}">${rel('rId1', 'slideMaster', 'slideMasters/slideMaster1.xml')}${slides.map((s, i) => rel('rId' + (i + 2), 'slide', `slides/slide${i + 1}.xml`)).join('')}${rel('rId' + (n + 2), 'theme', 'theme/theme1.xml')}</Relationships>` },
256
+ { name: 'ppt/slideMasters/slideMaster1.xml', data: masterXml },
257
+ { name: 'ppt/slideMasters/_rels/slideMaster1.xml.rels', data: `${DECL}<Relationships xmlns="${NS_PKG_REL}">${rel('rId1', 'slideLayout', '../slideLayouts/slideLayout1.xml')}${rel('rId2', 'theme', '../theme/theme1.xml')}</Relationships>` },
258
+ { name: 'ppt/slideLayouts/slideLayout1.xml', data: layoutXml },
259
+ { name: 'ppt/slideLayouts/_rels/slideLayout1.xml.rels', data: `${DECL}<Relationships xmlns="${NS_PKG_REL}">${rel('rId1', 'slideMaster', '../slideMasters/slideMaster1.xml')}</Relationships>` },
260
+ { name: 'ppt/theme/theme1.xml', data: themeXml },
261
+ ];
262
+ slides.forEach((s, i) => {
263
+ entries.push({ name: `ppt/slides/slide${i + 1}.xml`, data: slideXml(s) });
264
+ entries.push({ name: `ppt/slides/_rels/slide${i + 1}.xml.rels`, data: `${DECL}<Relationships xmlns="${NS_PKG_REL}">${rel('rId1', 'slideLayout', '../slideLayouts/slideLayout1.xml')}</Relationships>` });
265
+ });
266
+ return zip(entries);
267
+ }
268
+
269
+ // ── rows: the sheet and the CSV ─────────────────────────────────────────────────────────────────────
270
+ /** Rows of cells. A table is its own rows (the first row is its header). Any other line is [section, line]. */
271
+ function rowsOf(report) {
272
+ const rows = [];
273
+ let tables = 0;
274
+ for (const s of sectionsOf(report)) {
275
+ for (const b of s.blocks) {
276
+ if (b.t === 'table') {
277
+ if (tables++) rows.push([]);
278
+ b.rows.forEach((r) => rows.push(r));
279
+ rows.push([]);
280
+ }
281
+ }
282
+ }
283
+ if (tables) {
284
+ while (rows.length && !rows[rows.length - 1].length) rows.pop();
285
+ return rows;
286
+ }
287
+ rows.push(['Section', 'Line']);
288
+ for (const s of sectionsOf(report)) for (const b of s.blocks) {
289
+ if (b.t === 'h') rows.push([s.heading, b.text]);
290
+ else if (b.t !== 'table') rows.push([s.heading, b.text]);
291
+ }
292
+ return rows;
293
+ }
294
+
295
+ const colName = (i) => { let s = ''; for (let n = i + 1; n > 0; n = Math.floor((n - 1) / 26)) s = String.fromCharCode(65 + ((n - 1) % 26)) + s; return s; };
296
+ const isNumber = (s) => /^-?\d+(?:\.\d+)?$/.test(s) && s.replace(/^-/, '').replace(/\.\d+$/, '').length <= 12 && !/^-?0\d/.test(s);
297
+
298
+ function xlsx(report) {
299
+ const rows = rowsOf(report);
300
+ const width = rows.reduce((m, r) => Math.max(m, r.length), 1);
301
+ const sheetRows = rows.map((r, ri) => {
302
+ if (!r.length) return `<row r="${ri + 1}"/>`;
303
+ return `<row r="${ri + 1}">${r.map((c, ci) => {
304
+ const ref = colName(ci) + (ri + 1);
305
+ const v = String(c == null ? '' : c);
306
+ if (isNumber(v)) return `<c r="${ref}"><v>${v}</v></c>`;
307
+ return `<c r="${ref}" t="inlineStr"${ri === 0 ? ' s="1"' : ''}><is><t xml:space="preserve">${xml(v)}</t></is></c>`;
308
+ }).join('')}</row>`;
309
+ }).join('');
310
+ const cols = Array.from({ length: width }, (_, i) => `<col min="${i + 1}" max="${i + 1}" width="${i === width - 1 ? 60 : 28}" customWidth="1"/>`).join('');
311
+ const sheet = `${DECL}<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><dimension ref="A1:${colName(width - 1)}${Math.max(1, rows.length)}"/><sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews><sheetFormatPr defaultRowHeight="15"/><cols>${cols}</cols><sheetData>${sheetRows}</sheetData></worksheet>`;
312
+ const name = xml(plain(report.title).replace(/[\\/?*[\]:]/g, ' ').slice(0, 31) || 'Report');
313
+ return zip([
314
+ { name: '[Content_Types].xml', data: `${DECL}<Types xmlns="${NS_CT}"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>` },
315
+ { name: '_rels/.rels', data: `${DECL}<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${NS_R}/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>` },
316
+ { name: 'docProps/core.xml', data: coreProps(report.title) },
317
+ { name: 'xl/workbook.xml', data: `${DECL}<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="${NS_R}"><sheets><sheet name="${name}" sheetId="1" r:id="rId1"/></sheets></workbook>` },
318
+ { name: 'xl/_rels/workbook.xml.rels', data: `${DECL}<Relationships xmlns="${NS_PKG_REL}"><Relationship Id="rId1" Type="${NS_R}/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="${NS_R}/styles" Target="styles.xml"/></Relationships>` },
319
+ { name: 'xl/styles.xml', data: `${DECL}<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills><borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="2"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>` },
320
+ { name: 'xl/worksheets/sheet1.xml', data: sheet },
321
+ ]);
322
+ }
323
+
324
+ function csv(report) {
325
+ const q = (c) => { const v = String(c == null ? '' : c); return /[",\r\n]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : v; };
326
+ return rowsOf(report).map((r) => r.map(q).join(',')).join('\r\n') + '\r\n';
327
+ }
328
+
329
+ // ── the entry point ────────────────────────────────────────────────────────────────────────────────
330
+ const FORMATS = Object.freeze({
331
+ docx: Object.freeze({ mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }),
332
+ pptx: Object.freeze({ mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }),
333
+ xlsx: Object.freeze({ mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }),
334
+ csv: Object.freeze({ mime: 'text/csv; charset=utf-8' }),
335
+ });
336
+ const BUILD = { docx, pptx, xlsx, csv: (r) => Buffer.from(csv(r), 'utf8') };
337
+
338
+ function exportOf(markdown, fmt) {
339
+ if (!Object.prototype.hasOwnProperty.call(FORMATS, fmt)) return { ok: false, error: `fmt must be one of ${Object.keys(FORMATS).join(', ')}` };
340
+ const report = parseReport(markdown);
341
+ let buffer;
342
+ try { buffer = BUILD[fmt](report); } catch (e) { return { ok: false, error: 'the file could not be built: ' + String((e && e.message) || e).slice(0, 160) }; }
343
+ const stem = plain(report.title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'report';
344
+ return { ok: true, fmt, name: `${stem}.${fmt}`, mime: FORMATS[fmt].mime, buffer };
345
+ }
346
+
347
+ module.exports = { exportOf, parseReport, sectionsOf, rowsOf, docx, pptx, xlsx, csv, zip, unzip, crc32, FORMATS };