use crate::ast::{Field, Node, Row};
mod block;
use crate::glyphs::{
ARM_FALL, ARM_RISE, COL_MARK_BOT, COL_MARK_TOP, CROSSING, DOUBLE_BODY, FRAC_BAR, HEAD_LEFT,
HEAD_RIGHT, MID, Mark, NORM, OP_BAND, OVERLINE_CORNER, PLACEHOLDER, ROW_JUNCTION_L,
ROW_JUNCTION_R, STEM, brace_corners, is_display_marker, lattice_char,
};
use crate::symbols::{Accent, Delim, DrawnForm, subscript_char, superscript_char};
pub use block::Block;
use block::{Annots, center_pad, hcat, vstack};
pub use crate::glyphs::CURSOR_CHAR;
fn is_marker_node(n: &Node) -> bool {
matches!(n, Node::Sym(c) if is_display_marker(*c))
}
fn italic_char(c: char) -> char {
match c {
'h' => 'ℎ', 'a'..='z' => char::from_u32(0x1D44E + (c as u32 - 'a' as u32)).unwrap(),
'A'..='Z' => char::from_u32(0x1D434 + (c as u32 - 'A' as u32)).unwrap(),
_ => c,
}
}
pub fn unstyle_char(c: char) -> char {
let shift = |plain: char, styled: char| {
char::from_u32(plain as u32 + (c as u32 - styled as u32)).unwrap()
};
match c {
'ℎ' => 'h',
'∗' => '*',
'𝑎'..='𝑧' => shift('a', '𝑎'),
'𝐴'..='𝑍' => shift('A', '𝐴'),
'𝛼'..='𝜔' => shift('α', '𝛼'),
'𝛢'..='𝛺' => shift('Α', '𝛢'),
c => c,
}
}
fn display_char(c: char, ctx: &RenderCtx) -> char {
match c {
'*' => '∗',
c if ctx.italic => italic_char(c),
c => c,
}
}
pub fn is_inline_script_row(row: &Row, superscript: bool) -> bool {
let map = if superscript {
superscript_char
} else {
subscript_char
};
inline_script(row, map).is_some()
}
fn inline_script(row: &Row, map: fn(char) -> Option<char>) -> Option<Vec<char>> {
if row.is_empty() {
return None;
}
row.iter()
.map(|n| match n {
Node::Sym(c) if is_display_marker(*c) => None,
Node::Sym(c) => map(*c),
_ => None,
})
.collect()
}
#[derive(Clone, Copy)]
pub struct RenderCtx {
pub italic: bool,
}
impl RenderCtx {
pub fn canonical() -> Self {
RenderCtx { italic: true }
}
}
type CursorRef<'a> = (&'a [(usize, Field)], usize);
fn func_block(t: &str) -> Block {
let chars: Vec<char> = t.chars().collect();
let dotted_ok = chars.first().is_some_and(|c| c.is_ascii_alphabetic())
&& chars.iter().all(|&c| c.is_ascii_alphabetic() || c == '.')
&& chars.iter().enumerate().all(|(i, &c)| {
c != '.'
|| match chars.get(i + 1) {
Some(n) => n.is_ascii_alphabetic(),
None => chars[..i].contains(&'.'),
}
});
if dotted_ok && chars.len() >= 2 {
Block::from_chars(chars)
} else {
quoted(t, '\'')
}
}
fn roman_block(c: char, glue: bool) -> Block {
if glue {
Block::from_chars(vec![c])
} else {
quoted(&c.to_string(), '\'')
}
}
fn quoted(t: &str, q: char) -> Block {
let mut chars = vec![q];
for c in t.chars() {
match c {
' ' if q == '\'' => chars.push('␣'),
'"' | '\\' if q == '"' => {
chars.push('\\');
chars.push(c);
}
c => chars.push(c),
}
}
chars.push(q);
Block::from_chars(chars)
}
fn has_wide_accent(n: &Node) -> bool {
matches!(n, Node::WideAccent { .. })
}
fn glue_alpha(n: &Node, right_edge: bool) -> bool {
match n {
Node::Sym(c) => c.is_alphabetic(),
Node::Accent { overs, base, .. } => {
base.is_alphabetic() && !(right_edge && overs.iter().any(|m| m.cells().len() > 1))
}
_ => false,
}
}
#[derive(Clone, Copy, Default)]
struct Info {
script: bool,
spacer: bool,
dot_run: bool,
wide_accent: bool,
marker: bool,
}
impl Info {
fn of(node: &Node) -> Self {
Info {
wide_accent: has_wide_accent(node),
dot_run: matches!(node, Node::Func(t) if t.contains('.')),
marker: is_marker_node(node),
script: matches!(node, Node::Sup { .. } | Node::Sub { .. }),
spacer: matches!(node, Node::Spacer),
}
}
}
struct Side<'a> {
info: Info,
edge: Option<char>,
bars: &'a [isize],
}
fn bar_edge_rows(b: &Block, right: bool) -> Vec<isize> {
let w = b.width();
b.lines
.iter()
.enumerate()
.filter_map(|(i, l)| {
let c = if right {
(l.len() == w).then(|| l.last().copied()).flatten()
} else {
l.first().copied()
};
(c == Some(FRAC_BAR)).then_some(i as isize - b.baseline as isize)
})
.collect()
}
fn fuses(l: Side, r: Side) -> bool {
if l.info.spacer || r.info.spacer {
return false;
}
let fuse = match (l.edge, r.edge) {
(Some(a), Some(b)) => {
a == OP_BAND
|| b == OP_BAND
|| (a.is_ascii_alphabetic() && b.is_ascii_alphabetic())
|| (a == '.' && b.is_ascii_alphabetic())
|| (a == b && (a == FRAC_BAR || a == DOUBLE_BODY))
|| (a == FRAC_BAR && b == HEAD_RIGHT)
|| (a == DOUBLE_BODY && b == HEAD_RIGHT)
|| (a == HEAD_LEFT && (b == FRAC_BAR || b == DOUBLE_BODY))
}
_ => false,
};
let dotted = l.info.dot_run && r.edge.is_some_and(|b| b.is_ascii_alphabetic() || b == '.');
let bar_touch = !l.bars.is_empty() && r.bars.iter().any(|row| l.bars.contains(row));
fuse || dotted || l.info.wide_accent || r.info.wide_accent || bar_touch
}
pub(crate) fn absorb_row(row: &mut Row) {
let mut i = 0;
while i < row.len() {
if !matches!(row[i], Node::Spacer) {
i += 1;
continue;
}
let run = row[i..].iter().take_while(|n| **n == Node::Spacer).count();
let between = i > 0 && i + run < row.len();
if run == 1 && between {
let mut without = row.clone();
without.remove(i);
if forced_gap(&without, i) {
row.remove(i);
continue;
}
}
i += run;
}
}
pub fn absorb_spacers(row: &Row) -> Row {
let mut out: Row = row
.iter()
.map(|n| {
let mut n = n.clone();
for f in n.fields() {
let inner = absorb_spacers(n.field(f));
*n.field_mut(f) = inner;
}
n
})
.collect();
absorb_row(&mut out);
out
}
fn forced_gap(row: &Row, at: usize) -> bool {
let ctx = RenderCtx::canonical();
let mut prev: Option<(Info, Option<char>, Vec<isize>)> = None;
for (i, (block, info)) in row_blocks(row, None, &ctx).into_iter().enumerate() {
if info.marker {
continue;
}
if i == at {
return match prev {
Some((pi, edge, bars)) => fuses(
Side {
info: pi,
edge,
bars: &bars,
},
Side {
info,
edge: block.baseline_edge(true),
bars: &bar_edge_rows(&block, false),
},
),
None => false,
};
}
let edge = block.baseline_edge(false);
let bars = bar_edge_rows(&block, true);
prev = Some((info, edge, bars));
}
false
}
fn row_blocks(row: &Row, cursor: Option<CursorRef>, ctx: &RenderCtx) -> Vec<(Block, Info)> {
let mut blocks: Vec<(Block, Info)> = Vec::with_capacity(row.len() + 1);
for (i, node) in row.iter().enumerate() {
let child_cursor = match cursor {
Some((path, col)) => match path.first() {
Some(&(pi, pf)) if pi == i => Some((pf, (&path[1..], col))),
_ => None,
},
None => None,
};
let info = Info::of(node);
let mut block = match node {
Node::Roman(c) => {
let glue = row[..i]
.iter()
.rev()
.find(|n| !is_marker_node(n))
.is_some_and(|n| glue_alpha(n, true))
|| row[i + 1..]
.iter()
.find(|n| !is_marker_node(n))
.is_some_and(|n| glue_alpha(n, false));
roman_block(*c, glue)
}
_ => render_node(node, child_cursor, ctx),
};
let first_real = row.iter().position(|n| !is_marker_node(n)).unwrap_or(0);
if i == first_real && info.script {
block = hcat(&[Block::from_chars(vec![PLACEHOLDER]), block]);
}
blocks.push((block, info));
}
blocks
}
pub fn render_row(
row: &Row,
cursor: Option<CursorRef>,
placeholder: bool,
ctx: &RenderCtx,
) -> Block {
let cursor_col = match cursor {
Some(([], col)) => Some(col),
_ => None,
};
if row.iter().all(is_marker_node) {
let mut b = match (cursor_col, placeholder, row.is_empty()) {
(Some(_), _, _) => Block::from_chars(vec![PLACEHOLDER]).with_caret(0, 0),
(None, true, _) | (None, false, false) => Block::from_chars(vec![PLACEHOLDER]),
(None, false, true) => Block::empty(),
};
for n in row {
if let Node::Sym(c) = n {
b.marks.push((0, 0, *c));
}
}
return b;
}
let mut blocks = row_blocks(row, cursor, ctx);
for (open, close) in [
(Mark::Frame { open: true }, Mark::Frame { open: false }),
(Mark::Delims { open: true }, Mark::Delims { open: false }),
] {
let (pair_open, pair_close) = (open.ch(), close.ch());
for m in 0..row.len() {
if !matches!(&row[m], Node::Sym(c) if *c == pair_open) {
continue;
}
let Some(t) = (m + 1..row.len()).find(|&t| !is_marker_node(&row[t])) else {
continue;
};
let close =
(t + 1..row.len()).find(|&c| matches!(&row[c], Node::Sym(ch) if *ch == pair_close));
let Some(close) = close else { continue };
let (h, w) = (blocks[t].0.height(), blocks[t].0.width());
blocks[m].0.marks.retain(|&(_, _, c)| c != pair_open);
blocks[close].0.marks.retain(|&(_, _, c)| c != pair_close);
blocks[t].0.marks.push((0, 0, pair_open));
blocks[t]
.0
.marks
.push((h.saturating_sub(1), w.saturating_sub(1), pair_close));
}
}
if let Some(col) = cursor_col {
blocks.insert(
col,
(
Block::caret_marker(),
Info {
marker: true,
..Info::default()
},
),
);
}
let mut spaced: Vec<Block> = Vec::with_capacity(blocks.len() * 2);
let mut prev: Option<Info> = None;
let mut last_edge: Option<char> = None;
let mut last_bars: Vec<isize> = Vec::new();
for (block, info) in blocks {
if info.marker {
spaced.push(block);
continue;
}
let need = match prev {
Some(p) => fuses(
Side {
info: p,
edge: last_edge,
bars: &last_bars,
},
Side {
info,
edge: block.baseline_edge(true),
bars: &bar_edge_rows(&block, false),
},
),
_ => false,
};
if need {
spaced.push(Block::from_chars(vec![' ']));
}
let edge = block.baseline_edge(false);
last_bars = bar_edge_rows(&block, true);
spaced.push(block);
prev = Some(info);
last_edge = edge;
}
hcat(&spaced)
}
pub fn export_aa(row: &Row) -> String {
let row = crate::ast::normalize(row);
let canonical = render_root(&row, None, &RenderCtx::canonical()).to_text();
if !canonical.contains(PLACEHOLDER) {
return canonical;
}
let blanked: String = canonical
.lines()
.map(|l| l.replace(PLACEHOLDER, " "))
.map(|l| l.trim_end().to_string())
.collect::<Vec<_>>()
.join("\n");
match (
crate::parse::parse(&blanked),
crate::parse::parse(&canonical),
) {
(Ok(b), Ok(c)) if b == c => blanked,
_ => canonical,
}
}
pub fn render_root(row: &Row, cursor: Option<CursorRef>, ctx: &RenderCtx) -> Block {
if !row.iter().any(|n| matches!(n, Node::Break)) {
return render_row(row, cursor, false, ctx);
}
let mut segments: Vec<Block> = Vec::new();
let mut start = 0usize;
let cursor_col = match cursor {
Some(([], col)) => Some(col),
_ => None,
};
let bounds: Vec<usize> = row
.iter()
.enumerate()
.filter_map(|(i, n)| matches!(n, Node::Break).then_some(i))
.chain([row.len()])
.collect();
for &end in bounds.iter() {
let seg = &row[start..end];
let rebased: Option<(Vec<(usize, Field)>, usize)> = match cursor {
Some((path, col)) => match path.first() {
Some(&(i, f)) if (start..end).contains(&i) => {
let mut p = path.to_vec();
p[0] = (i - start, f);
Some((p, col))
}
None if cursor_col.is_some_and(|c| (start..=end).contains(&c)) => {
Some((Vec::new(), cursor_col.unwrap() - start))
}
_ => None,
},
None => None,
};
let seg_vec: Row = seg.to_vec();
let cur_ref = rebased.as_ref().map(|(p, c)| (p.as_slice(), *c));
segments.push(render_row(&seg_vec, cur_ref, false, ctx));
start = end + 1;
}
vstack(&segments)
}
fn l_placeholder(cursor: Option<(Field, CursorRef)>) -> bool {
cursor.is_some()
}
struct DelimShape<'a> {
left: char,
right: char,
mids: usize,
segs: &'a [Row],
fusable: bool,
is_norm: bool,
angle_sided: bool,
curly: bool,
}
fn delim_extent(
h: usize,
bl: usize,
angle: bool,
curly: bool,
inner_norm_full: bool,
) -> (usize, usize) {
if angle {
let k = (bl + 1).max(h - 1 - bl);
(2 * k, k - 1)
} else if curly && h == 2 {
(3, 1)
} else if inner_norm_full {
(h + 2, bl + 1)
} else {
(h, bl)
}
}
fn delim_shape(node: &Node) -> DelimShape<'_> {
match node {
Node::Norm { arg } => DelimShape {
left: NORM,
right: NORM,
mids: 0,
segs: std::slice::from_ref(arg),
fusable: false,
is_norm: true,
angle_sided: false,
curly: false,
},
Node::Delim {
left,
right,
mids,
segs,
} => DelimShape {
left: left.spec(true),
right: right.spec(false),
mids: *mids,
segs,
fusable: left.fuses() && right.fuses(),
is_norm: false,
angle_sided: *left == Delim::Angle || *right == Delim::Angle,
curly: [left, right]
.iter()
.any(|d| d.col().is_some_and(|c| c.info().vertex.is_some())),
},
_ => unreachable!("delim_shape is for Norm / Delim"),
}
}
fn render_node(node: &Node, cursor: Option<(Field, CursorRef)>, ctx: &RenderCtx) -> Block {
let cur = |f: Field| -> Option<CursorRef> {
match cursor {
Some((cf, c)) if cf == f => Some(c),
_ => None,
}
};
match node {
Node::Spacer => Block::from_chars(vec![' ']),
Node::Break => Block::empty(),
Node::Sym(c) if is_display_marker(*c) => {
let mut b = Block::empty();
b.marks.push((0, 0, *c));
b
}
Node::Sym(c) => Block::from_chars(vec![display_char(*c, ctx)]),
Node::Func(name) => func_block(name),
Node::Text(t) => quoted(t, '"'),
Node::Roman(c) => roman_block(*c, false),
Node::WideAccent {
overs,
unders,
base,
} => {
let b = render_row(base, cur(Field::WideBase), true, ctx);
let material = overs
.iter()
.chain(unders.iter())
.map(|m| m.cells().len())
.max()
.unwrap_or(1);
let bw = b.width().max(material).max(1);
let w = bw + 2;
let band_row = |m: Accent| {
let mut r = vec![OP_BAND; w];
match m.drawn() {
DrawnForm::Center(g) => r[w / 2] = g,
DrawnForm::Fill(g) => {
for cell in r.iter_mut().take(w - 1).skip(1) {
*cell = g;
}
}
DrawnForm::Dots => {
let cells = m.cells();
let s = (w - cells.len()) / 2;
for (i, &g) in cells.iter().enumerate() {
r[s + i] = g;
}
}
}
r
};
let mut lines: Vec<Vec<char>> = overs.iter().rev().map(|&m| band_row(m)).collect();
let off = lines.len();
let baseline = off + b.baseline.min(b.height().saturating_sub(1));
lines.extend(center_pad(&b, w));
lines.extend(unders.iter().map(|&m| band_row(m)));
Annots::default()
.centered(&b, w, off)
.into_block(lines, baseline)
}
Node::Accent {
overs,
unders,
base,
} => {
let b = display_char(*base, ctx);
let w = overs
.iter()
.chain(unders.iter())
.map(|m| m.cells().len())
.max()
.unwrap_or(1)
.max(1);
let pad = |mut v: Vec<char>| {
v.resize(w, ' ');
v
};
let mut lines: Vec<Vec<char>> = overs.iter().rev().map(|&m| pad(m.cells())).collect();
lines.push(pad(vec![b]));
lines.extend(unders.iter().map(|&m| pad(m.cells())));
Block::new(lines, overs.len())
}
Node::Frac { num, den } => {
let n = render_row(num, cur(Field::FracNum), true, ctx);
let d = render_row(den, cur(Field::FracDen), true, ctx);
let w = n.width().max(d.width()) + 2;
let mut lines = center_pad(&n, w);
let baseline = lines.len();
lines.push(vec![FRAC_BAR; w]);
lines.extend(center_pad(&d, w));
Annots::default()
.centered(&n, w, 0)
.centered(&d, w, baseline + 1)
.into_block(lines, baseline)
}
Node::Sqrt { arg, index } => {
let a = render_row(arg, cur(Field::SqrtArg), true, ctx);
let h = a.height();
let w = a.width();
let radical = index.glyph();
let mut lines = Vec::with_capacity(h + 1);
let mut top = vec![FRAC_BAR; w + 1];
top[0] = OVERLINE_CORNER;
lines.push(top);
for (r, line) in a.lines.iter().enumerate() {
let head = if r == h - 1 { radical } else { STEM };
let mut row = Vec::with_capacity(w + 1);
row.push(head);
row.extend_from_slice(line);
lines.push(row);
}
Block {
lines,
baseline: a.baseline + 1,
caret: a.caret.map(|(r, c)| (r + 1, c + 1)),
marks: a
.marks
.iter()
.map(|&(r, c, ch)| (r + 1, c + 1, ch))
.collect(),
}
}
Node::Sup { arg } => {
if cursor.is_none()
&& let Some(chars) = inline_script(arg, superscript_char)
{
return Block::from_chars(chars);
}
let a = render_row(arg, cur(Field::SupArg), true, ctx);
let h = a.height();
Block {
lines: a.lines,
baseline: h,
caret: a.caret,
marks: a.marks,
}
}
Node::Sub { arg } => {
if cursor.is_none()
&& let Some(chars) = inline_script(arg, subscript_char)
{
return Block::from_chars(chars);
}
let a = render_row(arg, cur(Field::SubArg), true, ctx);
let mut lines = vec![vec![' '; a.width()]];
lines.extend(a.lines);
Block {
lines,
baseline: 0,
caret: a.caret.map(|(r, c)| (r + 1, c)),
marks: a.marks.iter().map(|&(r, c, ch)| (r + 1, c, ch)).collect(),
}
}
Node::BigOp { .. } | Node::BigOpSym { .. } => {
let (lower, upper) = match node {
Node::BigOp { lower, upper, .. } | Node::BigOpSym { lower, upper, .. } => {
(lower, upper)
}
_ => unreachable!(),
};
let editing = cursor.is_some();
let u = render_row(upper, cur(Field::OpUpper), editing, ctx);
let l = render_row(lower, cur(Field::OpLower), editing, ctx);
let b = match node {
Node::BigOpSym { op, .. } => Block::from_chars(vec![display_char(*op, ctx)]),
Node::BigOp { name, .. } => Block::from_chars(name.chars().collect()),
_ => unreachable!(),
};
if u.is_empty() && l.is_empty() && cursor.is_none() {
return b;
}
let bw = b.width().max(1);
let w = u.width().max(l.width()).max(bw) + 2;
let mut band = vec![OP_BAND; w];
let left = (w - bw) / 2;
for (i, &c0) in b.lines[0].iter().enumerate() {
band[left + i] = c0;
}
let mut lines = center_pad(&u, w);
let baseline = lines.len();
lines.push(band);
lines.extend(center_pad(&l, w));
Annots::default()
.centered(&u, w, 0)
.centered(&l, w, baseline + 1)
.into_block(lines, baseline)
}
Node::Brace { over, arg, label } => {
let a = render_row(arg, cur(Field::BraceArg), true, ctx);
let l = render_row(label, cur(Field::BraceLabel), l_placeholder(cursor), ctx);
let w = a.width().max(l.width()).max(1) + 2;
let mut brace = vec![FRAC_BAR; w];
let corners = brace_corners(*over);
brace[0] = corners.0;
brace[w - 1] = corners.1;
let mut lines: Vec<Vec<char>> = Vec::new();
if *over {
lines.extend(center_pad(&l, w));
lines.push(brace);
let a_off = lines.len();
let baseline = a_off + a.baseline;
lines.extend(center_pad(&a, w));
Annots::default()
.centered(&l, w, 0)
.centered(&a, w, a_off)
.into_block(lines, baseline)
} else {
let baseline = a.baseline;
lines.extend(center_pad(&a, w));
let brace_off = lines.len();
lines.push(brace);
lines.extend(center_pad(&l, w));
Annots::default()
.centered(&a, w, 0)
.centered(&l, w, brace_off + 1)
.into_block(lines, baseline)
}
}
Node::Arrow { op, over, under } => {
let editing = cursor.is_some();
let o = render_row(over, cur(Field::ArrowOver), editing, ctx);
let u = render_row(under, cur(Field::ArrowUnder), editing, ctx);
let w = o.width().max(u.width()).max(1) + 3;
let mut body = vec![op.body(); w];
if !op.right() {
body[0] = HEAD_LEFT;
} else {
body[w - 1] = HEAD_RIGHT;
}
let mut lines = center_pad(&o, w);
let baseline = lines.len();
lines.push(body);
lines.extend(center_pad(&u, w));
Annots::default()
.centered(&o, w, 0)
.centered(&u, w, baseline + 1)
.into_block(lines, baseline)
}
Node::Norm { .. } | Node::Delim { .. } => {
let DelimShape {
left,
right,
mids,
segs,
fusable,
is_norm,
angle_sided,
curly,
} = delim_shape(node);
let (left, right) = (&left, &right);
if mids == 0
&& fusable
&& let [seg] = segs
&& let [Node::Array { rows, cols, cells }] =
&seg.iter().filter(|n| !is_marker_node(n)).collect::<Vec<_>>()[..]
{
let ai = seg.iter().position(|n| !is_marker_node(n)).unwrap_or(0);
let seg_cursor = cur(Field::Seg(0));
let acur = match seg_cursor {
Some((path, c)) => match path.first() {
Some(&(i, f)) if i == ai => Some((f, (&path[1..], c))),
_ => None,
},
None => None,
};
if !matches!(seg_cursor, Some(([], _))) {
let mut b = render_fused_grid(*left, *right, *rows, *cols, cells, acur, ctx);
let (bl, w, h) = (b.baseline, b.width(), b.height());
for (i, n) in seg.iter().enumerate() {
if let Node::Sym(c) = n
&& is_display_marker(*c)
{
let (y, x) = match Mark::decode(*c) {
Some(Mark::Frame { open: true }) => (0, 0),
Some(Mark::Frame { open: false }) => {
(h.saturating_sub(1), w.saturating_sub(1))
}
_ if i < ai => (bl, 1),
_ => (bl, w.saturating_sub(2)),
};
b.marks.push((y, x, *c));
}
}
return b;
}
}
let mut parts: Vec<Block> = Vec::with_capacity(segs.len() * 2);
for (k, seg) in segs.iter().enumerate() {
if k > 0 {
parts.push(Block::from_chars(vec![' ']));
}
parts.push(render_row(seg, cur(Field::Seg(k)), true, ctx));
}
let mut body = hcat(&parts);
let mut x = 0;
for (k, b) in parts.iter().enumerate() {
if k % 2 == 1 {
for line in body.lines.iter_mut() {
line[x] = MID;
}
}
x += b.width();
}
let h = body.height().max(1);
let bl = body.baseline.min(h - 1);
let angle = h >= 2 && angle_sided;
let inner_norm_full = is_norm
&& (0..body.width()).any(|c| {
body.lines
.iter()
.all(|line| line.get(c).copied() == Some(NORM))
});
let (ext_h, ext_bl) = delim_extent(h, bl, angle, curly, inner_norm_full);
let top_pad = ext_bl - bl;
let width = body.width();
let mut inner: Vec<Vec<char>> = Vec::with_capacity(ext_h);
for r in 0..ext_h {
inner.push(match r.checked_sub(top_pad) {
Some(br) if br < h && br < body.lines.len() => body.lines[br].clone(),
_ => vec![' '; width],
});
}
let mut x = 0;
for (kk, b) in parts.iter().enumerate() {
if kk % 2 == 1 {
for line in inner.iter_mut() {
line[x] = MID;
}
}
x += b.width();
}
let side = |spec: char, is_left: bool| -> Vec<Vec<char>> {
if angle && (spec == '⟨' || spec == '⟩') {
let k = ext_h / 2;
(0..ext_h)
.map(|r| {
let (dist, glyph) = if r <= ext_bl {
(ext_bl - r, if is_left { ARM_RISE } else { ARM_FALL })
} else {
(r - ext_bl - 1, if is_left { ARM_FALL } else { ARM_RISE })
};
let col = if is_left { dist } else { (k - 1) - dist };
let mut row = vec![' '; k];
row[col] = glyph;
row
})
.collect()
} else {
delim_column(spec, is_left, ext_h, ext_bl)
.into_iter()
.map(|c| vec![c])
.collect()
}
};
let lcols = side(*left, true);
let rcols = side(*right, false);
let lw = lcols[0].len();
let mut lines = Vec::with_capacity(ext_h);
for (r, line) in inner.into_iter().enumerate() {
let mut row = Vec::with_capacity(lw + width + rcols[0].len());
row.extend_from_slice(&lcols[r]);
row.extend(line);
row.extend_from_slice(&rcols[r]);
lines.push(row);
}
Block {
lines,
baseline: ext_bl,
caret: body.caret.map(|(r, c)| (r + top_pad, c + lw)),
marks: body
.marks
.iter()
.map(|&(r, c, ch)| (r + top_pad, c + lw, ch))
.collect(),
}
}
Node::Array { rows, cols, cells } => render_lattice(*rows, *cols, cells, cursor, ctx),
}
}
fn render_fused_grid(
left: char,
right: char,
rows: usize,
cols: usize,
cells: &[Row],
cursor: Option<(Field, CursorRef)>,
ctx: &RenderCtx,
) -> Block {
let blocks: Vec<Block> = cells
.iter()
.enumerate()
.map(|(i, cell)| {
let cur = match cursor {
Some((Field::Cell(ci), c)) if ci == i => Some(c),
_ => None,
};
render_row(cell, cur, true, ctx)
})
.collect();
let col_w: Vec<usize> = (0..cols)
.map(|j| {
(0..rows)
.map(|i| blocks[i * cols + j].width())
.max()
.unwrap_or(1)
})
.collect();
let mut marker_x: Vec<usize> = Vec::new();
let mut x = 0;
for (j, w) in col_w.iter().enumerate() {
x += w + 2;
if j + 1 < cols {
marker_x.push(x);
x += 1;
}
}
let width = x;
let edge_row = |mark: char| {
let mut r = vec![' '; width];
for &mx in &marker_x {
r[mx] = mark;
}
r
};
let one_row = rows == 1;
let mut lines: Vec<Vec<char>> = Vec::new();
let mut caret: Option<(usize, usize)> = None;
let mut marks: Vec<(usize, usize, char)> = Vec::new();
let mut sep_rows: Vec<usize> = Vec::new();
if one_row {
lines.push(edge_row(COL_MARK_TOP));
}
for i in 0..rows {
if i > 0 {
sep_rows.push(lines.len());
lines.push(edge_row(CROSSING));
}
let mut parts: Vec<Block> = Vec::new();
for j in 0..cols {
if j > 0 {
parts.push(Block::new(vec![vec![' ']], 0));
}
parts.push(Block::new(vec![vec![' ']], 0));
let b = &blocks[i * cols + j];
parts.push(
Annots::default()
.centered(b, col_w[j], 0)
.into_block(center_pad(b, col_w[j]), b.baseline),
);
parts.push(Block::new(vec![vec![' ']], 0));
}
let row_block = hcat(&parts);
let row_off = lines.len();
if let Some((r, c)) = row_block.caret {
caret = Some((r + row_off, c));
}
marks.extend(
row_block
.marks
.iter()
.map(|&(r, c, ch)| (r + row_off, c, ch)),
);
for line in row_block.lines {
let mut l = line;
l.resize(width, ' ');
lines.push(l);
}
}
if one_row {
lines.push(edge_row(COL_MARK_BOT));
}
let h = lines.len();
let bl = (h - 1) / 2;
let junction = cols == 1;
let lcol = delim_column(left, true, h, bl);
let rcol = delim_column(right, false, h, bl);
let mut out = Vec::with_capacity(h);
for (r, line) in lines.into_iter().enumerate() {
let (lc, rc) = if junction && sep_rows.contains(&r) {
(ROW_JUNCTION_L, ROW_JUNCTION_R)
} else {
(lcol[r], rcol[r])
};
let mut row = Vec::with_capacity(width + 2);
row.push(lc);
row.extend(line);
row.push(rc);
out.push(row);
}
Block {
lines: out,
baseline: bl,
caret: caret.map(|(r, c)| (r, c + 1)),
marks: marks.into_iter().map(|(r, c, ch)| (r, c + 1, ch)).collect(),
}
}
fn render_lattice(
rows: usize,
cols: usize,
cells: &[Row],
cursor: Option<(Field, CursorRef)>,
ctx: &RenderCtx,
) -> Block {
let cctx = *ctx;
let blocks: Vec<Block> = cells
.iter()
.enumerate()
.map(|(i, cell)| {
let cur = match cursor {
Some((Field::Cell(ci), c)) if ci == i => Some(c),
_ => None,
};
render_row(cell, cur, true, &cctx)
})
.collect();
let col_w: Vec<usize> = (0..cols)
.map(|j| {
(0..rows)
.map(|i| blocks[i * cols + j].width())
.max()
.unwrap_or(1)
})
.collect();
let mut marker_x = vec![0usize];
for w in &col_w {
marker_x.push(marker_x.last().unwrap() + w + 3);
}
let width = *marker_x.last().unwrap() + 1;
let kind = |i: usize, n: usize| {
if i == 0 {
0
} else if i == n {
2
} else {
1
}
};
let marker_row = |ri: usize| {
let mut r = vec![' '; width];
for (ci, &x) in marker_x.iter().enumerate() {
r[x] = lattice_char(kind(ri, rows), kind(ci, cols));
}
r
};
let mut lines: Vec<Vec<char>> = vec![marker_row(0)];
let mut caret: Option<(usize, usize)> = None;
let mut marks: Vec<(usize, usize, char)> = Vec::new();
for i in 0..rows {
let mut parts: Vec<Block> = Vec::new();
for j in 0..cols {
parts.push(Block::new(vec![vec![' '; 2]], 0));
let b = &blocks[i * cols + j];
parts.push(
Annots::default()
.centered(b, col_w[j], 0)
.into_block(center_pad(b, col_w[j]), b.baseline),
);
parts.push(Block::new(vec![vec![' ']], 0));
}
let row_block = hcat(&parts);
let row_off = lines.len();
if let Some((r, c)) = row_block.caret {
caret = Some((r + row_off, c));
}
marks.extend(
row_block
.marks
.iter()
.map(|&(r, c, ch)| (r + row_off, c, ch)),
);
for line in row_block.lines {
let mut l = line;
l.resize(width, ' ');
lines.push(l);
}
lines.push(marker_row(i + 1));
}
let h = lines.len();
Block {
lines,
baseline: (h - 1) / 2,
caret,
marks,
}
}
fn delim_column(spec: char, left: bool, h: usize, bl: usize) -> Vec<char> {
match spec {
'\u{27e8}' | '\u{27e9}' => {
let (up, down) = if spec == '\u{27e8}' {
(ARM_RISE, ARM_FALL)
} else {
(ARM_FALL, ARM_RISE)
};
return (0..h)
.map(|r| match r.cmp(&bl) {
std::cmp::Ordering::Equal => spec,
std::cmp::Ordering::Less => up,
std::cmp::Ordering::Greater => down,
})
.collect();
}
'\u{2016}' => return vec!['\u{2016}'; h],
_ => {}
}
let Some((d, spec_side)) = crate::symbols::Delim::of_spec(spec) else {
return vec![spec; h];
};
let Some(cd) = d.col() else {
return vec![spec; h];
};
let info = cd.info();
let is_left = spec_side.unwrap_or(left);
if h == 1 {
return vec![if is_left { info.short.0 } else { info.short.1 }];
}
let (top, ext, bot) = info.tall[usize::from(!is_left)];
(0..h)
.map(|r| match info.vertex {
Some(vx) if r == bl => {
if is_left {
vx.0
} else {
vx.1
}
}
_ if r == 0 => top,
_ if r == h - 1 => bot,
_ => ext,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn sym_row(s: &str) -> Row {
s.chars().map(Node::Sym).collect()
}
fn plain(root: &Row) -> Vec<String> {
let ctx = RenderCtx { italic: false };
render_row(root, None, false, &ctx)
.to_strings()
.iter()
.map(|l| l.trim_end().to_string())
.collect()
}
#[test]
fn fraction_renders_with_bar() {
let root = vec![Node::Frac {
num: sym_row("1"),
den: sym_row("x+1"),
}];
assert_eq!(plain(&root), vec![" 1", "─────", " x+1"]);
}
#[test]
fn inline_superscript() {
let root = vec![Node::Sym('x'), Node::Sup { arg: sym_row("2") }];
assert_eq!(plain(&root), vec!["x²"]);
}
#[test]
fn bigop_band_marks_limit_extent() {
let root = vec![Node::BigOpSym {
op: '∑',
lower: sym_row("i=0"),
upper: sym_row("n"),
}];
assert_eq!(plain(&root), vec![" n", "┈┈∑┈┈", " i=0"]);
}
#[test]
fn bigop_without_limits_is_bare() {
let root = vec![Node::BigOpSym {
op: '∫',
lower: vec![],
upper: vec![],
}];
assert_eq!(plain(&root), vec!["∫"]);
}
#[test]
fn sqrt_single_line() {
let root = vec![Node::Sqrt {
arg: sym_row("2"),
index: crate::symbols::Radical::Sqrt,
}];
assert_eq!(plain(&root), vec!["┌─", "√2"]);
}
#[test]
fn leading_script_gets_explicit_base() {
let root = vec![Node::Sup { arg: sym_row("2") }];
assert_eq!(plain(&root), vec!["⬚²"]);
}
#[test]
fn matrix_2x2() {
let root = vec![Node::Delim {
left: crate::symbols::Delim::Col(crate::symbols::ColDelim::Bracket),
right: crate::symbols::Delim::Col(crate::symbols::ColDelim::Bracket),
mids: 0,
segs: vec![vec![Node::Array {
rows: 2,
cols: 2,
cells: vec![sym_row("a"), sym_row("b"), sym_row("c"), sym_row("d")],
}]],
}];
assert_eq!(plain(&root), vec!["⎡ a b ⎤", "⎢ ┼ ⎥", "⎣ c d ⎦"]);
}
#[test]
fn delim_families_render() {
let root = vec![Node::Delim {
left: crate::symbols::Delim::Col(crate::symbols::ColDelim::Brace),
right: crate::symbols::Delim::Col(crate::symbols::ColDelim::Brace),
mids: 0,
segs: vec![vec![Node::Frac {
num: sym_row("1"),
den: sym_row("2"),
}]],
}];
assert_eq!(plain(&root), vec!["⎧ 1 ⎫", "⎨───⎬", "⎩ 2 ⎭"]);
let root = vec![Node::Delim {
left: crate::symbols::Delim::Angle,
right: crate::symbols::Delim::Angle,
mids: 1,
segs: vec![sym_row("x"), sym_row("y")],
}];
assert_eq!(plain(&root), vec!["⟨x│y⟩"]);
let root = vec![Node::Array {
rows: 2,
cols: 2,
cells: vec![sym_row("a"), sym_row("b"), sym_row("c"), sym_row("d")],
}];
assert_eq!(
plain(&root),
vec!["┌ ┬ ┐", " a b", "├ ┼ ┤", " c d", "└ ┴ ┘"]
);
}
#[test]
fn func_renders_upright() {
let root = vec![Node::Func("sin".into()), Node::Sym('x')];
let ctx = RenderCtx::canonical();
let b = render_row(&root, None, false, &ctx);
assert_eq!(b.to_text(), "sin𝑥");
}
#[test]
fn bigop_shows_placeholders_while_editing() {
let root = vec![Node::BigOpSym {
op: '∑',
lower: vec![],
upper: vec![],
}];
let path = [(0, Field::OpLower)];
let b = render_row(&root, Some((&path, 0)), false, &RenderCtx::canonical());
let text = b.to_text();
let (cy, cx) = b.caret.expect("caret present");
assert_eq!(b.lines[cy][cx], PLACEHOLDER, "caret on the ⬚:\n{}", text);
assert!(cy > b.baseline, "caret in the lower limit:\n{}", text);
assert!(
text.contains(PLACEHOLDER),
"empty upper slot visible:\n{}",
text
);
let plain = render_row(&root, None, false, &RenderCtx::canonical()).to_text();
assert_eq!(plain, "∑");
}
#[test]
fn baseline_alignment_of_fraction_in_row() {
let root = vec![
Node::Sym('a'),
Node::Sym('+'),
Node::Frac {
num: sym_row("1"),
den: sym_row("2"),
},
];
let lines = plain(&root);
assert_eq!(lines.len(), 3);
assert!(lines[1].starts_with("a+"));
}
}