Files
DoLuEdu-WordFileParse/scripts/omml_mathml2latex/core/OMML2MML.xsl
Quentin 3271fee5cc init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
      接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)

公式支持三类来源:
- OMML(Word 2007+ 公式)      → Python omml_mathml2latex
- MathType OLE 对象            → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力)      → EqToLatexUtil(本次新增)

本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
   移植自 jby backend-services office-core,EQ 语法 → LaTeX
   (\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
     \a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
   品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
   EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
   (原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)

修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留

安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
      OssService 的 @Value 默认密钥已清空
2026-09-29 09:43:13 +08:00

851 lines
40 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:mml="http://www.w3.org/1998/Math/MathML"
xmlns="http://www.w3.org/1998/Math/MathML"
exclude-result-prefixes="m w">
<xsl:output method="xml" indent="yes" encoding="UTF-8"/>
<!-- Root template -->
<xsl:template match="/">
<xsl:apply-templates select="//m:oMath"/>
</xsl:template>
<!-- Main math element -->
<xsl:template match="m:oMath">
<math>
<xsl:apply-templates select="*"/>
</math>
</xsl:template>
<!-- Text run -->
<xsl:template match="m:r">
<xsl:apply-templates select="m:t"/>
</xsl:template>
<!-- Text content -->
<xsl:template match="m:t">
<!-- Get style from parent <m:r><m:rPr><m:sty> -->
<xsl:variable name="sty" select="../m:rPr/m:sty/@m:val"/>
<!-- Get Word run properties (w:rPr) - check for italic (w:i) -->
<!-- w:rPr is a sibling of m:t (both are children of m:r) -->
<!-- CRITICAL: w:i with @w:val="0" or "false" means NOT italic! -->
<!-- Only treat as italic if w:i exists AND val is not "0" or "false" -->
<xsl:variable name="has_word_italic" select="boolean(../w:rPr/w:i[not(@w:val = '0') and not(@w:val = 'false')])"/>
<!-- Get math "normal" style (m:nor) - text mode button -->
<xsl:variable name="has_math_normal" select="boolean(../m:rPr/m:nor)"/>
<xsl:choose>
<xsl:when test="normalize-space(.) = ''">
<!-- Skip empty text -->
</xsl:when>
<xsl:when test="number(.) = number(.)">
<mn><xsl:value-of select="normalize-space(.)"/></mn>
</xsl:when>
<!-- Fence delimiters: parentheses, brackets, pipes (with fence="true") -->
<xsl:when test=". = '(' or . = ')' or . = '[' or . = ']' or . = '|'">
<mo fence="true"><xsl:value-of select="normalize-space(.)"/></mo>
</xsl:when>
<!-- Plain operators (no fence attribute) -->
<!-- NOTE: { and } are converted to markers to distinguish source braces from generated LaTeX braces -->
<xsl:when test=". = '{' or . = '}' or . = '+' or . = '-' or . = '=' or . = '&lt;' or . = '&gt;' or . = '±' or . = '≠' or . = '≤' or . = '≥' or . = '×' or . = '÷' or . = '∞'">
<mo>
<xsl:choose>
<xsl:when test=". = '{'">__LBRACE__</xsl:when>
<xsl:when test=". = '}'">__RBRACE__</xsl:when>
<xsl:otherwise><xsl:value-of select="normalize-space(.)"/></xsl:otherwise>
</xsl:choose>
</mo>
</xsl:when>
<!-- Mixed operators/delimiters (like ")+" or ")=" or "(A" or "={" or ",}") -->
<!-- Check if string contains multiple operator/delimiter characters -->
<xsl:when test="string-length(normalize-space(.)) &gt; 1 and (contains(., '(') or contains(., ')') or contains(., '[') or contains(., ']') or contains(., '{') or contains(., '}') or contains(., '+') or contains(., '-') or contains(., '=') or contains(., '&lt;') or contains(., '&gt;'))">
<!-- Split into individual characters and classify each one -->
<!-- Pass style info so letters get proper mathvariant (e.g., mathvariant="normal" for m:nor text mode) -->
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="normalize-space(.)"/>
<xsl:with-param name="math_variant">
<xsl:choose>
<xsl:when test="$has_word_italic">italic</xsl:when>
<xsl:when test="$sty = 'b' or $sty = 'bi'">bold</xsl:when>
<xsl:when test="$sty = 'p'">normal</xsl:when>
<xsl:when test="$has_math_normal">normal</xsl:when>
<xsl:otherwise></xsl:otherwise>
</xsl:choose>
</xsl:with-param>
</xsl:call-template>
</xsl:when>
<xsl:when test="contains(., '|')">
<!-- Special handling for text containing | (like "x|x", "A|B", or "||") -->
<!-- Split by | and create separate elements with fence="true" for absolute values -->
<xsl:variable name="before" select="substring-before(., '|')"/>
<xsl:variable name="after" select="substring-after(., '|')"/>
<!-- Process before part (plain text) -->
<xsl:if test="$before != ''">
<xsl:choose>
<!-- If before part contains operators or is multi-char, use split-operators -->
<xsl:when test="string-length($before) &gt; 1">
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="$before"/>
</xsl:call-template>
</xsl:when>
<!-- Single character - output as mi -->
<xsl:otherwise>
<mi><xsl:value-of select="$before"/></mi>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<!-- Output the | as fence for potential absolute value -->
<mo fence="true">|</mo>
<!-- Process after part recursively if it contains more | or operators -->
<xsl:if test="$after != ''">
<xsl:choose>
<!-- If after part is just |, output as fence -->
<xsl:when test="$after = '|'">
<mo fence="true">|</mo>
</xsl:when>
<!-- If after part contains more |, recursively process this m:t content -->
<xsl:when test="contains($after, '|')">
<xsl:call-template name="process-text-with-pipe">
<xsl:with-param name="text" select="$after"/>
</xsl:call-template>
</xsl:when>
<!-- If after part contains operators, use split-operators -->
<xsl:when test="string-length($after) &gt; 1 and (contains($after, '+') or contains($after, '-') or contains($after, '='))">
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="$after"/>
</xsl:call-template>
</xsl:when>
<!-- Single character or simple text - output as mi -->
<xsl:otherwise>
<mi><xsl:value-of select="$after"/></mi>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:when>
<xsl:when test="string-length(normalize-space(.)) = 1 and not(contains('0123456789', .))">
<mi>
<!-- Apply mathvariant based on style -->
<!-- Note: m:sty='i' (italic) is NOT handled here as italic is the default for variables -->
<!-- Priority: italic (w:i explicit) > bold > plain (m:sty='p') > normal (m:nor) -->
<xsl:choose>
<xsl:when test="$has_word_italic">
<xsl:attribute name="mathvariant">italic</xsl:attribute>
</xsl:when>
<xsl:when test="$sty = 'b' or $sty = 'bi'">
<xsl:attribute name="mathvariant">bold</xsl:attribute>
</xsl:when>
<xsl:when test="$sty = 'p'">
<xsl:attribute name="mathvariant">normal</xsl:attribute>
</xsl:when>
<xsl:when test="$has_math_normal">
<xsl:attribute name="mathvariant">normal</xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:value-of select="normalize-space(.)"/>
</mi>
</xsl:when>
<xsl:otherwise>
<mi>
<!-- Apply mathvariant for multi-character identifiers too -->
<!-- Note: m:sty='i' (italic) is NOT handled here as italic is the default for variables -->
<!-- Priority: italic (w:i explicit) > bold > plain (m:sty='p') > normal (m:nor) -->
<xsl:choose>
<xsl:when test="$has_word_italic">
<xsl:attribute name="mathvariant">italic</xsl:attribute>
</xsl:when>
<xsl:when test="$sty = 'b' or $sty = 'bi'">
<xsl:attribute name="mathvariant">bold</xsl:attribute>
</xsl:when>
<xsl:when test="$sty = 'p'">
<xsl:attribute name="mathvariant">normal</xsl:attribute>
</xsl:when>
<xsl:when test="$has_math_normal">
<xsl:attribute name="mathvariant">normal</xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:value-of select="normalize-space(.)"/>
</mi>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Superscript -->
<xsl:template match="m:sSup">
<msup>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:sup"/>
</msup>
</xsl:template>
<!-- Subscript -->
<xsl:template match="m:sSub">
<msub>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:sub"/>
</msub>
</xsl:template>
<!-- Sub-superscript -->
<xsl:template match="m:sSubSup">
<msubsup>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:sub"/>
<xsl:apply-templates select="m:sup"/>
</msubsup>
</xsl:template>
<!-- Pre-script (left-side subscript/superscript) -->
<!-- Used for nuclear notation like _{2}^{3}He -->
<xsl:template match="m:sPre">
<mmultiscripts>
<xsl:apply-templates select="m:e"/>
<mprescripts/>
<xsl:apply-templates select="m:sub"/>
<xsl:apply-templates select="m:sup"/>
</mmultiscripts>
</xsl:template>
<!-- Fraction -->
<!-- m:type values: bar (default), noBar, lin (linear), skw (skewed) -->
<xsl:template match="m:f">
<xsl:variable name="type" select="m:fPr/m:type/@m:val"/>
<mfrac>
<!-- Add linethickness="0" for noBar fractions (stacked without bar) -->
<xsl:if test="$type = 'noBar'">
<xsl:attribute name="linethickness">0</xsl:attribute>
</xsl:if>
<!-- Add bevelled="true" for linear fractions -->
<xsl:if test="$type = 'lin'">
<xsl:attribute name="bevelled">true</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="m:num"/>
<xsl:apply-templates select="m:den"/>
</mfrac>
</xsl:template>
<!-- Radical -->
<xsl:template match="m:rad">
<xsl:choose>
<xsl:when test="m:deg">
<mroot>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:deg"/>
</mroot>
</xsl:when>
<xsl:otherwise>
<msqrt>
<xsl:apply-templates select="m:e"/>
</msqrt>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Parentheses/delimiters -->
<xsl:template match="m:d">
<mrow>
<!-- Opening delimiter: skip if begChr has empty or whitespace-only val -->
<xsl:choose>
<xsl:when test="not(m:dPr/m:begChr)">
<mo fence="true">(</mo>
</xsl:when>
<xsl:when test="normalize-space(m:dPr/m:begChr/@m:val) != ''">
<mo fence="true">
<xsl:value-of select="m:dPr/m:begChr/@m:val"/>
</mo>
</xsl:when>
<!-- begChr with empty/whitespace val → no opening delimiter -->
</xsl:choose>
<!-- Handle multiple m:e elements with separators -->
<xsl:for-each select="m:e">
<xsl:apply-templates select="."/>
<!-- Insert separator | between elements (except after the last one) -->
<xsl:if test="position() != last()">
<mo>|</mo>
</xsl:if>
</xsl:for-each>
<!-- Closing delimiter: skip if endChr has empty or whitespace-only val -->
<xsl:choose>
<xsl:when test="not(m:dPr/m:endChr)">
<mo fence="true">)</mo>
</xsl:when>
<xsl:when test="normalize-space(m:dPr/m:endChr/@m:val) != ''">
<mo fence="true">
<xsl:value-of select="m:dPr/m:endChr/@m:val"/>
</mo>
</xsl:when>
<!-- endChr with empty/whitespace val → no closing delimiter -->
</xsl:choose>
</mrow>
</xsl:template>
<!-- Function -->
<xsl:template match="m:func">
<mrow>
<xsl:apply-templates select="m:fName"/>
<xsl:apply-templates select="m:e"/>
</mrow>
</xsl:template>
<!-- Matrix -->
<xsl:template match="m:m">
<mtable>
<xsl:apply-templates select="m:mr"/>
</mtable>
</xsl:template>
<!-- Matrix row -->
<xsl:template match="m:mr">
<mtr>
<xsl:apply-templates select="m:e"/>
</mtr>
</xsl:template>
<!-- Equation array (for piecewise functions) -->
<xsl:template match="m:eqArr">
<mtable>
<!-- Each <m:e> child represents one row in the piecewise function -->
<xsl:for-each select="m:e">
<mtr>
<mtd>
<xsl:apply-templates select="node()"/>
</mtd>
</mtr>
</xsl:for-each>
</mtable>
</xsl:template>
<!-- Bar (overbar/underbar) -->
<!-- OMML spec: no pos attribute = underline (default), pos="top" = overline, pos="bot" = underline -->
<xsl:template match="m:bar">
<xsl:choose>
<xsl:when test="m:barPr/m:pos/@m:val = 'top'">
<mover>
<xsl:apply-templates select="m:e"/>
<mo>‾</mo>
</mover>
</xsl:when>
<xsl:otherwise>
<!-- Default: no pos attribute or pos="bot" both mean underline -->
<munder>
<xsl:apply-templates select="m:e"/>
<mo>‾</mo>
</munder>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Limits -->
<!-- limLow: Lower limit position (content BELOW the base, like underscript) -->
<xsl:template match="m:limLow">
<munder>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:lim"/>
</munder>
</xsl:template>
<!-- limUpp: Upper limit position (content ABOVE the base, like overscript) -->
<xsl:template match="m:limUpp">
<mover>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:lim"/>
</mover>
</xsl:template>
<xsl:template match="m:limUppLow">
<munderover>
<xsl:apply-templates select="m:e"/>
<xsl:apply-templates select="m:lim[1]"/>
<xsl:apply-templates select="m:lim[2]"/>
</munderover>
</xsl:template>
<!-- N-ary operators (integrals, sums, products) -->
<xsl:template match="m:nary">
<xsl:variable name="chr" select="m:naryPr/m:chr/@m:val"/>
<xsl:variable name="limLoc" select="m:naryPr/m:limLoc/@m:val"/>
<xsl:variable name="subHide" select="m:naryPr/m:subHide/@m:val"/>
<xsl:variable name="supHide" select="m:naryPr/m:supHide/@m:val"/>
<xsl:choose>
<!-- Both limits hidden: just operator and integrand -->
<xsl:when test="$subHide = '1' and $supHide = '1'">
<mrow>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:e"/>
</mrow>
</xsl:when>
<!-- Limits as subscript/superscript (default for integrals) -->
<xsl:when test="not($limLoc) or $limLoc = 'subSup'">
<xsl:choose>
<!-- Both sub and sup present -->
<xsl:when test="m:sub and m:sup">
<msubsup>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sub"/>
<xsl:apply-templates select="m:sup"/>
</msubsup>
<xsl:apply-templates select="m:e"/>
</xsl:when>
<!-- Only subscript -->
<xsl:when test="m:sub">
<msub>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sub"/>
</msub>
<xsl:apply-templates select="m:e"/>
</xsl:when>
<!-- Only superscript -->
<xsl:when test="m:sup">
<msup>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sup"/>
</msup>
<xsl:apply-templates select="m:e"/>
</xsl:when>
<!-- No limits -->
<xsl:otherwise>
<mrow>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:e"/>
</mrow>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Limits as under/over (for sums, products) -->
<xsl:when test="$limLoc = 'undOvr'">
<mrow>
<xsl:choose>
<!-- Both under and over -->
<xsl:when test="m:sub and m:sup and not($subHide = '1') and not($supHide = '1')">
<munderover>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sub"/>
<xsl:apply-templates select="m:sup"/>
</munderover>
</xsl:when>
<!-- Only under -->
<xsl:when test="m:sub and not($subHide = '1')">
<munder>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sub"/>
</munder>
</xsl:when>
<!-- Only over -->
<xsl:when test="m:sup and not($supHide = '1')">
<mover>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:sup"/>
</mover>
</xsl:when>
<!-- No visible limits -->
<xsl:otherwise>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>∫</xsl:otherwise>
</xsl:choose>
</mo>
</xsl:otherwise>
</xsl:choose>
<xsl:apply-templates select="m:e"/>
</mrow>
</xsl:when>
</xsl:choose>
</xsl:template>
<!-- Accent (vector arrows, dots, etc.) -->
<xsl:template match="m:acc">
<xsl:variable name="chr" select="m:accPr/m:chr/@m:val"/>
<mover>
<!-- Add data attribute to distinguish default accents from explicit ones -->
<xsl:if test="not($chr)">
<xsl:attribute name="data-default-accent">true</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>⃗</xsl:otherwise><!-- Default accent (should be hat in most cases) -->
</xsl:choose>
</mo>
</mover>
</xsl:template>
<!-- Group character (for over/under arrows with text) -->
<xsl:template match="m:groupChr">
<xsl:variable name="chr" select="m:groupChrPr/m:chr/@m:val"/>
<xsl:variable name="pos" select="m:groupChrPr/m:pos/@m:val"/>
<xsl:variable name="vertJc" select="m:groupChrPr/m:vertJc/@m:val"/>
<xsl:choose>
<!-- vertJc='bot': Character at bottom (base), content above → mover(char, content) -->
<!-- This is used for extensible arrows like \xrightarrow[]{text} or \overset{text}{arrow} -->
<xsl:when test="$vertJc = 'bot'">
<mover accent="false">
<mo stretchy="true">
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>→</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="m:e"/>
</mover>
</xsl:when>
<!-- pos='bot': Character position at bottom, content above → munder -->
<xsl:when test="$pos = 'bot'">
<munder>
<xsl:apply-templates select="m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>⏟</xsl:otherwise><!-- Default underbrace -->
</xsl:choose>
</mo>
</munder>
</xsl:when>
<!-- pos='top' (or default): Character above content → mover(content, char) -->
<xsl:otherwise>
<mover>
<xsl:apply-templates select="m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$chr"><xsl:value-of select="$chr"/></xsl:when>
<xsl:otherwise>⏞</xsl:otherwise><!-- Default overbrace -->
</xsl:choose>
</mo>
</mover>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Box (but detect if it contains groupChr for reaction arrows) -->
<xsl:template match="m:box">
<xsl:choose>
<!-- Skip boxes with opEmu="1" (operator emulation - should be transparent) -->
<!-- This means process all children in m:e transparently -->
<xsl:when test="m:boxPr/m:opEmu/@m:val='1'">
<xsl:apply-templates select="m:e/*"/>
</xsl:when>
<!-- If box contains ONLY a single direct groupChr, handle specially -->
<!-- This is for cases like reaction arrows: text above/below extensible arrow -->
<xsl:when test="count(m:e/*) = 1 and m:e/m:groupChr">
<xsl:variable name="groupChr" select="m:e/m:groupChr"/>
<xsl:variable name="arrow" select="$groupChr/m:groupChrPr/m:chr/@m:val"/>
<xsl:variable name="vertJc" select="$groupChr/m:groupChrPr/m:vertJc/@m:val"/>
<xsl:variable name="pos" select="$groupChr/m:groupChrPr/m:pos/@m:val"/>
<xsl:choose>
<xsl:when test="$vertJc = 'bot'">
<!-- Arrow at bottom with text above (like \xrightarrow[]{text}) -->
<!-- For extensible arrows: mover with arrow base, text above -->
<mover accent="false">
<mo stretchy="true">
<xsl:choose>
<xsl:when test="$arrow"><xsl:value-of select="$arrow"/></xsl:when>
<xsl:otherwise>→</xsl:otherwise>
</xsl:choose>
</mo>
<xsl:apply-templates select="$groupChr/m:e"/>
</mover>
</xsl:when>
<xsl:when test="$pos = 'top'">
<!-- Arrow at top: \overrightarrow pattern -->
<!-- mover with content base, arrow above -->
<mover>
<xsl:apply-templates select="$groupChr/m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$arrow"><xsl:value-of select="$arrow"/></xsl:when>
<xsl:otherwise>→</xsl:otherwise>
</xsl:choose>
</mo>
</mover>
</xsl:when>
<xsl:otherwise>
<!-- Default: Vector arrow over content -->
<mover>
<xsl:apply-templates select="$groupChr/m:e"/>
<mo>
<xsl:choose>
<xsl:when test="$arrow"><xsl:value-of select="$arrow"/></xsl:when>
<xsl:otherwise>→</xsl:otherwise>
</xsl:choose>
</mo>
</mover>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Skip phantom/layout boxes (argSz with negative value) -->
<!-- These are used for sizing/spacing control, not actual visible boxes -->
<xsl:when test="m:e/m:argPr/m:argSz/@m:val &lt; 0">
<xsl:apply-templates select="m:e"/>
</xsl:when>
<!-- Regular box -->
<xsl:otherwise>
<menclose notation="box">
<xsl:apply-templates select="m:e"/>
</menclose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Border box with strikethrough and border notation support -->
<xsl:template match="m:borderBox">
<xsl:variable name="props" select="m:borderBoxPr"/>
<!-- Check for strikethrough properties -->
<xsl:variable name="strikeBLTR" select="boolean($props/m:strikeBLTR)"/>
<xsl:variable name="strikeTLBR" select="boolean($props/m:strikeTLBR)"/>
<xsl:variable name="strikeH" select="boolean($props/m:strikeH)"/>
<xsl:variable name="strikeV" select="boolean($props/m:strikeV)"/>
<!-- Check for border hiding properties -->
<xsl:variable name="hideTop" select="boolean($props/m:hideTop)"/>
<xsl:variable name="hideBot" select="boolean($props/m:hideBot)"/>
<xsl:variable name="hideLeft" select="boolean($props/m:hideLeft)"/>
<xsl:variable name="hideRight" select="boolean($props/m:hideRight)"/>
<!-- Determine notation attribute -->
<xsl:variable name="notation">
<xsl:choose>
<!-- Combined diagonal strikes → xcancel -->
<xsl:when test="$strikeBLTR and $strikeTLBR">updiagonalstrike downdiagonalstrike</xsl:when>
<!-- Single diagonal strike BLTR → cancel (updiagonal) -->
<xsl:when test="$strikeBLTR">updiagonalstrike</xsl:when>
<!-- Single diagonal strike TLBR → bcancel (downdiagonal) -->
<xsl:when test="$strikeTLBR">downdiagonalstrike</xsl:when>
<!-- Combined horizontal and vertical → cancel -->
<xsl:when test="$strikeH and $strikeV">horizontalstrike verticalstrike</xsl:when>
<!-- Single horizontal strike → cancel -->
<xsl:when test="$strikeH">horizontalstrike</xsl:when>
<!-- Single vertical strike → bcancel -->
<xsl:when test="$strikeV">verticalstrike</xsl:when>
<!-- Single side visible -->
<xsl:when test="not($hideTop) and $hideBot and $hideLeft and $hideRight">top</xsl:when>
<xsl:when test="$hideTop and not($hideBot) and $hideLeft and $hideRight">bottom</xsl:when>
<xsl:when test="$hideTop and $hideBot and not($hideLeft) and $hideRight">left</xsl:when>
<xsl:when test="$hideTop and $hideBot and $hideLeft and not($hideRight)">right</xsl:when>
<!-- Two sides visible -->
<xsl:when test="not($hideTop) and not($hideBot) and $hideLeft and $hideRight">top bottom</xsl:when>
<xsl:when test="not($hideTop) and $hideBot and not($hideLeft) and $hideRight">top left</xsl:when>
<xsl:when test="not($hideTop) and $hideBot and $hideLeft and not($hideRight)">top right</xsl:when>
<xsl:when test="$hideTop and not($hideBot) and not($hideLeft) and $hideRight">bottom left</xsl:when>
<xsl:when test="$hideTop and not($hideBot) and $hideLeft and not($hideRight)">bottom right</xsl:when>
<xsl:when test="$hideTop and $hideBot and not($hideLeft) and not($hideRight)">left right</xsl:when>
<!-- Three sides visible -->
<xsl:when test="not($hideTop) and not($hideBot) and not($hideLeft) and $hideRight">top bottom left</xsl:when>
<xsl:when test="not($hideTop) and not($hideBot) and $hideLeft and not($hideRight)">top bottom right</xsl:when>
<xsl:when test="not($hideTop) and $hideBot and not($hideLeft) and not($hideRight)">top left right</xsl:when>
<xsl:when test="$hideTop and not($hideBot) and not($hideLeft) and not($hideRight)">bottom left right</xsl:when>
<!-- All four sides visible (or none hidden) -->
<xsl:when test="not($hideTop or $hideBot or $hideLeft or $hideRight)">box</xsl:when>
<!-- Default: roundedbox -->
<xsl:otherwise>roundedbox</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<menclose notation="{$notation}">
<xsl:apply-templates select="m:e"/>
</menclose>
</xsl:template>
<!-- Math element base -->
<xsl:template match="m:e | m:num | m:den | m:sup | m:sub | m:deg | m:fName | m:lim">
<mrow>
<xsl:apply-templates select="*"/>
</mrow>
</xsl:template>
<!-- Template to split mixed operator/delimiter strings character by character -->
<!-- Recursive template to process text containing | characters -->
<xsl:template name="process-text-with-pipe">
<xsl:param name="text"/>
<xsl:if test="string-length($text) &gt; 0">
<xsl:choose>
<!-- If text contains |, split and recurse -->
<xsl:when test="contains($text, '|')">
<xsl:variable name="before" select="substring-before($text, '|')"/>
<xsl:variable name="after" select="substring-after($text, '|')"/>
<!-- Output before part if not empty -->
<xsl:if test="$before != ''">
<xsl:choose>
<!-- If before is single char, output as mi or mn -->
<xsl:when test="string-length($before) = 1">
<xsl:choose>
<xsl:when test="contains('0123456789', $before)">
<mn><xsl:value-of select="$before"/></mn>
</xsl:when>
<xsl:otherwise>
<mi><xsl:value-of select="$before"/></mi>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Multi-char - use split-operators -->
<xsl:otherwise>
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="$before"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<!-- Output | as fence -->
<mo fence="true">|</mo>
<!-- Recurse on after part -->
<xsl:if test="$after != ''">
<xsl:call-template name="process-text-with-pipe">
<xsl:with-param name="text" select="$after"/>
</xsl:call-template>
</xsl:if>
</xsl:when>
<!-- No more | in text - output as-is -->
<xsl:otherwise>
<xsl:choose>
<!-- Single char -->
<xsl:when test="string-length($text) = 1">
<xsl:choose>
<xsl:when test="contains('0123456789', $text)">
<mn><xsl:value-of select="$text"/></mn>
</xsl:when>
<xsl:otherwise>
<mi><xsl:value-of select="$text"/></mi>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<!-- Multi-char - use split-operators for proper handling -->
<xsl:otherwise>
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="$text"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
<xsl:template name="split-operators">
<xsl:param name="text"/>
<!-- Style variant to apply to identifier (letter) elements: 'normal', 'bold', 'italic', or empty -->
<xsl:param name="math_variant" select="''"/>
<xsl:if test="string-length($text) &gt; 0">
<!-- Process first character -->
<xsl:variable name="char" select="substring($text, 1, 1)"/>
<xsl:choose>
<!-- Fence delimiters with fence="true" -->
<xsl:when test="$char = '(' or $char = ')' or $char = '[' or $char = ']' or $char = '|'">
<mo fence="true"><xsl:value-of select="$char"/></mo>
</xsl:when>
<!-- Plain operators (no fence attribute) -->
<!-- NOTE: { and } are converted to markers to distinguish source braces from generated LaTeX braces -->
<xsl:when test="$char = '+' or $char = '-' or $char = '=' or $char = '&lt;' or $char = '&gt;' or $char = '*' or $char = '/' or $char = '±' or $char = '≠' or $char = '≤' or $char = '≥' or $char = '×' or $char = '÷'">
<mo><xsl:value-of select="$char"/></mo>
</xsl:when>
<!-- Source braces - use markers -->
<xsl:when test="$char = '{'">
<mo>__LBRACE__</mo>
</xsl:when>
<xsl:when test="$char = '}'">
<mo>__RBRACE__</mo>
</xsl:when>
<!-- Numbers -->
<xsl:when test="contains('0123456789', $char)">
<mn><xsl:value-of select="$char"/></mn>
</xsl:when>
<!-- Identifiers (letters) -->
<xsl:otherwise>
<mi>
<!-- Apply mathvariant from parent run style if available -->
<xsl:if test="$math_variant != ''">
<xsl:attribute name="mathvariant"><xsl:value-of select="$math_variant"/></xsl:attribute>
</xsl:if>
<xsl:value-of select="$char"/>
</mi>
</xsl:otherwise>
</xsl:choose>
<!-- Recursively process remaining characters -->
<xsl:if test="string-length($text) &gt; 1">
<xsl:call-template name="split-operators">
<xsl:with-param name="text" select="substring($text, 2)"/>
<xsl:with-param name="math_variant" select="$math_variant"/>
</xsl:call-template>
</xsl:if>
</xsl:if>
</xsl:template>
<!-- Properties elements - ignore -->
<xsl:template match="m:rPr | m:ctrlPr | m:dPr | m:fPr | m:funcPr | m:limLowPr | m:limUppPr | m:naryPr | m:phantPr | m:radPr | m:sPrePr | m:sSubPr | m:sSubSupPr | m:sSupPr | m:accPr | m:barPr | m:boxPr | m:borderBoxPr | m:dPr | m:eqArrPr | m:groupChrPr | m:mPr | w:rPr">
<!-- Ignore formatting properties -->
</xsl:template>
<!-- Control properties - ignore -->
<xsl:template match="m:ctrlPr">
<!-- Ignore control properties -->
</xsl:template>
<!-- Default template -->
<xsl:template match="*">
<xsl:apply-templates select="*"/>
</xsl:template>
</xsl:stylesheet>