@pyreon/document 0.11.5 → 0.11.6

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 (50) hide show
  1. package/README.md +7 -4
  2. package/lib/confluence-Bd3ua1Ut.js.map +1 -1
  3. package/lib/csv-COrS4qdy.js.map +1 -1
  4. package/lib/discord-BLUnkEh9.js.map +1 -1
  5. package/lib/docx-uNAel545.js.map +1 -1
  6. package/lib/email-D0bbfWq4.js.map +1 -1
  7. package/lib/google-chat-CkKCBUWC.js.map +1 -1
  8. package/lib/html-B5biprN2.js.map +1 -1
  9. package/lib/index.js.map +1 -1
  10. package/lib/markdown-CdtlFGC0.js.map +1 -1
  11. package/lib/notion-iG2C5bEY.js.map +1 -1
  12. package/lib/pdf-IuBgTb3T.js.map +1 -1
  13. package/lib/pptx-DXiMiYFM.js.map +1 -1
  14. package/lib/sanitize-O_3j1mNJ.js.map +1 -1
  15. package/lib/slack-BI3EQwYm.js.map +1 -1
  16. package/lib/svg-BKxumy-p.js.map +1 -1
  17. package/lib/teams-Cwz9lce0.js.map +1 -1
  18. package/lib/telegram-gYFqyMXb.js.map +1 -1
  19. package/lib/text-l1XNXBOC.js.map +1 -1
  20. package/lib/types/index.d.ts +27 -27
  21. package/lib/whatsapp-CjSGoOKx.js.map +1 -1
  22. package/lib/xlsx-Cvu4LBNy.js.map +1 -1
  23. package/package.json +21 -21
  24. package/src/builder.ts +36 -36
  25. package/src/download.ts +32 -32
  26. package/src/index.ts +5 -10
  27. package/src/nodes.ts +45 -45
  28. package/src/render.ts +43 -43
  29. package/src/renderers/confluence.ts +63 -63
  30. package/src/renderers/csv.ts +10 -10
  31. package/src/renderers/discord.ts +37 -37
  32. package/src/renderers/docx.ts +57 -57
  33. package/src/renderers/email.ts +72 -72
  34. package/src/renderers/google-chat.ts +34 -34
  35. package/src/renderers/html.ts +76 -76
  36. package/src/renderers/markdown.ts +42 -42
  37. package/src/renderers/notion.ts +60 -60
  38. package/src/renderers/pdf.ts +78 -78
  39. package/src/renderers/pptx.ts +51 -51
  40. package/src/renderers/slack.ts +48 -48
  41. package/src/renderers/svg.ts +47 -47
  42. package/src/renderers/teams.ts +67 -67
  43. package/src/renderers/telegram.ts +39 -39
  44. package/src/renderers/text.ts +43 -43
  45. package/src/renderers/whatsapp.ts +33 -33
  46. package/src/renderers/xlsx.ts +35 -35
  47. package/src/sanitize.ts +20 -20
  48. package/src/tests/document.test.ts +1302 -1302
  49. package/src/tests/stress.test.ts +110 -110
  50. package/src/types.ts +61 -61
@@ -1,5 +1,5 @@
1
- import { sanitizeHref, sanitizeImageSrc, sanitizeXmlColor } from "../sanitize"
2
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
1
+ import { sanitizeHref, sanitizeImageSrc, sanitizeXmlColor } from '../sanitize'
2
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
3
3
 
4
4
  /**
5
5
  * PPTX renderer — lazy-loads pptxgenjs on first use.
@@ -21,13 +21,13 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
21
21
  */
22
22
 
23
23
  function resolveColumn(col: string | TableColumn): TableColumn {
24
- return typeof col === "string" ? { header: col } : col
24
+ return typeof col === 'string' ? { header: col } : col
25
25
  }
26
26
 
27
27
  function getTextContent(children: DocChild[]): string {
28
28
  return children
29
- .map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
30
- .join("")
29
+ .map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
30
+ .join('')
31
31
  }
32
32
 
33
33
  /** Vertical position tracker for placing elements on a slide. */
@@ -73,7 +73,7 @@ function processNode(node: DocNode, ctx: SlideContext): void {
73
73
  const p = node.props
74
74
 
75
75
  switch (node.type) {
76
- case "heading": {
76
+ case 'heading': {
77
77
  const level = (p.level as number) ?? 1
78
78
  const fontSize = HEADING_SIZES[level] ?? 20
79
79
  ctx.slide.addText(getTextContent(node.children), {
@@ -83,14 +83,14 @@ function processNode(node: DocNode, ctx: SlideContext): void {
83
83
  h: 0.6,
84
84
  fontSize,
85
85
  bold: true,
86
- color: sanitizeXmlColor((p.color as string) ?? "#000000"),
87
- align: (p.align as string) ?? "left",
86
+ color: sanitizeXmlColor((p.color as string) ?? '#000000'),
87
+ align: (p.align as string) ?? 'left',
88
88
  })
89
89
  ctx.y += 0.7
90
90
  break
91
91
  }
92
92
 
93
- case "text": {
93
+ case 'text': {
94
94
  const text = getTextContent(node.children)
95
95
  ctx.slide.addText(text, {
96
96
  x: CONTENT_MARGIN,
@@ -100,21 +100,21 @@ function processNode(node: DocNode, ctx: SlideContext): void {
100
100
  fontSize: (p.size as number) ?? 14,
101
101
  bold: p.bold ?? false,
102
102
  italic: p.italic ?? false,
103
- underline: p.underline ? { style: "sng" } : undefined,
104
- strike: p.strikethrough ? "sngStrike" : undefined,
105
- color: sanitizeXmlColor((p.color as string) ?? "#333333"),
106
- align: (p.align as string) ?? "left",
103
+ underline: p.underline ? { style: 'sng' } : undefined,
104
+ strike: p.strikethrough ? 'sngStrike' : undefined,
105
+ color: sanitizeXmlColor((p.color as string) ?? '#333333'),
106
+ align: (p.align as string) ?? 'left',
107
107
  })
108
108
  ctx.y += 0.5
109
109
  break
110
110
  }
111
111
 
112
- case "image": {
112
+ case 'image': {
113
113
  const src = sanitizeImageSrc(p.src as string)
114
114
  const w = Math.min(((p.width as number) ?? 400) / 96, CONTENT_WIDTH)
115
115
  const h = ((p.height as number) ?? 300) / 96
116
116
 
117
- if (src.startsWith("data:")) {
117
+ if (src.startsWith('data:')) {
118
118
  ctx.slide.addImage({
119
119
  data: src,
120
120
  x: CONTENT_MARGIN,
@@ -128,7 +128,7 @@ function processNode(node: DocNode, ctx: SlideContext): void {
128
128
  break
129
129
  }
130
130
 
131
- case "table": {
131
+ case 'table': {
132
132
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
133
133
  const rows = (p.rows ?? []) as (string | number)[][]
134
134
  const hs = p.headerStyle as { background?: string; color?: string } | undefined
@@ -137,20 +137,20 @@ function processNode(node: DocNode, ctx: SlideContext): void {
137
137
  text: col.header,
138
138
  options: {
139
139
  bold: true,
140
- fill: { color: sanitizeXmlColor(hs?.background ?? "#f5f5f5") },
141
- color: sanitizeXmlColor(hs?.color ?? "#000000"),
142
- align: col.align ?? "left",
140
+ fill: { color: sanitizeXmlColor(hs?.background ?? '#f5f5f5') },
141
+ color: sanitizeXmlColor(hs?.color ?? '#000000'),
142
+ align: col.align ?? 'left',
143
143
  fontSize: 12,
144
144
  },
145
145
  }))
146
146
 
147
147
  const dataRows = rows.map((row, rowIdx) =>
148
148
  columns.map((col, colIdx) => ({
149
- text: String(row[colIdx] ?? ""),
149
+ text: String(row[colIdx] ?? ''),
150
150
  options: {
151
- align: col.align ?? "left",
151
+ align: col.align ?? 'left',
152
152
  fontSize: 11,
153
- fill: p.striped && rowIdx % 2 === 1 ? { color: "F9F9F9" } : undefined,
153
+ fill: p.striped && rowIdx % 2 === 1 ? { color: 'F9F9F9' } : undefined,
154
154
  },
155
155
  })),
156
156
  )
@@ -163,16 +163,16 @@ function processNode(node: DocNode, ctx: SlideContext): void {
163
163
  x: CONTENT_MARGIN,
164
164
  y: ctx.y,
165
165
  w: CONTENT_WIDTH,
166
- border: { pt: 0.5, color: "DDDDDD" },
166
+ border: { pt: 0.5, color: 'DDDDDD' },
167
167
  rowH: rowHeight,
168
168
  })
169
169
  ctx.y += tableHeight + 0.2
170
170
  break
171
171
  }
172
172
 
173
- case "list": {
173
+ case 'list': {
174
174
  const items = node.children
175
- .filter((c): c is DocNode => typeof c !== "string")
175
+ .filter((c): c is DocNode => typeof c !== 'string')
176
176
  .map((item) => getTextContent(item.children))
177
177
 
178
178
  const isOrdered = p.ordered as boolean
@@ -191,7 +191,7 @@ function processNode(node: DocNode, ctx: SlideContext): void {
191
191
  break
192
192
  }
193
193
 
194
- case "code": {
194
+ case 'code': {
195
195
  const text = getTextContent(node.children)
196
196
  ctx.slide.addText(text, {
197
197
  x: CONTENT_MARGIN,
@@ -199,15 +199,15 @@ function processNode(node: DocNode, ctx: SlideContext): void {
199
199
  w: CONTENT_WIDTH,
200
200
  h: 0.5,
201
201
  fontSize: 10,
202
- fontFace: "Courier New",
203
- fill: { color: "F5F5F5" },
204
- color: "333333",
202
+ fontFace: 'Courier New',
203
+ fill: { color: 'F5F5F5' },
204
+ color: '333333',
205
205
  })
206
206
  ctx.y += 0.6
207
207
  break
208
208
  }
209
209
 
210
- case "quote": {
210
+ case 'quote': {
211
211
  const text = getTextContent(node.children)
212
212
  ctx.slide.addText(text, {
213
213
  x: CONTENT_MARGIN + 0.3,
@@ -216,28 +216,28 @@ function processNode(node: DocNode, ctx: SlideContext): void {
216
216
  h: 0.5,
217
217
  fontSize: 13,
218
218
  italic: true,
219
- color: "555555",
219
+ color: '555555',
220
220
  })
221
221
  ctx.y += 0.6
222
222
  break
223
223
  }
224
224
 
225
- case "link": {
225
+ case 'link': {
226
226
  ctx.slide.addText(getTextContent(node.children), {
227
227
  x: CONTENT_MARGIN,
228
228
  y: ctx.y,
229
229
  w: CONTENT_WIDTH,
230
230
  h: 0.4,
231
231
  fontSize: 13,
232
- color: "4F46E5",
233
- underline: { style: "sng" },
232
+ color: '4F46E5',
233
+ underline: { style: 'sng' },
234
234
  hyperlink: { url: sanitizeHref(p.href as string) },
235
235
  })
236
236
  ctx.y += 0.5
237
237
  break
238
238
  }
239
239
 
240
- case "button": {
240
+ case 'button': {
241
241
  ctx.slide.addText(getTextContent(node.children), {
242
242
  x: CONTENT_MARGIN,
243
243
  y: ctx.y,
@@ -245,41 +245,41 @@ function processNode(node: DocNode, ctx: SlideContext): void {
245
245
  h: 0.5,
246
246
  fontSize: 14,
247
247
  bold: true,
248
- color: sanitizeXmlColor((p.color as string) ?? "#ffffff"),
248
+ color: sanitizeXmlColor((p.color as string) ?? '#ffffff'),
249
249
  fill: {
250
- color: sanitizeXmlColor((p.background as string) ?? "#4f46e5"),
250
+ color: sanitizeXmlColor((p.background as string) ?? '#4f46e5'),
251
251
  },
252
- align: "center",
252
+ align: 'center',
253
253
  hyperlink: { url: sanitizeHref(p.href as string) },
254
254
  })
255
255
  ctx.y += 0.6
256
256
  break
257
257
  }
258
258
 
259
- case "spacer": {
259
+ case 'spacer': {
260
260
  ctx.y += ((p.height as number) ?? 12) / 72
261
261
  break
262
262
  }
263
263
 
264
- case "divider": {
264
+ case 'divider': {
265
265
  // Render as a thin line using a text element with top border
266
- ctx.slide.addText("", {
266
+ ctx.slide.addText('', {
267
267
  x: CONTENT_MARGIN,
268
268
  y: ctx.y,
269
269
  w: CONTENT_WIDTH,
270
270
  h: 0.02,
271
- fill: { color: sanitizeXmlColor((p.color as string) ?? "#DDDDDD") },
271
+ fill: { color: sanitizeXmlColor((p.color as string) ?? '#DDDDDD') },
272
272
  })
273
273
  ctx.y += 0.2
274
274
  break
275
275
  }
276
276
 
277
277
  // Container types — recurse into children
278
- case "section":
279
- case "row":
280
- case "column":
278
+ case 'section':
279
+ case 'row':
280
+ case 'column':
281
281
  for (const child of node.children) {
282
- if (typeof child !== "string") {
282
+ if (typeof child !== 'string') {
283
283
  processNode(child, ctx)
284
284
  }
285
285
  }
@@ -295,7 +295,7 @@ function processSlide(pageNode: DocNode, pptx: PptxGen): void {
295
295
  const ctx: SlideContext = { slide, y: CONTENT_MARGIN }
296
296
 
297
297
  for (const child of pageNode.children) {
298
- if (typeof child !== "string") {
298
+ if (typeof child !== 'string') {
299
299
  processNode(child, ctx)
300
300
  }
301
301
  }
@@ -305,7 +305,7 @@ export const pptxRenderer: DocumentRenderer = {
305
305
  async render(node: DocNode, _options?: RenderOptions): Promise<Uint8Array> {
306
306
  let PptxGenJS: any
307
307
  try {
308
- PptxGenJS = await import("pptxgenjs")
308
+ PptxGenJS = await import('pptxgenjs')
309
309
  } catch {
310
310
  throw new Error(
311
311
  '[@pyreon/document] PPTX renderer requires "pptxgenjs" package. Install it: bun add pptxgenjs',
@@ -323,7 +323,7 @@ export const pptxRenderer: DocumentRenderer = {
323
323
  // Collect pages — each becomes a slide
324
324
  const pages: DocNode[] = []
325
325
  for (const child of node.children) {
326
- if (typeof child !== "string" && child.type === "page") {
326
+ if (typeof child !== 'string' && child.type === 'page') {
327
327
  pages.push(child)
328
328
  }
329
329
  }
@@ -331,7 +331,7 @@ export const pptxRenderer: DocumentRenderer = {
331
331
  // If no explicit pages, treat entire document content as one slide
332
332
  if (pages.length === 0) {
333
333
  const syntheticPage: DocNode = {
334
- type: "page",
334
+ type: 'page',
335
335
  props: {},
336
336
  children: node.children,
337
337
  }
@@ -342,7 +342,7 @@ export const pptxRenderer: DocumentRenderer = {
342
342
  processSlide(page, pptx)
343
343
  }
344
344
 
345
- const output = await pptx.write("arraybuffer")
345
+ const output = await pptx.write('arraybuffer')
346
346
  return new Uint8Array(output as ArrayBuffer)
347
347
  },
348
348
  }
@@ -1,5 +1,5 @@
1
- import { sanitizeHref, sanitizeImageSrc } from "../sanitize"
2
- import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from "../types"
1
+ import { sanitizeHref, sanitizeImageSrc } from '../sanitize'
2
+ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn } from '../types'
3
3
 
4
4
  /**
5
5
  * Slack Block Kit renderer — outputs JSON that can be posted via Slack's API.
@@ -7,13 +7,13 @@ import type { DocChild, DocNode, DocumentRenderer, RenderOptions, TableColumn }
7
7
  */
8
8
 
9
9
  function resolveColumn(col: string | TableColumn): TableColumn {
10
- return typeof col === "string" ? { header: col } : col
10
+ return typeof col === 'string' ? { header: col } : col
11
11
  }
12
12
 
13
13
  function getTextContent(children: DocChild[]): string {
14
14
  return children
15
- .map((c) => (typeof c === "string" ? c : getTextContent((c as DocNode).children)))
16
- .join("")
15
+ .map((c) => (typeof c === 'string' ? c : getTextContent((c as DocNode).children)))
16
+ .join('')
17
17
  }
18
18
 
19
19
  interface SlackBlock {
@@ -21,12 +21,12 @@ interface SlackBlock {
21
21
  [key: string]: unknown
22
22
  }
23
23
 
24
- function mrkdwn(text: string): { type: "mrkdwn"; text: string } {
25
- return { type: "mrkdwn", text }
24
+ function mrkdwn(text: string): { type: 'mrkdwn'; text: string } {
25
+ return { type: 'mrkdwn', text }
26
26
  }
27
27
 
28
- function plainText(text: string): { type: "plain_text"; text: string } {
29
- return { type: "plain_text", text }
28
+ function plainText(text: string): { type: 'plain_text'; text: string } {
29
+ return { type: 'plain_text', text }
30
30
  }
31
31
 
32
32
  function nodeToBlocks(node: DocNode): SlackBlock[] {
@@ -34,136 +34,136 @@ function nodeToBlocks(node: DocNode): SlackBlock[] {
34
34
  const blocks: SlackBlock[] = []
35
35
 
36
36
  switch (node.type) {
37
- case "document":
38
- case "page":
39
- case "section":
40
- case "row":
41
- case "column":
37
+ case 'document':
38
+ case 'page':
39
+ case 'section':
40
+ case 'row':
41
+ case 'column':
42
42
  for (const child of node.children) {
43
- if (typeof child !== "string") {
43
+ if (typeof child !== 'string') {
44
44
  blocks.push(...nodeToBlocks(child))
45
45
  }
46
46
  }
47
47
  break
48
48
 
49
- case "heading":
49
+ case 'heading':
50
50
  blocks.push({
51
- type: "header",
51
+ type: 'header',
52
52
  text: plainText(getTextContent(node.children)),
53
53
  })
54
54
  break
55
55
 
56
- case "text": {
56
+ case 'text': {
57
57
  let text = getTextContent(node.children)
58
58
  if (p.bold) text = `*${text}*`
59
59
  if (p.italic) text = `_${text}_`
60
60
  if (p.strikethrough) text = `~${text}~`
61
61
  blocks.push({
62
- type: "section",
62
+ type: 'section',
63
63
  text: mrkdwn(text),
64
64
  })
65
65
  break
66
66
  }
67
67
 
68
- case "link": {
68
+ case 'link': {
69
69
  const href = sanitizeHref(p.href as string)
70
70
  const text = getTextContent(node.children)
71
71
  blocks.push({
72
- type: "section",
72
+ type: 'section',
73
73
  text: mrkdwn(`<${href}|${text}>`),
74
74
  })
75
75
  break
76
76
  }
77
77
 
78
- case "image": {
78
+ case 'image': {
79
79
  const src = sanitizeImageSrc(p.src as string)
80
80
  // Slack only supports public URLs for images
81
- if (src.startsWith("http")) {
81
+ if (src.startsWith('http')) {
82
82
  blocks.push({
83
- type: "image",
83
+ type: 'image',
84
84
  image_url: src,
85
- alt_text: (p.alt as string) ?? "Image",
85
+ alt_text: (p.alt as string) ?? 'Image',
86
86
  ...(p.caption ? { title: plainText(p.caption as string) } : {}),
87
87
  })
88
88
  }
89
89
  break
90
90
  }
91
91
 
92
- case "table": {
92
+ case 'table': {
93
93
  const columns = ((p.columns ?? []) as (string | TableColumn)[]).map(resolveColumn)
94
94
  const rows = (p.rows ?? []) as (string | number)[][]
95
95
 
96
96
  // Slack doesn't have native tables — render as formatted text
97
- const header = columns.map((c) => `*${c.header}*`).join(" | ")
98
- const separator = columns.map(() => "---").join(" | ")
99
- const body = rows.map((row) => row.map((cell) => String(cell ?? "")).join(" | ")).join("\n")
97
+ const header = columns.map((c) => `*${c.header}*`).join(' | ')
98
+ const separator = columns.map(() => '---').join(' | ')
99
+ const body = rows.map((row) => row.map((cell) => String(cell ?? '')).join(' | ')).join('\n')
100
100
 
101
101
  let text = `${header}\n${separator}\n${body}`
102
102
  if (p.caption) text = `_${p.caption}_\n${text}`
103
103
 
104
104
  blocks.push({
105
- type: "section",
105
+ type: 'section',
106
106
  text: mrkdwn(`\`\`\`\n${text}\n\`\`\``),
107
107
  })
108
108
  break
109
109
  }
110
110
 
111
- case "list": {
111
+ case 'list': {
112
112
  const ordered = p.ordered as boolean | undefined
113
113
  const items = node.children
114
- .filter((c): c is DocNode => typeof c !== "string")
114
+ .filter((c): c is DocNode => typeof c !== 'string')
115
115
  .map((item, i) => {
116
- const prefix = ordered ? `${i + 1}.` : ""
116
+ const prefix = ordered ? `${i + 1}.` : ''
117
117
  return `${prefix} ${getTextContent(item.children)}`
118
118
  })
119
- .join("\n")
119
+ .join('\n')
120
120
  blocks.push({
121
- type: "section",
121
+ type: 'section',
122
122
  text: mrkdwn(items),
123
123
  })
124
124
  break
125
125
  }
126
126
 
127
- case "code": {
127
+ case 'code': {
128
128
  const text = getTextContent(node.children)
129
- const lang = (p.language as string) ?? ""
129
+ const lang = (p.language as string) ?? ''
130
130
  blocks.push({
131
- type: "section",
131
+ type: 'section',
132
132
  text: mrkdwn(`\`\`\`${lang}\n${text}\n\`\`\``),
133
133
  })
134
134
  break
135
135
  }
136
136
 
137
- case "divider":
138
- case "page-break":
139
- blocks.push({ type: "divider" })
137
+ case 'divider':
138
+ case 'page-break':
139
+ blocks.push({ type: 'divider' })
140
140
  break
141
141
 
142
- case "spacer":
142
+ case 'spacer':
143
143
  // No equivalent in Slack — skip
144
144
  break
145
145
 
146
- case "button": {
146
+ case 'button': {
147
147
  const href = sanitizeHref(p.href as string)
148
148
  const text = getTextContent(node.children)
149
149
  blocks.push({
150
- type: "actions",
150
+ type: 'actions',
151
151
  elements: [
152
152
  {
153
- type: "button",
153
+ type: 'button',
154
154
  text: plainText(text),
155
155
  url: href,
156
- style: "primary",
156
+ style: 'primary',
157
157
  },
158
158
  ],
159
159
  })
160
160
  break
161
161
  }
162
162
 
163
- case "quote": {
163
+ case 'quote': {
164
164
  const text = getTextContent(node.children)
165
165
  blocks.push({
166
- type: "section",
166
+ type: 'section',
167
167
  text: mrkdwn(`> ${text}`),
168
168
  })
169
169
  break