Skip to main content

latexml_core/
keyval.rs

1//! Key-Value Definitions
2//!
3//! Provides an interface to define and access KeyVal definition.
4//! Used in conjunction with `KeyVals` to
5//!  fully implement KeyVal pairs.
6
7use std::{borrow::Cow, rc::Rc};
8
9use crate::{
10  binding::def::dialect::{def_conditional, def_macro},
11  common::{def_parser::parse_parameters, error::*, store::Stored},
12  definition::{
13    ExpansionBody, ExpansionClosure, argument::ArgWrap, conditional::ConditionalOptions,
14  },
15  mouth::tokenize,
16  parameter::Parameter,
17  state,
18  token::{Catcode, Token},
19  tokens::{TeXString, Tokens},
20};
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct KeyVal {
24  // which KeyVals are we parsing and how do we behave?
25  prefix: String,
26  key:    String,
27  keyset: String,
28}
29
30impl Default for KeyVal {
31  fn default() -> Self {
32    KeyVal {
33      prefix: "KV".to_string(),
34      keyset: String::new(),
35      key:    String::new(),
36    }
37  }
38}
39
40impl KeyVal {
41  pub fn new(prefix: Option<String>, keyset: String, key: String) -> Self {
42    let prefix = prefix.unwrap_or_else(|| "KV".to_string());
43    KeyVal { prefix, key, keyset }
44  }
45
46  pub fn get_header(&self) -> String { s!("{}@{}@{}", self.prefix, self.keyset, self.key) }
47
48  //======================================================================
49  // Property access
50  //======================================================================
51
52  pub fn get_prop(&self, key: &str) -> Option<Stored> {
53    state::lookup_value(&s!("KEYVAL@{}@{}", key, self.get_header()))
54  }
55  pub fn get_default(&self) -> Option<Stored> { self.get_prop("default") }
56  pub fn get_type(&self) -> Option<Rc<Parameter>> {
57    // Read directly via with_value — avoids the Stored::clone that
58    // get_prop's lookup_value pays just so we can pattern-match on
59    // the Parameter variant and Rc::clone its body. Hot path during
60    // keyval parsing.
61    state::with_value(&s!("KEYVAL@type@{}", self.get_header()), |v| match v {
62      Some(Stored::Parameter(p)) => Some(Rc::clone(p)),
63      _ => None,
64    })
65  }
66}
67
68// semi-internals
69pub(crate) fn keyval_qname(prefix: &str, keyset: &str, key: &str) -> String {
70  let prefix = if prefix.is_empty() { "KV" } else { prefix };
71  s!("{prefix}@{keyset}@{key}")
72}
73
74pub(crate) fn keyval_get(qname: &str, prop: &str) -> Option<Stored> {
75  state::lookup_value(&s!("KEYVAL@{prop}@{qname}"))
76}
77
78/// Using local assignments in the State to set keyvals, but that necessitates a
79/// cast of `ArgWrap`` to `State`` on set and `State` to `ArgWrap` on get.
80/// Certain values don't really work well with that, e.g. Stored::Bool ...
81/// The original Perl made no type casts, as there weren't any concrete types.
82pub(crate) fn keyval_set(qname: &str, prop: &str, value: Stored) {
83  state::assign_value(&s!("KEYVAL@{prop}@{qname}"), value, None);
84}
85
86/// check if a key-value pair is defined
87pub fn has_keyval(prefix: &str, keyset: &str, key: &str) -> bool {
88  let qname = keyval_qname(prefix, keyset, key);
89  state::with_value(&s!("KEYVAL@defined@{}", qname), |v| v.is_some())
90    || state::has_meaning(&T_CS!(s!("\\{qname}")))
91}
92
93/// disable a given key-val
94pub fn disable_keyval(prefix: &str, keyset: &str, key: &str) -> Result<()> {
95  let qname = keyval_qname(prefix, keyset, key);
96  keyval_set(&qname, "disabled", true.into());
97  // disable the key
98  define_ordinary(
99    &qname,
100    Some(ExpansionBody::Tokens(tokenize(TeXString::assembled(s!(
101      "\\PackageWarning{{keyval}}{{`{key}' has been disabled. }}"
102    ))))),
103  )
104}
105
106//======================================================================
107// Key Definition
108//======================================================================
109#[derive(Debug, Default, Clone)]
110/// Configuration fields for declaring a new KeyVal pattern
111pub struct KeyvalConfig<'a> {
112  pub prefix:      &'a str,
113  pub keyset:      &'a str,
114  pub key:         &'a str,
115  pub vtype:       &'a str,
116  pub default:     Option<&'a str>,
117  pub kind:        Option<&'a str>,
118  pub code:        Option<ExpansionBody>,
119  pub macroprefix: Option<&'a str>,
120  pub mismatch:    Option<ExpansionBody>,
121  pub normalize:   Option<bool>,
122  pub bin:         Option<Tokens>,
123  pub choices:     Vec<&'static str>,
124}
125
126/// Register a keyval qname in the global registry for enumeration by \xkvview.
127fn register_keyval(qname: &str) {
128  use crate::common::arena;
129  let registry_key = "KEYVAL@registry";
130  // Borrow the registry via `with_value` to skip the outer Stored::clone
131  // (lookup_value clones the enum; we only need the inner Vec<SymStr>).
132  let mut registry: Vec<arena::SymStr> = state::with_value(registry_key, |v| match v {
133    Some(Stored::Strings(v)) => v.to_vec(),
134    _ => Vec::new(),
135  });
136  let sym = arena::pin(qname);
137  // avoid duplicates (re-definitions)
138  if !registry.contains(&sym) {
139    registry.push(sym);
140  }
141  state::assign_value(registry_key, Stored::Strings(registry.into()), None);
142}
143
144/// Metadata for a registered keyval, used by \xkvview.
145#[derive(Debug, Clone)]
146pub struct KeyvalMeta {
147  pub key:     String,
148  pub prefix:  String,
149  pub keyset:  String,
150  pub kind:    String,
151  pub default: String,
152}
153
154/// Enumerate all registered keyvals with their metadata (for \xkvview).
155pub fn enumerate_keyvals() -> Vec<KeyvalMeta> {
156  use crate::common::arena;
157  let registry_key = "KEYVAL@registry";
158  let registry = state::with_value(registry_key, |v| match v {
159    Some(Stored::Strings(v)) => v.to_vec(),
160    _ => Vec::new(),
161  });
162  if registry.is_empty() {
163    return Vec::new();
164  }
165  let mut result = Vec::new();
166  for sym in registry {
167    // Resolve the interned qname once via a closure — the five
168    // keyval_get calls below all take `&str`, so we hand each the same
169    // resolved borrow rather than allocating a per-key String.
170    let entry = arena::with(sym, |qname| {
171      let key = keyval_get(qname, "key_name")
172        .map(|s| s.to_string())
173        .unwrap_or_default();
174      let prefix = keyval_get(qname, "keyval_prefix")
175        .map(|s| s.to_string())
176        .unwrap_or_else(|| "KV".to_string());
177      let keyset = keyval_get(qname, "keyset")
178        .map(|s| s.to_string())
179        .unwrap_or_default();
180      let kind = keyval_get(qname, "kind")
181        .map(|s| s.to_string())
182        .unwrap_or_else(|| "ordinary".to_string());
183      let default = keyval_get(qname, "default")
184        .map(|s| s.to_string())
185        .unwrap_or_else(|| "[none]".to_string());
186      KeyvalMeta {
187        key,
188        prefix,
189        keyset,
190        kind,
191        default,
192      }
193    });
194    result.push(entry);
195  }
196  result
197}
198
199/// (Re-)defines this Key of kind 'kind'.
200///
201///Defines a keyword `key` used in keyval arguments for the set `keyset` and,
202///and if the option `code` is given, defines appropriate macros
203///when used with the `keyval` package (or extensions thereof).
204///
205///If `type` is given, it defines the type of value that must be supplied,
206///such as `Dimension`.  If `default` is given, that value will be used
207///when `key` is used without an equals and explicit value in a keyvals argument.
208///
209///A `scope` option can be given, which can be used to defined the key-value pair
210///globally instead of in the current scope.
211///
212///Several more `option`s can be given. These implement the behaviour of the
213///xkeyval package.
214///
215///The `prefix` parameter can be used to configure a custom prefix for
216///the macros to be defined. The `kind` parameter can be used to configure special types of xkeyval
217///pairs.
218///
219///The 'ordinary' kind behaves like a normal keyval parameter.
220///
221///The 'command' kind defines a command key, that when run stores the value of the
222///key in a special macro, which can be further specefied by the `macroprefix`
223///option.
224///
225///The 'choice' kind defines a choice key, which takes additional options
226///`choices` (to specify which choices are valid values), `mismatch` (to be run
227///if an invalid choice is made) and `bin` (see xkeyval documentation for
228///details).
229///
230///The 'boolean' kind defines a special choice key that takes possible values true and
231///false, and defines a new Conditional according to the assumed value. The name of
232///this conditional can be specified with the `macroprefix` option.
233///
234///The kind parameter only takes effect when `code` is given, otherwise only
235///meta-data is stored.
236pub fn define(options: KeyvalConfig) -> Result<()> {
237  let KeyvalConfig {
238    prefix,
239    keyset,
240    key,
241    vtype,
242    default,
243    kind,
244    code,
245    macroprefix,
246    mismatch,
247    normalize,
248    bin,
249    choices,
250  } = options;
251
252  let qname = keyval_qname(prefix, keyset, key);
253
254  // define that the key exists and is not disabled
255  keyval_set(&qname, "exists", true.into());
256  keyval_set(&qname, "disabled", false.into());
257  // store metadata for introspection (used by \xkvview)
258  // only register when xkvview tracking is enabled
259  if state::lookup_bool("XKVVIEW_TRACKING") {
260    // `assembled`: these four are bare identifiers (a key/keyset/prefix name),
261    // not markup — nothing to weld — but they arrive as borrowed `&str`.
262    let identifier = |s: &str| TeXString::assembled(s.to_string());
263    keyval_set(
264      &qname,
265      "kind",
266      Stored::Tokens(tokenize(identifier(kind.unwrap_or("ordinary")))),
267    );
268    keyval_set(
269      &qname,
270      "keyval_prefix",
271      Stored::Tokens(tokenize(identifier(prefix))),
272    );
273    keyval_set(
274      &qname,
275      "keyset",
276      Stored::Tokens(tokenize(identifier(keyset))),
277    );
278    keyval_set(
279      &qname,
280      "key_name",
281      Stored::Tokens(tokenize(identifier(key))),
282    );
283    register_keyval(&qname);
284  }
285  // set the type
286  let vtype = if vtype.is_empty() { "{}" } else { vtype };
287  let paramlist_opt = parse_parameters(
288    vtype,
289    &T_OTHER!(s!("KeyVal {key} in set {keyset} with prefix {prefix}")),
290    true,
291  )?;
292  match paramlist_opt {
293    None => {
294      Warn!(
295        "unexpected",
296        "keyval",
297        s!(
298          "No parameters in keyval {key} (in set {keyset} with prefix {prefix}) taking only first"
299        )
300      );
301    },
302    Some(paramlist) => {
303      if paramlist.get_num_args() != 1 {
304        Warn!(
305          "unexpected",
306          "keyval",
307          s!(
308            "Too many parameters in keyval {key} (in set {keyset} with prefix {prefix})\
309          taking only first"
310          )
311        );
312      }
313      keyval_set(&qname, "type", paramlist.take_parameters().remove(0).into());
314    },
315  };
316  // set the default
317  // Question: Why was $default converted ToString ???
318  if let Some(default_str) = default {
319    let default_tks = tokenize(TeXString::assembled(default_str.to_string()));
320    keyval_set(&qname, "default", Stored::Tokens(default_tks.clone()));
321    def_macro(
322      T_CS!(s!("\\{qname}@default")),
323      None,
324      ExpansionBody::Tokens(Tokens!(
325        T_CS!(s!("\\{qname}")),
326        T_BEGIN!(),
327        default_tks,
328        T_END!()
329      )),
330      None,
331    )?;
332  }
333
334  // figure out the kind of key-val parameter we are defining
335  let kind = kind.unwrap_or("ordinary");
336  match kind {
337    "ordinary" => define_ordinary(&qname, code)?,
338    "command" => {
339      // Perl #2777 (2026-03-27): macroprefix falls back to "cmd"+qname
340      // when undefined OR empty. The truthy check that existed pre-fix
341      // already treated empty-string as falsy; we match that semantics
342      // explicitly for Option<&str>.
343      let macroname = match macroprefix {
344        Some(mpfx) if !mpfx.is_empty() => s!("{mpfx}{key}"),
345        _ => s!("cmd{qname}"),
346      };
347      define_command(&qname, code, &macroname)?;
348    },
349    "choice" => define_choice(
350      &qname,
351      code,
352      mismatch,
353      choices,
354      normalize.unwrap_or(false),
355      bin,
356    )?,
357    "boolean" => define_boolean(
358      &qname,
359      code,
360      mismatch,
361      &if let Some(mpfx) = macroprefix {
362        Cow::Owned(s!("{mpfx}{key}"))
363      } else {
364        Cow::Borrowed(&qname)
365      },
366    )?,
367    _ => Warn!(
368      "unknown",
369      "undef",
370      s!(
371        "Unknown KeyVals kind {kind} should be one of 'ordinary', 'command', 'choice', 'boolean'. "
372      )
373    ),
374  };
375  Ok(())
376}
377
378/// Helper function to define state, neccesary for an ordinary key.
379fn define_ordinary(qname: &str, code_expansion: Option<ExpansionBody>) -> Result<()> {
380  let qname_cs = T_CS!(s!("\\{qname}"));
381  let plain_params = parse_parameters("{}", &qname_cs, true)?;
382  def_macro(qname_cs, plain_params, code_expansion, None)
383}
384
385/// Helper function to define state, neccesary for a command key.
386fn define_command(qname: &str, code: Option<ExpansionBody>, macroname: &str) -> Result<()> {
387  let qname_cs = T_CS!(s!("\\{qname}"));
388  let plain_params = parse_parameters("{}", &qname_cs, true)?;
389  let plainp = plain_params.clone();
390  let orig = s!("\\ltxml@orig@{qname}");
391  let macroname_cs = s!("\\{macroname}");
392  let closure: ExpansionClosure = Rc::new(move |value: Vec<ArgWrap>| {
393    def_macro(T_CS!(&orig), plainp.clone(), code.clone(), None)?;
394    let value_tks: Vec<Token> = value
395      .into_iter()
396      .flat_map(|v| {
397        v.owned_tokens()
398          .map(|inner| inner.unlist())
399          .unwrap_or_default()
400      })
401      .collect();
402    // $value !?!??! Is it a number 1--9 ???)
403    Ok(Tokens!(
404      T_CS!("\\def"),
405      T_CS!(&macroname_cs),
406      T_BEGIN!(),
407      value_tks.clone(),
408      T_END!(),
409      T_CS!(&orig),
410      T_BEGIN!(),
411      T_PARAM!(),
412      value_tks,
413      T_END!()
414    ))
415  });
416  def_macro(
417    qname_cs,
418    plain_params,
419    ExpansionBody::Closure(closure),
420    None,
421  )
422}
423
424/// Helper function to define state, neccesary for an choice key.
425fn define_choice(
426  qname: &str,
427  code_opt: Option<ExpansionBody>,
428  mismatch_opt: Option<ExpansionBody>,
429  choices: Vec<&'static str>,
430  normalize: bool,
431  bin: Option<Tokens>,
432) -> Result<()> {
433  let (varmacro_opt, idxmacro_opt) = if let Some(bin_tks) = bin {
434    let mut bin_iter = bin_tks.unlist().into_iter();
435    (bin_iter.next(), bin_iter.next())
436  } else {
437    (None, None)
438  };
439  let qname_cs = T_CS!(s!("\\{qname}"));
440  let orig = T_CS!(s!("\\ltxml@orig@{qname}"));
441  let plain_params = parse_parameters("{}", &qname_cs, true)?;
442  let plain_params_main = plain_params.clone();
443  let closure: ExpansionClosure = Rc::new(move |mut values| {
444    // Store the normalized value (if applicable)
445    let value = values.remove(0).owned_tokens().unwrap_or_default();
446    let mut nvalue = value.to_string();
447    if normalize {
448      nvalue = nvalue.to_lowercase();
449    }
450    if let Some(varmacro) = varmacro_opt {
451      def_macro(
452        varmacro,
453        None,
454        ExpansionBody::Tokens(Tokens::new(Explode!(nvalue))),
455        None,
456      )?;
457    }
458    // iterate over the possible choices and store them
459    let mut valid = false;
460    for (index, choice_str) in choices.iter().enumerate() {
461      if (normalize && (choice_str.to_lowercase() == nvalue)) || *choice_str == nvalue {
462        valid = true;
463        if let Some(idxmacro) = idxmacro_opt {
464          def_macro(
465            idxmacro,
466            None,
467            ExpansionBody::Tokens(Tokens::new(Explode!(index))),
468            None,
469          )?;
470        }
471      }
472    }
473    // find a name for the original macro to store in
474    let mut tokens = Vec::new();
475    // if we have chosen a valid index, run $code
476    if valid {
477      if let Some(ref code) = code_opt {
478        def_macro(orig, plain_params.clone(), code.clone(), None)?;
479        tokens.push(orig);
480        tokens.push(T_BEGIN!());
481        tokens.extend(value.unlist());
482        tokens.push(T_END!());
483      }
484    } else if let Some(ref mismatch) = mismatch_opt {
485      // else run `mismatch
486      def_macro(orig, plain_params.clone(), mismatch.clone(), None)?;
487      tokens.push(orig);
488      tokens.push(T_BEGIN!());
489      tokens.extend(value.unlist());
490      tokens.push(T_END!());
491    }
492    Ok(Tokens::new(tokens))
493  });
494  def_macro(
495    qname_cs,
496    plain_params_main,
497    ExpansionBody::Closure(closure),
498    None,
499  )
500}
501
502/// Helper function to define state, neccesary for a boolean key.
503fn define_boolean(
504  qname: &str,
505  code_opt: Option<ExpansionBody>,
506  mismatch: Option<ExpansionBody>,
507  macroname: &str,
508) -> Result<()> {
509  def_conditional(
510    T_CS!(s!("\\if{macroname}")),
511    None,
512    None,
513    ConditionalOptions::default(),
514  )?; // We might need to $scope here
515  let orig = s!("\\ltxml@@rig@{qname}");
516  let orig_cs = T_CS!(orig);
517  let plain_params = parse_parameters("{}", &orig_cs, true)?;
518  let macroname_true = T_CS!(s!("\\{macroname}true"));
519  let macroname_false = T_CS!(s!("\\{macroname}false"));
520  let closure: ExpansionClosure = Rc::new(move |mut values: Vec<ArgWrap>| {
521    // set the conditional to true/false
522    let value = values.remove(0).owned_tokens().unwrap_or_default();
523    let value_str = value.to_string().to_lowercase();
524    let mut tokens = vec![];
525    // Toggle the conditional by invoking \XXXtrue or \XXXfalse
526    if value_str == "true" {
527      tokens.push(macroname_true);
528    } else {
529      tokens.push(macroname_false);
530    }
531    // Store and invoke the original macro if needed
532    if let Some(ref code) = code_opt {
533      def_macro(orig_cs, plain_params.clone(), code.clone(), None)?;
534      tokens.push(orig_cs);
535      tokens.push(T_BEGIN!());
536      tokens.extend(value.unlist());
537      tokens.push(T_END!());
538    }
539    Ok(Tokens::new(tokens))
540  });
541
542  define_choice(
543    qname,
544    Some(ExpansionBody::Closure(closure)),
545    mismatch,
546    vec!["true", "false"],
547    true,
548    None,
549  )
550}
551
552#[cfg(test)]
553mod tests {
554  use super::*;
555
556  #[test]
557  fn keyval_default_fields() {
558    let kv = KeyVal::default();
559    assert_eq!(kv.prefix, "KV");
560    assert!(kv.keyset.is_empty());
561    assert!(kv.key.is_empty());
562  }
563
564  #[test]
565  fn keyval_new_custom_prefix() {
566    let kv = KeyVal::new(
567      Some("custom".to_string()),
568      "ks".to_string(),
569      "k".to_string(),
570    );
571    assert_eq!(kv.prefix, "custom");
572    assert_eq!(kv.keyset, "ks");
573    assert_eq!(kv.key, "k");
574  }
575
576  #[test]
577  fn keyval_new_default_prefix_on_none() {
578    // None prefix → default "KV".
579    let kv = KeyVal::new(None, "ks".to_string(), "k".to_string());
580    assert_eq!(kv.prefix, "KV");
581  }
582
583  #[test]
584  fn keyval_get_header_format() {
585    let kv = KeyVal::new(
586      Some("P".to_string()),
587      "set".to_string(),
588      "width".to_string(),
589    );
590    assert_eq!(kv.get_header(), "P@set@width");
591  }
592
593  #[test]
594  fn keyval_get_header_default_prefix() {
595    let kv = KeyVal::new(None, "tabular".to_string(), "vattach".to_string());
596    assert_eq!(kv.get_header(), "KV@tabular@vattach");
597  }
598
599  #[test]
600  fn keyval_equality_by_all_fields() {
601    let a = KeyVal::new(Some("P".to_string()), "ks".to_string(), "k".to_string());
602    let b = KeyVal::new(Some("P".to_string()), "ks".to_string(), "k".to_string());
603    let c = KeyVal::new(Some("P".to_string()), "ks".to_string(), "other".to_string());
604    assert_eq!(a, b);
605    assert_ne!(a, c);
606  }
607
608  #[test]
609  fn keyval_qname_normalizes_empty_prefix() {
610    // Empty prefix is substituted with "KV".
611    assert_eq!(keyval_qname("", "set", "k"), "KV@set@k");
612    assert_eq!(keyval_qname("P", "set", "k"), "P@set@k");
613  }
614}
615
616// ── keyval TeX-source splitting (winnow) ─────────────────────────────────
617// The same `winnow` grammar family as the XML-replacement template parser
618// (binding/def/replacement.rs); used by the runtime-bindings GetKeyVal(s)
619// accessors over a dict's TeX-source form.
620
621/// Split a keyval dict's TeX-source form (`"k=v, k2={v, 2}"`) into
622/// `(key, value)` pairs: comma/equals splitting at brace depth 0 only, one
623/// level of outer braces stripped from values, whitespace trimmed, empty
624/// keys dropped (keyval semantics).
625pub fn split_keyval_source(kv: &str) -> Vec<(String, String)> {
626  use winnow::{
627    combinator::{opt, separated},
628    prelude::*,
629  };
630
631  /// One item: everything up to a depth-0 comma (consumed by the separator).
632  fn item(input: &mut &str) -> ModalResult<(String, String)> {
633    let mut depth = 0usize;
634    let mut split = None; // byte offset of the depth-0 `=`, if any
635    let mut end = input.len();
636    for (i, c) in input.char_indices() {
637      match c {
638        '{' => depth += 1,
639        '}' => depth = depth.saturating_sub(1),
640        '=' if depth == 0 && split.is_none() => split = Some(i),
641        ',' if depth == 0 => {
642          end = i;
643          break;
644        },
645        _ => {},
646      }
647    }
648    let (raw, rest) = input.split_at(end);
649    *input = rest;
650    let (k, v) = match split.filter(|s| *s < end) {
651      Some(eq) => (&raw[..eq], &raw[eq + 1..]),
652      None => (raw, ""),
653    };
654    let v = v.trim();
655    let v = v
656      .strip_prefix('{')
657      .and_then(|s| s.strip_suffix('}'))
658      .unwrap_or(v);
659    Ok((k.trim().to_string(), v.to_string()))
660  }
661
662  let mut parser = (separated(0.., item, ","), opt(","));
663  let items: Vec<(String, String)> = match parser.parse(kv) {
664    Ok((items, _)) => items,
665    Err(_) => Vec::new(), // total parser: `item` consumes anything, so unreachable
666  };
667  items.into_iter().filter(|(k, _)| !k.is_empty()).collect()
668}
669
670#[cfg(test)]
671mod keyval_source_tests {
672  use super::split_keyval_source;
673
674  #[test]
675  fn splits_at_depth_zero_only() {
676    assert_eq!(split_keyval_source("lang=rust, size={1, 2}"), vec![
677      ("lang".to_string(), "rust".to_string()),
678      ("size".to_string(), "1, 2".to_string()),
679    ]);
680  }
681
682  #[test]
683  fn flag_keys_and_empties() {
684    assert_eq!(split_keyval_source("draft,, a=1 ,"), vec![
685      ("draft".to_string(), String::new()),
686      ("a".to_string(), "1".to_string()),
687    ]);
688  }
689
690  #[test]
691  fn braced_equals_is_not_a_split() {
692    assert_eq!(split_keyval_source("k={a=b}"), vec![(
693      "k".to_string(),
694      "a=b".to_string()
695    )]);
696  }
697
698  #[test]
699  fn empty_input() {
700    assert!(split_keyval_source("").is_empty());
701  }
702}