sidoc 0.1.3

Generate structured/scoped indented documents.
Documentation
use crate::{Doc, Error, Node};

/// Constructor for `Doc` objects.
#[derive(Default)]
pub struct Builder {
  nodes: Vec<Node>,
  scope_stack: Vec<Option<String>>
}

impl Builder {
  /// Create a new `Doc` builder context.
  #[must_use]
  pub fn new() -> Self {
    Self::default()
  }

  /// Begin a scope, pushing an optional scope terminator to the internal scope
  /// stack.
  ///
  /// If the scope generated using a terminator line, that line will appended
  /// to the document when the scope is closed using the `exit()` method.
  #[allow(clippy::needless_pass_by_value)]
  pub fn scope<L: ToString, K: ToString>(
    &mut self,
    begin_line: L,
    term_line: Option<K>
  ) -> &mut Self {
    self.nodes.push(Node::BeginScope(begin_line.to_string()));
    if let Some(ln) = term_line {
      self.scope_stack.push(Some(ln.to_string()));
    } else {
      self.scope_stack.push(None);
    }
    self
  }

  /// Wrap [`Builder::scope()`] and [`Builder::exit()`].
  ///
  /// Initialize a new scope, call caller-supplied closure, and automatically
  /// exit scope before returning.
  ///
  /// ```
  /// use std::sync::Arc;
  /// use sidoc::{Builder, RenderContext};
  ///
  /// let mut bldr = Builder::new();
  ///
  /// bldr
  ///   .line("<!DOCTYPE html>")
  ///   .autoscope("<html>", Some("</html>"), |bldr| {
  ///     bldr.autoscope("<head>", Some("</head>"), |bldr| {
  ///       bldr.line("<title>hello</title>");
  ///     });
  ///   });
  ///
  /// let doc = bldr.build().unwrap();
  /// let mut r = RenderContext::new();
  /// r.doc("root", Arc::new(doc));
  /// let buf = r.render("root").unwrap();
  ///
  /// assert_eq!(
  ///   buf,
  ///   "<!DOCTYPE html>\n<html>\n  <head>\n    <title>hello</title>\n  </head>\n</html>\n"
  /// );
  /// ```
  #[allow(clippy::needless_pass_by_value)]
  pub fn autoscope<F, L: ToString, K: ToString>(
    &mut self,
    begin_line: L,
    term_line: Option<K>,
    f: F
  ) -> &mut Self
  where
    F: FnOnce(&mut Self)
  {
    self.scope(begin_line, term_line);
    f(self);
    self.exit();
    self
  }

  /// Same as [`Builder::autoscope()`], but only init scope and call closure if
  /// predicate is true.
  #[allow(clippy::needless_pass_by_value)]
  pub fn autoscope_if<F, L: ToString, K: ToString>(
    &mut self,
    pred: bool,
    begin_line: L,
    term_line: Option<K>,
    f: F
  ) -> &mut Self
  where
    F: FnOnce(&mut Self)
  {
    if pred {
      self.scope(begin_line, term_line);
      f(self);
      self.exit();
    }
    self
  }

  /// Same as [`Builder::autoscope()`], but only init scope and call closure if
  /// `opt` is `Some(T)`.  `T` will be passed to the closure.
  #[allow(clippy::needless_pass_by_value)]
  pub fn autoscope_opt<F, T, L: ToString, K: ToString>(
    &mut self,
    opt: Option<T>,
    begin_line: L,
    term_line: Option<K>,
    f: F
  ) -> &mut Self
  where
    F: FnOnce(&mut Self, T)
  {
    if let Some(o) = opt {
      self.scope(begin_line, term_line);
      f(self, o);
      self.exit();
    }
    self
  }

  /// Leave a previously entered scope.
  ///
  /// If the `scope()` call that created the current scope
  ///
  /// # Panics
  /// The scope stack must not be empty.
  pub fn exit(&mut self) -> &mut Self {
    if let Some(s) = self.scope_stack.pop().unwrap() {
      self.nodes.push(Node::EndScope(Some(s)));
    } else {
      self.nodes.push(Node::EndScope(None));
    }
    self
  }

  /// Leave previously entered scope, adding a line passed by the caller rather
  /// than the scope stack.
  ///
  /// # Panics
  /// The scope stack must not be empty.
  #[allow(clippy::needless_pass_by_value)]
  pub fn exit_line<L: ToString>(&mut self, line: L) -> &mut Self {
    let _ = self.scope_stack.pop().unwrap();
    self.nodes.push(Node::EndScope(Some(line.to_string())));
    self
  }

  /// Add a new line at current scope.
  #[allow(clippy::needless_pass_by_value)]
  pub fn line<L: ToString>(&mut self, line: L) -> &mut Self {
    self.nodes.push(Node::Line(line.to_string()));
    self
  }

  /// Add a named optional reference.
  ///
  /// References are placeholders for other documents.  An optional reference
  /// means that this reference does not need to be resolved by the renderer.
  #[allow(clippy::needless_pass_by_value)]
  pub fn optref<N: ToString>(&mut self, name: N) -> &mut Self {
    self.nodes.push(Node::OptRef(name.to_string()));
    self
  }

  /// Add a named required reference.
  ///
  /// References are placeholders for other documents.  A required reference
  /// must be resolved by the renderer or it will return an error to its
  /// caller.
  #[allow(clippy::needless_pass_by_value)]
  pub fn reqref<N: ToString>(&mut self, name: N) -> &mut Self {
    self.nodes.push(Node::ReqRef(name.to_string()));
    self
  }

  /// Generate a `Doc` object from this document.
  ///
  /// The document must be properly nested before calling this function,
  /// meaning all scopes it opened must be closed.
  ///
  /// # Errors
  /// [`Error::BadNesting`] means one or more scopes are still open.
  pub fn build(self) -> Result<Doc, Error> {
    if self.scope_stack.is_empty() {
      Ok(Doc { nodes: self.nodes })
    } else {
      Err(Error::BadNesting(format!(
        "{} scope(s) remaining",
        self.scope_stack.len()
      )))
    }
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :