use jrsonnet_evaluator::{self, Val, manifest::JsonFormat, val::StrValue};
use pulldown_cmark::{self, CodeBlockKind, CowStr, Event, Tag, TagEnd};
use std::{collections::VecDeque, iter::Peekable, path::PathBuf};
pub struct Parser<'a> {
source_parser: Peekable<pulldown_cmark::Parser<'a>>,
eval_result: VecDeque<Event<'a>>,
library_paths: Vec<PathBuf>,
}
impl<'a> Parser<'a> {
pub fn new(text: &'a str) -> Self {
Parser {
source_parser: pulldown_cmark::Parser::new(text).peekable(),
eval_result: VecDeque::new(),
library_paths: vec![],
}
}
pub fn with_library_paths(mut self, jpaths: &Vec<PathBuf>) -> Self {
self.library_paths = jpaths.clone();
return self;
}
pub fn add_library_path(&mut self, jpath: PathBuf) {
self.library_paths.push(jpath);
}
fn has_eval_result(&self) -> bool {
self.eval_result.len() > 0
}
fn peek_marksonnet_block_start(&mut self) -> bool {
Self::is_marksonnet_block_start(&self.source_parser.peek())
}
fn is_marksonnet_block_start(event: &Option<&Event>) -> bool {
matches!(
event,
Some(Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(
CowStr::Borrowed("marksonnet")
))))
)
}
fn eval_block(&mut self) -> Result<(), jrsonnet_evaluator::Error> {
debug_assert!(
!self.has_eval_result(),
"eval_block called when eval_result is nonempty; it is {:?}",
self.eval_result
);
let block_start = self.source_parser.next();
debug_assert!(
Self::is_marksonnet_block_start(&block_start.as_ref()),
"eval_block called expecting next event to be a Marksonnet block start; it is {:?}",
block_start
);
drop(block_start);
let mut contents = String::new();
loop {
let event = self.source_parser.next();
match event {
Some(Event::Text(CowStr::Borrowed(text))) => contents.push_str(text),
Some(Event::End(TagEnd::CodeBlock)) => break,
_ => unimplemented!("handle malformatted code block (encountered {:?})", event),
}
}
let mut s = jrsonnet_evaluator::State::builder();
s.import_resolver(jrsonnet_evaluator::FileImportResolver::new(
self.library_paths.clone(),
));
s.context_initializer(jrsonnet_stdlib::ContextInitializer::new(
jrsonnet_evaluator::trace::PathResolver::new_cwd_fallback(),
));
let s = s.build(); let val = Some(s.evaluate_snippet("<marksonnet>", contents));
match val {
Some(Ok(Val::Str(StrValue::Flat(text)))) => {
for event in pulldown_cmark::Parser::new(text.as_str()) {
self.eval_result.push_back(event.into_static());
}
}
Some(Ok(val)) => {
let manifested = val.manifest(JsonFormat::cli(4))? + "\n";
self.eval_result = vec![
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Borrowed(
"json",
)))),
Event::Text(CowStr::Boxed(manifested.into())),
Event::End(TagEnd::CodeBlock),
]
.into();
}
_ => todo!("error or unsupported Val variant: {:?}", val),
}
Ok(())
}
}
impl<'a> Iterator for Parser<'a> {
type Item = Event<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.has_eval_result() {
self.eval_result.pop_front()
} else if self.peek_marksonnet_block_start() {
let _ = self.eval_block();
self.next()
} else {
self.source_parser.next()
}
}
}
#[cfg(test)]
mod test {
use super::*;
use indoc::indoc;
use pretty_assertions::assert_eq;
use pulldown_cmark_to_cmark::cmark;
macro_rules! test {
($name:ident, $input:expr, $expected:expr) => {
#[test]
fn $name() {
let mut input = String::new();
let _ = cmark(Parser::new($input), &mut input);
let mut expected = String::new();
let _ = cmark(pulldown_cmark::Parser::new($expected), &mut expected);
assert_eq!(input, expected);
}
};
($name:ident, $input:expr, $expected:expr, $expected_events:expr) => {
#[test]
fn $name() {
let input_events: Vec<_> = Parser::new($input).collect();
let mut input_cmark = String::new();
let _ = cmark(input_events.iter(), &mut input_cmark); let input = (input_cmark, input_events);
let mut expected_cmark = String::new();
let _ = cmark(pulldown_cmark::Parser::new($expected), &mut expected_cmark);
let expected = (expected_cmark, $expected_events);
assert_eq!(input, expected);
}
};
}
test!(
no_marksonnet,
indoc! {r#"This is a simple markdown document containing no marksonnet."#},
indoc! {r#"This is a simple markdown document containing no marksonnet."#}
);
test!(
empty_marksonnet_object,
indoc! {r#"
This is a simple markdown document containing a marksonnet object.
```marksonnet
{}
```
There is also text after it."#},
indoc! {r#"
This is a simple markdown document containing a marksonnet object.
```json
{ }
```
There is also text after it."#}
);
test!(
simple_marksonnet_object_calculation,
indoc! {r#"```marksonnet
{
"value": 2 + 2
}
```"#},
indoc! {r#"
```json
{
"value": 4
}
```"#},
vec![
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::Borrowed(
"json"
)))),
Event::Text(CowStr::Boxed("{\n \"value\": 4\n}\n".into())),
Event::End(TagEnd::CodeBlock)
]
);
test!(
simple_marksonnet_string,
indoc! {r#"```marksonnet
"Hello, world!"
```"#},
indoc! {r#"Hello, world!"#}
);
test!(
sample_import,
indoc! {r#"```marksonnet
import 'example/sample.json'
```"#},
indoc! {r#"
```json
{
"bar": "baz",
"foo": "bar"
}
```"#}
);
test!(
sample_importstr,
indoc! {r#"```marksonnet
importstr 'example/sample.md'
```"#},
indoc! {r#"
# Sample!
This file is a sample.
"#}
);
test!(
sample_importstr_with_prefix,
indoc! {r#"
Here we have a prefix.
```marksonnet
importstr 'example/sample.md'
```"#},
indoc! {r#"
Here we have a prefix.
# Sample!
This file is a sample.
"#}
);
}