npm.io
0.0.3 • Published 3d ago

@motolab/remark-flexi-ordered-list

Licence
MIT
Version
0.0.3
Deps
12
Size
66 kB
Vulns
0
Weekly
0

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) from I. (letter) and limits false positives in prose. Standard decimal lists (1.) always keep working everywhere via the core CommonMark construct.
  • Marker values are realC. starts a list at 3, IV. at 4, AA. at 27 (roman numerals are validated strictly, so IIII. is plain text). Like CommonMark's 1., 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 saferemark-stringify serializes 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: true to 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 strict style-src CSPs 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 .astro file is global (unlike a scoped <style> block), so one import in the layout your content pages use covers all of them. Prefer this over injectStyles in 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 the unified() 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 extension
  • flexiOrderedListFromMarkdown() / flexiOrderedListToMarkdown() — the mdast extensions
  • flexiOrderedListHast — the tree transform that attaches hProperties
  • parseMarker / formatMarker / defaultLevels — marker-scheme helpers

Keywords