asciidork-parser 0.38.1

Asciidork parser
Documentation
use crate::internal::*;

// https://docs.asciidoctor.org/asciidoc/latest/subs/apply-subs-to-blocks/
pub fn from_meta(current: Substitutions, attrs: &MultiAttrList) -> Substitutions {
  let Some(subs) = attrs.named("subs") else {
    return current;
  };

  let mut next = iff!(subs.contains(['-', '+']), current, Substitutions::none());
  for part in subs.split(',') {
    let len = part.len();
    if len < 4 {
      // TODO: error? warning?
      continue;
    }

    let bytes = part.as_bytes();
    let (strategy, name) = match (bytes[0], bytes[len - 1]) {
      (b'+', _) => (Strategy::Append, &bytes[1..len]),
      (_, b'+') => (Strategy::Prepend, &bytes[0..len - 1]),
      (b'-', _) => (Strategy::Remove, &bytes[1..len]),
      _ => (Strategy::Replace, bytes),
    };

    let Some(step_or_group) = StepOrGroup::from(name) else {
      // TODO: error? warning?
      continue;
    };

    match strategy {
      Strategy::Replace => match step_or_group {
        StepOrGroup::None => {}
        StepOrGroup::Normal => {
          next.insert(Subs::SpecialChars);
          next.insert(Subs::InlineFormatting);
          next.insert(Subs::AttrRefs);
          next.insert(Subs::CharReplacement);
          next.insert(Subs::Macros);
          next.insert(Subs::PostReplacement);
        }
        StepOrGroup::Verbatim => {
          next.insert(Subs::SpecialChars);
          next.insert(Subs::CharReplacement);
        }
        StepOrGroup::SpecialChars => next.insert(Subs::SpecialChars),
        StepOrGroup::Callouts => next.insert(Subs::Callouts),
        StepOrGroup::Quotes => next.insert(Subs::InlineFormatting),
        StepOrGroup::Attributes => next.insert(Subs::AttrRefs),
        StepOrGroup::Replacements => next.insert(Subs::CharReplacement),
        StepOrGroup::Macros => next.insert(Subs::Macros),
        StepOrGroup::PostReplacements => next.insert(Subs::PostReplacement),
      },
      Strategy::Append | Strategy::Prepend | Strategy::Remove => {
        let modify = match strategy {
          Strategy::Append => Substitutions::insert,
          Strategy::Prepend => Substitutions::prepend,
          Strategy::Remove => Substitutions::remove,
          _ => unreachable!(),
        };
        match step_or_group {
          StepOrGroup::None => {}
          StepOrGroup::Normal => {
            modify(&mut next, Subs::SpecialChars);
            modify(&mut next, Subs::InlineFormatting);
            modify(&mut next, Subs::AttrRefs);
            modify(&mut next, Subs::CharReplacement);
            modify(&mut next, Subs::Macros);
            modify(&mut next, Subs::PostReplacement);
          }
          StepOrGroup::Verbatim => {
            modify(&mut next, Subs::SpecialChars);
            modify(&mut next, Subs::CharReplacement);
          }
          StepOrGroup::SpecialChars => {
            modify(&mut next, Subs::SpecialChars);
          }
          StepOrGroup::Callouts => {
            modify(&mut next, Subs::Callouts);
          }
          StepOrGroup::Quotes => {
            modify(&mut next, Subs::InlineFormatting);
          }
          StepOrGroup::Attributes => {
            modify(&mut next, Subs::AttrRefs);
          }
          StepOrGroup::Replacements => {
            modify(&mut next, Subs::CharReplacement);
          }
          StepOrGroup::Macros => {
            modify(&mut next, Subs::Macros);
          }
          StepOrGroup::PostReplacements => {
            modify(&mut next, Subs::PostReplacement);
          }
        }
      }
    }
  }
  next
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Strategy {
  Replace,
  Append,
  Prepend,
  Remove,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StepOrGroup {
  None,
  Normal,
  Verbatim,
  SpecialChars,
  Callouts,
  Quotes,
  Attributes,
  Replacements,
  Macros,
  PostReplacements,
}

impl StepOrGroup {
  const fn from(s: &[u8]) -> Option<Self> {
    match s {
      b"none" => Some(Self::None),
      b"normal" => Some(Self::Normal),
      b"verbatim" => Some(Self::Verbatim),
      b"specialchars" => Some(Self::SpecialChars),
      b"callouts" => Some(Self::Callouts),
      b"quotes" => Some(Self::Quotes),
      b"attributes" => Some(Self::Attributes),
      b"replacements" => Some(Self::Replacements),
      b"macros" => Some(Self::Macros),
      b"post_replacements" => Some(Self::PostReplacements),
      _ => None,
    }
  }
}

// tests

#[cfg(test)]
mod tests {
  use super::*;
  use test_utils::*;

  fn exactly(subs: &[Subs]) -> Substitutions {
    let mut s = Substitutions::none();
    subs.iter().for_each(|sub| s.insert(*sub));
    s
  }

  fn all_except(subs: &[Subs]) -> Substitutions {
    let mut s = Substitutions::normal();
    subs.iter().for_each(|sub| s.remove(*sub));
    s
  }

  #[test]
  fn test_customize_subs_from_meta() {
    let cases = [
      (
        "[subs=none]",
        Substitutions::normal(),
        Substitutions::none(),
      ),
      (
        "[subs=\"none\"]",
        Substitutions::normal(),
        Substitutions::none(),
      ),
      (
        "[subs=normal]",
        Substitutions::none(),
        Substitutions::normal(),
      ),
      (
        "[subs=-specialchars]",
        Substitutions::normal(),
        all_except(&[Subs::SpecialChars]),
      ),
      (
        "[subs=macros+]",
        exactly(&[Subs::SpecialChars]),
        exactly(&[Subs::Macros, Subs::SpecialChars]),
      ),
      (
        "[subs=+macros]",
        exactly(&[Subs::SpecialChars]),
        exactly(&[Subs::SpecialChars, Subs::Macros]),
      ),
      (
        r#"[subs="attributes+,+quotes,-macros"]"#,
        exactly(&[Subs::SpecialChars, Subs::Macros]),
        exactly(&[Subs::AttrRefs, Subs::SpecialChars, Subs::InlineFormatting]),
      ),
    ];

    for (attrs, current, expected) in cases {
      let input: &'static str = format!("{attrs}\nfoo").leak();
      let block = parse_single_block!(&input);
      let attrs = block.meta.attrs;
      let next = from_meta(current, &attrs);
      assert_eq!(next, expected);
    }
  }
}