API Reference

February 17, 2026 · View on GitHub

All public types live in the markdown namespace. Headers are in markdown/include/markdown/.


ast.hpp -- AST Node Types

Defines the Markdown abstract syntax tree representation.

NodeType (enum class)

enum class NodeType {
    Document,        // Root container node
    Heading,         // Heading block (level 1-6)
    Paragraph,       // Paragraph block
    Text,            // Leaf text content
    Emphasis,        // Italic (*text* or _text_)
    Strong,          // Bold (**text** or __text__)
    Link,            // Hyperlink [text](url)
    ListItem,        // Single list item
    BulletList,      // Unordered list container
    OrderedList,     // Ordered list container
    CodeInline,      // Inline code (`code`)
    CodeBlock,       // Fenced code block (```)
    BlockQuote,      // Block quote (> text)
    SoftBreak,       // Soft line break (single newline)
    HardBreak,       // Hard line break (two spaces + newline)
    ThematicBreak,   // Horizontal rule (---)
    Image,           // Image (![alt](url))
};

ASTNode (struct)

struct ASTNode {
    NodeType type = NodeType::Document;
    std::string text;          // Leaf text content (Text, CodeInline, CodeBlock)
    std::string url;           // URL for Link and Image nodes
    int level = 0;             // Heading level (1-6), 0 for non-headings
    int list_start = 1;        // Starting number for OrderedList
    std::vector<ASTNode> children;
};

MarkdownAST (type alias)

using MarkdownAST = ASTNode;

The root node is always NodeType::Document with children representing top-level blocks.

Example: Walking an AST

#include "markdown/ast.hpp"
#include "markdown/parser.hpp"

void print_tree(markdown::ASTNode const& node, int depth = 0) {
    for (int i = 0; i < depth; ++i) std::cout << "  ";
    std::cout << static_cast<int>(node.type);
    if (!node.text.empty()) std::cout << " \"" << node.text << "\"";
    if (!node.url.empty()) std::cout << " url=" << node.url;
    if (node.level > 0) std::cout << " level=" << node.level;
    std::cout << "\n";
    for (auto const& child : node.children)
        print_tree(child, depth + 1);
}

auto parser = markdown::make_cmark_parser();
auto ast = parser->parse("# Hello **world**");
print_tree(ast);
// Output:
//   0           (Document)
//     1 level=1 (Heading)
//       3 "Hello " (Text)
//       5          (Strong)
//         3 "world" (Text)

parser.hpp -- Markdown Parser

MarkdownParser (abstract class)

class MarkdownParser {
public:
    virtual ~MarkdownParser() = default;

    // Parse Markdown text into an AST.
    // Returns false if parsing failed; out will contain a raw-text fallback.
    virtual bool parse(std::string_view input, MarkdownAST& out) = 0;

    // Convenience overload: returns AST directly, ignoring success/failure.
    MarkdownAST parse(std::string_view input);
};

make_cmark_parser()

std::unique_ptr<MarkdownParser> make_cmark_parser();

Factory function that creates a parser backed by cmark-gfm. The cmark-gfm headers and types are completely hidden inside the implementation. This is the only way to obtain a parser instance.

Example: Parsing Markdown

#include "markdown/parser.hpp"

auto parser = markdown::make_cmark_parser();

// Two-overload usage:
// 1. Convenience (returns AST)
auto ast = parser->parse("**bold** and *italic*");

// 2. Output parameter (returns success flag)
markdown::MarkdownAST ast2;
bool ok = parser->parse("Some text", ast2);
if (!ok) {
    // Parsing failed -- ast2 contains a raw-text fallback
}

viewer.hpp -- Markdown Viewer Component

LinkEvent (enum class)

enum class LinkEvent {
    Focus,  // Item received Tab focus
    Press,  // Item was activated with Enter
};

ViewerKeys (struct)

struct ViewerKeys {
    ftxui::Event activate   = ftxui::Event::Return;      // Enter edit/link-press mode
    ftxui::Event deactivate = ftxui::Event::Escape;       // Exit active mode
    ftxui::Event next       = ftxui::Event::Tab;          // Cycle forward through links
    ftxui::Event prev       = ftxui::Event::TabReverse;   // Cycle backward through links
};

Configurable key bindings for viewer interaction. Override any field to change the corresponding key. Default values match the standard terminal conventions.

Viewer (class)

class Viewer {
public:
    explicit Viewer(std::unique_ptr<MarkdownParser> parser);

Content Management

    // Set the Markdown text to display.
    // Triggers re-parse on next render.
    void set_content(std::string_view markdown_text);

Scroll Control

    // Set scroll position as a ratio: 0.0 = top, 1.0 = bottom.
    void set_scroll(float ratio);

    // Show or hide the vertical scroll indicator. Default: true.
    void show_scrollbar(bool show);

    // Query current scroll ratio.
    float scroll() const;

    // Query viewport/content height info (populated after render).
    ScrollInfo const& scroll_info() const;

    // Query scrollbar visibility.
    bool scrollbar_visible() const;

The built-in component handles scrolling via keyboard and mouse:

InputAction
Arrow Up/DownScroll by 5%
Page Up/DownScroll by one viewport height (proportional)
Home / EndJump to top / bottom
Mouse WheelScroll by 5% per tick
    // Register a callback for link focus and press events.
    // The callback receives the link URL and the event type (Focus or Press).
    void on_link_click(
        std::function<void(std::string const&, LinkEvent)> callback);

Tab Focus Integration

    // When set, Tab past last link calls cb(+1), Shift+Tab before first calls cb(-1).
    // The viewer clears its focus and deactivates. If not set, Tab wraps (default).
    void on_tab_exit(std::function<void(int direction)> callback);

    // Parent calls this to give the viewer link focus from a direction.
    // direction > 0: focus first link; direction < 0: focus last link.
    // Returns true if focus was accepted (there are links), false otherwise.
    // Sets active(true) and fires on_link_click with LinkEvent::Focus.
    bool enter_focus(int direction);

These two methods enable Tab cycling between parent UI elements and viewer links. The parent intercepts Tab when the viewer is not active, calls enter_focus() to hand off, and receives control back via the on_tab_exit callback. If no callback is set, Tab wraps through links as before.

Key Bindings

    // Override the default key bindings (Enter, Esc, Tab, Shift+Tab).
    void set_keys(ViewerKeys const& keys);

    // Query the current key bindings (useful for parent event handlers).
    ViewerKeys const& keys() const;

The keys() accessor lets parent components reference the same keys the viewer uses internally, so event checks stay consistent even when keys are customized.

Theming

    // Apply a theme. Only triggers re-build if the theme name changed.
    void set_theme(Theme const& theme);

    // Query current theme.
    Theme const& theme() const;

Component

    // Get the FTXUI Component for embedding in layouts.
    // The component is created on first call and cached.
    ftxui::Component component();

Active State

    // Whether the viewer is in "active" mode (accepting link navigation).
    bool active() const;
    void set_active(bool a);

Focus Tracking

    // Focus index into link_targets: -1 = nothing focused, 0..N-1 = link index.
    int focused_index() const;

    // URL of the currently focused link. Empty string if nothing is focused.
    std::string focused_value() const;

    // True if any link is focused.
    bool is_link_focused() const;

Embed Mode

    // In embed mode, the viewer skips its internal scroll frame and flex.
    // The caller is responsible for wrapping the element in a scroll container.
    // Useful for combining viewer content with other elements (like email headers)
    // in a single scrollable frame.
    void set_embed(bool embed);
    bool is_embed() const;
};

Example: Basic Viewer

#include "markdown/parser.hpp"
#include "markdown/viewer.hpp"

#include <ftxui/component/screen_interactive.hpp>

int main() {
    auto viewer = std::make_shared<markdown::Viewer>(
        markdown::make_cmark_parser());

    viewer->set_content("# Hello\n\nThis is **bold** and [a link](https://example.com).");
    viewer->show_scrollbar(true);

    viewer->on_link_click([](std::string const& url, markdown::LinkEvent ev) {
        if (ev == markdown::LinkEvent::Press) {
            // Handle link activation
        }
    });

    auto screen = ftxui::ScreenInteractive::Fullscreen();
    screen.Loop(viewer->component());
}

Example: Tab Integration with Parent

auto viewer = std::make_shared<markdown::Viewer>(
    markdown::make_cmark_parser());

viewer->set_content("Check the [docs](https://docs.example.com).");

// Viewer calls back when Tab exits past bounds
viewer->on_tab_exit([&](int direction) {
    // direction +1: Tab past last link → move focus to next parent element
    // direction -1: Shift+Tab past first link → move to previous parent element
});

// Parent hands off Tab focus to the viewer
viewer->enter_focus(+1);  // focus first link
viewer->enter_focus(-1);  // focus last link

// In the parent's event handler, use viewer->keys() for consistency:
if (event == viewer->keys().next && !viewer->active()) {
    viewer->enter_focus(+1);  // give viewer Tab focus
}

// Optionally override keys before creating the component:
markdown::ViewerKeys keys;
keys.next = ftxui::Event::Character('n');
keys.prev = ftxui::Event::Character('p');
viewer->set_keys(keys);

editor.hpp -- Markdown Editor Component

Editor (class)

class Editor {
public:
    explicit Editor();

Component

    // Get the FTXUI Component for embedding in layouts.
    ftxui::Component component();

Content

    // Read-only reference to the current document text.
    std::string const& content() const;

    // Replace the entire document content.
    void set_content(std::string text);

Cursor Information

    // Current cursor line (1-based).
    int cursor_line() const;

    // Current cursor column (1-based).
    int cursor_col() const;

    // Cursor byte offset in the text.
    int cursor_position() const;

    // Total number of lines in the document.
    int total_lines() const;

State

    // Whether the editor is in "active" mode (accepting text input).
    // Enter activates, Esc deactivates.
    bool active() const;

Cursor Manipulation

    // Set cursor position by byte offset.
    void set_cursor_position(int byte_offset);

    // Set cursor position by line and column (1-based).
    void set_cursor(int line, int col);

    // Move cursor up (negative) or down (positive) by delta lines,
    // preserving the current column. Clamped to document bounds.
    void move_cursor_lines(int delta);

The built-in component handles scrolling via keyboard and mouse:

InputAction
Page Up/DownMove cursor by 20 lines
Mouse WheelMove cursor by 3 lines per tick

Theming

    // Apply a theme for syntax highlighting colors.
    void set_theme(Theme const& theme);
};

Example: Editor with Live Preview

#include "markdown/editor.hpp"
#include "markdown/viewer.hpp"
#include "markdown/parser.hpp"

auto editor = std::make_shared<markdown::Editor>();
editor->set_content("# My Document\n\nStart typing...");

auto viewer = std::make_shared<markdown::Viewer>(
    markdown::make_cmark_parser());

// In a Renderer lambda:
viewer->set_content(editor->content());  // Live update

// Sync scroll to cursor position:
float ratio = 0.0f;
if (editor->total_lines() > 1) {
    ratio = static_cast<float>(editor->cursor_line() - 1) /
            static_cast<float>(editor->total_lines() - 1);
}
viewer->set_scroll(ratio);

dom_builder.hpp -- AST to FTXUI DOM

LinkTarget (struct)

struct LinkTarget {
    std::vector<ftxui::Box> boxes;  // Bounding boxes on screen
    std::string url;                // The link URL
};

Each link in the rendered output produces a LinkTarget with one or more bounding boxes (a link that wraps across lines will have multiple boxes).

DomBuilder (class)

class DomBuilder {
public:
    // Convert an AST into an FTXUI Element tree.
    // focused_link: index of link to highlight (-1 for none).
    // theme: styling configuration.
    ftxui::Element build(MarkdownAST const& ast,
                         int focused_link = -1,
                         Theme const& theme = theme_default());

    // Query link targets after build().
    // Returns the list of links found during the last build.
    std::vector<LinkTarget> const& link_targets() const;
};

Example: Direct DOM Building

#include "markdown/parser.hpp"
#include "markdown/dom_builder.hpp"

#include <ftxui/screen/screen.hpp>

auto parser = markdown::make_cmark_parser();
auto ast = parser->parse("Hello **world** with a [link](https://example.com)");

markdown::DomBuilder builder;
auto element = builder.build(ast, /*focused_link=*/0, markdown::theme_colorful());

// Render to a screen
auto screen = ftxui::Screen::Create(ftxui::Dimension::Fixed(60),
                                     ftxui::Dimension::Fixed(5));
ftxui::Render(screen, element);
std::cout << screen.ToString() << std::endl;

// Query link positions
for (auto const& link : builder.link_targets()) {
    std::cout << "Link: " << link.url
              << " at " << link.boxes.size() << " box(es)\n";
}

Rendering Rules

AST NodeFTXUI Rendering
Heading (level 1)theme.heading1 decorator (default: bold + underlined)
Heading (level 2)theme.heading2 decorator (default: bold)
Heading (level 3+)theme.heading3 decorator (default: bold + dim)
Strongftxui::bold
Emphasisftxui::italic (where supported)
Linkftxui::underlined + theme.link + ftxui::reflect() for boxes
Link (focused)adds ftxui::inverted + ftxui::focus
BulletList" * " prefix per item, indentation for nesting
OrderedList" N. " prefix per item, numbering from list_start
CodeInlinetheme.code_inline decorator (default: inverted)
CodeBlockbordered box + theme.code_block decorator
BlockQuote" | " prefix + theme.blockquote decorator
ThematicBreakftxui::separator()
Image"[img: alt_text]" placeholder
SoftBreaksingle space
HardBreakline break

highlight.hpp -- Lexical Syntax Highlighting

highlight_markdown_syntax()

ftxui::Element highlight_markdown_syntax(
    std::string_view text,
    Theme const& theme = theme_default());

Highlights Markdown syntax markers in raw text. Returns an FTXUI Element where syntax characters (#, *, _, `, [, ], (, ), >, -) are styled with theme.syntax. Regular text is unstyled.

This is lexical highlighting -- it colors characters based on pattern matching, not AST structure. It does not understand nesting or context.

highlight_markdown_with_cursor()

ftxui::Element highlight_markdown_with_cursor(
    std::string_view text,
    int cursor_position,         // Byte offset of cursor in text
    bool focused,                // Whether cursor should be visible
    bool hovered,                // Whether hover styling applies
    bool show_line_numbers = false,
    Theme const& theme = theme_default());

Same as highlight_markdown_syntax() but with an embedded cursor at the given byte position. When focused is true, the character at the cursor position is rendered with inverted colors. When show_line_numbers is true, a line number gutter is prepended.

Used internally by the Editor's InputOption::transform callback.

Example: Rendering Highlighted Text

#include "markdown/highlight.hpp"

#include <ftxui/screen/screen.hpp>

std::string text = "# Hello **world**";
auto element = markdown::highlight_markdown_syntax(text);

auto screen = ftxui::Screen::Create(ftxui::Dimension::Fixed(40),
                                     ftxui::Dimension::Fixed(1));
ftxui::Render(screen, element);
// Output: "# Hello **world**" with # and ** in yellow+dim

theme.hpp -- Theme Configuration

Theme (struct)

struct Theme {
    std::string name;               // Theme identifier (used for change detection)

    // Editor syntax highlighting
    ftxui::Decorator syntax;        // Color for syntax markers (#, *, `, etc.)
    ftxui::Decorator gutter;        // Color for line number gutter

    // Viewer elements
    ftxui::Decorator heading1;      // H1 style
    ftxui::Decorator heading2;      // H2 style
    ftxui::Decorator heading3;      // H3-H6 style
    ftxui::Decorator link;          // Link style (added on top of underlined)
    ftxui::Decorator code_inline;   // Inline code style
    ftxui::Decorator code_block;    // Code block content style
    ftxui::Decorator blockquote;    // Block quote text style
};

Built-in Themes

Theme const& theme_default();        // "Default" -- minimal, works everywhere
Theme const& theme_high_contrast();  // "Contrast" -- bold + white, high visibility
Theme const& theme_colorful();       // "Colorful" -- color-coded elements
FieldDefaultContrastColorful
syntaxyellow + dimwhite + boldgreen
gutterdimwhiteblue + dim
heading1bold + underlinedbold + underlinedbold + underlined + cyan
heading2boldboldbold + green
heading3bold + dimboldbold + yellow
linknothingboldmagenta
code_inlineinvertedinvertedinverted + yellow
code_blocknothingnothinggreen
blockquotedimnothingblue

Example: Custom Theme

#include "markdown/theme.hpp"

markdown::Theme my_theme{
    "MyTheme",
    ftxui::color(ftxui::Color::Red),                              // syntax
    ftxui::dim,                                                    // gutter
    ftxui::Decorator(ftxui::bold) | ftxui::underlined,            // heading1
    ftxui::bold,                                                   // heading2
    ftxui::Decorator(ftxui::bold) | ftxui::dim,                   // heading3
    ftxui::color(ftxui::Color::Blue),                              // link
    ftxui::inverted,                                               // code_inline
    ftxui::nothing,                                                // code_block
    ftxui::dim,                                                    // blockquote
};

viewer->set_theme(my_theme);
editor->set_theme(my_theme);

MSVC note: When composing decorators, ftxui::bold | ftxui::underlined fails on MSVC because both are raw function pointers. Wrap the first operand: ftxui::Decorator(ftxui::bold) | ftxui::underlined. Decorators returned by ftxui::color() are already std::function and compose normally.


scroll_frame.hpp -- Custom Scroll Container

ScrollInfo (struct)

struct ScrollInfo {
    int viewport_height = 0;
    int content_height = 0;
};

Populated by DirectScrollFrame during layout. Provides the viewport and content dimensions needed for proportional scroll calculations (e.g. viewport-sized page steps).

DirectScrollFrame (class)

class DirectScrollFrame : public ftxui::Node {
public:
    DirectScrollFrame(ftxui::Element child, float ratio,
                      ScrollInfo* info = nullptr);
};

A custom FTXUI Node that scrolls its child by a direct ratio. The scroll offset is computed as ratio * scrollable_height, where scrollable_height = content_height - viewport_height. The ratio is clamped to [0.0, 1.0].

Unlike FTXUI's yframe, which centers the element that has ftxui::focus, DirectScrollFrame ignores focus and positions purely by ratio.

If a ScrollInfo* is provided, the viewport and content heights are written to it during layout.

direct_scroll()

ftxui::Element direct_scroll(ftxui::Element child, float ratio,
                              ScrollInfo* info = nullptr);

Convenience function that creates a DirectScrollFrame. Pass a ScrollInfo* to capture layout dimensions.

Example: Scrollable Content

#include "markdown/scroll_frame.hpp"

ftxui::Elements lines;
for (int i = 0; i < 100; ++i) {
    lines.push_back(ftxui::text("Line " + std::to_string(i)));
}

float scroll_ratio = 0.5f;  // Middle of content
markdown::ScrollInfo info;
auto scrollable = markdown::direct_scroll(
    ftxui::vbox(std::move(lines)) | ftxui::vscroll_indicator,
    scroll_ratio, &info);

// Render in a fixed viewport
auto screen = ftxui::Screen::Create(ftxui::Dimension::Fixed(40),
                                     ftxui::Dimension::Fixed(10));
ftxui::Render(screen, scrollable);
// After render: info.viewport_height and info.content_height are set.

text_utils.hpp -- UTF-8 Utilities

All functions are inline and header-only.

Character Length and Counting

// Byte length of the UTF-8 character starting at leading_byte.
size_t utf8_byte_length(char leading_byte);

// Count UTF-8 characters (not bytes) in text.
int utf8_char_count(std::string_view text);

// Convert character index (0-based) to byte offset.
size_t utf8_char_to_byte(std::string_view text, int char_index);

Unicode Codepoint

// Decode UTF-8 codepoint starting at data[0..len).
uint32_t utf8_codepoint(const char* data, size_t len);

// Terminal display width of a Unicode codepoint.
// Returns 2 for CJK Unified Ideographs, Fullwidth forms, Hangul,
// Hiragana, Katakana, and other East Asian Wide characters.
// Returns 1 for everything else.
int codepoint_width(uint32_t cp);

Display Width

// Total terminal display width of a UTF-8 string.
// Accounts for wide characters (CJK etc.) that occupy 2 columns.
int utf8_display_width(std::string_view text);

// Map terminal visual column (0-based) to byte offset in text.
// Accounts for wide characters.
size_t visual_col_to_byte(std::string_view text, int col);

Line Utilities

// Split text into lines (without newline characters).
// A trailing newline produces an empty final element.
std::vector<std::string_view> split_lines(std::string_view text);

Gutter Utilities

// Number of digits needed to display total_lines (e.g. 100 -> 3).
int gutter_width(int total_lines);

// Total gutter width: digits + 3 (for " | " separator).
int gutter_chars(int total_lines);

Example: UTF-8 Width Calculation

#include "markdown/text_utils.hpp"

// ASCII
assert(markdown::utf8_display_width("Hello") == 5);

// CJK characters (2 columns each)
assert(markdown::utf8_display_width("\xe4\xb8\x96\xe7\x95\x8c") == 4);  // "世界"

// Mixed
assert(markdown::utf8_display_width("Hi\xe4\xb8\x96") == 4);  // "Hi世" = 2 + 2