Skip to content

P3 enhancement: H₀ Unicode subscript anchors don't match flatten output (post-#85 follow-up) #90

Description

@kiki830621

Problem

From verification of #85 (Devil's Advocate §1.1, BLOCKING-ish refutation of Requirements Reviewer):
Issue body explicitly names 「假設 H₀ 為...」 as the academic-paper inline-math anchor pattern. Probe-verified post-fix MathSubSuperScript.visibleText returns "H0" (ASCII H + ASCII 0), but user-typed anchor is "H₀" (Unicode U+2080 SUBSCRIPT ZERO). "H₀".contains("H0") is false.
— Source: team:devils-advocate + team:requirements

The post-#85 fix in v0.21.5 covers the t (<m:r><m:t>t</m:t></m:r>) case — primary repro from issue body. But the secondary inline-math identifier pattern with subscripts (H₀, , α₁) emits the base + subscript text as plain ASCII. Anchors typed using the natural Unicode glyph still 0-match.

Verified by executable probe against e53fa00:

[PROBE A] H₀ visibleText = 'H0' (expected 'H0', user anchor 'H₀' won't match)

Type

enhancement (or bug — depends on whether you count "headline use case still broken" as bug)

Proposed Solution

Two paths:

Path A — Unicode subscript/superscript translation in MathSubSuperScript.visibleText

In Sources/OOXMLSwift/Models/MathComponent.swift, modify MathSubSuperScript.visibleText:

public extension MathSubSuperScript {
    var visibleText: String {
        let baseText = base.visibleText
        let subText = (sub?.visibleText).map { translateToUnicodeSubscript($0) } ?? ""
        let supText = (sup?.visibleText).map { translateToUnicodeSuperscript($0) } ?? ""
        return baseText + subText + supText
    }
}

private func translateToUnicodeSubscript(_ s: String) -> String {
    // U+2080..U+2089 for digits 0-9; common subscript letters U+2090..U+209C.
    // Fall back to original char when no Unicode subscript exists.
    return String(s.map { c in
        switch c {
        case "0": return ""; case "1": return ""; case "2": return "";
        case "3": return ""; case "4": return ""; case "5": return "";
        case "6": return ""; case "7": return ""; case "8": return "";
        case "9": return ""
        case "+": return ""; case "-": return ""; case "=": return "";
        case "a": return ""; case "e": return ""; case "i": return "";
        // ... extend as needed
        default: return c
        }
    })
}
// translateToUnicodeSuperscript similar with U+2070..U+2079 + variants

Pros: anchors-as-typed work. Cons: lossy (subscript j has no canonical Unicode form — ⱼ is Latin); breaks anchors typed as "H0" ASCII (now mismatched against "H₀" Unicode).

Path B — Bidirectional matching at anchor lookup (recommended)

Don't change visibleText; change the anchor matcher. In Body.findBodyChildContainingText (or wherever anchor matching lives), normalize BOTH the haystack and the needle to a canonical form (e.g., strip Unicode subscripts/superscripts to ASCII): "H₀ + H₁".normalized == "H0 + H1". Caller can search for either form.

Pros: opt-in via a new AnchorLookupOptions field (e.g., caseInsensitiveAndScriptInsensitive); doesn't break existing callers; lossless on the AST side. Cons: requires the public API change in #86 (anchor lookup public surface).

Path C — Doc-only caveat

Document the limitation in the flattenedDisplayText docstring + #85 closing comment. Punt the fix to v0.22+ when MCP-layer string-matching gets a richer model.

Impact

LOW-MEDIUM. Depends on path. Path A is 30-40 LOC; Path B requires #86 first; Path C is documentation only.

Probably should pair with #86 (public anchor lookup API) so the matcher gets unified options for case + script + Unicode normalization in one redesign.

Related


Current Status

Key Decisions

  • Path B implemented: bidirectional lookup-time math-script normalization, exact matching remains default.
  • OOXMLSwift adds AnchorLookupOptions(mathScriptInsensitive:) and keeps MathSubSuperScript.visibleText / flattenedDisplayText() unchanged.
  • che-word-mcp exposes match_options.math_script_insensitive on scoped insertion text anchors.

Scope Changes

  • Implementation split into two PRs because che-word-mcp depends on the new OOXMLSwift public API.
  • che-word-mcp#115 is draft while it points at the OOXMLSwift feature branch; after OOXMLSwift release, switch back to a version dependency.

Blocking

  • OOXMLSwift PR/release blocks che-word-mcp merge readiness.

Related Commits

Current Status

Phase: diagnosed
Last updated: 2026-05-25 by /idd-diagnose (cross-repo chain context)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions