@polotno/pdf-export 0.1.37 → 0.1.39

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 (52) 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/browser-entry.d.ts +0 -7
  6. package/lib/browser-entry.js +0 -11
  7. package/lib/compare-render.d.ts +0 -1
  8. package/lib/compare-render.js +0 -185
  9. package/lib/core/index.d.ts +0 -26
  10. package/lib/core/index.js +0 -87
  11. package/lib/figure.d.ts +0 -10
  12. package/lib/figure.js +0 -54
  13. package/lib/filters.d.ts +0 -2
  14. package/lib/filters.js +0 -163
  15. package/lib/ghostscript.d.ts +0 -21
  16. package/lib/ghostscript.js +0 -132
  17. package/lib/group.d.ts +0 -5
  18. package/lib/group.js +0 -5
  19. package/lib/image.d.ts +0 -38
  20. package/lib/image.js +0 -279
  21. package/lib/line.d.ts +0 -10
  22. package/lib/line.js +0 -66
  23. package/lib/platform/adapter.d.ts +0 -37
  24. package/lib/platform/adapter.js +0 -13
  25. package/lib/platform/browser-polyfill.d.ts +0 -1
  26. package/lib/platform/browser-polyfill.js +0 -5
  27. package/lib/platform/browser.d.ts +0 -7
  28. package/lib/platform/browser.js +0 -145
  29. package/lib/platform/node.d.ts +0 -7
  30. package/lib/platform/node.js +0 -142
  31. package/lib/spot-colors.d.ts +0 -38
  32. package/lib/spot-colors.js +0 -141
  33. package/lib/svg-render.d.ts +0 -9
  34. package/lib/svg-render.js +0 -63
  35. package/lib/svg.d.ts +0 -12
  36. package/lib/svg.js +0 -224
  37. package/lib/text/fonts.d.ts +0 -16
  38. package/lib/text/fonts.js +0 -82
  39. package/lib/text/index.d.ts +0 -8
  40. package/lib/text/index.js +0 -42
  41. package/lib/text/layout.d.ts +0 -22
  42. package/lib/text/layout.js +0 -522
  43. package/lib/text/parser.d.ts +0 -46
  44. package/lib/text/parser.js +0 -415
  45. package/lib/text/render.d.ts +0 -8
  46. package/lib/text/render.js +0 -237
  47. package/lib/text/types.d.ts +0 -91
  48. package/lib/text/types.js +0 -1
  49. package/lib/text.d.ts +0 -49
  50. package/lib/text.js +0 -1277
  51. package/lib/utils.d.ts +0 -16
  52. package/lib/utils.js +0 -124
@@ -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
- };
@@ -1,415 +0,0 @@
1
- import { decode as decodeEntities } from 'html-entities';
2
- export function decodeHtmlEntities(text) {
3
- if (!text) {
4
- return text;
5
- }
6
- const decoded = decodeEntities(text);
7
- // Don't replace tabs here - we'll handle them with expandTabsToTabStops
8
- return decoded;
9
- }
10
- /**
11
- * Normalize rich text HTML by converting block-level line breaks into newline characters
12
- * while preserving inline formatting tags.
13
- */
14
- export function normalizeRichText(text) {
15
- if (!text) {
16
- return text;
17
- }
18
- let normalized = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
19
- // Convert explicit HTML break tags into newline characters
20
- normalized = normalized.replace(/<br\s*\/?>/gi, '\n');
21
- // Treat paragraph boundaries as newlines and drop opening tags
22
- normalized = normalized.replace(/<\/p\s*>/gi, '\n');
23
- normalized = normalized.replace(/<p[^>]*>/gi, '');
24
- // Collapse excessive consecutive newlines produced by HTML cleanup
25
- normalized = normalized.replace(/\n{3,}/g, '\n\n');
26
- // Trim stray leading/trailing newlines introduced by paragraph conversion
27
- normalized = normalized.replace(/^\n+/, '').replace(/\n+$/, '');
28
- // Decode common HTML non-breaking space entities into their unicode counterpart
29
- normalized = normalized.replace(/&(nbsp|#160|#xA0);/gi, '\u00A0');
30
- // Strip zero-width characters that can create missing-glyph boxes in PDF output
31
- normalized = normalized.replace(/[\u200B\u200C\u200D\uFEFF\u2060]/g, '');
32
- return normalized;
33
- }
34
- /**
35
- * Parse HTML text into styled segments
36
- */
37
- export function parseHTMLToSegments(html, baseElement) {
38
- const segments = [];
39
- const tagStack = [];
40
- // Regex to match tags and text content
41
- const regex = /<(\/?)(strong|b|em|i|u|span)([^>]*)>|([^<]+)/gi;
42
- let match;
43
- while ((match = regex.exec(html)) !== null) {
44
- if (match[4]) {
45
- // Text content
46
- const text = decodeHtmlEntities(match[4]);
47
- // Calculate current styles from tag stack
48
- let bold = false;
49
- let italic = false;
50
- let underline = false;
51
- let color = undefined;
52
- for (const tag of tagStack) {
53
- if (tag.tag === 'strong' || tag.tag === 'b')
54
- bold = true;
55
- if (tag.tag === 'em' || tag.tag === 'i')
56
- italic = true;
57
- if (tag.tag === 'u')
58
- underline = true;
59
- if (tag.color)
60
- color = tag.color;
61
- }
62
- segments.push({
63
- text,
64
- bold,
65
- italic,
66
- underline,
67
- color,
68
- });
69
- }
70
- else {
71
- // Tag
72
- const isClosing = match[1] === '/';
73
- const tagName = match[2].toLowerCase();
74
- const attributes = match[3];
75
- if (isClosing) {
76
- // Remove from stack
77
- const index = tagStack.findIndex((t) => t.tag === tagName);
78
- if (index !== -1) {
79
- tagStack.splice(index, 1);
80
- }
81
- }
82
- else {
83
- // Add to stack
84
- const tagData = { tag: tagName };
85
- // Parse color from span style attribute
86
- if (attributes) {
87
- const colorMatch = /style=["'](?:[^"']*)?color:\s*([^;"']+)/i.exec(attributes);
88
- if (colorMatch) {
89
- tagData.color = colorMatch[1].trim();
90
- }
91
- }
92
- tagStack.push(tagData);
93
- }
94
- }
95
- }
96
- return segments;
97
- }
98
- /**
99
- * Parse HTML into tokens (text and tags)
100
- */
101
- export function tokenizeHTML(html) {
102
- const tokens = [];
103
- const regex = /<(\/?)(strong|b|em|i|u|span)([^>]*)>|([^<]+)/gi;
104
- let match;
105
- while ((match = regex.exec(html)) !== null) {
106
- if (match[4]) {
107
- // Text content
108
- const decodedContent = decodeHtmlEntities(match[4]);
109
- tokens.push({
110
- type: 'text',
111
- content: match[4],
112
- decodedContent,
113
- });
114
- }
115
- else {
116
- // Tag
117
- const isClosing = match[1] === '/';
118
- const tagName = match[2].toLowerCase();
119
- tokens.push({
120
- type: 'tag',
121
- content: match[0],
122
- tagName: tagName,
123
- isClosing: isClosing,
124
- });
125
- }
126
- }
127
- return tokens;
128
- }
129
- const VOID_ELEMENTS = new Set(['br']);
130
- function parseAttributes(raw) {
131
- const attributes = {};
132
- const attrRegex = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
133
- let attrMatch;
134
- while ((attrMatch = attrRegex.exec(raw)) !== null) {
135
- const name = attrMatch[1].toLowerCase();
136
- const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? '';
137
- attributes[name] = value;
138
- }
139
- return attributes;
140
- }
141
- function parseSimpleHTML(html) {
142
- const root = {
143
- type: 'element',
144
- tagName: 'root',
145
- attributes: {},
146
- children: [],
147
- };
148
- const stack = [root];
149
- const tagRegex = /<\/?([a-zA-Z0-9:-]+)([^>]*)>/g;
150
- let lastIndex = 0;
151
- let match;
152
- while ((match = tagRegex.exec(html)) !== null) {
153
- if (match.index > lastIndex) {
154
- const textContent = html.slice(lastIndex, match.index);
155
- if (textContent) {
156
- stack[stack.length - 1].children.push({
157
- type: 'text',
158
- content: textContent,
159
- });
160
- }
161
- }
162
- const fullMatch = match[0];
163
- const isClosing = fullMatch.startsWith('</');
164
- const tagName = match[1].toLowerCase();
165
- const attrChunk = match[2] || '';
166
- if (isClosing) {
167
- for (let i = stack.length - 1; i >= 0; i--) {
168
- if (stack[i].tagName === tagName) {
169
- stack.length = i;
170
- break;
171
- }
172
- }
173
- }
174
- else {
175
- const attributes = parseAttributes(attrChunk);
176
- const elementNode = {
177
- type: 'element',
178
- tagName,
179
- attributes,
180
- children: [],
181
- };
182
- const parent = stack[stack.length - 1];
183
- parent.children.push(elementNode);
184
- const selfClosing = fullMatch.endsWith('/>') || VOID_ELEMENTS.has(tagName);
185
- if (!selfClosing) {
186
- stack.push(elementNode);
187
- }
188
- }
189
- lastIndex = tagRegex.lastIndex;
190
- }
191
- if (lastIndex < html.length) {
192
- const remaining = html.slice(lastIndex);
193
- if (remaining) {
194
- stack[stack.length - 1].children.push({
195
- type: 'text',
196
- content: remaining,
197
- });
198
- }
199
- }
200
- return root;
201
- }
202
- function serializeAttributes(attributes) {
203
- const entries = Object.entries(attributes);
204
- if (!entries.length) {
205
- return '';
206
- }
207
- return (' ' +
208
- entries
209
- .map(([key, value]) => value === '' ? key : `${key}="${value.replace(/"/g, '&quot;')}"`)
210
- .join(' '));
211
- }
212
- function shouldSkipNode(node) {
213
- if (node.type === 'element' && node.tagName === 'span') {
214
- const classAttr = node.attributes['class'] || node.attributes['className'] || '';
215
- if (/\bql-cursor\b/.test(classAttr)) {
216
- return true;
217
- }
218
- }
219
- return false;
220
- }
221
- function serializeNodes(nodes) {
222
- return nodes
223
- .map((node) => {
224
- if (shouldSkipNode(node)) {
225
- return '';
226
- }
227
- if (node.type === 'text') {
228
- return node.content.replace(/\uFEFF/g, '');
229
- }
230
- const attrs = serializeAttributes(node.attributes);
231
- if (VOID_ELEMENTS.has(node.tagName)) {
232
- return `<${node.tagName}${attrs}>`;
233
- }
234
- return `<${node.tagName}${attrs}>${serializeNodes(node.children)}</${node.tagName}>`;
235
- })
236
- .join('');
237
- }
238
- function getIndentLevelFromAttributes(attributes) {
239
- const classAttr = attributes['class'] || attributes['className'] || '';
240
- const match = classAttr.match(/ql-indent-(\d+)/);
241
- return match ? parseInt(match[1], 10) : 0;
242
- }
243
- function detectIndentLevel(node) {
244
- const directIndent = getIndentLevelFromAttributes(node.attributes);
245
- if (directIndent > 0) {
246
- return directIndent;
247
- }
248
- for (const child of node.children) {
249
- if (child.type === 'element' &&
250
- child.tagName !== 'ul' &&
251
- child.tagName !== 'ol') {
252
- const childIndent = detectIndentLevel(child);
253
- if (childIndent > 0) {
254
- return childIndent;
255
- }
256
- }
257
- }
258
- return 0;
259
- }
260
- function nodesToParagraphs(nodes, baseIndentLevel = 0) {
261
- const paragraphs = [];
262
- let pendingInline = [];
263
- const flushInline = () => {
264
- if (pendingInline.length === 0) {
265
- return;
266
- }
267
- const html = serializeNodes(pendingInline);
268
- paragraphs.push({ html });
269
- pendingInline = [];
270
- };
271
- for (const node of nodes) {
272
- if (shouldSkipNode(node)) {
273
- continue;
274
- }
275
- if (node.type === 'text') {
276
- const newlineRegex = /\n+/g;
277
- let lastIndex = 0;
278
- let match;
279
- while ((match = newlineRegex.exec(node.content)) !== null) {
280
- const chunk = node.content.slice(lastIndex, match.index);
281
- if (chunk.length > 0) {
282
- pendingInline.push({
283
- type: 'text',
284
- content: chunk,
285
- });
286
- }
287
- flushInline();
288
- const extraBreaks = match[0].length - 1;
289
- for (let extra = 0; extra < extraBreaks; extra++) {
290
- paragraphs.push({ html: '' });
291
- }
292
- lastIndex = newlineRegex.lastIndex;
293
- }
294
- const rest = node.content.slice(lastIndex);
295
- if (rest.length > 0) {
296
- pendingInline.push({
297
- type: 'text',
298
- content: rest,
299
- });
300
- }
301
- continue;
302
- }
303
- if (node.tagName === 'p') {
304
- flushInline();
305
- const html = serializeNodes(node.children);
306
- paragraphs.push({
307
- html,
308
- });
309
- continue;
310
- }
311
- if (node.tagName === 'br') {
312
- flushInline();
313
- paragraphs.push({ html: '' });
314
- continue;
315
- }
316
- if (node.tagName === 'ul' || node.tagName === 'ol') {
317
- flushInline();
318
- paragraphs.push(...collectListParagraphs(node, baseIndentLevel));
319
- continue;
320
- }
321
- pendingInline.push(node);
322
- }
323
- flushInline();
324
- return paragraphs;
325
- }
326
- function collectListParagraphs(listNode, baseIndentLevel) {
327
- const paragraphs = [];
328
- const listIndent = baseIndentLevel + getIndentLevelFromAttributes(listNode.attributes);
329
- let counter = 1;
330
- for (const child of listNode.children) {
331
- if (child.type !== 'element' || child.tagName !== 'li') {
332
- continue;
333
- }
334
- const liIndentFromAttribute = getIndentLevelFromAttributes(child.attributes);
335
- const detectedIndent = detectIndentLevel(child);
336
- const indentLevel = liIndentFromAttribute > 0
337
- ? listIndent + liIndentFromAttribute
338
- : listIndent + detectedIndent;
339
- const itemParagraphs = nodesToParagraphs(child.children, indentLevel);
340
- let markerAssigned = false;
341
- for (const paragraph of itemParagraphs) {
342
- if (!paragraph.listMeta) {
343
- paragraph.listMeta = {
344
- type: listNode.tagName,
345
- index: counter,
346
- indentLevel,
347
- displayMarker: !markerAssigned,
348
- };
349
- markerAssigned = true;
350
- }
351
- paragraphs.push(paragraph);
352
- }
353
- if (!markerAssigned) {
354
- paragraphs.push({
355
- html: '',
356
- listMeta: {
357
- type: listNode.tagName,
358
- index: counter,
359
- indentLevel,
360
- displayMarker: true,
361
- },
362
- });
363
- }
364
- if (listNode.tagName === 'ol') {
365
- counter += 1;
366
- }
367
- }
368
- return paragraphs;
369
- }
370
- export function parseHtmlToParagraphs(html) {
371
- const root = parseSimpleHTML(html);
372
- return nodesToParagraphs(root.children);
373
- }
374
- /**
375
- * Reconstruct HTML from tokens while maintaining proper tag nesting across line breaks
376
- * @param tokens - Array of parsed HTML tokens
377
- * @param openTags - Tags that were opened in previous lines and should be carried forward
378
- * @returns Reconstructed HTML string and the updated list of open tags
379
- */
380
- export function tokensToHTML(tokens, openTags) {
381
- let html = '';
382
- const tagStack = [...openTags]; // Clone the open tags
383
- // Prepend any open tags
384
- for (const tag of openTags) {
385
- html += tag.fullTag;
386
- }
387
- // Process tokens
388
- for (const token of tokens) {
389
- if (token.type === 'text') {
390
- html += token.content;
391
- }
392
- else if (token.type === 'tag') {
393
- html += token.content;
394
- if (token.isClosing) {
395
- // Remove from stack
396
- const idx = tagStack.findIndex((t) => t.name === token.tagName);
397
- if (idx !== -1) {
398
- tagStack.splice(idx, 1);
399
- }
400
- }
401
- else {
402
- // Add to stack
403
- tagStack.push({
404
- name: token.tagName,
405
- fullTag: token.content,
406
- });
407
- }
408
- }
409
- }
410
- // Close any remaining open tags for this line
411
- for (let i = tagStack.length - 1; i >= 0; i--) {
412
- html += `</${tagStack[i].name}>`;
413
- }
414
- return { html, openTags: tagStack };
415
- }
@@ -1,8 +0,0 @@
1
- import { TextElement, TextLine, LineRenderContext, SegmentRenderOptions, RenderSegment } from './types.js';
2
- export declare function drawListMarker(doc: PDFKit.PDFDocument, element: TextElement, line: TextLine, lineXOffset: number, lineYOffset: number, fonts: Record<string, boolean>, color: string, opacity: number, mode: 'fill' | 'stroke', textOptions: PDFKit.Mixins.TextOptions): Promise<void>;
3
- export declare function prepareLineForRendering(doc: PDFKit.PDFDocument, element: TextElement, line: TextLine, lineIndex: number, yOffset: number, lineHeightPx: number, cumulativeYOffset: number, textOptions: PDFKit.Mixins.TextOptions, fonts: Record<string, boolean>, markerColor: string, markerOpacity: number, markerMode: 'fill' | 'stroke'): Promise<LineRenderContext>;
4
- export declare function renderSegmentsForLine(doc: PDFKit.PDFDocument, element: TextElement, line: TextLine, renderSegments: RenderSegment[], context: LineRenderContext, textOptions: PDFKit.Mixins.TextOptions, options: SegmentRenderOptions): Promise<void>;
5
- export declare function renderPDFX1aStroke(doc: PDFKit.PDFDocument, element: TextElement, textLines: TextLine[], yOffset: number, lineHeightPx: number, textOptions: PDFKit.Mixins.TextOptions, fonts: Record<string, boolean>): Promise<void>;
6
- export declare function renderStandardStroke(doc: PDFKit.PDFDocument, element: TextElement, textLines: TextLine[], yOffset: number, lineHeightPx: number, textOptions: PDFKit.Mixins.TextOptions, fonts: Record<string, boolean>): Promise<void>;
7
- export declare function renderTextFill(doc: PDFKit.PDFDocument, element: TextElement, textLines: TextLine[], yOffset: number, lineHeightPx: number, textOptions: PDFKit.Mixins.TextOptions, fonts: Record<string, boolean>): Promise<void>;
8
- export declare function renderTextBackground(doc: PDFKit.PDFDocument, element: TextElement, verticalAlignmentOffset: number, textOptions: PDFKit.Mixins.TextOptions): void;