The tamboui-markdown module renders CommonMark + GFM (tables, task lists, strikethrough) to a TamboUI Buffer.

dependencies {
    implementation("dev.tamboui:tamboui-markdown:LATEST")

    // Toolkit DSL integration (optional)
    implementation("dev.tamboui:tamboui-toolkit-markdown:LATEST")
}

Requires Java 11.

Toolkit DSL

The tamboui-toolkit-markdown module provides MarkdownElement, a StyledElement that wraps MarkdownView so markdown content participates in the toolkit’s styling and layout system.

import static dev.tamboui.toolkit.Toolkit.*;
import static dev.tamboui.toolkit.markdown.MarkdownElement.markdown;

panel("README",
    markdown(readme).overflow(Overflow.WRAP_WORD).fill()
).rounded();

CSS rules attached to the MarkdownElement selector flow through to the underlying widget, so the same heading-1-color, link-color, blockquote-prefix and so on documented below apply.

Quick start

import dev.tamboui.markdown.MarkdownView;
import dev.tamboui.widgets.block.Block;
import dev.tamboui.widgets.block.Borders;
import dev.tamboui.widgets.block.Title;

MarkdownView view = MarkdownView.builder()
    .source("# Hello\n\nThis is **markdown**.")
    .block(Block.builder()
        .borders(Borders.ALL)
        .title(Title.from(" README "))
        .build())
    .scroll(0)
    .build();

view.render(area, buffer);

Supported elements

Element Notes

Headings (1-6)

Bold styling, level-scaled colors. H1 underlined with ═, H2 with ─.

Paragraphs

Width-aware word wrap. Soft line breaks become a single space; hard line breaks (two trailing spaces) start a new line.

Bullet, ordered and task lists

Bullet •, ordered N., task [x] / [ ]. Sub-lists indent by their parent marker width.

Block quotes

Single `│ ` bar with the configured blockquote style (default: dim).

Fenced and indented code blocks

Wrapped in a rounded Block. The info string is used as the block title. Long lines are clipped to the inner width. Fenced blocks are syntax-highlighted by default (see Syntax highlighting); indented blocks have no language tag so they render in the plain code style.

GFM tables

Delegated to Table with equal-percentage column widths. Header rows are bolded.

Thematic breaks

A row of ─ glyphs styled with MarkdownStyles.horizontalRule().

Inline emphasis, strong, strikethrough, code

Composed via Style.patch so explicit styles win.

Links

Underlined and colored. The link target is also attached to each span as an OSC-8 hyperlink, so terminals like iTerm2 and Kitty render them clickable.

Images

Rendered as a styled [image: alt](url) text span. There is no coupling to tamboui-image — image rendering in a markdown view would mean an image protocol decision per markdown, which is out of scope.

HTML blocks and inline HTML

Rendered as escaped text in a dim style. The HTML source stays visible but is clearly not interpreted.

Streaming

MarkdownView is safe to re-bind on every frame, including when the source is being typed token-by-token by an LLM. Before every parse the source goes through PartialMarkdownSanitizer, which trims trailing unmatched inline markers. Well-formed markdown is returned byte-identical, so the same code path serves both static and streaming consumers.

The sanitizer rules:

  • A trailing run of , _, ~, or single/double ` whose left boundary is start-of-string, whitespace, or an opening bracket is treated as an unmatched opener and dropped. A trailing run after non-whitespace is a closer (e.g. the at the end of bold*) and is left alone.

  • A trailing run of three or more backticks is left alone — that is a code fence delimiter, not inline code.

  • A dangling link [label]( with no closing ) is rewritten to plain [label].

  • A trailing partial ATX header line (## with no content) is dropped.

  • An open code fence is left alone — CommonMark already treats EOF as fence close.

Customizing styles

Styles resolve in this order: explicit MarkdownStyles value > CSS resolver > built-in default.

Programmatic overrides

MarkdownStyles controls the styles applied to each element. Override any subset and keep the rest at their defaults:

import dev.tamboui.markdown.MarkdownStyles;
import dev.tamboui.style.Color;
import dev.tamboui.style.Style;

MarkdownStyles styles = MarkdownStyles.builder()
    .heading(1, Style.EMPTY.bold().fg(Color.MAGENTA))
    .link(Style.EMPTY.fg(Color.GREEN).underlined())
    .build();

MarkdownView view = MarkdownView.builder()
    .source(source)
    .styles(styles)
    .build();

CSS resolver

Pass a StylePropertyResolver (the same single-resolver pattern Block and the other widgets use) to fill any slot the user did not set programmatically. Each markdown sub-element exposes three flat properties:

  • {element}-color — foreground color (named, hex, rgb, or indexed)

  • {element}-background — background color

  • {element}-text-style — space-separated modifiers (bold, italic, underlined, dim, reversed, crossed-out / strikethrough, hidden, slow-blink, rapid-blink)

Plus a few string properties (mirroring the Checkbox widget convention):

  • blockquote-prefix — a glyph drawn at the start of each quoted line; a single space is always appended after it.

  • task-checked-symbol / task-unchecked-symbol — glyphs drawn for GFM task-list items; defaults are [x] and [ ], again followed by a single space.

  • text-overflow — the standard text-overflow property, same as Paragraph. Supported values: wrap-word (default), wrap-character / wrap, clip, ellipsis, ellipsis-start, ellipsis-middle. Applies to prose: paragraphs, headings, list items, blockquote contents. Code-block lines always clip (their structure is meaningful and ellipsis would mislead) and table cells are sized by the Table widget.

Element names: heading-1 … heading-6, strong, emphasis, strikethrough, inline-code, code-block, link, blockquote, list-marker, html, horizontal-rule, task-checked, task-unchecked.

MarkdownView view = MarkdownView.builder()
    .source(source)
    .styleResolver(myCssEngine.resolver())
    .build();

A TCSS rule for the markdown widget looks like:

MarkdownView {
    text-overflow: ellipsis;
    heading-1-color: magenta;
    heading-1-text-style: bold;
    link-color: green;
    link-text-style: underlined;
    blockquote-color: yellow;
    blockquote-background: black;
    blockquote-prefix: ">";
    task-checked-color: green;
    task-checked-symbol: "✓";
    task-unchecked-color: gray;
    task-unchecked-symbol: "·";
}

Syntax highlighting

Fenced code blocks are highlighted by default using RegexSyntaxHighlighter (a highlight.js-inspired regex tokenizer) combined with a SyntaxTheme. Indented code blocks carry no language info and never highlight. Grammar definition lives in dev.tamboui.widgets.syntax:

  • TokenType — the semantic categories (KEYWORD, STRING, NUMBER, COMMENT, FUNCTION, TYPE, …​). PLAIN maps to the base code style.

  • SyntaxTheme — maps TokenType to a Style. The default (SyntaxTheme.DEFAULTS) is a light-on-dark palette. builder() starts with that palette; use token() to replace entries or clear() to make tokens inherit the enclosing code-block style.

  • Grammar — a declarative language definition: an id, aliases, and rules (pattern, literal, multiline for block comments / triple-quoted strings).

  • SyntaxHighlighter — the component interface (List<Line> highlight(String code, String language, Style base, SyntaxTheme)). Implement your own for a custom tokenizer; RegexSyntaxHighlighter is the regex-based built-in.

  • RegexSyntaxHighlighter — built-in grammars for java, kotlin, javascript, typescript, python, json, xml (incl. html), css, bash, yaml, properties, sql, go, and rust. RegexSyntaxHighlighter.defaults() uses these; .of(grammar…​) / .builder() let you add or replace grammars.

Highlighting combines a base, token, and theme style: each token is rendered as base.patch(tokenStyle). A token style overrides properties it defines, including the MarkdownStyles.codeBlock() foreground; properties not defined by the token style continue to come from the code-block base style.

When using MarkdownElement, syntax token styles can also be configured with CSS child selectors or generic token selectors:

MarkdownElement-syntax-keyword { color: magenta; }
syntax-comment { color: gray; text-style: italic; }

The resolution order is an explicit SyntaxTheme, an element-specific child selector, a generic syntax-<token-type> selector, then SyntaxTheme.DEFAULTS.

Custom theme

import dev.tamboui.widgets.syntax.SyntaxTheme;
import dev.tamboui.widgets.syntax.TokenType;
import dev.tamboui.style.Color;
import dev.tamboui.style.Style;

SyntaxTheme theme = SyntaxTheme.builder()
    .token(TokenType.KEYWORD, Style.EMPTY.bold().fg(Color.rgb(198, 120, 221)))
    .token(TokenType.STRING, Style.EMPTY.fg(Color.hex("#98c379")))
    .build();

MarkdownView view = MarkdownView.builder()
    .source(source)
    .syntaxTheme(theme)
    .build();

Disabling or custom highlighting

Use SyntaxHighlighter.none() to disable highlighting, or wire in a highlighter of your own (custom grammar additions, a different tokenizer, etc.):

import dev.tamboui.widgets.syntax.RegexSyntaxHighlighter;
import dev.tamboui.widgets.syntax.SyntaxHighlighter;

MarkdownView highlighted = MarkdownView.builder()
    .source(source)
    .syntaxHighlighter(RegexSyntaxHighlighter.defaults())
    .build();

MarkdownView plain = MarkdownView.builder()
    .source(source)
    .syntaxHighlighter(SyntaxHighlighter.none())
    .build();