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 默认密钥已清空
This commit is contained in:
2026-09-29 09:43:13 +08:00
commit 3271fee5cc
360 changed files with 86082 additions and 0 deletions

View File

@@ -0,0 +1,851 @@
<?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>