Skip to content

Commit 622b546

Browse files
authored
feat(markdown): add emphasis, blockquotes, breaks, autolinks and escapes (#84)
The inline renderer knew three constructs: **strong**, code spans and links. Everything else printed its own delimiters — *italic* rendered its asterisks, and a backslash escape rendered the backslash. Inline parsing now walks the source once against the mask that markdownSyntaxSource already produces, so a delimiter inside code, behind a backslash, or inside an autolink can never open a span, and spans nest by recursing on the same pass. Adds em, strong and both together in either delimiter, ~~del~~, autolinks, and CommonMark backslash escapes; underscore forms are refused mid-word so snake_case survives, and a delimiter that does not hug its content stays literal so 2 * 3 * 4 is arithmetic. Blocks gain > blockquotes, which re-enter the block renderer so a list or a heading inside a quote stays one, and --- thematic breaks, checked ahead of lists so - - - is a break. Two defects go with it: a closing hash sequence stayed in the heading label, and a void block element left inside a markdown block rendered as <p><hr/></p>, which no browser keeps nested. Costs 1.1 KB gzip, against roughly 11 KB for the smallest markdown engine that could replace this file.
1 parent 4c3d438 commit 622b546

10 files changed

Lines changed: 608 additions & 57 deletions

File tree

examples/component-tour.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,23 @@
241241

242242
###### Level six closes it out
243243

244+
## Inline markup
245+
246+
*Emphasis* and _its underscore form_ both italicize, **strong** and __its
247+
underscore form__ both bold, ***the two together*** nest, and ~~strikethrough~~
248+
marks a retraction. Identifiers like snake_case_name keep their underscores,
249+
`2 * 3 * 4` keeps its stars, and a \*backslash escape\* keeps the literal
250+
delimiters. Autolinks such as <https://github.com/wix-incubator/htmdx> and
251+
<team@example.com> resolve without a label.
252+
253+
> A blockquote holds its own Markdown: **strong text**, `code`, and lists.
254+
>
255+
> - The second paragraph of a quote keeps the same rule.
256+
257+
---
258+
259+
The rule above is a thematic break, written as `---`.
260+
244261
## Markdown and HTML images
245262

246263
![Mountain landscape](https://images.unsplash.com/photo-1535025183041-0991a977e25b?w=800 "Rendered from Markdown")

packages/htmdx/skill/authoring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ https://cdn.jsdelivr.net/npm/@wix/htmdx@4.10.1/dist/components.json
177177
- **Images** work as Markdown or as allowlisted `<img>`. Relative paths resolve
178178
from the artifact; `http:`, `https:`, and `data:image/*` sources are
179179
accepted. Give every image `alt` text.
180+
- **The Markdown is a subset.** Headings `#` to `######`, `*emphasis*`,
181+
`**strong**`, `~~strikethrough~~`, `` `code` ``, fenced blocks, links,
182+
`<https://autolinks>`, ordered and bulleted lists, `> blockquotes`, `---`
183+
breaks, and `\*` escapes all render. Tables belong in `DataTable`; task
184+
lists, footnotes, reference links, and setext headings do not render.
180185

181186
Optional frontmatter sets document metadata; unknown fields are ignored:
182187

packages/htmdx/src/components/shadcn/shared/theme.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,28 @@ htmdx-code {
167167
color: var(--primary);
168168
text-underline-offset: 2px;
169169
}
170+
.htmdx-doc-section-card em:not([data-slot]) {
171+
font-style: italic;
172+
}
173+
.htmdx-doc-section-card del:not([data-slot]) {
174+
text-decoration: line-through;
175+
color: var(--muted-foreground);
176+
}
177+
.htmdx-doc-section-card hr:not([data-slot]) {
178+
height: 1px;
179+
border: 0;
180+
margin: 1.5rem 0;
181+
background: var(--border);
182+
}
183+
.htmdx-doc-section-card blockquote:not([data-slot]) {
184+
margin: 1rem 0;
185+
padding: 0.125rem 0 0.125rem 1rem;
186+
border-left: 3px solid var(--primary);
187+
color: var(--muted-foreground);
188+
}
189+
.htmdx-doc-section-card blockquote > :last-child {
190+
margin-bottom: 0;
191+
}
170192
/* Tint from the artifact accent when the runtime theme is present, so inline
171193
code reads as an accent chip instead of gray-on-gray; a bare shadcn host
172194
falls back to its own primary. This rule wins over the runtime's own inline

packages/htmdx/src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,6 +1518,30 @@ const RUNTIME_CSS = `
15181518
font-weight: 700;
15191519
color: var(--md-sys-color-on-surface);
15201520
}
1521+
.htmdx-doc-section-card em:not([data-slot]) {
1522+
font-style: italic;
1523+
}
1524+
.htmdx-doc-section-card del:not([data-slot]) {
1525+
text-decoration: line-through;
1526+
color: var(--md-sys-color-on-surface-variant);
1527+
}
1528+
/* The rule reads as a rest between passages, so it carries the section's own
1529+
outline colour rather than a browser default border. */
1530+
.htmdx-doc-section-card hr:not([data-slot]) {
1531+
height: 1px;
1532+
border: 0;
1533+
margin: 24px 0;
1534+
background: var(--md-sys-color-outline-variant);
1535+
}
1536+
.htmdx-doc-section-card blockquote:not([data-slot]) {
1537+
margin: 16px 0;
1538+
padding: 2px 0 2px 16px;
1539+
border-left: 3px solid var(--md-sys-color-primary);
1540+
color: var(--md-sys-color-on-surface-variant);
1541+
}
1542+
.htmdx-doc-section-card blockquote > :last-child {
1543+
margin-bottom: 0;
1544+
}
15211545
/* A translucent accent tint rather than primary-container: ExecutiveSummary
15221546
already uses primary-container as its body, so an opaque chip would vanish
15231547
inside it. */

packages/htmdx/src/react/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,11 +1236,21 @@ function tokenize(
12361236
// element names inside a graphic never reach this loop.
12371237
const lower = rawName.toLowerCase();
12381238
const isSvgRoot = lower === 'svg';
1239-
if (
1240-
selfClosing ||
1241-
HTML_VOID_ELEMENTS.has(lower) ||
1242-
!(HTML_ELEMENTS.has(lower) || isSvgRoot)
1243-
) {
1239+
const closeless = selfClosing || HTML_VOID_ELEMENTS.has(lower);
1240+
// A block element with no close tag still owns its line: left inside the
1241+
// markdown block, an `<hr>` between two paragraphs would render as
1242+
// `<p><hr/></p>`, which no browser keeps nested.
1243+
if (closeless && HTML_BLOCK_ELEMENTS.has(lower) && opensLine(syntax, match.index)) {
1244+
pushMarkdown(blocks, source.slice(cursor, match.index), cursor);
1245+
blocks.push({
1246+
type: 'html',
1247+
value: source.slice(match.index, openTag.lastIndex),
1248+
offset: match.index,
1249+
});
1250+
cursor = openTag.lastIndex;
1251+
continue;
1252+
}
1253+
if (closeless || !(HTML_ELEMENTS.has(lower) || isSvgRoot)) {
12441254
continue;
12451255
}
12461256

0 commit comments

Comments
 (0)