Markdown

A Markdown document rendered as native, themed content, with code blocks, tables, alerts and task items composed from library components.

<tp-markdown>
Markdown
Default density Compact density Restart from the initial state Show the copyable source
## Release 2.4 Shared workspaces are here. Invite up to **50 teammates**, assign roles, and keep billing in one place. See the [migration notes](#usage) before you upgrade. - [x] Workspace roles and permissions - [x] Consolidated invoices - [ ] Audit log export | Plan | Seats | Price | | :--- | ---: | ---: | | Starter | 5 | $0 | | Team | 50 | $12 / seat |

Usage

Markdown is parsed by a built-in CommonMark and GitHub Flavored Markdown parser and rendered as real elements, never injected as HTML. The source comes from the source property or, when that is unset, from the element’s text content, dedented. Set source from script for dynamic content.

CommonMark blocks and inlines, plus GitHub Flavored Markdown tables (with column alignment), task list items, strikethrough, www./URL/email autolinks and footnotes. Numeric character references and the HTML 4 named references decode; other names stay literal.

Authored source

Plain text content is parsed by the browser first, so a literal <tag> in it would become an element. Wrap the source in a <script type="text/markdown"> child to keep < and & exactly as written; HTML the element policy allows, such as kbd, still renders.

Script child
Default density Compact density Restart from the initial state Show the copyable source

Headings

heading-offset shifts every heading level so the document fits under the page’s own outline: with an offset of 2, # renders as h3. Headings get ids, prefixed with id-prefix when several documents share a page.

Links to a heading or footnote of the same document (#section, footnote references and back-links) scroll to it and update the URL fragment. A URL that already names one of them scrolls there on the first render.

Heading offset
Default density Compact density Restart from the initial state Show the copyable source
# Billing Invoices now show usage per workspace. ## Fixes - Tax IDs are validated before checkout. - Receipts use the account's locale.

Composed blocks

Fenced code becomes a Code block, and the fence can set a title, highlighted lines and line numbers. Blockquotes starting with [!NOTE], [!TIP], [!WARNING] and similar become Alerts.

Prose becomes native semantic elements; fenced code, tables, rules, task items, alerts and keys compose the library’s Code block, Table, Separator, Checkbox, Alert and Kbd.

The first word of a fence’s info string is the language. The rest of the line can set Code block options: title="file.ts" (the label), {1,3-5} (highlighted lines) and showLineNumbers.

Code and alerts
Default density Compact density Restart from the initial state Show the copyable source
Rotate your key from the dashboard, then update the client: ```ts title="client.ts" {2} const client = new Tweakpad({ apiKey: process.env.TWEAKPAD_KEY, }); ``` > [!WARNING] > The old key stops working 24 hours after rotation.

Size

size="sm" steps body text and headings down one size, for chat bubbles, side panels and other dense surfaces.

Small
Default density Compact density Restart from the initial state Show the copyable source
**Deploy finished** in 1m 12s. 1. Built 214 pages 2. Purged the CDN cache 3. Notified #releases Preview it at `northwind-pr-482.tweakpad.app`.

Streaming

For replies that arrive in chunks, set streaming and call append(). Unfinished emphasis, code and links in the last block are completed before parsing, so their markers never flash, and blocks that did not change keep their DOM.

Blocks whose text did not change keep their DOM, and spacing only flows from each block’s start edge, so new content never restyles what is already on screen. Clear streaming when the source is complete.

markdown.streaming = true; for await (const chunk of reply) markdown.append(chunk); markdown.streaming = false;
Streaming
Default density Compact density Restart from the initial state Show the copyable source
Replay

Custom elements

Raw HTML is never inserted as markup. Tags in the elements policy become real elements with only the attributes you list; anything else shows as literal text. Here the policy admits tp-badge.

Element policy
Default density Compact density Restart from the initial state Show the copyable source

The default policy admits abbr, b, br, del, details, em, i, ins, kbd, mark, s, small, strong, sub, summary, sup and u, with title, lang and dir on every tag and open on details. Your policy adds to it; false removes a tag.

markdown.elements = { 'tp-badge': ['variant'], 'my-chart': ['data-series'], '*': ['title', 'lang', 'dir', 'class'], u: false, };
Status <tp-badge variant="secondary">Beta</tp-badge> <my-chart data-series="sales"> Markdown **content** between an open and a close tag becomes the element's children. </my-chart>

Event-handler attributes, style and srcdoc are always removed, and URL attributes (href, src, …) must pass the URL policy. Elements are created in the element’s document, so custom elements upgrade once they are defined.

Renderers

renderers replaces how one node type renders. Each renderer receives the parsed node and a context with children() and fallback(), the default rendering, and returns DOM or a Lit template, or undefined to keep the default. Here a checklist fence becomes a list of Checkboxes and links to other hosts get an outbound marker, while every other node renders as usual.

Custom node rendering
Default density Compact density Restart from the initial state Show the copyable source

Return undefined to keep the default; return Lit nothing to render nothing. A renderer that throws falls back to the default and reports markdown-renderer.

Node types: heading, paragraph, blockquote, list, listItem, code, table, thematicBreak, alert, footnotes, text, emphasis, strong, delete, inlineCode, break, link, image, footnoteReference and element (admitted raw HTML, with tagName and attributes).

Outline

headings lists the rendered headings as { element, id, label, depth }, and tp-markdown-render fires after every render, so a Table of contents can follow a document whose source changes. Each item targets a heading element directly.

With a table of contents
Default density Compact density Restart from the initial state Show the copyable source

Parser adapters

The parser property accepts any { name?, parse(source) } that returns an mdast root, for example an adapter around a markdown engine your application already uses. The library never imports a parser package. Reference links and definitions in the returned tree are resolved; a parser that throws or returns something other than a root falls back to the built-in parser and reports markdown-parser. The built-in parser is also available as parseMarkdown from @tweakpad/ui/markdown, with resolveMarkdown, completeMarkdown and serializeMarkdown.

Serializer

serializeMarkdown(root) writes a tree back as markdown in the same dialect. Parsing the result with parseMarkdown gives the same tree, apart from source positions, so a document can be read, changed as a tree and written again.

import { parseMarkdown, serializeMarkdown } from '@tweakpad/ui/markdown'; const tree = parseMarkdown('# Notes'); tree.children.push({ type: 'paragraph', children: [{ type: 'text', value: 'Added *as text*' }] }); const markdown = serializeMarkdown(tree);
# Notes Added \*as text\*
  • The same tree always gives the same text, and no value makes it throw.
  • Text is escaped only where it would otherwise be read as syntax: 5*3 and a * b stay as they are, *x* in text becomes \*x\*, and a line that would start a heading, quote or list item gets a backslash before its marker. Whitespace the parser would strip, such as a leading space, is written as a character reference (&#32;).
  • Each construct has one form: *emphasis*, **strong**, ~~strikethrough~~, - bullets, 1. ordered items, *** rules, # headings, fenced code whose fence no line of the code repeats, and links as [text](url "title"), or <url> when the text is the address. Where two spans meet, _ replaces * so that their delimiters stay apart. A heading that holds a line break is underlined at levels 1 and 2; deeper, the break becomes <br>.
  • An html node is written as its value. A node of a type the serializer does not know is written as its children, or its value as text.
  • Content a position cannot hold changes form without breaking the structure around it: a line break in a table cell becomes <br>, a pipe in a cell is escaped, and a paragraph without content is written as a no-break space, which reads back as an empty paragraph.
  • What resolveMarkdown adds resolves again: alerts are written as > [!NOTE] quotes, elements the element policy admitted as their HTML with escaped attribute values, and the footnotes section as footnote definitions.

The text ends with a line break; an empty tree gives an empty string.

Security

Links allow http:, https:, mailto:, tel:, relative references and fragments; images allow http:, https: and relative references. A blocked link renders its text and a blocked image its alternative text, and both report markdown-url. Change the lists with urlPolicy:

markdown.urlPolicy = { links: ['https:'], images: ['https:', 'data:'] };

Shared policies

The rules the resolve step applies are exported from @tweakpad/ui/markdown, so code that cleans imported HTML, such as an editor’s paste handling, applies the same definitions instead of keeping a copy:

ExportDefinitionDEFAULT_MARKDOWN_ELEMENTSthe default element policymergeElementPolicy(policy)the default policy with yours merged in, as a map of tag to attribute namesisAlwaysRemovedAttribute(name)true for event handlers (on…), style and srcdoc, whatever a policy listsLINK_URL_ATTRIBUTESattributes whose value is a link: href, action, formaction, cite, …IMAGE_URL_ATTRIBUTESattributes whose value is an image source: src, poster, srcsetDEFAULT_LINK_PROTOCOLS, DEFAULT_IMAGE_PROTOCOLSthe default protocol listslinkProtocols(policy), imageProtocols(policy)the protocols of a URL policy, the defaults where it sets noneallowedUrl(url, protocols)whether a destination passes a protocol list; relative references and fragments always pass

The lists and the default element policy are frozen. A value that cleaned content keeps in its own model, such as an alignment read from style, is read before the attribute is removed; the attribute itself is never kept.

import { allowedUrl, isAlwaysRemovedAttribute, LINK_URL_ATTRIBUTES, linkProtocols, } from '@tweakpad/ui/markdown'; function keeps(name, value) { if (isAlwaysRemovedAttribute(name)) return false; return !LINK_URL_ATTRIBUTES.includes(name) || allowedUrl(value, linkProtocols(null)); }

Accessibility

Headings, lists, quotations, emphasis, links and images keep their native semantics; heading-offset keeps the document outline consistent with the page. An image without alternative text is decorative. Task checkboxes are read-only and named by their item text. The footnotes section is a region named by the footnotes message, and each back-link is named by the backReference message and number. The element adds no landmark or tab stop of its own; composed tables and overflowing code blocks become focusable only to scroll.

API Reference

Every element also accepts motion-policy.

Attributes

AttributePropertyTypeDefaultDescriptionsourcesourcestring | undefinedundefinedThe Markdown source, in place of the element's text content; set it from script for dynamic content.streamingstreamingbooleanfalseMarks the source as arriving in chunks: unfinished emphasis, code and links in the last block are completed before parsing.sizesize'default' | 'sm''default'The typography scale; sm for chat bubbles, side panels and other dense surfaces.heading-offsetheadingOffsetnumber0Shifts every heading level, so with 2 a # heading renders as h3, to fit under the page's own outline.id-prefixidPrefixstring''A prefix for heading ids, for several documents on one page.

Properties

PropertyTypeDefaultDescriptionelementsMarkdownElementPolicy | nullnullThe HTML tags the source may use, each with the attributes it keeps; any other tag shows as literal text.renderersMarkdownRenderers{}Functions that replace how one node type renders; returning undefined keeps the default.urlPolicyMarkdownUrlPolicy | nullnullThe protocols links and images may use; others are blocked and reported.parserMarkdownParser | nullnullA parser of your own in place of the built-in one, producing the same tree.highlighterCodeHighlighter | nullnullThe highlighter of fenced code; the Code block's built-in one when unset.messagesMarkdownMessages{}Replacements for the built-in text, such as the alert titles; missing entries keep the defaults.astread-onlyMarkdownRoot—The resolved tree of the current source.headingsread-onlyMarkdownHeadingTarget[]—The rendered headings as { element, id, label, depth }, for a table of contents.markdownMessagesread-onlyRequired<MarkdownMessages>—The text in use: messages over the defaults.

Events

EventTypeDescriptiontp-markdown-renderCustomEvent<MarkdownRenderDetail>After each committed render; detail.headings.

Methods

MethodReturnsDescriptionappend(text)voidAdds text to the end of the source, as a stream of chunks does.

CSS parts

PartDescription::part(markdown)The flow root; size selects its typography and spacing scale.::part(markdown-heading)A heading; data-level is the rendered level.::part(markdown-paragraph)A paragraph.::part(markdown-list)An ordered or unordered list; data-task-list when every item is a task.::part(markdown-list-item)A list item; data-task for a task item.::part(markdown-blockquote)A quotation.::part(markdown-code)Inline code.::part(markdown-link)A link.::part(markdown-image)An image.::part(markdown-emphasis)Emphasis.::part(markdown-strong)Strong importance.::part(markdown-delete)Strikethrough.::part(markdown-footnotes)The footnotes section.::part(markdown-footnote-reference)A footnote reference.::part(markdown-footnote-back-reference)A link back to a footnote reference.