unitToTex function
Units: siunitx unit macros (\kilo\metre\per\second\squared) or
literal text (km/s, kg.m/s^2).
Implementation
String unitToTex(String input, {SiOptions options = const SiOptions()}) {
final s = input.trim().replaceAllMapped(
RegExp(r'\\([A-Za-z]+)(?![A-Za-z])'),
(m) => _abbreviations['\\${m[1]}'] ?? m[0]!,
);
if (s.isEmpty) return '';
// Literal units (`km/s`) unless the text is nothing but macros.
final rest = s.replaceAll(RegExp(r'\\[A-Za-z]+(\{[^}]*\}?)?'), '').trim();
if (rest.isNotEmpty) return _literal(s);
final units = <_Unit>[];
var prefix = '';
int? powerBefore;
var per = false;
final commands = RegExp(r'\\([A-Za-z]+)(?:\{([^}]*)\}?)?').allMatches(s);
for (final m in commands) {
final name = '\\${m[1]}';
final arg = m[2];
if (_prefixes.containsKey(name)) {
prefix += _prefixes[name]!;
} else if (_units.containsKey(name)) {
final u = _Unit('$prefix${_units[name]}', powerBefore ?? 1);
if (per) u.power = -u.power;
units.add(u);
prefix = '';
powerBefore = null;
per = false;
} else {
switch (name) {
case r'\per':
per = true;
case r'\square':
powerBefore = 2;
case r'\cubic':
powerBefore = 3;
case r'\raiseto':
powerBefore = int.tryParse(arg ?? '');
case r'\squared' || r'\cubed' || r'\tothe':
if (units.isNotEmpty) {
final p = name == r'\squared'
? 2
: name == r'\cubed'
? 3
: int.tryParse(arg ?? '') ?? 1;
final u = units.last;
u.power = u.power.sign * p;
}
default:
// Unknown: keep it as a unit so nothing is lost.
units.add(_Unit('$prefix$name', per ? -1 : 1));
prefix = '';
per = false;
}
}
}
if (prefix.isNotEmpty) units.add(_Unit(prefix, 1));
String one(_Unit u, {bool positive = false}) {
final p = positive ? u.power.abs() : u.power;
final symbol = u.symbol.startsWith(r'\%') || u.symbol.startsWith(r'\')
? u.symbol
: '\\mathrm{${u.symbol}}';
return p == 1 ? symbol : '$symbol^{$p}';
}
final top = units.where((u) => u.power > 0).toList();
final bottom = units.where((u) => u.power < 0).toList();
switch (options.perMode) {
case PerMode.power:
return units.map(one).join(r'\,');
case PerMode.symbol when bottom.isNotEmpty:
final den = bottom.map((u) => one(u, positive: true)).join(r'\,');
final num = top.isEmpty ? '1' : top.map(one).join(r'\,');
return bottom.length > 1 ? '$num/($den)' : '$num/$den';
case PerMode.fraction when bottom.isNotEmpty:
final den = bottom.map((u) => one(u, positive: true)).join(r'\,');
final num = top.isEmpty ? '1' : top.map(one).join(r'\,');
return '\\frac{$num}{$den}';
default:
return units.map(one).join(r'\,');
}
}