Skip to main content

latexml_core/
keyvals.rs

1use core::slice::Iter;
2use std::{borrow::Cow, fmt};
3
4use libxml::tree::Node;
5use rustc_hash::FxHashMap as HashMap;
6
7use super::keyval::{has_keyval, keyval_get, keyval_qname};
8use crate::{
9  BoxOps, Digested, NO_PROPERTIES,
10  common::{
11    arena::SymHashMap,
12    error::{emit_warn, *},
13    font::Font,
14    object::Object,
15    store::Stored,
16  },
17  definition::argument::ArgWrap,
18  document::Document,
19  gullet::{self, ExpansionLevel},
20  state,
21  token::{Catcode, Token},
22  tokens::Tokens,
23};
24
25#[derive(Debug, Clone)]
26struct KVData {
27  key:            String,
28  value:          Option<ArgWrap>,
29  use_default:    bool,
30  primary_keyset: String,
31  keysets:        Vec<String>,
32  digested_value: Option<Digested>,
33}
34
35#[allow(dead_code)] // TODO: remove when KeyVals is fully implemented
36#[derive(Debug, Clone)]
37pub struct KeyVals {
38  // which KeyVals are we parsing and how do we behave?
39  prefix:               String,
40  /// `keysets should be a list of keysets to find keys inside of.
41  /// It defaults to ["_anonymous_"] if empty.
42  keysets:              Vec<String>,
43  skip:                 Vec<String>,
44  set_all:              bool,
45  set_internals:        bool,
46  skip_missing:         SkipMissing,
47  was_digested:         bool,
48  hook_missing:         Option<Token>,
49  // all the internal representations
50  tuples:               Vec<KVData>,
51  cached_pairs:         Vec<(String, ArgWrap)>,
52  cached_hash:          HashMap<String, Vec<ArgWrap>>,
53  cached_hash_digested: HashMap<String, Vec<Digested>>,
54}
55
56impl Default for KeyVals {
57  fn default() -> Self {
58    KeyVals {
59      prefix:               "KV".to_string(),
60      keysets:              vec!["_anonymous_".to_string()],
61      skip:                 Vec::new(),
62      set_all:              false,
63      set_internals:        false,
64      skip_missing:         SkipMissing::None,
65      was_digested:         false,
66      hook_missing:         None,
67      tuples:               Vec::new(),
68      cached_pairs:         Vec::new(),
69      cached_hash:          HashMap::default(),
70      cached_hash_digested: HashMap::default(),
71    }
72  }
73}
74
75impl PartialEq for KeyVals {
76  fn eq(&self, _other: &KeyVals) -> bool {
77    false // TODO ?
78  }
79}
80
81impl fmt::Display for KeyVals {
82  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83    let mut first = true;
84    for (key, value) in &self.cached_pairs {
85      if !first {
86        // Perl uses comma without space for KeyVals serialization
87        write!(f, ",")?;
88      }
89      write!(f, "{}={}", key, value)?;
90      first = false;
91    }
92    Ok(())
93  }
94}
95
96impl Object for KeyVals {
97  fn stringify(&self) -> String { self.to_string() }
98
99  fn be_digested(mut self) -> Result<Digested> {
100    if self.was_digested {
101      Info!(
102        "ignore",
103        "keyvals",
104        "Skipping digestion of \\setkeys as requested (did you digest a KeyVals twice?) "
105      );
106    } else {
107      crate::stomach::digest(self.set_keys_expansion())?;
108    }
109
110    // iterate over the tuples, digesting the values
111    for tuple in self.tuples.iter_mut() {
112      let KVData {
113        key,
114        value,
115        primary_keyset,
116        digested_value,
117        ..
118      } = tuple;
119      if digested_value.is_none() {
120        // avoid accidental repeats?
121        let keytype_opt = keyval_get(&keyval_qname(&self.prefix, primary_keyset, key), "type");
122        let v = if let Some(Stored::Parameter(keytype)) = keytype_opt {
123          match value.take() {
124            Some(v) => keytype.digest(v, None)?,
125            _ => None,
126          }
127        } else {
128          match value.take() {
129            Some(v) => Some(v.be_digested()?),
130            _ => None,
131          }
132        };
133        tuple.digested_value = v;
134      }
135    }
136    // TODO: DG: KeyVals digestion feels very iffy while porting it over to Rust.
137    // had to add an explicit "rebuild" to cache the digested values in the new
138    // "cached_hash_digested" It feels like the entire object should be reorganized to leverage
139    // a little more of the well-typed capabilities we have here.
140    self.rebuild(None);
141    self.was_digested = true;
142    Ok(self.into())
143  }
144}
145
146impl BoxOps for KeyVals {
147  fn with_properties<R, FnR>(&self, caller: FnR) -> R
148  where FnR: FnOnce(&SymHashMap<Stored>) -> R {
149    caller(&NO_PROPERTIES)
150  }
151  fn get_string(&self) -> Result<Cow<'_, str>> { Ok(Cow::Owned(self.to_string())) }
152  fn set_property<T: Into<Stored>>(&mut self, _key: &str, _value: T) {
153    emit_warn(
154      "internal",
155      "keyvals",
156      "set_property on KeyVals not supported",
157    );
158  }
159  fn be_absorbed(&self, _document: &mut Document) -> Result<Vec<Node>> { Ok(Vec::new()) } // TODO
160  fn get_font(&self) -> Result<Option<std::rc::Rc<Font>>> { Ok(None) } // TODO
161  fn compute_size(
162    &self,
163    _options: SymHashMap<Stored>,
164  ) -> Result<(
165    crate::common::dimension::Dimension,
166    crate::common::dimension::Dimension,
167    crate::common::dimension::Dimension,
168  )> {
169    use crate::common::dimension::Dimension;
170    Ok((
171      Dimension::default(),
172      Dimension::default(),
173      Dimension::default(),
174    ))
175  }
176}
177#[derive(Debug, Clone, Default, PartialEq)]
178pub enum SkipMissing {
179  #[default]
180  /// throw errors
181  None,
182  /// silently ignore all missing keys
183  All,
184  /// store all missing keys under the provided token
185  Store(Token),
186}
187
188#[derive(Default)]
189pub struct KeyvalsConfig {
190  pub prefix:        Option<String>,
191  pub keysets:       Vec<String>,
192  pub set_all:       bool,
193  pub set_internals: bool,
194  pub skip:          Vec<String>,
195  pub skip_missing:  SkipMissing,
196  pub hook_missing:  Option<Token>,
197}
198
199impl KeyVals {
200  ///======================================================================
201  /// The KeyVals constructor
202  ///======================================================================
203  /// This defines the KeyVals data object that can appear in the datastream
204  /// along with tokens, boxes, etc.
205  /// Thus it has to be digestible, however we may not want to digest it more
206  /// than once.
207  ///**********************************************************************
208  pub fn new(options: KeyvalsConfig) -> Self {
209    // parse all the arguments
210    let KeyvalsConfig {
211      prefix,
212      mut keysets,
213      set_all,
214      set_internals,
215      skip,
216      skip_missing,
217      hook_missing,
218    } = options;
219    let prefix = prefix.unwrap_or_else(|| String::from("KV"));
220    // Perl KeyVals.pm #2777 (fdc8bf91, 2026-03-27):
221    // filter empty strings from the keyset list. Split("," , ",pstricks")
222    // (e.g. \pst@famlist accumulates as ",pstricks") yields ["", "pstricks"];
223    // the empty keyset caused keyval_qname("psset","","ArrowInside") to
224    // collide with raw \def\psset@@ArrowInside (a delimited-argument helper)
225    // and emit spurious "Missing argument" errors. Hardening here matches
226    // the Perl fix regardless of how keysets was constructed at the call
227    // site.
228    keysets.retain(|k| !k.is_empty());
229    if keysets.is_empty() {
230      keysets = vec![String::from("_anonymous_")];
231    }
232    KeyVals {
233      prefix,
234      keysets,
235      skip,
236      set_all,
237      set_internals,
238      skip_missing,
239      hook_missing,
240      ..KeyVals::default()
241    }
242  }
243
244  //======================================================================
245  // Resolution to KeySets
246  //======================================================================
247
248  /// Return a list of the keysets in which this key is defined
249  fn resolve_keyval_for(&self, key: &str) -> Vec<String> {
250    let prefix = &self.prefix;
251    let allkeysets = &self.keysets;
252    let keysets: Vec<_> = self
253      .keysets
254      .iter()
255      .filter(|kset| has_keyval(prefix, kset, key))
256      .collect();
257    // throw an error (not really), unless we record the missing macros
258    // Since we're not as obsessive about declaring ALL keys, we'll soften the blow
259    if keysets.is_empty() {
260      if self.skip_missing == SkipMissing::None {
261        // Rate-limit: only emit Info the first time this (prefix, key,
262        // keysets) tuple fires. A large `tabular` with 700 rows can
263        // otherwise produce 700 identical "Encountered unknown KeyVals
264        // key 'vattach'" messages (arxiv 1709.05096), each allocating
265        // a formatted String + going through the log backend. Perl's
266        // Info() has an equivalent deduper in Error.pm via
267        // maxWarnings limits; our rate-limit is per (prefix,key,keysets)
268        // and unbounded in count, so the first occurrence is always
269        // visible but repeats are silently dropped.
270        type SeenSet = rustc_hash::FxHashSet<(String, String, String)>;
271        thread_local! {
272          static SEEN_MISSING: std::cell::RefCell<SeenSet> =
273            std::cell::RefCell::new(SeenSet::default());
274        }
275        let all_joined = allkeysets.join(",");
276        let is_new = SEEN_MISSING.with(|cell| {
277          cell
278            .borrow_mut()
279            .insert((prefix.clone(), key.to_string(), all_joined.clone()))
280        });
281        if is_new {
282          // Intentional divergence from Perl (KeyVals.pm L97 uses Info).
283          // An unknown KeyVal key in `\setkeys` (non-starred) is the
284          // package binding admitting it doesn't recognise an option
285          // the user actually requested — the key's effect (formatting,
286          // rendering options) is silently dropped. For siunitx
287          // specifically this cascades into broken `\SI{}` expansion,
288          // which leaves bare control sequences in math and produces
289          // duplicated xml:id (witness: 1410.8171). Promoted to Warn
290          // so each unique missing key surfaces as a status_code=1
291          // (`[warn]` in the canvas), and a binding gap can't ship
292          // green.
293          //
294          // EXCEPT for the 'Frontmatter' keyset (PR #2767): its design
295          // passes undeclared keys by construction (the engine's own
296          // \lx@add@date uses `name={...}`; class bindings pass through
297          // arbitrary attributes). Perl Infos there; mirror that level
298          // so frontmatter-bearing papers don't all turn status_code=1.
299          if all_joined == "Frontmatter" {
300            Info!(
301              "undefined",
302              "Encountered unknown KeyVals key",
303              s!("'{key}' with prefix '{prefix}' not defined in '{all_joined}'")
304            );
305          } else {
306            Warn!(
307              "undefined",
308              "Encountered unknown KeyVals key",
309              s!(
310                "'{key}' with prefix '{prefix}' not defined in '{all_joined}', were you perhaps using \\setkeys instead of \\setkeys*?"
311              )
312            );
313          }
314        }
315      }
316      return Vec::new();
317    }
318    // return either the first or all of the KeyVal objects
319    // TODO: SymStr would avoid the allocation.
320    if self.set_all {
321      keysets.into_iter().cloned().collect()
322    } else {
323      vec![keysets[0].clone()]
324    }
325  }
326
327  fn can_resolve_keyval_for(&self, key: &str) -> bool {
328    // iterate over the keysets
329    self
330      .keysets
331      .iter()
332      .any(|keyset| has_keyval(&self.prefix, keyset, key))
333  }
334
335  /// Return the 1st of the keysets, or the 1st one of the KeyVals itself
336  fn get_primary_keyval<'a>(&'a self, keysets: &'a [String]) -> &'a str {
337    match keysets.first() {
338      None => self.keysets[0].as_str(),
339      Some(kset) => kset.as_str(),
340    }
341  }
342
343  fn read_keyword_from(&self, close: Token) -> Result<(Tokens, Option<Token>)> {
344    // set of tokens we will expand
345    let mut tokens = Vec::new();
346    let delim = &[close, T_OTHER!(","), T_OTHER!("=")];
347    // skip leading spaces
348    gullet::skip_spaces()?;
349
350    let mut last_token = None;
351    while let Some(token) = gullet::read_x_token(None, false, None)? {
352      // skip to the next iteration if we have a paragraph
353      if token == T_CS!("\\par") {
354        continue;
355      }
356      // if we have one of out delimiters, we end
357      if delim.contains(&token) {
358        last_token = Some(token);
359        break;
360      }
361      tokens.push(token);
362    }
363    // return the tokens and the last token
364    Ok((Tokens::new(tokens), last_token))
365  }
366
367  //======================================================================
368  // Public accessors of all the values
369  //======================================================================
370  // Note: The API of this need to be stable, as people may be using it
371
372  /// return the value of a given key. If multiple values are given, return the last one.
373  pub fn get_value(&self, key: &str) -> Option<&ArgWrap> {
374    // Since we (by default) accumulate lists of values when repeated,
375    // we need to provide the "common" thing: return the last value given.
376    match self.cached_hash.get(key) {
377      None => None,
378      Some(value) => value.last(),
379    }
380  }
381  /// return the digested value of a given key. If multiple values are given, return the last one.
382  /// This call does *not* digest the value, and will return None if called pre-digestion
383  pub fn get_value_digested(&self, key: &str) -> Option<&Digested> {
384    // Since we (by default) accumulate lists of values when repeated,
385    // we need to provide the "common" thing: return the last value given.
386    match self.cached_hash_digested.get(key) {
387      None => None,
388      Some(value) => value.last(),
389    }
390  }
391
392  /// return a list of values for a given key
393  pub fn get_values(&self, key: &str) -> Option<&Vec<ArgWrap>> { self.cached_hash.get(key) }
394
395  /// return the set of key-value pairs
396  pub fn get_pairs(&self) -> Iter<'_, (String, ArgWrap)> { self.cached_pairs.iter() }
397  /// consume KeyVals and return a flat HashMap
398  pub fn as_flat_hash(self) -> HashMap<String, ArgWrap> {
399    let mut flat_hash = HashMap::default();
400    for (k, mut vec) in self.cached_hash {
401      if let Some(v) = vec.pop() {
402        flat_hash.insert(k, v);
403      }
404    }
405    flat_hash
406  }
407  /// consume KeyVals and return the cached HashMap of input values
408  pub fn as_hash(self) -> HashMap<String, Vec<ArgWrap>> { self.cached_hash }
409  /// consume KeyVals and return the cached HashMap of digested values
410  pub fn as_hash_digested(self) -> HashMap<String, Vec<Digested>> { self.cached_hash_digested }
411  /// returns a key => ToString(value)
412  pub fn get_hash(&self) -> HashMap<String, String> {
413    let mut hashed = HashMap::default();
414    for (k, v) in &self.cached_hash {
415      hashed.insert(
416        k.clone(),
417        v.iter()
418          .map(ToString::to_string)
419          .collect::<Vec<String>>()
420          .join(""),
421      );
422    }
423    hashed
424  }
425  /// returns a key => ToString(value)
426  pub fn get_hash_digested(&self) -> HashMap<String, String> {
427    let mut hashed = HashMap::default();
428    for (k, v) in &self.cached_hash_digested {
429      hashed.insert(
430        k.clone(),
431        v.iter()
432          .map(ToString::to_string)
433          .collect::<Vec<String>>()
434          .join(""),
435      );
436    }
437    hashed
438  }
439
440  // return a hash of key-value pairs
441  pub fn get_keyvals(&self) -> &HashMap<String, Vec<ArgWrap>> { &self.cached_hash }
442
443  // checks if the value for a given key exists
444  pub fn has_key(&self, key: &str) -> bool { self.cached_hash.contains_key(key) }
445
446  //======================================================================
447  // Value Related Reversion
448  //======================================================================
449  pub fn set_keys_expansion(&self) -> Tokens {
450    let skip_keys = &self.skip;
451    let set_internals = self.set_internals;
452    let prefix = &self.prefix;
453
454    // Handle skipMissing store token (xkeyval feature)
455    let rmmacro = match &self.skip_missing {
456      SkipMissing::Store(token) => Some(*token),
457      _ => None,
458    };
459    let hook_missing = self.hook_missing;
460
461    // Read existing tokens from rmmacro (if defined and has meaning)
462    let mut rmtokens: Vec<Token> = Vec::new();
463    if let Some(rm) = rmmacro
464      && state::has_meaning(&rm)
465      && let Ok(expanded) = gullet::do_expand(Tokens!(rm))
466    {
467      rmtokens = expanded.unlist();
468    }
469
470    let mut tokens: Vec<Token> = Vec::new();
471
472    // Define xkeyval internals if needed
473    if set_internals {
474      let keysets_joined = self.keysets.join(",");
475      let skip_joined = self.skip.join(",");
476      tokens.push(T_CS!("\\def"));
477      tokens.push(T_CS!("\\XKV@fams"));
478      tokens.push(T_BEGIN!());
479      tokens.extend(Explode!(keysets_joined));
480      tokens.push(T_END!());
481      tokens.push(T_CS!("\\def"));
482      tokens.push(T_CS!("\\XKV@na"));
483      tokens.push(T_BEGIN!());
484      tokens.extend(Explode!(skip_joined));
485      tokens.push(T_END!());
486    }
487
488    // Iterate over key-value pairs
489    for tuple in &self.tuples {
490      let KVData {
491        key,
492        value,
493        use_default,
494        primary_keyset,
495        keysets,
496        ..
497      } = tuple;
498
499      // Skip keys in the skip list
500      if skip_keys.iter().any(|s| s == key) {
501        continue;
502      }
503
504      // If no keysets resolved for this key
505      if keysets.is_empty() {
506        // Store in rmmacro if defined
507        if rmmacro.is_some()
508          && let Ok(rev) = self.revert_keyval(
509            key,
510            primary_keyset,
511            value.as_ref(),
512            *use_default,
513            rmtokens.is_empty(),
514          )
515        {
516          rmtokens.extend(rev);
517        }
518        // Call hookMissing if defined
519        if let Some(hm) = hook_missing
520          && let Ok(rev) =
521            self.revert_keyval(key, primary_keyset, value.as_ref(), *use_default, true)
522        {
523          tokens.push(hm);
524          tokens.push(T_BEGIN!());
525          tokens.extend(rev);
526          tokens.push(T_END!());
527        }
528        continue;
529      }
530
531      // Iterate over all valid keysets
532      for keyset in keysets {
533        let qname = keyval_qname(prefix, keyset, key);
534        if !has_keyval(prefix, keyset, key) {
535          Info!(
536            "undefined",
537            "Encountered unknown KeyVals key",
538            s!("'{key}' with prefix '{prefix}' not defined in '{keyset}'")
539          );
540        } else if matches!(keyval_get(&qname, "disabled"), Some(Stored::Bool(true))) {
541          Warn!("undefined", "keyval", s!("`{key}' has been disabled. "));
542        } else {
543          // Define xkeyval internals per-key if needed
544          if set_internals {
545            tokens.push(T_CS!("\\def"));
546            tokens.push(T_CS!("\\XKV@prefix"));
547            tokens.push(T_BEGIN!());
548            tokens.extend(Explode!(s!("{prefix}@")));
549            tokens.push(T_END!());
550            tokens.push(T_CS!("\\def"));
551            tokens.push(T_CS!("\\XKV@tfam"));
552            tokens.push(T_BEGIN!());
553            tokens.extend(Explode!(keyset));
554            tokens.push(T_END!());
555            tokens.push(T_CS!("\\def"));
556            tokens.push(T_CS!("\\XKV@header"));
557            tokens.push(T_BEGIN!());
558            tokens.extend(Explode!(s!("{prefix}@{keyset}@")));
559            tokens.push(T_END!());
560            tokens.push(T_CS!("\\def"));
561            tokens.push(T_CS!("\\XKV@tkey"));
562            tokens.push(T_BEGIN!());
563            tokens.extend(Explode!(key));
564            tokens.push(T_END!());
565          }
566
567          // Perl: if ($useDefault) { push(@tokens, T_CS('\\' . $qname . '@default')); }
568          //       else { push(@tokens, T_CS('\\' . $qname), T_BEGIN, Revert($value), T_END); }
569          // Note: Perl unconditionally emits \qname@default for bare keys. In Rust, we guard
570          // with has_meaning to avoid undefined-CS errors when @default was never registered
571          // (e.g., xkeyval DeclareOptionX keys without default values).
572          if *use_default && state::has_meaning(&T_CS!(s!("\\{qname}@default"))) {
573            // Call the @default macro (bare key with registered default)
574            tokens.push(T_CS!(s!("\\{qname}@default")));
575          } else {
576            // Call the macro with the value (or empty if bare key without default)
577            tokens.push(T_CS!(s!("\\{qname}")));
578            tokens.push(T_BEGIN!());
579            if let Some(v) = value
580              && let Ok(reverted) = v.revert()
581            {
582              tokens.extend(reverted.unlist());
583            }
584            tokens.push(T_END!());
585          }
586
587          // Reset xkeyval internals per-key
588          if set_internals {
589            tokens.push(T_CS!("\\def"));
590            tokens.push(T_CS!("\\XKV@prefix"));
591            tokens.push(T_BEGIN!());
592            tokens.push(T_END!());
593            tokens.push(T_CS!("\\def"));
594            tokens.push(T_CS!("\\XKV@tfam"));
595            tokens.push(T_BEGIN!());
596            tokens.push(T_END!());
597            tokens.push(T_CS!("\\def"));
598            tokens.push(T_CS!("\\XKV@header"));
599            tokens.push(T_BEGIN!());
600            tokens.push(T_END!());
601            tokens.push(T_CS!("\\def"));
602            tokens.push(T_CS!("\\XKV@tkey"));
603            tokens.push(T_BEGIN!());
604            tokens.push(T_END!());
605          }
606        }
607      }
608    }
609
610    // Assign rmmacro with collected missing keys
611    if let Some(rm) = rmmacro {
612      tokens.push(T_CS!("\\def"));
613      tokens.push(rm);
614      tokens.push(T_BEGIN!());
615      tokens.extend(rmtokens);
616      tokens.push(T_END!());
617    }
618
619    // Reset all internals if applicable
620    if set_internals {
621      tokens.push(T_CS!("\\def"));
622      tokens.push(T_CS!("\\XKV@fams"));
623      tokens.push(T_BEGIN!());
624      tokens.push(T_END!());
625      tokens.push(T_CS!("\\def"));
626      tokens.push(T_CS!("\\XKV@na"));
627      tokens.push(T_BEGIN!());
628      tokens.push(T_END!());
629    }
630
631    Tokens::new(tokens)
632  }
633
634  pub fn revert(&self) -> Result<Tokens> {
635    let mut tokens = Vec::new();
636    // iterate over the key-value pairs
637    for tuple in &self.tuples {
638      let KVData {
639        key,
640        value,
641        use_default,
642        keysets: _,
643        primary_keyset,
644        digested_value: _,
645      } = tuple;
646      if !primary_keyset.is_empty() {
647        let reverted = self.revert_keyval(
648          key,
649          primary_keyset,
650          value.as_ref(),
651          *use_default,
652          tokens.is_empty(),
653        )?;
654        tokens.extend(reverted);
655      }
656    }
657    // and return the list of tokens
658    Ok(Tokens::new(tokens))
659  }
660
661  fn revert_keyval(
662    &self,
663    key: &str,
664    keyset: &str,
665    value_opt: Option<&ArgWrap>,
666    use_default: bool,
667    is_first: bool,
668  ) -> Result<Vec<Token>> {
669    // get the key-value definition
670    let keytype_stored = keyval_get(&keyval_qname(&self.prefix, keyset, key), "type");
671    // define the tokens
672    let mut tokens = Vec::new();
673    // write comma and key, unless in the first iteration
674    if !is_first {
675      tokens.push(T_OTHER!(","));
676    }
677    tokens.extend(Explode!(key));
678    // write the default (if applicable)
679    if !use_default && let Some(value) = value_opt {
680      tokens.push(T_OTHER!("="));
681      let mut reverted_tokens = Vec::new();
682      if let Some(Stored::Parameter(keytype)) = keytype_stored {
683        // TODO: The types here are a little curious. The stored value must be cast back into
684        // Tokens if Parameter's revert works on Tokens. Or should that revert call work on
685        // ArgWrap?
686        if let Some(reverted) = keytype.revert(Some(value.revert()?))? {
687          reverted_tokens.extend(reverted.unlist());
688        }
689      } else {
690        reverted_tokens.extend(value.revert()?.unlist());
691      }
692      tokens.extend(self.rebrace(Tokens::new(reverted_tokens)).unlist());
693    }
694    Ok(tokens)
695  }
696
697  /// When reverting a KeyVals value, we may need to wrap in {}
698  /// eg. if a "," appears outside of any bracing
699  /// Other cases?
700  fn rebrace(&self, tokens: Tokens) -> Tokens {
701    let mut level: i32 = 0;
702    let mut needs_brace = tokens.is_empty();
703    for t in tokens.unlist_ref() {
704      let cc = t.get_catcode();
705      if cc == Catcode::BEGIN {
706        level += 1;
707      }
708      if cc == Catcode::END {
709        level -= 1;
710        // Note that '{ }} {' is still unbalanced
711        // even though the left and right braces match in count.
712        if level < 0 {
713          break;
714        }
715      } else if level <= 0 && cc == Catcode::OTHER && t.with_str(|s| s == ",") {
716        // Outer comma?
717        needs_brace = true;
718        break;
719      }
720    }
721    if needs_brace {
722      let mut wrapped = vec![T_BEGIN!()];
723      wrapped.extend(tokens.unlist());
724      wrapped.push(T_END!());
725      Tokens::new(wrapped)
726    } else {
727      tokens
728    }
729  }
730
731  //======================================================================
732  // Changing contained values
733  //======================================================================
734
735  pub fn add_value(
736    &mut self,
737    key: &str,
738    value_arg: ArgWrap,
739    use_default: bool,
740    no_rebuild: bool,
741  ) -> Result<()> {
742    // figure out the keyset(s) for the key to be added
743    let keysets = self.resolve_keyval_for(key);
744    let primary_keyset = self.get_primary_keyval(keysets.as_slice()).to_owned();
745
746    // and add the new tuple to the set of tuples
747    let value = if use_default {
748      match keyval_get(&keyval_qname(&self.prefix, &primary_keyset, key), "default") {
749        None => Some(ArgWrap::Tokens(Tokens!())), // bare key with no default: empty value
750        Some(v) => {
751          let arg: Result<ArgWrap> = v.into();
752          Some(arg?)
753        },
754      }
755    } else {
756      Some(value_arg)
757    };
758    self.tuples.push(KVData {
759      key: key.to_string(),
760      value,
761      use_default,
762      keysets,
763      primary_keyset,
764      digested_value: None,
765    });
766    // we now need to rebuild, unless we were asked not to
767    // TODO: Maybe only update the last element?
768    if !no_rebuild {
769      self.rebuild(None);
770    }
771    Ok(())
772  }
773
774  pub fn set_value(&mut self, key: &str, value: ArgWrap, use_default: bool) -> Result<()> {
775    // delete the existing values by skipping key
776    self.rebuild(Some(key));
777    // Perl: if (ref $value eq 'ARRAY') { foreach ... addValue(..., 1) } rebuild()
778    //       elsif (defined($value)) { addValue($key, $value, $useDefault) }
779    //       else { just delete (already done by rebuild above) }
780    match &value {
781      ArgWrap::None => {
782        // undef — just delete (already done by rebuild above)
783        Ok(())
784      },
785      _ => {
786        // single value — set normally
787        self.add_value(key, value, use_default, false)
788      },
789    }
790  }
791
792  fn rebuild(&mut self, skip_opt: Option<&str>) {
793    // the new data structures to create
794    let mut newtuples: Vec<KVData> = Vec::new();
795    let mut pairs = Vec::new();
796    let mut hash: HashMap<String, Vec<ArgWrap>> = HashMap::default();
797    let mut hash_digested: HashMap<String, Vec<Digested>> = HashMap::default();
798
799    for tuple in self.tuples.drain(..) {
800      // take all the elements we need from the stack
801      let KVData {
802        key,
803        value,
804        use_default,
805        primary_keyset,
806        keysets,
807        digested_value,
808      } = tuple;
809      // if we want to skip some values, we need to store new tuples
810      let key_str = key.as_str();
811      if let Some(skip) = skip_opt
812        && skip == key_str
813      {
814        continue;
815      }
816      if let Some(v) = value.as_ref() {
817        // push key / value into the pair
818        pairs.push((key.clone(), v.clone()));
819
820        // we always use Vec<ArgWrap> storage, just push the new value in
821        let entry = hash.entry(key.clone()).or_default();
822        entry.push(v.clone());
823      } else if let Some(ref dv) = digested_value {
824        // After digestion, value is taken but digested_value is set.
825        // Populate cached_pairs from the digested value (matching Perl's rebuild behavior).
826        let fallback = ArgWrap::Tokens(dv.revert().unwrap_or_default());
827        pairs.push((key.clone(), fallback.clone()));
828        let entry = hash.entry(key.clone()).or_default();
829        entry.push(fallback);
830      }
831      // if we have a digested value, push that in the Vec<Digested> hash storage
832      if let Some(ref dvalue) = digested_value {
833        let entry = hash_digested.entry(key.clone()).or_default();
834        entry.push(dvalue.clone());
835      }
836
837      // Record.
838      newtuples.push(KVData {
839        key,
840        value,
841        use_default,
842        primary_keyset,
843        keysets,
844        digested_value,
845      });
846    }
847    // store all of the values
848    self.cached_pairs = pairs;
849    self.cached_hash = hash;
850    self.cached_hash_digested = hash_digested;
851    self.tuples = newtuples;
852  }
853
854  //======================================================================
855  // parsing values from a gullet
856  //======================================================================
857
858  // A KeyVal argument MUST be delimited by either braces or brackets (if optional)
859  // This method reads the keyval pairs INCLUDING the delimiters, (rather than
860  // parsing after the fact), since some values may have special catcode needs.
861
862  pub fn read_from(&mut self, until: Token, silence_missing: bool) -> Result<()> {
863    // if we want to force skip_missing keys, we set it up here
864    let skip_missing = self.skip_missing.clone();
865    let hook_missing = self.hook_missing;
866    // if we want to silence all missing errors, store them in a hook
867    if silence_missing {
868      self.skip_missing = SkipMissing::All;
869      self.hook_missing = None;
870    }
871
872    // read the opening token and figure out where we are
873    let startloc = gullet::get_locator();
874    // set and read tokens
875    let _open = gullet::read_token()?;
876
877    let punct_tks = Tokens!(T_OTHER!(","));
878    let until_tks = Tokens!(until);
879    // iterate over all the key-value pairs to read
880    loop {
881      // gobble leading spaces
882      gullet::skip_spaces()?;
883      if gullet::if_next(T_BEGIN!())? {
884        // Protect against redundant {} wrapping
885        gullet::read_token()?;
886        gullet::unread(gullet::read_balanced(ExpansionLevel::Off, false, false)?.strip_braces());
887        gullet::skip_spaces()?;
888      }
889      // Read a single keyword, get a delimiter and a set of keyword tokens
890      let (ktoks, mut delim_opt) = self.read_keyword_from(until)?;
891
892      // if there was no delimiter at the end, we throw an error
893      if delim_opt.is_none() {
894        let message = s!(
895          "Fell off end expecting {} while reading KeyVal key",
896          until.stringify()
897        );
898        let message2 = s!("key started at {}", startloc.to_string());
899        Error!("expected", until, message, message2);
900      }
901
902      // turn the key tokens into a string and trim whitespace
903      let key_str = ktoks.to_string();
904      let key = key_str.trim();
905
906      // if we have a non-empty key
907      if !key.is_empty() {
908        let mut value = ArgWrap::None;
909        // if we have an '=', we explcity assign a value
910        let is_explicit = delim_opt == Some(T_OTHER!("="));
911        if is_explicit {
912          // setup the key-codes to properly read
913          let resolved_kv = self.resolve_keyval_for(key);
914          let keyset = self.get_primary_keyval(&resolved_kv);
915          let keytype_opt = keyval_get(&keyval_qname(&self.prefix, keyset, key), "type");
916          if let Some(Stored::Parameter(ref keytype)) = keytype_opt {
917            keytype.setup_catcodes();
918          }
919          // read until comma
920          let mut toks = Vec::new();
921          loop {
922            // TODO: The types are a bit unnatural here - we need the plural Tokens for read_match,
923            //       but we expect the singular Token as a delimiter result, since we are matching
924            // on a char separator
925            delim_opt = gullet::read_match(&[&punct_tks, &until_tks])?.map(|tks| tks.into());
926            if delim_opt.is_some() {
927              break; // only until we hit a delim.
928            }
929            if let Some(tok) = gullet::read_token()? {
930              // Copy next token to args
931              toks.push(tok);
932              if tok.get_catcode() == Catcode::BEGIN {
933                let balanced_arg = gullet::read_balanced(ExpansionLevel::Off, false, false)?;
934                if !balanced_arg.is_empty() {
935                  toks.extend(balanced_arg.unlist());
936                }
937                toks.push(T_END!());
938              }
939            } else {
940              break;
941            }
942          }
943          // reparse (and expand) the tokens representing the value
944          if !toks.is_empty() {
945            let stripped_toks = Tokens::new(toks).strip_braces_n(2);
946            if !stripped_toks.is_empty() {
947              if let Some(Stored::Parameter(ref keytype)) = keytype_opt {
948                value = keytype.reparse(stripped_toks)?;
949              } else {
950                value = ArgWrap::Tokens(stripped_toks);
951              }
952            }
953          }
954          // An explicit `=` always assigns a value, even when it is empty
955          // (`key=` or `key={}`): that is an EXPLICIT empty override, distinct
956          // from a missing key. Keep it as empty Tokens rather than the
957          // `ArgWrap::None` the value was initialised to — `None` is reserved
958          // for a missing key and its Display is the literal string "None",
959          // which leaks into consumers that stringify the value. Concretely, a
960          // starred matrix with no alignment bracket emits `alignment=` (empty);
961          // without this the keyval value was "None", so `\lx@gen@matrix@bindings`
962          // saw `alignment="None"` instead of defaulting to "c", producing a
963          // malformed column alignment that made a `\dots` cell swallow the next
964          // `&` → "Stray alignment". Witness 1910.00678.
965          if value.is_none() {
966            value = ArgWrap::Tokens(Tokens!());
967          }
968          // and cleanup
969          if let Some(Stored::Parameter(ref keydef)) = keytype_opt {
970            keydef.revert_catcodes()?;
971          }
972        }
973        // and store our value please
974        if !silence_missing || self.can_resolve_keyval_for(key) {
975          self.add_value(key, value, !is_explicit, false)?;
976        }
977      }
978
979      // we finish if we have the last element
980      if delim_opt.as_ref() == Some(&until) {
981        break;
982      }
983    }
984
985    // rebuild and return nothing
986    self.rebuild(None);
987
988    // restore all settings if we silenced the missing keys
989    if silence_missing {
990      self.skip_missing = skip_missing;
991      self.hook_missing = hook_missing;
992    }
993    Ok(())
994  }
995
996  /// TODO: This is an improvised method for switching KeyVals into Tokens, but losing all collected
997  /// metadata.
998  /// The long-term solution ought to be via a type system extension, where the
999  /// arguments to our before-digest closures are a vector of a new type
1000  /// ReadValue ::= [Token, KeyVals, RegisterValue]       potentially?
1001  /// On the other hand, we can also put the
1002  /// extra effort of *postponing* the build of KV metadata until digestion,
1003  /// this way not losing any time reserializing metadata
1004  pub fn into_tokens(self) -> Result<Tokens> {
1005    let mut tks: Vec<Token> = Vec::new();
1006    for (k, v) in self.cached_pairs.into_iter() {
1007      tks.push(T_OTHER!(k));
1008      match v {
1009        ArgWrap::Tokens(vtks) => {
1010          let expanded = gullet::do_expand(vtks)?;
1011          let mut exp_str = expanded.to_string();
1012          if exp_str == "{}" {
1013            exp_str = String::new();
1014          }
1015          tks.push(T_OTHER!(exp_str));
1016        },
1017        ArgWrap::Token(vtk) => tks.push(vtk),
1018        other => {
1019          emit_warn(
1020            "internal",
1021            "keyvals",
1022            &format!("Unexpected ArgWrap variant in KeyVals revert: {other:?}"),
1023          );
1024        },
1025      }
1026    }
1027    Ok(Tokens::new(tks))
1028  }
1029}
1030
1031impl From<KeyVals> for Result<Option<Digested>> {
1032  fn from(value: KeyVals) -> Result<Option<Digested>> {
1033    let tmp: Digested = value.into();
1034    tmp.into()
1035  }
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040  use super::*;
1041
1042  #[test]
1043  fn skip_missing_default_is_none() {
1044    let s = SkipMissing::default();
1045    assert_eq!(s, SkipMissing::None);
1046  }
1047
1048  #[test]
1049  fn skip_missing_variants_not_equal() {
1050    assert_ne!(SkipMissing::None, SkipMissing::All);
1051  }
1052
1053  #[test]
1054  fn keyvals_config_default_all_empty() {
1055    let c = KeyvalsConfig::default();
1056    assert!(c.prefix.is_none());
1057    assert!(c.keysets.is_empty());
1058    assert!(!c.set_all);
1059    assert!(!c.set_internals);
1060    assert!(c.skip.is_empty());
1061    assert_eq!(c.skip_missing, SkipMissing::None);
1062    assert!(c.hook_missing.is_none());
1063  }
1064
1065  #[test]
1066  fn keyvals_default_prefix_and_anonymous_keyset() {
1067    // Default KeyVals has prefix=KV, keysets=["_anonymous_"].
1068    let kv = KeyVals::default();
1069    assert_eq!(kv.prefix, "KV");
1070    assert_eq!(kv.keysets, vec!["_anonymous_".to_string()]);
1071    assert!(!kv.set_all);
1072    assert!(!kv.set_internals);
1073  }
1074
1075  #[test]
1076  fn keyvals_new_with_empty_keysets_defaults_to_anonymous() {
1077    let kv = KeyVals::new(KeyvalsConfig::default());
1078    assert_eq!(kv.keysets, vec!["_anonymous_".to_string()]);
1079  }
1080
1081  #[test]
1082  fn keyvals_new_with_custom_keysets_preserved() {
1083    let cfg = KeyvalsConfig {
1084      keysets: vec!["tabular".to_string(), "array".to_string()],
1085      ..KeyvalsConfig::default()
1086    };
1087    let kv = KeyVals::new(cfg);
1088    assert_eq!(kv.keysets.len(), 2);
1089    assert_eq!(kv.keysets[0], "tabular");
1090  }
1091
1092  #[test]
1093  fn keyvals_new_custom_prefix() {
1094    let cfg = KeyvalsConfig {
1095      prefix: Some("P".to_string()),
1096      ..KeyvalsConfig::default()
1097    };
1098    let kv = KeyVals::new(cfg);
1099    assert_eq!(kv.prefix, "P");
1100  }
1101
1102  #[test]
1103  fn keyvals_new_default_prefix_on_none() {
1104    let cfg = KeyvalsConfig {
1105      prefix: None,
1106      ..KeyvalsConfig::default()
1107    };
1108    let kv = KeyVals::new(cfg);
1109    assert_eq!(kv.prefix, "KV");
1110  }
1111
1112  #[test]
1113  fn keyvals_new_set_all_flag() {
1114    let cfg = KeyvalsConfig {
1115      set_all: true,
1116      ..KeyvalsConfig::default()
1117    };
1118    let kv = KeyVals::new(cfg);
1119    assert!(kv.set_all);
1120  }
1121
1122  #[test]
1123  fn keyvals_new_filters_empty_keysets() {
1124    // Perl KeyVals.pm #2777 (fdc8bf91): \pst@famlist accumulates as
1125    // ",pstricks"; a naive split yields ["", "pstricks"]. The empty
1126    // entry would collide with `\def\psset@@ArrowInside` via the
1127    // keyval_qname("psset","","ArrowInside") → "psset@@ArrowInside"
1128    // path. Empty entries must be filtered before any default fallback.
1129    let cfg = KeyvalsConfig {
1130      keysets: vec!["".to_string(), "pstricks".to_string()],
1131      ..KeyvalsConfig::default()
1132    };
1133    let kv = KeyVals::new(cfg);
1134    assert_eq!(kv.keysets, vec!["pstricks".to_string()]);
1135  }
1136
1137  #[test]
1138  fn keyvals_new_all_empty_keysets_defaults_to_anonymous() {
1139    // If every keyset entry is empty, we still fall back to
1140    // _anonymous_ (not retain an empty keyset).
1141    let cfg = KeyvalsConfig {
1142      keysets: vec!["".to_string(), "".to_string()],
1143      ..KeyvalsConfig::default()
1144    };
1145    let kv = KeyVals::new(cfg);
1146    assert_eq!(kv.keysets, vec!["_anonymous_".to_string()]);
1147  }
1148}