Skip to content

Commit 18ef913

Browse files
authored
feat(convert): support color-related optional arguments for commands (#213)
* feat(converter): support color optional arguments - refactor: get-tex-color() - add `convert_command_color` for color command * fix(test): update accepted tests change * fix(ci): make ci happy, cleanup legacy, insta test accept
1 parent 22957a7 commit 18ef913

4 files changed

Lines changed: 178 additions & 19 deletions

File tree

crates/mitex/src/converter.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ impl Converter {
274274
"includegraphics" => {
275275
self.convert_command_includegraphics(f, &cmd)?;
276276
}
277+
"color" | "textcolor" | "colorbox" => {
278+
self.convert_command_color(f, &cmd, spec)?;
279+
}
277280
_ => {
278281
self.convert_normal_command(f, elem, spec)?;
279282
}
@@ -586,6 +589,130 @@ impl Converter {
586589
Ok(())
587590
}
588591

592+
// \color, \textcolor, \colorbox commands
593+
fn convert_command_color(
594+
&mut self,
595+
f: &mut fmt::Formatter<'_>,
596+
cmd: &CmdItem,
597+
spec: &CommandSpec,
598+
) -> Result<(), ConvertError> {
599+
let cmd_name = cmd
600+
.name_tok()
601+
.map(|t| t.text().to_string())
602+
.unwrap_or_default();
603+
let alias = spec
604+
.get_cmd(cmd_name.strip_prefix('\\').unwrap_or(""))
605+
.and_then(|s| s.alias.as_deref())
606+
.unwrap_or("mitexcolor");
607+
608+
// Parse arguments: (model, color, body)
609+
let (model, color, body) = if cmd_name == "\\color" {
610+
let root = cmd.arguments().next().expect("greedy cmd has args");
611+
let mut iter = root.children_with_tokens().peekable();
612+
613+
// Skip trivia
614+
while iter.peek().is_some_and(|e| e.kind().is_trivia()) {
615+
iter.next();
616+
}
617+
618+
// Parse optional [model]
619+
// Check for TokenLBracket directly because in greedy mode,
620+
// optional args like [rgb] are not automatically grouped.
621+
let model = if iter
622+
.peek()
623+
.is_some_and(|e| e.kind() == LatexSyntaxKind::TokenLBracket)
624+
{
625+
iter.next(); // Eat '['
626+
let mut s = String::new();
627+
// Collect text until ']'
628+
// Use by_ref() to keep the iterator alive for subsequent use
629+
for e in iter.by_ref() {
630+
if e.kind() == LatexSyntaxKind::TokenRBracket {
631+
break;
632+
}
633+
match e {
634+
LatexSyntaxElem::Node(n) => s.push_str(&n.text().to_string()),
635+
LatexSyntaxElem::Token(t) => s.push_str(t.text()),
636+
}
637+
}
638+
Some(s)
639+
} else {
640+
None
641+
};
642+
643+
// Skip trivia after [model]
644+
while iter.peek().is_some_and(|e| e.kind().is_trivia()) {
645+
iter.next();
646+
}
647+
(model, iter.next(), iter.collect::<Vec<_>>())
648+
} else {
649+
// \textcolor, \colorbox
650+
let mut args = cmd.arguments();
651+
let first = args.next();
652+
653+
// Check if the first argument is an optional [model]
654+
let is_model = first.as_ref().is_some_and(|n| {
655+
let t = n.text().to_string();
656+
t.starts_with('[') && t.ends_with(']') && t.len() >= 2
657+
});
658+
659+
if is_model {
660+
let text = first.unwrap().text().to_string();
661+
// Remove brackets safely
662+
let model = text.get(1..text.len() - 1).map(String::from);
663+
(
664+
model,
665+
args.next().map(LatexSyntaxElem::Node),
666+
args.map(LatexSyntaxElem::Node).collect::<Vec<_>>(),
667+
)
668+
} else {
669+
(
670+
None,
671+
first.map(LatexSyntaxElem::Node),
672+
args.map(LatexSyntaxElem::Node).collect::<Vec<_>>(),
673+
)
674+
}
675+
};
676+
677+
write!(f, "{}(", alias)?;
678+
679+
// Arg 1: Model
680+
match model {
681+
Some(m) => write!(f, "[{}], ", m)?,
682+
None => f.write_str("none, ")?,
683+
}
684+
685+
// Arg 2: Color
686+
f.write_char('[')?;
687+
if let Some(c) = color {
688+
let prev = self.enter_mode(LaTeXMode::Text);
689+
self.convert(f, c, spec)?;
690+
self.exit_mode(prev);
691+
} else {
692+
f.write_str("black")?;
693+
}
694+
f.write_str("]")?;
695+
f.write_char(')')?;
696+
697+
// Arg 3: Body
698+
f.write_char('[')?;
699+
for elem in body {
700+
if matches!(self.mode, LaTeXMode::Math) {
701+
f.write_char('$')?;
702+
self.convert(f, elem, spec)?;
703+
f.write_char('$')?;
704+
} else {
705+
self.convert(f, elem, spec)?;
706+
}
707+
}
708+
f.write_char(']')?;
709+
if matches!(self.mode, LaTeXMode::Text) {
710+
f.write_char(';')?;
711+
}
712+
713+
Ok(())
714+
}
715+
589716
/// Convert normal command
590717
fn convert_normal_command(
591718
&mut self,

crates/mitex/tests/cvt/arg_match.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use super::prelude::*;
33
#[test]
44
fn curly_group() {
55
assert_snapshot!(convert_math(r#"a \textbf{strong} text"#).unwrap(), @"a #textbf[strong]; t e x t ");
6-
assert_snapshot!(convert_math(r#"x \color {red} yz \frac{1}{2}"#).unwrap(), @"x mitexcolor( r e d , y z frac(1 ,2 ))");
6+
assert_snapshot!(convert_math(r#"x \color {red} yz \frac{1}{2}"#).unwrap(), @"x #mitexcolor(none, [red])[$ $$y z $$frac(1 ,2 )$]");
77
}
88

99
#[test]

crates/mitex/tests/cvt/misc.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,12 @@ fn test_convert_lr() {
123123

124124
#[test]
125125
fn test_convert_color() {
126-
assert_snapshot!(convert_math(r#"$x\color{red}yz\frac{1}{2}$"#).unwrap(), @"x mitexcolor(r e d ,y z frac(1 ,2 ))");
127-
assert_snapshot!(convert_math(r#"$x\textcolor{red}yz$"#).unwrap(), @"x colortext(r e d ,y )z ");
128-
assert_snapshot!(convert_math(r#"$x\textcolor{red}{yz}$"#).unwrap(), @"x colortext(r e d ,y z )");
129-
assert_snapshot!(convert_math(r#"$x\colorbox{red}yz$"#).unwrap(), @"x colorbox(r e d ,y )z "
126+
assert_snapshot!(convert_math(r#"$x\color{red}yz\frac{1}{2}$"#).unwrap(), @"x #mitexcolor(none, [red])[$y z $$frac(1 ,2 )$]");
127+
assert_snapshot!(convert_math(r#"$x\textcolor{red}yz$"#).unwrap(), @"x #colortext(none, [red])[$y $]z");
128+
assert_snapshot!(convert_math(r#"$x\textcolor{red}{yz}$"#).unwrap(), @"x #colortext(none, [red])[$y z $]");
129+
assert_snapshot!(convert_math(r#"$x\colorbox{red}yz$"#).unwrap(), @"x #mitexcolorbox(none, [red])[$y $]z"
130130
);
131-
assert_snapshot!(convert_math(r#"$x\colorbox{red}{yz}$"#).unwrap(), @"x colorbox(r e d ,y z )"
131+
assert_snapshot!(convert_math(r#"$x\colorbox{red}{yz}$"#).unwrap(), @"x #mitexcolorbox(none, [red])[$y z $]"
132132
);
133133
}
134134

@@ -248,7 +248,7 @@ fn test_convert_text() {
248248
assert_snapshot!(convert_math(r#"$\text{ab_c}$"#).unwrap(), @r###"#textmath[ab\_c];"###);
249249
assert_snapshot!(convert_math(r#"$\text{ab^c}$"#).unwrap(), @r###"#textmath[ab\^c];"###);
250250
// note: hack doesn't work in this case
251-
assert_snapshot!(convert_math(r#"$\text{ab\color{red}c}$"#).unwrap(), @"#textmath[abmitexcolor(red,c)];");
251+
assert_snapshot!(convert_math(r#"$\text{ab\color{red}c}$"#).unwrap(), @"#textmath[ab#mitexcolor(none, [red])[c];];");
252252
}
253253

254254
#[test]

packages/mitex/specs/latex/standard.typ

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,42 @@
2929
#let get-tex-color-from-arr(arr) = {
3030
mitex-color-map.at(lower(get-tex-str-from-arr(arr)), default: none)
3131
}
32-
#let get-tex-color(texcolor) = if texcolor.has("children") {
33-
get-tex-color-from-arr(texcolor.children)
34-
} else {
35-
texcolor.text
32+
#let get-tex-color(model, spec) = {
33+
let model = if type(model) == content and model.has("text") {
34+
model.text
35+
} else if type(model) == str {
36+
model
37+
} else {
38+
model
39+
}
40+
41+
let s = if type(spec) == str {
42+
spec
43+
} else if type(spec) == content and spec.has("text") {
44+
spec.text
45+
} else if (
46+
type(spec) == content and spec.has("children")
47+
) {
48+
spec.children.map(it => if it.has("text") { it.text } else { "" }).join("")
49+
} else {
50+
""
51+
}
52+
53+
if model == none {
54+
mitex-color-map.at(lower(s), default: none)
55+
} else if model == "gray" {
56+
luma(float(s) * 100%)
57+
} else if model == "rgb" {
58+
rgb(..s.split(",").map(x => float(x) * 100%))
59+
} else if model == "RGB" {
60+
rgb(..s.split(",").map(x => int(x)))
61+
} else if model == "HTML" {
62+
rgb("#" + s)
63+
} else if model == "cmyk" {
64+
cmyk(..s.split(",").map(x => float(x) * 100%))
65+
} else {
66+
none
67+
}
3668
}
3769

3870
// 1. functions created to make it easier to define a spec
@@ -206,26 +238,26 @@
206238
large: ignore-sym,
207239
tiny: ignore-sym,
208240
// Colors
209-
color: define-greedy-cmd("mitexcolor", handle: (texcolor, ..args) => {
210-
let color = get-tex-color(texcolor)
241+
color: define-greedy-cmd("#mitexcolor", handle: (model, texcolor, ..args) => {
242+
let color = get-tex-color(model, texcolor)
211243
if color != none {
212244
text(fill: color, args.pos().sum())
213245
} else {
214246
args.pos().sum()
215247
}
216248
}),
217-
textcolor: define-cmd(2, alias: "colortext", handle: (texcolor, body) => {
218-
let color = get-tex-color(texcolor)
249+
textcolor: define-glob-cmd("{,b}tt", "#colortext", handle: (model, texcolor, body) => {
250+
let color = get-tex-color(model, texcolor)
219251
if color != none {
220-
text(fill: get-tex-color(texcolor), body)
252+
text(fill: color, body)
221253
} else {
222254
body
223255
}
224256
}),
225-
colorbox: define-cmd(2, handle: (texcolor, body) => {
226-
let color = get-tex-color(texcolor)
257+
colorbox: define-glob-cmd("{,b}tt", "#mitexcolorbox", handle: (model, texcolor, body) => {
258+
let color = get-tex-color(model, texcolor)
227259
if color != none {
228-
box(fill: get-tex-color(texcolor), $body$)
260+
box(fill: color, inset: (x: 3pt), outset: (y: 3pt), radius: 2pt, body)
229261
} else {
230262
body
231263
}

0 commit comments

Comments
 (0)