@bendyline/squisq-formats 1.3.0 → 1.4.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 (224) hide show
  1. package/dist/chunk-6M7Z25LA.js +46 -0
  2. package/dist/chunk-6M7Z25LA.js.map +1 -0
  3. package/dist/{chunk-S3Y7H2BK.js → chunk-APSIAEXL.js} +9 -8
  4. package/dist/chunk-APSIAEXL.js.map +1 -0
  5. package/dist/{chunk-UDS45KUJ.js → chunk-BJQIJ4S4.js} +36 -52
  6. package/dist/chunk-BJQIJ4S4.js.map +1 -0
  7. package/dist/{chunk-33YRFXZZ.js → chunk-CRTC6DEX.js} +259 -13
  8. package/dist/chunk-CRTC6DEX.js.map +1 -0
  9. package/dist/chunk-FMOIGA5T.js +201 -0
  10. package/dist/chunk-FMOIGA5T.js.map +1 -0
  11. package/dist/{chunk-VN2KEOYB.js → chunk-I2GFTEOV.js} +321 -91
  12. package/dist/chunk-I2GFTEOV.js.map +1 -0
  13. package/dist/chunk-JBNVE2GV.js +116 -0
  14. package/dist/chunk-JBNVE2GV.js.map +1 -0
  15. package/dist/chunk-L53YIGYS.js +141 -0
  16. package/dist/chunk-L53YIGYS.js.map +1 -0
  17. package/dist/{chunk-ERZ627GR.js → chunk-ON232HJR.js} +11 -26
  18. package/dist/chunk-ON232HJR.js.map +1 -0
  19. package/dist/chunk-PN52A5AA.js +32 -0
  20. package/dist/chunk-PN52A5AA.js.map +1 -0
  21. package/dist/{chunk-7DEUGIOO.js → chunk-QSE7EWE7.js} +12 -87
  22. package/dist/chunk-QSE7EWE7.js.map +1 -0
  23. package/dist/container/index.d.ts +7 -5
  24. package/dist/container/index.js +39 -70
  25. package/dist/container/index.js.map +1 -1
  26. package/dist/csv/index.d.ts +37 -0
  27. package/dist/csv/index.js +13 -0
  28. package/dist/csv/index.js.map +1 -0
  29. package/dist/docx/index.d.ts +128 -15
  30. package/dist/docx/index.js +19 -22
  31. package/dist/docx/index.js.map +1 -1
  32. package/dist/epub/index.d.ts +66 -11
  33. package/dist/epub/index.js +11 -18
  34. package/dist/epub/index.js.map +1 -1
  35. package/dist/html/index.d.ts +315 -15
  36. package/dist/html/index.js +32 -165
  37. package/dist/html/index.js.map +1 -1
  38. package/dist/index.d.ts +10 -38
  39. package/dist/index.js +80 -37
  40. package/dist/index.js.map +1 -1
  41. package/dist/ooxml/index.d.ts +353 -14
  42. package/dist/ooxml/index.js +116 -22
  43. package/dist/ooxml/index.js.map +1 -1
  44. package/dist/pdf/index.d.ts +143 -17
  45. package/dist/pdf/index.js +16 -25
  46. package/dist/pdf/index.js.map +1 -1
  47. package/dist/pptx/index.d.ts +98 -21
  48. package/dist/pptx/index.js +18 -34
  49. package/dist/pptx/index.js.map +1 -1
  50. package/dist/xlsx/index.d.ts +29 -25
  51. package/dist/xlsx/index.js +13 -44
  52. package/dist/xlsx/index.js.map +1 -1
  53. package/package.json +7 -2
  54. package/src/__tests__/csvImport.test.ts +51 -0
  55. package/src/__tests__/exportThemeReconciliation.test.ts +87 -0
  56. package/src/__tests__/htmlImport.test.ts +57 -0
  57. package/src/__tests__/plainHtml.test.ts +31 -0
  58. package/src/__tests__/pptxExport.test.ts +138 -0
  59. package/src/__tests__/pptxImport.test.ts +70 -0
  60. package/src/__tests__/roundTripMatrix.fixtures.ts +86 -0
  61. package/src/__tests__/roundTripMatrix.helpers.ts +154 -0
  62. package/src/__tests__/roundTripMatrix.test.ts +136 -0
  63. package/src/__tests__/xlsxImport.test.ts +80 -0
  64. package/src/csv/index.ts +165 -0
  65. package/src/docx/export.ts +27 -44
  66. package/src/docx/import.ts +1 -2
  67. package/src/epub/export.ts +10 -27
  68. package/src/html/import.ts +297 -0
  69. package/src/html/index.ts +4 -0
  70. package/src/html/plainHtml.ts +66 -16
  71. package/src/index.ts +10 -3
  72. package/src/ooxml/namespaces.ts +3 -0
  73. package/src/pdf/export.ts +10 -5
  74. package/src/pdf/import.ts +1 -1
  75. package/src/pptx/export.ts +225 -85
  76. package/src/pptx/import.ts +156 -0
  77. package/src/pptx/index.ts +10 -28
  78. package/src/shared/inlineRuns.ts +99 -0
  79. package/src/shared/text.ts +41 -0
  80. package/src/xlsx/import.ts +159 -0
  81. package/src/xlsx/index.ts +11 -28
  82. package/dist/__tests__/container.test.d.ts +0 -8
  83. package/dist/__tests__/container.test.d.ts.map +0 -1
  84. package/dist/__tests__/container.test.js +0 -147
  85. package/dist/__tests__/container.test.js.map +0 -1
  86. package/dist/__tests__/docxExport.test.d.ts +0 -8
  87. package/dist/__tests__/docxExport.test.d.ts.map +0 -1
  88. package/dist/__tests__/docxExport.test.js +0 -383
  89. package/dist/__tests__/docxExport.test.js.map +0 -1
  90. package/dist/__tests__/docxImport.test.d.ts +0 -8
  91. package/dist/__tests__/docxImport.test.d.ts.map +0 -1
  92. package/dist/__tests__/docxImport.test.js +0 -309
  93. package/dist/__tests__/docxImport.test.js.map +0 -1
  94. package/dist/__tests__/epub.test.d.ts +0 -8
  95. package/dist/__tests__/epub.test.d.ts.map +0 -1
  96. package/dist/__tests__/epub.test.js +0 -472
  97. package/dist/__tests__/epub.test.js.map +0 -1
  98. package/dist/__tests__/html.test.d.ts +0 -8
  99. package/dist/__tests__/html.test.d.ts.map +0 -1
  100. package/dist/__tests__/html.test.js +0 -317
  101. package/dist/__tests__/html.test.js.map +0 -1
  102. package/dist/__tests__/ooxml.test.d.ts +0 -5
  103. package/dist/__tests__/ooxml.test.d.ts.map +0 -1
  104. package/dist/__tests__/ooxml.test.js +0 -152
  105. package/dist/__tests__/ooxml.test.js.map +0 -1
  106. package/dist/__tests__/pdfExport.test.d.ts +0 -8
  107. package/dist/__tests__/pdfExport.test.d.ts.map +0 -1
  108. package/dist/__tests__/pdfExport.test.js +0 -275
  109. package/dist/__tests__/pdfExport.test.js.map +0 -1
  110. package/dist/__tests__/pdfImport.test.d.ts +0 -11
  111. package/dist/__tests__/pdfImport.test.d.ts.map +0 -1
  112. package/dist/__tests__/pdfImport.test.js +0 -254
  113. package/dist/__tests__/pdfImport.test.js.map +0 -1
  114. package/dist/__tests__/plainHtml.test.d.ts +0 -5
  115. package/dist/__tests__/plainHtml.test.d.ts.map +0 -1
  116. package/dist/__tests__/plainHtml.test.js +0 -327
  117. package/dist/__tests__/plainHtml.test.js.map +0 -1
  118. package/dist/__tests__/plainHtmlBundle.test.d.ts +0 -11
  119. package/dist/__tests__/plainHtmlBundle.test.d.ts.map +0 -1
  120. package/dist/__tests__/plainHtmlBundle.test.js +0 -210
  121. package/dist/__tests__/plainHtmlBundle.test.js.map +0 -1
  122. package/dist/__tests__/roundTrip.test.d.ts +0 -7
  123. package/dist/__tests__/roundTrip.test.d.ts.map +0 -1
  124. package/dist/__tests__/roundTrip.test.js +0 -173
  125. package/dist/__tests__/roundTrip.test.js.map +0 -1
  126. package/dist/chunk-33YRFXZZ.js.map +0 -1
  127. package/dist/chunk-67KIJHV2.js +0 -21
  128. package/dist/chunk-67KIJHV2.js.map +0 -1
  129. package/dist/chunk-7DEUGIOO.js.map +0 -1
  130. package/dist/chunk-ERZ627GR.js.map +0 -1
  131. package/dist/chunk-S3Y7H2BK.js.map +0 -1
  132. package/dist/chunk-UDS45KUJ.js.map +0 -1
  133. package/dist/chunk-VN2KEOYB.js.map +0 -1
  134. package/dist/chunk-YN5HFCEW.js +0 -120
  135. package/dist/chunk-YN5HFCEW.js.map +0 -1
  136. package/dist/container/index.d.ts.map +0 -1
  137. package/dist/docx/export.d.ts +0 -69
  138. package/dist/docx/export.d.ts.map +0 -1
  139. package/dist/docx/export.js +0 -985
  140. package/dist/docx/export.js.map +0 -1
  141. package/dist/docx/import.d.ts +0 -65
  142. package/dist/docx/import.d.ts.map +0 -1
  143. package/dist/docx/import.js +0 -779
  144. package/dist/docx/import.js.map +0 -1
  145. package/dist/docx/index.d.ts.map +0 -1
  146. package/dist/docx/styles.d.ts +0 -68
  147. package/dist/docx/styles.d.ts.map +0 -1
  148. package/dist/docx/styles.js +0 -125
  149. package/dist/docx/styles.js.map +0 -1
  150. package/dist/epub/export.d.ts +0 -72
  151. package/dist/epub/export.d.ts.map +0 -1
  152. package/dist/epub/export.js +0 -698
  153. package/dist/epub/export.js.map +0 -1
  154. package/dist/epub/index.d.ts.map +0 -1
  155. package/dist/html/docsHtmlBundle.d.ts +0 -53
  156. package/dist/html/docsHtmlBundle.d.ts.map +0 -1
  157. package/dist/html/docsHtmlBundle.js +0 -299
  158. package/dist/html/docsHtmlBundle.js.map +0 -1
  159. package/dist/html/htmlTemplate.d.ts +0 -70
  160. package/dist/html/htmlTemplate.d.ts.map +0 -1
  161. package/dist/html/htmlTemplate.js +0 -282
  162. package/dist/html/htmlTemplate.js.map +0 -1
  163. package/dist/html/imageUtils.d.ts +0 -29
  164. package/dist/html/imageUtils.d.ts.map +0 -1
  165. package/dist/html/imageUtils.js +0 -63
  166. package/dist/html/imageUtils.js.map +0 -1
  167. package/dist/html/index.d.ts.map +0 -1
  168. package/dist/html/plainHtml.d.ts +0 -79
  169. package/dist/html/plainHtml.d.ts.map +0 -1
  170. package/dist/html/plainHtml.js +0 -614
  171. package/dist/html/plainHtml.js.map +0 -1
  172. package/dist/html/plainHtmlBundle.d.ts +0 -93
  173. package/dist/html/plainHtmlBundle.d.ts.map +0 -1
  174. package/dist/html/plainHtmlBundle.js +0 -357
  175. package/dist/html/plainHtmlBundle.js.map +0 -1
  176. package/dist/index.d.ts.map +0 -1
  177. package/dist/ooxml/index.d.ts.map +0 -1
  178. package/dist/ooxml/namespaces.d.ts +0 -77
  179. package/dist/ooxml/namespaces.d.ts.map +0 -1
  180. package/dist/ooxml/namespaces.js +0 -104
  181. package/dist/ooxml/namespaces.js.map +0 -1
  182. package/dist/ooxml/reader.d.ts +0 -52
  183. package/dist/ooxml/reader.d.ts.map +0 -1
  184. package/dist/ooxml/reader.js +0 -190
  185. package/dist/ooxml/reader.js.map +0 -1
  186. package/dist/ooxml/types.d.ts +0 -82
  187. package/dist/ooxml/types.d.ts.map +0 -1
  188. package/dist/ooxml/types.js +0 -9
  189. package/dist/ooxml/types.js.map +0 -1
  190. package/dist/ooxml/writer.d.ts +0 -71
  191. package/dist/ooxml/writer.d.ts.map +0 -1
  192. package/dist/ooxml/writer.js +0 -200
  193. package/dist/ooxml/writer.js.map +0 -1
  194. package/dist/ooxml/xmlUtils.d.ts +0 -76
  195. package/dist/ooxml/xmlUtils.d.ts.map +0 -1
  196. package/dist/ooxml/xmlUtils.js +0 -108
  197. package/dist/ooxml/xmlUtils.js.map +0 -1
  198. package/dist/pdf/export.d.ts +0 -56
  199. package/dist/pdf/export.d.ts.map +0 -1
  200. package/dist/pdf/export.js +0 -768
  201. package/dist/pdf/export.js.map +0 -1
  202. package/dist/pdf/import.d.ts +0 -94
  203. package/dist/pdf/import.d.ts.map +0 -1
  204. package/dist/pdf/import.js +0 -796
  205. package/dist/pdf/import.js.map +0 -1
  206. package/dist/pdf/index.d.ts.map +0 -1
  207. package/dist/pdf/styles.d.ts +0 -142
  208. package/dist/pdf/styles.d.ts.map +0 -1
  209. package/dist/pdf/styles.js +0 -145
  210. package/dist/pdf/styles.js.map +0 -1
  211. package/dist/pptx/export.d.ts +0 -75
  212. package/dist/pptx/export.d.ts.map +0 -1
  213. package/dist/pptx/export.js +0 -724
  214. package/dist/pptx/export.js.map +0 -1
  215. package/dist/pptx/index.d.ts.map +0 -1
  216. package/dist/pptx/styles.d.ts +0 -54
  217. package/dist/pptx/styles.d.ts.map +0 -1
  218. package/dist/pptx/styles.js +0 -73
  219. package/dist/pptx/styles.js.map +0 -1
  220. package/dist/pptx/templates.d.ts +0 -24
  221. package/dist/pptx/templates.d.ts.map +0 -1
  222. package/dist/pptx/templates.js +0 -158
  223. package/dist/pptx/templates.js.map +0 -1
  224. package/dist/xlsx/index.d.ts.map +0 -1
@@ -1,768 +0,0 @@
1
- /**
2
- * PDF Export
3
- *
4
- * Converts a squisq MarkdownDocument (or Doc) into a PDF file
5
- * using pdf-lib. Generates paginated, styled output with support for
6
- * headings, paragraphs, inline formatting, lists, code blocks,
7
- * blockquotes, tables, thematic breaks, and hyperlinks.
8
- *
9
- * Uses only the 14 standard PDF fonts (no font embedding required),
10
- * keeping output size small and rendering fast.
11
- *
12
- * @example
13
- * ```ts
14
- * import { parseMarkdown } from '@bendyline/squisq/markdown';
15
- * import { markdownDocToPdf } from '@bendyline/squisq-formats/pdf';
16
- *
17
- * const md = parseMarkdown('# Hello\n\nWorld **bold** text');
18
- * const buffer = await markdownDocToPdf(md);
19
- * ```
20
- */
21
- import { PDFDocument, StandardFonts, rgb } from 'pdf-lib';
22
- import { resolveTheme } from '@bendyline/squisq/schemas';
23
- import { docToMarkdown } from '@bendyline/squisq/doc';
24
- import { PAGE_WIDTH_LETTER, PAGE_HEIGHT_LETTER, PAGE_WIDTH_A4, PAGE_HEIGHT_A4, DEFAULT_MARGIN, DEFAULT_FONT_SIZE, HEADING_SIZES, CODE_FONT_SIZE, LINE_HEIGHT_FACTOR, HEADING_SPACE_BEFORE, HEADING_SPACE_AFTER, PARAGRAPH_SPACING, LIST_INDENT, BULLET_CHAR, BLOCKQUOTE_INDENT, BLOCKQUOTE_BAR_WIDTH, COLOR_TEXT, COLOR_HEADING, COLOR_LINK, COLOR_CODE_TEXT, COLOR_BLOCKQUOTE_BAR, COLOR_BLOCKQUOTE_TEXT, COLOR_THEMATIC_BREAK, COLOR_TABLE_BORDER, COLOR_TABLE_HEADER_BG, TABLE_CELL_PAD_X, TABLE_CELL_PAD_Y, TABLE_BORDER_WIDTH, } from './styles.js';
25
- /**
26
- * Convert a MarkdownDocument to a PDF ArrayBuffer.
27
- */
28
- export async function markdownDocToPdf(doc, options = {}) {
29
- const pdfDoc = await PDFDocument.create();
30
- // Metadata
31
- if (options.title)
32
- pdfDoc.setTitle(options.title);
33
- if (options.author)
34
- pdfDoc.setAuthor(options.author);
35
- pdfDoc.setCreationDate(new Date());
36
- pdfDoc.setModificationDate(new Date());
37
- const ctx = await createExportContext(pdfDoc, options);
38
- renderBlocks(doc.children, ctx, 0);
39
- const bytes = await pdfDoc.save();
40
- return bytes.buffer;
41
- }
42
- /**
43
- * Convert a squisq Doc to a PDF ArrayBuffer.
44
- *
45
- * Convenience wrapper: Doc → MarkdownDocument → PDF.
46
- */
47
- export async function docToPdf(doc, options = {}) {
48
- const markdownDoc = docToMarkdown(doc);
49
- return markdownDocToPdf(markdownDoc, options);
50
- }
51
- async function createExportContext(pdfDoc, options) {
52
- const [regular, bold, italic, boldItalic, mono, monoBold] = await Promise.all([
53
- pdfDoc.embedFont(StandardFonts.Helvetica),
54
- pdfDoc.embedFont(StandardFonts.HelveticaBold),
55
- pdfDoc.embedFont(StandardFonts.HelveticaOblique),
56
- pdfDoc.embedFont(StandardFonts.HelveticaBoldOblique),
57
- pdfDoc.embedFont(StandardFonts.Courier),
58
- pdfDoc.embedFont(StandardFonts.CourierBold),
59
- ]);
60
- const isA4 = options.pageSize === 'a4';
61
- const pageWidth = isA4 ? PAGE_WIDTH_A4 : PAGE_WIDTH_LETTER;
62
- const pageHeight = isA4 ? PAGE_HEIGHT_A4 : PAGE_HEIGHT_LETTER;
63
- const margin = options.margin ?? DEFAULT_MARGIN;
64
- const fontSize = options.defaultFontSize ?? DEFAULT_FONT_SIZE;
65
- const page = pdfDoc.addPage([pageWidth, pageHeight]);
66
- // Resolve theme colors if themeId is set
67
- let colorText = COLOR_TEXT;
68
- let colorHeading = COLOR_HEADING;
69
- let colorLink = COLOR_LINK;
70
- if (options.themeId) {
71
- const theme = resolveTheme(options.themeId);
72
- if (theme.colors) {
73
- colorText = hexToRgb(theme.colors.text) ?? COLOR_TEXT;
74
- colorHeading = hexToRgb(theme.colors.primary) ?? COLOR_HEADING;
75
- colorLink = hexToRgb(theme.colors.highlight || theme.colors.secondary) ?? COLOR_LINK;
76
- }
77
- }
78
- return {
79
- pdfDoc,
80
- fonts: { regular, bold, italic, boldItalic, mono, monoBold },
81
- pageWidth,
82
- pageHeight,
83
- margin,
84
- fontSize,
85
- page,
86
- y: pageHeight - margin,
87
- contentWidth: pageWidth - 2 * margin,
88
- bottomY: margin,
89
- colors: { text: colorText, heading: colorHeading, link: colorLink },
90
- };
91
- }
92
- // ============================================
93
- // Page Break Management
94
- // ============================================
95
- function ensureSpace(ctx, needed) {
96
- if (ctx.y - needed < ctx.bottomY) {
97
- newPage(ctx);
98
- }
99
- }
100
- function newPage(ctx) {
101
- const page = ctx.pdfDoc.addPage([ctx.pageWidth, ctx.pageHeight]);
102
- ctx.page = page;
103
- ctx.y = ctx.pageHeight - ctx.margin;
104
- }
105
- /**
106
- * Flatten an inline node tree into a flat list of TextSpans,
107
- * accumulating bold/italic/code state as we recurse.
108
- */
109
- function flattenInlines(nodes, ctx, state) {
110
- const spans = [];
111
- for (const node of nodes) {
112
- switch (node.type) {
113
- case 'text': {
114
- const font = state.code
115
- ? state.bold
116
- ? ctx.fonts.monoBold
117
- : ctx.fonts.mono
118
- : pickFont(ctx, state.bold, state.italic);
119
- spans.push({
120
- text: node.value,
121
- font,
122
- fontSize: state.code ? CODE_FONT_SIZE : ctx.fontSize,
123
- color: state.code ? COLOR_CODE_TEXT : (state.color ?? ctx.colors.text),
124
- link: state.link,
125
- strikethrough: state.strikethrough,
126
- });
127
- break;
128
- }
129
- case 'strong':
130
- spans.push(...flattenInlines(node.children, ctx, { ...state, bold: true }));
131
- break;
132
- case 'emphasis':
133
- spans.push(...flattenInlines(node.children, ctx, { ...state, italic: true }));
134
- break;
135
- case 'delete':
136
- spans.push(...flattenInlines(node.children, ctx, {
137
- ...state,
138
- strikethrough: true,
139
- }));
140
- break;
141
- case 'inlineCode': {
142
- spans.push({
143
- text: node.value,
144
- font: ctx.fonts.mono,
145
- fontSize: CODE_FONT_SIZE,
146
- color: COLOR_CODE_TEXT,
147
- link: state.link,
148
- });
149
- break;
150
- }
151
- case 'link': {
152
- const linkNode = node;
153
- spans.push(...flattenInlines(linkNode.children, ctx, {
154
- ...state,
155
- link: linkNode.url,
156
- color: ctx.colors.link,
157
- }));
158
- break;
159
- }
160
- case 'image': {
161
- const imgNode = node;
162
- spans.push({
163
- text: imgNode.alt ? `[Image: ${imgNode.alt}]` : '[Image]',
164
- font: ctx.fonts.italic,
165
- fontSize: ctx.fontSize,
166
- color: COLOR_BLOCKQUOTE_TEXT,
167
- });
168
- break;
169
- }
170
- case 'break':
171
- spans.push({
172
- text: '\n',
173
- font: ctx.fonts.regular,
174
- fontSize: ctx.fontSize,
175
- color: ctx.colors.text,
176
- });
177
- break;
178
- case 'htmlInline': {
179
- const html = node.rawHtml;
180
- if (html) {
181
- spans.push({
182
- text: html,
183
- font: ctx.fonts.mono,
184
- fontSize: CODE_FONT_SIZE,
185
- color: COLOR_CODE_TEXT,
186
- });
187
- }
188
- break;
189
- }
190
- case 'inlineMath':
191
- spans.push({
192
- text: node.value,
193
- font: ctx.fonts.mono,
194
- fontSize: CODE_FONT_SIZE,
195
- color: COLOR_CODE_TEXT,
196
- });
197
- break;
198
- case 'footnoteReference': {
199
- const ref = node;
200
- spans.push({
201
- text: `[${ref.identifier}]`,
202
- font: ctx.fonts.regular,
203
- fontSize: ctx.fontSize * 0.75,
204
- color: ctx.colors.link,
205
- });
206
- break;
207
- }
208
- // linkReference, imageReference, textDirective — render children or identifier
209
- default: {
210
- const fallback = node;
211
- if (fallback.children) {
212
- spans.push(...flattenInlines(fallback.children, ctx, state));
213
- }
214
- else if (fallback.value) {
215
- spans.push({
216
- text: fallback.value,
217
- font: pickFont(ctx, state.bold, state.italic),
218
- fontSize: ctx.fontSize,
219
- color: state.color ?? ctx.colors.text,
220
- });
221
- }
222
- break;
223
- }
224
- }
225
- }
226
- return spans;
227
- }
228
- function pickFont(ctx, bold, italic) {
229
- if (bold && italic)
230
- return ctx.fonts.boldItalic;
231
- if (bold)
232
- return ctx.fonts.bold;
233
- if (italic)
234
- return ctx.fonts.italic;
235
- return ctx.fonts.regular;
236
- }
237
- // ============================================
238
- // Word-Wrap & Draw
239
- // ============================================
240
- /**
241
- * Word-wraps and draws a flat list of TextSpans within the given
242
- * available width, starting at ctx.y. Updates ctx.y after drawing.
243
- * Returns the y position after the last line.
244
- */
245
- function drawSpans(spans, ctx, availableWidth, x0) {
246
- if (spans.length === 0)
247
- return;
248
- // Split spans at \n and word boundaries, then wrap into lines
249
- const lines = wrapSpans(spans, availableWidth);
250
- for (const line of lines) {
251
- const lineHeight = getLineHeight(line);
252
- ensureSpace(ctx, lineHeight);
253
- let x = x0;
254
- for (const span of line) {
255
- ctx.page.drawText(span.text, {
256
- x,
257
- y: ctx.y - span.fontSize, // pdf-lib y is baseline
258
- size: span.fontSize,
259
- font: span.font,
260
- color: rgb(span.color.r, span.color.g, span.color.b),
261
- });
262
- const textWidth = span.font.widthOfTextAtSize(span.text, span.fontSize);
263
- // Underline for links
264
- if (span.link) {
265
- ctx.page.drawLine({
266
- start: { x, y: ctx.y - span.fontSize - 1 },
267
- end: { x: x + textWidth, y: ctx.y - span.fontSize - 1 },
268
- thickness: 0.5,
269
- color: rgb(span.color.r, span.color.g, span.color.b),
270
- });
271
- }
272
- // Strikethrough
273
- if (span.strikethrough) {
274
- const midY = ctx.y - span.fontSize * 0.6;
275
- ctx.page.drawLine({
276
- start: { x, y: midY },
277
- end: { x: x + textWidth, y: midY },
278
- thickness: 0.5,
279
- color: rgb(span.color.r, span.color.g, span.color.b),
280
- });
281
- }
282
- x += textWidth;
283
- }
284
- ctx.y -= lineHeight;
285
- }
286
- }
287
- /**
288
- * Break a flat list of spans into wrapped lines that fit within
289
- * `maxWidth`. Respects explicit \n characters.
290
- */
291
- function wrapSpans(spans, maxWidth) {
292
- const lines = [];
293
- let currentLine = [];
294
- let lineWidth = 0;
295
- for (const span of spans) {
296
- // Handle explicit newlines
297
- if (span.text === '\n') {
298
- lines.push(currentLine);
299
- currentLine = [];
300
- lineWidth = 0;
301
- continue;
302
- }
303
- // Split by whitespace for word-wrapping
304
- const words = splitIntoWords(span.text);
305
- for (const word of words) {
306
- const wordWidth = span.font.widthOfTextAtSize(word, span.fontSize);
307
- if (lineWidth + wordWidth > maxWidth && currentLine.length > 0 && word.trim().length > 0) {
308
- // Wrap to next line
309
- lines.push(currentLine);
310
- currentLine = [];
311
- lineWidth = 0;
312
- }
313
- // Trim leading space on new line
314
- const trimmedWord = currentLine.length === 0 ? word.replace(/^\s+/, '') : word;
315
- if (trimmedWord.length > 0) {
316
- const tw = span.font.widthOfTextAtSize(trimmedWord, span.fontSize);
317
- currentLine.push({ ...span, text: trimmedWord });
318
- lineWidth += tw;
319
- }
320
- }
321
- }
322
- if (currentLine.length > 0) {
323
- lines.push(currentLine);
324
- }
325
- return lines.length > 0 ? lines : [[]];
326
- }
327
- /**
328
- * Split text into "words" preserving whitespace as separate tokens
329
- * so the wrapping logic can break at whitespace.
330
- */
331
- function splitIntoWords(text) {
332
- const tokens = [];
333
- let current = '';
334
- let inSpace = false;
335
- for (const ch of text) {
336
- const isSpace = ch === ' ' || ch === '\t';
337
- if (isSpace !== inSpace && current.length > 0) {
338
- tokens.push(current);
339
- current = '';
340
- }
341
- current += ch;
342
- inSpace = isSpace;
343
- }
344
- if (current.length > 0)
345
- tokens.push(current);
346
- return tokens;
347
- }
348
- function getLineHeight(line) {
349
- let maxSize = 0;
350
- for (const span of line) {
351
- if (span.fontSize > maxSize)
352
- maxSize = span.fontSize;
353
- }
354
- return (maxSize || DEFAULT_FONT_SIZE) * LINE_HEIGHT_FACTOR;
355
- }
356
- // ============================================
357
- // Block Renderers
358
- // ============================================
359
- function renderBlocks(nodes, ctx, extraIndent) {
360
- for (const node of nodes) {
361
- renderBlock(node, ctx, extraIndent);
362
- }
363
- }
364
- function renderBlock(node, ctx, extraIndent) {
365
- switch (node.type) {
366
- case 'heading':
367
- renderHeading(node, ctx, extraIndent);
368
- break;
369
- case 'paragraph':
370
- renderParagraph(node, ctx, extraIndent);
371
- break;
372
- case 'blockquote':
373
- renderBlockquote(node, ctx, extraIndent);
374
- break;
375
- case 'list':
376
- renderList(node, ctx, extraIndent, 0);
377
- break;
378
- case 'code':
379
- renderCodeBlock(node, ctx, extraIndent);
380
- break;
381
- case 'table':
382
- renderTable(node, ctx, extraIndent);
383
- break;
384
- case 'thematicBreak':
385
- renderThematicBreak(ctx, extraIndent);
386
- break;
387
- case 'htmlBlock':
388
- renderHtmlBlock(node, ctx, extraIndent);
389
- break;
390
- case 'math':
391
- renderMathBlock(node, ctx, extraIndent);
392
- break;
393
- case 'footnoteDefinition':
394
- renderFootnoteDefinition(node, ctx, extraIndent);
395
- break;
396
- default:
397
- // containerDirective, leafDirective, definitionList, etc.
398
- // Render any children or value as best-effort
399
- renderFallbackBlock(node, ctx, extraIndent);
400
- break;
401
- }
402
- }
403
- // ---- Heading ----
404
- function renderHeading(node, ctx, extraIndent) {
405
- const origFontSize = ctx.fontSize;
406
- ctx.fontSize = HEADING_SIZES[node.depth] ?? DEFAULT_FONT_SIZE;
407
- ctx.y -= HEADING_SPACE_BEFORE;
408
- const x0 = ctx.margin + extraIndent;
409
- const w = ctx.contentWidth - extraIndent;
410
- const spans = flattenInlines(node.children, ctx, {
411
- bold: true,
412
- italic: false,
413
- code: false,
414
- color: ctx.colors.heading,
415
- });
416
- drawSpans(spans, ctx, w, x0);
417
- ctx.y -= HEADING_SPACE_AFTER;
418
- ctx.fontSize = origFontSize;
419
- }
420
- // ---- Paragraph ----
421
- function renderParagraph(node, ctx, extraIndent, colorOverride) {
422
- const x0 = ctx.margin + extraIndent;
423
- const w = ctx.contentWidth - extraIndent;
424
- const spans = flattenInlines(node.children, ctx, {
425
- bold: false,
426
- italic: false,
427
- code: false,
428
- color: colorOverride,
429
- });
430
- drawSpans(spans, ctx, w, x0);
431
- ctx.y -= PARAGRAPH_SPACING;
432
- }
433
- // ---- Blockquote ----
434
- function renderBlockquote(node, ctx, extraIndent) {
435
- const barX = ctx.margin + extraIndent + 4;
436
- const indent = extraIndent + BLOCKQUOTE_INDENT;
437
- const startY = ctx.y;
438
- for (const child of node.children) {
439
- if (child.type === 'paragraph') {
440
- renderParagraph(child, ctx, indent, COLOR_BLOCKQUOTE_TEXT);
441
- }
442
- else {
443
- renderBlock(child, ctx, indent);
444
- }
445
- }
446
- // Draw left bar from startY to ctx.y
447
- const endY = ctx.y + PARAGRAPH_SPACING; // undo last paragraph spacing
448
- if (startY > endY) {
449
- // Bar might span pages — draw on current page only
450
- ctx.page.drawRectangle({
451
- x: barX,
452
- y: endY,
453
- width: BLOCKQUOTE_BAR_WIDTH,
454
- height: startY - endY,
455
- color: rgb(COLOR_BLOCKQUOTE_BAR.r, COLOR_BLOCKQUOTE_BAR.g, COLOR_BLOCKQUOTE_BAR.b),
456
- });
457
- }
458
- ctx.y -= PARAGRAPH_SPACING;
459
- }
460
- // ---- List ----
461
- function renderList(node, ctx, extraIndent, depth) {
462
- const ordered = node.ordered ?? false;
463
- let counter = node.start ?? 1;
464
- for (const child of node.children) {
465
- if (child.type === 'listItem') {
466
- renderListItem(child, ctx, extraIndent, depth, ordered, counter);
467
- if (ordered)
468
- counter++;
469
- }
470
- }
471
- }
472
- function renderListItem(item, ctx, extraIndent, depth, ordered, counter) {
473
- const indent = extraIndent + depth * LIST_INDENT;
474
- const bullet = ordered ? `${counter}.` : BULLET_CHAR;
475
- const bulletFont = ctx.fonts.regular;
476
- const bulletWidth = bulletFont.widthOfTextAtSize(bullet + ' ', ctx.fontSize);
477
- // Draw bullet
478
- const lineHeight = ctx.fontSize * LINE_HEIGHT_FACTOR;
479
- ensureSpace(ctx, lineHeight);
480
- ctx.page.drawText(bullet, {
481
- x: ctx.margin + indent,
482
- y: ctx.y - ctx.fontSize,
483
- size: ctx.fontSize,
484
- font: bulletFont,
485
- color: rgb(ctx.colors.text.r, ctx.colors.text.g, ctx.colors.text.b),
486
- });
487
- const textIndent = indent + bulletWidth + 4;
488
- const textWidth = ctx.contentWidth - textIndent;
489
- // Render children at the text indent
490
- let isFirstChild = true;
491
- for (const child of item.children) {
492
- if (child.type === 'paragraph' && isFirstChild) {
493
- // First paragraph: render on same line as bullet
494
- const spans = flattenInlines(child.children, ctx, {
495
- bold: false,
496
- italic: false,
497
- code: false,
498
- });
499
- drawSpans(spans, ctx, textWidth, ctx.margin + textIndent);
500
- ctx.y -= PARAGRAPH_SPACING / 2;
501
- }
502
- else if (child.type === 'list') {
503
- renderList(child, ctx, extraIndent, depth + 1);
504
- }
505
- else {
506
- renderBlock(child, ctx, textIndent);
507
- }
508
- isFirstChild = false;
509
- }
510
- }
511
- // ---- Code Block ----
512
- function renderCodeBlock(node, ctx, extraIndent) {
513
- const x0 = ctx.margin + extraIndent;
514
- const _w = ctx.contentWidth - extraIndent;
515
- const lines = node.value.split('\n');
516
- const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
517
- const totalHeight = lines.length * lineH + 12; // 12 = vertical padding
518
- ensureSpace(ctx, Math.min(totalHeight, ctx.y - ctx.bottomY));
519
- const _bgTop = ctx.y;
520
- // Draw background first (we'll adjust after we know the final y)
521
- const _bgStartY = ctx.y;
522
- ctx.y -= 6; // top padding
523
- for (const line of lines) {
524
- ensureSpace(ctx, lineH);
525
- if (line.length > 0) {
526
- ctx.page.drawText(line, {
527
- x: x0 + 8,
528
- y: ctx.y - CODE_FONT_SIZE,
529
- size: CODE_FONT_SIZE,
530
- font: ctx.fonts.mono,
531
- color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b),
532
- });
533
- }
534
- ctx.y -= lineH;
535
- }
536
- ctx.y -= 6; // bottom padding
537
- // Draw background rectangle (behind text — pdf-lib draws on top,
538
- // so we could draw it first on a separate pass, but since we can't
539
- // easily re-order, we leave it as a visual approximation).
540
- // For a production implementation, you'd use a two-pass approach.
541
- // Here we draw the bg rect at the saved position. Text drawn after
542
- // will already be on the page. Since pdf-lib paints in order, we
543
- // accept that the bg goes on top — but only if bg is on same page.
544
- // A simpler approach: draw bg *before* text per page.
545
- // Actually, let's use a cleaner approach: draw line by line with bg
546
- // We already drew the text. For the background box, we'll just
547
- // not draw it behind (it would overlay). This is a known limitation
548
- // with pdf-lib's immediate mode. In practice the light grey bg makes
549
- // text hard to read if drawn on top. So we skip the bg for now and
550
- // rely on the monospace font to visually distinguish code.
551
- ctx.y -= PARAGRAPH_SPACING;
552
- }
553
- // ---- Table ----
554
- function renderTable(node, ctx, extraIndent) {
555
- if (node.children.length === 0)
556
- return;
557
- const x0 = ctx.margin + extraIndent;
558
- const tableWidth = ctx.contentWidth - extraIndent;
559
- // Measure columns: get max width per column across all rows
560
- const numCols = Math.max(...node.children.map((row) => row.children.length));
561
- if (numCols === 0)
562
- return;
563
- // Simple equal-width columns
564
- const colWidth = tableWidth / numCols;
565
- for (let rowIdx = 0; rowIdx < node.children.length; rowIdx++) {
566
- const row = node.children[rowIdx];
567
- const isHeader = rowIdx === 0 && row.children.some((c) => c.isHeader);
568
- // Calculate row height
569
- const rowCellHeights = row.children.map((cell) => {
570
- const cellNode = cell;
571
- const spans = flattenInlines(cellNode.children, ctx, {
572
- bold: isHeader,
573
- italic: false,
574
- code: false,
575
- });
576
- const lines = wrapSpans(spans, colWidth - 2 * TABLE_CELL_PAD_X);
577
- return lines.length * ctx.fontSize * LINE_HEIGHT_FACTOR + 2 * TABLE_CELL_PAD_Y;
578
- });
579
- const rowHeight = Math.max(...rowCellHeights, ctx.fontSize * LINE_HEIGHT_FACTOR + 2 * TABLE_CELL_PAD_Y);
580
- ensureSpace(ctx, rowHeight);
581
- // Draw header background
582
- if (isHeader) {
583
- ctx.page.drawRectangle({
584
- x: x0,
585
- y: ctx.y - rowHeight,
586
- width: tableWidth,
587
- height: rowHeight,
588
- color: rgb(COLOR_TABLE_HEADER_BG.r, COLOR_TABLE_HEADER_BG.g, COLOR_TABLE_HEADER_BG.b),
589
- });
590
- }
591
- // Draw cell borders and text
592
- for (let colIdx = 0; colIdx < numCols; colIdx++) {
593
- const cellX = x0 + colIdx * colWidth;
594
- // Draw cell border
595
- ctx.page.drawRectangle({
596
- x: cellX,
597
- y: ctx.y - rowHeight,
598
- width: colWidth,
599
- height: rowHeight,
600
- borderColor: rgb(COLOR_TABLE_BORDER.r, COLOR_TABLE_BORDER.g, COLOR_TABLE_BORDER.b),
601
- borderWidth: TABLE_BORDER_WIDTH,
602
- });
603
- if (colIdx < row.children.length) {
604
- const cellNode = row.children[colIdx];
605
- const spans = flattenInlines(cellNode.children, ctx, {
606
- bold: isHeader,
607
- italic: false,
608
- code: false,
609
- });
610
- // Draw text inside cell
611
- const savedY = ctx.y;
612
- ctx.y = ctx.y - TABLE_CELL_PAD_Y;
613
- const cellAvailableWidth = colWidth - 2 * TABLE_CELL_PAD_X;
614
- const wrappedLines = wrapSpans(spans, cellAvailableWidth);
615
- for (const line of wrappedLines) {
616
- let lx = cellX + TABLE_CELL_PAD_X;
617
- for (const span of line) {
618
- ctx.page.drawText(span.text, {
619
- x: lx,
620
- y: ctx.y - span.fontSize,
621
- size: span.fontSize,
622
- font: span.font,
623
- color: rgb(span.color.r, span.color.g, span.color.b),
624
- });
625
- lx += span.font.widthOfTextAtSize(span.text, span.fontSize);
626
- }
627
- ctx.y -= getLineHeight(line);
628
- }
629
- ctx.y = savedY; // restore for next cell
630
- }
631
- }
632
- ctx.y -= rowHeight;
633
- }
634
- ctx.y -= PARAGRAPH_SPACING;
635
- }
636
- // ---- Thematic Break ----
637
- function renderThematicBreak(ctx, extraIndent) {
638
- ctx.y -= PARAGRAPH_SPACING;
639
- ensureSpace(ctx, 10);
640
- const x0 = ctx.margin + extraIndent;
641
- const x1 = ctx.margin + ctx.contentWidth;
642
- ctx.page.drawLine({
643
- start: { x: x0, y: ctx.y },
644
- end: { x: x1, y: ctx.y },
645
- thickness: 1,
646
- color: rgb(COLOR_THEMATIC_BREAK.r, COLOR_THEMATIC_BREAK.g, COLOR_THEMATIC_BREAK.b),
647
- });
648
- ctx.y -= PARAGRAPH_SPACING;
649
- }
650
- // ---- HTML Block ----
651
- function renderHtmlBlock(node, ctx, extraIndent) {
652
- if (!node.rawHtml)
653
- return;
654
- // Render raw HTML as monospace text (best effort)
655
- const lines = node.rawHtml.split('\n');
656
- const x0 = ctx.margin + extraIndent;
657
- for (const line of lines) {
658
- if (line.trim().length === 0)
659
- continue;
660
- const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
661
- ensureSpace(ctx, lineH);
662
- ctx.page.drawText(line, {
663
- x: x0,
664
- y: ctx.y - CODE_FONT_SIZE,
665
- size: CODE_FONT_SIZE,
666
- font: ctx.fonts.mono,
667
- color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b),
668
- });
669
- ctx.y -= lineH;
670
- }
671
- ctx.y -= PARAGRAPH_SPACING;
672
- }
673
- // ---- Math Block ----
674
- function renderMathBlock(node, ctx, extraIndent) {
675
- // Render LaTeX source in monospace as fallback
676
- const lines = node.value.split('\n');
677
- const x0 = ctx.margin + extraIndent;
678
- for (const line of lines) {
679
- const lineH = CODE_FONT_SIZE * LINE_HEIGHT_FACTOR;
680
- ensureSpace(ctx, lineH);
681
- if (line.length > 0) {
682
- ctx.page.drawText(line, {
683
- x: x0,
684
- y: ctx.y - CODE_FONT_SIZE,
685
- size: CODE_FONT_SIZE,
686
- font: ctx.fonts.mono,
687
- color: rgb(COLOR_CODE_TEXT.r, COLOR_CODE_TEXT.g, COLOR_CODE_TEXT.b),
688
- });
689
- }
690
- ctx.y -= lineH;
691
- }
692
- ctx.y -= PARAGRAPH_SPACING;
693
- }
694
- // ---- Footnote Definition ----
695
- function renderFootnoteDefinition(node, ctx, extraIndent) {
696
- // Draw footnote identifier
697
- const label = `[${node.identifier}]`;
698
- const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
699
- ensureSpace(ctx, lineH);
700
- ctx.page.drawText(label, {
701
- x: ctx.margin + extraIndent,
702
- y: ctx.y - ctx.fontSize * 0.75,
703
- size: ctx.fontSize * 0.75,
704
- font: ctx.fonts.bold,
705
- color: rgb(ctx.colors.link.r, ctx.colors.link.g, ctx.colors.link.b),
706
- });
707
- // Render children indented
708
- renderBlocks(node.children, ctx, extraIndent + LIST_INDENT);
709
- }
710
- // ---- Fallback ----
711
- function renderFallbackBlock(node, ctx, extraIndent) {
712
- const fallback = node;
713
- if (fallback.children && Array.isArray(fallback.children)) {
714
- // Could be block children or inline children
715
- if (fallback.children.length > 0 && typeof fallback.children[0]?.type === 'string') {
716
- const firstType = fallback.children[0].type;
717
- // Heuristic: if first child looks like an inline node, wrap as paragraph
718
- const inlineTypes = new Set([
719
- 'text',
720
- 'strong',
721
- 'emphasis',
722
- 'delete',
723
- 'inlineCode',
724
- 'link',
725
- 'image',
726
- 'break',
727
- ]);
728
- if (inlineTypes.has(firstType)) {
729
- const spans = flattenInlines(fallback.children, ctx, {
730
- bold: false,
731
- italic: false,
732
- code: false,
733
- });
734
- drawSpans(spans, ctx, ctx.contentWidth - extraIndent, ctx.margin + extraIndent);
735
- ctx.y -= PARAGRAPH_SPACING;
736
- return;
737
- }
738
- }
739
- renderBlocks(fallback.children, ctx, extraIndent);
740
- }
741
- else if (fallback.value && typeof fallback.value === 'string') {
742
- const lineH = ctx.fontSize * LINE_HEIGHT_FACTOR;
743
- ensureSpace(ctx, lineH);
744
- ctx.page.drawText(fallback.value, {
745
- x: ctx.margin + extraIndent,
746
- y: ctx.y - ctx.fontSize,
747
- size: ctx.fontSize,
748
- font: ctx.fonts.regular,
749
- color: rgb(ctx.colors.text.r, ctx.colors.text.g, ctx.colors.text.b),
750
- });
751
- ctx.y -= lineH + PARAGRAPH_SPACING;
752
- }
753
- }
754
- // ============================================
755
- // Color Helpers
756
- // ============================================
757
- function hexToRgb(hex) {
758
- const h = hex.startsWith('#') ? hex.slice(1) : hex;
759
- if (h.length !== 6)
760
- return undefined;
761
- const r = parseInt(h.slice(0, 2), 16);
762
- const g = parseInt(h.slice(2, 4), 16);
763
- const b = parseInt(h.slice(4, 6), 16);
764
- if (isNaN(r) || isNaN(g) || isNaN(b))
765
- return undefined;
766
- return { r: r / 255, g: g / 255, b: b / 255 };
767
- }
768
- //# sourceMappingURL=export.js.map