@polotno/pdf-export 0.1.38 → 0.1.40

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 (56) hide show
  1. package/README.md +61 -8
  2. package/lib/index.d.ts +66 -8
  3. package/lib/index.js +25 -145
  4. package/package.json +17 -18
  5. package/lib/compare-render.d.ts +0 -1
  6. package/lib/compare-render.js +0 -185
  7. package/lib/figure.d.ts +0 -10
  8. package/lib/figure.js +0 -54
  9. package/lib/filters.d.ts +0 -2
  10. package/lib/filters.js +0 -163
  11. package/lib/ghostscript.d.ts +0 -21
  12. package/lib/ghostscript.js +0 -132
  13. package/lib/group.d.ts +0 -5
  14. package/lib/group.js +0 -5
  15. package/lib/image.d.ts +0 -38
  16. package/lib/image.js +0 -279
  17. package/lib/line.d.ts +0 -10
  18. package/lib/line.js +0 -66
  19. package/lib/pdf-import/coordinate-transform.d.ts +0 -51
  20. package/lib/pdf-import/coordinate-transform.js +0 -99
  21. package/lib/pdf-import/element-builder.d.ts +0 -21
  22. package/lib/pdf-import/element-builder.js +0 -163
  23. package/lib/pdf-import/font-mapper.d.ts +0 -17
  24. package/lib/pdf-import/font-mapper.js +0 -142
  25. package/lib/pdf-import/index.d.ts +0 -35
  26. package/lib/pdf-import/index.js +0 -105
  27. package/lib/pdf-import/parser.d.ts +0 -29
  28. package/lib/pdf-import/parser.js +0 -285
  29. package/lib/pdf-import/text-analysis.d.ts +0 -17
  30. package/lib/pdf-import/text-analysis.js +0 -186
  31. package/lib/pdf-import/types.d.ts +0 -101
  32. package/lib/pdf-import/types.js +0 -1
  33. package/lib/scripts/compare-json.d.ts +0 -1
  34. package/lib/scripts/compare-json.js +0 -141
  35. package/lib/spot-colors.d.ts +0 -38
  36. package/lib/spot-colors.js +0 -141
  37. package/lib/svg-render.d.ts +0 -9
  38. package/lib/svg-render.js +0 -63
  39. package/lib/svg.d.ts +0 -12
  40. package/lib/svg.js +0 -224
  41. package/lib/text/fonts.d.ts +0 -16
  42. package/lib/text/fonts.js +0 -113
  43. package/lib/text/index.d.ts +0 -8
  44. package/lib/text/index.js +0 -42
  45. package/lib/text/layout.d.ts +0 -22
  46. package/lib/text/layout.js +0 -522
  47. package/lib/text/parser.d.ts +0 -46
  48. package/lib/text/parser.js +0 -415
  49. package/lib/text/render.d.ts +0 -8
  50. package/lib/text/render.js +0 -237
  51. package/lib/text/types.d.ts +0 -91
  52. package/lib/text/types.js +0 -1
  53. package/lib/text.d.ts +0 -39
  54. package/lib/text.js +0 -576
  55. package/lib/utils.d.ts +0 -16
  56. package/lib/utils.js +0 -124
@@ -1,522 +0,0 @@
1
- import { stripHtml } from 'string-strip-html';
2
- import { tokenizeHTML, parseHtmlToParagraphs, tokensToHTML, decodeHtmlEntities, parseHTMLToSegments, } from './parser.js';
3
- import { loadFontForSegment } from './fonts.js';
4
- /**
5
- * Expand tabs to spaces based on character positions (every 8 characters by default).
6
- * This is a lightweight character-based approximation used for line-breaking calculations.
7
- */
8
- function expandTabsToTabStops(text, tabSize = 8, startPosition = 0) {
9
- if (!text) {
10
- return text;
11
- }
12
- let result = '';
13
- let position = startPosition; // Current character position
14
- for (let i = 0; i < text.length; i++) {
15
- const char = text[i];
16
- if (char === '\t') {
17
- // Calculate how many spaces needed to reach next tab stop
18
- const spacesNeeded = tabSize - (position % tabSize);
19
- result += ' '.repeat(spacesNeeded);
20
- position += spacesNeeded;
21
- }
22
- else if (char === '\n') {
23
- // Reset position on newline (tab stops reset at line start)
24
- result += char;
25
- position = 0;
26
- }
27
- else {
28
- result += char;
29
- position++;
30
- }
31
- }
32
- return result;
33
- }
34
- /**
35
- * Expand tabs in text with word spacing adjustment for accurate visual alignment.
36
- */
37
- export function expandTabsWithWordSpacing(text, doc, textOptions, tabSizeInSpaces = 8, currentWidth = 0) {
38
- if (!text || !text.includes('\t')) {
39
- // No tabs, return as-is
40
- const width = currentWidth + doc.widthOfString(text, textOptions);
41
- return {
42
- segments: [{ type: 'text', text, wordSpacing: 0 }],
43
- finalWidth: width,
44
- };
45
- }
46
- // Measure the width of one space character (for rendering tab spaces)
47
- const spaceWidth = doc.widthOfString(' ', textOptions);
48
- const tabStopWidth = Math.max(spaceWidth * tabSizeInSpaces, spaceWidth || 1);
49
- const segments = [];
50
- let width = currentWidth;
51
- let currentSegment = '';
52
- for (let i = 0; i < text.length; i++) {
53
- const char = text[i];
54
- if (char === '\t') {
55
- // Flush current segment if any
56
- if (currentSegment) {
57
- const segmentWidth = doc.widthOfString(currentSegment, textOptions);
58
- segments.push({ type: 'text', text: currentSegment, wordSpacing: 0 });
59
- width += segmentWidth;
60
- currentSegment = '';
61
- }
62
- // Calculate the exact distance to next tab stop
63
- const currentTabPosition = width % tabStopWidth;
64
- const targetWidth = tabStopWidth - currentTabPosition;
65
- const widthBeforeTab = width;
66
- // Emit a tab instruction so the renderer can manually reposition the cursor.
67
- segments.push({
68
- type: 'tab',
69
- advanceWidth: widthBeforeTab + targetWidth,
70
- });
71
- width += targetWidth;
72
- }
73
- else if (char === '\n') {
74
- // Flush current segment and add newline
75
- if (currentSegment) {
76
- const segmentWidth = doc.widthOfString(currentSegment, textOptions);
77
- segments.push({ type: 'text', text: currentSegment, wordSpacing: 0 });
78
- width += segmentWidth;
79
- currentSegment = '';
80
- }
81
- segments.push({ type: 'text', text: '\n', wordSpacing: 0 });
82
- width = 0; // Reset width on newline
83
- }
84
- else {
85
- currentSegment += char;
86
- }
87
- }
88
- // Flush remaining segment
89
- if (currentSegment) {
90
- const segmentWidth = doc.widthOfString(currentSegment, textOptions);
91
- segments.push({ type: 'text', text: currentSegment, wordSpacing: 0 });
92
- width += segmentWidth;
93
- }
94
- return { segments, finalWidth: width };
95
- }
96
- export async function buildRenderSegmentsForLine(doc, element, lineText, textOptions, fonts) {
97
- const parsedSegments = parseHTMLToSegments(lineText, element);
98
- let currentLineWidth = 0;
99
- const renderSegments = [];
100
- for (const segment of parsedSegments) {
101
- const fontKey = await loadFontForSegment(doc, segment, element, fonts);
102
- doc.font(fontKey);
103
- doc.fontSize(element.fontSize);
104
- if (segment.text.includes('\t')) {
105
- const expanded = expandTabsWithWordSpacing(segment.text, doc, textOptions, 8, currentLineWidth);
106
- currentLineWidth = expanded.finalWidth;
107
- for (const tabSegment of expanded.segments) {
108
- if (tabSegment.type === 'tab') {
109
- renderSegments.push({
110
- segment,
111
- fontKey,
112
- type: 'tab',
113
- advanceWidth: tabSegment.advanceWidth,
114
- });
115
- }
116
- else {
117
- renderSegments.push({
118
- segment,
119
- fontKey,
120
- type: 'text',
121
- text: tabSegment.text,
122
- wordSpacing: tabSegment.wordSpacing,
123
- });
124
- }
125
- }
126
- }
127
- else {
128
- const segmentWidth = doc.widthOfString(segment.text, textOptions);
129
- currentLineWidth += segmentWidth;
130
- renderSegments.push({
131
- segment,
132
- fontKey,
133
- type: 'text',
134
- text: segment.text,
135
- wordSpacing: 0,
136
- });
137
- }
138
- }
139
- return renderSegments;
140
- }
141
- function cloneListMetaForLine(meta, showMarker) {
142
- if (!meta) {
143
- return undefined;
144
- }
145
- return {
146
- ...meta,
147
- showMarker,
148
- };
149
- }
150
- function createListLineMeta(doc, element, props, paragraphMeta) {
151
- const indentPx = paragraphMeta.indentLevel * element.fontSize * 0.5;
152
- const markerText = paragraphMeta.type === 'ul' ? '•' : `${paragraphMeta.index.toString()}.`;
153
- const previousFontSize = doc._fontSize !== undefined
154
- ? doc._fontSize
155
- : element.fontSize;
156
- const markerFontSize = paragraphMeta.type === 'ul' ? element.fontSize * 1.2 : element.fontSize;
157
- doc.fontSize(markerFontSize);
158
- const markerLabelWidth = doc.widthOfString(markerText, {
159
- ...props,
160
- width: undefined,
161
- });
162
- doc.fontSize(previousFontSize);
163
- const markerGapPx = element.fontSize * (paragraphMeta.type === 'ul' ? 1.5 : 0.8);
164
- const markerBoxMinPx = element.fontSize * (paragraphMeta.type === 'ul' ? 2.5 : 2.8);
165
- const markerBoxWidth = Math.max(markerLabelWidth + markerGapPx, markerBoxMinPx);
166
- const textStartPx = indentPx + markerBoxWidth;
167
- return {
168
- type: paragraphMeta.type,
169
- markerText,
170
- indentPx,
171
- markerBoxWidth,
172
- markerFontSize,
173
- markerAlignment: paragraphMeta.type === 'ul' ? 'center' : 'right',
174
- showMarker: paragraphMeta.displayMarker,
175
- textStartPx,
176
- };
177
- }
178
- function encodeHtmlEntities(text) {
179
- return text
180
- .replace(/&/g, '&amp;')
181
- .replace(/</g, '&lt;')
182
- .replace(/>/g, '&gt;')
183
- .replace(/"/g, '&quot;')
184
- .replace(/'/g, '&apos;');
185
- }
186
- export function splitTextIntoLines(doc, element, props) {
187
- const lines = [];
188
- const rawText = typeof element.text === 'string'
189
- ? element.text
190
- : String(element.text ?? '');
191
- const paragraphs = parseHtmlToParagraphs(rawText);
192
- const lineHeightPx = element.lineHeight * element.fontSize;
193
- // Calculate max allowed lines. Ensure at least 1 line is allowed.
194
- // Use a very large number if height check is disabled or infinite
195
- // If element.height is very large (like in 8.json which is a page), we should treat it as effectively infinite for text
196
- // But wait, the element in 8.json has specific height:
197
- // "height": 60 for title, 40 for subtitle, 40 for chapter title, 600 for content text.
198
- // The content text has height 600 and font size 14. Line height 1.5 -> 21px per line.
199
- // 600 / 21 = ~28 lines.
200
- // So maxLines is finite.
201
- const maxLines = element.height > 0
202
- ? Math.max(1, Math.round(element.height / lineHeightPx))
203
- : Infinity;
204
- if (paragraphs.length === 0) {
205
- paragraphs.push({ html: '' });
206
- }
207
- for (const paragraph of paragraphs) {
208
- // Tokenize the paragraph
209
- const tokens = tokenizeHTML(paragraph.html);
210
- // Extract plain text for width calculation
211
- // Expand tabs to tab stops for accurate width measurement
212
- const plainText = expandTabsToTabStops(tokens
213
- .filter((t) => t.type === 'text')
214
- .map((t) => t.decodedContent ?? decodeHtmlEntities(t.content))
215
- .join(''), 8);
216
- const baseMeta = paragraph.listMeta
217
- ? createListLineMeta(doc, element, props, paragraph.listMeta)
218
- : undefined;
219
- const availableWidthRaw = element.width - (baseMeta ? baseMeta.textStartPx : 0);
220
- const availableWidth = element.align === 'justify'
221
- ? Math.max(availableWidthRaw, 1)
222
- : Math.max(availableWidthRaw, element.width * 0.1, 1);
223
- const paragraphWidth = doc.widthOfString(plainText, props);
224
- let showMarkerForLine = baseMeta?.showMarker ?? false;
225
- // Justify alignment using native pdfkit instruments
226
- if (paragraphWidth <= availableWidth || element.align === 'justify') {
227
- // Paragraph fits on one line
228
- const listMeta = cloneListMetaForLine(baseMeta, showMarkerForLine);
229
- lines.push({
230
- text: paragraph.html,
231
- width: paragraphWidth,
232
- fullWidth: paragraphWidth + (listMeta ? listMeta.textStartPx : 0),
233
- listMeta,
234
- });
235
- }
236
- else {
237
- // Need to split paragraph into multiple lines
238
- let currentLineDecoded = '';
239
- let currentWidth = 0;
240
- let currentTokens = [];
241
- let openTags = [];
242
- for (const token of tokens) {
243
- if (token.type === 'tag') {
244
- currentTokens.push(token);
245
- continue;
246
- }
247
- // Text token - split by words
248
- // Don't expand tabs here - we need to preserve tabs for proper alignment
249
- const rawWords = token.content.split(' ');
250
- const decodedText = token.decodedContent ?? decodeHtmlEntities(token.content);
251
- const decodedWords = decodedText.split(' ');
252
- for (let i = 0; i < rawWords.length; i++) {
253
- const rawWord = rawWords[i];
254
- const decodedWord = decodedWords[i] ?? decodeHtmlEntities(rawWord);
255
- const separator = i > 0 ? ' ' : '';
256
- const hasCurrentLine = currentLineDecoded.length > 0;
257
- const testLineDecoded = hasCurrentLine
258
- ? `${currentLineDecoded}${separator}${decodedWord}`
259
- : decodedWord;
260
- // Expand tabs in test line for accurate width measurement
261
- // Tabs are expanded based on the full line position, maintaining tab stop alignment
262
- const testLineExpanded = expandTabsToTabStops(testLineDecoded, 8);
263
- const testWidth = doc.widthOfString(testLineExpanded, props);
264
- if (testWidth <= availableWidth) {
265
- currentLineDecoded = testLineDecoded;
266
- currentWidth = testWidth;
267
- // Add text token (with space if not first word in token)
268
- const rawContent = separator.length > 0 ? `${separator}${rawWord}` : rawWord;
269
- const decodedContent = separator.length > 0 ? `${separator}${decodedWord}` : decodedWord;
270
- currentTokens.push({
271
- type: 'text',
272
- content: rawContent,
273
- decodedContent,
274
- });
275
- }
276
- else {
277
- // Line is too long, save current line and start new one
278
- // Check if we can add a new line (wrapping).
279
- // If we wrap, we push the current line and start a new one.
280
- // So we will have lines.length + 1 lines + the new one we are starting.
281
- // We can wrap only if lines.length + 1 <= maxLines.
282
- if (lines.length < maxLines - 1) {
283
- // Case 1: Line has content, so we wrap to next line
284
- if (currentLineDecoded.length > 0) {
285
- const result = tokensToHTML(currentTokens, openTags);
286
- const listMeta = cloneListMetaForLine(baseMeta, showMarkerForLine);
287
- lines.push({
288
- text: result.html,
289
- width: currentWidth,
290
- fullWidth: currentWidth + (listMeta ? listMeta.textStartPx : 0),
291
- listMeta,
292
- });
293
- openTags = result.openTags;
294
- currentTokens = [];
295
- showMarkerForLine = false;
296
- // Handle the current word on the new line
297
- currentLineDecoded = decodedWord;
298
- // Expand tabs for accurate width measurement
299
- const decodedWordExpanded = expandTabsToTabStops(decodedWord, 8);
300
- currentWidth = doc.widthOfString(decodedWordExpanded, props);
301
- currentTokens.push({
302
- type: 'text',
303
- content: rawWord,
304
- decodedContent: decodedWord,
305
- });
306
- }
307
- else {
308
- // Case 2: Line is empty (start of line), but word is too big for line
309
- // Since we can wrap (have height), but the word itself is wider than line width,
310
- // we must split the word across lines.
311
- // The original logic for splitting word was inside "else if (currentLineDecoded.length === 0)" block
312
- // which is technically this else block if lines.length check was outside.
313
- const word = decodedWord;
314
- // logic for splitting word...
315
- let processedWord = '';
316
- let processedWidth = 0;
317
- for (const char of word) {
318
- const testChunk = processedWord + char;
319
- const testChunkExpanded = expandTabsToTabStops(testChunk, 8);
320
- const testChunkWidth = doc.widthOfString(testChunkExpanded, props);
321
- if (testChunkWidth > availableWidth) {
322
- if (processedWord.length > 0 &&
323
- lines.length < maxLines - 1) {
324
- const chunkContent = encodeHtmlEntities(processedWord);
325
- currentTokens.push({
326
- type: 'text',
327
- content: chunkContent,
328
- decodedContent: processedWord,
329
- });
330
- const result = tokensToHTML(currentTokens, openTags);
331
- const listMeta = cloneListMetaForLine(baseMeta, showMarkerForLine);
332
- lines.push({
333
- text: result.html,
334
- width: processedWidth,
335
- fullWidth: processedWidth +
336
- (listMeta ? listMeta.textStartPx : 0),
337
- listMeta,
338
- });
339
- openTags = result.openTags;
340
- currentTokens = [];
341
- showMarkerForLine = false;
342
- processedWord = char;
343
- processedWidth = doc.widthOfString(expandTabsToTabStops(char, 8), props);
344
- }
345
- else {
346
- processedWord += char;
347
- processedWidth = testChunkWidth;
348
- }
349
- }
350
- else {
351
- processedWord += char;
352
- processedWidth = testChunkWidth;
353
- }
354
- }
355
- currentLineDecoded = processedWord;
356
- currentWidth = processedWidth;
357
- const chunkContent = encodeHtmlEntities(processedWord);
358
- currentTokens.push({
359
- type: 'text',
360
- content: chunkContent,
361
- decodedContent: processedWord,
362
- });
363
- }
364
- }
365
- else {
366
- // Cannot wrap anymore (max lines reached).
367
- // Append to current line regardless of width.
368
- if (currentLineDecoded.length > 0) {
369
- currentLineDecoded = testLineDecoded;
370
- currentWidth = testWidth;
371
- const rawContent = separator.length > 0 ? `${separator}${rawWord}` : rawWord;
372
- const decodedContent = separator.length > 0
373
- ? `${separator}${decodedWord}`
374
- : decodedWord;
375
- currentTokens.push({
376
- type: 'text',
377
- content: rawContent,
378
- decodedContent: decodedContent,
379
- });
380
- }
381
- else {
382
- // Edge case: Start of line, can't wrap, but word is too big.
383
- // Since we can't wrap, we just force it in.
384
- currentLineDecoded = decodedWord;
385
- const decodedWordExpanded = expandTabsToTabStops(decodedWord, 8);
386
- currentWidth = doc.widthOfString(decodedWordExpanded, props);
387
- currentTokens.push({
388
- type: 'text',
389
- content: rawWord,
390
- decodedContent: decodedWord,
391
- });
392
- }
393
- }
394
- }
395
- }
396
- }
397
- // Add the last line
398
- if (currentLineDecoded.length > 0) {
399
- const result = tokensToHTML(currentTokens, openTags);
400
- const listMeta = cloneListMetaForLine(baseMeta, showMarkerForLine);
401
- lines.push({
402
- text: result.html,
403
- width: currentWidth,
404
- fullWidth: currentWidth + (listMeta ? listMeta.textStartPx : 0),
405
- listMeta,
406
- });
407
- }
408
- else if (currentTokens.length === 0) {
409
- // Handle case when paragraph becomes empty after wrapping logic
410
- const listMeta = cloneListMetaForLine(baseMeta, showMarkerForLine);
411
- lines.push({
412
- text: '',
413
- width: 0,
414
- fullWidth: listMeta ? listMeta.textStartPx : 0,
415
- listMeta,
416
- });
417
- }
418
- }
419
- }
420
- return lines;
421
- }
422
- export function calculateTextMetrics(doc, element) {
423
- const textOptions = {
424
- align: element.align === 'justify' ? 'justify' : 'left',
425
- baseline: 'alphabetic',
426
- lineGap: 1,
427
- width: element.width,
428
- underline: element.textDecoration.indexOf('underline') >= 0,
429
- characterSpacing: element.letterSpacing
430
- ? element.letterSpacing * element.fontSize
431
- : 0,
432
- lineBreak: false,
433
- stroke: false,
434
- fill: false,
435
- };
436
- const currentLineHeight = doc.heightOfString('A', textOptions);
437
- const lineHeight = element.lineHeight * element.fontSize;
438
- const fontBoundingBoxAscent = (doc._font.ascender / 1000) * element.fontSize;
439
- const fontBoundingBoxDescent = (doc._font.descender / 1000) * element.fontSize;
440
- // Calculate baseline offset based on font metrics (similar to Konva rendering)
441
- const baselineOffset = (fontBoundingBoxAscent - Math.abs(fontBoundingBoxDescent)) / 2 +
442
- lineHeight / 2;
443
- // Adjust line gap to match desired line height
444
- const lineHeightDiff = currentLineHeight - lineHeight;
445
- textOptions.lineGap = textOptions.lineGap - lineHeightDiff;
446
- const textLines = splitTextIntoLines(doc, element, textOptions);
447
- return {
448
- textOptions,
449
- lineHeightPx: lineHeight,
450
- baselineOffset,
451
- textLines,
452
- };
453
- }
454
- export function calculateVerticalAlignment(doc, element, textOptions) {
455
- if (!element.verticalAlign || element.verticalAlign === 'top') {
456
- return 0;
457
- }
458
- const strippedContent = stripHtml(element.text).result;
459
- const textHeight = doc.heightOfString(strippedContent, textOptions);
460
- if (element.verticalAlign === 'middle') {
461
- return (element.height - textHeight) / 2;
462
- }
463
- else if (element.verticalAlign === 'bottom') {
464
- return element.height - textHeight;
465
- }
466
- return 0;
467
- }
468
- export function fitTextToHeight(doc, element, textOptions) {
469
- const strippedContent = stripHtml(element.text).result;
470
- for (let size = element.fontSize; size > 0; size -= 1) {
471
- doc.fontSize(size);
472
- const height = doc.heightOfString(strippedContent, textOptions);
473
- if (height <= element.height) {
474
- break;
475
- }
476
- }
477
- }
478
- /**
479
- * Calculate X offset for list markers (not used for text content positioning)
480
- */
481
- export function calculateLineXOffset(element, line) {
482
- // Markers are always at the left edge, regardless of text alignment
483
- if (line.listMeta) {
484
- return 0;
485
- }
486
- // For non-list lines, markers follow text alignment
487
- const align = element.align;
488
- const targetWidth = line.width;
489
- if (align === 'right') {
490
- return element.width - targetWidth;
491
- }
492
- else if (align === 'center') {
493
- return (element.width - targetWidth) / 2;
494
- }
495
- // left or justify: markers at position 0
496
- return 0;
497
- }
498
- export function calculateTextContentXOffset(element, line) {
499
- const align = element.align;
500
- const textWidth = line.width;
501
- const baseStart = line.listMeta?.textStartPx ?? 0;
502
- const availableWidth = Math.max(element.width - baseStart, 0);
503
- if (align === 'right') {
504
- return baseStart + Math.max(availableWidth - textWidth, 0);
505
- }
506
- else if (align === 'center') {
507
- return baseStart + Math.max((availableWidth - textWidth) / 2, 0);
508
- }
509
- return baseStart;
510
- }
511
- /**
512
- * Calculate effective width for text rendering, considering justify and underline constraints
513
- */
514
- export function calculateEffectiveWidth(element, line, widthOption, hasUnderline) {
515
- if (widthOption !== undefined) {
516
- return widthOption;
517
- }
518
- if (hasUnderline) {
519
- return element.width;
520
- }
521
- return undefined;
522
- }
@@ -1,46 +0,0 @@
1
- import { TextElement, TextSegment, ListParagraphMeta } from './types.js';
2
- export declare function decodeHtmlEntities(text: string): string;
3
- /**
4
- * Normalize rich text HTML by converting block-level line breaks into newline characters
5
- * while preserving inline formatting tags.
6
- */
7
- export declare function normalizeRichText(text: string): string;
8
- /**
9
- * Parse HTML text into styled segments
10
- */
11
- export declare function parseHTMLToSegments(html: string, baseElement: TextElement): TextSegment[];
12
- /**
13
- * Represents a parsed HTML token (either text content or a tag)
14
- */
15
- export interface Token {
16
- type: 'text' | 'tag';
17
- content: string;
18
- decodedContent?: string;
19
- tagName?: string;
20
- isClosing?: boolean;
21
- }
22
- /**
23
- * Parse HTML into tokens (text and tags)
24
- */
25
- export declare function tokenizeHTML(html: string): Token[];
26
- export interface ParagraphDefinition {
27
- html: string;
28
- listMeta?: ListParagraphMeta;
29
- }
30
- export declare function parseHtmlToParagraphs(html: string): ParagraphDefinition[];
31
- /**
32
- * Reconstruct HTML from tokens while maintaining proper tag nesting across line breaks
33
- * @param tokens - Array of parsed HTML tokens
34
- * @param openTags - Tags that were opened in previous lines and should be carried forward
35
- * @returns Reconstructed HTML string and the updated list of open tags
36
- */
37
- export declare function tokensToHTML(tokens: Token[], openTags: Array<{
38
- name: string;
39
- fullTag: string;
40
- }>): {
41
- html: string;
42
- openTags: Array<{
43
- name: string;
44
- fullTag: string;
45
- }>;
46
- };