unitToTex function

String unitToTex(
  1. String input, {
  2. SiOptions options = const SiOptions(),
})

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'\,');
  }
}