Skip to content

NKARDAZ/typography-rules

Repository files navigation

@nkardaz/typography-rules

A modular, locale-aware typography rules engine for transforming plain text into typographically correct output. Ships with a glyph registry, smart text functions, and a composable rule pipeline.

Used as a rules provider for typography plugins such as @nkardaz/typography-core / @nkardaz/remark-typography.


Installation

npm i @nkardaz/typography-rules

Requires Node.js ≥ 24.0.0


Family of @nkardaz typography packages

Package Type Details
Typography Rules Rules engine
Typography Core Core
Adapters
Remark Typography Remark adapter
Rehype Typography Rehype adapter
Obsidian Typography Obsidian adapter
Vanilla Typography HTML5 adapter
Plugins
Preview Typography Obsidian plugin

Package Exports

Export path Description
@nkardaz/typography-rules Main entry — rules, store, types, functions
@nkardaz/typography-rules/glyphs Glyph registries (DASHES, SPACES, PUNCTUATION, …)
@nkardaz/typography-rules/helpers Text pipeline helpers (protect/unprotect, node markers, pattern registry)
@nkardaz/typography-rules/functions Composable text-processing functions

Styles

Optional companion stylesheet with modifier classes consumed by markup rules that render structural elements. Currently covers <ruby>; more elements will gain styles here as markup rules expand.

import '@nkardaz/typography-rules/style';

Ruby (<ruby>)

Parental class: @nkardaz-typography-ruby

Class Effect
--alternate ruby-position: alternate
--over ruby-position: over
--under ruby-position: under
--center ruby-align: center
--start ruby-align: start
--space-between ruby-align: space-between
--space-around ruby-align: space-around

Apply these via the className tag setting on rubyText, or per-instance through its @class() header, e.g. [@class(--under --center):...].


Quick Start

Using default rules

import {
  initTypographyRules,
  getWeightedRules,
} from '@nkardaz/typography-rules';

// Register all built-in rule groups (common, ru, en, …)
initTypographyRules();

// Register all built-in rules for markup, e.g. [^text] → <sup>text</sup>
initMarkupRules();

// Or apply only a specific locale group
initTypographyRules('ru');

// Retrieve the merged, weight-sorted pipeline for a locale
const rules = getWeightedRules('ru'); // common + ru rules, sorted by weight

Defining custom rules

import { newRule, registerRule } from '@nkardaz/typography-rules';

// Replace rule — static string substitution
registerRule('en', newRule('/english/copyright', /\(c\)/gi, '©'));

// Transform rule — dynamic replacement per match
registerRule(
  'en',
  newRule('/english/bracket-numbers', /\d+/g, (match) => `[${match[0]}]`)
);

// Function rule — full custom processing function
import { smartQuotes } from '@nkardaz/typography-rules/functions';

// Danish quotes: »Jeg husker, at hun sagde ›det her er vigtigt‹ i går.«
registerRule(
  'da',
  newRule('/danish/typography/quotes', smartQuotes, [
    { outer: ['»', '«'], inner: ['›', '‹'] },
  ])
);

Registering multiple rules at once

import { newRule, registerRule } from '@nkardaz/typography-rules';
import { DASHES } from '@nkardaz/typography-rules/glyphs';

registerRule(
  'en',
  newRule('/english/em-dash', /--/g, DASHES.em),
  newRule('/english/registered', /\(r\)/gi, '®'),
  newRule('/english/trademark', /\(tm\)/gi, '™')
);

Core API

newRule(label, rule, second?, weight?)

Creates a typed typography rule object. Supports three overloads:

// 1. Replace rule
newRule('/my/rule/label', /--/g, '—');

// 2. Transform rule
newRule('/my/rule/label', /\d+/g, (match: RegExpExecArray) => `[${match[0]}]`);

// 3. Function rule
newRule('/my/rule/label', myFunction, ['arg1', 'arg2']);
Parameter Type Description
label string Unique rule identifier path, e.g. '/en/math/fractions'. Used by the blacklist system to enable/disable rules selectively
rule RegExp | RuleFunction Pattern or processing function
second string | transform fn | args[] Replacement, transformer, or arguments
weight number Execution priority — lower values run first (default: 0)

registerRule(locale, rules[])

Registers one or more rules for a locale. Automatically invalidates the weighted rule cache for that locale and 'common'.

registerRule('common', newRule('/common/space/cleanup', /\s+/g, ' '));
registerRule('de', newRule('/deutsch/em-dash', /--/g, '—'), newRule(/"/g, '„'));

rulesBase(locale, base, label?, excludes?, ...rules)

Registers rules for a locale, inheriting from an existing base locale. Useful for defining locale variants that share most rules with a parent.

rulesBase(
  'fr-CA', // target locale
  'fr', // inherit from French
  { expression: /^\/french\//, replacement: '/french-ca/' }, // rename labels
  ['/french/quotes/guillemets'], // exclude specific rules
  newRule('/french-ca/extra', //g, '...') // add custom rules on top
);
Parameter Type Description
locale string Target locale to register rules for
base string Source locale to inherit rules from
label LabelTransform? Optional { expression, replacement } to rewrite labels
excludes string[]? Rule labels from the base to skip
...rules Rule[] Additional rules appended after the inherited ones

initTypographyRules(from?)

Populates the global rule registry with the built-in default ruleset.

initTypographyRules(); // All locales
initTypographyRules('en'); // English rules only
initTypographyRules('ru'); // Russian rules only

initMarkupRules(from?)

Populates the global rule registry with built-in markup rules (superscript, subscript, chemical notation, ruby text).

initMarkupRules(); // All markup rule groups
initMarkupRules('common'); // Common markup rules only

getWeightedRules(locale)

Returns a merged, weight-sorted rule pipeline for the given locale: common rules + locale-specific rules, sorted ascending by weight.

const pipeline = getWeightedRules('en'); // Rule[]

resetTypographyRules()

Clears all registered rules from the global registry and cache.


rulesHas(locale) / rulesCount(locale)

Utility functions for inspecting the rule registry:

rulesHas('en'); // boolean
rulesCount('en'); // number

Rule Blacklist

A trie-based system for selectively disabling rules by their label path without removing them from the registry. Supports hierarchical matching — disabling a path prefix disables all rules nested under it.

import {
  disableRule,
  enableRule,
  toggleRule,
  isRuleDisabled,
  isGloballyDisabled,
  clearBlacklist,
} from '@nkardaz/typography-rules';

disableRule(rule)

Disables a rule or an entire rule subtree by path prefix. The special value '*' disables all rules globally.

disableRule('/common/math/negative-number'); // disable one rule
disableRule('/english/ligatures'); // disable all ligature rules
disableRule('*'); // disable everything

enableRule(rule)

Re-enables a previously disabled rule. Clears the global flag if '*' is passed.

enableRule('/english/ligatures/fi'); // re-enable a single rule
enableRule('*'); // lift global disable

toggleRule(rule)

Flips the disabled state of a rule — disables if enabled, enables if disabled.

toggleRule('/common/typography/runt');

isRuleDisabled(rule)

Returns true if the rule is disabled either directly, via a parent prefix, or globally.

isRuleDisabled('/common/math/negative-number'); // boolean

isGloballyDisabled()

Returns true if all rules have been globally disabled via disableRule('*').

isGloballyDisabled(); // boolean

clearBlacklist()

Resets the entire blacklist — removes all disabled paths and clears the global flag.

clearBlacklist();

Label path conventions

Built-in rule labels follow a consistent hierarchy:

Segment Example Meaning
common /common/math/… Applies to all locales
english /english/ligatures/… English-only rules
russian /russian/typography/… Russian-only rules
Second segment /common/space/…, /common/typography/… Rule category
Third segment /common/math/negative-number Specific case
Fourth segment /common/punctuation/dashes/em-dash Specific rule

Built-in Functions

These are composable text-processing functions that can be used directly or wrapped with newRule.

Must be imported with @nkardaz/typography-rules/functions

smartQuotes(text, settings?)

Converts straight quotes (" and ') into typographically correct opening/closing quote pairs, with support for nested quotation levels and apostrophe detection.

import { smartQuotes } from '@nkardaz/typography-rules/functions';

smartQuotes('"Hello"'); // “Hello” (en defaults)
smartQuotes('"He said \'hi\'"'); // “He said ‘hi’”

Settings:

Option Type Default Description
outer [string, string] [“, ”] (English) Opening and closing outer quotes
inner [string, string] [‘, ’] (English) Opening and closing inner quotes

smartNumberGrouping(text, settings?)

Inserts symbols (e.g. ,) as thousands separators into large numeric sequences based on locale (uses Intl.NumberFormat).

import { smartNumberGrouping } from '@nkardaz/typography-rules/functions';

smartNumberGrouping('Price: 1234567');
// “Price: 1,234,567”

smartNumberGrouping('Value: 1234567.891011', { locale: 'ru-RU' });
// “Value: 1 234 567,891011”

Settings:

Option Type Default Description
locale string 'en-US' BCP 47 locale tag used by Intl.NumberFormat
minLength number 5 Minimum integer digit count before grouping is applied

clearSpaces(text, settings?)

Collapses runs of two or more identical space characters into a single one. By default targets non-breaking, hair, and thin spaces.

import { clearSpaces } from '@nkardaz/typography-rules/functions';

clearSpaces('a  b  c'); // 'a b c'

Settings:

Option Type Default Description
spaces Spaces[] | string[] [noBreak, hair, thin] Space characters to deduplicate

runt(text, settings?)

Prevents typographic runts — single short words isolated at the end of a paragraph — by replacing the preceding space with a non-breaking space. For longer last words, also protects the penultimate word with white-space: nowrap.

Settings:

Option Type Default Description
threshold number 10 Maximum character length of the last word to trigger runt protection
space Spaces | string SPACES.noBreak Replacement space character
minLineLength number 150 Minimum text length required to apply runt protection at all

wrapWithTag(text, settings?, tagSettings?)

Wraps matched bracket-marker syntax into an HTML element node. Returns Node[].

import { wrapWithTag } from '@nkardaz/typography-rules/functions';

wrapWithTag('H[^2]O');
// → [text('H'), sup([text('2')]), text('O')]

wrapWithTag('H[*2]O', { marker: '*', tag: 'sup' });
// → [text('H'), sup([text('2')]), text('O')]

Settings:

Option Type Default Description
marker string '^' Character after opening bracket
tag string 'sup' HTML tag name for the wrapping element
wrapper [string, string] ['[', ']'] Bracket pair delimiting the marked range

Tag settings:

Option Type Description
className string CSS class on the element
attrs Record<string, string> Additional HTML attributes

rubyText(text, settings?, tagSettings?)

Parses ruby annotation syntax into a <ruby> node tree with <rb> / <rt> pairs. Returns Node[].

import { rubyText } from '@nkardaz/typography-rules/functions';

rubyText('[:平安時代][:へいあんじだい]');
// → ruby → [ rb('平安時代'), rt('へいあんじだい') ]

// Multiple base|furigana pairs separated by |
rubyText('[:東|京][:とう|きょう]');
// → ruby → [ rb('東'), rt('ひがし'), rb('京'), rt('きょう') ]

// Optional @class()/@style() header — placed between the wrapper-open
// character and the marker. Base bracket targets <ruby>, furigana bracket
// targets <rt>. @class and @style may appear in any order; either, both,
// or neither may be present.
rubyText(
  '[@class(--center):высшая сила][@class(&__large-font) @style(font-weight: 800):Астарот]',
  { marker: ':' },
  { className: '@nkardaz-typography-ruby' }
);
// → <ruby class="@nkardaz-typography-ruby --center">
//     <rb>высшая сила</rb>
//     <rt class="@nkardaz-typography-ruby__large-font" style="font-weight: 800">Астарот</rt>
//   </ruby>

Header syntax:

  • A class name is used as-is — write modifier classes (--under, --center, …) exactly as they appear in the stylesheet.
  • A class name prefixed with & is concatenated onto the first class of tagSettings.className (e.g. &__large-font@nkardaz-typography-ruby__large-font).
  • @class/@style on the base (first) bracket apply to the <ruby> element; on the furigana (second) bracket they apply to that pair's <rt> element.

Settings:

Option Type Default Description
marker string ':' Character after opening bracket associated with the ruby
wrapper [string, string] ['[', ']'] Bracket pair delimiting the ruby group

Tag settings: same as wrapWithTag — applies to the <ruby> element and is merged with any per-instance @class()/@style() header from the base bracket.


chemNotation(text, settings?, tagSettings?)

Parses chemical notation syntax into MathML <mmultiscripts> node trees for correct rendering of nuclear/chemical scripts on both sides of a base symbol. Returns Node[].

import { chemNotation } from '@nkardaz/typography-rules/functions';

chemNotation('Вода [%H(_2)-O]');
chemNotation('[%(^14)(_6)C]');

Syntax inside [%…]:

Notation Meaning
(_val) Subscript (lower index)
(^val) Superscript (upper index)
Before base Left-side prescripts (e.g. (^14)C)
After base Right-side scripts (e.g. C(_6))
- separator Joins multiple parts in one block (e.g. H(_2)-O)

Settings:

Option Type Default Description
marker string '%' Character after opening bracket
wrapper [string, string] ['[', ']'] Bracket pair delimiting the chemical notation block

Tag settings: same as wrapWithTag.

Examples:

[%H(_2)-O]            →  H₂O
[%NH(_4)-ClO(_4)]     →  NH₄ClO₄
[%(^239)U]            →  ²³⁹U
[%(^14)(_6)C]         →  ¹⁴₆C
[%(^2)(_1)H(^7)(_5)]  →  ²₁H⁷₅  (left: 2,1 — right: 7,5)

Note on font styling: MathML ignores font-family and font-style. Use a math-capable OpenType font (e.g. STIX Two Math, Latin Modern Math) via @font-face on the math element. To suppress automatic italicisation of <mi>, pass attrs: { mathvariant: 'normal' } via tagSettings.


Glyphs

The @nkardaz/typography-rules/glyphs export provides typed, prototype-enhanced glyph registries. All registries support shared utility methods.

Available registries

Export Description
DASHES Em dash, en dash, soft hyphen, figure dash, non-breaking hyphen, etc.
SPACES All Unicode space variants — non-breaking, thin, hair, narrow, zero-width, etc.
PUNCTUATION Multi-locale quote characters, ellipsis, interrobang, and punctuation marks
MATHS Minus sign (), fraction slash ()
LIGATURES Typographic ligatures: fi, fl, ffi, ffl, Æ, Œ, etc.
CHARACTERS Dagger, double dagger, numero (), section sign (§), etc.
TEMPERATURES Temperature unit symbols: ℃, ℉, K and text forms
WALLET Currency symbols and ISO 4217 codes
DIGITS ASCII digits and Unicode Roman numeral characters
RANGES Character range strings for use in RegExp character classes

GlyphSet utility methods

All glyph sets expose the following methods:

DASHES.join(); // '—|–|⸺|…' — joined string of all values
DASHES.join(''); // '—–⸺…'
DASHES.hasKey('em'); // true
DASHES.hasValue('—'); // true
DASHES.findKey('—'); // 'em'
DASHES.find('em', 'en'); // ['—', '–']
DASHES.insert({ myDash: '\u2E1A' }); // mutably extend the set

PUNCTUATION locale access

import { PUNCTUATION } from '@nkardaz/typography-rules/glyphs';

PUNCTUATION.get('ru', 'leftSided'); // common + ru leftSided merged
PUNCTUATION.get('en', 'rightSided'); // common + en rightSided merged
PUNCTUATION.getList(); // ['common', 'ru', 'en', 'fr', 'is']
PUNCTUATION.hasKey('de'); // false

Supported locales in PUNCTUATION:

Locale Outer quotes Inner quotes
ru «…» „…“
en “…” ‘…’
fr «…» ‹…›
is „…“ ‚…‘

Aliases

The @nkardaz/typography-rules export provides an ALIAS utility for mapping various locale identifiers to a single root key. All keys and values are automatically normalized to lowercase, and lookups are case-insensitive.

createAlias(map)

Creates a normalized alias map with utility methods.

import { createAlias } from '@nkardaz/typography-rules';

const ALIAS = createAlias({
  en: ['en-US', 'English'],
  ru: ['ru-RU', 'Russian'],
});
Method Description
has(alias) Checks if an alias exists as a root key or an alternative name.
resolve(alias) Resolves an alias to its root key.
push(root, ...aliases) Adds new alternative names to an existing or new root key.
normalize(...alias) Helper to lowercase one or more strings.

Global ALIAS

A pre-configured instance used internally for supported locales:

import { ALIAS } from '@nkardaz/typography-rules';

ALIAS.ru; // ['ru-ru', 'russian', 'русский']
ALIAS.resolve('Russian'); // 'ru'
ALIAS.has('Old English'); // true

Helpers

The @nkardaz/typography-rules/helpers export provides utilities for safe text pipeline construction.

Protection system

Temporarily wraps structured content (URLs, emails, code, identifiers) in protection markers before typography transformations, then restores originals afterward.

import { protect, unprotect } from '@nkardaz/typography-rules/helpers';

const [protectedText, captured] = protect(text);
// ... apply typography rules to `protectedText` ...
const processed = applyRules(protectedText); // your pipeline here
const result = unprotect(processed, captured);

Protected patterns (not modified by typography rules):

  • Email addresses, URLs
  • Unix and Windows file paths
  • XML/HTML tags
  • Inline and block code (backtick syntax)
  • UUIDs, git hashes
  • IPv4, IPv6, MAC addresses
  • Version strings (v1.2.3, etc.)
  • CSS selectors, CLI flags (--option)
  • ISBN, ISSN, DOI, ORCID identifiers
  • [##(...)##] — Special protected block for protect any text inside ().

Pattern registry

import { createPatterns, PROTECTED_PATTERNS } from '@nkardaz/typography-rules/helpers';

const PATTERNS = createPatterns({
  email: /[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+/g,
  url: /https?:\/\/[^\s]+/g,
});

PATTERNS.email;            // fresh RegExp instance (lastIndex = 0) on every access
[...PATTERNS];             // [RegExp, RegExp]
PATTERNS.combined();       // single alternation RegExp
PATTERNS.insert({ ... });  // extend with new patterns

Node markers

Used to join/split text nodes across boundaries during multi-node processing:

import {
  joinNodes,
  splitNodes,
  NODE_MARKER,
} from '@nkardaz/typography-rules/helpers';

const joined = joinNodes(nodes); // 'text1\uE000\uEDFD\uF43Etext2'
// ... apply rules to `joined` ...
splitNodes(processed, nodes); // writes segments back to nodes

Default Rules Reference

Common (applied to all locales)

Expressions

Shared named expression patterns used across common rules (typography/expressions/common.ts):

Name Pattern description
plusMinus + followed by - or
minusPlus - or followed by +
sectionNumeral Section sign § followed by numeral(s)
percentValue Number followed by %, , or
numeralsRange Two digit sequences separated by - (e.g. 1-2)
ellipsisRange Number, then or , then number (e.g. −2–3)
multipleEllipsis Two or more consecutive
walletSymbolBeforeValue Currency symbol followed by digits (e.g. $100)
walletSymbolAfterValue Digits followed by currency symbol (e.g. 100$)
walletISOBeforeValue ISO currency code followed by digits (e.g. USD 100)
walletISOAfterValue Digits followed by ISO currency code (e.g. 100 USD)
expressiveAposiopesis Expressive punctuation (!, ?, , etc.) followed by dots or ellipsis
backwardsExpressiveAposiopesis Dots or ellipsis followed by expressive punctuation
temperature Digit followed by a temperature unit symbol (℃, ℉, K, etc.)

Rules

Label Pattern / Trigger Replacement Description
/common/space/cleanup/multiple Multiple identical special spaces Single space Collapses duplicate non-breaking, hair, and thin spaces via clearSpaces
/common/space/cleanup/trim Leading / trailing whitespace (removed) Trims surrounding whitespace from the processed text
/common/number/negative Hyphen-minus before digit (-123) −123 Replaces ASCII hyphen-minus with Unicode minus sign
/common/number/range/en-dash Digit range with hyphen (1-2) 1–2 Converts hyphen between two digit sequences into an en dash
/common/number/range/ellipsis-on-negative Range with en dash or minus −2…3 Converts numeric ranges using en dash or minus into ellipsis notation
/common/number/dimension NxN or NхN (latin/cyrillic x) N×N Replaces dimension separator with multiplication sign ×
/common/number/multiply N*N N×N Replaces asterisk between numbers with multiplication sign ×
/common/number/fraction N/N N⁄N 16⁄9 1000⁄7 Replaces slash with fraction slash
/common/symbol/copyright (c) or (с) (latin/cyrillic) © Copyright symbol substitution
/common/symbol/trademark (tm) or (тм) Trademark symbol substitution
/common/symbol/registered (r) ® Registered trademark symbol substitution
/common/symbol/section (s) § Section sign substitution
/common/symbol/math/plus-minus +- or +− ± Plus-minus sign substitution
/common/symbol/math/minus-plus -+ or −+ Minus-plus sign substitution
/common/punctuation/dashes/em-dash Double hyphen -- Replaces double hyphen-minus with a typographic em dash
/common/punctuation/dots/overload Four or more consecutive dots .... ... Normalizes over-long dot sequences before ellipsis conversion
/common/punctuation/dots/ellipsis Three dots ... Converts ASCII triple-dot into the Unicode ellipsis character
/common/punctuation/dots/ellipsis-overload Two or more consecutive Deduplicates repeated ellipsis characters
/common/punctuation/apostrophe Straight apostrophe ' Replaces with Unicode right single quotation mark , weight 200
/common/symbol/section/value § followed by numeral(s) § <numeral> Adds narrow non-breaking space between section sign and numeral, weight 1
/common/typography/runt Short last word(s) in a paragraph Preceding space →   Prevents typographic runts. Weight: Infinity — always runs last

Markup Rules

Label Pattern / Trigger Replacement Description
/common/wraps/chem [%…] marker syntax <math> node tree Parses chemical notation into MathML <mmultiscripts> via chemNotation
/common/wraps/ruby [:base|…][:annotation|…] syntax, with optional @class()/@style() header <ruby> node tree Ruby annotation, styled via --over/--under/--alternate/--center/--start/--space-between/--space-around classes or custom styles, via rubyText
/common/wraps/sup [^…] marker syntax <sup> node Wraps bracket-marker content in a superscript element via wrapWithTag
/common/wraps/sub [_…] marker syntax <sub> node Wraps bracket-marker content in a subscript element via wrapWithTag

Note on markup rule order: rules are registered with weight Infinity and applied in the order shown — chem first, then ruby, then sup/sub last.


Russian (ru)

Expressions

Russian-specific named expression patterns (typography/expressions/ru.ts), extending common expressions:

Name Pattern description
numeroNumeral Numero sign followed by numeral(s)
invalidPunctuationSpacing Space after left punctuation or before right punctuation (locale-aware)
dialogEmDash Em dash at the start of a line (dialogue opener)
attributionEmDash Left punctuation, then , then a word (attribution pattern)
subjectPredicateEmDash Word word (subject–predicate dash pattern)
siUnitMul, siUnitDiv SI unit multiplication / division expressions (Cyrillic prefixes)
siUnitBase Digit followed by a Russian SI unit
siUnitPowAfterNum Digit, SI unit, then exponent digit
siUnitPow SI unit followed by exponent digit (not preceded by digit)
date Numeral followed by a Russian date abbreviation (в, г, мес, нед, дн, д, etc.)

Rules

Label Pattern / Trigger Replacement Description
/russian/currency/wallet/symbol-flip Currency symbol before value ($100) 100$ Moves currency symbol after the value (Russian convention)
/russian/currency/wallet/iso-flip ISO code before value (USD 100) 100 USD Moves ISO code after the value
/russian/currency/wallet/symbol-value Value then currency symbol (100$) 100 $ Adds non-breaking space between value and currency symbol
/russian/currency/wallet/iso-value Value then ISO code (100 USD) 100 USD Adds non-breaking space between value and ISO code
/russian/currency/rub-to-symbol рублей, руб., р. after digits N ₽ Replaces Russian rouble word forms with symbol
/russian/currency/eur-to-symbol евро after digits N € Replaces euro word with symbol
/russian/currency/usd-to-symbol долларов, дол. after digits N $ Replaces dollar word forms with $ symbol
/russian/number/groups Large numbers (5+ digits) 1 234 567 Digit grouping via smartNumberGrouping with locale: 'ru-RU'
/russian/number/normalize/dot->comma N.N decimal dot N,N Converts decimal dot to comma (Russian numeric standard)
/russian/metric/si-unit/base Digit followed by SI unit N Unit Narrow non-breaking space between value and unit
/russian/metric/si-unit/n*n-n SI unit multiplication (м*с) м·с Н·м/с Replaces * between SI units with middle dot ·
/russian/metric/si-unit/n-n*n SI unit division м·с Дж/Кл·с Same as above for division form
/russian/metric/si-unit/pow-after-value N Unit<exp> N Unit<sup>exp</sup> Superscript exponent with narrow non-breaking space, weight -1
/russian/metric/si-unit/pow Unit<exp> (no preceding number) Unit<sup>exp</sup>
м³/(кг·с²)
Superscript exponent, weight -1
/russian/scientific/temperature/value N ℃ / N ℉ etc. N ℃ Non-breaking space between temperature value and unit
°C °F K °D °L °N °W °Da °H °R °Ré °Rø
/russian/symbol/percent-like/value N% / N‰ / N‱ N<NBSP>% Non-breaking space between value and percent-like symbol
/russian/symbol/numero/value followed by numeral(s) № <numeral> Narrow non-breaking space between numero sign and numeral
/russian/number/division N / N N ÷ N Replaces slash between numbers with obelus ÷
/russian/number/division-times N /* N N ⋇ N Division-times operator substitution
/russian/punctuation/dashes/dialog-em-dash at line start —<NBSP> Non-breaking space after dialogue em dash
/russian/punctuation/dashes/attribution-em-dash Right punctuation, <SP>—<SP>, word Right punctuation, <NBSP>—<NBSP>, word Non-breaking spaces around attribution dash
/russian/punctuation/dashes/subject-predicate-em-dash word, <SP>—<SP>, word word, <NBSP>—<SP>, word Non-breaking spaces around subject–predicate dash
/russian/punctuation/quotes Straight quotes "…" «…» / „…“ Russian typographic quotes via smartQuotes, weight 100
/russian/punctuation/dot-after-quote ». Moves period outside closing guillemet, weight 1000
/russian/punctuation/dot-after-expression Expressive punctuation near dots
!... !… etc.
Normalized form
!.. ?.. ‽..
Normalizes aposiopesis punctuation patterns
/russian/punctuation/invalid-spacing Space after « or before » (removed) Removes invalid spaces around guillemets, weight 1000
/russian/compositions/initials Б. Ю. Александров etc. Б. Ю. Александров
Thin-space separated
Replaces regular spaces between initials and name with thin spaces
/russian/text/conjunctions Short particles: бы, же, ли etc. <NBSP>particle Prevents particles from being orphaned at line start
/russian/text/conjunctions Prepositions: за, из, на, не etc. preposition<NBSP> Prevents prepositions from being left alone at line end
/russian/text/adress мкр-н, дом, д., ул. etc. With <NBSP> Prevents address abbreviations from splitting across lines
/russian/text/common-shorts коп., см., рис. etc. With <NBSP> Prevents common abbreviations from splitting
/russian/text/organizations АО, ООО, ПАО, НИИ etc. ООО<NBSP> Non-breaking space after legal entity abbreviations
/russian/text/dates N в. / N г. / N мес. etc. N<NBSP>в. Non-breaking space between numeral and date abbreviation
/russian/text/millions N тыс. / N млн. / N млрд. N<NBSP>тыс. Non-breaking space before large-number abbreviations
/russian/text/no-break-hyphen кто-то, кое-что, ну-ка, всё-таки etc. With non-breaking hyphen Replaces hyphens in fixed compound words/particles with non-breaking hyphen
/russian/text/orphan-letters Single Cyrillic letter followed by space letter<NBSP> Prevents single-letter words from being orphaned at line end

Markup Rules

No locale-specific markup rules for ru currently.


English (en)

Expressions

English-specific named expression patterns (typography/expressions/en.ts), extending common expressions:

Name Pattern description
numberNumeral Number sign # followed by digits (e.g. #42)
invalidPunctuationSpacing Space after left punctuation or before right punctuation (en-locale)
siUnitMul, siUnitDiv SI unit multiplication / division expressions
siUnitBase Digit followed by an SI unit
siUnitPowAfterNum Digit, SI unit, then exponent digit
siUnitPow SI unit followed by exponent digit (not preceded by digit)

Rules

Label Pattern / Trigger Replacement Description
/english/currency/wallet/symbol-flip Value then currency symbol (100$) $100 Moves currency symbol before the value (English convention)
/english/currency/wallet/iso-flip ISO code before value (USD 100) 100 USD Moves ISO code after the value
/english/currency/wallet/symbol-value Currency symbol before value ($100) $100 Ensures no extra space between symbol and value
/english/currency/wallet/iso-value Value then ISO code (100 USD) 100 USD Adds non-breaking space between value and ISO code
/english/number/groups Large numbers (5+ digits) 1,234,567 Digit grouping via smartNumberGrouping with locale: 'en-US'
/english/metric/si-unit/base Digit followed by SI unit N Unit Narrow non-breaking space between value and unit
/english/metric/si-unit/n*n-n SI unit multiplication (m*s) m·s N·m/s Replaces * between SI units with middle dot ·
/english/metric/si-unit/n-n*n SI unit division m·s J/C·s Same as above for division form
/english/metric/si-unit/pow-after-value N Unit<exp> N Unit<sup>exp</sup> Superscript exponent with narrow non-breaking space, weight -1
/english/metric/si-unit/pow Unit<exp> (no preceding number) Unit<sup>exp</sup> Superscript exponent, weight -1
/english/scientific/temperature/value N ℃ / N ℉ etc. N℃ Removes space between temperature value and unit
/english/symbol/percent-like/value N% / N‰ / N‱ N% Normalizes space between value and percent-like symbol
/english/symbol/hash/value # followed by digits (#42) #42 Normalizes space between number sign and numeral
/english/number/division N/N N÷N Replaces slash between numbers with obelus ÷
/english/number/division-times N/*N Division-times form Division-times operator substitution
/english/punctuation/quotes Straight quotes "…" / '…' “…” / ‘…’ US typographic quotes via smartQuotes, weight 100
/english/punctuation/dot-before-expression Expressive punctuation near dots Normalized form Normalizes aposiopesis punctuation patterns
/english/punctuation/invalid-spacing Space after or before etc. (removed) Removes invalid spaces around punctuation, weight 1000
/english/ligatures/fi fi Typographic fi ligature
/english/ligatures/fl fl Typographic fl ligature
/english/ligatures/ffi ffi Typographic ffi ligature
/english/ligatures/ffl ffl Typographic ffl ligature

Markup Rules

No locale-specific markup rules for en currently.


Old English / Ænglisċ (ang)

Rules

Label Pattern / Trigger Replacement Description
/ænglisċ/articles/þe The / the / Þe / þe Þͤ / þͤ Replaces modern “the” with Old English thorn + combining letter e (ͤ)

Rule Weights

Rules are applied in ascending weight order. Rules with equal weight preserve their registration order (stable sort).

Weight Meaning
-Infinity Always first, which must run before all text transforms
0 (default) Standard priority
< 0 Applied before standard rules
> 0 Applied after standard rules
100 Early-stage — e.g. quote normalization before apostrophe processing
200 Late-stage — e.g. apostrophe normalization after quote processing
Infinity Always last — e.g. runt, which must run after all text transforms

Node utilities

import {
  htmlNode,
  renderNode,
  renderNodes,
  nodeToMdast,
} from '@nkardaz/typography-rules';
Function Signature Description
htmlNode (text, settings?) => Node[] Splits text into a mixed array of text and element nodes via RegExp
renderNode (node: Node) => string Serializes a single Node to an HTML string
renderNodes (nodes: Node[]) => string Serializes an array of Node to an HTML string
nodeToMdast (node: Node) => Text | MdxJsxTextElement Converts an internal Node to an mdast-compatible AST node

TypeScript

The package is fully typed. Key exported types:

import type {
  Rule,
  RegExpReplaceRule,
  RegExpTransformRule,
  FunctionRule,
  RuleFunction,
  Node,
  TextNode,
  ElementNode,
  QuoteSettings,
  NumberSpaceSettings,
  ClearSpacesSettings,
  RuntSettings,
  HtmlNodeSettings,
  WrapWithTagsSettings,
  RubyTextSettings,
  ChemNotationSettings,
  TagSettings,
} from '@nkardaz/typography-rules';