rich-text-vista: RichTextVista: The HarmonyOS text engine that turns Markdown, HTML, and custom nodes into one native string

Inside ByteDance’s pluggable rich-text pipeline, where a CommonMark-based AST becomes a fast rendering layer instead of a forest of spans.

8 min read • View on GitHub • More from larksuite

A wide customs checkpoint where several document types arrive separately and leave as one standardized scroll. It explains RichTextVista's core idea: normalize heterogeneous inputs before rendering, so HarmonyOS only has to deal with one native styled string.
Multiple front doors. One compiler. One native text surface.

RichTextVista is a high-performance, extensible rich text component designed for HarmonyOS applications, supporting diverse rich text styling capabilities.

StinsonOvO, Project Contributor · larksuite/rich-text-vista
Key Takeaways

RichTextVista behaves less like a rich text widget and more like a compiler. It takes plain text, Markdown, HTML, or a raw node tree, normalizes them into one canonical AST, then compiles that into a single native `MutableStyledString`. On HarmonyOS, that is the right trade. The UI stays simple, while the hard work happens before render.

Why HarmonyOS wants this shape

Rich text gets expensive when every style becomes a node the UI has to manage. Nested `Span` and `Text` trees are easy to start with, then they become hard to reuse, hard to cache, and awkward in scrolling surfaces. RichTextVista pushes complexity upstream so the view layer receives one styled string and one controller instead of a thicket of fragments.

ApproachInput handlingInternal modelOutputExtension pathBest fit
RichTextVistaMarkdown, HTML, plain text, raw ASTCommonMark-shaped ASTOne `MutableStyledString` through `TextController`Pluggable producers and custom nodesHarmonyOS feeds and content-heavy screens
Nested span treesUsually one format at a timeUI nodes as structureMany `Span` and `Text` childrenTedious special casesSmall, simple snippets
Parser-first widgetsMarkdown or HTML onlyParser-specific treeWidget fragments or mixed outputLimited or framework-specificQuick prototypes

That design gives the project a narrow waist. Every input format is allowed to be messy, but every output path is forced through one native shape. The result is not a bigger text feature set. It is a cleaner contract.

The AST is the real product

A close-up of a conversion funnel where different document fragments are shaved into the same node-shaped stream. It explains that the project's real asset is the canonical AST, not any one parser or UI wrapper.
Normalize first. Render second.

The hub is `RichTextRepresentation.ets`. Plain text gets wrapped, Markdown goes through `Markdown2AST.ets`, HTML goes through `HTML2AST.ets`, and raw nodes are carried forward without pretending they came from somewhere else. That lets the rest of the pipeline read like one language instead of a collection of format adapters.

const rep = new RichTextRepresentation({
  markdown,
  extensions: [mentionExtension]
})

const ast = rep.getElements()
const styled = assembleInlineElementsToMutableStyledString(ast)
textController.setStyledString(styled)

That CommonMark-shaped tree is the real product. Once the engine has a normalized node graph, the renderer can stop asking what the original input looked like and start asking how to map nodes into styles.

Pluggable styles without exploding the tree

From there, `ParsePlugin.ets` and `AssembleBasicMutableStyledString.ets` turn the AST into output. The dispatcher and producer pattern keep built-in tags and custom nodes on the same rails. A mention, an emoji, or a bespoke token can join the flow without forcing the whole renderer to learn a new branch.

A close-up of a conveyor rail where one custom token leaves the main text stream, passes through a plugin station, and returns as a styled segment. It explains how extensions like mentions can stay in the same pipeline without hardcoding every special case.
The extension rail is where custom nodes stop being special cases.
const customNode = {
  tag: 'CUSTOM',
  attributes: { kind: '@mention', userId: '123' }
}

const extensionList = [mentionProducer]
const result = assembleInlineElementsToMutableStyledString(customNode, extensionList)

That matters more than it sounds. Extension points are where rich text engines usually crack, because every special case wants its own shortcut. RichTextVista instead makes the shortcut a plugin.

BISU keeps style state flat

BISU is a small acronym with a big payoff: Bold, Italic, Stroke, Underline. Instead of multiplying node types for every style combination, the project keeps style state compact in attributes and lets traversal stay regular.

That flatness is why the tree stays readable. Fewer node classes means fewer branches in the renderer, and fewer branches usually means fewer surprises when the content gets weird.

What it looks like against the alternatives

RichTextVista is not trying to replace every rich text tool. It is carving out a specific sweet spot: native output, heterogeneous input, and extension points that do not wreck the render tree.

Project shapeStrengthCostWhere it wins
RichTextVistaOne native text surface with pluggable inputsPreprocessing and a custom pipelineHarmonyOS apps that need extensible rich text
Nested span treesDirect and familiarComponent bloat as content growsTiny snippets and simple screens
Parser-only widgetsFast to prototypeLimited output controlNarrow content formats

If you need a full editor, this is not that. If you need a reliable bridge from content formats to HarmonyOS text, the shape makes sense immediately. The project sits between a formatting library and a rendering runtime, and that is exactly why it feels useful.

What the repo says about the Lark team

Early-stage projects show their seams in the issue tracker, and that is useful here. One user noted, "不支持 html 中的列表". That complaint does not undercut the design. It shows the repo is still accumulating edge cases while the core architecture stays coherent.

不支持 html 中的列表

lulululbj, Issue Opener · Issue #7

The repository still looks young, but the structure is disciplined. `commonmark/` handles parsing, `rich_text_core/` carries the HarmonyOS-specific engine, and `entry/` demonstrates practical use cases like at-mentions. That is the footprint of an internal tool that was shaped for real workloads before it was opened up.

Hedcut portrait of StinsonOvO based on a verified GitHub avatar. It gives a human face to the contributor behind the project's own description of RichTextVista as a high-performance, extensible HarmonyOS component.

That is the most revealing line in the repo. It does not pitch novelty for its own sake. It describes a component that is meant to survive real product pressure, while still staying open enough for custom content and future growth.