@zumer/snapdom-plugins 3.0.0-beta.0 → 4.0.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 (77) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +54 -20
  3. package/agent-map.d.ts +32 -0
  4. package/agent-map.js +5 -3
  5. package/ascii-export.d.ts +10 -0
  6. package/ascii-export.js +21 -6
  7. package/capture-frames.js +5 -5
  8. package/color-tint.js +4 -0
  9. package/context-export.d.ts +23 -0
  10. package/context-export.js +4 -1
  11. package/filter.js +1 -1
  12. package/gif-export.d.ts +6 -0
  13. package/gif-export.js +3 -3
  14. package/html-export.d.ts +8 -0
  15. package/html-export.js +6 -4
  16. package/index.d.ts +2 -1
  17. package/index.js +2 -1
  18. package/package.json +17 -6
  19. package/pdf/LICENSE +21 -0
  20. package/pdf/README.md +22 -0
  21. package/pdf/REFERENCE.md +520 -0
  22. package/pdf/src/frontmatter.js +200 -0
  23. package/pdf/src/furniture.js +398 -0
  24. package/pdf/src/index.js +2415 -0
  25. package/pdf/src/mega/README.md +47 -0
  26. package/pdf/src/mega/diag.js +148 -0
  27. package/pdf/src/mega/flow.js +56 -0
  28. package/pdf/src/mega/fonts.js +287 -0
  29. package/pdf/src/mega/forms.js +479 -0
  30. package/pdf/src/mega/labels.js +43 -0
  31. package/pdf/src/mega/meta.js +145 -0
  32. package/pdf/src/nav.js +357 -0
  33. package/pdf/src/paginate.js +583 -0
  34. package/pdf/src/tagged.js +355 -0
  35. package/pdf/src/text-layer.js +1771 -0
  36. package/pdf/src/writer/encrypt.js +218 -0
  37. package/pdf/src/writer/font.js +507 -0
  38. package/pdf/src/writer/index.js +1135 -0
  39. package/pdf/src/writer/subset.js +387 -0
  40. package/pdf.d.ts +294 -0
  41. package/pdf.js +1 -0
  42. package/redact-inputs.js +6 -6
  43. package/vector/CONTRACT.md +906 -0
  44. package/vector/LICENSE +21 -0
  45. package/vector/README.md +35 -0
  46. package/vector/src/adapters/snapdom.js +795 -0
  47. package/vector/src/collect/box.js +1676 -0
  48. package/vector/src/collect/font.js +487 -0
  49. package/vector/src/collect/image.js +1458 -0
  50. package/vector/src/collect/mathml.js +272 -0
  51. package/vector/src/collect/text.js +3027 -0
  52. package/vector/src/css/color.js +823 -0
  53. package/vector/src/css/gradient.js +1126 -0
  54. package/vector/src/css/matrix.js +398 -0
  55. package/vector/src/css/radius.js +124 -0
  56. package/vector/src/css/shadow.js +201 -0
  57. package/vector/src/emit/figma-json.js +1995 -0
  58. package/vector/src/emit/h2d-vector.js +215 -0
  59. package/vector/src/emit/h2d.js +1147 -0
  60. package/vector/src/emit/pdf.js +2571 -0
  61. package/vector/src/emit/svg-flat.js +4842 -0
  62. package/vector/src/exclusion.js +13 -0
  63. package/vector/src/index.js +2910 -0
  64. package/vector/src/paint.js +2908 -0
  65. package/vector/src/public.js +57 -0
  66. package/vector/src/writer/encrypt.js +227 -0
  67. package/vector/src/writer/font.js +507 -0
  68. package/vector/src/writer/index.js +1148 -0
  69. package/vector/src/writer/subset.js +387 -0
  70. package/vector/svd/index.js +72 -0
  71. package/vector/svd/schema.js +985 -0
  72. package/vector.d.ts +28 -0
  73. package/vector.js +1 -0
  74. package/video-export.d.ts +6 -0
  75. package/video-export.js +7 -7
  76. package/pdf-image.d.ts +0 -18
  77. package/pdf-image.js +0 -114
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Document structure: the tree a tagged PDF exposes to assistive technology.
3
+ *
4
+ * This is the one thing this plugin can do cheaply that nothing starting from a
5
+ * raster or from a layout engine can. Reading order, heading levels, table cells,
6
+ * list items and alternative text are all facts about the DOM, and the DOM is
7
+ * already open in front of us at the moment the text layer is measured. A tool
8
+ * that receives a finished page of ink has to infer every one of them.
9
+ *
10
+ * Two decisions shape everything here.
11
+ *
12
+ * **The page image is an ARTIFACT, not a figure.** The raster is a picture OF the
13
+ * same words the invisible layer carries, so tagging it as content would make a
14
+ * screen reader announce a full-page image on top of the text it duplicates. Every
15
+ * pixel this plugin draws — page image, repeated header, watermark, running head,
16
+ * page number, index leader — is furniture, and PDF has a word for furniture.
17
+ * `index.js` wraps them all in `/Artifact`; the structure below covers only the
18
+ * text layer.
19
+ *
20
+ * **Only block structure is tagged.** Wrapping every `<span>` and `<em>` produces a
21
+ * tree that is technically richer and practically unreadable, and the inline text
22
+ * is already in reading order inside its block. The two inline exceptions carry
23
+ * something no block can: a `/Link` node per anchor and a `/Form` node per
24
+ * fillable control, each existing to hold the `/OBJR` that ties its annotation
25
+ * into the tree — see the carrier-node comment in `buildStructure`.
26
+ *
27
+ * Nothing in this file writes PDF, and nothing in it knows how the page is
28
+ * painted. It maps elements to structure types and hands back a tree.
29
+ */
30
+
31
+ // `isFigure` and `semanticAlt` are exported for the text layer, which decides
32
+ // which textless objects earn a marked-content run. That decision and this one
33
+ // have to be the same decision — a run whose owner this file types differently
34
+ // ends up in whatever paragraph encloses it — so it is asked here rather than
35
+ // answered twice.
36
+
37
+ /**
38
+ * HTML → PDF standard structure type. The writer emits PDF 1.7, so the table
39
+ * row-group types that arrived in 1.5 — `THead`, `TBody`, `TFoot` — map
40
+ * directly instead of staying transparent.
41
+ */
42
+ const ROLE = {
43
+ H1: 'H1', H2: 'H2', H3: 'H3', H4: 'H4', H5: 'H5', H6: 'H6',
44
+ P: 'P',
45
+ BLOCKQUOTE: 'BlockQuote',
46
+ FIGURE: 'Figure', IMG: 'Figure', FIGCAPTION: 'Caption', CAPTION: 'Caption',
47
+ UL: 'L', OL: 'L', DL: 'L',
48
+ LI: 'LI', DT: 'Lbl', DD: 'LBody',
49
+ TABLE: 'Table', TR: 'TR', TH: 'TH', TD: 'TD',
50
+ THEAD: 'THead', TBODY: 'TBody', TFOOT: 'TFoot',
51
+ ARTICLE: 'Art', SECTION: 'Sect', ASIDE: 'Sect', NAV: 'Sect',
52
+ HEADER: 'Sect', FOOTER: 'Sect', MAIN: 'Sect',
53
+ PRE: 'Code', CODE: 'Code',
54
+ }
55
+
56
+ /**
57
+ * Types that GROUP other structure elements rather than carrying text of their
58
+ * own. Text that lands directly on one of these gets an implicit `P` in between,
59
+ * because "a list containing words" is not a structure any reader can use.
60
+ */
61
+ const GROUPING = new Set(['Art', 'Sect', 'Div', 'L', 'LI', 'Table', 'TR', 'Document', 'Figure',
62
+ 'THead', 'TBody', 'TFoot'])
63
+
64
+ /** Inline boxes never become structure — see the file header. */
65
+ const INLINE = /^(inline|contents|ruby|none)/
66
+
67
+ /**
68
+ * The flattened-tree parent, the same one the text layer places against: a
69
+ * slotted node renders inside its slot, and a shadow root's children have no
70
+ * `parentElement`. Structure follows the tree that is PAINTED, so reading order
71
+ * and visual order are the same order.
72
+ */
73
+ function up(node) {
74
+ if (node.assignedSlot) return node.assignedSlot
75
+ if (node.parentElement) return node.parentElement
76
+ const root = node.getRootNode()
77
+ return root instanceof ShadowRoot ? root.host : null
78
+ }
79
+
80
+ /** ARIA role → structure type, for the roles that name a structure PDF 1.4 has. */
81
+ const ARIA_ROLE = {
82
+ list: 'L', listitem: 'LI', table: 'Table', row: 'TR',
83
+ cell: 'TD', gridcell: 'TD', columnheader: 'TH', rowheader: 'TH', img: 'Figure',
84
+ }
85
+
86
+ /** The first token of `role`, which is the one that applies. */
87
+ function roleToken(el) {
88
+ return String(el.getAttribute && el.getAttribute('role') || '')
89
+ .trim().split(/\s+/)[0].toLowerCase()
90
+ }
91
+
92
+ /** Which axis a header cell heads, plus its spans — written as the /A table
93
+ * attributes the writer knows how to say. */
94
+ function thAttrs(el) {
95
+ const scopeAttr = String(el.getAttribute && el.getAttribute('scope') || '').toLowerCase()
96
+ const scope = scopeAttr === 'row' || scopeAttr === 'rowgroup' ? 'Row'
97
+ : scopeAttr === 'col' || scopeAttr === 'colgroup' ? 'Column'
98
+ : (el.closest && el.closest('thead')) ? 'Column' : 'Row'
99
+ return { scope, colSpan: el.colSpan || 1, rowSpan: el.rowSpan || 1 }
100
+ }
101
+
102
+ /**
103
+ * The structure type an element carries on its own, before layout has a say.
104
+ *
105
+ * ARIA wins where it is set, because it is what assistive tech already reads and
106
+ * the structure tree is the same promise made to a different reader. A role this
107
+ * table does not know is NOT a refusal, though: `role="banner"` names no PDF
108
+ * structure, so the tag's own meaning still stands.
109
+ */
110
+ function structureType(el) {
111
+ const aria = roleToken(el)
112
+ if (aria === 'heading') {
113
+ const n = parseInt(el.getAttribute('aria-level'), 10)
114
+ return n >= 1 && n <= 6 ? `H${n}` : 'H1'
115
+ }
116
+ if (ARIA_ROLE[aria]) return ARIA_ROLE[aria]
117
+ if (aria === 'presentation' || aria === 'none') return null
118
+ return ROLE[el.tagName ? el.tagName.toUpperCase() : ''] || null
119
+ }
120
+
121
+ /**
122
+ * Whether this element is a Figure — the one question the text layer has to ask
123
+ * too, so it asks it here instead of keeping its own copy of the rule.
124
+ */
125
+ export const isFigure = (el) => structureType(el) === 'Figure'
126
+
127
+ function roleOf(el, styleOf) {
128
+ const role = structureType(el)
129
+ // A semantic tag laid out inline is a phrase, not a block — `<code>` in the
130
+ // middle of a sentence must not break the sentence into three structures.
131
+ if (role === 'Code' && INLINE.test(styleOf(el).display)) return null
132
+ return role
133
+ }
134
+
135
+ /** The nearest ancestor-or-self that generates a block-level box. */
136
+ function blockOf(el, root, styleOf) {
137
+ let node = el
138
+ while (node && node !== root) {
139
+ if (!INLINE.test(styleOf(node).display)) return node
140
+ node = up(node)
141
+ }
142
+ return root
143
+ }
144
+
145
+ /**
146
+ * Build the structure tree for one capture.
147
+ *
148
+ * @param {Element} root
149
+ * @param {Array<Element>} owners the text layer's `owners`, parallel to its runs
150
+ * @param {object} [options]
151
+ * @param {string} [options.lang] overrides the document language
152
+ * @param {Array<Element>} [options.links] link owners, one per link rect — the
153
+ * `/Link` carrier nodes come back in `ofLink`, memoised per element
154
+ * @param {Array<Element>} [options.controls] form control owners — the `/Form`
155
+ * carrier nodes come back in `ofControl`
156
+ * @returns {{root: object, ofRun: Array<object|null>, ofLink: Array<object|null>,
157
+ * ofControl: Array<object|null>, lang: string|null, warnings: string[]}}
158
+ * `root` is a node `{ type, alt, lang, children }` — the `/Document`. `ofRun[i]`
159
+ * is the node run `i` belongs to, or null when the run had no owner. Nodes carry
160
+ * no element references outward: what leaves here is a shape, not a live DOM.
161
+ *
162
+ * `children` holds BOTH child nodes and marked-content references, in one list,
163
+ * because reading order runs through both: a block that carries a sentence and
164
+ * then a nested list has to read the sentence first. The caller appends
165
+ * `{ mcid, page }` entries as it draws. A node has a `type`; a mark has an
166
+ * `mcid`.
167
+ */
168
+ export function buildStructure(root, owners, options = {}) {
169
+ const styles = new WeakMap()
170
+ const styleOf = (el) => {
171
+ let s = styles.get(el)
172
+ if (!s) { s = getComputedStyle(el); styles.set(el, s) }
173
+ return s
174
+ }
175
+
176
+ const doc = { type: 'Document', alt: null, lang: null, children: [] }
177
+ const byEl = new Map()
178
+ const implicit = new Map()
179
+ const warnings = []
180
+ const directlyOwned = new Set(owners)
181
+
182
+ /** aria-hidden=true on any flattened ancestor suppresses the whole subtree. */
183
+ const hiddenCache = new WeakMap()
184
+ const hiddenFromAT = (el) => {
185
+ if (!el) return false
186
+ if (hiddenCache.has(el)) return hiddenCache.get(el)
187
+ const own = el.getAttribute &&
188
+ String(el.getAttribute('aria-hidden')).trim().toLowerCase() === 'true'
189
+ const hidden = !!own || (el !== root && hiddenFromAT(up(el)))
190
+ hiddenCache.set(el, hidden)
191
+ return hidden
192
+ }
193
+
194
+ /**
195
+ * Language introduced between this node and its nearest structural ancestor.
196
+ * Transparent layout containers disappear from the PDF tree, so a `lang` on one
197
+ * of them must move down to the first structure node that survives the collapse.
198
+ */
199
+ const structuralLang = (el) => {
200
+ let node = el
201
+ while (node) {
202
+ const own = langOf(node)
203
+ if (own) return own
204
+ if (node === root) break
205
+ const parent = up(node)
206
+ if (!parent || parent === root || roleOf(parent, styleOf)) break
207
+ node = parent
208
+ }
209
+ return null
210
+ }
211
+
212
+ /**
213
+ * The structure node an element belongs to, memoised. An element with no role
214
+ * of its own is TRANSPARENT: it contributes nothing and its content attaches to
215
+ * whatever encloses it, which is what keeps a page of nested layout `<div>`s
216
+ * from becoming a page of nested structure.
217
+ */
218
+ const nodeFor = (el) => {
219
+ if (!el) return doc
220
+ if (byEl.has(el)) return byEl.get(el)
221
+ if (hiddenFromAT(el)) { byEl.set(el, null); return null }
222
+ const parent = el === root ? doc : nodeFor(up(el))
223
+ if (!parent) { byEl.set(el, null); return null }
224
+ const type = roleOf(el, styleOf)
225
+ if (!type) { byEl.set(el, parent); return parent }
226
+ const node = { type, alt: altOf(el), lang: el === root ? null : structuralLang(el), children: [] }
227
+ // A textless image has no marked-content run for index.js to attach. `keep`
228
+ // tells the byte writer that this deliberate Figure-with-Alt is still useful
229
+ // structure rather than an empty branch to prune.
230
+ if (type === 'Figure' && node.alt && directlyOwned.has(el)) node.keep = true
231
+ // A header cell says which axis it heads. The attribute wins; without one,
232
+ // a cell in a thead heads its column and any other header cell its row —
233
+ // the same inference AT applies to the HTML.
234
+ if (type === 'TH') node.attrs = thAttrs(el)
235
+ parent.children.push(node)
236
+ byEl.set(el, node)
237
+ return node
238
+ }
239
+
240
+ /** Where run `i`'s glyphs belong: its own block, or an implicit paragraph. */
241
+ const place = (owner) => {
242
+ if (!owner || hiddenFromAT(owner)) return null
243
+ // A synthetic whitespace marker for an image-only object belongs to the
244
+ // Figure itself, not to an implicit paragraph around it.
245
+ if (roleOf(owner, styleOf) === 'Figure') return nodeFor(owner)
246
+ const block = blockOf(owner, root, styleOf)
247
+ const node = nodeFor(block)
248
+ if (!node) return null
249
+ if (!GROUPING.has(node.type)) return node
250
+ // Text sitting straight inside a grouping element. One implicit paragraph per
251
+ // block, so consecutive words of the same sentence share it.
252
+ const key = block
253
+ let p = implicit.get(key)
254
+ if (!p) {
255
+ p = { type: 'P', alt: null, lang: structuralLang(block), children: [] }
256
+ node.children.push(p)
257
+ implicit.set(key, p)
258
+ }
259
+ return p
260
+ }
261
+
262
+ // Prime nodes in flattened document order, including semantic images with no
263
+ // text owner. Merely iterating `owners` cannot see those images at all; priming
264
+ // also keeps an empty Figure between the paragraphs that surround it instead of
265
+ // appending every synthetic node at the end.
266
+ const seen = new Set()
267
+ const prime = (node) => {
268
+ if (!node || seen.has(node)) return
269
+ seen.add(node)
270
+ if (node.nodeType === Node.TEXT_NODE) {
271
+ if (/\S/.test(node.data || '')) place(up(node))
272
+ return
273
+ }
274
+ if (node.nodeType !== Node.ELEMENT_NODE) return
275
+ const tag = node.tagName.toUpperCase()
276
+ if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'HEAD', 'TITLE'].includes(tag) || hiddenFromAT(node)) return
277
+ nodeFor(node)
278
+
279
+ let kids
280
+ if (node.shadowRoot) kids = node.shadowRoot.childNodes
281
+ else if (tag === 'SLOT') kids = node.assignedNodes({ flatten: true })
282
+ else kids = node.childNodes
283
+ for (const kid of kids) prime(kid)
284
+ }
285
+ prime(root)
286
+
287
+ const ofRun = owners.map(place)
288
+
289
+ /**
290
+ * Persistent per-element carrier nodes for things whose CONTENT is per-export:
291
+ * a link's annotation and a field's widget join the tree through `/OBJR`
292
+ * entries pushed into `marksOf` at draw time, so the node itself has to exist
293
+ * once per capture — the same tree-vs-marks split everything else obeys. A
294
+ * node with no marks in a given export is pruned by the writer's `alive`.
295
+ *
296
+ * A multi-line anchor produces several link rects for ONE element, so carriers
297
+ * memoise: all of an anchor's annotations belong to one `/Link`.
298
+ */
299
+ const carriers = new Map()
300
+ const carrierFor = (el, type) => {
301
+ if (!el || hiddenFromAT(el)) return null
302
+ if (carriers.has(el)) return carriers.get(el)
303
+ const parent = place(el)
304
+ if (!parent) { carriers.set(el, null); return null }
305
+ const node = { type, alt: null, lang: null, children: [] }
306
+ parent.children.push(node)
307
+ carriers.set(el, node)
308
+ return node
309
+ }
310
+ const ofLink = (options.links || []).map(el => carrierFor(el, 'Link'))
311
+ const ofControl = (options.controls || []).map(el => carrierFor(el, 'Form'))
312
+
313
+ const lang = options.lang || langOf(root) ||
314
+ (root.ownerDocument && root.ownerDocument.documentElement.getAttribute('lang')) || null
315
+ if (!lang) {
316
+ warnings.push('no `lang` on the capture or on <html> — a tagged PDF with no language ' +
317
+ 'leaves a screen reader guessing which one to pronounce. Set one, or pass `lang`.')
318
+ }
319
+
320
+ return { root: doc, ofRun, ofLink, ofControl, lang, warnings }
321
+ }
322
+
323
+ /**
324
+ * The accessible name, and the fallback order a browser reads it in.
325
+ *
326
+ * Exported for the text layer, which needs the same answer to decide whether a
327
+ * textless Figure is worth reaching at all.
328
+ */
329
+ export function semanticAlt(el) {
330
+ if (!el.getAttribute) return null
331
+ const aria = el.getAttribute('aria-label')
332
+ if (aria && aria.trim()) return aria.trim()
333
+ // `alt=""` is an explicit request for a decorative image. Do not resurrect it
334
+ // from title text and make a screen reader announce what the author suppressed.
335
+ if (el.hasAttribute('alt')) {
336
+ const alt = el.getAttribute('alt')
337
+ return alt && alt.trim() ? alt.trim() : null
338
+ }
339
+ const title = el.getAttribute('title')
340
+ return title && title.trim() ? title.trim() : null
341
+ }
342
+
343
+ const altOf = semanticAlt
344
+
345
+ function langOf(el) {
346
+ const lang = el.getAttribute && el.getAttribute('lang')
347
+ return lang && lang.trim() ? lang.trim() : null
348
+ }
349
+
350
+ // There is no `pruneStructure` here any more. Dropping a branch nothing marked is
351
+ // the byte writer's job and it already does it, by READING the tree rather than
352
+ // mutating it (`writeStructTree`'s `alive`) — which matters, because one measured
353
+ // tree is exported more than once. The copy that used to live here was called by
354
+ // nothing and still had to be patched in lockstep with the writer's version; the
355
+ // `keep` flag for a textless Figure was fixed in both on the same day.