Skip to content

Latest commit

 

History

History
794 lines (654 loc) · 41.4 KB

File metadata and controls

794 lines (654 loc) · 41.4 KB

Markdown4D API reference

This document describes the public surface of Markdown4D: the facade, the pipeline builder, the abstract syntax tree, the document builder, the table of contents, the theme, math, and the VCL / FMX viewer and editor components.

All public enumerations are scoped ({$SCOPEDENUMS ON}), so qualify them: TMarkdownDialect.Gfm, TMarkdownNodeKind.Heading, and so on.

Contents

Facade: TMarkdown

Unit Markdown4D. The one-stop entry point for the common cases.

type
  TMarkdown = class
    class function Version: string;
    class function ToHtml(const Source: string;
      const Dialect: TMarkdownDialect = TMarkdownDialect.CommonMark): string;
    class function ToUnsafeHtml(const Source: string;
      const Dialect: TMarkdownDialect = TMarkdownDialect.CommonMark): string;
    class function Parse(const Source: string;
      const Dialect: TMarkdownDialect = TMarkdownDialect.CommonMark): IMarkdownDocument;
    class function ToMarkdown(const Document: IMarkdownDocument): string;
    class function CreateIncrementalParser(
      const Dialect: TMarkdownDialect = TMarkdownDialect.CommonMark): IMarkdownIncrementalParser;
  end;

Version returns the library version string ('2.0.0', defined as Markdown4DVersion in unit Markdown4D.Version).

TMarkdownDialect (unit Markdown4D.Defines) is (CommonMark, Gfm). The facade caches one pipeline per dialect and rendering mode.

ToHtml renders safely: raw HTML becomes <!-- raw HTML omitted -->, and a link or image destination using javascript:, vbscript:, file: or a non-image data: scheme is emptied. Use it for any document the application did not produce itself.

ToUnsafeHtml renders what the CommonMark and GFM specifications prescribe: raw HTML and every destination reach the output untouched. It is the right choice for trusted input, or when the result passes through an HTML sanitizer afterwards. The conformance suites are checked against this method.

For finer control build your own pipeline; UnsafeHtml and UnsafeLinks on the builder correspond to the two halves of ToUnsafeHtml. For untrusted input, such as user text or the output of a language model, the builder can go further than ToHtml: EscapeRawHtml shows raw HTML as text, so TList<T> outside a code span stays readable, and AllowUrlSchemes keeps only the schemes you name. Both apply to what the renderer writes itself; a renderer hook you register writes its own HTML and has to check its own destinations.

const Pipeline = TMarkdownPipeline.Create.UseGfm
  .EscapeRawHtml
  .AllowUrlSchemes(['http', 'https'])
  .NoOpenerLinks
  .Build;
const Html = Pipeline.ToHtml(Source);
uses
  Markdown4D,
  Markdown4D.Defines,
  Markdown4D.Ast.Interfaces;

const Doc = TMarkdown.Parse(Source, TMarkdownDialect.Gfm);
const Markdown = TMarkdown.ToMarkdown(Doc);

Errors raised by the library derive from EMarkdownError (unit Markdown4D.Defines).

Pipeline builder

Units Markdown4D.Pipeline and Markdown4D.Extensions.Interfaces. TMarkdownPipeline.Create returns a fluent IMarkdownPipelineBuilder. Every configuration method returns the builder, so calls chain; Build produces an immutable, reusable IMarkdownPipeline.

type
  IMarkdownPipelineBuilder = interface
    function UseCommonMark: IMarkdownPipelineBuilder;
    function UseGfm: IMarkdownPipelineBuilder;
    function Use(const Extension: IMarkdownExtension): IMarkdownPipelineBuilder;
    function XhtmlOutput: IMarkdownPipelineBuilder;
    function UnsafeHtml: IMarkdownPipelineBuilder;
    function UnsafeLinks: IMarkdownPipelineBuilder;
    function TagFilter: IMarkdownPipelineBuilder;
    function EscapeRawHtml: IMarkdownPipelineBuilder;
    function AllowUrlSchemes(const Schemes: array of string): IMarkdownPipelineBuilder;
    function NoOpenerLinks: IMarkdownPipelineBuilder;
    function RegisterBlockParser(const Parser: IMarkdownBlockParser;
      const TriggerCharacters: string; const Priority: Integer): IMarkdownPipelineBuilder;
    function RegisterInlineParser(const Parser: IMarkdownInlineParser;
      const TriggerCharacters: string; const Priority: Integer): IMarkdownPipelineBuilder;
    function RegisterDelimiterProcessor(const Processor: IMarkdownDelimiterProcessor;
      const Priority: Integer): IMarkdownPipelineBuilder;
    function RegisterRendererHook(const Hook: IMarkdownRendererHook;
      const Priority: Integer): IMarkdownPipelineBuilder;
    function RegisterDocumentProcessor(const Processor: IMarkdownDocumentProcessor;
      const Priority: Integer): IMarkdownPipelineBuilder;
    function Build: IMarkdownPipeline;
  end;

  IMarkdownPipeline = interface
    function ToHtml(const Source: string): string;
    function Parse(const Source: string): IMarkdownDocument;
  end;

Configuration options

Method Effect
UseCommonMark Registers the full CommonMark 0.31.2 block and inline grammar
UseGfm UseCommonMark plus tables, task lists, strikethrough, autolinks, tag filter, math, GitHub alerts
Use(ext) Installs a custom IMarkdownExtension
UnsafeHtml Allows raw HTML in the rendered output (CommonMark spec behaviour)
UnsafeLinks Writes every link and image destination out, including javascript:, vbscript:, file: and non-image data: (spec behaviour). Without it those destinations are emptied
XhtmlOutput Emits self-closing XHTML tags
TagFilter Applies the GFM tag filter to raw HTML
EscapeRawHtml Writes raw HTML as escaped text instead of <!-- raw HTML omitted -->: inline HTML where it stands, an HTML block in a paragraph. Wins over UnsafeHtml
AllowUrlSchemes(['http', 'https']) Keeps a link or image destination only when it is relative or its scheme is on the list; otherwise a link gets href="#" and an image an empty src. //host counts as https, and a GFM e-mail autolink needs mailto. An empty list keeps relative destinations only; a second call replaces the list; a name that is not a scheme, such as 'https://', raises EMarkdownError. The dangerous schemes stay blocked even when listed, and the list also applies after UnsafeLinks
NoOpenerLinks Adds rel="noopener noreferrer" to every link
Register* Adds a single parser, processor or hook at a given priority

Higher priority wins; ties break by registration order. Rather than passing magic numbers, use the named constants on TMarkdownPriorities (unit Markdown4D.Extensions.Interfaces): Highest, High, AboveNormal, Normal, BelowNormal, Low, Lowest, and the extension slots ExtensionProcessor, ExtensionRenderer and ExtensionLayoutOverride. A built pipeline is thread-safe to reuse for parsing and rendering.

uses
  Markdown4D.Pipeline,
  Markdown4D.Extensions.Interfaces;

const Html = TMarkdownPipeline.Create
  .UseGfm
  .UnsafeHtml
  .Build
  .ToHtml(Source);

See EXTENSIONS.md for the extension interfaces used by the Register* and Use methods.

Abstract syntax tree

Unit Markdown4D.Ast.Interfaces. Parse returns an IMarkdownDocument, the root of a tree of IMarkdownNode. Nodes are reference-counted interfaces; hold the document and the whole tree stays alive.

type
  IMarkdownNode = interface
    function GetKind: TMarkdownNodeKind;
    function GetSegment: TMarkdownSegment;
    function GetChildCount: Integer;
    function GetChild(const Index: Integer): IMarkdownNode;
    procedure Accept(const Visitor: IMarkdownVisitor);
    procedure SetExtensionData(const Key: string; const Data: IInterface);
    function TryGetExtensionData(const Key: string; out Data: IInterface): Boolean;
    property Kind: TMarkdownNodeKind read GetKind;
    property Segment: TMarkdownSegment read GetSegment;
    property ChildCount: Integer read GetChildCount;
    property Children[const Index: Integer]: IMarkdownNode read GetChild;
  end;

TMarkdownNodeKind enumerates every node type: Document, Paragraph, Heading, ThematicBreak, CodeBlock, BlockQuote, List, ListItem, HtmlBlock, Text, Emphasis, Strong, CodeSpan, Link, Image, Autolink, SoftLineBreak, HardLineBreak, InlineHtml, CustomInline, Table, TableRow, TableCell, Math.

TMarkdownSegment (StartOffset, EndOffset, Length) locates the node in the source string: StartOffset is 1-based and EndOffset points at the first character past the node. Blocks and inlines both carry one, so a Strong covers its ** markers and the Text inside it covers only the characters between them. A node whose text no longer matches the source character for character reports StartOffset = 0: that is the case inside a table cell, on a line whose tab the parser replaced by spaces, and for nodes an extension built itself. SetExtensionData / TryGetExtensionData attach arbitrary interface payloads keyed by string, the mechanism the chart extension uses to cache its parsed model on the node.

Typed node interfaces

Query a node for a richer interface with as or Supports:

Interface Extra members
IMarkdownHeading Level, SourceLine
IMarkdownCodeBlock Literal, InfoString, IsFenced
IMarkdownList IsOrdered, StartNumber, IsTight
IMarkdownText Literal (also used for code spans, HTML blocks, inline HTML)
IMarkdownLink Destination, Title (also used for images and autolinks)
IMarkdownMath Literal (the LaTeX source), IsDisplay; extends IMarkdownText
IMarkdownCustomInline NodeName (extension inline nodes such as strikethrough)
IMarkdownTableRow IsHeader
IMarkdownTableCell Alignment (TMarkdownTableColumnAlignment)
const Doc = TMarkdown.Parse(Source, TMarkdownDialect.Gfm);

for var Index := 0 to Doc.ChildCount - 1 do
begin
  const Child = Doc.Children[Index];
  if Child.Kind = TMarkdownNodeKind.Heading then
  begin
    const Heading = Child as IMarkdownHeading;
    Writeln(Format('H%d at line %d', [Heading.Level, Heading.SourceLine]));
  end;
end;

IMarkdownVisitor offers a Visit* method per node kind for double-dispatch traversal via Node.Accept(Visitor). Version 2.2 added VisitMath; a visitor written against an earlier version needs that one method to compile again.

Document builder

Unit Markdown4D.Ast.Builder. Constructs a valid document in code, then hands it to the writer or the layout engine. TMarkdownDocumentBuilder.Create returns a fluent IMarkdownDocumentBuilder.

Convenience methods (Heading, Paragraph, Bold, Italic, Code, Math, MathBlock, Link, Image, Cell, …) emit a complete node in one call. Begin… / End… pairs open a container you fill with nested content (BeginParagraph, BeginBulletList, BeginOrderedList, BeginListItem, BeginTaskListItem, BeginTable, BeginTableRow, BeginTableCell, BeginBlockQuote, BeginBold, BeginItalic, BeginStrikethrough, BeginLink, BeginHeading). Structural rules are enforced (a table may contain only rows, a row only cells), and Build raises EMarkdownError if any node is left open.

uses
  Markdown4D,
  Markdown4D.Ast.Builder;

const Doc = TMarkdownDocumentBuilder.Create
  .Heading(1, 'Report')
  .Paragraph('Generated by Markdown4D.')
  .BeginBulletList
    .BeginListItem.Text('First item').EndListItem
    .BeginListItem.Text('Second item').EndListItem
  .EndList
  .Build;

const Markdown = TMarkdown.ToMarkdown(Doc);

Table of contents

Unit Markdown4D.Toc. TMarkdownToc.FromDocument walks the headings of any document and returns a nested IMarkdownToc. Each IMarkdownTocEntry carries Caption, Level, Anchor (a GitHub-style slug, de-duplicated with a numeric suffix), SourceLine, and nested Children.

uses
  Markdown4D,
  Markdown4D.Toc;

const Doc = TMarkdown.Parse(Source, TMarkdownDialect.Gfm);
const Toc = TMarkdownToc.FromDocument(Doc);

for var Index := 0 to Toc.EntryCount - 1 do
begin
  const Entry = Toc.Entries[Index];
  Writeln(Format('%s -> #%s', [Entry.Caption, Entry.Anchor]));
end;

Theme

Unit Markdown4D.Theme. TMarkdownTheme holds every colour, font and metric the layout engine uses. Construct one with CreateLight, CreateDark or CreatePreset(TMarkdownThemePreset); you own the instance and must Free it (the viewer/editor take ownership when you assign their Theme property).

Selected properties: BaseFont, CodeFont, MathFont, HeadingFonts[Level], TextColor, BackgroundColor, LinkColor, CodeTextColor, CodeBackgroundColor, BlockQuoteBarColor, TableHeaderBackgroundColor, TableBorderColor, ThematicBreakColor, MathErrorColor, BlockSpacing, ListIndent, ContentPadding, the Chart* colours and ChartPalette, TokenColors[Kind] for code highlighting, DiffInsertedBackgroundColor and DiffDeletedBackgroundColor for the lines of a diff block, MarkBackgroundColor behind text in a <mark> tag, and AlertColors[Kind] for GitHub alerts. Colours are TLayoutColor ($AARRGGBB). Chart sizing and axis-label formatting are not part of the theme; see Chart layout options.

Vertical spacing works like CSS margins. Each block has a spacing above and below it: HeadingSpacingAbove[Level] and HeadingSpacingBelow[Level] for headings, ThematicBreakSpacing on both sides of a thematic break, and BlockSpacing below every other block. Two adjacent spacings collapse to the larger of the two instead of adding up. The defaults follow GitHub's stylesheet.

SaveToJson / LoadFromJson serialise a complete theme so you can ship it as a resource or let users edit it. A theme saved before blockSpacing and thematicBreakSpacing existed does not load; add those two keys. The colours a theme does not contain come from the preset, light or dark, that its background matches.

uses
  Markdown4D.Theme;

const Theme = TMarkdownTheme.CreateDark;
try
  Theme.LinkColor := $FF3B82F6;
  const Json = Theme.SaveToJson;
finally
  Theme.Free;
end;

Alerts

GitHub alerts are part of the GFM dialect. A block quote at the top level of the document whose first line is [!NOTE], [!TIP], [!IMPORTANT], [!WARNING] or [!CAUTION], in any case, becomes an alert. A quote inside a list or another quote keeps its marker as text, as on GitHub.

The marker line is taken out of the quote, and the quote carries the kind: TMarkdownAlerts.TryGetKind(Node, Kind) in unit Markdown4D.Extensions.Alerts. The HTML renderer writes GitHub's markup, a <div class="markdown-alert markdown-alert-note"> with a <p class="markdown-alert-title">, without GitHub's icon; the markdown writer writes the marker back. The viewers draw the bar, an icon and the title in Theme.AlertColors[Kind] and the text in Theme.TextColor.

Table of contents

GitLab's table of contents marker is part of the GFM dialect. A paragraph at the top level of the document that holds nothing but [[_TOC_]] or [TOC] gets a nested list of links to every heading, one link per heading to its anchor (see ScrollToAnchor). The paragraph stays in the document and carries the list: TMarkdownTocMarkers.TryGetContents(Node, Contents) in unit Markdown4D.Extensions.Toc. The viewers draw the list in place of the marker; the HTML renderer and the markdown writer keep the marker as text.

Math

Formulas are part of the GFM dialect, so TMarkdown.Parse(Source, TMarkdownDialect.Gfm), ToHtml with that dialect, and both viewers recognise them. The CommonMark dialect leaves dollars alone.

Syntax

The rules follow Pandoc, GitHub and Markdig, so a document written for those renders the same here.

Form Example Rule
Inline $E = mc^2$ The opening $ may not follow a letter or digit and may not be followed by whitespace; the closing $ may not follow whitespace and may not be followed by a letter or digit. $100 and $200 therefore stays text.
Inline, display style $$\sum_{i=1}^n i$$ Same rules; the formula stays on the line but takes display style, with limits above and below.
Block $$ on a line of its own, the formula, $$ again A block, centred in the column. $$x$$ on one line is inline display math.
Fence alias ```math The GitHub and GitLab form; same result as the block.
GitLab inline $`x^2`$ The backticks fence the formula off from the markdown around it.

Backslash escapes work as usual: \$ is a dollar sign. Inside a formula a backslash escapes the character after it, so \$ never closes one. A code span outranks math: `$x$` is code, and a formula never runs into one.

In the tree, in HTML, in markdown

A formula is a node of kind TMarkdownNodeKind.Math; query it as IMarkdownMath for Literal (the LaTeX source, untouched) and IsDisplay. The HTML renderer writes <span class="math">\(...\)</span> inline, <span class="math">\[...\]</span> for inline display math, and <div class="math">\[...\]</div> for a block, which is what KaTeX and MathJax pick up without configuration. The markdown writer round-trips both forms, the table of contents and image alt text carry the source, and the document builder has Math(Literal, IsDisplay) and MathBlock(Literal).

In the viewers

The viewers set formulas themselves, with TeX's box model: the eight atom classes and their spacing, fractions on the math axis, super- and subscripts with the clearance rules, limits above and below large operators in display style, radicals with an optional index, delimiters that grow into drawn shapes when a glyph is too short, accents, and matrix environments. An inline formula sits on the text baseline and pushes the lines apart when it is tall; a display formula is centred in the column. A formula selects as one unit and copies as its markdown, $...$ or a $$ block, so a paste lands back in a document as the same formula; find searches the LaTeX source, not the drawn glyphs.

Theme.MathFont names the family and size; the default family is the generic math, which the painters resolve to the bundled STIX Two Math when Markdown4D.Vcl.MathFont or Markdown4D.Fmx.MathFont is in a uses clause, and to the platform's math font otherwise (Cambria Math on Windows, STIX Two Math on macOS 13 and later; see packages/INSTALL.md for the other platforms). Inline formulas take the size of the surrounding text. An unknown command is drawn by name in Theme.MathErrorColor.

The parser never raises: an unclosed group closes at the end, a stray closing brace is dropped, \right without \left keeps its delimiter. A formula streaming in token by token therefore draws at every flush and only grows.

Supported LaTeX

Area Commands
Structure \frac, \dfrac, \tfrac, \binom, \sqrt[n]{}, ^, _, ', {} groups, \left ... \right with ( ) [ ] \{ \} | \langle \rangle \lfloor \rfloor \lceil \rceil .
Environments matrix, pmatrix, bmatrix, Bmatrix, vmatrix, Vmatrix, cases, aligned, align, gathered; \\ at the top level breaks a display formula into lines
Operators \sum, \prod, \int, \iint, \oint, \bigcup, \bigcap, \lim, \max, \min, \sup, \inf, \det, \gcd, \sin and the other function names, \operatorname{}, \limits, \nolimits
Symbols The Greek alphabet, \pm \times \cdot \div \circ \cup \cap \wedge \vee \oplus \otimes, \leq \geq \neq \approx \equiv \sim \subset \subseteq \in \notin \perp \parallel \mid, the arrows, \infty \partial \nabla \forall \exists \emptyset \neg \hbar \ell \aleph \ldots \cdots \vdots \ddots \prime \angle
Alphabets \mathrm, \mathbf, \boldsymbol, \mathit, \mathbb, \mathcal, \mathfrak, \mathsf, \mathtt, \text, \textit, \textbf
Accents \hat, \bar, \vec, \dot, \ddot, \tilde, \check, \breve, \acute, \grave, \overline
Spacing \,, \:, \;, \!, \ , \space, ~, \quad, \qquad
Colour \textcolor{colour}{...} colours its argument; \color{colour} is a switch that colours the rest of its group, as in LaTeX, KaTeX and MathJax 3. A colour is a CSS colour name or a hex value in three or six digits, with or without #

Anything else comes out as its name, in the error colour, so the author sees what did not resolve.

Chemistry

Inside a formula, \ce{...} is a subset of mhchem. The stored formula is unchanged: HTML, the markdown writer, find, and copy still carry \ce{...}. The viewers lower the command onto the math list above before drawing it.

Form Result
H2O, 2H2O, 2.5H2O Element symbols stay upright. A count after an atom is a subscript. A leading number is a coefficient and may contain a decimal point.
Ca3(PO4)2, [Cu(NH3)4]^2+ A parenthesised or bracketed group takes a count or a charge like an atom.
SO4^2-, Na+, [AgCl2]- A charge sticks to the last atom or group.
^{14}C, ^{227}_{90}Th Mass and atomic numbers stand to the left of the element that follows.
(s), (l), (g), (aq) The state of matter stays upright, and Cl-(aq) keeps its charge.
->, <-, <->, <=> Reaction, reverse, resonance, and equilibrium arrows.
CH3-CH2-OH, C=C, C#N The bonds stay upright and tight between their atoms: a hyphen, an equals sign, and an equivalence sign for the triple bond.
CuSO4*5H2O, CuSO4.5H2O An asterisk, or a dot before a number, is the addition dot of a hydrate.

A space ends a species, so the + in Na + is an operator rather than a charge, but it is not drawn: the spacing around + and the arrows comes from the math layout, as it does for any operator.

A piece the subset does not recognise is copied into the formula, so an unknown command is still drawn by name in Theme.MathErrorColor. Text over an arrow (->[H2O]), the \pu units command, and italic variables such as the n in C_nH_{2n+2} are outside the subset. The mass and atomic numbers of an isotope align on their left edge, where mhchem aligns them on the right. The lowering stops at 32 nested groups, drawing a deeper group as an ellipsis, and at a \ce nested inside four others, drawing it in the error colour; real formulas never come close.

The native viewers need no setup. An HTML consumer must make \ce available to its typesetter: load KaTeX's contrib/mhchem extension after KaTeX and before auto-render, or enable MathJax's mhchem package when it is not already provided by autoload. Both typeset the whole of mhchem, so the forms outside the subset still render in HTML.

Incremental parser

Units Markdown4D and Markdown4D.Parser.Interfaces. TMarkdown.CreateIncrementalParser returns an IMarkdownIncrementalParser that keeps state between edits and reparses only the affected region.

type
  IMarkdownIncrementalParser = interface
    procedure Append(const Chunk: string);
    procedure ReplaceRange(const StartIndex, Count: Integer; const Replacement: string);
    function ToHtml: string;
  end;

Append adds text at the end (the streaming case); ReplaceRange edits an existing region (the editor case); ToHtml renders the current document.

uses
  Markdown4D,
  Markdown4D.Defines,
  Markdown4D.Parser.Interfaces;

const Parser = TMarkdown.CreateIncrementalParser(TMarkdownDialect.Gfm);

Parser.Append('# Live'#10);
Parser.Append('More **text** streaming in.'#10);

const Html = Parser.ToHtml;

The viewer components use the same incremental machinery internally; see STREAMING.md.

Viewer components

TMarkdownViewer renders markdown natively onto the control canvas, without a browser. The VCL control lives in Markdown4D.Vcl.Viewer (a TCustomControl); the FMX control in Markdown4D.Fmx.Viewer (a TControl). Their public surface is the same.

Published properties

Property Type Notes
Text string The whole markdown document as one value
ThemePreset TMarkdownThemePreset Light / Dark, editable in the Object Inspector
Images TMarkdownViewerImageSettings How image destinations are resolved and fetched (see below)
Zoom Integer In percent (default 100, from 25 to 500). Every font, spacing and image grows with it; the theme the application assigned stays as it is. Ctrl+wheel and Ctrl+Plus/Minus step through the levels browsers use (25, 33, 50, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300, 400, 500), Ctrl+0 resets it. The line at the top of the view stays there, and a document that lays out slowly waits until the wheel rests
CopyAsMarkdown Boolean Offers Copy as Markdown on the context menu and Ctrl+Shift+C (default True). Off, the menu leaves the entry out and Ctrl+Shift+C copies plain text, for an application that keeps the source to itself
AutoScroll Boolean Middle-click autoscroll: the content scrolls faster the further the pointer is from where it was pressed, until the next click, key or wheel turn (default True). The key that ends it reaches no shortcut, menu or form handler; only an Alt+letter accelerator in FMX goes to the form first

Public members

Member Description
Theme: TMarkdownTheme Assign a fully customised theme at run time (the control takes ownership)
AppendMarkdown(const Markdown: string) Append text and repaint; thread-safe, debounced
LoadFromFile(const FileName) / LoadFromStream(const Stream) Load a document
FindText(const Needle[; const Options]): Boolean Select the next match and scroll it to the middle of the view when it is out of sight; a repeated search moves on to the following match and wraps to the first after the last. TMarkdownFindOptions (unit Markdown4D.Layout.TextSearch) adds MatchCase and WholeWord, as in the editor
FindPrevious(const Needle[; const Options]): Boolean The same walk backwards; wraps to the last match before the first
FindMatchCount(const Needle[; const Options]): Integer How many matches a walk with FindText visits; a formula counts once
FindMatchIndex(const Needle; const Options): Integer Which of those matches the selection is, counted from 0, for a "3 of 12"; -1 when the selection is not a match
HighlightMatches(const Needle[; const Options]) / ClearHighlights Mark every match, independently of the selection; the marks follow the document as it changes, an empty needle clears them, and a formula is marked once
HighlightCount: Integer How many matches are marked
CopySelectionToClipboard Copy the current selection
CopySelectionAsMarkdown / SelectedMarkdown: string Copy, or read, the markdown behind the selection. Within one line that is the selected source with the markup directly around it (selecting bold in **bold** gives **bold**); over several lines whole source lines, so list markers, quote markers and indentation come along; a table always whole; and a selection of everything the whole source. Line ends are the platform's
SelectAll Select the whole document
ClearSelection Drop the selection
SelectedText: string The selected text
IsAutoScrolling: Boolean True while middle-click autoscroll runs
ZoomIn / ZoomOut / ResetZoom Step Zoom to the next level up or down, or back to 100, as the keys do
TryGetSelectionSourceSegment(out Segment: TMarkdownSegment): Boolean The stretch of markdown the selection was rendered from, so an editor can format exactly those characters; False when there is no selection or the runs carry no source
ContentHeight: Integer Laid-out document height, for auto-sizing
ScrollOffset: Single Read / set the vertical scroll position
ScrollToAnchor(const Anchor: string): Boolean Scroll the heading a link such as #getting-started points at to the top of the view. Anchors follow GitHub: lower case, punctuation dropped, accented letters kept, every space a dash, and a repeated heading gets -1, -2. A percent-encoded anchor is decoded first. False when the document has no such heading
LayoutCount: Integer Advances on every relayout (first width, resize, arriving images), so a host can notice layout-derived state going stale
DisplayList: IMarkdownDisplayList The rendered primitives, for advanced hosts

Events

Event Signature Raised when
OnLinkClick (const Sender: TObject; const Url: string) A link is clicked. A #... link to a heading in the document scrolls there instead and raises nothing
OnLinkHover (const Sender: TObject; const Url: string) The hovered link changes ('' on leave)
OnResolveImage (const Sender: TObject; const Url: string; const Picture/Bitmap; var Handled: Boolean) An image needs resolving; set Handled to supply it yourself
OnRemoteImageRequest (const Sender: TObject; const Url: string; var Allow: Boolean) About to fetch a remote image. Allow arrives holding Images.AllowRemote; clear it to refuse this address
OnScroll TNotifyEvent The scroll position changes
OnAutoScrollChange TNotifyEvent Autoscroll starts or stops
OnZoomChange TNotifyEvent Zoom changes and the document has been laid out at it
OnClick TNotifyEvent A click in the text, on release. Not for a link, the copy button of a code block, the scroll bar, a drag that selects text or the click that ends autoscroll
OnDblClick TNotifyEvent A double click in the text, on release of the second click, with the same exceptions. The word it selected stays selected. The second click raises no OnClick
OnMouseDown / OnMouseMove / OnMouseUp the standard mouse events Every press, move and release, as on any control
OnExtensionError (const Sender: TObject; const Extension: string; const Error: Exception) A block override or a document processor raised. The document shows without what that extension would have drawn or added (see EXTENSIONS.md)

The viewer keeps the size of every word it measured, so laying a document out again (a new width, an arriving image, an edited text) costs a fraction of the first layout. When a layout takes longer than a frame, as with very large documents, a new width is laid out once the drag ends rather than on every pixel: when the mouse button is released, or once the width rests and no button is held.

The viewer loads http(s) images asynchronously and local images relative to Images.BaseUrl or the loaded document's folder. Code blocks tagged pascal, sql, json, xml or diff are syntax-highlighted, and the added and removed lines of a diff block get a background as well; chart blocks render as graphics when the chart block override is registered (see EXTENSIONS.md).

An HTML block in the document is never painted as markup: unlike ToHtml and ToUnsafeHtml, which either omit or emit raw HTML as text, the viewer translates an allowed subset to markdown and lays that out through the ordinary path, so it gets selection, hit-testing and theming for free. The subset covers p, div, center, section, article, h1-h6, hr, br, strong/b, em/i, code/kbd/samp/tt, del/s/strike, a, img, ul/ol/li, blockquote, details/summary and pre. script, style, head, iframe and object are dropped along with their content; any other tag disappears while its content stays, the way a browser would show it with the styling removed. The translation is implemented in Markdown4D.Html.Subset and cached on the AST node, since layout runs again on every resize and every streamed chunk.

A tag inside a paragraph, heading or table cell is never painted either. The viewer follows the subset GitHub renders: b/strong bold, i/em/var italic, code/kbd/samp/tt in the code font, s/del/strike struck through, ins underlined, sub and sup smaller and below or above the baseline, small smaller, mark on Theme.MarkBackgroundColor, br a line break and a href a link. A comment shows nothing, and any other tag disappears while its text stays, u among them, as on GitHub. The document itself keeps the tags, so the editor and the markdown writer see them as written.

The mouse wheel scrolls the control only while its content overflows; otherwise the wheel passes through to the parent, so viewers stacked inside a scroll box scroll the list they sit in. The VCL controls carry the native window scrollbar; the FMX viewer and editor draw a draggable overlay thumb whenever their content overflows.

A focused viewer scrolls on the arrow keys, PgUp / PgDn, Home and End. Ctrl+A selects the document, Ctrl+C copies the selection and Ctrl+Shift+C copies its markdown. Right-clicking opens a Copy / Copy as Markdown / Select All menu; assigning PopupMenu replaces it with the host's own menu.

Image settings

Unit Markdown4D.Viewer.ImageSettings, republished by both viewer units.

Property Default Notes
BaseUrl '' Resolves relative image destinations
AllowRemote True Whether http(s) destinations may be fetched at all
MaxBytes 8 MB Upper bound on one downloaded image; 0 removes the bound
RestrictToDocumentFolder False Keeps a relative path inside the document's own folder

Opening a document fetches every remote image it names, which tells those hosts that the document was read. An application showing documents it did not write should decide what it wants here: clear AllowRemote to fetch nothing, or leave it on and refuse individual addresses through OnRemoteImageRequest.

procedure TMainForm.ViewerRemoteImageRequest(const Sender: TObject; const Url: string;
  var Allow: Boolean);
begin
  Allow := Url.StartsWith('https://cdn.example.com/', True);
end;
uses
  Markdown4D.Theme,
  Markdown4D.Vcl.Viewer;

const Viewer = TMarkdownViewer.Create(Self);
Viewer.Parent := Self;
Viewer.Align := alClient;
Viewer.ThemePreset := TMarkdownThemePreset.Dark;
Viewer.Text := '# Welcome'#10#10 + 'This is **Markdown4D**.';

Editor components

TMarkdownEditor is a syntax-highlighting source editor for markdown. The VCL control lives in Markdown4D.Vcl.Editor, the FMX control in Markdown4D.Fmx.Editor.

Published properties

Property Type Notes
Text string The markdown source
ThemePreset TMarkdownThemePreset Light / Dark
ShowLineNumbers Boolean Gutter line numbers (default False)
Zoom Integer In percent, as in the viewer: the text, its line height and the gutter grow with it, with the same keys and levels. The source line at the top of the view stays there. Editor and attached preview zoom independently; a host that wants them together sets one from the other's OnZoomChange
AutoScroll Boolean Middle-click autoscroll, as in the viewer. Default True, except in the FMX editor on Linux, where the middle button pastes the primary selection

Public members

Member Description
CaretPosition: Integer Read / set the caret offset
SelectedText: string The current selection
IsAutoScrolling: Boolean True while middle-click autoscroll runs
ZoomIn / ZoomOut / ResetZoom Step Zoom to the next level up or down, or back to 100, as the keys do
SelectRange(const StartOffset, CharacterCount: Integer) Select CharacterCount characters from StartOffset, counted from 0 as the caret is, and scroll them into view
TryAdoptPreviewSelection: Boolean Move the selection the reader made in the attached preview onto the same characters here, leaving the whitespace at its edges out; False when the preview holds no selection or is still showing older text
Theme: TMarkdownTheme Assign a custom theme at run time
ExecuteCommand(const Command: TEditorCommand) Apply Bold, Italic, Link or CodeBlock to the selection. Bold, Italic and Strikethrough read the markdown first: a selection that half covers marks of the same style takes that whole stretch in, and a selection that already carries them throughout has them taken off, so running the command twice leaves the text as it was
Undo / Redo / CanUndo / CanRedo Undo stack
AttachPreview(const Viewer: TMarkdownViewer) / DetachPreview Bind a live preview viewer
FlushPreview Force a pending preview refresh immediately

Events

Event Signature Raised when
OnChange TNotifyEvent The text changes
OnScroll TNotifyEvent The editor scrolls
OnAutoScrollChange TNotifyEvent Autoscroll starts or stops
OnZoomChange TNotifyEvent Zoom changes and the text has been wrapped at it
OnClick TNotifyEvent A click in the text, on release. Not for a fold marker, the scroll bar, a drag that selects or moves text or the click that ends autoscroll
OnDblClick TNotifyEvent A double click in the text, on release of the second click, with the same exceptions. The second click raises no OnClick
OnMouseDown / OnMouseMove / OnMouseUp the standard mouse events Every press, move and release, as on any control

AttachPreview wires the editor to a TMarkdownViewer: edits refresh the preview after a pause in typing, and the preview keeps its scroll aligned with the editor's first visible source line. The pause grows with how long the last update took, so a large document does not stall the typing, and a hidden preview waits until it shows. Like the viewer, the editor wraps a very large document to a new width once a drag ends rather than on every pixel.

uses
  Markdown4D.Vcl.Editor,
  Markdown4D.Vcl.Viewer;

FEditor.AttachPreview(FPreview);
FEditor.Text := '# Live preview'#10#10 + 'Type on the left, rendered on the right.';

Translating

Every text the viewer and the editor show is a resourcestring in unit Markdown4D.Consts: the context menu captions, the copy button on code blocks and the alert titles (Note, Tip, Important, Warning, Caution). The Delphi translation tools and resource DLLs pick them up like the VCL's own strings. The alert titles appear in the HTML that ToHtml writes as well.

The context menus show each entry's shortcut through the menu item's ShortCut, so its text (Ctrl+C, or Strg+C in a German build) comes from Delphi's own translated key names.

Drawing SVG

The viewers render SVG images with an engine of this project. Nothing has to be switched on for that: adding a viewer to a form brings it along.

Markdown4D.Image.Svg is the hook the viewers route image bytes through, and Markdown4D.Image.Svg.Native is the engine registered on it:

uses
  Markdown4D.Image.Svg,
  Markdown4D.Image.Svg.Native;

// Draws with this engine alone, reporting False for anything it will not draw
// rather than passing it on. TMarkdownSvgSupport.TryRasterize goes through
// whichever engine is registered.
var Raster: TMarkdownSvgRaster;
if TryRasterizeSvgNatively(Bytes, 200, 200, Raster) then
  // Raster.Pixels is premultiplied BGRA, top down, stride = Width * 4.

Covered: path, rect with corner radii, circle, ellipse, line, polyline, polygon, g, use, svg with a viewBox, transform lists, solid fills, linear and radial gradients, patterns, strokes with miter and round joins and caps, opacity, both fill rules, clipPath, mask, image carrying a data URI, text, and filters built from feGaussianBlur, feOffset, feFlood, feComposite and feMerge.

Refused whole, so a document is never drawn half right: foreignObject, an image pointing outside the document, and any filter primitive not in that list.

Two units underneath are useful on their own. Markdown4D.Image.Rasterizer fills polygons with anti-aliasing, one colour or a gradient or a tile, held inside an optional mask. Markdown4D.Image.Filters holds the pixel operations the filters are built from.

Glyph outlines and image decoding

Two things drawing an SVG needs from the machine it runs on. Both sit behind a seam, and both already have an answer on every platform, so an application normally does nothing here.

uses
  Markdown4D.Image.Glyphs;

// The outlines of a run of text, laid out from an origin at the baseline
// start. Registered by Markdown4D.Image.Glyphs.Gdi on Windows and by
// Markdown4D.Fmx.Glyphs everywhere FMX runs.
TMarkdownGlyphSupport.RegisterOutliner(
  function (const FamilyName: string; const PixelSize: Single; const Bold, Italic: Boolean;
    const Text: string; out Run: TMarkdownGlyphRun): Boolean
  begin
    // Run.Contours in the units of PixelSize, Run.Advance is the width.
  end);
uses
  Markdown4D.Image.Decoder;

// Encoded image bytes to pixels. Registered by Markdown4D.Vcl.ImageDecoder and
// Markdown4D.Fmx.ImageDecoder, whichever the application already has.
TMarkdownImageDecoding.RegisterDecoder(
  function (const Data: TBytes; out Raster: TMarkdownPixelRaster): Boolean
  begin
  end);

Register your own to reach a font or a format the platform does not offer. A provider registered before the viewer unit initialises keeps its place: the bundled ones step aside when something is already there.