@pyreon/document 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/lib/analysis/index.js.html +5406 -0
  3. package/lib/chunk-ErZ26oRB.js +48 -0
  4. package/lib/confluence-Va8e7RxQ.js +192 -0
  5. package/lib/confluence-Va8e7RxQ.js.map +1 -0
  6. package/lib/csv-2c38ub-Y.js +32 -0
  7. package/lib/csv-2c38ub-Y.js.map +1 -0
  8. package/lib/discord-DAoUZqvE.js +134 -0
  9. package/lib/discord-DAoUZqvE.js.map +1 -0
  10. package/lib/dist-BsqdI2nY.js +20179 -0
  11. package/lib/dist-BsqdI2nY.js.map +1 -0
  12. package/lib/docx-CorFwEH9.js +450 -0
  13. package/lib/docx-CorFwEH9.js.map +1 -0
  14. package/lib/email-Bn_Brjdp.js +131 -0
  15. package/lib/email-Bn_Brjdp.js.map +1 -0
  16. package/lib/exceljs-BoIDUUaw.js +34377 -0
  17. package/lib/exceljs-BoIDUUaw.js.map +1 -0
  18. package/lib/google-chat-B6I017I1.js +125 -0
  19. package/lib/google-chat-B6I017I1.js.map +1 -0
  20. package/lib/html-De_iS_f0.js +151 -0
  21. package/lib/html-De_iS_f0.js.map +1 -0
  22. package/lib/index.js +619 -0
  23. package/lib/index.js.map +1 -0
  24. package/lib/markdown-BYC_3C9i.js +75 -0
  25. package/lib/markdown-BYC_3C9i.js.map +1 -0
  26. package/lib/notion-DHaQHO6P.js +187 -0
  27. package/lib/notion-DHaQHO6P.js.map +1 -0
  28. package/lib/pdf-CDPc5Itc.js +419 -0
  29. package/lib/pdf-CDPc5Itc.js.map +1 -0
  30. package/lib/pdfmake-DnmLxK4Q.js +55511 -0
  31. package/lib/pdfmake-DnmLxK4Q.js.map +1 -0
  32. package/lib/pptx-DKQU6bjq.js +252 -0
  33. package/lib/pptx-DKQU6bjq.js.map +1 -0
  34. package/lib/pptxgen.es-COcgXsyx.js +5697 -0
  35. package/lib/pptxgen.es-COcgXsyx.js.map +1 -0
  36. package/lib/slack-CJRJgkag.js +139 -0
  37. package/lib/slack-CJRJgkag.js.map +1 -0
  38. package/lib/svg-BM8biZmL.js +187 -0
  39. package/lib/svg-BM8biZmL.js.map +1 -0
  40. package/lib/teams-S99tonRG.js +176 -0
  41. package/lib/teams-S99tonRG.js.map +1 -0
  42. package/lib/telegram-CbEO_2PN.js +77 -0
  43. package/lib/telegram-CbEO_2PN.js.map +1 -0
  44. package/lib/text-B5U8ucRr.js +75 -0
  45. package/lib/text-B5U8ucRr.js.map +1 -0
  46. package/lib/types/index.d.ts +528 -0
  47. package/lib/types/index.d.ts.map +1 -0
  48. package/lib/vfs_fonts-Df1kkZ4Y.js +19 -0
  49. package/lib/vfs_fonts-Df1kkZ4Y.js.map +1 -0
  50. package/lib/whatsapp-DJ2D1jGG.js +64 -0
  51. package/lib/whatsapp-DJ2D1jGG.js.map +1 -0
  52. package/lib/xlsx-D47x-gZ5.js +199 -0
  53. package/lib/xlsx-D47x-gZ5.js.map +1 -0
  54. package/package.json +62 -0
  55. package/src/builder.ts +266 -0
  56. package/src/download.ts +76 -0
  57. package/src/env.d.ts +17 -0
  58. package/src/index.ts +98 -0
  59. package/src/nodes.ts +315 -0
  60. package/src/render.ts +222 -0
  61. package/src/renderers/confluence.ts +231 -0
  62. package/src/renderers/csv.ts +67 -0
  63. package/src/renderers/discord.ts +192 -0
  64. package/src/renderers/docx.ts +612 -0
  65. package/src/renderers/email.ts +230 -0
  66. package/src/renderers/google-chat.ts +211 -0
  67. package/src/renderers/html.ts +225 -0
  68. package/src/renderers/markdown.ts +144 -0
  69. package/src/renderers/notion.ts +264 -0
  70. package/src/renderers/pdf.ts +427 -0
  71. package/src/renderers/pptx.ts +353 -0
  72. package/src/renderers/slack.ts +192 -0
  73. package/src/renderers/svg.ts +254 -0
  74. package/src/renderers/teams.ts +234 -0
  75. package/src/renderers/telegram.ts +137 -0
  76. package/src/renderers/text.ts +154 -0
  77. package/src/renderers/whatsapp.ts +121 -0
  78. package/src/renderers/xlsx.ts +342 -0
  79. package/src/tests/document.test.ts +2920 -0
  80. package/src/types.ts +291 -0
@@ -0,0 +1,353 @@
1
+ import type {
2
+ DocChild,
3
+ DocNode,
4
+ DocumentRenderer,
5
+ RenderOptions,
6
+ TableColumn,
7
+ } from '../types'
8
+
9
+ /**
10
+ * PPTX renderer — lazy-loads pptxgenjs on first use.
11
+ * Each `<Page>` becomes a slide. Document nodes map to PPTX elements.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { render, Document, Page, Heading, Text } from '@pyreon/document'
16
+ *
17
+ * const doc = Document({
18
+ * title: 'Presentation',
19
+ * children: [
20
+ * Page({ children: [Heading({ children: 'Slide 1' }), Text({ children: 'Hello' })] }),
21
+ * Page({ children: [Heading({ children: 'Slide 2' })] }),
22
+ * ],
23
+ * })
24
+ * const pptx = await render(doc, 'pptx') // → Uint8Array
25
+ * ```
26
+ */
27
+
28
+ function resolveColumn(col: string | TableColumn): TableColumn {
29
+ return typeof col === 'string' ? { header: col } : col
30
+ }
31
+
32
+ function getTextContent(children: DocChild[]): string {
33
+ return children
34
+ .map((c) =>
35
+ typeof c === 'string' ? c : getTextContent((c as DocNode).children),
36
+ )
37
+ .join('')
38
+ }
39
+
40
+ /** Vertical position tracker for placing elements on a slide. */
41
+ interface SlideContext {
42
+ slide: PptxSlide
43
+ y: number
44
+ }
45
+
46
+ // Duck-typed pptxgenjs interfaces to avoid hard dependency on types
47
+ interface PptxSlide {
48
+ addText(text: string | PptxTextProps[], opts?: Record<string, unknown>): void
49
+ addImage(opts: Record<string, unknown>): void
50
+ addTable(rows: unknown[][], opts?: Record<string, unknown>): void
51
+ }
52
+
53
+ interface PptxTextProps {
54
+ text: string
55
+ options?: Record<string, unknown>
56
+ }
57
+
58
+ interface PptxGen {
59
+ addSlide(): PptxSlide
60
+ write(outputType: string): Promise<unknown>
61
+ title: string
62
+ author: string
63
+ subject: string
64
+ }
65
+
66
+ const HEADING_SIZES: Record<number, number> = {
67
+ 1: 28,
68
+ 2: 24,
69
+ 3: 20,
70
+ 4: 18,
71
+ 5: 16,
72
+ 6: 14,
73
+ }
74
+
75
+ const SLIDE_WIDTH = 10 // inches
76
+ const CONTENT_MARGIN = 0.5
77
+ const CONTENT_WIDTH = SLIDE_WIDTH - CONTENT_MARGIN * 2
78
+
79
+ function processNode(node: DocNode, ctx: SlideContext): void {
80
+ const p = node.props
81
+
82
+ switch (node.type) {
83
+ case 'heading': {
84
+ const level = (p.level as number) ?? 1
85
+ const fontSize = HEADING_SIZES[level] ?? 20
86
+ ctx.slide.addText(getTextContent(node.children), {
87
+ x: CONTENT_MARGIN,
88
+ y: ctx.y,
89
+ w: CONTENT_WIDTH,
90
+ h: 0.6,
91
+ fontSize,
92
+ bold: true,
93
+ color: ((p.color as string) ?? '#000000').replace('#', ''),
94
+ align: (p.align as string) ?? 'left',
95
+ })
96
+ ctx.y += 0.7
97
+ break
98
+ }
99
+
100
+ case 'text': {
101
+ const text = getTextContent(node.children)
102
+ ctx.slide.addText(text, {
103
+ x: CONTENT_MARGIN,
104
+ y: ctx.y,
105
+ w: CONTENT_WIDTH,
106
+ h: 0.4,
107
+ fontSize: (p.size as number) ?? 14,
108
+ bold: p.bold ?? false,
109
+ italic: p.italic ?? false,
110
+ underline: p.underline ? { style: 'sng' } : undefined,
111
+ strike: p.strikethrough ? 'sngStrike' : undefined,
112
+ color: ((p.color as string) ?? '#333333').replace('#', ''),
113
+ align: (p.align as string) ?? 'left',
114
+ })
115
+ ctx.y += 0.5
116
+ break
117
+ }
118
+
119
+ case 'image': {
120
+ const src = p.src as string
121
+ const w = Math.min(((p.width as number) ?? 400) / 96, CONTENT_WIDTH)
122
+ const h = ((p.height as number) ?? 300) / 96
123
+
124
+ if (src.startsWith('data:')) {
125
+ ctx.slide.addImage({
126
+ data: src,
127
+ x: CONTENT_MARGIN,
128
+ y: ctx.y,
129
+ w,
130
+ h,
131
+ })
132
+ ctx.y += h + 0.2
133
+ }
134
+ // HTTP URLs and local paths are not supported — skip silently
135
+ break
136
+ }
137
+
138
+ case 'table': {
139
+ const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
140
+ resolveColumn,
141
+ )
142
+ const rows = (p.rows ?? []) as (string | number)[][]
143
+ const hs = p.headerStyle as
144
+ | { background?: string; color?: string }
145
+ | undefined
146
+
147
+ const headerRow = columns.map((col) => ({
148
+ text: col.header,
149
+ options: {
150
+ bold: true,
151
+ fill: { color: (hs?.background ?? '#f5f5f5').replace('#', '') },
152
+ color: (hs?.color ?? '#000000').replace('#', ''),
153
+ align: col.align ?? 'left',
154
+ fontSize: 12,
155
+ },
156
+ }))
157
+
158
+ const dataRows = rows.map((row, rowIdx) =>
159
+ columns.map((col, colIdx) => ({
160
+ text: String(row[colIdx] ?? ''),
161
+ options: {
162
+ align: col.align ?? 'left',
163
+ fontSize: 11,
164
+ fill:
165
+ p.striped && rowIdx % 2 === 1 ? { color: 'F9F9F9' } : undefined,
166
+ },
167
+ })),
168
+ )
169
+
170
+ const allRows = [headerRow, ...dataRows]
171
+ const rowHeight = 0.35
172
+ const tableHeight = allRows.length * rowHeight
173
+
174
+ ctx.slide.addTable(allRows, {
175
+ x: CONTENT_MARGIN,
176
+ y: ctx.y,
177
+ w: CONTENT_WIDTH,
178
+ border: { pt: 0.5, color: 'DDDDDD' },
179
+ rowH: rowHeight,
180
+ })
181
+ ctx.y += tableHeight + 0.2
182
+ break
183
+ }
184
+
185
+ case 'list': {
186
+ const items = node.children
187
+ .filter((c): c is DocNode => typeof c !== 'string')
188
+ .map((item) => getTextContent(item.children))
189
+
190
+ const isOrdered = p.ordered as boolean
191
+ const listText = items.map((item, i) => ({
192
+ text: isOrdered ? `${i + 1}. ${item}\n` : `\u2022 ${item}\n`,
193
+ options: { fontSize: 13, bullet: false },
194
+ }))
195
+
196
+ ctx.slide.addText(listText, {
197
+ x: CONTENT_MARGIN,
198
+ y: ctx.y,
199
+ w: CONTENT_WIDTH,
200
+ h: items.length * 0.35,
201
+ })
202
+ ctx.y += items.length * 0.35 + 0.1
203
+ break
204
+ }
205
+
206
+ case 'code': {
207
+ const text = getTextContent(node.children)
208
+ ctx.slide.addText(text, {
209
+ x: CONTENT_MARGIN,
210
+ y: ctx.y,
211
+ w: CONTENT_WIDTH,
212
+ h: 0.5,
213
+ fontSize: 10,
214
+ fontFace: 'Courier New',
215
+ fill: { color: 'F5F5F5' },
216
+ color: '333333',
217
+ })
218
+ ctx.y += 0.6
219
+ break
220
+ }
221
+
222
+ case 'quote': {
223
+ const text = getTextContent(node.children)
224
+ ctx.slide.addText(text, {
225
+ x: CONTENT_MARGIN + 0.3,
226
+ y: ctx.y,
227
+ w: CONTENT_WIDTH - 0.3,
228
+ h: 0.5,
229
+ fontSize: 13,
230
+ italic: true,
231
+ color: '555555',
232
+ })
233
+ ctx.y += 0.6
234
+ break
235
+ }
236
+
237
+ case 'link': {
238
+ ctx.slide.addText(getTextContent(node.children), {
239
+ x: CONTENT_MARGIN,
240
+ y: ctx.y,
241
+ w: CONTENT_WIDTH,
242
+ h: 0.4,
243
+ fontSize: 13,
244
+ color: '4F46E5',
245
+ underline: { style: 'sng' },
246
+ hyperlink: { url: p.href as string },
247
+ })
248
+ ctx.y += 0.5
249
+ break
250
+ }
251
+
252
+ case 'button': {
253
+ ctx.slide.addText(getTextContent(node.children), {
254
+ x: CONTENT_MARGIN,
255
+ y: ctx.y,
256
+ w: 3,
257
+ h: 0.5,
258
+ fontSize: 14,
259
+ bold: true,
260
+ color: ((p.color as string) ?? '#ffffff').replace('#', ''),
261
+ fill: {
262
+ color: ((p.background as string) ?? '#4f46e5').replace('#', ''),
263
+ },
264
+ align: 'center',
265
+ hyperlink: { url: p.href as string },
266
+ })
267
+ ctx.y += 0.6
268
+ break
269
+ }
270
+
271
+ case 'spacer': {
272
+ ctx.y += ((p.height as number) ?? 12) / 72
273
+ break
274
+ }
275
+
276
+ case 'divider': {
277
+ // Render as a thin line using a text element with top border
278
+ ctx.slide.addText('', {
279
+ x: CONTENT_MARGIN,
280
+ y: ctx.y,
281
+ w: CONTENT_WIDTH,
282
+ h: 0.02,
283
+ fill: { color: ((p.color as string) ?? '#DDDDDD').replace('#', '') },
284
+ })
285
+ ctx.y += 0.2
286
+ break
287
+ }
288
+
289
+ // Container types — recurse into children
290
+ case 'section':
291
+ case 'row':
292
+ case 'column':
293
+ for (const child of node.children) {
294
+ if (typeof child !== 'string') {
295
+ processNode(child, ctx)
296
+ }
297
+ }
298
+ break
299
+
300
+ default:
301
+ break
302
+ }
303
+ }
304
+
305
+ function processSlide(pageNode: DocNode, pptx: PptxGen): void {
306
+ const slide = pptx.addSlide()
307
+ const ctx: SlideContext = { slide, y: CONTENT_MARGIN }
308
+
309
+ for (const child of pageNode.children) {
310
+ if (typeof child !== 'string') {
311
+ processNode(child, ctx)
312
+ }
313
+ }
314
+ }
315
+
316
+ export const pptxRenderer: DocumentRenderer = {
317
+ async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
318
+ const PptxGenJS = await import('pptxgenjs')
319
+ const PptxGenClass = PptxGenJS.default ?? PptxGenJS
320
+
321
+ const pptx = new PptxGenClass() as PptxGen
322
+
323
+ // Set metadata
324
+ if (node.props.title) pptx.title = node.props.title as string
325
+ if (node.props.author) pptx.author = node.props.author as string
326
+ if (node.props.subject) pptx.subject = node.props.subject as string
327
+
328
+ // Collect pages — each becomes a slide
329
+ const pages: DocNode[] = []
330
+ for (const child of node.children) {
331
+ if (typeof child !== 'string' && child.type === 'page') {
332
+ pages.push(child)
333
+ }
334
+ }
335
+
336
+ // If no explicit pages, treat entire document content as one slide
337
+ if (pages.length === 0) {
338
+ const syntheticPage: DocNode = {
339
+ type: 'page',
340
+ props: {},
341
+ children: node.children,
342
+ }
343
+ pages.push(syntheticPage)
344
+ }
345
+
346
+ for (const page of pages) {
347
+ processSlide(page, pptx)
348
+ }
349
+
350
+ const output = await pptx.write('arraybuffer')
351
+ return new Uint8Array(output as ArrayBuffer)
352
+ },
353
+ }
@@ -0,0 +1,192 @@
1
+ import type {
2
+ DocChild,
3
+ DocNode,
4
+ DocumentRenderer,
5
+ RenderOptions,
6
+ TableColumn,
7
+ } from '../types'
8
+
9
+ /**
10
+ * Slack Block Kit renderer — outputs JSON that can be posted via Slack's API.
11
+ * Maps document nodes to Slack blocks (section, header, divider, image, etc.).
12
+ */
13
+
14
+ function resolveColumn(col: string | TableColumn): TableColumn {
15
+ return typeof col === 'string' ? { header: col } : col
16
+ }
17
+
18
+ function getTextContent(children: DocChild[]): string {
19
+ return children
20
+ .map((c) =>
21
+ typeof c === 'string' ? c : getTextContent((c as DocNode).children),
22
+ )
23
+ .join('')
24
+ }
25
+
26
+ interface SlackBlock {
27
+ type: string
28
+ [key: string]: unknown
29
+ }
30
+
31
+ function mrkdwn(text: string): { type: 'mrkdwn'; text: string } {
32
+ return { type: 'mrkdwn', text }
33
+ }
34
+
35
+ function plainText(text: string): { type: 'plain_text'; text: string } {
36
+ return { type: 'plain_text', text }
37
+ }
38
+
39
+ function nodeToBlocks(node: DocNode): SlackBlock[] {
40
+ const p = node.props
41
+ const blocks: SlackBlock[] = []
42
+
43
+ switch (node.type) {
44
+ case 'document':
45
+ case 'page':
46
+ case 'section':
47
+ case 'row':
48
+ case 'column':
49
+ for (const child of node.children) {
50
+ if (typeof child !== 'string') {
51
+ blocks.push(...nodeToBlocks(child))
52
+ }
53
+ }
54
+ break
55
+
56
+ case 'heading':
57
+ blocks.push({
58
+ type: 'header',
59
+ text: plainText(getTextContent(node.children)),
60
+ })
61
+ break
62
+
63
+ case 'text': {
64
+ let text = getTextContent(node.children)
65
+ if (p.bold) text = `*${text}*`
66
+ if (p.italic) text = `_${text}_`
67
+ if (p.strikethrough) text = `~${text}~`
68
+ blocks.push({
69
+ type: 'section',
70
+ text: mrkdwn(text),
71
+ })
72
+ break
73
+ }
74
+
75
+ case 'link': {
76
+ const href = p.href as string
77
+ const text = getTextContent(node.children)
78
+ blocks.push({
79
+ type: 'section',
80
+ text: mrkdwn(`<${href}|${text}>`),
81
+ })
82
+ break
83
+ }
84
+
85
+ case 'image': {
86
+ const src = p.src as string
87
+ // Slack only supports public URLs for images
88
+ if (src.startsWith('http')) {
89
+ blocks.push({
90
+ type: 'image',
91
+ image_url: src,
92
+ alt_text: (p.alt as string) ?? 'Image',
93
+ ...(p.caption ? { title: plainText(p.caption as string) } : {}),
94
+ })
95
+ }
96
+ break
97
+ }
98
+
99
+ case 'table': {
100
+ const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(
101
+ resolveColumn,
102
+ )
103
+ const rows = (p.rows ?? []) as (string | number)[][]
104
+
105
+ // Slack doesn't have native tables — render as formatted text
106
+ const header = columns.map((c) => `*${c.header}*`).join(' | ')
107
+ const separator = columns.map(() => '---').join(' | ')
108
+ const body = rows
109
+ .map((row) => row.map((cell) => String(cell ?? '')).join(' | '))
110
+ .join('\n')
111
+
112
+ let text = `${header}\n${separator}\n${body}`
113
+ if (p.caption) text = `_${p.caption}_\n${text}`
114
+
115
+ blocks.push({
116
+ type: 'section',
117
+ text: mrkdwn(`\`\`\`\n${text}\n\`\`\``),
118
+ })
119
+ break
120
+ }
121
+
122
+ case 'list': {
123
+ const ordered = p.ordered as boolean | undefined
124
+ const items = node.children
125
+ .filter((c): c is DocNode => typeof c !== 'string')
126
+ .map((item, i) => {
127
+ const prefix = ordered ? `${i + 1}.` : '•'
128
+ return `${prefix} ${getTextContent(item.children)}`
129
+ })
130
+ .join('\n')
131
+ blocks.push({
132
+ type: 'section',
133
+ text: mrkdwn(items),
134
+ })
135
+ break
136
+ }
137
+
138
+ case 'code': {
139
+ const text = getTextContent(node.children)
140
+ const lang = (p.language as string) ?? ''
141
+ blocks.push({
142
+ type: 'section',
143
+ text: mrkdwn(`\`\`\`${lang}\n${text}\n\`\`\``),
144
+ })
145
+ break
146
+ }
147
+
148
+ case 'divider':
149
+ case 'page-break':
150
+ blocks.push({ type: 'divider' })
151
+ break
152
+
153
+ case 'spacer':
154
+ // No equivalent in Slack — skip
155
+ break
156
+
157
+ case 'button': {
158
+ const href = p.href as string
159
+ const text = getTextContent(node.children)
160
+ blocks.push({
161
+ type: 'actions',
162
+ elements: [
163
+ {
164
+ type: 'button',
165
+ text: plainText(text),
166
+ url: href,
167
+ style: 'primary',
168
+ },
169
+ ],
170
+ })
171
+ break
172
+ }
173
+
174
+ case 'quote': {
175
+ const text = getTextContent(node.children)
176
+ blocks.push({
177
+ type: 'section',
178
+ text: mrkdwn(`> ${text}`),
179
+ })
180
+ break
181
+ }
182
+ }
183
+
184
+ return blocks
185
+ }
186
+
187
+ export const slackRenderer: DocumentRenderer = {
188
+ async render(node: DocNode, _options?: RenderOptions): Promise<string> {
189
+ const blocks = nodeToBlocks(node)
190
+ return JSON.stringify({ blocks }, null, 2)
191
+ },
192
+ }