@motolab/remark-flexi-ordered-list
remark-flexi-ordered-list
A remark plugin adding first-class support for ordered lists with a configurable per-depth marker scheme: uppercase/lowercase letters, decimals, uppercase/lowercase roman numerals, and parenthesized variants of each.
Overview
Markdown and the remark ecosystem lack the ability to define ordered lists in a flexible way.
CommonMark only supports the basic numbered ordered list (1., 2., 3.). That nests
natively, but the source can never express a standard technical-documentation nesting schema
like:
A. This
a. is
1. a nested
I. ordered
(A). list
(a). sjdkhbc
(1). sdioucahsodiu
(I). kjcdhsabc
This plugin makes exactly that parse, serialize, and render — with the nesting structure defined by plugin options.
Installation
npm install remark-flexi-ordered-list
Usage
import rehypeStringify from "rehype-stringify";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
import remarkFlexiOrderedList from "remark-flexi-ordered-list";
const html = await unified()
.use(remarkParse)
.use(remarkFlexiOrderedList, { levels: ["A", "a", "1", "I", "(A)", "(a)", "(1)"] })
.use(remarkRehype)
.use(rehypeStringify)
.process("A. first\n a. nested\nB. second\n");
Options
levels
An array of marker schemes, one per nesting depth, outermost first. Defaults to
["A", "a", "1", "I", "(A)", "(a)", "(1)"] (the example above). Depths beyond the end of the
array cycle back to the start.
The available schemes:
| Scheme | Markers | Scheme | Markers |
|---|---|---|---|
"A" |
A. B. … Z. AA. |
"(A)" |
(A). (B). … |
"a" |
a. b. … |
"(a)" |
(a). (b). … |
"1" |
1. 2. … |
"(1)" |
(1). (2). … |
"I" |
I. II. IV. … |
"(I)" |
(I). (II). … |
"i" |
i. ii. iv. … |
"(i)" |
(i). (ii). … |
A marker is always <marker><period><space>, e.g. A. or (iv). .
injectStyles
When true, any document containing parenthesized lists gets a <style> element appended to
its rendered output, so the (A). markers work with zero page setup — no stylesheet import
needed. Off by default. See HTML output for the trade-offs and the manual
alternatives.
Behavior
- Strict per depth — at each nesting depth only the configured scheme is recognized, which
disambiguates
I.(roman) fromI.(letter) and limits false positives in prose. Standard decimal lists (1.) always keep working everywhere via the core CommonMark construct. - Marker values are real —
C.starts a list at 3,IV.at 4,AA.at 27 (roman numerals are validated strictly, soIIII.is plain text). Like CommonMark's1., only a marker with value 1 can interrupt a paragraph. - Indentation works like standard remark ordered lists: child content aligns with the first
character after the marker, so a nested list under
A.is indented three columns and under(A).five. - Round-trip safe —
remark-stringifyserializes flexi lists back to their original markers and alignment.
HTML output
Plain schemes render as native <ol type> (with start when the first marker isn't 1):
<ol type="A">
<li>…</li>
</ol>
Parenthesized schemes have no native HTML equivalent, so they render with counter classes:
<ol class="fol-paren fol-upper-alpha" type="A">
<li>…</li>
</ol>
The (A). markers themselves are drawn with CSS counters, so the page that displays the HTML
must include the plugin's stylesheet once.
The expected CSS
This is the exact stylesheet the plugin ships — available both as the file
remark-flexi-ordered-list/styles.css and as the exported flexiOrderedListStyles string. It
hides the native list marker, then draws (<n>). in a ::before pseudo-element with a CSS
counter, adding one override per alphabet:
ol.fol-paren {
list-style: none;
counter-reset: fol;
}
ol.fol-paren > li {
counter-increment: fol;
}
ol.fol-paren > li::before {
content: "(" counter(fol) "). ";
}
ol.fol-paren.fol-upper-alpha > li::before {
content: "(" counter(fol, upper-alpha) "). ";
}
ol.fol-paren.fol-lower-alpha > li::before {
content: "(" counter(fol, lower-alpha) "). ";
}
ol.fol-paren.fol-upper-roman > li::before {
content: "(" counter(fol, upper-roman) "). ";
}
ol.fol-paren.fol-lower-roman > li::before {
content: "(" counter(fol, lower-roman) "). ";
}
The markup contract it targets: every parenthesized list carries fol-paren plus a
fol-<style> class naming its alphabet (fol-upper-alpha, fol-lower-alpha, fol-upper-roman,
fol-lower-roman; decimal needs no override), and a list starting past 1 gets an inline
style="counter-reset: fol <start − 1>". As long as those class names reach the browser any
equivalent CSS works — you are not tied to the shipped file, and you can restyle the marker
(font, color, spacing) by editing the ::before rules.
Including the stylesheet
Pick whichever fits your setup:
Zero setup — pass
injectStyles: trueto the plugin and every document containing parenthesized lists carries its own<style>element in the rendered output:.use(remarkFlexiOrderedList, { levels: [...], injectStyles: true })Nothing else to wire up. The trade-offs: the CSS is repeated in each rendered document, and HTML sanitizers (e.g.
rehype-sanitize) or strictstyle-srcCSPs may strip or block inline<style>elements — use one of the manual options below in those environments.Bundled app (Vite, webpack, Next.js, …) — import the shipped CSS file in your entry point or layout:
import "remark-flexi-ordered-list/styles.css";Astro — register the plugin in
astro.config.mjs, then import the stylesheet once from a shared layout so every Markdown/MDX page picks it up:// astro.config.mjs import { defineConfig } from "astro/config"; import remarkFlexiOrderedList from "remark-flexi-ordered-list"; export default defineConfig({ markdown: { remarkPlugins: [ [remarkFlexiOrderedList, { levels: ["A", "a", "1", "I", "(A)", "(a)", "(1)"] }], ], }, });--- // src/layouts/Layout.astro import "remark-flexi-ordered-list/styles.css"; --- <slot />A stylesheet
imported from a.astrofile is global (unlike a scoped<style>block), so one import in the layout your content pages use covers all of them. Prefer this overinjectStylesin Astro: the CSS is bundled and deduplicated once by Vite instead of repeated in every page's HTML. (If you render Markdown yourself through a manual unified pipeline rather than Astro's content layer, use theunified()usage shown at the top instead.)Generating full HTML documents — inject the CSS string into a
<style>element:import { flexiOrderedListStyles } from "remark-flexi-ordered-list"; const page = `<!doctype html><style>${flexiOrderedListStyles}</style>${html}`;Static site / no build step — copy
styles.css(or the string above) into your site's stylesheet.
Without the stylesheet, browsers fall back to their default decimal markers (1., 2., …)
for parenthesized lists. To soften that, the type attribute is emitted as a graceful
fallback, so an unstyled page still shows the right alphabet (A. instead of (A).); once the
stylesheet is present, its list-style: none takes over and the full parenthesized markers
appear.
API
The default export is the remark plugin. The underlying building blocks are exported too:
flexiOrderedList(options)— the micromark syntax extensionflexiOrderedListFromMarkdown()/flexiOrderedListToMarkdown()— the mdast extensionsflexiOrderedListHast— the tree transform that attacheshPropertiesparseMarker/formatMarker/defaultLevels— marker-scheme helpers