diff --git a/data/demo-result.html b/data/demo-result.html new file mode 100644 index 0000000..d33d8da --- /dev/null +++ b/data/demo-result.html @@ -0,0 +1,12 @@ + + + + + + Document + + +

some content

+

this is some text and contains a link

+ + diff --git a/data/demo.ihl b/data/demo.ihl new file mode 100644 index 0000000..791956a --- /dev/null +++ b/data/demo.ihl @@ -0,0 +1,36 @@ +// defined base element +document [ + title: "example", + lang: "de", +] { + h1.class "some content"; + p { + "this is some text and contains "; + a [ href: "https://example.com" ] "a link"; + "."; + } +} + +// attributes are by default parsed as: +// key: value +// and are comma seperated. +// +// items can define custom parsers for attributes + +// content is by default parsed as semicolon seperated further item structures + +// plain text is just plain text + +// definitions have a : after the name +// definitions only allow default parsing +// @slot is the attribute that defines the element body, but can be overwritten +navItem: [ + dest: string, +] { + li.nav-item { + // use attributes using @ + // interpolate in @{} blocks + a[ href: "https://example.com@{dest}" ] @dest; + } +} + diff --git a/example.html b/example.html new file mode 100644 index 0000000..f2859cf --- /dev/null +++ b/example.html @@ -0,0 +1 @@ +test file!

hello world

this is a test file

some text followed by a hyperlink

diff --git a/src/element.rs b/src/element.rs new file mode 100644 index 0000000..b8fb57a --- /dev/null +++ b/src/element.rs @@ -0,0 +1,63 @@ +use std::{collections::HashMap, fmt::{Display, Write}}; + + +#[derive(Debug)] +pub struct Element<'a> { + pub name: &'a str, + pub attributes: Option>, + pub children: ElBody<'a>, +} + +impl<'a> Display for Element<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "<{0}{1}>{2}", + self.name, + if let Some(attrs) = &self.attributes { + let mut attributes = String::from(" "); + + attrs.iter().for_each(|(key, value)| { + attributes + .write_fmt(format_args!("{key}=\"{value}\"")) + .unwrap() + }); + + attributes + } else { + "".to_owned() + }, + self.children + ) + } +} + +/// the direct content of `children`, so it can be a plaintext element +#[derive(Debug)] +pub enum ElBody<'a> { + Elements(Vec>), + Text(&'a str), +} +impl<'a> Display for ElBody<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ElBody::Text(s) => f.write_str(s), + ElBody::Elements(els) => els.iter().try_for_each(|e| f.write_str(&e.to_string())), + } + } +} + +#[derive(Debug)] +pub enum ElContent<'a> { + El(Element<'a>), + Text(&'a str), +} + +impl<'a> Display for ElContent<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ElContent::Text(s) => f.write_str(s), + ElContent::El(el) => f.write_str(&el.to_string()), + } + } +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..d221a00 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,53 @@ +use std::collections::HashMap; + +use winnow::{IResult, combinator, character::alphanumeric1, branch, sequence::delimited, multi::many0, bytes::take_until0, Parser}; + +use crate::{element::{Element, ElBody, ElContent}, util::ws}; + +pub fn el_parser(input: &str) -> IResult<&str, Element> { + ( + name_parser, + combinator::opt(el_attrlist_parser), + el_body_parser, + ) + .map(|(name, attributes, children)| Element { + name, + attributes, + children, + }) + .parse_next(input) +} + +pub fn name_parser(input: &str) -> IResult<&str, &str> { + ws(alphanumeric1).parse_next(input) +} + +pub fn el_body_parser(input: &str) -> IResult<&str, ElBody> { + ws(branch::alt(( + string_parser.map(|s| ElBody::Text(s)), + delimited('{', many0(content_parser).map(|v| ElBody::Elements(v)), '}'), + ))) + .parse_next(input) +} + +pub fn el_attrlist_parser(input: &str) -> IResult<&str, HashMap<&str, &str>> { + ws(delimited('[', many0(attribute_parser), ']')).parse_next(input) +} + +pub fn attribute_parser(input: &str) -> IResult<&str, (&str, &str)> { + (name_parser, '=', ws(string_parser)) + .map(|(key, _, value)| (key, value)) + .parse_next(input) +} + +pub fn content_parser(input: &str) -> IResult<&str, ElContent> { + ws(branch::alt(( + string_parser.map(|s| ElContent::Text(s)), + el_parser.map(|e| ElContent::El(e)), + ))) + .parse_next(input) +} + +pub fn string_parser(input: &str) -> IResult<&str, &str> { + delimited('"', take_until0("\""), '"').parse_next(input) +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..cea83c5 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,10 @@ +use winnow::{error::ParseError, Parser, sequence::delimited, character::multispace0}; + + +// whitespace combinator from [winnow docs](https://docs.rs/winnow/latest/winnow/_topic/language/index.html#wrapper-combinators-that-eat-whitespace-before-and-after-a-parser) +pub fn ws<'a, F, O, E: ParseError<&'a str>>(inner: F) -> impl Parser<&'a str, O, E> +where + F: Parser<&'a str, O, E>, +{ + delimited(multispace0, inner, multispace0) +}