@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,272 @@
|
|
|
1
|
+
/** Native MathML, reconstructed from the mounted clone's measured layout.
|
|
2
|
+
*
|
|
3
|
+
* Browsers do not expose their MATH-table glyph assemblies or rule metrics.
|
|
4
|
+
* Text stays text; rules and radicals become geometry. This is deliberately an
|
|
5
|
+
* approximation, never a raster/foreignObject fallback or a semantic equation.
|
|
6
|
+
*/
|
|
7
|
+
import { isExcluded } from '../exclusion.js'
|
|
8
|
+
import { collectText } from './text.js'
|
|
9
|
+
|
|
10
|
+
const NS = 'http://www.w3.org/2000/svg'
|
|
11
|
+
const TOKENS = new Set(['mi', 'mn', 'mo', 'mtext', 'ms'])
|
|
12
|
+
const CONTAINERS = new Set(['math', 'mrow', 'mstyle', 'mpadded', 'mspace',
|
|
13
|
+
'msub', 'msup', 'msubsup', 'munder', 'mover', 'munderover', 'mmultiscripts',
|
|
14
|
+
'mprescripts', 'none', 'mtable', 'mtr', 'mlabeledtr', 'mtd', 'semantics'])
|
|
15
|
+
const SKIP = new Set(['annotation', 'annotation-xml', 'mphantom'])
|
|
16
|
+
const STRETCH = /^[()\[\]{}|‖⌈⌉⌊⌋⟨⟩∑∏∐∫∬∭∮√]$/u
|
|
17
|
+
const esc = value => String(value).replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]))
|
|
18
|
+
const num = value => String(Math.round(value * 10000) / 10000)
|
|
19
|
+
const positive = value => Number.isFinite(value) && value > 0
|
|
20
|
+
const MATH_AUTO_DEFAULTS = new WeakMap()
|
|
21
|
+
|
|
22
|
+
/** Some engines expose default <mi> italicization as CSS; others keep it internal.
|
|
23
|
+
* A shadow root isolates this capability probe from the captured page's CSS.
|
|
24
|
+
* Mount/read/remove is synchronous, so neither a paint nor an await sees it.
|
|
25
|
+
*/
|
|
26
|
+
function exposesMathAuto (doc) {
|
|
27
|
+
if (MATH_AUTO_DEFAULTS.has(doc)) return MATH_AUTO_DEFAULTS.get(doc)
|
|
28
|
+
const host = doc.createElement('div')
|
|
29
|
+
host.style.cssText = 'all:initial!important;position:fixed!important;left:-10000px!important;top:0!important;visibility:hidden!important;pointer-events:none!important'
|
|
30
|
+
const shadow = host.attachShadow({ mode: 'closed' })
|
|
31
|
+
const math = doc.createElementNS('http://www.w3.org/1998/Math/MathML', 'math')
|
|
32
|
+
const mi = doc.createElementNS(math.namespaceURI, 'mi')
|
|
33
|
+
mi.textContent = 'x'
|
|
34
|
+
math.appendChild(mi)
|
|
35
|
+
shadow.appendChild(math)
|
|
36
|
+
let exposed
|
|
37
|
+
try {
|
|
38
|
+
doc.documentElement.appendChild(host)
|
|
39
|
+
exposed = doc.defaultView.getComputedStyle(mi).textTransform === 'math-auto'
|
|
40
|
+
} finally { host.remove() }
|
|
41
|
+
MATH_AUTO_DEFAULTS.set(doc, exposed)
|
|
42
|
+
return exposed
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// MathML Core's default single-letter <mi> is Unicode math italic, not a
|
|
46
|
+
// synthetically slanted Latin glyph. U+210E fills the deliberate hole for h.
|
|
47
|
+
function italicIdentifier (text) {
|
|
48
|
+
if ([...text].length !== 1) return text
|
|
49
|
+
const cp = text.codePointAt(0)
|
|
50
|
+
if (cp >= 65 && cp <= 90) return String.fromCodePoint(0x1d434 + cp - 65)
|
|
51
|
+
if (cp >= 97 && cp <= 122) return cp === 104 ? '\u210e' : String.fromCodePoint(0x1d44e + cp - 97)
|
|
52
|
+
const greek = 'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡϴΣΤΥΦΧΨΩ∇αβγδεζηθικλμνξοπρςστυφχψω∂ϵϑϰϕϱϖ'
|
|
53
|
+
const at = [...greek].indexOf(text)
|
|
54
|
+
return at < 0 ? text : String.fromCodePoint(0x1d6e2 + at)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Axis-aligned ancestor scale is divided out; the normal node transform owns it. */
|
|
58
|
+
function scaleAbove (el, styleOf, warn) {
|
|
59
|
+
let sx = 1, sy = 1
|
|
60
|
+
const Matrix = el.ownerDocument.defaultView.DOMMatrix
|
|
61
|
+
for (let node = el; node; node = node.parentElement) {
|
|
62
|
+
const cs = styleOf(node)
|
|
63
|
+
if (cs.transform && cs.transform !== 'none') {
|
|
64
|
+
try {
|
|
65
|
+
const m = new Matrix(cs.transform)
|
|
66
|
+
if (!m.is2D || Math.abs(m.b) > 1e-6 || Math.abs(m.c) > 1e-6 || m.a <= 0 || m.d <= 0) {
|
|
67
|
+
warn('transform', 'Rotated, skewed or mirrored MathML uses bounding-box geometry; glyph placement is approximate.')
|
|
68
|
+
} else { sx *= m.a; sy *= m.d }
|
|
69
|
+
} catch { warn('transform', 'The MathML transform could not be resolved into local geometry.') }
|
|
70
|
+
}
|
|
71
|
+
if (cs.scale && cs.scale !== 'none') {
|
|
72
|
+
const values = cs.scale.trim().split(/\s+/).map(Number)
|
|
73
|
+
if (positive(values[0]) && (values.length === 1 || positive(values[1]))) {
|
|
74
|
+
sx *= values[0]; sy *= values[1] ?? values[0]
|
|
75
|
+
} else warn('transform', 'A non-positive MathML scale uses bounding-box geometry.')
|
|
76
|
+
}
|
|
77
|
+
if (cs.rotate && !['none', '0deg'].includes(cs.rotate)) warn('transform', 'Rotated MathML uses bounding-box geometry.')
|
|
78
|
+
const zoom = parseFloat(cs.zoom)
|
|
79
|
+
if (positive(zoom)) { sx *= zoom; sy *= zoom }
|
|
80
|
+
}
|
|
81
|
+
return { sx, sy }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assetId (ctx) {
|
|
85
|
+
if (typeof ctx.uid === 'function') return ctx.uid('a')
|
|
86
|
+
if (!ctx.assetIds) ctx.assetIds = new Set()
|
|
87
|
+
let id = 'a_mathml', index = 1
|
|
88
|
+
while (ctx.assetIds.has(id) || Object.prototype.hasOwnProperty.call(ctx.assets || {}, id)) id = `a_mathml_${index++}`
|
|
89
|
+
ctx.assetIds.add(id)
|
|
90
|
+
return id
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @returns {Promise<{asset:object, fill:object, warnings:object[], fontStyles:object[]}|null>} */
|
|
94
|
+
export async function collectMathML (el, cs, ctx = {}) {
|
|
95
|
+
if (!el || el.localName?.toLowerCase() !== 'math') return null
|
|
96
|
+
const warnings = []
|
|
97
|
+
const seen = new Set()
|
|
98
|
+
const warn = (code, message, grade = 'A') => {
|
|
99
|
+
if (seen.has(`${code}:${message}`)) return
|
|
100
|
+
seen.add(`${code}:${message}`)
|
|
101
|
+
warnings.push({ code: `collect.mathml-${code}`, grade, message })
|
|
102
|
+
}
|
|
103
|
+
const styleOf = node => typeof ctx.styleOf === 'function' ? ctx.styleOf(node) : el.ownerDocument.defaultView.getComputedStyle(node)
|
|
104
|
+
let fullyClipped = false
|
|
105
|
+
for (let node = el; node && !fullyClipped; node = node.parentElement) {
|
|
106
|
+
const style = styleOf(node)
|
|
107
|
+
const clip = /^rect\(([^)]+)\)$/.exec(style.clip || '')
|
|
108
|
+
if (clip && ['absolute', 'fixed'].includes(style.position)) {
|
|
109
|
+
const values = clip[1].trim().split(/[,\s]+/).map(value => value === 'auto' ? NaN : parseFloat(value))
|
|
110
|
+
if (values.length === 4) fullyClipped = values[1] <= values[3] || values[2] <= values[0]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const { sx, sy } = fullyClipped ? { sx: 1, sy: 1 } : scaleAbove(el, styleOf, warn)
|
|
114
|
+
const origin = el.getBoundingClientRect()
|
|
115
|
+
const w = origin.width / sx, h = origin.height / sy
|
|
116
|
+
const box = node => {
|
|
117
|
+
const r = node.getBoundingClientRect()
|
|
118
|
+
return { x: (r.left - origin.left) / sx, y: (r.top - origin.top) / sy, w: r.width / sx, h: r.height / sy }
|
|
119
|
+
}
|
|
120
|
+
const rangeBox = node => {
|
|
121
|
+
const range = el.ownerDocument.createRange()
|
|
122
|
+
range.selectNodeContents(node)
|
|
123
|
+
const r = range.getBoundingClientRect()
|
|
124
|
+
return { x: (r.left - origin.left) / sx, y: (r.top - origin.top) / sy, w: r.width / sx, h: r.height / sy }
|
|
125
|
+
}
|
|
126
|
+
const canvas = el.ownerDocument.createElement('canvas').getContext('2d')
|
|
127
|
+
const markup = [], fontStyles = [], inkBounds = new WeakMap()
|
|
128
|
+
const exclude = [].concat(ctx.exclude || [])
|
|
129
|
+
const tokenText = node => {
|
|
130
|
+
if (SKIP.has(node.localName) || isExcluded(node, exclude)) return ''
|
|
131
|
+
const style = styleOf(node)
|
|
132
|
+
if (style.display === 'none' || style.contentVisibility === 'hidden' || style.opacity === '0') return ''
|
|
133
|
+
return [...node.childNodes].map(child => child.nodeType === 1 ? tokenText(child)
|
|
134
|
+
: child.nodeType === 3 && !['hidden', 'collapse'].includes(style.visibility) ? child.data : '').join('')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const token = (node, style, rect, opacity) => {
|
|
138
|
+
let text = tokenText(node).replace(/\s+/g, ' ').trim()
|
|
139
|
+
if (!text) return
|
|
140
|
+
if (node.children.length) warn('approximation', 'Nested styling inside a MathML token uses its visible text and the token font; descendant styling may differ.')
|
|
141
|
+
if (node.localName === 'ms') {
|
|
142
|
+
text = `${node.getAttribute('lquote') ?? '"'}${text}${node.getAttribute('rquote') ?? '"'}`
|
|
143
|
+
warn('approximation', 'MathML string quotes are reconstructed around the measured text box.')
|
|
144
|
+
}
|
|
145
|
+
const variant = node.getAttribute('mathvariant')
|
|
146
|
+
// Where the UA exposes math-auto, computed `none` is an actual opt-out.
|
|
147
|
+
// Legacy engines still apply default <mi> italicization internally.
|
|
148
|
+
const transform = style.textTransform || 'none'
|
|
149
|
+
const automatic = node.localName === 'mi' && !variant && (transform === 'math-auto' ||
|
|
150
|
+
(transform === 'none' && !exposesMathAuto(el.ownerDocument)))
|
|
151
|
+
if (automatic || variant === 'italic') text = italicIdentifier(text)
|
|
152
|
+
else if (variant && variant !== 'normal') warn('unsupported', `MathML mathvariant="${variant}" is retained as text without its mathematical alphabet mapping.`, 'O')
|
|
153
|
+
if (!['none', 'math-auto'].includes(transform)) warn('unsupported', `MathML text-transform:${transform} is retained as source text without the CSS character transformation.`, 'O')
|
|
154
|
+
const size = parseFloat(style.fontSize) || 16
|
|
155
|
+
const family = style.fontFamily || 'serif'
|
|
156
|
+
const fontStyle = style.fontStyle || 'normal'
|
|
157
|
+
const weight = style.fontWeight || '400'
|
|
158
|
+
canvas.font = `${fontStyle} ${weight} ${size}px ${family}`
|
|
159
|
+
const metrics = canvas.measureText(text)
|
|
160
|
+
const rr = rangeBox(node)
|
|
161
|
+
let x = rect.x, baseline = rr.y + (metrics.fontBoundingBoxAscent || size * 0.8)
|
|
162
|
+
let advance = rect.w, yScale = 1
|
|
163
|
+
if (node.localName === 'mo') {
|
|
164
|
+
const inkHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent
|
|
165
|
+
const inkBox = Math.abs(rect.h - rr.h) > 1
|
|
166
|
+
const stretched = STRETCH.test(text) && inkBox && positive(inkHeight)
|
|
167
|
+
advance = stretched ? rect.w : Math.min(rect.w, rr.w || rect.w)
|
|
168
|
+
x += (rect.w - advance) / 2
|
|
169
|
+
if (inkBox && positive(inkHeight)) {
|
|
170
|
+
yScale = stretched ? rect.h / inkHeight : 1
|
|
171
|
+
baseline = rect.y + metrics.actualBoundingBoxAscent * yScale
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const inkWidth = metrics.actualBoundingBoxLeft + metrics.actualBoundingBoxRight
|
|
175
|
+
if (!positive(advance) && !positive(inkWidth)) {
|
|
176
|
+
warn('unsupported', 'A visible MathML token has no measurable advance or glyph ink and could not be reconstructed.', 'O')
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
inkBounds.set(node, { x, y: baseline - metrics.actualBoundingBoxAscent * yScale,
|
|
180
|
+
w: advance || inkWidth, h: (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) * yScale })
|
|
181
|
+
// Use the existing resolver for font metadata, including @font-face aliases.
|
|
182
|
+
// Geometry/characters come from MathML, not the prose collector's text runs.
|
|
183
|
+
const collected = collectText(node, ctx)
|
|
184
|
+
const resolved = collected && Object.values(collected.styles)[0]
|
|
185
|
+
if (resolved) fontStyles.push({ ...resolved, characters: text, fontStyle, fontSize: size, textTransform: 'none' })
|
|
186
|
+
const svgTransform = yScale === 1 ? '' : ` transform="translate(0 ${num(baseline)}) scale(1 ${num(yScale)})"`
|
|
187
|
+
const length = positive(advance) ? ` textLength="${num(advance)}" lengthAdjust="spacingAndGlyphs"` : ''
|
|
188
|
+
markup.push(`<text x="${num(x)}" y="${yScale === 1 ? num(baseline) : '0'}"${svgTransform} font-family="${esc(family)}" font-size="${num(size)}" font-weight="${esc(weight)}" font-style="${esc(fontStyle)}" fill="${esc(style.color)}" opacity="${num(opacity)}"${length} xml:space="preserve">${esc(text)}</text>`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const visit = (node, inheritedOpacity = 1) => {
|
|
192
|
+
const tag = node.localName?.toLowerCase()
|
|
193
|
+
if (!tag || SKIP.has(tag) || isExcluded(node, exclude)) return
|
|
194
|
+
const style = node === el ? cs : styleOf(node)
|
|
195
|
+
if (style.display === 'none' || style.contentVisibility === 'hidden') return
|
|
196
|
+
const opacity = inheritedOpacity * (node === el ? 1 : (Number.parseFloat(style.opacity) || (style.opacity === '0' ? 0 : 1)))
|
|
197
|
+
if (!opacity) return
|
|
198
|
+
const rect = box(node)
|
|
199
|
+
const visible = style.visibility !== 'hidden' && style.visibility !== 'collapse'
|
|
200
|
+
if (!(rect.w > 0 || rect.h > 0)) {
|
|
201
|
+
if (visible && TOKENS.has(tag) && parseFloat(style.fontSize) > 0 && tokenText(node).trim()) {
|
|
202
|
+
warn('unsupported', 'A visible MathML token has no measurable box and could not be reconstructed.', 'O')
|
|
203
|
+
}
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (node !== el && visible && style.backgroundColor && !['transparent', 'rgba(0, 0, 0, 0)'].includes(style.backgroundColor)) {
|
|
207
|
+
markup.push(`<rect x="${num(rect.x)}" y="${num(rect.y)}" width="${num(rect.w)}" height="${num(rect.h)}" fill="${esc(style.backgroundColor)}" opacity="${num(opacity)}"/>`)
|
|
208
|
+
}
|
|
209
|
+
if (node !== el && visible && ((style.transform && style.transform !== 'none') || (style.filter && style.filter !== 'none') || (style.clipPath && style.clipPath !== 'none') || (style.maskImage && style.maskImage !== 'none') || (style.textShadow && style.textShadow !== 'none'))) {
|
|
210
|
+
warn('approximation', 'Effects or transforms on MathML descendants use measured boxes without their complete CSS paint.')
|
|
211
|
+
}
|
|
212
|
+
if (node !== el && visible && ['Top', 'Right', 'Bottom', 'Left'].some(side => parseFloat(style[`border${side}Width`]) > 0)) {
|
|
213
|
+
warn('unsupported', 'Borders on MathML descendants are not reconstructed; their text and mathematical layout remain.', 'O')
|
|
214
|
+
}
|
|
215
|
+
if (TOKENS.has(tag)) {
|
|
216
|
+
if (visible) token(node, style, rect, opacity)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
const children = [...node.children].filter(child => !SKIP.has(child.localName))
|
|
220
|
+
if (visible && tag === 'mfrac' && children.length >= 2) {
|
|
221
|
+
const numerator = box(children[0]), denominator = box(children[1])
|
|
222
|
+
const size = parseFloat(style.fontSize) || 16
|
|
223
|
+
const raw = node.getAttribute('linethickness')
|
|
224
|
+
let thickness = size * 0.04
|
|
225
|
+
if (raw !== null) {
|
|
226
|
+
const n = parseFloat(raw)
|
|
227
|
+
if (Number.isFinite(n)) thickness = raw.trim().endsWith('%') ? thickness * n / 100 : /em$/.test(raw) ? size * n : n
|
|
228
|
+
else if (raw === 'thin') thickness *= 0.5
|
|
229
|
+
else if (raw === 'thick') thickness *= 2
|
|
230
|
+
else if (raw !== 'medium') warn('approximation', `MathML fraction thickness "${raw}" uses the default rule width.`)
|
|
231
|
+
}
|
|
232
|
+
if (node.getAttribute('bevelled') === 'true') warn('unsupported', 'Bevelled MathML fraction separators are not reconstructed; their child content remains.', 'O')
|
|
233
|
+
else if (thickness > 0) {
|
|
234
|
+
const y = (numerator.y + numerator.h + denominator.y) / 2
|
|
235
|
+
markup.push(`<line x1="${num(rect.x)}" y1="${num(y)}" x2="${num(rect.x + rect.w)}" y2="${num(y)}" stroke="${esc(style.color)}" stroke-width="${num(thickness)}" opacity="${num(opacity)}"/>`)
|
|
236
|
+
}
|
|
237
|
+
} else if (visible && !CONTAINERS.has(tag) && tag !== 'msqrt' && tag !== 'mroot') {
|
|
238
|
+
warn('unsupported', `MathML <${tag}> decoration or generated content is not reconstructed; measurable child tokens remain.`, 'O')
|
|
239
|
+
}
|
|
240
|
+
if (visible && tag === 'mtable' && ['rowlines', 'columnlines', 'frame'].some(attr => node.hasAttribute(attr) && !/^(none\s*)+$/.test(node.getAttribute(attr)))) {
|
|
241
|
+
warn('unsupported', 'MathML table rules and frames are not reconstructed; cell contents and placement remain.', 'O')
|
|
242
|
+
}
|
|
243
|
+
for (const child of tag === 'semantics' ? children.slice(0, 1) : children) visit(child, opacity)
|
|
244
|
+
if (visible && (tag === 'msqrt' || tag === 'mroot') && children.length) {
|
|
245
|
+
const contents = tag === 'mroot' ? children.slice(0, 1) : children
|
|
246
|
+
const boxes = contents.map(box)
|
|
247
|
+
const ink = contents.flatMap(child => [child, ...child.querySelectorAll('*')]).map(child => inkBounds.get(child)).filter(Boolean)
|
|
248
|
+
const left = Math.min(...boxes.map(r => r.x)), right = Math.max(...boxes.map(r => r.x + r.w))
|
|
249
|
+
const top = Math.min(...(ink.length ? ink : boxes).map(r => r.y))
|
|
250
|
+
const bottom = Math.max(...(ink.length ? ink : boxes).map(r => r.y + r.h))
|
|
251
|
+
const size = parseFloat(style.fontSize) || 16, stroke = Math.max(0.5, size * 0.04)
|
|
252
|
+
const gap = Math.min(size * 0.7, Math.max(stroke * 3, left - rect.x))
|
|
253
|
+
const start = left - gap, y = Math.max(rect.y, top - size * 0.12) + stroke / 2
|
|
254
|
+
const height = Math.max(stroke * 3, bottom - y)
|
|
255
|
+
const d = `M${num(start)} ${num(y + height * 0.58)} L${num(start + gap * 0.2)} ${num(y + height * 0.49)} L${num(start + gap * 0.43)} ${num(bottom)} L${num(left)} ${num(y)} H${num(right)}`
|
|
256
|
+
markup.push(`<path d="${d}" fill="none" stroke="${esc(style.color)}" stroke-width="${num(stroke)}" stroke-linejoin="miter" opacity="${num(opacity)}"/>`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (!fullyClipped) visit(el)
|
|
260
|
+
if (markup.length) warn('approximation', 'Native MathML is reconstructed as measured SVG text, fraction rules and radical paths. Font-dependent glyph assemblies, rule metrics and local system math fonts may differ from the browser.')
|
|
261
|
+
const id = assetId(ctx)
|
|
262
|
+
const grade = warnings.some(warning => warning.grade === 'O') ? 'O' : warnings.length ? 'A' : 'E'
|
|
263
|
+
return {
|
|
264
|
+
asset: { id, kind: 'vector', mime: 'image/svg+xml', w, h, viewBox: [0, 0, w, h], src: '', data: null,
|
|
265
|
+
markup: `<svg xmlns="${NS}" width="${num(w)}" height="${num(h)}" viewBox="0 0 ${num(w)} ${num(h)}" overflow="visible">${markup.join('')}</svg>` },
|
|
266
|
+
fill: { type: 'image', asset: id, fit: 'stretch', crop: { x: 0, y: 0, w, h }, transform: [w, 0, 0, h, 0, 0], clipBox: 'border', opacity: 1, grade, notes: ['native-mathml-measured'] },
|
|
267
|
+
warnings,
|
|
268
|
+
fontStyles,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export default collectMathML
|