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        // Post-digestion `be_digested` has `take()`n the raw `value`, leaving
648        // the value only in `digested_value`; fall back to reverting that, so a
649        // reverted KeyVals keeps `key=value` instead of collapsing to a bare
650        // `key` (issue #627). Mirrors `rebuild`'s digested-value fallback.
651        // `strip_braces`: the digested value reverts with its own `{…}` wrapper,
652        // but `revert_keyval`/`rebrace` re-add braces only when the value has an
653        // outer comma — so strip one level first to match Perl's
654        // `rebrace(Revert($value))` (no braces for a plain value).
655        let digested_fallback = match (value, digested_value) {
656          (None, Some(dv)) => Some(ArgWrap::Tokens(dv.revert()?.strip_braces())),
657          _ => None,
658        };
659        let reverted = self.revert_keyval(
660          key,
661          primary_keyset,
662          value.as_ref().or(digested_fallback.as_ref()),
663          *use_default,
664          tokens.is_empty(),
665        )?;
666        tokens.extend(reverted);
667      }
668    }
669    // and return the list of tokens
670    Ok(Tokens::new(tokens))
671  }
672
673  fn revert_keyval(
674    &self,
675    key: &str,
676    keyset: &str,
677    value_opt: Option<&ArgWrap>,
678    use_default: bool,
679    is_first: bool,
680  ) -> Result<Vec<Token>> {
681    // get the key-value definition
682    let keytype_stored = keyval_get(&keyval_qname(&self.prefix, keyset, key), "type");
683    // define the tokens
684    let mut tokens = Vec::new();
685    // write comma and key, unless in the first iteration
686    if !is_first {
687      tokens.push(T_OTHER!(","));
688    }
689    tokens.extend(Explode!(key));
690    // write the default (if applicable)
691    if !use_default && let Some(value) = value_opt {
692      tokens.push(T_OTHER!("="));
693      let mut reverted_tokens = Vec::new();
694      if let Some(Stored::Parameter(keytype)) = keytype_stored {
695        // TODO: The types here are a little curious. The stored value must be cast back into
696        // Tokens if Parameter's revert works on Tokens. Or should that revert call work on
697        // ArgWrap?
698        if let Some(reverted) = keytype.revert(Some(value.revert()?))? {
699          reverted_tokens.extend(reverted.unlist());
700        }
701      } else {
702        reverted_tokens.extend(value.revert()?.unlist());
703      }
704      tokens.extend(self.rebrace(Tokens::new(reverted_tokens)).unlist());
705    }
706    Ok(tokens)
707  }
708
709  /// When reverting a KeyVals value, we may need to wrap in {}
710  /// eg. if a "," appears outside of any bracing
711  /// Other cases?
712  fn rebrace(&self, tokens: Tokens) -> Tokens {
713    let mut level: i32 = 0;
714    let mut needs_brace = tokens.is_empty();
715    for t in tokens.unlist_ref() {
716      let cc = t.get_catcode();
717      if cc == Catcode::BEGIN {
718        level += 1;
719      }
720      if cc == Catcode::END {
721        level -= 1;
722        // Note that '{ }} {' is still unbalanced
723        // even though the left and right braces match in count.
724        if level < 0 {
725          break;
726        }
727      } else if level <= 0 && cc == Catcode::OTHER && t.with_str(|s| s == ",") {
728        // Outer comma?
729        needs_brace = true;
730        break;
731      }
732    }
733    if needs_brace {
734      let mut wrapped = vec![T_BEGIN!()];
735      wrapped.extend(tokens.unlist());
736      wrapped.push(T_END!());
737      Tokens::new(wrapped)
738    } else {
739      tokens
740    }
741  }
742
743  //======================================================================
744  // Changing contained values
745  //======================================================================
746
747  pub fn add_value(
748    &mut self,
749    key: &str,
750    value_arg: ArgWrap,
751    use_default: bool,
752    no_rebuild: bool,
753  ) -> Result<()> {
754    // figure out the keyset(s) for the key to be added
755    let keysets = self.resolve_keyval_for(key);
756    let primary_keyset = self.get_primary_keyval(keysets.as_slice()).to_owned();
757
758    // and add the new tuple to the set of tuples
759    let value = if use_default {
760      match keyval_get(&keyval_qname(&self.prefix, &primary_keyset, key), "default") {
761        None => Some(ArgWrap::Tokens(Tokens!())), // bare key with no default: empty value
762        Some(v) => {
763          let arg: Result<ArgWrap> = v.into();
764          Some(arg?)
765        },
766      }
767    } else {
768      Some(value_arg)
769    };
770    self.tuples.push(KVData {
771      key: key.to_string(),
772      value,
773      use_default,
774      keysets,
775      primary_keyset,
776      digested_value: None,
777    });
778    // we now need to rebuild, unless we were asked not to
779    // TODO: Maybe only update the last element?
780    if !no_rebuild {
781      self.rebuild(None);
782    }
783    Ok(())
784  }
785
786  pub fn set_value(&mut self, key: &str, value: ArgWrap, use_default: bool) -> Result<()> {
787    // delete the existing values by skipping key
788    self.rebuild(Some(key));
789    // Perl: if (ref $value eq 'ARRAY') { foreach ... addValue(..., 1) } rebuild()
790    //       elsif (defined($value)) { addValue($key, $value, $useDefault) }
791    //       else { just delete (already done by rebuild above) }
792    match &value {
793      ArgWrap::None => {
794        // undef — just delete (already done by rebuild above)
795        Ok(())
796      },
797      _ => {
798        // single value — set normally
799        self.add_value(key, value, use_default, false)
800      },
801    }
802  }
803
804  fn rebuild(&mut self, skip_opt: Option<&str>) {
805    // the new data structures to create
806    let mut newtuples: Vec<KVData> = Vec::new();
807    let mut pairs = Vec::new();
808    let mut hash: HashMap<String, Vec<ArgWrap>> = HashMap::default();
809    let mut hash_digested: HashMap<String, Vec<Digested>> = HashMap::default();
810
811    for tuple in self.tuples.drain(..) {
812      // take all the elements we need from the stack
813      let KVData {
814        key,
815        value,
816        use_default,
817        primary_keyset,
818        keysets,
819        digested_value,
820      } = tuple;
821      // if we want to skip some values, we need to store new tuples
822      let key_str = key.as_str();
823      if let Some(skip) = skip_opt
824        && skip == key_str
825      {
826        continue;
827      }
828      if let Some(v) = value.as_ref() {
829        // push key / value into the pair
830        pairs.push((key.clone(), v.clone()));
831
832        // we always use Vec<ArgWrap> storage, just push the new value in
833        let entry = hash.entry(key.clone()).or_default();
834        entry.push(v.clone());
835      } else if let Some(ref dv) = digested_value {
836        // After digestion, value is taken but digested_value is set.
837        // Populate cached_pairs from the digested value (matching Perl's rebuild behavior).
838        let fallback = ArgWrap::Tokens(dv.revert().unwrap_or_default());
839        pairs.push((key.clone(), fallback.clone()));
840        let entry = hash.entry(key.clone()).or_default();
841        entry.push(fallback);
842      }
843      // if we have a digested value, push that in the Vec<Digested> hash storage
844      if let Some(ref dvalue) = digested_value {
845        let entry = hash_digested.entry(key.clone()).or_default();
846        entry.push(dvalue.clone());
847      }
848
849      // Record.
850      newtuples.push(KVData {
851        key,
852        value,
853        use_default,
854        primary_keyset,
855        keysets,
856        digested_value,
857      });
858    }
859    // store all of the values
860    self.cached_pairs = pairs;
861    self.cached_hash = hash;
862    self.cached_hash_digested = hash_digested;
863    self.tuples = newtuples;
864  }
865
866  //======================================================================
867  // parsing values from a gullet
868  //======================================================================
869
870  // A KeyVal argument MUST be delimited by either braces or brackets (if optional)
871  // This method reads the keyval pairs INCLUDING the delimiters, (rather than
872  // parsing after the fact), since some values may have special catcode needs.
873
874  pub fn read_from(&mut self, until: Token, silence_missing: bool) -> Result<()> {
875    // if we want to force skip_missing keys, we set it up here
876    let skip_missing = self.skip_missing.clone();
877    let hook_missing = self.hook_missing;
878    // if we want to silence all missing errors, store them in a hook
879    if silence_missing {
880      self.skip_missing = SkipMissing::All;
881      self.hook_missing = None;
882    }
883
884    // read the opening token and figure out where we are
885    let startloc = gullet::get_locator();
886    // set and read tokens
887    let _open = gullet::read_token()?;
888
889    let punct_tks = Tokens!(T_OTHER!(","));
890    let until_tks = Tokens!(until);
891    // iterate over all the key-value pairs to read
892    loop {
893      // gobble leading spaces
894      gullet::skip_spaces()?;
895      if gullet::if_next(T_BEGIN!())? {
896        // Protect against redundant {} wrapping
897        gullet::read_token()?;
898        gullet::unread(gullet::read_balanced(ExpansionLevel::Off, false, false)?.strip_braces());
899        gullet::skip_spaces()?;
900      }
901      // Read a single keyword, get a delimiter and a set of keyword tokens
902      let (ktoks, mut delim_opt) = self.read_keyword_from(until)?;
903
904      // if there was no delimiter at the end, we throw an error
905      if delim_opt.is_none() {
906        let message = s!(
907          "Fell off end expecting {} while reading KeyVal key",
908          until.stringify()
909        );
910        let message2 = s!("key started at {}", startloc.to_string());
911        Error!("expected", until, message, message2);
912      }
913
914      // turn the key tokens into a string and trim whitespace
915      let key_str = ktoks.to_string();
916      let key = key_str.trim();
917
918      // if we have a non-empty key
919      if !key.is_empty() {
920        let mut value = ArgWrap::None;
921        // if we have an '=', we explcity assign a value
922        let is_explicit = delim_opt == Some(T_OTHER!("="));
923        if is_explicit {
924          // setup the key-codes to properly read
925          let resolved_kv = self.resolve_keyval_for(key);
926          let keyset = self.get_primary_keyval(&resolved_kv);
927          let keytype_opt = keyval_get(&keyval_qname(&self.prefix, keyset, key), "type");
928          if let Some(Stored::Parameter(ref keytype)) = keytype_opt {
929            keytype.setup_catcodes();
930          }
931          // read until comma
932          let mut toks = Vec::new();
933          loop {
934            // TODO: The types are a bit unnatural here - we need the plural Tokens for read_match,
935            //       but we expect the singular Token as a delimiter result, since we are matching
936            // on a char separator
937            delim_opt = gullet::read_match(&[&punct_tks, &until_tks])?.map(|tks| tks.into());
938            if delim_opt.is_some() {
939              break; // only until we hit a delim.
940            }
941            if let Some(tok) = gullet::read_token()? {
942              // Copy next token to args
943              toks.push(tok);
944              if tok.get_catcode() == Catcode::BEGIN {
945                let balanced_arg = gullet::read_balanced(ExpansionLevel::Off, false, false)?;
946                if !balanced_arg.is_empty() {
947                  toks.extend(balanced_arg.unlist());
948                }
949                toks.push(T_END!());
950              }
951            } else {
952              break;
953            }
954          }
955          // reparse (and expand) the tokens representing the value
956          if !toks.is_empty() {
957            let stripped_toks = Tokens::new(toks).strip_braces_n(2);
958            if !stripped_toks.is_empty() {
959              if let Some(Stored::Parameter(ref keytype)) = keytype_opt {
960                value = keytype.reparse(stripped_toks)?;
961              } else {
962                value = ArgWrap::Tokens(stripped_toks);
963              }
964            }
965          }
966          // An explicit `=` always assigns a value, even when it is empty
967          // (`key=` or `key={}`): that is an EXPLICIT empty override, distinct
968          // from a missing key. Keep it as empty Tokens rather than the
969          // `ArgWrap::None` the value was initialised to — `None` is reserved
970          // for a missing key and its Display is the literal string "None",
971          // which leaks into consumers that stringify the value. Concretely, a
972          // starred matrix with no alignment bracket emits `alignment=` (empty);
973          // without this the keyval value was "None", so `\lx@gen@matrix@bindings`
974          // saw `alignment="None"` instead of defaulting to "c", producing a
975          // malformed column alignment that made a `\dots` cell swallow the next
976          // `&` → "Stray alignment". Witness 1910.00678.
977          if value.is_none() {
978            value = ArgWrap::Tokens(Tokens!());
979          }
980          // and cleanup
981          if let Some(Stored::Parameter(ref keydef)) = keytype_opt {
982            keydef.revert_catcodes()?;
983          }
984        }
985        // and store our value please
986        if !silence_missing || self.can_resolve_keyval_for(key) {
987          self.add_value(key, value, !is_explicit, false)?;
988        }
989      }
990
991      // we finish if we have the last element
992      if delim_opt.as_ref() == Some(&until) {
993        break;
994      }
995    }
996
997    // rebuild and return nothing
998    self.rebuild(None);
999
1000    // restore all settings if we silenced the missing keys
1001    if silence_missing {
1002      self.skip_missing = skip_missing;
1003      self.hook_missing = hook_missing;
1004    }
1005    Ok(())
1006  }
1007
1008  /// TODO: This is an improvised method for switching KeyVals into Tokens, but losing all collected
1009  /// metadata.
1010  /// The long-term solution ought to be via a type system extension, where the
1011  /// arguments to our before-digest closures are a vector of a new type
1012  /// ReadValue ::= [Token, KeyVals, RegisterValue]       potentially?
1013  /// On the other hand, we can also put the
1014  /// extra effort of *postponing* the build of KV metadata until digestion,
1015  /// this way not losing any time reserializing metadata
1016  pub fn into_tokens(self) -> Result<Tokens> {
1017    let mut tks: Vec<Token> = Vec::new();
1018    for (k, v) in self.cached_pairs.into_iter() {
1019      tks.push(T_OTHER!(k));
1020      match v {
1021        ArgWrap::Tokens(vtks) => {
1022          let expanded = gullet::do_expand(vtks)?;
1023          let mut exp_str = expanded.to_string();
1024          if exp_str == "{}" {
1025            exp_str = String::new();
1026          }
1027          tks.push(T_OTHER!(exp_str));
1028        },
1029        ArgWrap::Token(vtk) => tks.push(vtk),
1030        other => {
1031          emit_warn(
1032            "internal",
1033            "keyvals",
1034            &format!("Unexpected ArgWrap variant in KeyVals revert: {other:?}"),
1035          );
1036        },
1037      }
1038    }
1039    Ok(Tokens::new(tks))
1040  }
1041}
1042
1043impl From<KeyVals> for Result<Option<Digested>> {
1044  fn from(value: KeyVals) -> Result<Option<Digested>> {
1045    let tmp: Digested = value.into();
1046    tmp.into()
1047  }
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052  use super::*;
1053
1054  #[test]
1055  fn skip_missing_default_is_none() {
1056    let s = SkipMissing::default();
1057    assert_eq!(s, SkipMissing::None);
1058  }
1059
1060  #[test]
1061  fn skip_missing_variants_not_equal() {
1062    assert_ne!(SkipMissing::None, SkipMissing::All);
1063  }
1064
1065  #[test]
1066  fn keyvals_config_default_all_empty() {
1067    let c = KeyvalsConfig::default();
1068    assert!(c.prefix.is_none());
1069    assert!(c.keysets.is_empty());
1070    assert!(!c.set_all);
1071    assert!(!c.set_internals);
1072    assert!(c.skip.is_empty());
1073    assert_eq!(c.skip_missing, SkipMissing::None);
1074    assert!(c.hook_missing.is_none());
1075  }
1076
1077  #[test]
1078  fn keyvals_default_prefix_and_anonymous_keyset() {
1079    // Default KeyVals has prefix=KV, keysets=["_anonymous_"].
1080    let kv = KeyVals::default();
1081    assert_eq!(kv.prefix, "KV");
1082    assert_eq!(kv.keysets, vec!["_anonymous_".to_string()]);
1083    assert!(!kv.set_all);
1084    assert!(!kv.set_internals);
1085  }
1086
1087  #[test]
1088  fn keyvals_new_with_empty_keysets_defaults_to_anonymous() {
1089    let kv = KeyVals::new(KeyvalsConfig::default());
1090    assert_eq!(kv.keysets, vec!["_anonymous_".to_string()]);
1091  }
1092
1093  #[test]
1094  fn keyvals_new_with_custom_keysets_preserved() {
1095    let cfg = KeyvalsConfig {
1096      keysets: vec!["tabular".to_string(), "array".to_string()],
1097      ..KeyvalsConfig::default()
1098    };
1099    let kv = KeyVals::new(cfg);
1100    assert_eq!(kv.keysets.len(), 2);
1101    assert_eq!(kv.keysets[0], "tabular");
1102  }
1103
1104  #[test]
1105  fn keyvals_new_custom_prefix() {
1106    let cfg = KeyvalsConfig {
1107      prefix: Some("P".to_string()),
1108      ..KeyvalsConfig::default()
1109    };
1110    let kv = KeyVals::new(cfg);
1111    assert_eq!(kv.prefix, "P");
1112  }
1113
1114  #[test]
1115  fn keyvals_new_default_prefix_on_none() {
1116    let cfg = KeyvalsConfig {
1117      prefix: None,
1118      ..KeyvalsConfig::default()
1119    };
1120    let kv = KeyVals::new(cfg);
1121    assert_eq!(kv.prefix, "KV");
1122  }
1123
1124  #[test]
1125  fn keyvals_new_set_all_flag() {
1126    let cfg = KeyvalsConfig {
1127      set_all: true,
1128      ..KeyvalsConfig::default()
1129    };
1130    let kv = KeyVals::new(cfg);
1131    assert!(kv.set_all);
1132  }
1133
1134  #[test]
1135  fn keyvals_new_filters_empty_keysets() {
1136    // Perl KeyVals.pm #2777 (fdc8bf91): \pst@famlist accumulates as
1137    // ",pstricks"; a naive split yields ["", "pstricks"]. The empty
1138    // entry would collide with `\def\psset@@ArrowInside` via the
1139    // keyval_qname("psset","","ArrowInside") → "psset@@ArrowInside"
1140    // path. Empty entries must be filtered before any default fallback.
1141    let cfg = KeyvalsConfig {
1142      keysets: vec!["".to_string(), "pstricks".to_string()],
1143      ..KeyvalsConfig::default()
1144    };
1145    let kv = KeyVals::new(cfg);
1146    assert_eq!(kv.keysets, vec!["pstricks".to_string()]);
1147  }
1148
1149  #[test]
1150  fn keyvals_new_all_empty_keysets_defaults_to_anonymous() {
1151    // If every keyset entry is empty, we still fall back to
1152    // _anonymous_ (not retain an empty keyset).
1153    let cfg = KeyvalsConfig {
1154      keysets: vec!["".to_string(), "".to_string()],
1155      ..KeyvalsConfig::default()
1156    };
1157    let kv = KeyVals::new(cfg);
1158    assert_eq!(kv.keysets, vec!["_anonymous_".to_string()]);
1159  }
1160}