@hedeqiang/dsh-nl2sql 0.12.1 → 0.13.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.
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Dependency-free PowerPoint (.pptx, OOXML PresentationML) writer.
3
+ *
4
+ * buildPptx({ title, subtitle, lines, slides: [Slide] }) → Buffer
5
+ * Slide = { title, image?: Buffer (PNG), imageAspect?: number, table?: { columns:[{name,kind}], rows }, kpis?: [{ label, value }],
6
+ * text?: string, notes?: string, footer?: string }
7
+ *
8
+ * One 16:9 deck: a title slide, then one slide per entry. Parts: one slide master,
9
+ * one blank layout, a theme, a notes master (with its own theme) and one notes
10
+ * slide per slide that has notes. Also exports a tiny zip reader (`unzip`) used by tests.
11
+ */
12
+ import zlib from 'node:zlib'
13
+ import { Buffer } from 'node:buffer'
14
+
15
+ const EMU = 914400 // per inch
16
+ const SLIDE_W = 12192000 // 13.333 in
17
+ const SLIDE_H = 6858000 // 7.5 in
18
+ const FONT = 'Microsoft YaHei'
19
+
20
+ const NS = {
21
+ a: 'http://schemas.openxmlformats.org/drawingml/2006/main',
22
+ r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
23
+ p: 'http://schemas.openxmlformats.org/presentationml/2006/main',
24
+ }
25
+ const REL = {
26
+ officeDocument: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
27
+ core: 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties',
28
+ app: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties',
29
+ slide: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide',
30
+ slideMaster: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster',
31
+ slideLayout: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout',
32
+ theme: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme',
33
+ image: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
34
+ notesSlide: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide',
35
+ notesMaster: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster',
36
+ presProps: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps',
37
+ viewProps: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps',
38
+ tableStyles: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles',
39
+ }
40
+ const CT = {
41
+ presentation: 'application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml',
42
+ slide: 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml',
43
+ slideMaster: 'application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml',
44
+ slideLayout: 'application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml',
45
+ notesMaster: 'application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml',
46
+ notesSlide: 'application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml',
47
+ theme: 'application/vnd.openxmlformats-officedocument.theme+xml',
48
+ presProps: 'application/vnd.openxmlformats-officedocument.presentationml.presProps+xml',
49
+ viewProps: 'application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml',
50
+ tableStyles: 'application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml',
51
+ core: 'application/vnd.openxmlformats-package.core-properties+xml',
52
+ app: 'application/vnd.openxmlformats-officedocument.extended-properties+xml',
53
+ }
54
+ export const PPTX_MEDIA_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
55
+
56
+ const XML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
57
+ const ILLEGAL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g
58
+
59
+ /** XML text/attribute escape (drops characters illegal in XML 1.0). */
60
+ export function xmlEsc(s) {
61
+ return String(s ?? '').replace(ILLEGAL, '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
62
+ }
63
+
64
+ const emu = (inches) => Math.round(inches * EMU)
65
+
66
+ // ------------------------------------------------------------------ zip
67
+
68
+ function dosDateTime(d) {
69
+ return { time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2), date: ((Math.max(1980, d.getFullYear()) - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate() }
70
+ }
71
+
72
+ /** Zip archive from [{ name, data }]; PNGs are stored, XML deflated. */
73
+ export function zip(files) {
74
+ const { time, date } = dosDateTime(new Date())
75
+ const chunks = []
76
+ const central = []
77
+ let offset = 0
78
+ for (const f of files) {
79
+ const name = Buffer.from(f.name, 'utf8')
80
+ const data = Buffer.isBuffer(f.data) ? f.data : Buffer.from(f.data, 'utf8')
81
+ const crc = zlib.crc32(data) >>> 0
82
+ const store = /\.(png|jpe?g)$/i.test(f.name)
83
+ const comp = store ? data : zlib.deflateRawSync(data, { level: 6 })
84
+ const method = store ? 0 : 8
85
+ const local = Buffer.alloc(30)
86
+ local.writeUInt32LE(0x04034b50, 0)
87
+ local.writeUInt16LE(20, 4)
88
+ local.writeUInt16LE(0x0800, 6)
89
+ local.writeUInt16LE(method, 8)
90
+ local.writeUInt16LE(time, 10)
91
+ local.writeUInt16LE(date, 12)
92
+ local.writeUInt32LE(crc, 14)
93
+ local.writeUInt32LE(comp.length, 18)
94
+ local.writeUInt32LE(data.length, 22)
95
+ local.writeUInt16LE(name.length, 26)
96
+ chunks.push(local, name, comp)
97
+ const cd = Buffer.alloc(46)
98
+ cd.writeUInt32LE(0x02014b50, 0)
99
+ cd.writeUInt16LE(20, 4)
100
+ cd.writeUInt16LE(20, 6)
101
+ cd.writeUInt16LE(0x0800, 8)
102
+ cd.writeUInt16LE(method, 10)
103
+ cd.writeUInt16LE(time, 12)
104
+ cd.writeUInt16LE(date, 14)
105
+ cd.writeUInt32LE(crc, 16)
106
+ cd.writeUInt32LE(comp.length, 20)
107
+ cd.writeUInt32LE(data.length, 24)
108
+ cd.writeUInt16LE(name.length, 28)
109
+ cd.writeUInt32LE(offset, 42)
110
+ central.push(cd, name)
111
+ offset += 30 + name.length + comp.length
112
+ }
113
+ const cdBuf = Buffer.concat(central)
114
+ const eocd = Buffer.alloc(22)
115
+ eocd.writeUInt32LE(0x06054b50, 0)
116
+ eocd.writeUInt16LE(files.length, 8)
117
+ eocd.writeUInt16LE(files.length, 10)
118
+ eocd.writeUInt32LE(cdBuf.length, 12)
119
+ eocd.writeUInt32LE(offset, 16)
120
+ return Buffer.concat([...chunks, cdBuf, eocd])
121
+ }
122
+
123
+ /** Read a (non-zip64) archive into Map<name, Buffer>. */
124
+ export function unzip(buf) {
125
+ let eocd = -1
126
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65557); i--) if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break }
127
+ if (eocd < 0) throw new Error('not a zip archive')
128
+ const count = buf.readUInt16LE(eocd + 10)
129
+ let p = buf.readUInt32LE(eocd + 16)
130
+ const out = new Map()
131
+ for (let n = 0; n < count; n++) {
132
+ if (buf.readUInt32LE(p) !== 0x02014b50) throw new Error('bad central directory')
133
+ const method = buf.readUInt16LE(p + 10)
134
+ const csize = buf.readUInt32LE(p + 20)
135
+ const nameLen = buf.readUInt16LE(p + 28)
136
+ const extra = buf.readUInt16LE(p + 30)
137
+ const comment = buf.readUInt16LE(p + 32)
138
+ const off = buf.readUInt32LE(p + 42)
139
+ const name = buf.subarray(p + 46, p + 46 + nameLen).toString('utf8')
140
+ const lname = buf.readUInt16LE(off + 26)
141
+ const lextra = buf.readUInt16LE(off + 28)
142
+ const data = buf.subarray(off + 30 + lname + lextra, off + 30 + lname + lextra + csize)
143
+ out.set(name, method === 0 ? Buffer.from(data) : zlib.inflateRawSync(data))
144
+ p += 46 + nameLen + extra + comment
145
+ }
146
+ return out
147
+ }
148
+
149
+ // ------------------------------------------------------------------ static parts
150
+
151
+ function rels(list) {
152
+ return `${XML}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${list.map(r => `<Relationship Id="${r.id}" Type="${r.type}" Target="${xmlEsc(r.target)}"/>`).join('')}</Relationships>`
153
+ }
154
+
155
+ function theme(name) {
156
+ const clr = (n, v) => `<a:${n}><a:srgbClr val="${v}"/></a:${n}>`
157
+ const solid = '<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>'
158
+ const ln = (w) => `<a:ln w="${w}" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/><a:miter lim="800000"/></a:ln>`
159
+ return `${XML}<a:theme xmlns:a="${NS.a}" name="${name}"><a:themeElements>`
160
+ + `<a:clrScheme name="nl2sql"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1>${clr('dk2', '1F2329')}${clr('lt2', 'F5F6F7')}${clr('accent1', '3370FF')}${clr('accent2', '34C724')}${clr('accent3', 'FF8800')}${clr('accent4', 'F54A45')}${clr('accent5', '7F3BF5')}${clr('accent6', '14C0FF')}${clr('hlink', '3370FF')}${clr('folHlink', '7F3BF5')}</a:clrScheme>`
161
+ + `<a:fontScheme name="nl2sql"><a:majorFont><a:latin typeface="${FONT}"/><a:ea typeface="${FONT}"/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="${FONT}"/><a:ea typeface="${FONT}"/><a:cs typeface=""/></a:minorFont></a:fontScheme>`
162
+ + `<a:fmtScheme name="nl2sql"><a:fillStyleLst>${solid}${solid}${solid}</a:fillStyleLst><a:lnStyleLst>${ln(6350)}${ln(12700)}${ln(19050)}</a:lnStyleLst>`
163
+ + '<a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst>'
164
+ + `<a:bgFillStyleLst>${solid}${solid}${solid}</a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:objectDefaults/><a:extraClrSchemeLst/></a:theme>`
165
+ }
166
+
167
+ const CLR_MAP = 'bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"'
168
+ const GROUP = '<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>'
169
+ const lvl = (size) => `<a:lvl1pPr><a:defRPr sz="${size}"><a:latin typeface="+mn-lt"/><a:ea typeface="+mn-ea"/></a:defRPr></a:lvl1pPr>`
170
+
171
+ function slideMaster() {
172
+ return `${XML}<p:sldMaster xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:p="${NS.p}"><p:cSld><p:bg><p:bgPr><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill><a:effectLst/></p:bgPr></p:bg><p:spTree>${GROUP}</p:spTree></p:cSld>`
173
+ + `<p:clrMap ${CLR_MAP}/><p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>`
174
+ + `<p:txStyles><p:titleStyle>${lvl(3200)}</p:titleStyle><p:bodyStyle>${lvl(1800)}</p:bodyStyle><p:otherStyle>${lvl(1400)}</p:otherStyle></p:txStyles></p:sldMaster>`
175
+ }
176
+
177
+ function slideLayout() {
178
+ return `${XML}<p:sldLayout xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:p="${NS.p}" type="blank" preserve="1"><p:cSld name="Blank"><p:spTree>${GROUP}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>`
179
+ }
180
+
181
+ function notesMaster() {
182
+ const ph = (id, name, type, idx, x, y, w, hgt) => `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${name}"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="${type}"${idx ? ` idx="${idx}"` : ''}/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${w}" cy="${hgt}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>${type === 'body' ? '<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="zh-CN"/></a:p></p:txBody>' : ''}</p:sp>`
183
+ return `${XML}<p:notesMaster 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>${GROUP}`
184
+ + ph(2, 'Slide Image Placeholder 1', 'sldImg', 2, 381000, 685800, 6096000, 3429000)
185
+ + ph(3, 'Notes Placeholder 2', 'body', 3, 685800, 4400550, 5486400, 3600450)
186
+ + `</p:spTree></p:cSld><p:clrMap ${CLR_MAP}/><p:notesStyle>${lvl(1200)}</p:notesStyle></p:notesMaster>`
187
+ }
188
+
189
+ function notesSlide(text) {
190
+ const paras = String(text).split(/\r?\n/).map(line => `<a:p><a:r><a:rPr lang="zh-CN" dirty="0"/><a:t>${xmlEsc(line)}</a:t></a:r></a:p>`).join('')
191
+ return `${XML}<p:notes xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:p="${NS.p}"><p:cSld><p:spTree>${GROUP}`
192
+ + '<p:sp><p:nvSpPr><p:cNvPr id="2" name="Slide Image Placeholder 1"/><p:cNvSpPr><a:spLocks noGrp="1" noRot="1" noChangeAspect="1"/></p:cNvSpPr><p:nvPr><p:ph type="sldImg"/></p:nvPr></p:nvSpPr><p:spPr/></p:sp>'
193
+ + `<p:sp><p:nvSpPr><p:cNvPr id="3" name="Notes Placeholder 2"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body" idx="1"/></p:nvPr></p:nvSpPr><p:spPr/><p:txBody><a:bodyPr/><a:lstStyle/>${paras || '<a:p><a:endParaRPr lang="zh-CN"/></a:p>'}</p:txBody></p:sp>`
194
+ + `</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:notes>`
195
+ }
196
+
197
+ // ------------------------------------------------------------------ shapes
198
+
199
+ /** A text box. runs: string | [{ text, size, bold, color }] per paragraph. */
200
+ function textBox(id, name, { x, y, w, h, paragraphs, align = 'l', anchor = 't', size = 1800, color = '1F2329', bold = false }) {
201
+ const paras = paragraphs.map(p => {
202
+ const runs = (Array.isArray(p) ? p : [{ text: p }]).map(r => `<a:r><a:rPr lang="zh-CN" sz="${r.size ?? size}" b="${(r.bold ?? bold) ? 1 : 0}" dirty="0"><a:solidFill><a:srgbClr val="${r.color ?? color}"/></a:solidFill><a:latin typeface="${FONT}"/><a:ea typeface="${FONT}"/></a:rPr><a:t>${xmlEsc(r.text)}</a:t></a:r>`).join('')
203
+ return `<a:p><a:pPr algn="${align}"/>${runs}</a:p>`
204
+ }).join('')
205
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${xmlEsc(name)}"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${w}" cy="${h}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr>`
206
+ + `<p:txBody><a:bodyPr wrap="square" lIns="91440" tIns="45720" rIns="91440" bIns="45720" anchor="${anchor}"><a:normAutofit/></a:bodyPr><a:lstStyle/>${paras}</p:txBody></p:sp>`
207
+ }
208
+
209
+ function rect(id, { x, y, w, h, fill }) {
210
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="Bar ${id}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${w}" cy="${h}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:solidFill><a:srgbClr val="${fill}"/></a:solidFill><a:ln><a:noFill/></a:ln></p:spPr></p:sp>`
211
+ }
212
+
213
+ function picture(id, rid, { x, y, w, h }) {
214
+ return `<p:pic><p:nvPicPr><p:cNvPr id="${id}" name="Chart ${id}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="${rid}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${w}" cy="${h}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`
215
+ }
216
+
217
+ function cellText(v, max = 40) {
218
+ if (v === null || v === undefined) return ''
219
+ const s = String(v).replace(/\r\n?|\n/g, ' ')
220
+ return s.length > max ? `${s.slice(0, max - 1)}…` : s
221
+ }
222
+
223
+ function fmtNumber(v) {
224
+ if (v === null || v === undefined || v === '') return ''
225
+ const n = Number(v)
226
+ if (!Number.isFinite(n)) return String(v)
227
+ return n.toLocaleString('en-US', { maximumFractionDigits: 4 })
228
+ }
229
+
230
+ /** A native table (graphicFrame) with ≤ 15 rows × ≤ 8 columns. */
231
+ function tableFrame(id, table, { x, y, w, maxRows = 15, maxCols = 8 }) {
232
+ const cols = table.columns.slice(0, maxCols)
233
+ const rows = table.rows.slice(0, maxRows)
234
+ const rowH = rows.length > 10 ? emu(0.3) : emu(0.36)
235
+ const colW = Math.floor(w / Math.max(1, cols.length))
236
+ const size = cols.length > 6 || rows.length > 10 ? 1000 : 1200
237
+ const cell = (text, { header, num, odd }) => `<a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr algn="${num ? 'r' : 'l'}"/><a:r><a:rPr lang="zh-CN" sz="${size}" b="${header ? 1 : 0}" dirty="0"><a:solidFill><a:srgbClr val="${header ? 'FFFFFF' : '1F2329'}"/></a:solidFill><a:latin typeface="${FONT}"/><a:ea typeface="${FONT}"/></a:rPr><a:t>${xmlEsc(text)}</a:t></a:r></a:p></a:txBody>`
238
+ + `<a:tcPr marL="68580" marR="68580" marT="22860" marB="22860" anchor="ctr"><a:lnL w="6350"><a:solidFill><a:srgbClr val="DEE0E3"/></a:solidFill></a:lnL><a:lnR w="6350"><a:solidFill><a:srgbClr val="DEE0E3"/></a:solidFill></a:lnR><a:lnT w="6350"><a:solidFill><a:srgbClr val="DEE0E3"/></a:solidFill></a:lnT><a:lnB w="6350"><a:solidFill><a:srgbClr val="DEE0E3"/></a:solidFill></a:lnB><a:solidFill><a:srgbClr val="${header ? '3370FF' : odd ? 'F5F6F7' : 'FFFFFF'}"/></a:solidFill></a:tcPr></a:tc>`
239
+ const head = `<a:tr h="${rowH}">${cols.map(c => cell(cellText(c.name, 30), { header: true, num: c.kind === 'number' })).join('')}</a:tr>`
240
+ const body = rows.map((r, ri) => `<a:tr h="${rowH}">${cols.map((c, ci) => cell(c.kind === 'number' ? fmtNumber(r[ci]) : r[ci] === null ? 'NULL' : cellText(r[ci]), { num: c.kind === 'number', odd: ri % 2 === 1 })).join('')}</a:tr>`).join('')
241
+ const h = rowH * (rows.length + 1)
242
+ return `<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="${id}" name="Table ${id}"/><p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr><p:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${colW * cols.length}" cy="${h}"/></p:xfrm>`
243
+ + `<a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table"><a:tbl><a:tblPr firstRow="1" bandRow="1"/><a:tblGrid>${cols.map(() => `<a:gridCol w="${colW}"/>`).join('')}</a:tblGrid>${head}${body}</a:tbl></a:graphicData></a:graphic></p:graphicFrame>`
244
+ }
245
+
246
+ function slideXml(shapes) {
247
+ return `${XML}<p:sld xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:p="${NS.p}"><p:cSld><p:spTree>${GROUP}${shapes.join('')}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`
248
+ }
249
+
250
+ const MARGIN = emu(0.6)
251
+ const CONTENT_W = SLIDE_W - 2 * MARGIN
252
+
253
+ function titleSlideShapes({ title, subtitle, lines = [] }) {
254
+ return [
255
+ rect(2, { x: 0, y: 0, w: SLIDE_W, h: emu(0.16), fill: '3370FF' }),
256
+ textBox(3, 'Title', { x: MARGIN, y: emu(2.3), w: CONTENT_W, h: emu(1.3), paragraphs: [title || 'Untitled'], size: 4000, bold: true, anchor: 'b' }),
257
+ textBox(4, 'Subtitle', { x: MARGIN, y: emu(3.75), w: CONTENT_W, h: emu(1.8), paragraphs: [subtitle, ...lines].filter(Boolean).map(s => String(s)), size: 1800, color: '646A73' }),
258
+ ]
259
+ }
260
+
261
+ function contentSlideShapes(s, index, imageRid) {
262
+ const shapes = [
263
+ rect(2, { x: 0, y: 0, w: SLIDE_W, h: emu(0.08), fill: '3370FF' }),
264
+ textBox(3, 'Title', { x: MARGIN, y: emu(0.3), w: CONTENT_W, h: emu(0.8), paragraphs: [s.title || `#${index}`], size: 2600, bold: true, anchor: 'ctr' }),
265
+ ]
266
+ const top = emu(1.25)
267
+ const bottom = SLIDE_H - emu(0.6)
268
+ const availH = bottom - top
269
+ let id = 4
270
+ if (imageRid) {
271
+ const aspect = s.imageAspect || 2
272
+ let w = CONTENT_W
273
+ let h = Math.round(w / aspect)
274
+ if (h > availH) { h = availH; w = Math.round(h * aspect) }
275
+ shapes.push(picture(id++, imageRid, { x: Math.round((SLIDE_W - w) / 2), y: top, w, h }))
276
+ } else if (s.kpis?.length) {
277
+ const list = s.kpis.slice(0, 6)
278
+ const per = Math.floor(CONTENT_W / list.length)
279
+ list.forEach((k, i) => shapes.push(textBox(id++, `KPI ${i + 1}`, { x: MARGIN + i * per, y: emu(2.3), w: per, h: emu(2.4), align: 'ctr', anchor: 'ctr', paragraphs: [[{ text: k.value, size: list.length > 3 ? 4000 : 5400, bold: true, color: '3370FF' }], [{ text: k.label, size: 1600, color: '646A73' }]] })))
280
+ } else if (s.table?.columns?.length) {
281
+ shapes.push(tableFrame(id++, s.table, { x: MARGIN, y: top, w: CONTENT_W }))
282
+ } else if (s.text) {
283
+ shapes.push(textBox(id++, 'Text', { x: MARGIN, y: top, w: CONTENT_W, h: availH, paragraphs: String(s.text).split(/\r?\n/).slice(0, 20), size: 1600, color: s.error ? 'F54A45' : '1F2329' }))
284
+ }
285
+ if (s.footer) shapes.push(textBox(id++, 'Footer', { x: MARGIN, y: SLIDE_H - emu(0.55), w: CONTENT_W, h: emu(0.4), paragraphs: [s.footer], size: 1000, color: '8F959E' }))
286
+ return shapes
287
+ }
288
+
289
+ /** Build the deck. Returns a Buffer. */
290
+ export function buildPptx({ title, subtitle, lines = [], slides = [], author = 'DeepSeek Harness 数据问答' } = {}) {
291
+ const files = []
292
+ const add = (name, data) => files.push({ name, data })
293
+ const deck = [{ kind: 'title' }, ...slides.map(s => ({ kind: 'content', ...s }))]
294
+ const ctOverrides = []
295
+ let image = 0
296
+ let notes = 0
297
+ const slideIds = []
298
+ deck.forEach((s, i) => {
299
+ const n = i + 1
300
+ const sRels = [{ id: 'rId1', type: REL.slideLayout, target: '../slideLayouts/slideLayout1.xml' }]
301
+ let imageRid
302
+ if (s.image) {
303
+ image++
304
+ imageRid = `rId${sRels.length + 1}`
305
+ sRels.push({ id: imageRid, type: REL.image, target: `../media/image${image}.png` })
306
+ add(`ppt/media/image${image}.png`, s.image)
307
+ }
308
+ if (s.notes) {
309
+ notes++
310
+ sRels.push({ id: `rId${sRels.length + 1}`, type: REL.notesSlide, target: `../notesSlides/notesSlide${notes}.xml` })
311
+ add(`ppt/notesSlides/notesSlide${notes}.xml`, notesSlide(s.notes))
312
+ add(`ppt/notesSlides/_rels/notesSlide${notes}.xml.rels`, rels([{ id: 'rId1', type: REL.notesMaster, target: '../notesMasters/notesMaster1.xml' }, { id: 'rId2', type: REL.slide, target: `../slides/slide${n}.xml` }]))
313
+ ctOverrides.push([`/ppt/notesSlides/notesSlide${notes}.xml`, CT.notesSlide])
314
+ }
315
+ const shapes = s.kind === 'title' ? titleSlideShapes({ title, subtitle, lines }) : contentSlideShapes(s, i, imageRid)
316
+ add(`ppt/slides/slide${n}.xml`, slideXml(shapes))
317
+ add(`ppt/slides/_rels/slide${n}.xml.rels`, rels(sRels))
318
+ ctOverrides.push([`/ppt/slides/slide${n}.xml`, CT.slide])
319
+ slideIds.push(n)
320
+ })
321
+ // presentation rels: rId1 master, rId2..N+1 slides, then notes master, props, theme.
322
+ const pRels = [{ id: 'rId1', type: REL.slideMaster, target: 'slideMasters/slideMaster1.xml' }]
323
+ for (const n of slideIds) pRels.push({ id: `rId${n + 1}`, type: REL.slide, target: `slides/slide${n}.xml` })
324
+ const k = slideIds.length + 2
325
+ pRels.push({ id: `rId${k}`, type: REL.notesMaster, target: 'notesMasters/notesMaster1.xml' })
326
+ pRels.push({ id: `rId${k + 1}`, type: REL.presProps, target: 'presProps.xml' })
327
+ pRels.push({ id: `rId${k + 2}`, type: REL.viewProps, target: 'viewProps.xml' })
328
+ pRels.push({ id: `rId${k + 3}`, type: REL.theme, target: 'theme/theme1.xml' })
329
+ pRels.push({ id: `rId${k + 4}`, type: REL.tableStyles, target: 'tableStyles.xml' })
330
+ const presentation = `${XML}<p:presentation xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:p="${NS.p}" saveSubsetFonts="1">`
331
+ + '<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>'
332
+ + `<p:notesMasterIdLst><p:notesMasterId r:id="rId${k}"/></p:notesMasterIdLst>`
333
+ + `<p:sldIdLst>${slideIds.map(n => `<p:sldId id="${255 + n}" r:id="rId${n + 1}"/>`).join('')}</p:sldIdLst>`
334
+ + `<p:sldSz cx="${SLIDE_W}" cy="${SLIDE_H}"/><p:notesSz cx="6858000" cy="9144000"/>`
335
+ + `<p:defaultTextStyle>${lvl(1800)}</p:defaultTextStyle></p:presentation>`
336
+ add('ppt/presentation.xml', presentation)
337
+ add('ppt/_rels/presentation.xml.rels', rels(pRels))
338
+ add('ppt/slideMasters/slideMaster1.xml', slideMaster())
339
+ add('ppt/slideMasters/_rels/slideMaster1.xml.rels', rels([{ id: 'rId1', type: REL.slideLayout, target: '../slideLayouts/slideLayout1.xml' }, { id: 'rId2', type: REL.theme, target: '../theme/theme1.xml' }]))
340
+ add('ppt/slideLayouts/slideLayout1.xml', slideLayout())
341
+ add('ppt/slideLayouts/_rels/slideLayout1.xml.rels', rels([{ id: 'rId1', type: REL.slideMaster, target: '../slideMasters/slideMaster1.xml' }]))
342
+ add('ppt/notesMasters/notesMaster1.xml', notesMaster())
343
+ add('ppt/notesMasters/_rels/notesMaster1.xml.rels', rels([{ id: 'rId1', type: REL.theme, target: '../theme/theme2.xml' }]))
344
+ add('ppt/theme/theme1.xml', theme('nl2sql'))
345
+ add('ppt/theme/theme2.xml', theme('nl2sql notes'))
346
+ add('ppt/presProps.xml', `${XML}<p:presentationPr xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:p="${NS.p}"/>`)
347
+ add('ppt/viewProps.xml', `${XML}<p:viewPr xmlns:a="${NS.a}" xmlns:r="${NS.r}" xmlns:p="${NS.p}"><p:normalViewPr><p:restoredLeft sz="15620"/><p:restoredTop sz="94660"/></p:normalViewPr><p:gridSpacing cx="76200" cy="76200"/></p:viewPr>`)
348
+ add('ppt/tableStyles.xml', `${XML}<a:tblStyleLst xmlns:a="${NS.a}" def="{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"/>`)
349
+ const now = new Date().toISOString().replace(/\.\d+Z$/, 'Z')
350
+ add('docProps/core.xml', `${XML}<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:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>${xmlEsc(title)}</dc:title><dc:creator>${xmlEsc(author)}</dc:creator><cp:lastModifiedBy>${xmlEsc(author)}</cp:lastModifiedBy><dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`)
351
+ add('docProps/app.xml', `${XML}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>${xmlEsc(author)}</Application><Slides>${slideIds.length}</Slides><Notes>${notes}</Notes><PresentationFormat>On-screen Show (16:9)</PresentationFormat></Properties>`)
352
+ add('_rels/.rels', rels([{ id: 'rId1', type: REL.officeDocument, target: 'ppt/presentation.xml' }, { id: 'rId2', type: REL.core, target: 'docProps/core.xml' }, { id: 'rId3', type: REL.app, target: 'docProps/app.xml' }]))
353
+ const overrides = [
354
+ ['/ppt/presentation.xml', CT.presentation], ['/ppt/slideMasters/slideMaster1.xml', CT.slideMaster], ['/ppt/slideLayouts/slideLayout1.xml', CT.slideLayout],
355
+ ['/ppt/notesMasters/notesMaster1.xml', CT.notesMaster], ['/ppt/theme/theme1.xml', CT.theme], ['/ppt/theme/theme2.xml', CT.theme],
356
+ ['/ppt/presProps.xml', CT.presProps], ['/ppt/viewProps.xml', CT.viewProps], ['/ppt/tableStyles.xml', CT.tableStyles],
357
+ ['/docProps/core.xml', CT.core], ['/docProps/app.xml', CT.app], ...ctOverrides,
358
+ ]
359
+ const contentTypes = `${XML}<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"/><Default Extension="png" ContentType="image/png"/>${overrides.map(([p, c]) => `<Override PartName="${p}" ContentType="${c}"/>`).join('')}</Types>`
360
+ // [Content_Types].xml first, as Office expects.
361
+ return zip([{ name: '[Content_Types].xml', data: contentTypes }, ...files.sort((a, b) => (a.name === '_rels/.rels' ? -1 : b.name === '_rels/.rels' ? 1 : 0))])
362
+ }