Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/.vitepress/carve-lib/heading-ids.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { Document, InlineNode } from './ast.js';
/** The automatic-identifier rule. Pure, context-free, no dedup. */
export declare function slugify(plainText: string): string;
/**
* The automatic-identifier rule. Pure, context-free, no dedup.
*
* Default follows jgm/djot#393 (case + non-ASCII preserved). With `asciiFold`
* (opt-in via the `asciiHeadingIds` parse option) the slug is transliterated to
* ASCII for URL/CSS-fragment portability and re-slugged.
*/
export declare function slugify(plainText: string, asciiFold?: boolean): string;
/**
* Visible plain text of an inline run (markup stripped).
*
Expand All @@ -18,5 +24,5 @@ export declare function inlineText(nodes: InlineNode[]): string;
* crossrefs (first-occurrence target, link text cloned from the target
* heading; unresolved -> literal text). Mutates and returns `doc`.
*/
export declare function resolveHeadingIds(doc: Document): Document;
export declare function resolveHeadingIds(doc: Document, asciiFold?: boolean): Document;
//# sourceMappingURL=heading-ids.d.ts.map
2 changes: 1 addition & 1 deletion docs/.vitepress/carve-lib/heading-ids.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 24 additions & 14 deletions docs/.vitepress/carve-lib/heading-ids.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/.vitepress/carve-lib/heading-ids.js.map

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion docs/.vitepress/carve-lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export declare function renderHtml(ast: Document, opts?: RenderOptions): string;
* (no explicit `[label]: url` def and no matching heading) to its
* literal source text.
*/
export declare function resolve(doc: Document): Document;
export declare function resolve(doc: Document, opts?: {
asciiHeadingIds?: boolean;
}): Document;
/** Convenience: parse + resolve + render in one call. */
export declare function carveToHtml(source: string, opts?: ParseOptions & RenderOptions): string;
//# sourceMappingURL=index.d.ts.map
2 changes: 1 addition & 1 deletion docs/.vitepress/carve-lib/index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions docs/.vitepress/carve-lib/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/.vitepress/carve-lib/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/.vitepress/carve-lib/parse.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export interface ParseOptions {
positions?: boolean;
/** Format label applied to a bare `---` frontmatter fence. Default 'yaml'. */
defaultFrontmatterFormat?: string;
/**
* Fold auto-generated heading ids to ASCII (Über -> Uber) for URL/CSS-fragment
* portability. Default false: ids preserve case and non-ASCII per jgm/djot#393.
*/
asciiHeadingIds?: boolean;
}
export declare function parse(source: string, opts?: ParseOptions): Document;
/**
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/carve-lib/parse.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/.vitepress/carve-lib/parse.js.map

Large diffs are not rendered by default.

100 changes: 44 additions & 56 deletions docs/case-study/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,65 +91,53 @@ Every heading has an identifier. If the heading carries an explicit
(no normalization). Otherwise the identifier is generated from the
heading text by the following algorithm, applied in order:

1. Take the heading's rendered plain text (inline markup removed:
`# *Setup* guide` yields `Setup guide`).
2. NFC-normalize, then **transliterate non-ASCII to ASCII** via a baked
Unicode→ASCII map covering Latin / IPA / combining marks / Cyrillic /
Latin-Extended-Additional / punctuation / super- and sub-script /
currency / letterlike ranges. Code points not in the map — Greek
(context-sensitive in ICU, deliberately excluded), CJK, Arabic — pass
through unchanged.
3. Lowercase it (Unicode-aware; only matters for any letters that
survived step 2 unmapped).
4. Trim leading and trailing whitespace.
5. Delete every CSS-unsafe punctuation character: `'` `"` `;` `:`
(so `What's New` becomes `whats-new`, not `what-s-new`).
6. Replace each maximal run of characters that are not Unicode letters,
Unicode digits, `_`, or `-` — spaces included — with a single `-`.
7. Unicode letters, Unicode digits, `_` and `-` are preserved. `_` and
`-` are valid CSS identifier characters and meaningful in technical
headings, so they are kept rather than folded into `-`.
8. Collapse runs of `-`, then trim leading and trailing `-`.
9. If the result starts with a digit, prefix `section-`
(`2024 Recap` becomes `section-2024-recap`). A CSS identifier may not
start with a digit, and generated IDs must be usable as CSS selectors
and `querySelector` arguments, not only as URL fragments.
10. If the result is empty, the identifier is `section`.
11. Deduplicate against the document's identifier namespace. Explicit
`{#id}` values are reserved first, in document order; generated IDs
are reserved as headings are processed. The first use of an
identifier is kept bare; each later collision takes the next numeric
suffix, 1-based (the second is `-2`, the third `-3`). Explicit and
generated identifiers share one namespace.
1. Take the heading's rendered plain text (inline markup removed; symbols
`:name:` and footnote references excluded): `# *Setup* guide` yields
`Setup guide`.
2. NFC-normalize.
3. Replace each maximal run of **non-alphanumeric ASCII** characters
(spaces, punctuation, `_`, and runs of `-`) with a single `-`.
4. Trim leading and trailing `-`.
5. **Letter case and all non-ASCII characters are preserved** — no
lowercasing, no transliteration. `Über café` → `Über-café`, `日本語`
stays `日本語`.
6. If the result starts with a digit, prefix `s-` (a bare leading digit
is a valid HTML id but an invalid CSS selector). If the result is
empty, the identifier is `s`.
7. Deduplicate against the document's identifier namespace. Explicit
`{#id}` values are reserved first, in document order; generated IDs
are reserved as headings are processed. The first use is kept bare;
each later collision takes the next numeric suffix (`-2`, `-3`).
Matching is case-sensitive, so `intro` and `Intro` do not collide.

| Heading | Identifier |
|---|---|
| `# Getting Started` | `getting-started` |
| `# Café & Crème` | `cafe-creme` |
| `# Über uns` | `uber-uns` |
| `# Привет мир` | `privet-mir` |
| `# RFC 2119: Key Words` | `rfc-2119-key-words` |
| `# 2024 Recap` | `section-2024-recap` |
| `# What's New?` | `whats-new` |
| `# user_id field` | `user_id-field` |
| `# 日本語の見出し` | `日本語の見出し` (unmapped script — pass-through) |
| `# Καλημέρα` | `καλημέρα` (Greek pass-through, deliberately) |
| `# !!!` | `section` |
| `# Setup` then `# Setup` | `setup`, then `setup-2` |
| `# Introduction {#intro}` then `# Intro` | `intro`, then `intro-2` |

Identifiers are lowercased, ASCII-safe, and CSS-selector-safe by design.
The rendered `id` is consumed by code Carve does not control: anchor
highlighting, `:target` rules, `document.querySelector('#' + id)`, and
crucially **URL-fragment autolinkers in chat / email / aggregators that
routinely truncate or mis-encode non-ASCII fragments**. The ASCII step
makes the slug survive being shared as `https://…/page#some-id`. A
predictable lowercase ASCII slug is also what an author must be able to
guess when writing a `</#id>` cross-reference.

For scripts the baked map does not cover (CJK, Arabic, Greek), the slug
keeps the Unicode letters. If a share-safe slug is required for those
headings, attach an explicit `{#share-safe-id}`.
| `# Getting Started` | `Getting-Started` |
| `# Café & Crème` | `Café-Crème` |
| `# Über uns` | `Über-uns` |
| `# Привет мир` | `Привет-мир` |
| `# RFC 2119: Key Words` | `RFC-2119-Key-Words` |
| `# 2024 Recap` | `s-2024-Recap` |
| `# What's New?` | `What’s-New` (the apostrophe smart-quotes to `’`, a non-ASCII char, then is preserved) |
| `# user_id field` | `user-id-field` |
| `# 日本語の見出し` | `日本語の見出し` |
| `# Καλημέρα` | `Καλημέρα` |
| `# !!!` | `s` |
| `# Setup` then `# Setup` | `Setup`, then `Setup-2` |
| `# Introduction {#intro}` then `# Intro` | `intro`, then `Intro` (case-sensitive, no collision) |

Identifiers **preserve case and non-ASCII characters**, matching the settled djot
spec ([jgm/djot#393](https://github.com/jgm/djot/pull/393)). The rendered `id` is
consumed by anchor highlighting, `:target` rules,
`document.querySelector('#' + id)`, and URL fragments. A leading digit gets the `s-`
prefix so the id is always a valid bare CSS selector.

Non-ASCII ids are valid HTML5 and resolve in browsers (the fragment is
percent-encoded when shared, e.g. `…/page#%C3%9Cber-uns`). For **ASCII-only**
anchors — no percent-encoding, friendlier to legacy autolinkers — implementations
offer an opt-in fold: carve-js's `asciiHeadingIds` parse option and carve-php's
`AsciiHeadingIdsExtension`, which transliterate the id (`Über uns` → `uber-uns`). It
is never the default; attach an explicit `{#id}` to pin any specific anchor.

### 4.2 Inline Formatting

Expand Down
Loading