diff --git a/biome.json b/biome.json index f2cd2c39..008474d3 100644 --- a/biome.json +++ b/biome.json @@ -5,6 +5,9 @@ "tailwindDirectives": true } }, + "files": { + "includes": ["**", "!src/vendor"] + }, "formatter": { "indentStyle": "space", "lineWidth": 100, diff --git a/src/components/organisms/ExposureDetail.test.ts b/src/components/organisms/ExposureDetail.test.ts index ee1f2afb..ca99fa80 100644 --- a/src/components/organisms/ExposureDetail.test.ts +++ b/src/components/organisms/ExposureDetail.test.ts @@ -21,6 +21,13 @@ vi.mock('@/utils/analytics', () => ({ trackButtonClick: vi.fn(), })) +// Mock MathML transformer to avoid loading the vendored polyfill bundle in tests. +vi.mock('@/utils/mathTransformer', () => ({ + initMathPolyfills: vi.fn().mockResolvedValue(undefined), + transformMathString: (s: string) => s, + formatMathMLTable: (s: string) => s, +})) + describe('ExposureDetail', () => { let exposureStore: ReturnType let searchStore: ReturnType diff --git a/src/components/organisms/ExposureDetail.vue b/src/components/organisms/ExposureDetail.vue index 293609d3..55285e9d 100644 --- a/src/components/organisms/ExposureDetail.vue +++ b/src/components/organisms/ExposureDetail.vue @@ -25,6 +25,7 @@ import { downloadFileFromContent, downloadWorkspaceFile } from '@/utils/download import { getExposureIdFromResourcePath } from '@/utils/exposure' import { formatFileCount } from '@/utils/format' import { formatLicenseUrl } from '@/utils/license' +import { formatMathMLTable, initMathPolyfills, transformMathString } from '@/utils/mathTransformer' import { buildSearchQuery, isValidTerm } from '@/utils/search' import TermButton from '../atoms/TermButton.vue' @@ -247,6 +248,8 @@ const toggleCodeWrap = () => { const generateMath = async () => { error.value = null + await initMathPolyfills() + try { const response = await exposureStore.getExposureRawContent( exposureId.value, @@ -260,7 +263,11 @@ const generateMath = async () => { (value): value is [string, string[]] => Array.isArray(value[1]) && value[1].length > 0, ) : [] - mathsJSON.value = filteredMathsJSON + const transformedMathsJSON = filteredMathsJSON.map((entry): [string, string[]] => { + const mathMLArray = entry[1].map((mathML) => formatMathMLTable(transformMathString(mathML))) + return [entry[0], mathMLArray] + }) + mathsJSON.value = transformedMathsJSON } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to parse mathematics data.' error.value = { @@ -544,7 +551,7 @@ onMounted(async () => { class="mb-6 pb-6 last:mb-0 last:pb-0 border-b border-gray-200 dark:border-gray-700 last:border-0" >

{{ value[0] }}

-
+
@@ -912,10 +919,87 @@ onMounted(async () => { } .math-view { - @apply p-2 text-center text-sm overflow-auto; + @apply p-2 text-center text-base overflow-auto; + + & :deep(math > mtable) { + border-spacing: 0.5em 0.75em; + } + + & :deep(math mi), + & :deep(math mo), + & :deep(math mn) { + line-height: 1.4; + padding-left: 0.05em; + padding-right: 0.05em; + } + + & :deep(math mi) { + font-style: italic; + } + + /* Override sup and sub script text size for nested levels. */ + & :deep(math msub > msub > :nth-child(2)), + & :deep(math msup > :nth-child(2)) { + font-size: 0.7rem; + } + + & :deep(math msub > msub + mi), + & :deep(math msup msup > :nth-child(2)) { + font-size: 0.58rem; + } + + /* Keep first-level fraction content at the parent math font size. */ + & :deep(math mfrac > :first-child), + & :deep(math mfrac > :nth-child(2)) { + font-size: 1em; + } + + & :deep(math mfrac > :first-child) { + padding-bottom: 0.14em; + } + + & :deep(math mfrac > :nth-child(2)) { + padding-top: 0.14em; + } + + & :deep(math > mtable > mtr + mtr > mtd) { + padding-top: 0.5em; + } + + & :deep(math > mtable > mtr > mtd:nth-child(1)) { + display: flex; + justify-content: flex-end; + padding-right: 0.5em; + } + + & :deep(math > mtable > mtr > mtd[data-math-operator='equals']) { + text-align: center; + padding-left: 0.25em; + padding-right: 0.25em; + } + + & :deep(math > mtable > mtr > mtd:nth-child(3)) { + text-align: left; + padding-left: 0.5em; + } + + & :deep(mtable[data-math-piecewise='true']) { + border-spacing: 0.5em 0.35em; + } + + & :deep(mtable[data-math-piecewise='true'] > mtr > mtd[data-math-piecewise='expression']) { + white-space: nowrap; + } + + & :deep(mtable[data-math-piecewise='true'] > mtr > mtd[data-math-piecewise='keyword']) { + text-align: left; + white-space: nowrap; + padding-left: 0.15em; + padding-right: 0.35em; + } - & :deep(math) { - @apply flex flex-col gap-4; + & :deep(mtable[data-math-piecewise='true'] > mtr > mtd[data-math-piecewise='condition']) { + white-space: nowrap; } } diff --git a/src/utils/mathTransformer.test.ts b/src/utils/mathTransformer.test.ts new file mode 100644 index 00000000..fa15a8ed --- /dev/null +++ b/src/utils/mathTransformer.test.ts @@ -0,0 +1,424 @@ +import { describe, expect, it, vi } from 'vitest' +import { formatMathMLTable, initMathPolyfills, transformMathString } from '@/utils/mathTransformer' + +/** + * Tests for MathML transformation utilities. + * + * Key behaviours tested: + * 1. Polyfill initialisation with idempotency and style injection + * 2. MathML transformation applying polyfill transforms + * 3. Table formatting with proper mtable attributes + * 4. Mismatched fence pair removal (e.g., '(' paired with '>') + * 5. Non-DOM environment graceful degradation (returns input unchanged) + * 6. Equals operator detection and marking for styling + * 7. Structure preservation through transformations + */ + +// Mock the polyfill import. +vi.mock('../vendor/mathml-polyfills/all-polyfills-bundle.js', () => ({ + _MathTransforms: { + getCSSStyleSheet: () => { + const style = document.createElement('style') + style.textContent = '/* Mock polyfill CSS */' + return style + }, + transform: (container: HTMLElement) => { + // Mock transform: add a marker attribute to indicate it was called. + container.setAttribute('data-transformed', 'true') + }, + }, +})) + +describe('initMathPolyfills', () => { + it('injects the polyfill CSS into the document head', async () => { + // Remove any previously injected styles (from other tests). + for (const el of document.head.querySelectorAll('[data-math-polyfills="true"]')) { + el.remove() + } + + await initMathPolyfills() + + const style = document.head.querySelector('[data-math-polyfills="true"]') + expect(style).toBeTruthy() + expect(style?.tagName).toBe('STYLE') + }) + + it('does not throw when the document is unavailable', async () => { + const originalDocument = globalThis.document + // @ts-expect-error + delete globalThis.document + + await expect(initMathPolyfills()).resolves.toBeUndefined() + + globalThis.document = originalDocument + }) +}) + +describe('transformMathString', () => { + const simpleEquation = ` + + x + = + 5 + + ` + + it('transforms a MathML string and applies the polyfill transform', () => { + const result = transformMathString(simpleEquation) + + expect(result).toBeTruthy() + expect(result).toContain('x') + expect(result).toContain('=') + expect(result).toContain('5') + }) + + it('preserves the MathML structure after transformation', () => { + const result = transformMathString(simpleEquation) + + expect(result).toContain('math') + expect(result).toContain('mrow') + expect(result).toContain('mi') + }) + + it('returns the input unchanged when the document is unavailable', () => { + const originalDocument = globalThis.document + // @ts-expect-error + delete globalThis.document + + const result = transformMathString(simpleEquation) + expect(result).toBe(simpleEquation) + + globalThis.document = originalDocument + }) + + it('handles an empty MathML string', () => { + const result = transformMathString('') + expect(result).toBe('') + }) + + it('replaces invisible times separators with multiplication dots', () => { + const equationWithInvisibleTimes = `ab` + + const result = transformMathString(equationWithInvisibleTimes) + + expect(result).toContain('·') + expect(result).not.toContain('⁢') + expect(result).not.toContain('\u2062') + }) + + it('replaces the exponential glyph with plain e', () => { + const equationWithExponentialGlyph = `^x` + + const result = transformMathString(equationWithExponentialGlyph) + + expect(result).toContain('e') + expect(result).not.toContain('ⅇ') + }) +}) + +describe('formatMathMLTable', () => { + const multiRowEquation = ` + + vcell + = + 1000 + + + Ageo + = + 2 + + ` + + const equationWithFence = ` + + + + a + = + 1 + + + + ` + + // Test case for mismatched fence pairs (e.g., opening '(' but closing '>'). + const mismatchedFenceEquation = ` + + ( + x + > + + + result + = + 0 + + ` + + const piecewiseEquation = ` + + GKr + = + + + + GKr_b·1.3 + + if + + celltype=1 + + + + + GKr_b·0.8 + + if + + celltype=2 + + + + + GKr_b + + otherwise + + + + + ` + + it('wraps multiple rows in an mtable', () => { + const result = formatMathMLTable(multiRowEquation) + + expect(result).toContain('mtable') + expect(result).toContain('mtr') + expect(result).toContain('mtd') + }) + + it('sets the correct attributes on the mtable', () => { + const result = formatMathMLTable(multiRowEquation) + + expect(result).toContain('columnalign="right center left"') + expect(result).toContain('rowspacing="0.75em"') + }) + + it('marks equals operators with the data-math-operator attribute', () => { + const result = formatMathMLTable(multiRowEquation) + + expect(result).toContain('data-math-operator="equals"') + }) + + it('preserves the MathML structure in table cells', () => { + const result = formatMathMLTable(multiRowEquation) + + expect(result).toContain('vcell') + expect(result).toContain('Ageo') + expect(result).toContain('1,000') + }) + + it('formats large integer numerals with thousands separators', () => { + const numberFormattingEquation = ` + + 1000+2500000+999 + + ` + + const result = formatMathMLTable(numberFormattingEquation) + + expect(result).toContain('1,000') + expect(result).toContain('2,500,000') + expect(result).toContain('999') + }) + + it('handles MathML containing fenced expressions', () => { + const result = formatMathMLTable(equationWithFence) + + expect(result).toContain('mtable') + expect(result).toContain('mfenced') + }) + + it('returns the input unchanged when the document is unavailable', () => { + const originalDocument = globalThis.document + // @ts-expect-error + delete globalThis.document + + const result = formatMathMLTable(multiRowEquation) + expect(result).toBe(multiRowEquation) + + globalThis.document = originalDocument + }) + + it('handles an empty MathML string', () => { + const result = formatMathMLTable('') + expect(result).toBe('') + }) + + it('does not create a table when there are no mrow elements', () => { + const noRowsEquation = ` + x + ` + + const result = formatMathMLTable(noRowsEquation) + + expect(result).not.toContain('mtable') + }) + + it('removes a mismatched closing fence from an mrow element', () => { + const result = formatMathMLTable(mismatchedFenceEquation) + + // The mismatched closing fence '>' should be removed, leaving only the opening '('. + expect(result).toContain('mtable') + expect(result).toContain('result') + expect(result).toContain('data-math-operator="equals"') + }) + + it('normalises invisible times separators in formatted output', () => { + const equationWithInvisibleTimes = `a\u2062b` + + const result = formatMathMLTable(equationWithInvisibleTimes) + + expect(result).toContain('·') + expect(result).not.toContain('\u2062') + }) + + it('splits piecewise inner tables into aligned expression, keyword, and condition columns', () => { + const result = formatMathMLTable(piecewiseEquation) + + expect(result).toContain('data-math-piecewise="true"') + expect(result).toContain('data-math-piecewise="expression"') + expect(result).toContain('data-math-piecewise="keyword"') + expect(result).toContain('data-math-piecewise="condition"') + expect(result).toContain('data-math-piecewise-keyword="if"') + expect(result).toContain('data-math-piecewise-keyword="otherwise"') + expect(result).toContain('columnalign="right left left"') + }) + + it('converts underscore-delimited identifiers into nested subscripts', () => { + const underscoredIdentifierEquation = ` + + i_Stim_Amplitude + + ` + + const result = formatMathMLTable(underscoredIdentifierEquation) + + const subscriptMatches = result.match(/i') + expect(result).toContain('Stim') + expect(result).toContain('Amplitude') + }) + + it('replaces logical operator symbols with text labels', () => { + const logicalOperatorsEquation = ` + + ab + c + d + ¬e + + ` + + const result = formatMathMLTable(logicalOperatorsEquation) + + expect(result).toContain('and') + expect(result).toContain('or') + expect(result).toContain('xor') + expect(result).toContain('not') + expect(result).not.toContain('∧') + expect(result).not.toContain('∨') + expect(result).not.toContain('⊻') + expect(result).not.toContain('¬') + }) + + it('converts named Greek identifiers into Greek symbols', () => { + const greekIdentifiersEquation = ` + + alpha+beta+Gamma + + ` + + const result = formatMathMLTable(greekIdentifiersEquation) + + expect(result).toContain('α') + expect(result).toContain('β') + expect(result).toContain('γ') + expect(result).not.toContain('alpha') + expect(result).not.toContain('beta') + expect(result).not.toContain('Gamma') + }) + + it('converts scientific e-notation tokens into multiplication by 10 to an exponent', () => { + const scientificNotationEquation = ` + + 3.1 + e + 5 + + ` + + const result = formatMathMLTable(scientificNotationEquation) + + expect(result).toContain('3.1') + expect(result).toContain('·') + expect(result).toContain('105') + expect(result).not.toContain('e') + }) +}) + +describe('mathTransformer integration', () => { + it('transforms and formats a complete equation', () => { + const equation = ` + + v + = + + d + t + + + + a + = + + dv + dt + + + ` + + const transformed = transformMathString(equation) + const formatted = formatMathMLTable(transformed) + + expect(formatted).toContain('mtable') + expect(formatted).toContain('data-math-operator="equals"') + expect(formatted).toContain('mfrac') + }) + + it('handles complex nested MathML structures', () => { + const complexEquation = ` + + + + + time + period + + + + = + floor + + ` + + const result = formatMathMLTable(complexEquation) + + expect(result).toContain('mtable') + expect(result).toContain('mfrac') + expect(result).toContain('⌊') + expect(result).toContain('⌋') + }) +}) diff --git a/src/utils/mathTransformer.ts b/src/utils/mathTransformer.ts new file mode 100644 index 00000000..3bdd0495 --- /dev/null +++ b/src/utils/mathTransformer.ts @@ -0,0 +1,407 @@ +import { _MathTransforms as mathPolyfills } from '../vendor/mathml-polyfills/all-polyfills-bundle.js' +import { formatNumber } from './format' + +let sharedDOMParser: DOMParser | null = null + +const getDOMParser = (): DOMParser => { + if (!sharedDOMParser) { + sharedDOMParser = new DOMParser() + } + return sharedDOMParser +} + +let isMathPolyfillsInitialized = false +const MATH_POLYFILLS_STYLE_ATTR = 'data-math-polyfills' +const OPEN_TO_CLOSE_FENCE: Record = { + '(': ')', + '[': ']', + '{': '}', + '<': '>', + '⟨': '⟩', + '⌊': '⌋', + '⌈': '⌉', +} +const INVISIBLE_TIMES_CHAR = '\u2062' +const VISIBLE_MULTIPLICATION_DOT = '·' +const EXPONENTIAL_E_CHAR = 'ⅇ' +const PIECEWISE_KEYWORDS = new Set(['if', 'otherwise']) +const LOGICAL_OPERATOR_LABELS: Record = { + '∧': 'and', + '∨': 'or', + '⊻': 'xor', + '⊕': 'xor', + '¬': 'not', +} +const GREEK_IDENTIFIER_SYMBOLS: Record = { + alpha: 'α', + beta: 'β', + gamma: 'γ', + delta: 'δ', + epsilon: 'ε', + zeta: 'ζ', + eta: 'η', + theta: 'θ', + iota: 'ι', + kappa: 'κ', + lambda: 'λ', + mu: 'μ', + nu: 'ν', + xi: 'ξ', + omicron: 'ο', + pi: 'π', + rho: 'ρ', + sigma: 'σ', + tau: 'τ', + upsilon: 'υ', + phi: 'φ', + chi: 'χ', + psi: 'ψ', + omega: 'ω', +} + +const isFenceOperator = (element: Element): element is HTMLElement => + element.tagName.toLowerCase() === 'mo' && element.getAttribute('fence') === 'true' + +const getFenceText = (element: Element): string => (element.textContent || '').trim() + +const fixMismatchedFencePairs = (root: ParentNode) => { + const rows = root.querySelectorAll('mrow') + + rows.forEach((row) => { + const children = Array.from(row.children) + if (children.length < 2) return + + const firstFence = children.find(isFenceOperator) + const lastFence = [...children].reverse().find(isFenceOperator) + + if (!firstFence || !lastFence || firstFence === lastFence) return + + const openingFence = getFenceText(firstFence) + const closingFence = getFenceText(lastFence) + const expectedClosingFence = OPEN_TO_CLOSE_FENCE[openingFence] + + if (expectedClosingFence && closingFence && closingFence !== expectedClosingFence) { + // Remove only the mismatched trailing fence injected by polyfills. + lastFence.remove() + } + }) +} + +const normaliseInvisibleTimesSeparators = (mathml: string): string => + mathml + .replaceAll(INVISIBLE_TIMES_CHAR, VISIBLE_MULTIPLICATION_DOT) + .replaceAll(EXPONENTIAL_E_CHAR, 'e') + .replaceAll(/⁢|⁢|⁢/gi, VISIBLE_MULTIPLICATION_DOT) + +const copyElementAttributes = (from: Element, to: Element) => { + Array.from(from.attributes).forEach((attribute) => { + to.setAttribute(attribute.name, attribute.value) + }) +} + +const normaliseUnderscoreIdentifiers = (root: ParentNode) => { + const identifiers = Array.from(root.querySelectorAll('mi')) + + identifiers.forEach((identifier) => { + if (identifier.children.length > 0) return + + const rawText = (identifier.textContent || '').trim() + if (!rawText.includes('_')) return + + const parts = rawText + .split('_') + .map((part) => part.trim()) + .filter(Boolean) + if (parts.length < 2) return + + const doc = identifier.ownerDocument + const namespace = identifier.namespaceURI || 'http://www.w3.org/1998/Math/MathML' + const createIdentifier = (text: string) => { + const mi = doc.createElementNS(namespace, 'mi') + mi.textContent = text + return mi + } + + const baseIdentifier = createIdentifier(parts[0] || '') + copyElementAttributes(identifier, baseIdentifier) + + let current: Element = doc.createElementNS(namespace, 'msub') + current.appendChild(baseIdentifier) + current.appendChild(createIdentifier(parts[1] || '')) + + parts.slice(2).forEach((part) => { + const next = doc.createElementNS(namespace, 'msub') + next.appendChild(current) + next.appendChild(createIdentifier(part)) + current = next + }) + + identifier.replaceWith(current) + }) +} + +const normaliseLogicalOperators = (root: ParentNode) => { + const operators = Array.from(root.querySelectorAll('mo')) + + operators.forEach((operator) => { + if (operator.children.length > 0) return + + const normalisedText = (operator.textContent || '').trim() + const replacement = LOGICAL_OPERATOR_LABELS[normalisedText] + if (!replacement) return + + operator.textContent = replacement + }) +} + +const normaliseNumericLiterals = (root: ParentNode) => { + const numerals = Array.from(root.querySelectorAll('mn')) + + numerals.forEach((numeral) => { + if (numeral.children.length > 0) return + + const rawText = (numeral.textContent || '').trim() + if (!/^-?\d+$/.test(rawText)) return + + const parsed = Number(rawText) + if (!Number.isInteger(parsed) || Math.abs(parsed) < 1000) return + + numeral.textContent = formatNumber(parsed) + }) +} + +const isNumberToken = (value: string): boolean => /^[-+]?\d*\.?\d+$/.test(value) +const isIntegerToken = (value: string): boolean => /^[-+]?\d+$/.test(value) + +const normaliseScientificENotation = (root: ParentNode) => { + const rows = Array.from(root.querySelectorAll('mrow')) + + rows.forEach((row) => { + const children = Array.from(row.children) + if (children.length < 3) return + + for (let i = 0; i <= children.length - 3; i += 1) { + const baseNode = children[i] + const eNode = children[i + 1] + const exponentNode = children[i + 2] + if (!baseNode || !eNode || !exponentNode) continue + + if ( + baseNode.tagName.toLowerCase() !== 'mn' || + eNode.tagName.toLowerCase() !== 'mo' || + exponentNode.tagName.toLowerCase() !== 'mn' + ) { + continue + } + + const baseText = (baseNode.textContent || '').trim() + const eText = (eNode.textContent || '').trim().toLowerCase() + const exponentText = (exponentNode.textContent || '').trim() + if (eText !== 'e' || !isNumberToken(baseText) || !isIntegerToken(exponentText)) { + continue + } + + const doc = row.ownerDocument + const namespace = row.namespaceURI || 'http://www.w3.org/1998/Math/MathML' + const multiplication = doc.createElementNS(namespace, 'mo') + multiplication.textContent = VISIBLE_MULTIPLICATION_DOT + + const msup = doc.createElementNS(namespace, 'msup') + const ten = doc.createElementNS(namespace, 'mn') + ten.textContent = '10' + const exponent = doc.createElementNS(namespace, 'mn') + exponent.textContent = exponentText + msup.appendChild(ten) + msup.appendChild(exponent) + + const afterExponent = exponentNode.nextSibling + row.insertBefore(multiplication, eNode) + row.insertBefore(msup, afterExponent) + row.removeChild(eNode) + row.removeChild(exponentNode) + } + }) +} + +const normaliseNamedGreekIdentifiers = (root: ParentNode) => { + const identifiers = Array.from(root.querySelectorAll('mi')) + + identifiers.forEach((identifier) => { + if (identifier.children.length > 0) return + + const rawText = (identifier.textContent || '').trim() + if (!rawText) return + + const replacement = GREEK_IDENTIFIER_SYMBOLS[rawText.toLowerCase()] + if (!replacement) return + + identifier.textContent = replacement + }) +} + +const isMathMLElement = (node: Node, tagName: string): node is Element => + node.nodeType === Node.ELEMENT_NODE && (node as Element).tagName.toLowerCase() === tagName + +const normalisePiecewiseTables = (root: ParentNode, doc: Document, namespace: string) => { + const mathTables = Array.from(root.querySelectorAll('mtable')) + + mathTables.forEach((table) => { + const rows = Array.from(table.children).filter((child) => child.tagName.toLowerCase() === 'mtr') + if (!rows.length) return + + let hasPiecewiseRows = false + + rows.forEach((row) => { + const rowCells = Array.from(row.children).filter( + (child) => child.tagName.toLowerCase() === 'mtd', + ) + if (rowCells.length !== 1) return + + const [singleCell] = rowCells + if (!singleCell) return + + const cellNodes = Array.from(singleCell.childNodes) + const keywordIndex = cellNodes.findIndex( + (node) => + isMathMLElement(node, 'mtext') && + PIECEWISE_KEYWORDS.has((node.textContent || '').trim().toLowerCase()), + ) + + if (keywordIndex <= 0) return + + hasPiecewiseRows = true + + const expressionCell = doc.createElementNS(namespace, 'mtd') + expressionCell.setAttribute('data-math-piecewise', 'expression') + + cellNodes.slice(0, keywordIndex).forEach((node) => { + expressionCell.appendChild(node) + }) + + const keywordCell = doc.createElementNS(namespace, 'mtd') + keywordCell.setAttribute('data-math-piecewise', 'keyword') + + const keywordNode = cellNodes[keywordIndex] + const keywordText = (keywordNode?.textContent || '').trim().toLowerCase() + if (keywordText) { + keywordCell.setAttribute('data-math-piecewise-keyword', keywordText) + } + if (keywordNode) { + keywordCell.appendChild(keywordNode) + } + + const conditionCell = doc.createElementNS(namespace, 'mtd') + conditionCell.setAttribute('data-math-piecewise', 'condition') + cellNodes.slice(keywordIndex + 1).forEach((node) => { + conditionCell.appendChild(node) + }) + + while (row.firstChild) { + row.removeChild(row.firstChild) + } + + row.appendChild(expressionCell) + row.appendChild(keywordCell) + row.appendChild(conditionCell) + }) + + if (hasPiecewiseRows) { + table.setAttribute('data-math-piecewise', 'true') + table.setAttribute('columnalign', 'right left left') + } + }) +} + +/** + * Injects the necessary CSS for polyfills into the document head. + */ +export async function initMathPolyfills() { + if (typeof document === 'undefined' || isMathPolyfillsInitialized) return + if (!mathPolyfills) return + + const existingStyle = document.head.querySelector(`[${MATH_POLYFILLS_STYLE_ATTR}="true"]`) + if (existingStyle) { + isMathPolyfillsInitialized = true + return + } + + const style = mathPolyfills.getCSSStyleSheet() + style.setAttribute(MATH_POLYFILLS_STYLE_ATTR, 'true') + document.head.appendChild(style) + isMathPolyfillsInitialized = true +} + +/** + * Transforms a raw MathML string from an API into modern MathML Core. + * @param rawMathML The string containing legacy tags. + */ +export function transformMathString(rawMathML: string): string { + if (typeof document === 'undefined') return rawMathML + + const container = document.createElement('div') + container.innerHTML = rawMathML + + mathPolyfills?.transform(container) + + return normaliseInvisibleTimesSeparators(container.innerHTML) +} + +/** + * Formats a MathML string into a table structure for better rendering. + * @param rawMathML The string containing updated tags. + * @returns The formatted MathML string. + */ +export const formatMathMLTable = (rawMathML: string): string => { + if (typeof document === 'undefined') return rawMathML + + const parser = getDOMParser() + const doc = parser.parseFromString(rawMathML, 'text/html') + const mathBlocks = doc.querySelectorAll('math') + const NS = 'http://www.w3.org/1998/Math/MathML' + + mathBlocks.forEach((math) => { + fixMismatchedFencePairs(math) + normaliseUnderscoreIdentifiers(math) + normaliseLogicalOperators(math) + normaliseNumericLiterals(math) + normaliseScientificENotation(math) + normaliseNamedGreekIdentifiers(math) + + const rows = Array.from(math.children).filter((child) => child.tagName.toLowerCase() === 'mrow') + + if (rows.length) { + const mtable = doc.createElementNS(NS, 'mtable') + mtable.setAttribute('columnalign', 'right center left') + mtable.setAttribute('rowspacing', '0.75em') + + rows.forEach((row) => { + const mtr = doc.createElementNS(NS, 'mtr') + + Array.from(row.childNodes).forEach((node) => { + const mtd = doc.createElementNS(NS, 'mtd') + + if ( + node.nodeType === Node.ELEMENT_NODE && + (node as Element).tagName.toLowerCase() === 'mo' && + ((node.textContent || '').trim() === '=' || + (node as Element).getAttribute('form') === 'infix') + ) { + mtd.setAttribute('data-math-operator', 'equals') + } + + mtd.appendChild(node) + mtr.appendChild(mtd) + }) + + mtable.appendChild(mtr) + row.remove() + }) + + math.appendChild(mtable) + } + + normalisePiecewiseTables(math, doc, NS) + }) + + return normaliseInvisibleTimesSeparators(doc.body.innerHTML) +} diff --git a/src/vendor/mathml-polyfills/README.md b/src/vendor/mathml-polyfills/README.md new file mode 100644 index 00000000..3fbf8783 --- /dev/null +++ b/src/vendor/mathml-polyfills/README.md @@ -0,0 +1,27 @@ +# MathML polyfills + +This directory contains vendored MathML polyfill bundle files. + +## Source + +- Upstream repository: https://github.com/w3c/mathml-polyfills +- Fork used to generate the bundle in this project: https://github.com/akhuoa/mathml-polyfills/tree/rolldown + +The upstream `w3c/mathml-polyfills` repository provides polyfills for MathML features using MathML Core and browser-native web technologies. For this project, the files here were generated from the `rolldown` branch in the fork above so the polyfills can be imported locally as an ESM bundle. + +## Vendored files + +- `all-polyfills-bundle.js`: minified ESM bundle +- `all-polyfills-bundle.d.ts`: generated TypeScript declarations for the bundle + +## Update process + +To refresh these vendored files: + +1. Clone or open `https://github.com/akhuoa/mathml-polyfills`. +2. Check out the `rolldown` branch. +3. Install dependencies. +4. Run `npm run build`. +5. Copy `dist/all-polyfills-bundle.js` and `dist/all-polyfills-bundle.d.ts` into this directory. + +If the fork, branch, or build pipeline changes, update this README so future maintainers can trace where these files came from and how they were produced. diff --git a/src/vendor/mathml-polyfills/all-polyfills-bundle.d.ts b/src/vendor/mathml-polyfills/all-polyfills-bundle.d.ts new file mode 100644 index 00000000..ed00aa19 --- /dev/null +++ b/src/vendor/mathml-polyfills/all-polyfills-bundle.d.ts @@ -0,0 +1,1202 @@ +/** +* Same as cloneNode(true) except that shadow roots are copied +* If you are using the transforms and you need to clone a node that potentially has a shadowRoot, use this so the shadowRoot is copied +* As of November, 2020, Elementary Math and Linebreaking transforms are the only transforms that have shadowRoots. +* @param {Element} el +* @param {Element} [clone] +* @returns {Element} -- the clone (only useful if function is called with one arg) +*/ +function cloneElementWithShadowRoot(el, clone) { + if (clone === void 0) clone = el.cloneNode(true); + if (el.shadowRoot) { + let shadowRoot = clone.attachShadow({ + mode: "open" + }); + shadowRoot.appendChild(_MathTransforms.getCSSStyleSheet()); + for (let i = 0; i < el.shadowRoot.childElementCount; i++) shadowRoot.appendChild(cloneElementWithShadowRoot(el.shadowRoot.children[i])); + } + for (let i = 0; i < el.childElementCount; i++) cloneElementWithShadowRoot(el.children[i], clone.children[i]); + return clone; +} +/** +* Converts a CSS length unit to pixels and returns that as a number +* @param{Element} element +* @param {string} length +* @returns {number} +*/ +function convertToPx(element, length) { + if (/px/.test(length)) return parseFloat(length); + let img = document.createElement("img"); + let leafWrapper = document.createElementNS(MATHML_NS, "mtext"); + leafWrapper.appendChild(img); + leafWrapper.style.overflow = "hidden"; + leafWrapper.style.visibility = "hidden"; + img.style.width = length; + element.appendChild(leafWrapper); + const result = leafWrapper.getBoundingClientRect().width; + leafWrapper.remove(); + return result; +} + +//#endregion +//#region mglyph/mglyph.js +/*** +* Convert mglyph into img element. +* This conversion should be valid everwhere mglyph is legal. +***/ +/** +* @param {HTMLElement} el +*/ + +function collapseWhiteSpace$1(text) { + return text.replace(/^[\s]+|[\s]+$/g, "").replace(/[\s]+/g, " "); +} +function newOperator(text, separator) { + let operator = document.createElementNS(namespaceURI$1, "mo"); + operator.appendChild(document.createTextNode(text)); + operator.setAttribute(separator ? "separator" : "fence", "true"); + return operator; +} +function newMrow() { + return document.createElementNS(namespaceURI$1, "mrow"); +} +function getSeparatorList(text) { + if (text === null) return [","]; + let separatorList = []; + for (let i = 0; i < text.length; i++) if (!/\s/g.test(text.charAt(i))) { + let c = text.charCodeAt(i); + if (c >= 55296 && c < 56320 && i + 1 < text.length) { + separatorList.push(text.substr(i, 2)); + i++; + } else separatorList.push(text.charAt(i)); + } + return separatorList; +} +function shouldCopyAttribute(attribute) { + return attribute.namespaceURI || !["dir", "open", "close", "separators"].includes(attribute.localName); +} +/** +* +* @param {HTMLElement} el +* @returns {boolean} +*/ +function isSpaceLike(el) { + if (el.tagName === "mtext" || el.tagName === "mspace" || el.tagName === "maligngroup" || el.tagName === "malignmark") return true; + if (MRowLikeElements.includes(el.tagName)) { + for (let i = 0; i < el.children.length; i++) if (!isSpaceLike(el.children[i])) return false; + if (el.tagName === "maction" && el.hasAttribute("selection") && isSpaceLike(el.children[el.elAttribute("selection")])) return true; + return true; + } + return false; +} +/** +* +* @param {HTMLElement} el +* @returns {[HTMLElement|null]} +*/ +function getEmbellishedOperator$1(el) { + if (el.tagName === "mo") return el; + if (EmbellishedOpsElements.includes(el.tagName)) return getEmbellishedOperator$1(el.firstChild); + if (MRowLikeElements.includes(el.tagName)) { + for (let i = 0; i < el.children.length; i++) if (!isSpaceLike(el.children[i])) return getEmbellishedOperator$1(el.children[i]); + return null; + } + return null; +} +/** +* +* @param {HTMLElement} child +* @param {string} attrName +*/ +function setAccentValue(child, attrName) { + const op = getEmbellishedOperator$1(child); + if (op === null) return; + let accentVal = op.getAttribute("accent"); + if (accentVal === null && child.tagName === "mo") accentVal = "true"; + if (accentVal !== null) child.parentElement.setAttribute(attrName, accentVal); +} +/** +* +* @param {HTMLElement} el +*/ + +//#endregion +//#region horiz-align/horiz-align.js +/*** +* Handles the "numalign" and "denomalign" attributes on mfrac +* Handles the "align" attribute on munder, mover, and munderover +***/ +/** +* @param {HTMLElement} child +* @returns {number} +*/ +function getChildWidth(child) { + return child.getBoundingClientRect().width; +} +/** +* Handles left/right alignment by creating an mspace of the appropriate width +* on either the left or right side. +* For something like a fraction numerator, 'child' is the numerator and 'maxWidth' +* is the denominator's width +* @param {HTMLElement} child +* @param {number} childWidth +* @param {string} align +* @param {number} maxWidth +* @returns {HTMLElement} +*/ +function doAlignment(child, childWidth, align, maxWidth) { + if (childWidth >= maxWidth || align === "center") return child; + if (child.tagName !== "mrow") { + const sibling = child.nextElementSibling; + const mrow = document.createElementNS(MATHML_NS, "mrow"); + const parent = child.parentElement; + mrow.appendChild(child); + parent.insertBefore(mrow, sibling); + child = mrow; + } + let mspace = document.createElementNS(MATHML_NS, "mspace"); + mspace.setAttribute("width", `${(maxWidth - childWidth).toPrecision(2)}px`); + if (align === "left") child.appendChild(mspace);else if (align === "right") child.insertBefore(mspace, child.firstElementChild); + return child; +} +/** +* @param {HTMLElement} el +* @param {number} iChild +* @param {number} iOther +* @param {string} attr +* @returns {HTMLElement} +*/ +function alignChild(el, iChild, iOther, attr) { + doAlignment(el.children[iChild], getChildWidth(el.children[iChild]), el.getAttribute(attr), getChildWidth(el.children[iOther])); + return el; +} +/** +* @param {HTMLElement} mfrac +*/ + +function getWidthOf(mathmlStr) { + const math = document.createElementNS(MATHML_NS, "math"); + math.innerHTML = mathmlStr; + document.body.appendChild(math); + const width = math.getBoundingClientRect().width; + document.body.lastElementChild.remove(); + return width; +} +/** +* +* @param {string} notationAttrValue +* @returns {boolean} -- true if the transform should be used +*/ +function useMencloseTransform(notationAttrValue) { + if (getWidthOf("x") === getWidthOf("x")) return true; + if (/arrow/.test(notationAttrValue)) { + if (getWidthOf("x") === getWidthOf("x")) return true; + } + if (/phasorangle/.test(notationAttrValue)) { + if (getWidthOf("x") === getWidthOf("x")) return true; + } + return false; +} +/** +* +* @param {string[]} notationArray +* @returns {string[]} notationArray +*/ +function removeRedundantNotations(notationArray) { + notationArray = Array.from(new Set(notationArray)); + if (notationArray.includes("box")) notationArray = notationArray.filter(notation => !BORDER_NOTATIONS.includes(notation)); + for (const [notation, values] of Object.entries(ARROW_INFO)) { + const removeArray = values[3]; + if (removeArray !== [""] && notationArray.includes(notation)) notationArray = notationArray.filter(notation => !removeArray.includes(notation)); + } + return notationArray; +} +/** +* +* @param {HTMLElement} element +* @returns {boolean} +*/ +function isDirLTR(element) { + let lookingForMathElement = true; + do { + if (element.hasAttribute("dir")) return element.getAttribute("dir") === "ltr"; + lookingForMathElement = element.tagName !== "math"; + element = element.parentElement; + } while (lookingForMathElement); + return true; +} +/** +* +* @param {HTMLElement} el +* @param {string[]} notationArray +* @returns {number} -- amount of padding in pixels +*/ +function padAmount(el, notationArray) { + let padding = "0.467em"; + if (notationArray.includes("roundedbox") || notationArray.includes("circle")) padding = "0.601em"; + return convertToPx(el, padding); +} +/** +* +* @param {HTMLElement} el // menclose element +* @returns +*/ + +//#endregion +//#region ms/ms.js +/*** +* Handles lqoute and rquote attrs on ms +***/ +/** +* +* @param {string} text +*/ +function collapseWhiteSpace(text) { + return text.replace(/^[\s]+|[\s]+$/g, "").replace(/[\s]+/g, " "); +} +/** +* @param {HTMLElement} ms +*/ + +function getMathAlphanumeric(ch, mathStyle) { + if (!mathStyle || mathStyle == "mup") return ch; + let code = ch.charCodeAt(0); + let n; + if (ch >= "0" && ch <= "9") { + code += 120734; + n = setsDigit.indexOf(mathStyle); + return n != -1 ? String.fromCodePoint(code + n * 10) : ch; + } + if (/[A-Za-z]/.test(ch)) { + let varsel = ""; + if (mathStyle == "mchan" || mathStyle == "mrhnd") { + varsel = mathStyle == "mchan" ? "︀" : "︁"; + mathStyle = "mscr"; + } + let chT = ""; + switch (mathStyle) { + case "mit": + if (ch == "h") return "ℎ"; + break; + case "mfrak": + chT = letterlikeFraktur[ch]; + break; + case "mscr": + chT = letterlikeScript[ch]; + break; + case "Bbb": + chT = letterlikeDoubleStruck[ch]; + break; + } + if (chT) return chT + varsel; + n = setsEn.indexOf(mathStyle); + if (n == -1) return ch; + code -= 65; + if (code > 26) code -= 6; + return String.fromCodePoint(code + 52 * n + 119808) + varsel; + } + if (ch >= "Α" && ch <= "ϵ" || ch == "∂" || ch == "∇") { + if (mathStyle == "mbf") { + if (ch == "Ϝ") return "𝟊"; + if (ch == "ϝ") return "𝟋"; + } + n = setsGr.indexOf(mathStyle); + if (n == -1) return ch; + let code0 = offsetsGr[ch]; + if (code0) code = code0;else { + code -= 913; + if (code > 25) code -= 6; + } + return String.fromCodePoint(code + 58 * n + 120488); + } + if (code < 1575) return ch == "ı" ? "𝚤" : ch == "ȷ" ? "𝚥" : ch; + if (code > 1722) return ch; + n = setsAr.indexOf(mathStyle); + if (n == -1) return ch; + if (code <= 1610) { + code = abjad[code - 1575]; + if (code == -1) return ch; + } else { + code = dottedChars.indexOf(ch); + if (code == -1) return ch; + code += 28; + } + if (mathStyle == "misol") { + if (code == 4) n = 1; + } else if (1 << code & missingCharMask[n - 1]) return ch; + return String.fromCodePoint(32 * n + code + 126464); +} +//#endregion +//#region mpadded/mpadded.js +/*** +* Handles width/height/depth attributes with % values for mpadded +***/ +/** +* @param {HTMLElement} mpadded +* @returns {{width:number, height: number: depth: number}} +*/ +function getDimensions(mpadded) { + const mrow = document.createElementNS(MATHML_NS, "mrow"); + mrow.appendChild(document.createElementNS(MATHML_NS, "mspace")); + const cloneMpadded = cloneElementWithShadowRoot(mpadded); + for (let i = 0; i < cloneMpadded.children.length; i++) mrow.appendChild(cloneMpadded.children[i]); + cloneMpadded.appendChild(mrow); + mpadded.parentElement.replaceChild(cloneMpadded, mpadded); + const mspaceRect = mrow.firstElementChild.getBoundingClientRect(); + const mpaddedRect = mrow.getBoundingClientRect(); + cloneMpadded.parentElement.replaceChild(mpadded, cloneMpadded); + return { + width: mpaddedRect.width, + height: mspaceRect.y - mpaddedRect.top, + depth: mpaddedRect.bottom - mspaceRect.y + }; +} +/** +* @param {HTMLElement} el +* @param {string} attr +* @param {'width'|'height'|'depth'} dimension +* @param {{width:number, height: number: depth: number}} dimensions +* @returns {boolean} +*/ +function replacePseudoAttr(el, attr, dimension, dimensions) { + const attrValue = el.getAttribute(attr).toLowerCase(); + if (attrValue.includes(dimension)) { + const floatVal = parseFloat(attrValue) * dimensions[dimension] / (attrValue.includes("%") ? 100 : 1); + el.setAttribute(attr, floatVal.toFixed(1) + "px"); + return true; + } + return false; +} +/** +* @param {HTMLElement} el +* @param {attr} align +* @param {{width:number, height: number: depth: number}} dimensions +* @returns {boolean} // true if handled +*/ +function handleAttr(el, attr, dimensions) { + if (!el.hasAttribute(attr)) return false; + if (replacePseudoAttr(el, attr, "width", dimensions)) return true; + if (replacePseudoAttr(el, attr, "height", dimensions)) return true; + if (replacePseudoAttr(el, attr, "depth", dimensions)) return true; + return false; +} +/** +* @param {HTMLElement} el +*/ + +/** +* +* @param {HTMLElement} mtable +*/ +function makeTableSquare(mtable) { + return mtable; +} +/** +* +* @param {HTMLElement} mtable +*/ +function handleLabeledRows(mtable) { + if (mtable.getElementsByTagName("mlabeledtr").length === 0) return mtable; + const side = mtable.getAttribute("side") || "right"; + let emptyColumnEntry = document.createElementNS(namespaceURI, "mtd"); + emptyColumnEntry.setAttribute("intent", ":no-equation-label"); + for (let i = 0; i < mtable.children.length; i++) { + let row = mtable.children[i]; + if (row.tagName === "mlabeledtr") { + let label = row.firstElementChild; + addIntent(label); + let newRow = document.createElementNS(namespaceURI, "mtr"); + for (const attr of row.attributes) newRow.setAttribute(attr.name, attr.value); + let mtd = row.children[side == "left" ? 0 : 1]; + newRow.appendChild(mtd); + while (row.children.length > 0) newRow.appendChild(row.firstChild); + if (side === "right") newRow.appendChild(label); + row.replaceWith(newRow); + } else { + const newColEntry = emptyColumnEntry.cloneNode(); + if (side === "right") row.appendChild(newColEntry);else row.insertBefore(newColEntry, row.firstElementChild); + } + } + return mtable; +} +/** +* +* @param {HTMLElement} mtd +*/ +function addIntent(mtd) { + if (!mtd.hasAttribute("intent")) { + mtd.setAttribute("intent", ":equation-label"); + return; + } + let intentValue = mtd.getAttribute("intent"); + let iOpenParen = intentValue.indexOf("("); + let head = iOpenParen == -1 ? intentValue : intentValue.substring(0, iOpenParen); + if (head.includes(":equation-label")) return; + intentValue = head + ":equation-label" + intentValue.substring(head.length); + mtd.setAttribute("intent", intentValue); +} +/** +* +* @param {HTMLElement} mtable +*/ + +/** +* Look first in the shadowRoot for the 'id'; if not found, check the whole document +* @param {string} id +* @returns {Element | null} +*/ +function getElementByIdEverywhere(id) { + const found = shadowRoot.get().getElementById(id); + if (found) return found; + return document.getElementById(id); +} +/** +* Creates a new MathML element +* @param {string} tagName +* @returns {Element} +*/ +function newElement(tagName) { + return document.createElementNS(MATHML_NS, tagName); +} +/** +* Copies the attributes from 'source' to 'target' +* 'target' is unchanged. +* @param {Element} target +* @param {Element} source +* @returns {Element} // target +*/ +function copyAttributes(target, source) { + const attrs = source.attributes; + for (let i = 0; i < attrs.length; i++) target.setAttribute(attrs[i].name, attrs[i].value); + return target; +} +/** +* Looks at 'element' and its ancestors to see if the value is set on an attr; if so, it is returned. +* @param {Element} element +* @param {string} attrName +* @param {string} defaultVal +* @returns {string} +*/ +function getMathMLAttrValueAsString(element, attrName, defaultVal) { + let lookingForMathElement = true; + do { + if (element.hasAttribute(attrName)) return element.getAttribute(attrName); + lookingForMathElement = element.tagName !== "math"; + element = element.parentElement; + } while (lookingForMathElement); + return defaultVal; +} +/** +* @returns {Element} +*/ +function createLineBreakMTable() { + const mtable = newElement("mtable"); + mtable.setAttribute(MTABLE_HAS_LINEBREAKS, "true"); + mtable.setAttribute("displaystyle", "true"); + return mtable; +} +/** +* +* @param {Element} mtd +* @returns {boolean} +*/ +function isInLineBreakTable(mtd) { + return mtd.tagName === "mtd" && mtd.parentElement.tagName === "mtr" && mtd.parentElement.parentElement.tagName === "mtable" && mtd.parentElement.parentElement.hasAttribute(MTABLE_HAS_LINEBREAKS); +} +/** +* +* @param {Element} child +* @returns {Element} +*/ +function createNewTableRowWithChild(child) { + const mtr = newElement("mtr"); + const mtd = newElement("mtd"); + mtd.appendChild(child); + mtr.appendChild(mtd); + return mtr; +} +/** +* +* @param {Element} mo +* @param {'first' | 'middle' | 'last'} firstMiddleOrLast +* @returns {Object} +*/ +function computeIndentAttrObject(mo, firstMiddleOrLast) { + const attrObject = {}; + let linebreakstyle = getMathMLAttrValueAsString(mo, "linebreakstyle", "before"); + if (linebreakstyle === "infixLineBreakStyle") linebreakstyle = getMathMLAttrValueAsString(mo, "infixLineBreakStyle", "before"); + attrObject.linebreakstyle = linebreakstyle; + attrObject.indentAlign = getMathMLAttrValueAsString(mo, "indentalign", "auto"); + attrObject.indentShift = getMathMLAttrValueAsString(mo, "indentshift", "0px"); + if (firstMiddleOrLast == "first") { + attrObject.indentAlign = getMathMLAttrValueAsString(mo, "indentalignfirst", attrObject.indentAlign); + attrObject.indentShift = getMathMLAttrValueAsString(mo, "indentshiftfirst", attrObject.indentShift); + } else if (firstMiddleOrLast === "last") { + attrObject.indentAlign = getMathMLAttrValueAsString(mo, "indentalignlast", attrObject.indentAlign); + attrObject.indentShift = getMathMLAttrValueAsString(mo, "indentshiftlast", attrObject.indentShift); + } + attrObject.indentShift = convertToPx(mo, attrObject.indentShift); + attrObject.target = getMathMLAttrValueAsString(mo, "indenttarget", ""); + attrObject.firstMiddleOrLast = firstMiddleOrLast; + return attrObject; +} +/** +* Stores the attrs used for indenting on the 'mtd' so they can be found easily later +* @param {Element} mtd +* @param {Element} mo +*/ +function storeLineBreakAttrsOnMtd(mtd, mo) { + /** @type {'first' | 'middle' | 'last'} */ + let firstMiddleOrLast = "middle"; + if (mtd.parentElement === mtd.parentElement.parentElement.firstElementChild) firstMiddleOrLast = "first";else if (mtd.parentElement === mtd.parentElement.parentElement.lastElementChild) firstMiddleOrLast = "last"; + mtd.setAttribute(INDENT_ATTRS, JSON.stringify(computeIndentAttrObject(mo, firstMiddleOrLast))); +} +/** +* Either create a new (linebreak) mtable with the new table row or if it already exists, add the row +* It exists if we stopped at a and it is an mtable inserted for linebreaking purposes +* @param {Element} parent // 'mtd' if stopped in existing table, otherwise some non-mrow element +* @param {Element} upToBreak // the first part of the split line +* @param {Element} afterBreak // the remainder of the current line +* @returns {Element} // the last row added to the table (one or two rows are created) +*/ +function addNewLineBreakRow(parent, upToBreak, afterBreak) { + const mtr = createNewTableRowWithChild(upToBreak); + if (isInLineBreakTable(parent)) { + copyAttributes(mtr.firstElementChild, parent); + while (parent.attributes.length > 0) parent.removeAttributeNode(parent.attributes[0]); + parent.parentElement.parentElement.insertBefore(mtr, parent.parentElement); + return parent.parentElement; + } else { + const mtable = createLineBreakMTable(); + mtable.setAttribute("style", "width: 100%"); + mtable.appendChild(mtr); + afterBreak.replaceWith(mtable); + mtable.appendChild(createNewTableRowWithChild(afterBreak)); + return mtable.lastElementChild; + } +} +/** +* Splits the line at the 'mo' -- at beginning/end of line depending on 'linebreakstyle' +* @param {Element} mo // operator to split +* @returns {Element} // the last row added to the table +*/ +function splitLine(mo) { + let linebreakstyle = getMathMLAttrValueAsString(mo, "linebreakstyle", "before"); + if (linebreakstyle === "infixLineBreakStyle") linebreakstyle = getMathMLAttrValueAsString(mo, "infixLineBreakStyle", "before"); + let upToBreak = null; + let breakElement = mo; + if (mo.previousElementSibling !== null && mo.nextElementSibling !== null) mo.setAttribute("form", "infix"); + let parent = breakElement.parentElement; + for (; parent.tagName === "mrow"; parent = parent.parentElement) { + let newMRow = newElement("mrow"); + while (parent.firstElementChild) { + const child = parent.firstElementChild; + if (child === breakElement) { + if (linebreakstyle === "after") { + newMRow.appendChild(child); + linebreakstyle = "before"; + break; + } else if (linebreakstyle === "duplicate") { + linebreakstyle = "before"; + newMRow.appendChild(child.cloneNode(true)); + } + break; + } + newMRow.appendChild(child); + } + breakElement = parent; + if (upToBreak) newMRow.appendChild(upToBreak); + upToBreak = newMRow.children.length === 1 ? newMRow.firstElementChild : newMRow; + } + if (breakElement.tagName === "mrow" && breakElement.children.length === 1) { + const newBreakElement = breakElement.firstElementChild; + breakElement.replaceWith(newBreakElement); + breakElement = newBreakElement; + } + return addNewLineBreakRow(parent, upToBreak, breakElement); +} +/** +* +* @param {Element} mo +* @returns {Element} +*/ +function computeLineBreakRoot(mo) { + let mrow = mo; + let parent = mo.parentElement; + while (parent.tagName === "mrow" || parent.tagName === "mstyle" || parent.tagName === "mpadded") { + mrow = parent; + parent = parent.parentElement; + } + return mrow; +} +/** +* Finds all the forced linebreaks, splits the lines, and stores the indent info on the 'mtd' +* @param {Element} math +*/ +function splitIntoLinesAtForcedBreaks(math, maxLineWidth) { + const forcedBreakElements = math.querySelectorAll("mo[linebreak=\"newline\"]"); + if (forcedBreakElements.length === 0) return; + /** @type {Element} */ + let lastRow = null; + forcedBreakElements.forEach(mo => { + lastRow = splitLine(mo); + }); + const tableChildren = lastRow.parentElement.children; + storeLineBreakAttrsOnMtd(tableChildren[0].firstElementChild, tableChildren[0].firstElementChild); + for (let i = 0; i < forcedBreakElements.length; i++) storeLineBreakAttrsOnMtd(tableChildren[i + 1].firstElementChild, forcedBreakElements[i]); +} +/** +* Returns true if first line of math +* @param {Element} mtr +* returns {boolean} +*/ +function isFirstRow(mtr) { + return mtr === mtr.parentElement.firstElementChild; +} +/** +* Find the leftMostChild not counting an mspace +* @param {Element} element +*/ +function leftMostChild(element) { + while (element.children.length > 0) element = element.firstElementChild; + return element.tagName == "mspace" ? element.nextElementSibling : element; +} +function isMatchLessThanHalfWay(xStart, indent, maxWidth) { + return indent - xStart <= .5 * maxWidth; +} +/** +* Return the operators that match 'char'. For the match, "+"/"-" match each other +* @param {Element[]} operators +* @param {string} char +* @returns {Element[]} +*/ +function filterOnCharMatch(operators, char) { + if (char === "-") char = "+"; + return operators.filter(function (operator) { + let opChar = operator.textContent.trim(); + if (opChar === "-") opChar = "+"; + return char === opChar; + }); +} +/** +* Look through all the previous lines and find a good indent +* Potential breakpoints are those 'mo's at the same depth as the 'mo' that starts the current line +* Preference is given to an 'mo' with the same char (i.e, if we have a '+', find another '+' at the same depth). +* Of those 'mo' that match, the one with the minimum amount of indent is chosen so that more fits on that line. +* @param {Element} mtd +* @returns {number} +*/ +function computeAutoShiftAmount(mtd) { + if (isFirstRow(mtd.parentElement)) return 0; + const mo = leftMostChild(mtd); + if (!mo.hasAttribute(ELEMENT_DEPTH)) console.log(`Linebreaking error: depth not set on ${mo.tagName} with content '${mo.textContent.trim()}'`); + const moDepth = mo.getAttribute(ELEMENT_DEPTH); + const moChar = mo.textContent.trim(); + let minIndentAmount = 1e21; + let operatorMatched = false; + const xStart = mtd.getBoundingClientRect().left; + const maxWidth = parseFloat(mtd.parentElement.parentElement.getAttribute(MTABLE_LINEBREAKS_ATTR)); + let previousLine = mtd.parentElement.previousElementSibling; + while (previousLine) { + const previousLineOperators = getAllBreakPoints(previousLine.firstElementChild).filter(operator => moDepth === operator.getAttribute(ELEMENT_DEPTH)); + previousLineOperators.length === 0 || previousLineOperators[0].textContent.trim(); + const previousLineMatches = filterOnCharMatch(previousLineOperators, moChar); + let indent = previousLineMatches.length === 0 ? minIndentAmount : previousLineMatches[0].getBoundingClientRect().left; + if (isMatchLessThanHalfWay(xStart, indent, maxWidth)) { + if (indent < minIndentAmount || !operatorMatched) { + operatorMatched = true; + minIndentAmount = indent; + } + } + indent = previousLineOperators.length === 0 ? minIndentAmount : previousLineOperators[0].getBoundingClientRect().left; + if (!operatorMatched && isMatchLessThanHalfWay(xStart, indent, maxWidth)) minIndentAmount = Math.min(indent, minIndentAmount); + previousLine = previousLine.previousElementSibling; + } + if (minIndentAmount == 1e21) return convertToPx(mo, FALLBACK_INDENT_AMOUNT); + return minIndentAmount - xStart; +} +/** +* Adds shift amounts to the mtd +* The amount is finalized in a pass after linebreaking. +* It is not done now because center/right alignment positioning would mess up linebreaking +* @param {Element} mtd +* @param {string} alignment // should be one of 'left'|'center'|'right' +* @param {number} shiftAmount +*/ +function setupLineShifts(mtd, alignment, shiftAmount) { + mtd.setAttribute("style", `text-align: ${alignment};`); + const mspace = newElement("mspace"); + mspace.setAttribute("width", shiftAmount.toString() + "px"); + mtd.setAttribute(INDENT_AMOUNT, shiftAmount.toString()); + if (mtd.children.length !== 1 || mtd.firstElementChild.tagName !== "mrow") { + console.log(`unexpected element '${mtd.firstElementChild.tagName}' encountered while trying to indent line`); + return; + } + const mrow = mtd.firstElementChild; + if (alignment === "right") mrow.appendChild(mrow);else mrow.insertBefore(mspace, mrow.firstElementChild); +} +/** +* Return the amount of indent that should happen if we break on 'mo' +* @param {Element} mo // mo or mtd +* @param {number} xLineStart +* @param {Object} indentAttrs +* @returns {number} +*/ +function computeIndentAmount(mo, xLineStart, indentAttrs) { + let indentShiftAsPx = parseFloat(indentAttrs.indentShift); + let indentAlign = indentAttrs.indentAlign; + if (indentAlign === "id") { + const elementWithID = getElementByIdEverywhere(indentAttrs.target); + if (elementWithID) return elementWithID.getBoundingClientRect().left - xLineStart + indentShiftAsPx; + indentAlign = "auto"; + } + if (indentAlign == "auto") { + if (indentAttrs.firstMiddleOrLast !== "first") { + while (mo.tagName !== "mtd" && !mo.parentElement.parentElement.hasAttribute(MTABLE_HAS_LINEBREAKS)) mo = mo.parentElement; + indentShiftAsPx += computeAutoShiftAmount(mo); + } + } + return indentShiftAsPx; +} +/** +* Indent the line +* @param {Element} mtd +*/ +function indentLine(mtd) { + if (mtd.hasAttribute(INDENT_AMOUNT)) return; + const indentAttrs = JSON.parse(mtd.getAttribute(INDENT_ATTRS)); + const xLineStart = mtd.getBoundingClientRect().left; + let indentShiftAsPx = computeIndentAmount(mtd, xLineStart, indentAttrs); + let indentAlign = indentAttrs.indentAlign; + if (indentAlign === "id") { + if (getElementByIdEverywhere(indentAttrs.target) && !mtd.querySelector("#" + indentAttrs.target)) { + setupLineShifts(mtd, "left", indentShiftAsPx); + return; + } + indentAlign = "auto"; + } + if (indentAlign == "auto") indentAlign = "left"; + setupLineShifts(mtd, indentAlign, indentShiftAsPx); +} +/** +* Returns the outermost embellishment of an 'mo' +* @param {Element} mo +* @returns {Element} +*/ +function expandToEmbellishedElement(mo) { + let el = mo; + let parent = mo.parentElement; + do { + if (parent.firstElementChild !== mo || !EMBELLISHED_ELEMENT_NAMES.includes(parent.tagName)) { + if (el !== mo) { + if (!mo.hasAttribute(ELEMENT_DEPTH)) console.log(`Linebreaking error: depth not set on ${mo.tagName} with content '${mo.textContent.trim()}'`); + el.setAttribute(ELEMENT_DEPTH, mo.getAttribute(ELEMENT_DEPTH)); + } + return el; + } + el = parent; + parent = parent.parentElement; + } while (parent); + console.log(`In linebreaking in expandToEmbellishedElement: unexpected loop termination. mo = '${mo.tagName}'`); + return mo; +} +/** +* Return all the potential break points inside 'element' (math or mtd) +* @param {Element} element +* @returns {Element[]} +*/ +function getAllBreakPoints(element) { + return Array.from(element.querySelectorAll("mo:not([linebreak=\"nobreak\"])")).filter(mo => { + do mo = mo.parentElement; while (mo.tagName === "mrow" || mo.tagName === "mstyle" || mo.tagName === "mpadded"); + return mo.tagName === "math" || isInLineBreakTable(mo); + }).map(mo => expandToEmbellishedElement(mo)); +} +/** +* +* @param {string} ch +* @returns number +*/ +function operatorPrecedence(ch) { + const precedence = PrecedenceTable[ch]; + if (precedence === void 0) return 40; + return precedence; +} +/** +* +* @param {Element} el +* @returns {Element} +*/ +function getEmbellishedOperator(el) { + if (el.tagName === "mo") return el; + let firstChild = el; + while (EMBELLISHED_ELEMENT_NAMES.includes(firstChild.tagName)) { + firstChild = firstChild.firstElementChild; + if (!firstChild) return el; + } + return firstChild.tagName === "mo" ? firstChild : el; +} +/** +* +* @param {[any]} stack +* @returns {any} +*/ +function top(stack) { + return stack[stack.length - 1]; +} +/** +* +* @param {[] | Element} elementStackEntry +* @returns {boolean} +*/ +function isOperand(elementStackEntry) { + return Array.isArray(elementStackEntry); +} +/** +* +* @param {string} mo +* @returns {boolean} +*/ +function isPrefix(mo) { + return OpenList.includes(mo); +} +/** +* The "reduce" step of parsing. +* @param {[number]} opStack +* @param {[Element | [Element]]} elementStack +* @param {number} childPrecedence +* @returns {[[number], [Element | [Element]]} + +*/ +function parseReduce(opStack, elementStack, childPrecedence) { + let stackPrecedence = top(opStack); + let previousStackPrecedence = stackPrecedence + 1; + while (childPrecedence < stackPrecedence) { + let iPopTo = elementStack.length - 1; + while (Array.isArray(elementStack[iPopTo])) iPopTo--; + iPopTo--; + opStack.pop(); + while (Array.isArray(elementStack[iPopTo])) iPopTo--; + const elementsPopped = elementStack.splice(iPopTo + 1); + if (stackPrecedence === previousStackPrecedence && Array.isArray(top(elementsPopped))) { + const lastElement = elementsPopped.pop(); + elementStack.push(elementsPopped.concat(lastElement)); + } else elementStack.push(elementsPopped); + previousStackPrecedence = stackPrecedence; + stackPrecedence = top(opStack); + } + return [opStack, elementStack]; +} +function addInvisibleFunctionApply(opStack, elementStack) { + const childPrecedence = operatorPrecedence(InvisibleFunctionApply); + [opStack, elementStack] = parseReduce(opStack, elementStack, childPrecedence); + opStack.push(childPrecedence); + elementStack.push(InvisibleFunctionApplyMo); +} +/** +* @param {Element} treeRoot +* @param {[number]} opStack +* @param {[Element | [Element]]} elementStack +* @returns {[[number], [Element | [Element]]} +*/ +function buildParseTree(treeRoot, opStack, elementStack) { + for (let i = 0; i < treeRoot.children.length; i++) { + const child = getEmbellishedOperator(treeRoot.children[i]); + if (child.tagName === "mo") { + const childCh = child.textContent.trim(); + if (isOperand(top(elementStack)) && isPrefix(childCh)) addInvisibleFunctionApply(opStack, elementStack); + if (isPrefix(childCh)) { + opStack.push(0); + elementStack.push(child); + } else if (CloseList.includes(childCh)) { + [opStack, elementStack] = parseReduce(opStack, elementStack, 0); + elementStack.push(child); + if (top(opStack) !== 0) console.log(`In linebreaking, parsing error with close char -- top of op stack is ${top(opStack)}`); + opStack.pop(); + const elementsPopped = elementStack.splice(elementStack.length - 3); + elementStack.push(elementsPopped); + } else { + const childPrecedence = operatorPrecedence(childCh); + [opStack, elementStack] = parseReduce(opStack, elementStack, childPrecedence); + opStack.push(childPrecedence); + elementStack.push(child); + } + } else if (child.tagName === "mrow" || child.tagName === "mpadded" || child.tagName === "mstyle") [opStack, elementStack] = buildParseTree(child, opStack, elementStack);else { + if (isOperand(top(elementStack))) addInvisibleFunctionApply(opStack, elementStack); + elementStack.push([child]); + } + } + return [opStack, elementStack]; +} +/** +* Store nesting depth info for each 'mo' as an attr. Depth is based on depth in tree of arrays +* @param {[Element | [Element]]} elementStack +* @param {number} depth +*/ +function setDepthAttr(elementStack, depth) { + elementStack.forEach(child => { + if (Array.isArray(child)) setDepthAttr(child, depth + 1);else if (child.tagName === "mo") child.setAttribute(ELEMENT_DEPTH, depth.toString()); + }); +} +/** +* Tries to determine if there is good mrow structure. If so returns true. +* @param {Element} mrow +* @returns {boolean} +*/ +function isMRowWellStructured(mrow) { + if (mrow.childElementCount <= 3) return true; + if (mrow.childElementCount % 2 === 0) return false; + const precedence = operatorPrecedence(mrow.children[1].textContent.trim()); + for (let i = 0; i < mrow.childElementCount - 1; i += 2) if (mrow.children[i].tagName === "mo" || mrow.children[i + 1].tagName !== "mo" || operatorPrecedence(mrow.children[i + 1].textContent.trim()) !== precedence) return false; + return true; +} +/** +* Tries to determine if there is good mrow structure. If so returns true. +* @param {Element} treeRoot +* @returns {boolean} +*/ +function isWellStructured(treeRoot) { + const mrows = Array.from(treeRoot.querySelectorAll("mrow")); + if (treeRoot.tagName === "mrow" || treeRoot.tagName === "math") mrows.push(treeRoot); + switch (mrows.length) { + case 0: + return true; + case 1: + return isMRowWellStructured(mrows[0]); + case 2: + return isMRowWellStructured(mrows[0]) && isMRowWellStructured(mrows[1]); + default: + return isMRowWellStructured(mrows[0]) && isMRowWellStructured(mrows[Math.floor(mrows.length / 2)]) && isMRowWellStructured(mrows[mrows.length - 1]); + } +} +/** +* Store nesting depth info for each 'mo' as an attr. Depth is based on depth in tree +* @param {Element} el +* @param {number} depth +*/ +function setDepthAttrBasedOnOriginalTree(el, depth) { + const embellishedOp = getEmbellishedOperator(el); + if (embellishedOp.tagName === "mo") { + embellishedOp.setAttribute(ELEMENT_DEPTH, depth.toString()); + return; + } + if (el.tagName === "mrow" || el.tagName === "mstyle" || el.tagName === "mpadded" || el.tagName === "math") for (let i = 0; i < el.childElementCount; i++) setDepthAttrBasedOnOriginalTree(el.children[i], depth + (el.tagName === "mrow" ? 1 : 0)); +} +/** +* Store nesting depth info for each 'mo' as an attr +* @param {Element} linebreakRoot +*/ +function addDepthInfo(linebreakRoot) { + /** @type {Element[]} */ + let linebreakRoots = []; + const linebreakElements = Array.from(linebreakRoot.querySelectorAll("mo[linebreak=\"newline\"]")); + linebreakElements.push(linebreakRoot); + linebreakElements.forEach(mo => { + const linebreakRoot = computeLineBreakRoot(mo); + if (!linebreakRoots.includes(linebreakRoot)) { + linebreakRoots.push(linebreakRoot); + if (isWellStructured(linebreakRoot)) setDepthAttrBasedOnOriginalTree(linebreakRoot, 0);else { + let [opStack, elementStack] = buildParseTree(linebreakRoot, [-1], [null]); + if (elementStack.length != 2) [opStack, elementStack] = parseReduce(opStack, elementStack, -1); + setDepthAttr(elementStack[1], 0); + } + } + }); +} +/********* linebreaking penalty computation *******/ +/** +* Used in penalty computation; 0 <= x <= max +* @param {number} x +* @param {number} xMax +* @returns {number} +*/ +function computeLineFillPenalty(x, xMax) { + const penalty = (LINE_FILL_TARGET * xMax - x) / xMax; + return penalty * penalty; +} +/** +* Used in penalty computation +* @param {Element} mo +* @returns {number} +*/ +function computeDepthPenalty(mo) { + const depthTable = [.05, .090909, .173554, .248685, .316987, .379079, .435526, .486842, .533493, .575902, .614457, .649506, .681369, .710336, .736669, .760608]; + if (!mo.hasAttribute(ELEMENT_DEPTH)) console.log(`Linebreaking error: depth not set on ${mo.tagName} with content '${mo.textContent.trim()}'`); + let depth = parseInt(mo.getAttribute(ELEMENT_DEPTH)); + return depth >= depthTable.length ? 1 - 3.482066 / depth : depthTable[depth]; +} +/** +* Computes a penalty based on % line filled, depth in the syntax tree, and whether the user indicated a break here is good/bad +* @param {Element} mo +* @param {number } x +* @param {number} xMax +* @returns {number} +*/ +function computePenalty(mo, x, xMax) { + const penalty = DEPTH_PENALTY_TO_FILL_PENALTY_RATIO * computeDepthPenalty(mo) + computeLineFillPenalty(x, xMax); + const linebreakAttrVal = getMathMLAttrValueAsString(mo, "linebreak", "auto"); + if (linebreakAttrVal === "goodbreak") return penalty / GOOD_PENALTY_SCALE_FACTOR;else if (linebreakAttrVal === "badbreak") return BAD_PENALTY_SCALE_FACTOR * penalty;else return penalty; +} +/** +* Handles substitution of char if InvisibleTimes ('linebreakmultchar' mo attr) +* The array is modified and the node replaced in the DOM +* @param {Element[]} potentialBreaks +* @param {number} index // index of char in +* @returns {Element} // the mo @index or it's replacement +*/ +function substituteCharIfNeeded(potentialBreaks, index) { + const mo = potentialBreaks[index]; + if (mo.textContent.trim() === "⁢") { + const replaceChar = getMathMLAttrValueAsString(mo, "linebreakmultchar", "⁢"); + if (replaceChar !== "⁢") { + const replacementMO = newElement("mo"); + replacementMO.textContent = replaceChar; + copyAttributes(replacementMO, mo); + mo.replaceWith(replacementMO); + potentialBreaks[index] = replacementMO; + return replacementMO; + } + } + return mo; +} +/** +* The entry point to linebreaking +* @param {Element} element // or (if previously split due to manual linebreak) +* @param {number} maxLineWidth +*/ +function linebreakLine(element, maxLineWidth) { + if (parseFloat(element.getAttribute(FULL_WIDTH)) <= maxLineWidth) return; + const potentialBreaks = getAllBreakPoints(element); + let lineBreakMO; + /** @type {Element} */ + let lastRow = element.tagName === "mtd" ? element.parentElement : element.parentNode; + let nLines = 0; + let iOperator = 1; + while (iOperator < potentialBreaks.length) { + let iLine = iOperator; + const firstMTD = element.tagName === "mtd" ? lastRow.firstElementChild : lastRow.lastElementChild; + const indentAttrs = JSON.parse(firstMTD.getAttribute(INDENT_ATTRS)); + const leftSide = indentAttrs.linebreakstyle === "before" ? potentialBreaks[iOperator - 1].getBoundingClientRect().left : firstMTD.firstElementChild.getBoundingClientRect().left; + const lineBreakWidth = maxLineWidth - computeIndentAmount(potentialBreaks[iOperator - 1], firstMTD.getBoundingClientRect().left, indentAttrs); + let minPenalty = 1e5; + let iMinPenalty = -1; + while (iLine < potentialBreaks.length) { + const xRelativePosition = potentialBreaks[iLine].getBoundingClientRect().right - leftSide; + if (xRelativePosition > lineBreakWidth) break; + const penalty = computePenalty(potentialBreaks[iLine], xRelativePosition, lineBreakWidth); + if (penalty <= minPenalty) { + minPenalty = penalty; + iMinPenalty = iLine; + } + iLine++; + } + if (iMinPenalty === -1) { + console.log(`Linebreaking error: no breakpoint found on line ${nLines + 1}`); + iMinPenalty = iOperator; + } + nLines++; + iOperator = iMinPenalty + 1; + if (iOperator < potentialBreaks.length) { + lineBreakMO = substituteCharIfNeeded(potentialBreaks, iMinPenalty); + lastRow = splitLine(potentialBreaks[iMinPenalty]); + lastRow.parentElement.setAttribute(MTABLE_LINEBREAKS_ATTR, maxLineWidth.toString()); + storeLineBreakAttrsOnMtd(lastRow.firstElementChild, lineBreakMO); + const previousRow = lastRow.previousElementSibling; + if (!previousRow.firstElementChild.hasAttribute(INDENT_ATTRS)) previousRow.firstElementChild.setAttribute(INDENT_ATTRS, element.getAttribute(INDENT_ATTRS)); + indentLine(previousRow.firstElementChild); + } else if (nLines === 1) return; + } + if (nLines > 0) indentLine(lastRow.firstElementChild); +} +/** +* Linebreak/indent display math +* There is no good target in core, so the following hack is used if linebreaking is needed: +* 1. The custom element 'math-with-linebreaks' is created as the parent of 'math' if it isn't already there. +* 2. A clone is made and added into the shadow DOM (avoids duplicate 'id' problems) +* 3. A marked is created at the appropriate point (typically a child of ) and each line of the math is a row in the table +* +* On resize, we throw out the old shadow and start from fresh with a clone of the element. +* +* Since most math doesn't need to be linebroken, we start with a quick check to see if there are forced linebreaks or if it is wide. +* @param {HTMLElement} math +*/ + +/** +* The main starting point +* @param {Element} customElement // (likely inside a shadow DOM) +* @param {number} maxLineWidth +*/ +function lineBreakDisplayMath(customElement, maxLineWidth) { + maxLineWidth = Math.min(maxLineWidth, parseFloat(customElement.getAttribute(FULL_WIDTH))); + const math = customElement.shadowRoot.lastElementChild; + if (math.childElementCount > 1) { + const mrow = newElement("mrow"); + while (math.firstElementChild) mrow.appendChild(math.firstElementChild); + math.appendChild(mrow); + } + shadowRoot.set(customElement.shadowRoot); + splitIntoLinesAtForcedBreaks(math, maxLineWidth); + let linebreakGroups = Array.from(math.querySelectorAll(`mtable[${MTABLE_HAS_LINEBREAKS}]`)); + if (linebreakGroups.length > 0) linebreakGroups.forEach(table => { + table.setAttribute(MTABLE_LINEBREAKS_ATTR, maxLineWidth.toString()); + Array.from(table.children).forEach(line => { + const mtd = line.firstElementChild; + indentLine(mtd); + if (mtd.firstElementChild.getBoundingClientRect().right - mtd.getBoundingClientRect().left > maxLineWidth) linebreakLine(mtd, maxLineWidth); + }); + });else if (parseInt(customElement.getAttribute(FULL_WIDTH)) >= maxLineWidth) { + math.setAttribute(INDENT_ATTRS, JSON.stringify(computeIndentAttrObject(math, "first"))); + linebreakLine(math, maxLineWidth); + } +} +/** +* +* @param {Element} customElement +* @param {Element} math +*/ +function setShadowRootContents(customElement, math) { + /** @type {HTMLElement} */ + const mathClone = cloneElementWithShadowRoot(math); + customElement.shadowRoot.appendChild(mathClone); + let fullWidth = mathClone.lastElementChild.getBoundingClientRect().right - mathClone.firstElementChild.getBoundingClientRect().left; + if (mathClone.hasAttribute("maxwidth")) fullWidth = Math.min(fullWidth, convertToPx(mathClone, mathClone.getAttribute("maxwidth"))); + customElement.setAttribute(FULL_WIDTH, fullWidth.toString()); + lineBreakDisplayMath(customElement, fullWidth); + customElement.setAttribute(LINE_BREAK_WIDTH, (2 * fullWidth).toString()); +} +function addCustomElement(math) { + const computedStyle = getComputedStyle(math).getPropertyValue("display"); + const displayValue = math.hasAttribute("display") ? math.getAttribute("display") : "inline"; + if (computedStyle === "inline" || displayValue === "inline") return null; + if (math.tagName.toLowerCase() === SHADOW_ELEMENT_NAME) return math;else if (math.parentElement.tagName.toLowerCase() === SHADOW_ELEMENT_NAME) return math;else { + const mathParent = math.parentElement; + const nextSibling = math.nextElementSibling; + const shadowHost = document.createElement(SHADOW_ELEMENT_NAME); + shadowHost.appendChild(math); + mathParent.insertBefore(shadowHost, nextSibling); + addDepthInfo(math); + setShadowRootContents(shadowHost, math); + return null; + } +} +{ + let UAStyle = document.createElement("style"); + UAStyle.innerHTML = ` + math-with-linebreaks { + display: block; + } + `; + document.head.insertBefore(UAStyle, document.head.firstElementChild); +} + +//#endregion +//#region href/href.js +/*** +* Make href work on all MathML elements by adding click, mouseover, +* and mouseout events +***/ +/** +* @param {MathMLElement} el +*/ + +//#endregion +export { _MathTransforms }; \ No newline at end of file diff --git a/src/vendor/mathml-polyfills/all-polyfills-bundle.js b/src/vendor/mathml-polyfills/all-polyfills-bundle.js new file mode 100644 index 00000000..2bd6a403 --- /dev/null +++ b/src/vendor/mathml-polyfills/all-polyfills-bundle.js @@ -0,0 +1,295 @@ +const e=`http://www.w3.org/1998/Math/MathML`,t={_plugins:new Map,_css:``,_createStyleSheet:e=>{if(e.length!==t.cssKey){t.cssKey=e.length;let n=document.createElement(`style`);n.textContent=e,document.head.appendChild(n),t.styleSheet=n,document.head.removeChild(n)}return t.styleSheet},getCSSStyleSheet:()=>t._createStyleSheet(t._css).cloneNode(!0),transform:e=>{for(let n of t._plugins.keys()){let r=t._plugins.get(n);Array.from(e.querySelectorAll(n)).reverse().forEach(e=>{let t=r(e);t&&t!==e&&e.parentElement.replaceChild(t,e)})}},add:(e,n,r=``)=>{t._plugins.set(e,n),t._css+=r}};function n(e,r){if(r===void 0&&(r=e.cloneNode(!0)),e.shadowRoot){let i=r.attachShadow({mode:`open`});i.appendChild(t.getCSSStyleSheet());for(let t=0;t{let t=document.createElement(`img`),n=e.attributes;for(let i=n.length-1;i>=0;i--)switch(n[i].name){case`valign`:t.setAttribute(`style`,`vertical-align: ${n[i].value}`);break;case`width`:case`height`:t.setAttribute(n[i].name,r(e.parentElement,n[i].value).toString());break;default:t.setAttribute(n[i].name,n[i].value);break}return t});const i=`http://www.w3.org/1998/Math/MathML`;function a(e){return e.replace(/^[\s]+|[\s]+$/g,``).replace(/[\s]+/g,` `)}function o(e,t){let n=document.createElementNS(i,`mo`);return n.appendChild(document.createTextNode(e)),n.setAttribute(t?`separator`:`fence`,`true`),n}function s(){return document.createElementNS(i,`mrow`)}function c(e){if(e===null)return[`,`];let t=[];for(let n=0;n=55296&&r<56320&&n+1{let t=s();if(t.appendChild(o(a(e.getAttribute(`open`)||`(`))),e.childElementCount===1)t.appendChild(n(e.firstElementChild));else if(e.childElementCount>1){let r=c(e.getAttribute(`separators`)),i=s(),a=e.firstElementChild;for(;a;)i.appendChild(n(a)),a=a.nextElementSibling,a&&r.length&&i.appendChild(o(r.length>1?r.shift():r[0]));t.appendChild(i)}t.appendChild(o(a(e.getAttribute(`close`)||`)`)));for(let n=0;n{let n=t.firstElementChild.getBoundingClientRect().height,i=r(t.firstElementChild,`0.5em`),a=Math.max(n,t.lastElementChild.getBoundingClientRect().height)+i,o=document.createElementNS(e,`mrow`),s=document.createElementNS(e,`mpadded`);s.setAttribute(`height`,`${n+i}px`),s.setAttribute(`voffset`,`${i}px`),s.appendChild(t.firstElementChild),o.appendChild(s);let c=document.createElementNS(e,`mo`);c.setAttribute(`stretchy`,`true`),c.setAttribute(`symmetric`,`false`),c.setAttribute(`lspace`,`0px`),c.setAttribute(`rspace`,`0px`);let l=Math.round(-.2*a);return c.setAttribute(`style`,`margin-left: ${l}px; margin-right: ${l}px`),c.appendChild(document.createTextNode(`/`)),o.appendChild(c),o.appendChild(t.lastElementChild),o});const u=[`msub`,`msup`,`msubsup`,`munder`,`mover`,`munderover`,`mmultiscripts`,`mfrac`,`semantics`],d=[`mrow`,`mstyle`,`mphantom`,`mpadded`];function f(e){if(e.tagName===`mtext`||e.tagName===`mspace`||e.tagName===`maligngroup`||e.tagName===`malignmark`)return!0;if(d.includes(e.tagName)){for(let t=0;t{!e.getAttribute(`accentunder`)&&e.tagName!==`mover`&&m(e.children[1],`accentunder`),!e.getAttribute(`accent`)&&e.tagName!==`munder`&&m(e.children.length===2?e.children[1]:e.children[2],`accent`)};t.add(`munder`,h),t.add(`mover`,h),t.add(`munderover`,h);function g(e){return e.getBoundingClientRect().width}function _(t,n,r,i){if(n>=i||r===`center`)return t;if(t.tagName!==`mrow`){let n=t.nextElementSibling,r=document.createElementNS(e,`mrow`),i=t.parentElement;r.appendChild(t),i.insertBefore(r,n),t=r}let a=document.createElementNS(e,`mspace`);return a.setAttribute(`width`,`${(i-n).toPrecision(2)}px`),r===`left`?t.appendChild(a):r===`right`&&t.insertBefore(a,t.firstElementChild),t}function v(e,t,n,r){return _(e.children[t],g(e.children[t]),e.getAttribute(r),g(e.children[n])),e}const ee=e=>v(e,0,1,`numalign`),te=e=>v(e,1,0,`denomalign`),y=e=>v(e,1,0,`align`);t.add(`mfrac[numalign]`,ee),t.add(`mfrac[denomalign]`,te),t.add(`munder[align]`,y),t.add(`mover[align]`,y),t.add(`munderover[align]`,e=>{let t=e.getAttribute(`align`),n=g(e.children[0]),r=g(e.children[1]),i=g(e.children[2]),a=Math.max(n,r,i);return _(e.children[1],r,t,a),_(e.children[2],i,t,a),e}),t.add(`[mathsize="small"]`,e=>(e.setAttribute(`mathsize`,`75%`),e)),t.add(`[mathsize="normal"]`,e=>(e.setAttribute(`mathsize`,`100%`),e)),t.add(`[mathsize="big"]`,e=>(e.setAttribute(`mathsize`,`150%`),e));const b={veryverythinmathspace:`0.05555555555555555em`,verythinmathspace:`0.1111111111111111em`,thinmathspace:`0.16666666666666666em`,veryverythickmathspace:`0.3888888888888889em`,verythickmathspace:`0.3333333333333333em`,thickmathspace:`0.2777777777777778em`,mediummathspace:`0.2222222222222222em`},x=e=>{let t=e.getAttribute(`rspace`);if(t){for(let[e,n]of Object.entries(b))t=t.replaceAll(e,n);t=t.replaceAll(`negative0`,`-0`),e.setAttribute(`rspace`,t)}if(t=e.getAttribute(`lspace`),t){for(let[e,n]of Object.entries(b))t=t.replaceAll(e,n);t=t.replaceAll(`negative0`,`-0`),e.setAttribute(`lspace`,t)}return e};t.add(`math *[rspace*="mathspace"]`,x),t.add(`math *[lspace*="mathspace"]`,x);const S=[`left`,`right`,`top`,`bottom`,`actuarial`,`madruwb`],C={longdiv:`padding: 0.05em 0.2em 0.0em 0.433em; border-top: 0.067em solid;`,actuarial:`padding-top: 0.01em; padding-right: 0.1em;`,radical:`padding-top: 0.403em; padding-bottom: 0.112em; padding-left: 1.02em;`,box:`padding: 0.2em;`,roundedbox:`padding: 0.267em;`,circle:`padding: 0.267em;`,phasorangle:`border-bottom: 0.067em solid; padding: 0.2em 0.2em 0.0em 0.7em;`,phasoranglertl:`border-bottom: 0.067em solid; padding: 0.2em 0.7em 0.0em 0.2em;`,madruwb:`padding-bottom: 0.2em; padding-right: 0.2em;`},w={horizontalstrike:[0,0,!1,[``]],verticalstrike:[0,Math.PI/2,!1,[``]],updiagonalstrike:[-1,0,!1,[``]],downdiagonalstrike:[1,0,!1,[``]],uparrow:[0,-Math.PI/2,!1,[`verticalstrike`]],downarrow:[0,Math.PI/2,!1,[`verticakstrike`]],rightarrow:[0,0,!1,[`horizontalstrike`]],leftarrow:[0,Math.PI,!1,[`horizontalstrike`]],updownarrow:[0,Math.PI/2,!0,[`verticalstrike`,`uparrow`,`downarrow`]],leftrightarrow:[0,0,!0,[`horizontalstrike`,`leftarrow`,`rightarrow`]],northeastarrow:[-1,0,!1,[`updiagonalstrike`,`updiagonalarrow`]],southeastarrow:[1,0,!1,[`downdiagonalstrike`]],northwestarrow:[1,Math.PI,!1,[`downdiagonalstrike`]],southwestarrow:[-1,Math.PI,!1,[`updiagonalstrike`]],northeastsouthwestarrow:[-1,0,!0,[`updiagonalstrike`,`northeastarrow`,`updiagonalarrow`,`southwestarrow`]],northwestsoutheastarrow:[1,0,!0,[`downdiagonalstrike`,`northwestarrow`,`southeastarrow`]]},ne=Array.from(new Set(S.concat(Object.keys(C),Object.keys(w))));function T(t){let n=document.createElementNS(e,`math`);n.innerHTML=t,document.body.appendChild(n);let r=n.getBoundingClientRect().width;return document.body.lastElementChild.remove(),r}function re(e){return!!(T(`x`)===T(`x`)||/arrow/.test(e)&&T(`x`)===T(`x`)||/phasorangle/.test(e)&&T(`x`)===T(`x`))}function ie(e){e=Array.from(new Set(e)),e.includes(`box`)&&(e=e.filter(e=>!S.includes(e)));for(let[t,n]of Object.entries(w)){let r=n[3];r!==[``]&&e.includes(t)&&(e=e.filter(e=>!r.includes(e)))}return e}function ae(e){let t=!0;do{if(e.hasAttribute(`dir`))return e.getAttribute(`dir`)===`ltr`;t=e.tagName!==`math`,e=e.parentElement}while(t);return!0}function oe(e,t){let n=`0.467em`;return(t.includes(`roundedbox`)||t.includes(`circle`))&&(n=`0.601em`),r(e,n)}t.add(`menclose`,t=>{let i=t.getAttribute(`notation`)||``;if(!re(i))return t;let a=i.split(` `);if(a=a.filter(e=>ne.includes(e)),a.length===0&&a.push(`longdiv`),a=ie(a),a.includes(`phasorangle`)&&!ae(t)){let e=a.indexOf(`phasorangle`);a[e]=`phasoranglertl`}let o=document.createElementNS(e,`mrow`),s=t.firstElementChild;for(;s;)o.appendChild(n(s)),s=s.nextElementSibling;let c=document.createElementNS(e,`mrow`);if(c.className=`menclose`,c.appendChild(o),a.includes(`radical`)){let t=document.createElementNS(e,`msqrt`);t.appendChild(c.firstElementChild),c.appendChild(t),a=a.filter(e=>e!==`radical`)}let l=``,u=oe(t,a),d=t.getBoundingClientRect(),f=d.width+u,p=d.height+u;return a.forEach(n=>{let i=document.createElementNS(e,`mrow`);if(w[n]!==void 0){let[t,r,a,o]=w[n],s=r===0||r===Math.PI,c=t===0?r:t*(Math.atan2(p,f)-r),l=f,u=p;/arrow/.test(n)&&(l--,u--);let d=t===0?s?l:u:Math.sqrt(l*l+u*u);i.style.width=`${d}px`,i.style.transform=(c?`rotate(${c}rad) `:``)+`translate(0.067em, 0.0em)`,i.style.left=`${(l-d)/2}px`;let m=document.createElementNS(e,`mrow`);if(m.className=`line`,i.appendChild(m),/arrow/.test(n)){let t=document.createElementNS(e,`mrow`);t.className=`rthead`,i.appendChild(t);let n=document.createElementNS(e,`mrow`);if(n.className=`rbhead`,i.appendChild(n),a){let t=document.createElementNS(e,`mrow`);t.className=`lthead`,i.appendChild(t);let n=document.createElementNS(e,`mrow`);n.className=`lbhead`,i.appendChild(n)}}}else if(n===`phasorangle`||n===`phasoranglertl`){let e=r(t,`.7em`),a=d.height;i.style.width=`${Math.sqrt(e*e+a*a)}px`,i.style.transform=`rotate(${n===`phasoranglertl`?``:`-`}${Math.atan(a/e)}rad) translateY(0.067em)`}let a=C[n]||``;a===``&&l.length===0&&(a=`padding: 0.2em;`),l.includes(a)||(l+=a),i.className=`menclose-${w[n]===void 0?n:`arrow`}`,c.appendChild(i)}),c.setAttribute(`style`,l),c},` +mrow.menclose { + display: inline-block; + text-align: left; + position: relative; +} + +/* the following class names should be of the form 'menclose-[notation name]' */ +mrow.menclose-longdiv { + position: absolute; + top: 0; + bottom: 0.1em; + left: -0.4em; + width: 0.7em; + border: 0.067em solid; + transform: translateY(-0.067em); + border-radius: 70%; + clip-path: inset(0 0 0 0.4em); + box-sizing: border-box; +} + +mrow.menclose-actuarial { + position: absolute; + display: inline-block; + top: 0; + bottom: 0; + right: 0; + left: 0; + border-top: 0.067em solid; + border-right: 0.067em solid; +} + +mrow.menclose-phasorangle { + display: inline-block; + left: 0; + bottom: 0; + position: absolute; + border-top: 0.067em solid; + transform-origin: bottom left; +} + +mrow.menclose-phasoranglertl { + display: inline-block; + right: 0; + bottom: 0; + position: absolute; + border-top: 0.067em solid; + transform-origin: bottom right; +} + +mrow.menclose-box { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border: 0.067em solid; +} + +mrow.menclose-roundedbox { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border: 0.067em solid; + border-radius: 0.267em; +} + +mrow.menclose-circle { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border: 0.067em solid; + border-radius: 50%; +} + +mrow.menclose-box { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border: 0.067em solid; +} + +mrow.menclose-left { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border-left: 0.067em solid; +} + +mrow.menclose-right { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border-right: 0.067em solid; +} + +mrow.menclose-top { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border-top: 0.067em solid; +} + +mrow.menclose-bottom { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border-bottom: 0.067em solid; +} + +mrow.menclose-madruwb { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + border-right: 0.067em solid; + border-bottom: 0.067em solid; +} + +/* + * Arrows and 'strikes' are composed of an 'menclose-arrow' wrapper and one, three or five children + * Strikes just have class 'line' + * Single-headed arrows have the children with classes 'line', 'rthead' (right top arrow head), and 'rbhead' + * Double-headed arrows add lthead' (left top arrow head), and 'lbhead' + */ +mrow.menclose-arrow { + position: absolute; + left: 0; + bottom: 50%; + height: 0; + width: 0; +} + +mrow.menclose > mrow.menclose-arrow > * { + display: block; + position: absolute; + transform-origin: bottom; + border-left: 0.268em solid; + border-right: 0; + box-sizing: border-box; + } + +mrow.menclose-arrow > mrow.line{ + left: 0; + top: -0.0335em; + right: 0.201em; + height: 0; + border-top: 0.067em solid; + border-left: 0; +} + +mrow.menclose-arrow > mrow.rthead { + transform: skewX(0.464rad); + right: 1px; + bottom: -1px; + border-bottom: 1px solid transparent; + border-top: 0.134em solid transparent; +} + +mrow.menclose-arrow > mrow.rbhead { + transform: skewX(-0.464rad); + transform-origin: top; + right: 1px; + top: -1px; + border-top: 1px solid transparent; + border-bottom: 0.134em solid transparent; +} + +mrow.menclose-arrow > mrow.lthead { + transform: skewX(-0.464rad); + left: 0; + bottom: -1px; + border-left: 0; + border-right: 0.268em solid; + border-bottom: 1px solid transparent; + border-top: 0.134em solid transparent; +} + +mrow.menclose-arrow > mrow.lbhead { + transform: skewX(0.464rad); + transform-origin: top; + left: 0; + top: -1px; + border-left: 0; + border-right: 0.268em solid; + border-top: 1px solid transparent; + border-bottom: 0.134em solid transparent; +} +`),t.add(`[linethickness="thin"]`,e=>(e.setAttribute(`linethickness`,`67%`),e)),t.add(`[linethickness="medium"]`,e=>(e.setAttribute(`linethickness`,`100%`),e)),t.add(`[linethickness="thick"]`,e=>(e.setAttribute(`linethickness`,`167%`),e));function se(e){return e.replace(/^[\s]+|[\s]+$/g,``).replace(/[\s]+/g,` `)}t.add(`ms`,e=>{let t=e.getAttribute(`lquote`)||`"`,n=e.getAttribute(`rquote`)||`"`,r=se(e.textContent);r=r.replace(t,`\\`+t),n!==t&&(r=r.replace(n,`\\`+n)),e.textContent=t+r+n});const ce={normal:`mup`,bold:`mbf`,italic:`mit`,"bold-italic":`mbfit`,"double-struck":`Bbb`,"bold-fraktur":`mbffrak`,script:`mscr`,"bold-script":`mbfscr`,fraktur:`mfrak`,"sans-serif":`msans`,"bold-sans-serif":`mbfsans`,"sans-serif-italic":`mitsans`,"sans-serif-bold-italic":`mbfitsans`,monospace:`mtt`,isolated:`misol`,initial:`minit`,tailed:`mtail`,looped:`mloop`,stretched:`mstrc`,chancery:`mchan`,roundhand:`mrhnd`},le=e=>{let t=e.getAttribute(`mathvariant`);if(!t||t==`normal`)return;let n=ce[t];if(!n)return;let r=e.textContent,i=``,a=!0;for(let e=0;e=`0`&&e<=`9`)return n+=120734,r=_e.indexOf(t),r==-1?e:String.fromCodePoint(n+r*10);if(/[A-Za-z]/.test(e)){let i=``;(t==`mchan`||t==`mrhnd`)&&(i=t==`mchan`?`︀`:`︁`,t=`mscr`);let a=``;switch(t){case`mit`:if(e==`h`)return`ℎ`;break;case`mfrak`:a=fe[e];break;case`mscr`:a=pe[e];break;case`Bbb`:a=de[e];break}return a?a+i:(r=ve.indexOf(t),r==-1?e:(n-=65,n>26&&(n-=6),String.fromCodePoint(n+52*r+119808)+i))}if(e>=`Α`&&e<=`ϵ`||e==`∂`||e==`∇`){if(t==`mbf`){if(e==`Ϝ`)return`𝟊`;if(e==`ϝ`)return`𝟋`}if(r=ye.indexOf(t),r==-1)return e;let i=he[e];return i?n=i:(n-=913,n>25&&(n-=6)),String.fromCodePoint(n+58*r+120488)}if(n<1575)return e==`ı`?`𝚤`:e==`ȷ`?`𝚥`:e;if(n>1722||(r=ge.indexOf(t),r==-1))return e;if(n<=1610){if(n=ue[n-1575],n==-1)return e}else{if(n=`ٮںڡٯ`.indexOf(e),n==-1)return e;n+=28}if(t==`misol`)n==4&&(r=1);else if(1<{let t=xe(e);return D(e,`width`,t),D(e,`height`,t),D(e,`depth`,t),D(e,`lspace`,t),D(e,`voffset`,t),e});const O=`http://www.w3.org/1998/Math/MathML`;function Se(e){return e}function Ce(e){if(e.getElementsByTagName(`mlabeledtr`).length===0)return e;let t=e.getAttribute(`side`)||`right`,n=document.createElementNS(O,`mtd`);n.setAttribute(`intent`,`:no-equation-label`);for(let r=0;r0;)n.appendChild(i.firstChild);t===`right`&&n.appendChild(e),i.replaceWith(n)}else{let e=n.cloneNode();t===`right`?i.appendChild(e):i.insertBefore(e,i.firstElementChild)}}return e}function we(e){if(!e.hasAttribute(`intent`)){e.setAttribute(`intent`,`:equation-label`);return}let t=e.getAttribute(`intent`),n=t.indexOf(`(`),r=n==-1?t:t.substring(0,n);r.includes(`:equation-label`)||(t=r+`:equation-label`+t.substring(r.length),e.setAttribute(`intent`,t))}t.add(`mtable`,e=>{let t=Se(n(e));return Ce(t),t});const k=`.35ex`;var A=class{constructor(e,t){if(this.attrs={},!t){for(;e&&e.tagName.toLowerCase()!==`math`;)e.tagName.toLowerCase()===`mstyle`&&this.addAttrs(e),e=e.parentElement;e&&e.tagName.toLowerCase()===`math`&&this.addAttrs(e)}else if(this.attrs=Object.assign({},t),e.tagName.toLowerCase()===`mstyle`)for(let t of e.attributes)this.attrs[t.name]=t.value}addAttrs(e){for(let t of e.attributes)this.attrs[t.name]||(this.attrs[t.name]=t.value)}getAttr(e,t,n){return e.hasAttribute(t)?e.getAttribute(t):this.attrs[t]?this.attrs[t]:n}},Te=class{constructor(e,t,n){this.location=e,this.crossout=t,this.scriptsizemultiplier=n}},j=class{constructor(e,t,n){if(n){if(typeof e!=`object`)throw Error(`Elementary math mscarry isn't an 'object'`);this.data=document.createElement(n.location===`n`||n.location===`s`?`div`:`span`),this.data.appendChild(e),this.data.className=`carry`,this.data.style.fontSize=Math.round(n.scriptsizemultiplier).toString()+`%`}else{if(typeof e!=`string`)throw Error(`Elementary math mscarry isn't a 'string'`);this.data=document.createTextNode(e)}this.carry=n,this.style=t||``}},M=class{constructor(e,t,n){n===0?this.data=e:n>0?this.data=this.padOnRight(e,n):n<0&&(this.data=this.padOnLeft(e,-n),t-=n),this.nRight=t,this.shift=n,this.style=``,this.addSpacingAfterRow=!1,this.alignAt=0}addUnderline(e,t){this.style+=`border-bottom: ${e} solid ${t};`,this.addSpacingAfterRow=!0}addUnderlineToCells(e,t,n,r){let i=this.data.length-this.nRight,a=i-e;e+t>i&&(this.data=this.padOnLeft(this.data,e+t-i),a=t),e<-this.nRight&&(this.data=this.padOnRight(this.data,this.nRight-e),this.nRight-=e,a=this.data.length);for(let e=a-t;ee.carry)){e.push(t);return}let a=t.data.length-t.nRight-(i.data.length-i.nRight);a!==0&&(a<0?t.data=t.padOnLeft(t.data,-a):i.data=i.padOnLeft(i.data,a));let o=t.nRight-i.nRight;o!==0&&(o<0?t.data=t.padOnRight(t.data,-o):i.data=i.padOnRight(i.data,o));for(let e=0;enew j(e))),t)n+=e.length;else{let r=a.textContent.trim().indexOf(this.getAttr(a,`decimalpoint`,`.`));n=r<0?0:e.length-r,t=!0}}else{let e=a.textContent.trim();e===`-`&&(e=`−`),r.push(new j(e)),t&&(n+=1)}}return[r,this.stackAlign===`decimalpoint`?n:0]}process_mscarries(e,t,n,r){let i=[],a=e.children[0];for(;a;){let e=a.nextElementSibling,o=t,s=n;a.tagName.toLowerCase()===`mscarry`&&(o=this.getAttr(a,`location`,`n`),s=this.getAttr(a,`crossout`,`none`)),i.push(new j(a,``,new Te(o,s,r))),a=e}return i}processChildren(e,t,n,r){if(!e.children)return t;r||=0;for(let i=e.tagName.toLowerCase()===`mlongdiv`?2:0;inew j(e));this.add(t,new M(o,a,r));break}case`msgroup`:t=this.processChildren(e,t,r,parseInt(this.getAttr(e,`shift`,`0`)));break;case`msline`:{let n=parseInt(this.getAttr(e,`length`,`0`)),i=this.getAttr(e,`mslinethickness`,`medium`);i===`medium`?i=k:i===`thin`?i=`.1ex`:i===`thick`&&(i=`.65ex`),t.length===0&&this.add(t,new M([],0,0));let a=t[t.length-1],o=this.getAttr(e,`mathcolor`,`black`);n===0?a.addUnderline(i,o):a.addUnderlineToCells(r,n,i,o);break}case`mscarries`:{let n=this.getAttr(e,`location`,`n`),i=this.getAttr(e,`crossout`,`none`),a=parseFloat(this.getAttr(e,`scriptsizemultiplier`,`0.6`));this.add(t,new M(this.process_mscarries(e,n,i,100*a),0,r));break}case`mstyle`:{let n=this.attrs;if(this.attrs=new A(e,n),e.children.length===1&&e.children[0].tagName.toLowerCase()===`msline`)this.processChild(e.children[0],t,r);else{let i,a;[i,a]=this.process_msrow(e),this.add(t,new M(i,a,r)),this.attrs=n}break}default:{let n,i=0;e.tagName.toLowerCase()==`msrow`?[n,i]=this.process_msrow(e):n=[new j(e.textContent.trim())],this.add(t,new M(n,i,r));break}}return t}processShifts(e,t){let n=0,r=0;for(let i of e)t===`decimalpoint`?(n=Math.max(n,i.data.length-i.nRight),r=Math.max(r,i.nRight)):n=Math.max(n,i.data.length);for(let i of e)switch(t){case`decimalpoint`:i.data=i.padOnLeft(i.data,n-(i.data.length-i.nRight)),i.data=i.padOnRight(i.data,r-i.nRight),i.nRight=r;break;case`left`:i.data=i.padOnRight(i.data,n-i.data.length);break;case`center`:{let e=n-i.data.length;i.data=i.padOnRight(i.data,e/2),i.data=i.padOnLeft(i.data,e-e/2);break}case`right`:i.data=i.padOnLeft(i.data,n-i.data.length);break;default:console.log(`Unknown mstack stackalign attr value: "${t}"`);break}return e}addOnLongDivParts(e,t,n){function r(e){for(let t=e.data.length-1;t>=0;t--)if(e.data[t].data.textContent!==` `)return e.data.length-1-t;return e.data.length}function i(e,t){let n=0;for(let r=e.data.length-1;r>=0&&e.data[r].data.textContent===` `;r--)t>0?t--:(e.data.pop(),n++);for(let n=0;n0&&(o.data=o.padOnRight(o.data,t)),o.addUnderlineToCells(-o.nRight,o.data.length,k,a),o.addSpacingAfterRow=!1,n[0].data=n[0].data.concat(o.data),n[0].nRight+=o.data.length,n[1].data=n[1].data.concat(l.data),n[1].nRight+=l.data.length;break}case`stackedleftleft`:{n.length==1&&(n.push(new M([new j(` `)],0,0)),n=this.processShifts(n,this.stackAlign));for(let e=0;e0&&(o.data=o.padOnLeft(o.data,e)),o.addUnderlineToCells(-o.nRight,o.data.length,k,a),o.addSpacingAfterRow=!1,n[0].data=o.data.concat(n[0].data),n[1].data=l.data.concat(n[1].data);break}case`righttop`:{l.addUnderline(k,a),l.addSpacingAfterRow=!1;let e=c.concat(n);n=this.processShifts(e,this.stackAlign),o.data[0].style+=`border-left: ${k} solid ${a};`,o.addUnderlineToCells(-o.nRight,o.data.length,k,a),n[1].data=n[1].data.concat(o.data),n[1].nRight+=o.data.length;break}default:{l.addUnderlineToCells(-l.nRight,Math.max(l.data.length,n[0].data.length),k,a),l.addSpacingAfterRow=!1;let e=c.concat(n);n=this.processShifts(e,this.stackAlign),this.longdivstyle===`stackedleftlinetop`?(o.data[o.data.length-1].style+=`border-right: ${k} solid ${a};`,o.data[o.data.length-1].style+=`border-right: ${k} solid ${a};`,o.data[o.data.length-1].data.style+=`position:relative`,o.addUnderlineToCells(-o.nRight,o.data.length,k,a)):(o.data=o.padOnRight(o.data,1),s+=1,o.data[s].class=`curved-line`,o.data[s].style=``),n[1].data=o.data.concat(n[1].data);break}}let u=this.processShifts(n,this.stackAlign);return this.longdivstyle===`lefttop`&&(n[0].data[s].style+=`border-bottom: ${k} solid ${a};`),u}shrinkSeparatorColumns(e){if(e.length===0)return;let t=new Set(Array(e[0].data.length).keys()),n=new Set(Array(e[0].data.length).keys());for(let r of e){let e=r.data;for(let r=0;rt.delete(e));for(let n of t)e.forEach(e=>{e.data[n].class=`separator`,n>0&&(e.data[n-1].class=`precedes-separator`)})}expandMStackElement(e){let t=/[-+]?\d*\.?\d*/g,n=parseFloat(t.exec(this.charSpacing)[0])/2+this.charSpacing.slice(t.lastIndex);this.charSpacing.slice(t.lastIndex);let r=`padding: .1ex ${n} 0 ${n}; text-align: ${this.charAlign};`,i=[];i=this.processChildren(e,i,0,0),i=this.processShifts(i,this.stackAlign),e.tagName.toLowerCase()===`mlongdiv`&&(i=this.addOnLongDivParts(e.children[0],e.children[1],i)),i.length>0&&(i[i.length-1].addSpacingAfterRow=!1),this.shrinkSeparatorColumns(i);let a=document.createElement(`table`);a.setAttribute(`class`,`elem-math`);for(let e of i){let t=document.createElement(`tr`);e.style&&t.setAttribute(`style`,e.style);for(let n of e.data){let e=document.createElement(`td`);if(n.alignAt){let e=document.createElement(`span`);e.style.display=n.alignAt===1?`inline-table`:`inline-block`,e.appendChild(n.data),n.data=e}n.class===`curved-line`&&(n.data.textContent=`\xA0`),e.appendChild(n.data),n.class!==`curved-line`&&e.setAttribute(`style`,r+n.style),n.class&&e.setAttribute(`class`,n.class),t.appendChild(e)}if(a.appendChild(t),e.addSpacingAfterRow){let t=document.createElement(`tr`);t.style.height=`.5ex`;for(let n of e.data){let e=document.createElement(`td`);if(/(border-left|border-right)/.test(n.style)){let t=n.style.match(/(border-left|border-right).*?;/g);e.setAttribute(`style`,t)}t.appendChild(e)}a.appendChild(t)}}return a}};let P=n=>{if(n.parentElement&&(n.parentElement.tagName===`M-ELEM-MATH`||n.parentElement.parentElement&&n.parentElement.parentElement.tagName===`M-ELEM-MATH`))return;let r=document.createElement(`span`);r.attachShadow({mode:`open`}).appendChild(t.getCSSStyleSheet());let i=n.parentElement,a=n.nextElementSibling,o=new N(n).expandMStackElement(n);r.shadowRoot.appendChild(o);let s=document.createElementNS(e,`mtext`);s.appendChild(r);let c=document.createElementNS(e,`math`);return r.appendChild(c),c.appendChild(n),i.insertBefore(s,a),null};t.add(`mstack`,P,` +table.elem-math { + border-collapse: collapse; + border-spacing: 0px; +} +table.elem-math tr { + vertical-align: baseline; +} + +td.curved-line { + position: absolute; + padding-top: 0em; + width: 0.75em; + border: 0.3ex solid; /* match border bottom */ + transform: translate(0.48em, -0.15em); + border-radius: 70%; + clip-path: inset(0.1em 0 0 0.45em); + box-sizing: border-box; + margin-left: -0.85em; + margin-right: 0.75em; +} + +mtd.precedes-separator { + padding-right: 0 !important; /* override an inline style */ +} + +mtd.separator { + padding-left: 0 !important; /* override an inline style */ + padding-right: 0 !important; /* override an inline style */ +} + +.carry { + font-size: 60%; + line-height: 90%; + width: 1px; + overflow: visible; +} + +.hidden-digit { + visibility: hidden; +} + +.crossout-horiz, .crossout-vert, .crossout-up, .crossout-down{ + position: relative; + display: inline-block; +} +.crossout-horiz:before { + content: ''; + border-bottom: .3ex solid black; + width: 140%; + position: absolute; + right: -20%; + top: 40%; +} + +.crossout-vert::before { + content: ''; + border-left: .3ex solid black; + height: 100%; + position: absolute; + right: 35%; + top: 0%; +} + +.crossout-up::before { + content: ''; + width: 100%; + position: absolute; + right: 0; + top: 40%; +} +.crossout-up::before { + border-bottom: .2em solid black; + transform: skewY(-60deg); +} + +.crossout-down::after { + content: ''; + width: 100%; + position: absolute; + right: 0; + top: 40%; +} +.crossout-down::after { + border-bottom: .2em solid black; + transform: skewY(60deg); +} +`),t.add(`mlongdiv`,P),customElements.define(`m-elem-math`,class extends HTMLElement{constructor(){super();let e=new N(this.children[0]).expandMStackElement(this.children[0]),n=this.attachShadow({mode:`open`});n.appendChild(t.getCSSStyleSheet()),n.appendChild(e)}});const F=`data-has-linebreaks`,I=`data-max-linebreak-width`,L=`data-saved-indent-attrs`,R=`data-x-indent`,z=`data-nesting-depth`,B=[`msub`,`msub`,`msubsup`,`mover`,`munder`,`munderover`,`mfrac`,`mmultiscripts`];var Ee=(function(){var e=null,t=0,n=0;return{set:function(r,i,a){e=r,t=i,n=a},get:function(){return e},getEmInPixels:function(){return t},getBreakWidth:function(){return n}}})();function De(e){return Ee.get().getElementById(e)||document.getElementById(e)}function V(t){return document.createElementNS(e,t)}function Oe(e,t){let n=t.attributes;for(let t=0;t0;)e.removeAttributeNode(e.attributes[0]);return e.parentElement.parentElement.insertBefore(r,e.parentElement),e.parentElement}else{let e=ke();return e.setAttribute(`style`,`width: 100%`),e.appendChild(r),n.replaceWith(e),e.appendChild(je(n)),e.lastElementChild}}function Pe(e){let t=H(e,`linebreakstyle`,`before`);t===`infixLineBreakStyle`&&(t=H(e,`infixLineBreakStyle`,`before`));let n=null,r=e;e.previousElementSibling!==null&&e.nextElementSibling!==null&&e.setAttribute(`form`,`infix`);let i=r.parentElement;for(;i.tagName===`mrow`;i=i.parentElement){let e=V(`mrow`);for(;i.firstElementChild;){let n=i.firstElementChild;if(n===r){if(t===`after`){e.appendChild(n),t=`before`;break}else t===`duplicate`&&(t=`before`,e.appendChild(n.cloneNode(!0)));break}e.appendChild(n)}r=i,n&&e.appendChild(n),n=e.children.length===1?e.firstElementChild:e}if(r.tagName===`mrow`&&r.children.length===1){let e=r.firstElementChild;r.replaceWith(e),r=e}return Ne(i,n,r)}function Fe(e){let t=e,n=e.parentElement;for(;n.tagName===`mrow`||n.tagName===`mstyle`||n.tagName===`mpadded`;)t=n,n=n.parentElement;return t}function Ie(e,t){let n=e.querySelectorAll(`mo[linebreak="newline"]`);if(n.length===0)return;let r=null;n.forEach(e=>{r=Pe(e)});let i=r.parentElement.children;U(i[0].firstElementChild,i[0].firstElementChild);for(let e=0;e0;)e=e.firstElementChild;return e.tagName==`mspace`?e.nextElementSibling:e}function ze(e,t,n){return t-e<=.5*n}function Be(e,t){return t===`-`&&(t=`+`),e.filter(function(e){let n=e.textContent.trim();return n===`-`&&(n=`+`),t===n})}function Ve(e){if(Le(e.parentElement))return 0;let t=Re(e);t.hasAttribute(z)||console.log(`Linebreaking error: depth not set on ${t.tagName} with content '${t.textContent.trim()}'`);let n=t.getAttribute(z),i=t.textContent.trim(),a=1e21,o=!1,s=e.getBoundingClientRect().left,c=parseFloat(e.parentElement.parentElement.getAttribute(I)),l=e.parentElement.previousElementSibling;for(;l;){let e=Ge(l.firstElementChild).filter(e=>n===e.getAttribute(z));e.length===0||e[0].textContent.trim();let t=Be(e,i),r=t.length===0?a:t[0].getBoundingClientRect().left;ze(s,r,c)&&(r{do e=e.parentElement;while(e.tagName===`mrow`||e.tagName===`mstyle`||e.tagName===`mpadded`);return e.tagName===`math`||Ae(e)}).map(e=>We(e))}const Ke={"(":0,")":0,"=":10,"+":30,"±":30,"-":30,"*":40,"×":40,InvisibleTimes:40,InvisibleFunctionApply:50},qe=[`(`,`[`,`{`],Je=[`)`,`]`,`}`];function G(e){let t=Ke[e];return t===void 0?40:t}function Ye(e){if(e.tagName===`mo`)return e;let t=e;for(;B.includes(t.tagName);)if(t=t.firstElementChild,!t)return e;return t.tagName===`mo`?t:e}function K(e){return e[e.length-1]}function Xe(e){return Array.isArray(e)}function Ze(e){return qe.includes(e)}function q(e,t,n){let r=K(e),i=r+1;for(;n{Array.isArray(e)?tt(e,t+1):e.tagName===`mo`&&e.setAttribute(z,t.toString())})}function J(e){if(e.childElementCount<=3)return!0;if(e.childElementCount%2==0)return!1;let t=G(e.children[1].textContent.trim());for(let n=0;n{let n=Fe(e);if(!t.includes(n))if(t.push(n),nt(n))rt(n,0);else{let[e,t]=et(n,[-1],[null]);t.length!=2&&([e,t]=q(e,t,-1)),tt(t[1],0)}})}function at(e,t){let n=(.9*t-e)/t;return n*n}function ot(e){let t=[.05,.090909,.173554,.248685,.316987,.379079,.435526,.486842,.533493,.575902,.614457,.649506,.681369,.710336,.736669,.760608];e.hasAttribute(z)||console.log(`Linebreaking error: depth not set on ${e.tagName} with content '${e.textContent.trim()}'`);let n=parseInt(e.getAttribute(z));return n>=t.length?1-3.482066/n:t[n]}function st(e,t,n){let r=3*ot(e)+at(t,n),i=H(e,`linebreak`,`auto`);return i===`goodbreak`?r/3:i===`badbreak`?3*r:r}function ct(e,t){let n=e[t];if(n.textContent.trim()===`⁢`){let r=H(n,`linebreakmultchar`,`⁢`);if(r!==`⁢`){let i=V(`mo`);return i.textContent=r,Oe(i,n),n.replaceWith(i),e[t]=i,i}}return n}function lt(e,t){if(parseFloat(e.getAttribute(Z))<=t)return;let n=Ge(e),r,i=e.tagName===`mtd`?e.parentElement:e.parentNode,a=0,o=1;for(;od)break;let t=st(n[s],e,d);t<=f&&(f=t,p=s),s++}if(p===-1&&(console.log(`Linebreaking error: no breakpoint found on line ${a+1}`),p=o),a++,o=p+1,o0&&W(i.firstElementChild)}const Y=`math-with-linebreaks`;function X(e,t){t=Math.min(t,parseFloat(e.getAttribute(Z)));let n=e.shadowRoot.lastElementChild;if(n.childElementCount>1){let e=V(`mrow`);for(;n.firstElementChild;)e.appendChild(n.firstElementChild);n.appendChild(e)}Ee.set(e.shadowRoot),Ie(n,t);let r=Array.from(n.querySelectorAll(`mtable[${F}]`));r.length>0?r.forEach(e=>{e.setAttribute(I,t.toString()),Array.from(e.children).forEach(e=>{let n=e.firstElementChild;W(n),n.firstElementChild.getBoundingClientRect().right-n.getBoundingClientRect().left>t&<(n,t)})}):parseInt(e.getAttribute(Z))>=t&&(n.setAttribute(L,JSON.stringify(Me(n,`first`))),lt(n,t))}const Z=`data-full-width`,Q=`data-linebreak-width`;function $(e,t){let i=n(t);e.shadowRoot.appendChild(i);let a=i.lastElementChild.getBoundingClientRect().right-i.firstElementChild.getBoundingClientRect().left;i.hasAttribute(`maxwidth`)&&(a=Math.min(a,r(i,i.getAttribute(`maxwidth`)))),e.setAttribute(Z,a.toString()),X(e,a),e.setAttribute(Q,(2*a).toString())}function ut(e){let t=getComputedStyle(e).getPropertyValue(`display`),n=e.hasAttribute(`display`)?e.getAttribute(`display`):`inline`;if(t===`inline`||n===`inline`)return null;if(e.tagName.toLowerCase()===Y||e.parentElement.tagName.toLowerCase()===Y)return e;{let t=e.parentElement,n=e.nextElementSibling,r=document.createElement(Y);return r.appendChild(e),t.insertBefore(r,n),it(e),$(r,e),null}}t.add(`math`,ut);const dt=new ResizeObserver(e=>{for(let t of e)if(t.target.tagName.toLowerCase()===Y){let e=t.target;if(t.contentRect.width(e.namespaceURI==`http://www.w3.org/1998/Math/MathML`&&(e.style.cursor=`pointer`,e.tabIndex=0,e.setAttribute(`role`,`link`),e.addEventListener(`click`,e=>{document.location=e.currentTarget.getAttribute(`href`)}),e.addEventListener(`keydown`,e=>{e.key==`Enter`&&(document.location=e.currentTarget.getAttribute(`href`))}),e.addEventListener(`mouseover`,e=>{e.currentTarget.style.textDecoration=`solid underline`}),e.addEventListener(`mouseout`,e=>{e.currentTarget.style.textDecoration=``})),e));export{t as _MathTransforms}; \ No newline at end of file