Files
Kigi-CLI/crates/codegen/kigi-markdown/src/latex/commands.rs
T
ZacharyZhang-NY d6c20fc13f M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
2026-07-17 05:31:01 -04:00

533 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Core renderer: sequences, commands, scripts, fractions, accents.
use std::fmt::Write as _;
use super::cursor::Cursor;
use super::environments::render_environment;
use super::math_box::MathBox;
use super::symbols::{
map_mathbb, map_mathbf, map_mathcal, map_mathfrak, symbol, to_subscript, to_superscript,
};
use super::{MAX_DEPTH, Mode};
/// Render an atom's source to a flat (single-line) Unicode string.
///
/// Atoms are arguments to commands (fraction sides, script bodies, accent
/// targets); they always render flat — multi-row content inside them joins
/// with `; `.
pub(super) fn render_atom(atom: &str, depth: usize, mode: Mode) -> String {
let mut cursor = Cursor::new(atom);
let mut out = MathBox::new(true);
render_sequence(&mut cursor, &mut out, depth + 1, mode, None);
out.into_lines().concat()
}
/// Core renderer: walks `cursor`, appending Unicode to `out`.
///
/// `stop_at` optionally terminates the sequence at an unbalanced `}` (used
/// when rendering inside a group whose `{` was consumed by the caller).
pub(super) fn render_sequence(
cursor: &mut Cursor<'_>,
out: &mut MathBox,
depth: usize,
mode: Mode,
stop_at: Option<char>,
) {
while let Some(ch) = cursor.peek() {
if Some(ch) == stop_at {
cursor.bump();
return;
}
match ch {
'\\' => {
cursor.bump();
render_command(cursor, out, depth, mode);
}
'{' => {
cursor.bump();
if depth >= MAX_DEPTH {
// Too deep: render the group body flat, without recursing.
out.push_str(cursor.read_group_body());
} else {
// Render the group body into the same box so environments
// inside groups keep their 2D layout.
let body = cursor.read_group_body();
let mut sub = Cursor::new(body);
render_sequence(&mut sub, out, depth + 1, mode, None);
}
}
'}' => {
// Unbalanced closing brace: drop it.
cursor.bump();
}
'^' => {
cursor.bump();
render_script(cursor, out, depth, mode, Script::Super);
}
'_' => {
cursor.bump();
render_script(cursor, out, depth, mode, Script::Sub);
}
'~' => {
cursor.bump();
out.push(' ');
}
'&' => {
// Alignment marker outside an environment: drop.
cursor.bump();
}
'$' => {
// Stray math delimiter inside math: drop.
cursor.bump();
}
'-' if mode == Mode::Math => {
cursor.bump();
out.push('');
}
'\'' if mode == Mode::Math => {
cursor.bump();
out.push('');
}
c if c.is_whitespace() => {
cursor.skip_ws();
// TeX collapses whitespace runs (including newlines) to
// nothing semantically; keep a single space for readability.
if !out.at_line_start() && !out.ends_with_space() {
out.push(' ');
}
}
c => {
cursor.bump();
out.push(c);
}
}
}
}
/// Which script position is being rendered.
#[derive(Copy, Clone, PartialEq, Eq)]
enum Script {
Super,
Sub,
}
/// Render `^atom` / `_atom` using Unicode script chars when every char of
/// the rendered atom has a script form; otherwise `^x` / `^(...)` fallback.
///
/// Word-like atoms take the fallback even when fully mappable: labels such as
/// `p_{\text{torso}}` or `x_{max}` would otherwise become long modifier-letter
/// runs (`pₜₒᵣₛₒ`) that are hard to read and render with visible gaps in
/// terminal fonts lacking those glyphs. Index-like atoms (`x_{ij}`,
/// `T_{i+1}`, `n^{th}`) keep the compact Unicode form.
fn render_script(
cursor: &mut Cursor<'_>,
out: &mut MathBox,
depth: usize,
mode: Mode,
kind: Script,
) {
let Some(atom) = cursor.read_atom() else {
out.push(match kind {
Script::Super => '^',
Script::Sub => '_',
});
return;
};
let rendered = render_atom(atom, depth, mode);
let mapped: Option<String> = if script_atom_is_wordlike(atom, &rendered) {
None
} else {
rendered
.chars()
.map(|c| match kind {
Script::Super => to_superscript(c),
Script::Sub => to_subscript(c),
})
.collect()
};
match mapped {
Some(s) if !s.is_empty() => out.push_str(&s),
_ => {
out.push(match kind {
Script::Super => '^',
Script::Sub => '_',
});
if rendered.chars().count() > 1 {
let _ = write!(out, "({rendered})");
} else {
out.push_str(&rendered);
}
}
}
}
/// `true` if a script atom is a word-like label rather than indices.
///
/// Two signals, checked on the atom *source* and its rendered form:
///
/// - the source routes through a text-family command (`\text{…}`, `\mathrm{…}`,
/// `\operatorname{…}`, …): the author explicitly marked the content as a
/// word;
/// - the rendered form contains a run of 3+ ASCII letters: multi-letter runs
/// read as words (`max`, `torso`), while 12 letter runs are index
/// juxtapositions (`ij`, `th`) that stay compact.
fn script_atom_is_wordlike(atom: &str, rendered: &str) -> bool {
// `\text` also catches `\textrm`/`\textbf`/`\textit`/`\textsf`/`\texttt`/
// `\textnormal` by prefix; `\math…` variants and box commands likewise.
const TEXT_MARKERS: [&str; 8] = [
"\\text",
"\\mathrm",
"\\mathsf",
"\\mathtt",
"\\mathit",
"\\operatorname",
"\\mbox",
"\\hbox",
];
if TEXT_MARKERS.iter().any(|m| atom.contains(m)) {
return true;
}
let mut run = 0usize;
for c in rendered.chars() {
if c.is_ascii_alphabetic() {
run += 1;
if run >= 3 {
return true;
}
} else {
run = 0;
}
}
false
}
/// Render a `\command` whose backslash was already consumed.
fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode: Mode) {
let name = cursor.read_command_name();
match name {
// ── Structure ────────────────────────────────────────────────────
"" => out.push('\\'),
"\\" => out.push('\n'),
"begin" => render_environment(cursor, out, depth, mode),
"end" => {
// Stray \end without matching \begin: drop its argument.
let _ = take_brace_arg(cursor);
}
"left" | "right" => {
// Keep the delimiter that follows; `.` means "no delimiter".
cursor.skip_ws();
match cursor.peek() {
Some('.') => {
cursor.bump();
}
Some('\\') => {
cursor.bump();
render_command(cursor, out, depth, mode);
}
Some(c) => {
cursor.bump();
out.push(c);
}
None => {}
}
}
// ── Fractions / binomials / roots ────────────────────────────────
"frac" | "dfrac" | "tfrac" | "cfrac" => {
let num = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
let den = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
match (num, den) {
(Some(n), Some(d)) => out.push_str(&format_fraction(&n, &d)),
(Some(n), None) => out.push_str(&n),
_ => {}
}
}
"binom" | "tbinom" | "dbinom" => {
let n = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
let k = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
if let (Some(n), Some(k)) = (n, k) {
let _ = write!(out, "C({n}, {k})");
}
}
"sqrt" => {
cursor.skip_ws();
let index = if cursor.peek() == Some('[') {
cursor.bump();
let start = cursor.pos;
while let Some(c) = cursor.peek() {
if c == ']' {
break;
}
cursor.bump();
}
let idx = &cursor.src[start..cursor.pos];
cursor.bump(); // consume `]`
Some(render_atom(idx, depth, mode))
} else {
None
};
let radical = match index.as_deref() {
None | Some("2") => "√",
Some("3") => "∛",
Some("4") => "∜",
Some(other) => {
// ⁿ√ style prefix for other indices.
let sup: Option<String> = other.chars().map(to_superscript).collect();
out.push_str(&sup.unwrap_or_else(|| format!("({other})")));
"√"
}
};
out.push_str(radical);
if let Some(arg) = cursor.read_atom() {
let rendered = render_atom(arg, depth, mode);
// Parenthesize any multi-char radicand: `√ab` would read as
// `(√a)b`.
if rendered.chars().count() > 1 {
let _ = write!(out, "({rendered})");
} else {
out.push_str(&rendered);
}
}
}
// ── Boxes (frame dropped; content preserved) ─────────────────────
"boxed" => {
if let Some(arg) = take_brace_arg(cursor) {
out.push_str(&render_atom(arg, depth, mode));
}
}
"fbox" | "framebox" => {
if let Some(arg) = take_brace_arg(cursor) {
out.push_str(&render_atom(arg, depth, Mode::Text));
}
}
// ── Text / alphabets ─────────────────────────────────────────────
"text" | "textrm" | "textit" | "textbf" | "textsf" | "texttt" | "textnormal" | "mbox"
| "hbox" => {
if let Some(arg) = take_brace_arg(cursor) {
out.push_str(&render_atom(arg, depth, Mode::Text));
}
}
"mathrm" | "operatorname" | "mathit" | "mathsf" | "mathtt" | "mathnormal" => {
if let Some(arg) = take_brace_arg(cursor) {
out.push_str(&render_atom(arg, depth, Mode::Text));
}
}
"mathbb" => render_mapped_alphabet(cursor, out, depth, mode, map_mathbb),
"mathcal" | "mathscr" => render_mapped_alphabet(cursor, out, depth, mode, map_mathcal),
"mathfrak" => render_mapped_alphabet(cursor, out, depth, mode, map_mathfrak),
"mathbf" | "boldsymbol" | "bm" | "bold" => {
render_mapped_alphabet(cursor, out, depth, mode, map_mathbf)
}
// ── Accents (combining marks) ────────────────────────────────────
"hat" | "widehat" => render_accent(cursor, out, depth, mode, '\u{0302}'),
"bar" | "overline" => render_accent(cursor, out, depth, mode, '\u{0304}'),
"tilde" | "widetilde" => render_accent(cursor, out, depth, mode, '\u{0303}'),
"vec" => render_accent(cursor, out, depth, mode, '\u{20D7}'),
"dot" => render_accent(cursor, out, depth, mode, '\u{0307}'),
"ddot" => render_accent(cursor, out, depth, mode, '\u{0308}'),
"check" => render_accent(cursor, out, depth, mode, '\u{030C}'),
"breve" => render_accent(cursor, out, depth, mode, '\u{0306}'),
"acute" => render_accent(cursor, out, depth, mode, '\u{0301}'),
"grave" => render_accent(cursor, out, depth, mode, '\u{0300}'),
"mathring" => render_accent(cursor, out, depth, mode, '\u{030A}'),
"underline" => render_accent(cursor, out, depth, mode, '\u{0332}'),
// ── Negation ─────────────────────────────────────────────────────
"not" => {
if let Some(atom) = cursor.read_atom() {
let rendered = render_atom(atom, depth, mode);
match rendered.as_str() {
"∈" => out.push('∉'),
"=" => out.push('≠'),
"<" => out.push('≮'),
">" => out.push('≯'),
"≡" => out.push('≢'),
"⊂" => out.push('⊄'),
"⊆" => out.push('⊈'),
"∃" => out.push('∄'),
other => {
out.push_str(other);
// Combining long solidus overlay on the last char.
if !other.is_empty() {
out.push('\u{0338}');
}
}
}
}
}
// ── Decorations rendered as base + script ────────────────────────
"overset" | "stackrel" => {
let over = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
let base = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
if let (Some(over), Some(base)) = (over, base) {
out.push_str(&base);
let sup: Option<String> = over.chars().map(to_superscript).collect();
match sup {
Some(s) if !s.is_empty() => out.push_str(&s),
_ => {}
}
}
}
"underset" => {
let under = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
let base = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
if let (Some(under), Some(base)) = (under, base) {
out.push_str(&base);
let sub: Option<String> = under.chars().map(to_subscript).collect();
match sub {
Some(s) if !s.is_empty() => out.push_str(&s),
_ => {}
}
}
}
// ── Modular arithmetic ───────────────────────────────────────────
"pmod" => {
if let Some(arg) = take_brace_arg(cursor) {
if !out.at_line_start() && !out.ends_with_space() {
out.push(' ');
}
let _ = write!(out, "(mod {})", render_atom(arg, depth, mode));
}
}
"bmod" => {
if !out.at_line_start() && !out.ends_with_space() {
out.push(' ');
}
out.push_str("mod ");
}
// ── Spacing ──────────────────────────────────────────────────────
"," | ";" | ":" | ">" | " " | "space" | "thinspace" | "medspace" | "thickspace"
| "enspace" => {
if !out.at_line_start() && !out.ends_with_space() {
out.push(' ');
}
}
"quad" => out.push_str(" "),
"qquad" => out.push_str(" "),
"!" | "negthinspace" | "negmedspace" | "negthickspace" => {}
// ── No-ops (sizing/styling/structure hints) ──────────────────────
"limits" | "nolimits" | "displaystyle" | "textstyle" | "scriptstyle"
| "scriptscriptstyle" | "big" | "Big" | "bigg" | "Bigg" | "bigl" | "Bigl" | "biggl"
| "Biggl" | "bigr" | "Bigr" | "biggr" | "Biggr" | "bigm" | "Bigm" | "biggm" | "Biggm"
| "mathstrut" | "strut" | "allowbreak" | "nonumber" | "notag" | "mathopen"
| "mathclose" | "mathbin" | "mathrel" | "mathord" | "mathpunct" | "mathinner"
| "mathop" | "ensuremath" | "label" | "tag" => {
// \label/\tag carry non-visual arguments: drop them.
if matches!(name, "label" | "tag") {
let _ = take_brace_arg(cursor);
}
}
// ── Symbol table ─────────────────────────────────────────────────
_ => {
if let Some(sym) = symbol(name) {
out.push_str(sym);
} else {
// Unknown command: keep its name as plain text.
out.push_str(name);
}
}
}
}
/// Consume `{...}` (after optional whitespace) and return the body source.
pub(super) fn take_brace_arg<'a>(cursor: &mut Cursor<'a>) -> Option<&'a str> {
cursor.skip_ws();
if cursor.peek() == Some('{') {
cursor.bump();
Some(cursor.read_group_body())
} else {
None
}
}
/// `true` if a fraction/root operand needs parentheses for readability.
fn needs_parens(s: &str) -> bool {
s.chars().count() > 1 && s.contains([' ', '+', '', '-', '=', '/'])
}
/// Format `num/den`, mapping common numeric fractions to vulgar fractions.
fn format_fraction(num: &str, den: &str) -> String {
let vulgar = match (num, den) {
("1", "2") => Some('½'),
("1", "3") => Some('⅓'),
("2", "3") => Some('⅔'),
("1", "4") => Some('¼'),
("3", "4") => Some('¾'),
("1", "5") => Some('⅕'),
("2", "5") => Some('⅖'),
("3", "5") => Some('⅗'),
("4", "5") => Some('⅘'),
("1", "6") => Some('⅙'),
("5", "6") => Some('⅚'),
("1", "7") => Some('⅐'),
("1", "8") => Some('⅛'),
("3", "8") => Some('⅜'),
("5", "8") => Some('⅝'),
("7", "8") => Some('⅞'),
("1", "9") => Some('⅑'),
("1", "10") => Some('⅒'),
_ => None,
};
if let Some(v) = vulgar {
return v.to_string();
}
let n = if needs_parens(num) {
format!("({num})")
} else {
num.to_string()
};
let d = if needs_parens(den) {
format!("({den})")
} else {
den.to_string()
};
format!("{n}/{d}")
}
/// Render an alphabet-mapping command (`\mathbb{R}` etc.): map chars that
/// have a styled form, keep the rest as rendered.
fn render_mapped_alphabet(
cursor: &mut Cursor<'_>,
out: &mut MathBox,
depth: usize,
mode: Mode,
map: fn(char) -> Option<char>,
) {
let Some(atom) = cursor.read_atom() else {
return;
};
let rendered = render_atom(atom, depth, mode);
for c in rendered.chars() {
out.push(map(c).unwrap_or(c));
}
}
/// Render an accent command by appending a combining mark to each char of
/// the argument.
fn render_accent(
cursor: &mut Cursor<'_>,
out: &mut MathBox,
depth: usize,
mode: Mode,
combining: char,
) {
let Some(atom) = cursor.read_atom() else {
return;
};
let rendered = render_atom(atom, depth, mode);
for c in rendered.chars() {
out.push(c);
if !c.is_whitespace() {
out.push(combining);
}
}
}