@zumer/snapdom-plugins 3.0.0 → 4.0.1
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.
- package/CHANGELOG.md +21 -0
- package/README.md +50 -16
- package/agent-map.d.ts +32 -0
- package/agent-map.js +5 -3
- package/ascii-export.d.ts +10 -0
- package/ascii-export.js +21 -6
- package/capture-frames.js +5 -5
- package/color-tint.js +4 -0
- package/context-export.d.ts +23 -0
- package/context-export.js +4 -1
- package/filter.js +1 -1
- package/gif-export.d.ts +6 -0
- package/gif-export.js +3 -3
- package/html-export.d.ts +8 -0
- package/html-export.js +6 -4
- package/index.d.ts +2 -1
- package/index.js +2 -1
- package/package.json +22 -6
- package/pdf/LICENSE +21 -0
- package/pdf/README.md +22 -0
- package/pdf/REFERENCE.md +520 -0
- package/pdf/src/frontmatter.js +200 -0
- package/pdf/src/furniture.js +398 -0
- package/pdf/src/index.js +2415 -0
- package/pdf/src/mega/README.md +47 -0
- package/pdf/src/mega/diag.js +148 -0
- package/pdf/src/mega/flow.js +56 -0
- package/pdf/src/mega/fonts.js +287 -0
- package/pdf/src/mega/forms.js +479 -0
- package/pdf/src/mega/labels.js +43 -0
- package/pdf/src/mega/meta.js +145 -0
- package/pdf/src/nav.js +357 -0
- package/pdf/src/paginate.js +583 -0
- package/pdf/src/tagged.js +355 -0
- package/pdf/src/text-layer.js +1771 -0
- package/pdf/src/writer/encrypt.js +218 -0
- package/pdf/src/writer/font.js +507 -0
- package/pdf/src/writer/index.js +1135 -0
- package/pdf/src/writer/subset.js +387 -0
- package/pdf.d.ts +294 -0
- package/pdf.js +1 -0
- package/redact-inputs.js +6 -6
- package/vector/CONTRACT.md +906 -0
- package/vector/LICENSE +21 -0
- package/vector/README.md +35 -0
- package/vector/src/adapters/snapdom.js +795 -0
- package/vector/src/collect/box.js +1676 -0
- package/vector/src/collect/font.js +487 -0
- package/vector/src/collect/image.js +1458 -0
- package/vector/src/collect/mathml.js +272 -0
- package/vector/src/collect/text.js +3027 -0
- package/vector/src/css/color.js +823 -0
- package/vector/src/css/gradient.js +1126 -0
- package/vector/src/css/matrix.js +398 -0
- package/vector/src/css/radius.js +124 -0
- package/vector/src/css/shadow.js +201 -0
- package/vector/src/emit/figma-json.js +1995 -0
- package/vector/src/emit/h2d-vector.js +215 -0
- package/vector/src/emit/h2d.js +1147 -0
- package/vector/src/emit/pdf.js +2571 -0
- package/vector/src/emit/svg-flat.js +4842 -0
- package/vector/src/exclusion.js +13 -0
- package/vector/src/index.js +2910 -0
- package/vector/src/paint.js +2908 -0
- package/vector/src/public.js +57 -0
- package/vector/src/writer/encrypt.js +227 -0
- package/vector/src/writer/font.js +507 -0
- package/vector/src/writer/index.js +1148 -0
- package/vector/src/writer/subset.js +387 -0
- package/vector/svd/index.js +72 -0
- package/vector/svd/schema.js +985 -0
- package/vector.d.ts +28 -0
- package/vector.js +1 -0
- package/video-export.d.ts +6 -0
- package/video-export.js +7 -7
- package/pdf-image.d.ts +0 -18
- 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.
|