1use std::borrow::Cow;
34
35use libxml::tree::Node;
36use rustc_hash::FxHashMap as HashMap;
37use winnow::{
38 combinator::{alt, opt, peek, repeat},
39 error::{ContextError, ErrMode},
40 prelude::*,
41 token::{literal, one_of, take_while},
42};
43
44use crate::{
45 common::{
46 arena::SymHashMap,
47 error::{Error, Result},
48 font::Font,
49 store::Stored,
50 },
51 definition::FontDirective,
52 digested::Digested,
53 document::Document,
54};
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum FloatKind {
64 Single,
65 Double,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum Value {
73 Arg(usize),
75 Prop(String),
77 Func { name: String, args: Vec<FuncArg> },
80 Literal(String),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum FuncArg {
87 Value(Value),
88 Str(AttrValue),
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct AttrValue {
94 pub parts: Vec<AttrPart>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum AttrPart {
100 Literal(String),
101 Value(Value),
102 Conditional {
105 test: Value,
106 then_val: Value,
107 else_val: Value,
108 },
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum AttrPair {
115 KeyValue {
116 key: String,
117 value: AttrValue,
118 },
119 Conditional {
120 test: Value,
121 then_attrs: Vec<AttrPair>,
122 else_attrs: Vec<AttrPair>,
123 },
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum ReplacementOp {
129 OpenElement {
132 qname: String,
133 attrs: Vec<AttrPair>,
134 float: Option<FloatKind>,
135 self_closing: bool,
136 },
137 CloseElement { qname: String },
139 ProcessingInstruction { qname: String, attrs: Vec<AttrPair> },
141 AbsorbValue { value: Value },
143 SetAttribute {
146 key: String,
147 value: AttrValue,
148 float: bool,
149 },
150 Text { text: String },
152 Conditional {
154 test: Value,
155 then_ops: Vec<ReplacementOp>,
156 else_ops: Vec<ReplacementOp>,
157 },
158}
159
160pub fn parse_replacement(template: &str) -> Result<Vec<ReplacementOp>> {
165 ops_with_float
166 .parse(template)
167 .map_err(|e| Error::from(format!("replacement template parse error: {e}")))
168}
169
170fn ops_with_float(input: &mut &str) -> ModalResult<Vec<ReplacementOp>> {
175 let float = opt(float_prefix).parse_next(input)?;
176 let mut ops: Vec<ReplacementOp> = repeat(0.., op).parse_next(input)?;
177 if let Some(fk) = float {
178 attach_float(&mut ops, fk);
179 }
180 Ok(ops)
181}
182
183fn attach_float(ops: &mut [ReplacementOp], fk: FloatKind) {
186 for o in ops.iter_mut() {
187 match o {
188 ReplacementOp::OpenElement { float, .. } => {
189 *float = Some(fk);
190 return;
191 },
192 ReplacementOp::SetAttribute { float, .. } => {
193 *float = true;
194 return;
195 },
196 _ => {},
197 }
198 }
199}
200
201fn float_prefix(input: &mut &str) -> ModalResult<FloatKind> {
203 let carets = take_while(1.., '^').parse_next(input)?;
204 ws(input)?;
205 Ok(if carets.chars().count() >= 2 {
206 FloatKind::Double
207 } else {
208 FloatKind::Single
209 })
210}
211
212fn op(input: &mut &str) -> ModalResult<ReplacementOp> {
216 alt((
217 conditional_op,
218 pi_op,
219 open_tag_op,
220 close_tag_op,
221 absorb_value_op,
222 attribute_op,
223 text_op,
224 ))
225 .parse_next(input)
226}
227
228fn conditional_op(input: &mut &str) -> ModalResult<ReplacementOp> {
229 peek((literal("?"), one_of(['#', '&']))).parse_next(input)?;
230 let (test, if_s, else_s) = parse_conditional_raw(input)?;
231 let then_ops = reparse_ops(&if_s)?;
232 let else_ops = reparse_ops(&else_s)?;
233 Ok(ReplacementOp::Conditional { test, then_ops, else_ops })
234}
235
236fn pi_op(input: &mut &str) -> ModalResult<ReplacementOp> {
237 ws(input)?;
238 literal("<?").parse_next(input)?;
239 let name = cut_err_(qname, input)?;
240 let attrs = avpairs(input)?;
241 ws(input)?;
242 cut_err_lit("?>", input)?;
243 Ok(ReplacementOp::ProcessingInstruction { qname: name, attrs })
244}
245
246fn open_tag_op(input: &mut &str) -> ModalResult<ReplacementOp> {
247 ws(input)?;
248 literal("<").parse_next(input)?;
249 let name = qname.parse_next(input)?;
251 let attrs = avpairs(input)?;
252 let self_closing = opt(literal("/")).parse_next(input)?.is_some();
253 cut_err_lit(">", input)?;
254 Ok(ReplacementOp::OpenElement {
255 qname: name,
256 attrs,
257 float: None,
258 self_closing,
259 })
260}
261
262fn close_tag_op(input: &mut &str) -> ModalResult<ReplacementOp> {
263 literal("</").parse_next(input)?;
265 let name = cut_err_(qname, input)?;
266 ws(input)?;
267 cut_err_lit(">", input)?;
268 Ok(ReplacementOp::CloseElement { qname: name })
269}
270
271fn absorb_value_op(input: &mut &str) -> ModalResult<ReplacementOp> {
272 peek(one_of(['#', '&'])).parse_next(input)?;
274 let value = content_value(input)?;
275 Ok(ReplacementOp::AbsorbValue { value })
276}
277
278fn attribute_op(input: &mut &str) -> ModalResult<ReplacementOp> {
279 let key = qname.parse_next(input)?;
281 if opt((ws_p, literal("="), ws_p)).parse_next(input)?.is_none() {
282 return Err(ErrMode::Backtrack(ContextError::new()));
284 }
285 let value = attr_string(input)?;
286 Ok(ReplacementOp::SetAttribute { key, value, float: false })
287}
288
289fn text_op(input: &mut &str) -> ModalResult<ReplacementOp> {
290 let raw = take_literal_run(input, "", true)?;
292 Ok(ReplacementOp::Text { text: unquote(&raw) })
293}
294
295fn parse_conditional_raw(input: &mut &str) -> ModalResult<(Value, String, String)> {
302 literal("?").parse_next(input)?;
303 let test = match content_value(input) {
304 Ok(v) => v,
305 Err(ErrMode::Backtrack(e)) => return Err(ErrMode::Cut(e)),
306 Err(e) => return Err(e),
307 };
308 match opt(bracketed).parse_next(input)? {
309 None => Ok((test, String::new(), String::new())),
310 Some(a) => {
311 let b = opt(bracketed).parse_next(input)?.unwrap_or_default();
312 Ok((test, a, b))
313 },
314 }
315}
316
317fn bracketed(input: &mut &str) -> ModalResult<String> {
322 let s: &str = input;
323 let mut level: i32 = 0;
324 let mut has_open = false;
325 let mut extracted = String::new();
326 let mut consumed = 0usize;
327 let mut closed = false;
328 for (i, c) in s.char_indices() {
329 match c {
330 ')' => {
331 level -= 1;
332 if level < 1 {
333 consumed = i + c.len_utf8();
334 closed = true;
335 break;
336 }
337 extracted.push(c);
338 },
339 '(' => {
340 has_open = true;
341 level += 1;
342 if level > 1 {
343 extracted.push(c);
344 }
345 },
346 other => {
347 if level > 0 {
348 extracted.push(other);
349 } else if !other.is_whitespace() {
350 break; }
352 },
353 }
354 }
355 if has_open && closed {
356 *input = &s[consumed..];
357 Ok(extracted)
358 } else {
359 Err(ErrMode::Backtrack(ContextError::new()))
360 }
361}
362
363fn content_value(input: &mut &str) -> ModalResult<Value> {
366 alt((func_value, arg_value, prop_value)).parse_next(input)
367}
368
369fn full_value(input: &mut &str, exclude: &str) -> ModalResult<Value> {
372 if let Some(v) = opt(content_value).parse_next(input)? {
373 return Ok(v);
374 }
375 let raw = take_literal_run(input, exclude, false)?;
376 Ok(Value::Literal(unquote(&raw)))
377}
378
379fn arg_value(input: &mut &str) -> ModalResult<Value> {
380 literal("#").parse_next(input)?;
381 let digits = take_while(1.., |c: char| c.is_ascii_digit()).parse_next(input)?;
382 let n: usize = digits
383 .parse()
384 .map_err(|_| ErrMode::Backtrack(ContextError::new()))?;
385 if !(1..=9).contains(&n) {
386 return Err(ErrMode::Cut(ContextError::new()));
387 }
388 Ok(Value::Arg(n))
389}
390
391fn prop_value(input: &mut &str) -> ModalResult<Value> {
392 literal("#").parse_next(input)?;
393 let name = take_while(1.., |c: char| c.is_alphanumeric() || c == '_' || c == '-')
394 .map(str::to_string)
395 .parse_next(input)?;
396 Ok(Value::Prop(name))
397}
398
399fn func_value(input: &mut &str) -> ModalResult<Value> {
400 literal("&").parse_next(input)?;
402 let name = take_while(0.., |c: char| c.is_alphanumeric() || c == '_' || c == ':')
403 .map(str::to_string)
404 .parse_next(input)?;
405 literal("(").parse_next(input)?; let mut args = Vec::new();
407 loop {
408 if probe(|i| (ws_p, literal(")")).void().parse_next(i), input) {
409 break;
410 }
411 ws(input)?;
412 let arg = if probe(|i| one_of(['\'', '"']).parse_next(i), input) {
413 FuncArg::Str(attr_string(input)?)
414 } else {
415 FuncArg::Value(full_value(input, ",)")?)
416 };
417 args.push(arg);
418 if opt((ws_p, literal(","), ws_p)).parse_next(input)?.is_none() {
419 break;
420 }
421 }
422 ws(input)?;
423 cut_err_lit(")", input)?;
424 Ok(Value::Func { name, args })
425}
426
427fn attr_string(input: &mut &str) -> ModalResult<AttrValue> {
431 ws(input)?;
432 let quote = match opt(one_of(['\'', '"'])).parse_next(input)? {
433 Some(q) => q,
434 None => {
435 if let Some(c) = input.chars().next() {
436 *input = &input[c.len_utf8()..];
437 }
438 return Ok(AttrValue { parts: Vec::new() });
439 },
440 };
441 let mut parts = Vec::new();
442 loop {
443 if input.is_empty() {
444 break;
445 }
446 if input.starts_with(quote) {
447 *input = &input[quote.len_utf8()..];
448 break;
449 }
450 if probe(
451 |i| (literal("?"), one_of(['#', '&'])).void().parse_next(i),
452 input,
453 ) {
454 let (test, if_s, else_s) = parse_conditional_raw(input)?;
455 let then_val = parse_single_value(&if_s)?;
456 let else_val = parse_single_value(&else_s)?;
457 parts.push(AttrPart::Conditional { test, then_val, else_val });
458 continue;
459 }
460 if probe(|i| one_of(['#', '&']).parse_next(i), input) {
461 parts.push(AttrPart::Value(content_value(input)?));
462 continue;
463 }
464 let raw = take_literal_run(input, "'\"", false)?;
465 parts.push(AttrPart::Literal(unquote(&raw)));
466 }
467 Ok(AttrValue { parts })
468}
469
470fn avpairs(input: &mut &str) -> ModalResult<Vec<AttrPair>> {
473 let mut pairs = Vec::new();
474 loop {
475 ws(input)?;
476 if let Some(c) = opt(avpair_conditional).parse_next(input)? {
477 pairs.push(c);
478 continue;
479 }
480 if let Some(kv) = opt(avpair_keyval).parse_next(input)? {
481 pairs.push(kv);
482 continue;
483 }
484 break;
485 }
486 Ok(pairs)
487}
488
489fn avpair_conditional(input: &mut &str) -> ModalResult<AttrPair> {
490 peek((literal("?"), one_of(['#', '&']))).parse_next(input)?;
491 let (test, if_s, else_s) = parse_conditional_raw(input)?;
492 Ok(AttrPair::Conditional {
493 test,
494 then_attrs: reparse_avpairs(&if_s),
495 else_attrs: reparse_avpairs(&else_s),
496 })
497}
498
499fn avpair_keyval(input: &mut &str) -> ModalResult<AttrPair> {
500 let key = qname.parse_next(input)?;
501 if opt((ws_p, literal("="), ws_p)).parse_next(input)?.is_none() {
502 return Err(ErrMode::Backtrack(ContextError::new()));
503 }
504 let value = attr_string(input)?;
505 Ok(AttrPair::KeyValue { key, value })
506}
507
508fn parse_single_value(s: &str) -> ModalResult<Value> {
512 if s.is_empty() {
513 return Ok(Value::Literal(String::new()));
514 }
515 let mut inp: &str = s;
516 full_value(&mut inp, "")
517}
518
519fn reparse_ops(s: &str) -> ModalResult<Vec<ReplacementOp>> {
522 ops_with_float
523 .parse(s)
524 .map_err(|_| ErrMode::Cut(ContextError::new()))
525}
526
527fn reparse_avpairs(s: &str) -> Vec<AttrPair> {
530 let mut inp: &str = s;
531 avpairs(&mut inp).unwrap_or_default()
532}
533
534fn qname(input: &mut &str) -> ModalResult<String> {
537 (one_of(is_qname_start), take_while(0.., is_qname_continue))
538 .take()
539 .map(str::to_string)
540 .parse_next(input)
541}
542
543fn is_qname_start(c: char) -> bool { c.is_alphabetic() || c == '_' || c == ':' }
544fn is_qname_continue(c: char) -> bool { c.is_alphanumeric() || matches!(c, '_' | ':' | '.' | '-') }
545
546fn ws(input: &mut &str) -> ModalResult<()> {
548 let _ = take_while(0.., |c: char| c.is_whitespace()).parse_next(input)?;
549 Ok(())
550}
551
552fn ws_p(input: &mut &str) -> ModalResult<()> { ws(input) }
554
555fn probe<O>(mut p: impl FnMut(&mut &str) -> ModalResult<O>, input: &str) -> bool {
559 let mut s: &str = input;
560 p(&mut s).is_ok()
561}
562
563fn cut_err_lit(lit: &'static str, input: &mut &str) -> ModalResult<()> {
565 match literal(lit).parse_next(input) {
566 Ok(_) => Ok(()),
567 Err(ErrMode::Backtrack(e)) => Err(ErrMode::Cut(e)),
568 Err(e) => Err(e),
569 }
570}
571
572fn cut_err_<O>(mut p: impl FnMut(&mut &str) -> ModalResult<O>, input: &mut &str) -> ModalResult<O> {
574 match p(input) {
575 Ok(o) => Ok(o),
576 Err(ErrMode::Backtrack(e)) => Err(ErrMode::Cut(e)),
577 Err(e) => Err(e),
578 }
579}
580
581fn take_literal_run(input: &mut &str, exclude: &str, exclude_lt: bool) -> ModalResult<String> {
586 let s: &str = input;
587 let mut pos = 0usize;
588 while pos < s.len() {
589 let rest = &s[pos..];
590 let c = rest.chars().next().unwrap();
591 if c == '&' {
592 if rest.starts_with("&") {
593 pos += 5;
594 continue;
595 }
596 break; }
598 if c == '\\' {
599 let after = &rest[1..];
600 let letters: usize = after
601 .chars()
602 .take_while(|ch| ch.is_ascii_alphabetic() || *ch == '@')
603 .map(char::len_utf8)
604 .sum();
605 if letters > 0 {
606 pos += 1 + letters; continue;
608 }
609 if let Some(nc) = after.chars().next() {
610 pos += 1 + nc.len_utf8(); continue;
612 }
613 break; }
615 if c == '#' || c == '?' {
616 break;
617 }
618 if exclude_lt && c == '<' {
619 break;
620 }
621 if exclude.contains(c) {
622 break;
623 }
624 pos += c.len_utf8();
625 }
626 if pos == 0 {
627 return Err(ErrMode::Backtrack(ContextError::new()));
628 }
629 let raw = s[..pos].to_string();
630 *input = &s[pos..];
631 Ok(raw)
632}
633
634pub fn unquote(text: &str) -> String {
639 const ESCAPED: &[char] = &['#', '?', '(', '&', ',', '<', '>', '\\', '%'];
640 let mut out = String::with_capacity(text.len());
641 let mut i = 0usize;
642 while i < text.len() {
643 let rest = &text[i..];
644 let c = rest.chars().next().unwrap();
645 if c == '\\'
646 && let Some(nc) = rest[1..].chars().next()
647 && ESCAPED.contains(&nc)
648 {
649 i += 1 + nc.len_utf8(); continue;
651 }
652 out.push(c);
653 i += c.len_utf8();
654 }
655 out.replace("##", "#").replace("&", "&")
656}
657
658pub fn slashify(text: &str) -> String { text.replace('\\', "\\\\") }
661
662pub fn apply_ops(
667 ops: &[ReplacementOp],
668 document: &mut Document,
669 args: &[Option<Digested>],
670 props: &SymHashMap<Stored>,
671) -> Result<()> {
672 let mut savenode: Option<Node> = None;
673 exec_ops(ops, document, args, props, &mut savenode)?;
674 if let Some(sn) = savenode {
675 document.set_node(&sn);
676 }
677 Ok(())
678}
679
680fn exec_ops(
681 ops: &[ReplacementOp],
682 document: &mut Document,
683 args: &[Option<Digested>],
684 props: &SymHashMap<Stored>,
685 savenode: &mut Option<Node>,
686) -> Result<()> {
687 for op in ops {
688 match op {
689 ReplacementOp::OpenElement {
690 qname,
691 attrs,
692 float,
693 self_closing,
694 } => {
695 if let Some(fk) = float {
696 *savenode = document.float_to_element(qname, matches!(fk, FloatKind::Double))?;
697 }
698 let av = eval_avpairs(attrs, args, props)?;
699 if av.is_empty() {
700 document.open_element(qname, None, None)?;
701 } else {
702 let mut map: HashMap<String, String> = HashMap::default();
703 for (k, v) in av {
704 map.insert(k, v);
705 }
706 let this_font_opt: Option<Cow<Font>> = match props.get("font") {
707 Some(Stored::Font(f)) => Some(Cow::Borrowed(&**f)),
708 Some(Stored::FontDirective(FontDirective::Asset(fa))) => Some(Cow::Borrowed(&**fa)),
709 Some(Stored::FontDirective(FontDirective::Closure(code))) => {
710 Some(Cow::Owned(code(None)?))
711 },
712 _ => None,
713 };
714 if let Some(this_font) = this_font_opt {
715 document.open_element(qname, Some(map), Some(&this_font))?;
716 } else {
717 document.open_element(qname, Some(map), None)?;
718 }
719 }
720 if *self_closing {
721 document.close_element(qname)?;
722 }
723 },
724 ReplacementOp::CloseElement { qname } => {
725 document.close_element(qname)?;
726 },
727 ReplacementOp::ProcessingInstruction { qname, attrs } => {
728 let av = eval_avpairs(attrs, args, props)?;
729 if av.is_empty() {
730 document.insert_pi(qname, None)?;
731 } else {
732 let mut map: HashMap<String, String> = HashMap::default();
733 for (k, v) in av {
734 map.insert(k, v);
735 }
736 document.insert_pi(qname, Some(map))?;
737 }
738 },
739 ReplacementOp::AbsorbValue { value } => {
740 absorb_value(value, document, args, props)?;
741 },
742 ReplacementOp::SetAttribute { key, value, float } => {
743 let val_str = eval_attr_value(value, args, props)?;
744 if *float {
745 *savenode = document.float_to_attribute(key);
746 let mut node = document.get_node().clone();
747 document.set_attribute(&mut node, key, &val_str)?;
748 if let &mut Some(ref sn) = savenode {
749 document.set_node(sn);
750 }
751 } else {
752 let mut node = document.get_node().clone();
753 document.set_attribute(&mut node, key, &val_str)?;
754 }
755 },
756 ReplacementOp::Text { text } => {
757 document.absorb_string(text, props)?;
758 },
759 ReplacementOp::Conditional { test, then_ops, else_ops } => {
760 if eval_bool(test, args, props)? {
761 exec_ops(then_ops, document, args, props, savenode)?;
762 } else {
763 exec_ops(else_ops, document, args, props, savenode)?;
764 }
765 },
766 }
767 }
768 Ok(())
769}
770
771fn eval_avpairs(
774 attrs: &[AttrPair],
775 args: &[Option<Digested>],
776 props: &SymHashMap<Stored>,
777) -> Result<Vec<(String, String)>> {
778 let mut out = Vec::new();
779 for a in attrs {
780 match a {
781 AttrPair::KeyValue { key, value } => {
782 if key == "font" {
783 continue;
784 }
785 out.push((key.clone(), eval_attr_value(value, args, props)?));
786 },
787 AttrPair::Conditional { test, then_attrs, else_attrs } => {
788 let branch = if eval_bool(test, args, props)? {
789 then_attrs
790 } else {
791 else_attrs
792 };
793 out.extend(eval_avpairs(branch, args, props)?);
794 },
795 }
796 }
797 Ok(out)
798}
799
800fn eval_attr_value(
801 v: &AttrValue,
802 args: &[Option<Digested>],
803 props: &SymHashMap<Stored>,
804) -> Result<String> {
805 let mut s = String::new();
806 for part in &v.parts {
807 match part {
808 AttrPart::Literal(lit) => s.push_str(lit),
809 AttrPart::Value(val) => s.push_str(&value_to_attribute(val, args, props)?),
810 AttrPart::Conditional { test, then_val, else_val } => {
811 let chosen = if eval_bool(test, args, props)? {
812 then_val
813 } else {
814 else_val
815 };
816 s.push_str(&value_to_attribute(chosen, args, props)?);
817 },
818 }
819 }
820 Ok(s)
821}
822
823fn value_to_attribute(
827 v: &Value,
828 args: &[Option<Digested>],
829 props: &SymHashMap<Stored>,
830) -> Result<String> {
831 Ok(match v {
832 Value::Arg(n) => match args.get(n - 1) {
833 Some(Some(d)) => d.to_attribute(),
834 _ => String::new(),
835 },
836 Value::Prop(name) => match props.get(name) {
837 Some(stored) => stored.to_attribute(),
838 None => String::new(),
839 },
840 Value::Func { name, args: fargs } => call_func(name, fargs, args, props)?,
841 Value::Literal(lit) => lit.clone(),
842 })
843}
844
845fn absorb_value(
848 v: &Value,
849 document: &mut Document,
850 args: &[Option<Digested>],
851 props: &SymHashMap<Stored>,
852) -> Result<()> {
853 match v {
854 Value::Arg(n) => {
855 if let Some(Some(d)) = args.get(n - 1) {
856 document.absorb(d, None)?;
857 }
858 },
859 Value::Prop(name) => {
860 if let Some(stored) = props.get(name) {
861 let dig: Option<Digested> = stored.into();
862 if let Some(ref d) = dig {
863 document.absorb(d, None)?;
864 }
865 }
866 },
867 Value::Func { name, args: fargs } => {
868 let s = call_func(name, fargs, args, props)?;
869 if !s.is_empty() {
870 document.absorb_string(&s, props)?;
871 }
872 },
873 Value::Literal(_) => {}, }
875 Ok(())
876}
877
878fn eval_bool(v: &Value, args: &[Option<Digested>], props: &SymHashMap<Stored>) -> Result<bool> {
881 Ok(match v {
882 Value::Arg(n) => match args.get(n - 1) {
883 Some(Some(d)) => is_truthy(&d.to_string()),
884 _ => false,
885 },
886 Value::Prop(name) => match props.get(name) {
887 Some(stored) => is_truthy(&stored.to_string()),
888 None => false,
889 },
890 Value::Func { name, args: fargs } => is_truthy(&call_func(name, fargs, args, props)?),
891 Value::Literal(s) => is_truthy(s),
892 })
893}
894
895fn is_truthy(s: &str) -> bool { !s.is_empty() && s != "false" }
896
897fn call_func(
903 name: &str,
904 fargs: &[FuncArg],
905 args: &[Option<Digested>],
906 props: &SymHashMap<Stored>,
907) -> Result<String> {
908 let mut argv: Vec<String> = Vec::with_capacity(fargs.len());
909 for fa in fargs {
910 argv.push(match fa {
911 FuncArg::Value(v) => value_to_attribute(v, args, props)?,
912 FuncArg::Str(s) => eval_attr_value(s, args, props)?,
913 });
914 }
915 match name {
916 "ToString" => Ok(argv.join("")),
917 "GetKeyVal" => {
922 let kv = argv.first().map(String::as_str).unwrap_or("");
923 let key = argv.get(1).map(String::as_str).unwrap_or("");
924 Ok(
925 crate::keyval::split_keyval_source(kv)
926 .into_iter()
927 .find(|(k, _)| k == key)
928 .map(|(_, v)| v)
929 .unwrap_or_default(),
930 )
931 },
932 _ => Err(Error::from(format!(
933 "runtime template: function &{name}(…) is not in the whitelist"
934 ))),
935 }
936}
937
938#[cfg(test)]
941mod tests {
942 use super::*;
943
944 fn lit(s: &str) -> AttrValue {
945 AttrValue {
946 parts: vec![AttrPart::Literal(s.to_string())],
947 }
948 }
949 fn argval(n: usize) -> AttrValue {
950 AttrValue {
951 parts: vec![AttrPart::Value(Value::Arg(n))],
952 }
953 }
954
955 #[test]
956 fn plain_element_with_arg() {
957 let ops = parse_replacement("<ltx:emph>#1</ltx:emph>").unwrap();
958 assert_eq!(ops, vec![
959 ReplacementOp::OpenElement {
960 qname: "ltx:emph".into(),
961 attrs: vec![],
962 float: None,
963 self_closing: false,
964 },
965 ReplacementOp::AbsorbValue { value: Value::Arg(1) },
966 ReplacementOp::CloseElement { qname: "ltx:emph".into() },
967 ]);
968 }
969
970 #[test]
971 fn element_with_literal_attribute_and_arg() {
972 let ops = parse_replacement("<ltx:text class='ok'>#1</ltx:text>").unwrap();
973 assert_eq!(ops, vec![
974 ReplacementOp::OpenElement {
975 qname: "ltx:text".into(),
976 attrs: vec![AttrPair::KeyValue {
977 key: "class".into(),
978 value: lit("ok"),
979 }],
980 float: None,
981 self_closing: false,
982 },
983 ReplacementOp::AbsorbValue { value: Value::Arg(1) },
984 ReplacementOp::CloseElement { qname: "ltx:text".into() },
985 ]);
986 }
987
988 #[test]
989 fn attribute_value_interpolates_arg() {
990 let ops = parse_replacement("<ltx:ref class='#2'>#1</ltx:ref>").unwrap();
991 let ReplacementOp::OpenElement { attrs, .. } = &ops[0] else {
992 panic!()
993 };
994 assert_eq!(attrs, &vec![AttrPair::KeyValue {
995 key: "class".into(),
996 value: argval(2),
997 }]);
998 }
999
1000 #[test]
1001 fn self_closing_element() {
1002 let ops = parse_replacement("<ltx:break/>").unwrap();
1003 assert_eq!(ops, vec![ReplacementOp::OpenElement {
1004 qname: "ltx:break".into(),
1005 attrs: vec![],
1006 float: None,
1007 self_closing: true,
1008 }]);
1009 }
1010
1011 #[test]
1012 fn whitespace_before_tag_is_dropped_but_text_kept() {
1013 let ops = parse_replacement("<a>\n <b></b>\n </a>").unwrap();
1016 assert_eq!(ops, vec![
1017 ReplacementOp::OpenElement {
1018 qname: "a".into(),
1019 attrs: vec![],
1020 float: None,
1021 self_closing: false,
1022 },
1023 ReplacementOp::OpenElement {
1024 qname: "b".into(),
1025 attrs: vec![],
1026 float: None,
1027 self_closing: false,
1028 },
1029 ReplacementOp::CloseElement { qname: "b".into() },
1030 ReplacementOp::Text { text: "\n ".into() },
1031 ReplacementOp::CloseElement { qname: "a".into() },
1032 ]);
1033 }
1034
1035 #[test]
1036 fn footnote_corpus_specimen() {
1037 let ops = parse_replacement(
1039 "^<ltx:note role='footnote' ?#mark(mark='#mark')()>?#prenote(#prenote )()#2</ltx:note>",
1040 )
1041 .unwrap();
1042 assert_eq!(ops, vec![
1043 ReplacementOp::OpenElement {
1044 qname: "ltx:note".into(),
1045 attrs: vec![
1046 AttrPair::KeyValue {
1047 key: "role".into(),
1048 value: lit("footnote"),
1049 },
1050 AttrPair::Conditional {
1051 test: Value::Prop("mark".into()),
1052 then_attrs: vec![AttrPair::KeyValue {
1053 key: "mark".into(),
1054 value: AttrValue {
1055 parts: vec![AttrPart::Value(Value::Prop("mark".into()))],
1056 },
1057 }],
1058 else_attrs: vec![],
1059 },
1060 ],
1061 float: Some(FloatKind::Single),
1062 self_closing: false,
1063 },
1064 ReplacementOp::Conditional {
1065 test: Value::Prop("prenote".into()),
1066 then_ops: vec![
1067 ReplacementOp::AbsorbValue {
1068 value: Value::Prop("prenote".into()),
1069 },
1070 ReplacementOp::Text { text: " ".into() },
1071 ],
1072 else_ops: vec![],
1073 },
1074 ReplacementOp::AbsorbValue { value: Value::Arg(2) },
1075 ReplacementOp::CloseElement { qname: "ltx:note".into() },
1076 ]);
1077 }
1078
1079 #[test]
1080 fn pi_corpus_specimen() {
1081 let ops = parse_replacement("<?latexml class='#2' ?#1(options='#1')?>").unwrap();
1083 assert_eq!(ops, vec![ReplacementOp::ProcessingInstruction {
1084 qname: "latexml".into(),
1085 attrs: vec![
1086 AttrPair::KeyValue {
1087 key: "class".into(),
1088 value: argval(2),
1089 },
1090 AttrPair::Conditional {
1091 test: Value::Arg(1),
1092 then_attrs: vec![AttrPair::KeyValue {
1093 key: "options".into(),
1094 value: argval(1),
1095 }],
1096 else_attrs: vec![],
1097 },
1098 ],
1099 }]);
1100 }
1101
1102 #[test]
1103 fn float_double_caret() {
1104 let ops = parse_replacement("^^<ltx:x/>").unwrap();
1105 let ReplacementOp::OpenElement { float, .. } = &ops[0] else {
1106 panic!()
1107 };
1108 assert_eq!(float, &Some(FloatKind::Double));
1109 }
1110
1111 #[test]
1112 fn top_level_conditional_with_else() {
1113 let ops = parse_replacement("?#1(<a/>)(<b/>)").unwrap();
1114 assert_eq!(ops, vec![ReplacementOp::Conditional {
1115 test: Value::Arg(1),
1116 then_ops: vec![ReplacementOp::OpenElement {
1117 qname: "a".into(),
1118 attrs: vec![],
1119 float: None,
1120 self_closing: true,
1121 }],
1122 else_ops: vec![ReplacementOp::OpenElement {
1123 qname: "b".into(),
1124 attrs: vec![],
1125 float: None,
1126 self_closing: true,
1127 }],
1128 }]);
1129 }
1130
1131 #[test]
1132 fn prop_hole_at_content_and_arg_distinguished() {
1133 let ops = parse_replacement("#mark#2").unwrap();
1134 assert_eq!(ops, vec![
1135 ReplacementOp::AbsorbValue {
1136 value: Value::Prop("mark".into()),
1137 },
1138 ReplacementOp::AbsorbValue { value: Value::Arg(2) },
1139 ]);
1140 }
1141
1142 #[test]
1143 fn func_value_parses() {
1144 let ops = parse_replacement("<a x='&ToString(#1)'/>").unwrap();
1145 let ReplacementOp::OpenElement { attrs, .. } = &ops[0] else {
1146 panic!()
1147 };
1148 assert_eq!(attrs, &vec![AttrPair::KeyValue {
1149 key: "x".into(),
1150 value: AttrValue {
1151 parts: vec![AttrPart::Value(Value::Func {
1152 name: "ToString".into(),
1153 args: vec![FuncArg::Value(Value::Arg(1))],
1154 })],
1155 },
1156 }]);
1157 }
1158
1159 #[test]
1160 fn unquote_reproduces_original_quirks() {
1161 assert_eq!(unquote("a&b"), "a&b");
1162 assert_eq!(unquote(r"\#"), ""); assert_eq!(unquote(r"\textbf"), r"\textbf"); assert_eq!(unquote("a##b"), "a#b");
1165 }
1166
1167 #[test]
1168 fn empty_template_is_empty_oplist() {
1169 assert_eq!(parse_replacement("").unwrap(), vec![]);
1170 }
1171
1172 use crate::common::arena;
1181
1182 fn dig(s: &str) -> Digested { s.to_string().into() }
1183
1184 fn props_with(pairs: &[(&str, &str)]) -> SymHashMap<Stored> {
1185 let mut m = SymHashMap::default();
1186 for (k, v) in pairs {
1187 m.insert(k, Stored::String(arena::pin(v)));
1188 }
1189 m
1190 }
1191
1192 fn footnote_attrs() -> Vec<AttrPair> {
1194 let ops = parse_replacement(
1195 "^<ltx:note role='footnote' ?#mark(mark='#mark')()>?#prenote(#prenote )()#2</ltx:note>",
1196 )
1197 .unwrap();
1198 match &ops[0] {
1199 ReplacementOp::OpenElement { attrs, .. } => attrs.clone(),
1200 _ => panic!("expected OpenElement"),
1201 }
1202 }
1203
1204 #[test]
1205 fn footnote_conditional_attr_fires_when_prop_present() {
1206 let attrs = footnote_attrs();
1207 let props = props_with(&[("mark", "MK")]);
1208 let av = eval_avpairs(&attrs, &[], &props).unwrap();
1209 let mk = Stored::String(arena::pin("MK")).to_attribute();
1212 assert_eq!(av, vec![
1213 ("role".to_string(), "footnote".to_string()),
1214 ("mark".to_string(), mk)
1215 ]);
1216 }
1217
1218 #[test]
1219 fn footnote_conditional_attr_absent_when_prop_missing() {
1220 let attrs = footnote_attrs();
1221 let av = eval_avpairs(&attrs, &[], &SymHashMap::default()).unwrap();
1222 assert_eq!(av, vec![("role".to_string(), "footnote".to_string())]);
1223 }
1224
1225 #[test]
1226 fn footnote_prenote_condition_truth_test() {
1227 assert!(
1229 eval_bool(
1230 &Value::Prop("prenote".into()),
1231 &[],
1232 &props_with(&[("prenote", "P")])
1233 )
1234 .unwrap()
1235 );
1236 assert!(!eval_bool(&Value::Prop("prenote".into()), &[], &SymHashMap::default()).unwrap());
1237 assert!(
1238 !eval_bool(
1239 &Value::Prop("x".into()),
1240 &[],
1241 &props_with(&[("x", "false")])
1242 )
1243 .unwrap()
1244 );
1245 assert!(!eval_bool(&Value::Prop("x".into()), &[], &props_with(&[("x", "")])).unwrap());
1246 }
1247
1248 #[test]
1249 fn pi_attr_interpolation_uses_to_attribute_and_conditional() {
1250 let ops = parse_replacement("<?latexml class='#2' ?#1(options='#1')?>").unwrap();
1251 let ReplacementOp::ProcessingInstruction { attrs, .. } = &ops[0] else {
1252 panic!()
1253 };
1254
1255 let a1 = dig("opts");
1256 let a2 = dig("article");
1257 let args = vec![Some(a1.clone()), Some(a2.clone())];
1258 let av = eval_avpairs(attrs, &args, &SymHashMap::default()).unwrap();
1259 assert_eq!(av, vec![
1262 ("class".to_string(), a2.to_attribute()),
1263 ("options".to_string(), a1.to_attribute()),
1264 ]);
1265
1266 let av2 = eval_avpairs(attrs, &[None, Some(a2.clone())], &SymHashMap::default()).unwrap();
1268 assert_eq!(av2, vec![("class".to_string(), a2.to_attribute())]);
1269 }
1270
1271 #[test]
1272 fn font_attribute_key_is_dropped() {
1273 let ops = parse_replacement("<ltx:x font='ignored' class='keep'/>").unwrap();
1276 let ReplacementOp::OpenElement { attrs, .. } = &ops[0] else {
1277 panic!()
1278 };
1279 let av = eval_avpairs(attrs, &[], &SymHashMap::default()).unwrap();
1280 assert_eq!(av, vec![("class".to_string(), "keep".to_string())]);
1281 }
1282
1283 #[test]
1284 fn plain_text_only() {
1285 assert_eq!(parse_replacement("hello world").unwrap(), vec![
1286 ReplacementOp::Text { text: "hello world".into() }
1287 ]);
1288 }
1289}