Skip to content

Instantly share code, notes, and snippets.

@stefanv
Created March 20, 2026 23:02
Show Gist options
  • Select an option

  • Save stefanv/00cdc4e815b06668d55cce2ee60fef9c to your computer and use it in GitHub Desktop.

Select an option

Save stefanv/00cdc4e815b06668d55cce2ee60fef9c to your computer and use it in GitHub Desktop.
si2 role for myst
const UNITS = {
// SI base units
ampere: 'A',
candela: 'cd',
kelvin: 'K',
kilogram: 'kg',
metre: 'm',
meter: 'm',
mole: 'mol',
second: 's',
// SI derived units
becquerel: 'Bq',
degreeCelsius: '°C',
coulomb: 'C',
farad: 'F',
gray: 'Gy',
hertz: 'Hz',
henry: 'H',
joule: 'J',
lumen: 'lm',
katal: 'kat',
lux: 'lx',
newton: 'N',
ohm: 'Ω',
pascal: 'Pa',
radian: 'rad',
siemens: 'S',
sievert: 'Sv',
steradian: 'sr',
tesla: 'T',
volt: 'V',
watt: 'W',
weber: 'Wb',
// Non-SI units accepted for use with SI
astronomicalunit: 'au',
bel: 'B',
dalton: 'Da',
day: 'd',
decibel: 'dB',
degree: '°',
electronvolt: 'eV',
hectare: 'ha',
hour: 'h',
litre: 'L',
liter: 'L',
arcminute: '′', // minute (plane angle) U+2032
minute: 'min', // minute (time)
arcsecond: '″', // second (plane angle) U+2033
neper: 'Np',
tonne: 't',
// SI prefixes
yocto: 'y', // -24
zepto: 'z', // -21
atto: 'a', // -18
femto: 'f', // -15
pico: 'p', // -12
nano: 'n', // -9
micro: 'µ', // -6
milli: 'm', // -3
centi: 'c', // -2
deci: 'd', // -1
deca: 'da', // 1
hecto: 'h', // 2
kilo: 'k', // 3
mega: 'M', // 6
giga: 'G', // 9
tera: 'T', // 12
peta: 'P', // 15
exa: 'E', // 18
zetta: 'Z', // 21
yotta: 'Y', // 24
// Special
angstrom: 'Å',
};
/** Format a number string as LaTeX: commas → {,}, eN → \times 10^{N} */
function formatNumber(num) {
if (!num) return '';
const eMatch = num.match(/^(-?[\d,.]+)e(\d+)$/);
if (eMatch) {
const base = eMatch[1].replace(/,/g, '{,}');
return `${base} \\times 10^{${eMatch[2]}}`;
}
return num.replace(/,/g, '{,}');
}
const PREFIXES = [
'yocto', 'zepto', 'atto', 'femto', 'pico', 'nano', 'micro', 'milli',
'centi', 'deci', 'deca', 'hecto', 'kilo', 'mega', 'giga', 'tera',
'peta', 'exa', 'zetta', 'yotta',
];
/** Resolve a unit name to its symbol, handling combined prefix+unit names like 'millimeter' */
function resolveSymbol(name) {
if (name in UNITS) return UNITS[name];
for (const prefix of PREFIXES) {
if (name.startsWith(prefix)) {
const remainder = name.slice(prefix.length);
if (remainder in UNITS) {
return (UNITS[prefix] ?? prefix) + (UNITS[remainder] ?? remainder);
}
}
}
return name;
}
/** Convert a unit name to a \mathrm{} LaTeX token */
function unitToLatex(name) {
return `\\mathrm{${resolveSymbol(name)}}`;
}
const si2Role = {
name: 'si2',
doc: 'Format a number with SI units, e.g. {si2}`1.5e3 <\\kilo\\hertz>`',
body: {
type: String,
required: true,
},
run(data) {
const value = data.body;
// Supports: optional sign, comma-grouped integer, optional decimal, optional exponent
const match = value.match(
/^(-?(\d+(,\d+)*(\.\d+)?)?(e\d+)?)\s?<([\\a-zA-Z\s]+)>$/,
);
if (!match) {
return [{ type: 'inlineMath', value }];
}
const number = match[1];
const commands = match[match.length - 1];
const unitNames = [...commands.matchAll(/\\([a-zA-Z]+)/g)].map(([, c]) => c);
// Split on \per: units before → numerator, units after → denominator (^{-1})
const perIdx = unitNames.indexOf('per');
const numerator = perIdx === -1 ? unitNames : unitNames.slice(0, perIdx);
const denominator = perIdx === -1 ? [] : unitNames.slice(perIdx + 1);
const numLatex = numerator.map(unitToLatex).join('\\,');
const denLatex = denominator.map((u) => `${unitToLatex(u)}^{-1}`).join('\\,');
const unitLatex = [numLatex, denLatex].filter(Boolean).join('\\,');
const latex = `${formatNumber(number)}\\,${unitLatex}`;
return [{ type: 'inlineMath', value: latex }];
},
};
const plugin = { name: 'SI Units', roles: [si2Role] };
export default plugin;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment