Skip to content

feat(android): math plugin review follow-ups - #936

Open
jnowakow wants to merge 41 commits into
mainfrom
feat/android-math-plugin-follow-ups
Open

jnowakow wants to merge 41 commits into
mainfrom
feat/android-math-plugin-follow-ups

Conversation

@jnowakow

@jnowakow jnowakow commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

What/Why?

Review follow-ups to the Android math plugin (#873):

  • Tables render on the render thread. New RenderedTable renders and measures cells off the main thread, like text segments. Cell renderers include plugin renderers, which must not run on the main thread. TableContainerView now only builds the grid. StyleConfig.withParagraphOverride, which mutated a shared config, is replaced by a withParagraphStyle copy. This also fixes withExtension dropping highlightStyle.
  • Parsed equations are cached (DisplayListCache, LRU of 256), so streaming no longer re-parses every equation on each token.
  • Screen readers read plugin spans' plain text instead of the object-replacement character. Spanned.readableText is shared with copy.
  • setSelectionMenuConfig reaches plugin segment views through a new SelectionMenuConfigurable interface, which MathContainerView implements.
  • Whitespace next to LaTeX delimiters is dropped from the source (the line breaks of a multi-line $$ block), so it stays out of error events, copies and the fallback. A trailing \ is kept.

Testing

  • New and updated unit tests: DisplayListCacheTest, PluginSpanAccessibilityTest, TableSegmentTest, TableContainerViewTest, PluginViewIntegrationTest, PluginNodeRendererTest, StyleConfigExtensionTest.
  • ui, math and compose unit tests and ktlint pass after syncing with main.
  • Android example app, Text screen: tables and math examples render correctly.
math-tables-small.mp4

PR Checklist

  • Code compiles and runs on iOS
  • Code compiles and runs on Android
  • Updated documentation/README if applicable
  • Ran example app to verify changes
  • E2E tests are passing
  • Required E2E tests have been added (if applicable)

jnowakow and others added 30 commits September 23, 2026 08:33
Adds the extension surface an optional feature needs to render node types
core does not: a global registry that publishes an immutable snapshot, hooks
for node renderers and block segments, a keyed style extension map on
StyleConfig, and a generic per-view PluginEvent channel.

Core keeps no knowledge of any particular plugin. The Class.forName lookup
for MathInlineSpan in HTMLGenerator gives way to a PluginInlineSpan
interface, and the dead IS_MATH_ENABLED flag is gone. Segment signatures are
computed by core from plugin-supplied bytes rather than invented by plugins,
because an unstable signature means wrong view recycling on every token of
streamed content.

"No plugin installed" is the default state, so every hook has a fallback:
unclaimed latex nodes echo their own source, delimiters included, and a
plugin that declines a node has it rendered as text rather than dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MarkdownStyleBuilder gains updatePluginPatch, so a plugin module can ship its
own style block as an extension function that merges into an earlier one the
way core's own blocks do, and the resolved value lands in StyleConfig's
extension map. Patches are data classes rather than lambdas: style layers are
compared for equality to decide whether to recompose, and a lambda is never
equal to an identically written one.

Adds MarkdownStyle.resolveStyleConfig, which does outside a composition what
EnrichedMarkdownText does inside one. A plugin module otherwise has no way to
assert on what its DSL produced without waiting on a Dispatchers.Default hop
the test scheduler does not control.

EnrichedMarkdownText gains onPluginEvent, appended last so no positional call
site shifts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LaTeX math moves out of the core rendering path into a published artifact of
its own, so an app that renders no math pays neither the download nor RaTeX's
missing 32-bit x86 native library. Ported from the work on #814 and adapted
to the plugin seam: display math is a block segment, inline and mid-line math
are node renderers, and errors travel on core's per-view event channel as
LatexErrorEvent.

Every entry into RaTeX goes through one helper that catches LinkageError as
well as Exception. A missing native library arrives as an Error rather than
an Exception, so without that the source-echoing fallback would not have held
on exactly the ABI that motivated splitting math out. RaTeXUnavailableTest
pins the behaviour: a JVM test host has no native library either.

The Compose DSL ships from here as extension functions on
MarkdownStyleBuilder, compiled against :compose with compileOnly, so the
published POM carries :ui and ratex-android and no Compose at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The example app installs LatexMathPlugin from its Application, turns on the
latexMath flag, styles math through the DSL on two screens so the difference
is visible, and logs LatexErrorEvents. Its sample markdown covers display,
inline and mid-line math, a wide equation that scrolls, and two deliberately
broken expressions that fall back to their source.

README documents the artifact, the install call, the two switches it takes to
render math, and onPluginEvent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	apps/android-example/app/src/main/java/swmansion/enriched/markdown/android/example/TextScreen.kt
#	packages/enriched-markdown-android/README.md
#	packages/enriched-markdown-android/build.gradle
#	packages/enriched-markdown-android/compose/src/main/java/com/swmansion/enriched/markdown/compose/EnrichedMarkdownText.kt
#	packages/enriched-markdown-android/compose/src/main/java/com/swmansion/enriched/markdown/compose/MarkdownStyle.kt
#	packages/enriched-markdown-android/compose/src/main/java/com/swmansion/enriched/markdown/compose/MarkdownStyleBuilder.kt
#	packages/enriched-markdown-android/compose/src/main/java/com/swmansion/enriched/markdown/compose/MarkdownStyleLayer.kt
#	packages/enriched-markdown-android/compose/src/main/java/com/swmansion/enriched/markdown/compose/style/StyleConfigMerger.kt
#	packages/enriched-markdown-android/settings.gradle
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt
…ocks

Plain-text copy left the object-replacement character in place of inline
equations; plugin spans now supply their own plain text. Display math's
long-press menu never fired over most of the block because the scroll view
swallowed the touch; it now listens on the viewport-filling wrapper and uses
the same ContextMenuPopup as tables.
Freeze the plugin registry on the first render and drop runtime uninstall
and change listeners. Resolve math style defaults from the Context like core
does, pass MathContainerView its config through the constructor, remove the
empty consumer-rules.pro, and tighten the plugin tests.
- Make the RendererFactory constructor internal; only core builds one.
- Move StyleConfig.extensions after spoilerStyle so positional callers keep working.
- Drop PluginInlineSpan.toHtml(), which no plugin used.
- Keep $$ delimiters and displayMode for mid-line display math on copy, fallback and error events.
- Drop plugin events from a render whose markdown has since been replaced or recycled.
- Apply the current selection menu config when a math block view is updated.
The table serializer had no case for LaTeX nodes, so Copy as Markdown
dropped the $/$$ delimiters (or the whole equation) and the pasted
table rendered its math as plain text.
Base automatically changed from feat/android-math-plugin to main October 7, 2026 10:38
…gin-follow-ups

# Conflicts:
#	packages/enriched-markdown-android/README.md
#	packages/enriched-markdown-android/compose/src/main/java/com/swmansion/enriched/markdown/compose/EnrichedMarkdownText.kt
#	packages/enriched-markdown-android/compose/src/main/java/com/swmansion/enriched/markdown/compose/MarkdownStyleBuilder.kt
#	packages/enriched-markdown-android/compose/src/main/java/com/swmansion/enriched/markdown/compose/MarkdownStyleLayer.kt
#	packages/enriched-markdown-android/math/src/main/java/com/swmansion/enriched/markdown/math/MathBlockSegment.kt
#	packages/enriched-markdown-android/math/src/main/java/com/swmansion/enriched/markdown/math/MathContainerView.kt
#	packages/enriched-markdown-android/math/src/main/java/com/swmansion/enriched/markdown/math/MathInlineSpan.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/plugin/PluginInlineSpan.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/renderer/LatexSourceRenderer.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/segments/SegmentViewCreators.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/spoiler/SpoilerDrawUtils.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/plugin/PluginInlineSpanExportTest.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/plugin/PluginNodeRendererTest.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/plugin/PluginViewIntegrationTest.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/segments/TableSegmentTest.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/styles/StyleConfigExtensionTest.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/test/FakePluginSupport.kt
…gin-follow-ups

# Conflicts:
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/segments/RenderedSegment.kt
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/plugin/PluginViewIntegrationTest.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/segments/TableContainerViewTest.kt
#	packages/enriched-markdown-android/ui/src/test/java/com/swmansion/enriched/markdown/segments/TableSegmentTest.kt
…gin-follow-ups

# Conflicts:
#	packages/enriched-markdown-android/ui/src/main/java/com/swmansion/enriched/markdown/segments/TableContainerView.kt

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

LaTeX trimming can introduce a control space absent from the original source, changing rendering and copied text.

1 open finding
What changed in this PR

Addresses Android math-plugin follow-ups from #873, improving rendering, accessibility and copy behavior.

Changes:

  • Moves table rendering and measurement off the main thread using copied styles.
  • Caches parsed equations and normalizes LaTeX whitespace.
  • Shares readable plugin text across accessibility and copying, and propagates selection-menu settings.

Paths below are relative to packages/enriched-markdown-android/.

File Description
ui/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​test/​FakePluginSupport.kt Supports selection-menu configuration in test views.
ui/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​styles/​StyleConfigExtensionTest.kt Tests paragraph-style copy isolation.
ui/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​segments/​TableSegmentTest.kt Tests plugin rendering in table cells.
ui/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​segments/​TableContainerViewTest.kt Tests deferred link callbacks.
ui/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​plugin/​PluginViewIntegrationTest.kt Tests menu-setting propagation.
ui/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​plugin/​PluginNodeRendererTest.kt Tests LaTeX whitespace handling.
ui/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​plugin/​PluginInlineSpanExportTest.kt Uses shared readable-text conversion.
ui/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​accessibility/​PluginSpanAccessibilityTest.kt Tests screen-reader text for plugin spans.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​utils/​text/​view/​SelectionActionMode.kt Adds configurable menus and shared copy conversion.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​styles/​StyleConfig.kt Copies paragraph styles and preserves highlighting.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​segments/​TableContainerView.kt Builds grids from rendered tables.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​segments/​SegmentViewCreators.kt Applies pre-rendered tables.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​segments/​RenderedTable.kt Renders and measures table cells.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​segments/​RenderedSegment.kt Carries rendered table data.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​segments/​CodeBlockContainerView.kt Implements configurable selection menus.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​renderer/​LatexSourceRenderer.kt Trims delimiter-adjacent whitespace.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​plugin/​PluginInlineSpan.kt Adds shared readable-text extraction.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​EnrichedMarkdownInternalText.kt Implements configurable selection menus.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​EnrichedMarkdown.kt Propagates menu settings to plugin views.
ui/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​accessibility/​MarkdownAccessibilityHelper.kt Reads plugin spans as plain text.
math/​src/​test/​java/​com/​swmansion/​enriched/​markdown/​math/​DisplayListCacheTest.kt Tests cache keys, reuse and eviction.
math/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​math/​MathInlineSpan.kt Uses cached inline equations.
math/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​math/​MathContainerView.kt Implements configurable selection menus.
math/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​math/​MathBlockSegment.kt Uses cached display equations.
math/​src/​main/​java/​com/​swmansion/​enriched/​markdown/​math/​DisplayListCache.kt Adds a shared 256-entry equation cache.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

/** [String.trim], except that it keeps the space of a trailing control space (`\ `), which TeX typesets. */
private fun trimLatex(source: String): String {
val trimmed = source.trim()
if (trimmed.length == source.length || !trimmed.endsWith('\\')) return trimmed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Symbol-only equations beside links remain omitted from the accessibility hierarchy.

2 open findings

🧠 Review effort: Balanced

Comment on lines 292 to 293
val beforeText = spanned.readableText(segmentPos, span.start).trim()
if (beforeText.isNotEmpty() && beforeText.any { it.isLetterOrDigit() }) {
@jnowakow
jnowakow marked this pull request as ready for review October 22, 2026 07:30

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants