This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Configure (adjust CMAKE_PREFIX_PATH to your Qt installation)
cmake -B build -DCMAKE_PREFIX_PATH=<path-to-qt> -DQT=ON
# Build
cmake --build build
# Build with tests enabled
cmake -B build -DCMAKE_PREFIX_PATH=<path-to-qt> -DQT=ON -DBUILD_TEST=ON
cmake --build build
# Run all tests
ctest --test-dir build
# Run a single test
ctest --test-dir build -R test_parser
# Or run the test binary directly:
./build/test/test_parser
# Editor tests need Qt offscreen:
QT_QPA_PLATFORM=offscreen ./build/test/test_editorTests use the doctest framework. Some test files define their own main via DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN; others use DOCTEST_CONFIG_IMPLEMENT with a manual doctest::Context. Each test binary is self-contained. Test files use #define private public / #define protected public to access internal members directly.
QtMarkdown has four layers with a strict dependency chain: Parser → Render → EditorCore → Platform Shells.
QtMarkdownParser (zero Qt dependency, pure C++26)
↑
QtMarkdownRender (depends on Qt::Gui, MicroTeX)
↑
QtMarkdownEditorCore (platform-agnostic editing logic)
↑
QtMarkdownPlatform → QtWidgetMarkdownEditor / QtQuickMarkdownEditor
Parses Markdown text into an AST using a two-pass approach:
- Tokenization:
parseLine()inParser.cppsplits a line intoTokenobjects (type + offset/length into the original text). Special characters#,*,~,`,$,[],(),!,>become typed tokens; everything else istext. - Block parsing: Block parsers are
std::function<ParseResult(const LineList&, int)>registered in a static vector inParserPrivate::parse(), tried in order until one succeeds.parseParagraphis last as the fallback. Each block parser internally registers its own static vector ofLineParserFnfor inline parsing.ParseResult={ bool success; int offset; unique_ptr<Node> node }— ifsuccessis true, the parser consumedoffsetlines and produced an ASTnode; if false, the next parser is tried. - Inline parsing:
LineParserFn=std::function<ParseResult(const TokenList&, int)>. Inline parsers live insrc/parser/parsers/(ImageParser,LinkParser,InlineCodeParser,InlineLatexParser,SemanticTextParser). - AST nodes:
Nodebase class withNodeTypeenum (inNode.h).Containernodes hold children viaNodePtrList=std::vector<std::unique_ptr<Node>>. Leaf nodes:Text,Image,Link,InlineCode, etc., insrc/parser/nodes/. The classic Visitor pattern is used:NodeVisitor(inVisitor.h) declares avisit()overload for every concrete node type; each node implementsaccept(NodeVisitor* v) { v->visit(this); }. - List nodes:
ListNodeandListItemNode(innodes/ListNode.h) extendContainer; concrete list classes inlineaccept()to avoid diamond inheritance. - PieceTable (
PieceTable.h):PieceTableItemstruct backsTextnodes for incremental edits. EachTextholds a list ofPieceTableItementries referencing spans (offset+length) in eitherDocument::m_originalBufferorDocument::m_addBuffer. When text is inserted, new content goes into the add buffer and a newPieceTableItempoints to it — the original text is never modified. - Node virtual methods:
Nodedeclares three virtual methods used by the editor for cursor-to-markdown-position mapping:contentLength(doc)— rendered content length (excludes markup delimiters)serializedLength(doc)— markdown length (includes**,*,#prefixes, etc.)calcMarkdownOffset(doc, contentPos, mdPos)— maps a content position to a markdown positionclone()— deep copy, used for snapshot-based undo Each of the 22+ concrete node types overrides these as needed. Types without editable content (Hr, Lf, Table) use the defaultreturn 0/return falseimplementations.
- ParseContext (
ParseContext.h):thread_local ParseContextstruct tells theText(offset, length)constructor which PieceTable buffer to reference (originaloradd). Set byParserPrivate::parse()via RAIIParseContextGuard. This lets the editor re-parse add-buffer content without modifying every inline parser.
Key types from mddef.h: String = md::String (custom UTF-8 wrapper around std::string, defined in MdString.h), SizeType = int64_t, sptr<T> = std::shared_ptr<T>, Char = char. Block/inline parser function signatures are in ParserDetail.h.
Each module layers additional type aliases:
src/render/mddef.h:InstructionPtr,InstructionPtrList, plus re-exports fromeditor/core/Types.h(Color,Point,Size,Rect,Font)src/editor/core/Types.h:Point,Size,Rect,Color,Margins,FontDescription,ImageDatasrc/editor/core/Event.h:KeyEvent,MouseEventsrc/editor/core/AbstractPainter.h: abstractPainterinterfacesrc/editor/core/Timer.h:Timerabstraction
Converts an AST node into a draw list for painting. Render::render() is a static method — the class is stateless, just a factory for Block objects.
RenderPrivateimplementsNodeVisitor, producing aBlockper AST node. Internally uses aLayoutPassto break blocks into lines and aPaintPassto generate instructions.- Block → LogicalLine → VisualLine → Cell: a block contains logical lines; each logical line may wrap into multiple visual lines; each visual line holds cells (text runs, images, etc.). Destruction order matters:
Instructionobjects (which hold rawCell*pointers) must be destroyed beforeLogicalLineobjects (which own theCells viaVisualLine::vector<unique_ptr<Cell>>). - Instruction list: Each
BlockaccumulatesInstructionobjects (a draw list). Concrete instructions:TextInstruction,StaticTextInstruction,ImageInstruction,FillRectInstruction,EllipseInstruction,LatexInstruction,StaticImageInstruction. Painting is just executing this list with aQPainter. StringUtil::split()segments text into Chinese/English/Emoji runs for correct line-breaking.- LaTeX math: MicroTeX submodule (
3rd/MicroTeX), linked viamicrotex+microtex-qt.graphic_qt.his the Qt graphics backend. RenderSetting(defined inRender.h) holds all visual config: fonts, margins (docMargin,codeMargin,listMargin,checkboxMargin,quoteMargin), heading sizes (headerFontSize),maxWidth,lineSpacing, etc.
Document: Wraps aparser::Document(composition viaunique_ptr<parser::Document>, NOT inheritance). Maintains renderedBlockList, owns theCommandStackfor undo/redo, and provides cursor navigation methods. Key methods for text-based editing:serializeBlock(blockNo)— serializes a block's AST back to markdown viaMarkdownSerializercursorToMarkdownPosition(coord)— maps cursor to a position in the serialized markdown, usingNode::calcMarkdownOffset()replaceBlocksFromText(start, end, md, offset, len)— re-parses edited markdown and replaces the AST subtreefindCursorFromContentPosition(blockNo, contentPos)— maps back from content position toCursorCoordafter re-parse
- Editing flow (text-based, NOT direct AST manipulation):
- Save a
clone()snapshot of the affected AST block(s) for undo - Serialize the block to markdown text via
MarkdownSerializer - Compute the markdown edit position via
cursorToMarkdownPosition() - Edit the markdown text (insert/delete characters)
- Re-parse with
Parser::parse(text, PieceTableItem::add, offset) - Replace the old AST subtree with
replaceBlocksFromText() - Compute the new cursor position from content position
- Save a
CursorCoord: Position model =(blockNo, lineNo, offset)— which block, which logical line, and character offset within that line. This is the fundamental addressing scheme for all cursor operations.Cursor: Holds aCursorCoord+ screen-spacePointposition and height.SelectionRangestores acaretandanchorcursor pair.Editor: Central controller. Handles keyboard/mouse events, cursor movement, text insertion/deletion. Delegates text mutations toDocument.Commandpattern:InsertTextCommand,RemoveTextCommand,InsertReturnCommand,RemoveTextRangeCommand,UpgradeToHeaderCommandfor undo/redo.CommandStackuses avector<unique_ptr<Command>>+int m_topindex (single list with position marker, not two separate stacks). Each command saves aclone()snapshot of affected AST blocks before executing; undo restores the snapshot. No more per-command undo state tracking (text_delete, block_merge, header_degrade, etc.).MarkdownSerializer: ImplementsNodeVisitorto serialize an AST subtree back to markdown text. Used by the editing flow to convert AST → text before applying edits.- Three library tiers:
QtMarkdownEditorCore(platform-agnostic logic),QtWidgetMarkdownEditor(QWidget),QtQuickMarkdownEditor(QML).
Thin Qt adapter layer implementing the abstract interfaces from editor/core/:
QtAdapters.h,QtFontMetricsProvider.h,QtImageProvider.h,QtLatexPlatform.hQtWidgetMarkdownEditorandQtQuickMarkdownEditor— platform-specific editor shells- The core parser/render/editor layers have zero Qt dependency; all Qt types are contained here
Git submodules: MicroTeX (LaTeX math rendering), magic_enum (enum reflection utility).
QtWidgetMarkdownEditorExample, QtQuickMarkdownEditorExample, and QtMarkdownParserExample (parser-only demo).
- The
DEBUGmacro (fromdebug.h) outputs[debug]prefixed messages with function name, file, and line number. Use it instead of rawqDebug()for diagnostic output. - AST nodes are owned by parent
Containerviastd::unique_ptr(using theNodePtrListalias inNode.h). Usestd::make_uniquefor allocation.Text*pointers in semantic nodes (ItalicText,BoldText, etc.) are non-owning. - The
ASSERT()macro (fromdebug.h) callsBacktrace::backtrace()beforeqt_assertin debug builds. BUILD_STATICCMake option controls static vs shared library builds.- The
.clang-formatfile at the repo root defines code style. - CI uses the
cmake-build-*pattern in.gitignore;buildis also gitignored. - Export macro
QTMARKDOWNSHARED_EXPORTis defined insrc/QtMarkdown_global.h— used on all public classes.