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}{0}>",
+ 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)
+}