init
This commit is contained in:
commit
5cef0b4563
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1466
Cargo.lock
generated
Normal file
1466
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "html"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
html_parser = "0.7"
|
||||
minify-html = "0.15"
|
||||
grass = "0.13"
|
||||
toml = "0.8"
|
||||
walkdir = "2.5"
|
7
blueprint.html
Normal file
7
blueprint.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<A>
|
||||
<a href=@href>
|
||||
<bold>
|
||||
<tp:children />
|
||||
</bold>
|
||||
</a>
|
||||
</A>
|
5
default.toml
Normal file
5
default.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[compile]
|
||||
output="dist"
|
||||
templates="tp"
|
||||
source="src"
|
||||
minify=false
|
0
index.html
Normal file
0
index.html
Normal file
5
lg.toml
Normal file
5
lg.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[compile]
|
||||
output="dist"
|
||||
templates="tp"
|
||||
source="src"
|
||||
minify=false
|
12
simple.html
Normal file
12
simple.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<link>
|
||||
<lg:include rel='css' href="./style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<A href="fdfs"> asdf </A>
|
||||
</body>
|
||||
|
||||
</html>
|
0
src/README.md
Normal file
0
src/README.md
Normal file
278
src/compiler.rs
Normal file
278
src/compiler.rs
Normal file
|
@ -0,0 +1,278 @@
|
|||
use html_parser::{Dom, DomVariant, Element, ElementVariant, Node};
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use crate::{
|
||||
macros::*,
|
||||
trace::{WithContext, *},
|
||||
};
|
||||
|
||||
/// Replaces @variables in template with appropriate
|
||||
/// values, or removes if not provided
|
||||
fn init_vars(
|
||||
node: Node,
|
||||
attributes: &HashMap<String, Option<String>>,
|
||||
children: &Vec<Node>,
|
||||
) -> Node {
|
||||
let mut element = match node {
|
||||
Node::Text(_) | Node::Comment(_) => return node,
|
||||
Node::Element(e) => e,
|
||||
};
|
||||
|
||||
let mut new_classes = Vec::with_capacity(element.classes.len());
|
||||
|
||||
for cls in &mut element.classes {
|
||||
if !cls.starts_with('@') {
|
||||
new_classes.push(cls.clone());
|
||||
continue;
|
||||
}
|
||||
let name = &cls[1..];
|
||||
// Attributes in class must not be null
|
||||
if let Some(Some(k)) = attributes.get(name) {
|
||||
new_classes.push(k.clone());
|
||||
}
|
||||
// Otherwise discard variable from classes
|
||||
}
|
||||
element.classes = new_classes;
|
||||
|
||||
// Attributes in ID must not be null
|
||||
if let Some(id) = &element.id {
|
||||
if let Some(name) = id.strip_prefix('@') {
|
||||
element.id = attributes.get(name).unwrap_or(&None).clone();
|
||||
} else {
|
||||
element.id = None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_attributes = HashMap::new();
|
||||
|
||||
for (key, value) in &element.attributes {
|
||||
if let Some(id) = value {
|
||||
if let Some(variable_name) = id.strip_prefix('@') {
|
||||
if let Some(value) = attributes.get(variable_name) {
|
||||
// Insert null and non-null variables if key exists
|
||||
new_attributes.insert(key.clone(), value.clone());
|
||||
}
|
||||
// Otherwise discard variable
|
||||
}
|
||||
// Insert non-null non-variable value
|
||||
else {
|
||||
new_attributes.insert(key.clone(), value.clone());
|
||||
}
|
||||
} else {
|
||||
// Insert null non-variable value
|
||||
new_attributes.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
element.attributes = new_attributes;
|
||||
let mut new_children = Vec::with_capacity(element.children.len());
|
||||
for child in &element.children {
|
||||
let name = child.element().map(|c| c.name.as_str()).unwrap_or("");
|
||||
if "tp:children" == name {
|
||||
for child in children {
|
||||
new_children.push(child.clone());
|
||||
}
|
||||
} else {
|
||||
let child = init_vars(child.clone(), attributes, children);
|
||||
new_children.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
element.children = new_children;
|
||||
|
||||
Node::Element(element)
|
||||
}
|
||||
|
||||
/// Swaps out a template invocation for its definition
|
||||
fn expand_templates(
|
||||
invocation: Element,
|
||||
templates: &Element,
|
||||
) -> Result<Vec<Node>> {
|
||||
for child in &invocation.children {
|
||||
if child
|
||||
.element()
|
||||
.map(|c| c.name.as_str().strip_prefix("tp:").is_some())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Compilation,
|
||||
"Illegal use of tp namespace",
|
||||
));
|
||||
}
|
||||
for subchild in child {
|
||||
if subchild
|
||||
.element()
|
||||
.map(|c| c.name.as_str().strip_prefix("tp:").is_some())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(Error::new(
|
||||
ErrorKind::Compilation,
|
||||
"Illegal use of tp namespace",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Collect params
|
||||
let mut attributes = invocation.attributes;
|
||||
let classes = invocation.classes.join(" ");
|
||||
if classes.len() != 0 {
|
||||
attributes.insert("class".into(), Some(classes));
|
||||
}
|
||||
if let Some(id) = invocation.id {
|
||||
attributes.insert("id".into(), Some(id));
|
||||
}
|
||||
// Swap params
|
||||
let expanded = init_vars(
|
||||
Node::Element(templates.clone()),
|
||||
&attributes,
|
||||
&invocation.children,
|
||||
);
|
||||
Ok(expanded.element().unwrap().children.clone())
|
||||
}
|
||||
|
||||
/// Serializes HTML node
|
||||
/// * `node` - Node to serialize
|
||||
/// * `templates` - Map of templates by their names
|
||||
fn node_to_string(
|
||||
node: &Node,
|
||||
templates: &HashMap<String, Element>,
|
||||
macros: &HashMap<String, Macro>,
|
||||
) -> Result<String> {
|
||||
const OPEN: bool = false;
|
||||
const CLOSE: bool = true;
|
||||
let mut stack: Vec<(Node, bool)> = vec![(node.clone(), OPEN)];
|
||||
let mut buf = String::new();
|
||||
while let Some((current, closing)) = stack.pop() {
|
||||
if closing == OPEN {
|
||||
stack.push((current.clone(), CLOSE));
|
||||
match current {
|
||||
Node::Text(t) => {
|
||||
buf.push_str(&t);
|
||||
},
|
||||
Node::Element(e) => {
|
||||
// Expand if macro
|
||||
if let Some(mc) = macros.get(&e.name) {
|
||||
stack.pop().unwrap();
|
||||
let expanded =
|
||||
mc(&e.attributes).ctx(format!("Expanding macro: {}", e.name))?;
|
||||
buf.push_str(&expanded);
|
||||
continue;
|
||||
}
|
||||
// Expand if template
|
||||
if let Some(tp) = templates.get(&e.name) {
|
||||
let _ = stack.pop();
|
||||
let elements = expand_templates(e.clone(), tp)
|
||||
.ctx(format!("Expanding template: {}", e.name))?;
|
||||
for element in elements.into_iter().rev() {
|
||||
stack.push((element, OPEN));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// <
|
||||
buf.push('<');
|
||||
// Tag name
|
||||
buf.push_str(&e.name);
|
||||
// Classes
|
||||
if !e.classes.is_empty() {
|
||||
buf.push_str(&format!(" class='{}'", e.classes.join(" ")));
|
||||
}
|
||||
// ID
|
||||
if let Some(id) = &e.id {
|
||||
buf.push_str(&format!(" id='{}'", id));
|
||||
}
|
||||
// Attributes
|
||||
for (k, v) in &e.attributes {
|
||||
match v {
|
||||
Some(v) => buf.push_str(&format!(" {}='{}'", k, v)),
|
||||
None => buf.push_str(&format!(" {}", k)),
|
||||
}
|
||||
}
|
||||
match e.variant {
|
||||
ElementVariant::Normal => {
|
||||
buf.push_str(">\n");
|
||||
for child in e.children.iter().rev() {
|
||||
stack.push((child.clone(), OPEN));
|
||||
}
|
||||
},
|
||||
ElementVariant::Void => {
|
||||
buf.push_str("/>\n");
|
||||
let _ = stack.pop();
|
||||
},
|
||||
}
|
||||
},
|
||||
Node::Comment(_) => {},
|
||||
}
|
||||
} else {
|
||||
match current {
|
||||
Node::Text(_) => buf.push('\n'),
|
||||
Node::Comment(_) => {},
|
||||
Node::Element(e) => {
|
||||
buf.push_str(&format!("</{}>\n", e.name));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
pub struct Compiler {
|
||||
templates: HashMap<String, Element>,
|
||||
macros: HashMap<String, Macro>,
|
||||
}
|
||||
|
||||
impl Compiler {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
templates: HashMap::new(),
|
||||
macros: default_macros(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_templates_file(&mut self, path: impl AsRef<Path>) -> Result<()> {
|
||||
let file = read_file(&path)?;
|
||||
self.parse_templates(&file).ctx(format!(
|
||||
"Parsing templates file: {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_templates(&mut self, html: &str) -> Result<()> {
|
||||
let tps: HashMap<String, Element> = Dom::parse(html)?
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|i| i.element().cloned())
|
||||
.flatten()
|
||||
.map(|i| (i.name.clone(), i))
|
||||
.collect();
|
||||
if tps.is_empty() {
|
||||
return Err(Error::new(ErrorKind::Parsing, "No blueprints found"));
|
||||
}
|
||||
self.templates.extend(tps);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn compile_source_file(&self, path: impl AsRef<Path>) -> Result<String> {
|
||||
let file = read_file(&path)?;
|
||||
self.compile_source(&file).ctx(format!(
|
||||
"Compiling source file: {}",
|
||||
path.as_ref().to_string_lossy()
|
||||
))
|
||||
}
|
||||
|
||||
pub fn compile_source(&self, html: &str) -> Result<String> {
|
||||
let dom = Dom::parse(html)?;
|
||||
if !dom.errors.is_empty() {}
|
||||
if dom.tree_type == DomVariant::Empty {
|
||||
return Err(Error::new(ErrorKind::Parsing, "Empty DOM"));
|
||||
}
|
||||
if dom.tree_type != DomVariant::Document {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Parsing,
|
||||
"DOM must exactly have 1 root node",
|
||||
));
|
||||
}
|
||||
let tree = &dom.children[0];
|
||||
node_to_string(&tree, &self.templates, &self.macros)
|
||||
}
|
||||
}
|
32
src/macros.rs
Normal file
32
src/macros.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use crate::trace::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn default_macros() -> HashMap<String, Macro> {
|
||||
let mut hm: HashMap<String, Macro> = HashMap::new();
|
||||
hm.insert("lg:include".into(), INCLUDE_MACRO);
|
||||
hm
|
||||
}
|
||||
|
||||
pub type Macro = fn(&HashMap<String, Option<String>>) -> Result<String>;
|
||||
|
||||
const INCLUDE_MACRO: Macro = |input| {
|
||||
let href = input
|
||||
.get("href")
|
||||
.ok_or(compile_error("'href' attribute missing"))?
|
||||
.as_ref()
|
||||
.ok_or(compile_error("'href' attribute is empty"))?;
|
||||
|
||||
let rel = input
|
||||
.get("rel")
|
||||
.ok_or(compile_error("'rel' attribute missing"))?
|
||||
.as_ref()
|
||||
.ok_or(compile_error("'rel' attribute is empty"))?;
|
||||
|
||||
let r = match rel.as_str() {
|
||||
"css" => format!("<style>\n{}\n</style>\n", read_file(href)?),
|
||||
"js" => format!("<script>\n{}\n</script>\n", read_file(href)?),
|
||||
"inline" => read_file(href)?,
|
||||
_ => return Err(compile_error(format!("Invalid 'rel' value: {}", rel))),
|
||||
};
|
||||
Ok(r)
|
||||
};
|
81
src/main.rs
Normal file
81
src/main.rs
Normal file
|
@ -0,0 +1,81 @@
|
|||
mod compiler;
|
||||
mod macros;
|
||||
mod parser;
|
||||
mod trace;
|
||||
use compiler::*;
|
||||
use trace::*;
|
||||
|
||||
fn run_compiler() -> Result<()> {
|
||||
// Read config file
|
||||
let default_config = include_str!("../default.toml")
|
||||
.parse::<toml::Table>()
|
||||
.unwrap();
|
||||
let user_config = std::fs::read_to_string("lg.toml")
|
||||
.ctx("Reading config file")?
|
||||
.parse::<toml::Table>()
|
||||
.ctx("Parsing config file")?;
|
||||
let mut config = default_config.clone();
|
||||
config.extend(user_config);
|
||||
|
||||
let mut c = Compiler::new();
|
||||
let compile_table = config["compile"]
|
||||
.as_table()
|
||||
.ctx("Reading compile options")?;
|
||||
|
||||
// Parse all templates
|
||||
let templates_path = compile_table["templates"]
|
||||
.as_str()
|
||||
.ctx("Reading templates path")?;
|
||||
let dirs = walkdir::WalkDir::new(templates_path)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|v| {
|
||||
if v.file_name().to_str().unwrap_or("").ends_with(".html")
|
||||
&& v.file_type().is_file()
|
||||
{
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
for file in dirs {
|
||||
c.parse_templates_file(file.path())?;
|
||||
}
|
||||
// Compile all source files
|
||||
let source_path = compile_table["source"]
|
||||
.as_str()
|
||||
.ctx("Reading templates path")?;
|
||||
let dirs = walkdir::WalkDir::new(source_path)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|v| {
|
||||
if v.file_name().to_str().unwrap_or("").ends_with(".html")
|
||||
&& v.file_type().is_file()
|
||||
{
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
for file in dirs {}
|
||||
|
||||
// println!("{}", s);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let test = include_str!("../simple.html").to_string();
|
||||
// let test = " <as> </as> ".to_string();
|
||||
let r = parser::parse_html(&test);
|
||||
for l in r {
|
||||
println!("{l:?}");
|
||||
}
|
||||
// match run_compiler() {
|
||||
// Ok(_) => {},
|
||||
// Err(e) => {
|
||||
// eprintln!("{}", e);
|
||||
// std::process::exit(1);
|
||||
// },
|
||||
// }
|
||||
}
|
199
src/parser.rs
Normal file
199
src/parser.rs
Normal file
|
@ -0,0 +1,199 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::trace::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Lexeme<'a> {
|
||||
OpenTag {
|
||||
name: &'a str,
|
||||
attributes: HashMap<&'a str, Option<&'a str>>,
|
||||
is_void: bool,
|
||||
},
|
||||
CloseTag {
|
||||
name: &'a str,
|
||||
},
|
||||
Content(&'a str),
|
||||
}
|
||||
|
||||
fn normalize_whitespace(s: &str) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn error(message: impl Into<String>) -> Error {
|
||||
Error {
|
||||
kind: ErrorKind::Parsing,
|
||||
reason: message.into(),
|
||||
backtrace: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Try parsing single specific character
|
||||
fn parse_char(i: &str, c: char) -> Option<(&str, &str)> {
|
||||
if i.starts_with(c) {
|
||||
Some((&i[0..1], &i[1..]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Parse until condition is not true for next character
|
||||
fn parse_while(tail: &str, condition: impl Fn(char) -> bool) -> (&str, &str) {
|
||||
let mut end;
|
||||
let mut it = tail.char_indices();
|
||||
'outer: loop {
|
||||
match it.next() {
|
||||
Some((i, c)) => {
|
||||
end = i;
|
||||
if !condition(c) {
|
||||
break 'outer;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
// Reached end of input
|
||||
return (&tail, "");
|
||||
},
|
||||
};
|
||||
}
|
||||
(&tail[0..end], &tail[end..])
|
||||
}
|
||||
|
||||
fn parse_whitespace(i: &str) -> (&str, &str) {
|
||||
parse_while(i, |c| c.is_whitespace())
|
||||
}
|
||||
|
||||
fn parse_doctype(tail: &str) -> Option<(&str, &str)> {
|
||||
const doctype_str = "<!DOCTYPE>"
|
||||
}
|
||||
|
||||
/// Try parsing all characters between two delimiter
|
||||
/// characters
|
||||
fn parse_delimited(i: &str, delimiter: char) -> Option<(&str, &str)> {
|
||||
let (_, tail) = parse_char(i, delimiter)?;
|
||||
let (value, tail) = parse_while(tail, |c| c != delimiter);
|
||||
let (_, tail) = parse_char(tail, delimiter)?;
|
||||
Some((value, tail))
|
||||
}
|
||||
|
||||
fn parse_tag_name(i: &str) -> Option<(&str, &str)> {
|
||||
let (value, tail) = parse_while(i, |c| c.is_ascii_alphanumeric() || c == ':');
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((value, tail))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_attribute_key(i: &str) -> Option<(&str, &str)> {
|
||||
let (value, tail) = parse_while(i, |c| {
|
||||
!(['"', '\'', '>', '/', '='].contains(&c) || c.is_control())
|
||||
});
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((value, tail))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_attribute_val(i: &str) -> Option<(&str, &str)> {
|
||||
const SINGLE_QUOTE: char = '\'';
|
||||
const DOUBLE_QUOTE: char = '"';
|
||||
let (value, tail) = parse_delimited(i, '\'') // Single quote delimit
|
||||
.or_else(|| parse_delimited(i, '"')) // Double quote delimit
|
||||
.or_else(|| { // Unquoted
|
||||
Some(parse_while(i, |c| {
|
||||
!(c.is_whitespace()
|
||||
|| [SINGLE_QUOTE, DOUBLE_QUOTE, '=', '<', '>', '`'].contains(&c))
|
||||
}))
|
||||
})?;
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((value, tail))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Option<((key, value), tail)>
|
||||
fn parse_key_val(tail: &str) -> Option<((&str, Option<&str>), &str)> {
|
||||
// Require whitespace
|
||||
let (ws, tail) = parse_whitespace(tail);
|
||||
if ws.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Fail when no key found
|
||||
let (key, tail) = parse_attribute_key(tail)?;
|
||||
let (_, tail) = parse_whitespace(tail);
|
||||
if let Some((_, tail)) = parse_char(tail, '=') {
|
||||
let (_, tail) = parse_whitespace(tail);
|
||||
// Fail when = is not followed by value
|
||||
let (val, tail) = parse_attribute_val(tail)?;
|
||||
Some(((key, Some(val)), tail))
|
||||
} else {
|
||||
Some(((key, None), tail))
|
||||
}
|
||||
}
|
||||
|
||||
const VOID_ELEMENTS: [&str; 16] = [
|
||||
"area", "base", "br", "col", "command", "embed", "hr", "img", "input",
|
||||
"keygen", "link", "meta", "param", "source", "track", "wbr",
|
||||
];
|
||||
|
||||
fn parse_open_tag(tail: &str) -> Option<(Lexeme, &str)> {
|
||||
// <
|
||||
let (_, tail) = parse_char(tail, '<')?;
|
||||
// tag name
|
||||
let (name, mut tail) = parse_tag_name(tail)?;
|
||||
// attributes
|
||||
let mut attributes: HashMap<&str, Option<&str>> = HashMap::new();
|
||||
while let Some((kv, new_tail)) = parse_key_val(tail) {
|
||||
attributes.insert(kv.0, kv.1);
|
||||
tail = new_tail;
|
||||
}
|
||||
let (_, tail) = parse_whitespace(tail);
|
||||
let (is_void, tail) = parse_char(tail, '/').unwrap_or(("", tail));
|
||||
let is_void = !is_void.is_empty() || VOID_ELEMENTS.contains(&name);
|
||||
let (_, tail) = parse_char(tail, '>')?;
|
||||
Some((
|
||||
Lexeme::OpenTag {
|
||||
name,
|
||||
attributes,
|
||||
is_void,
|
||||
},
|
||||
tail,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_close_tag(tail: &str) -> Option<(Lexeme, &str)> {
|
||||
let (_, tail) = parse_char(tail, '<')?;
|
||||
let (_, tail) = parse_char(tail, '/')?;
|
||||
let (name, tail) = parse_tag_name(tail)?;
|
||||
let (_, tail) = parse_whitespace(tail);
|
||||
let (_, tail) = parse_char(tail, '>')?;
|
||||
Some((Lexeme::CloseTag { name }, tail))
|
||||
}
|
||||
|
||||
fn parse_text(tail: &str) -> Option<(Lexeme, &str)> {
|
||||
let (txt, tail) = parse_while(tail, |c| c != '<');
|
||||
if txt.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((Lexeme::Content(txt), tail))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_html(mut tail: &str) -> Vec<Lexeme> {
|
||||
let mut stack = vec![];
|
||||
while !tail.is_empty() {
|
||||
let (_, new_tail) = parse_whitespace(tail);
|
||||
if new_tail.is_empty() {
|
||||
break;
|
||||
}
|
||||
let (lm, new_tail) = parse_open_tag(new_tail)
|
||||
.or_else(|| parse_close_tag(new_tail))
|
||||
.or_else(|| parse_text(new_tail))
|
||||
.unwrap();
|
||||
stack.push(lm);
|
||||
tail = new_tail;
|
||||
}
|
||||
stack
|
||||
}
|
137
src/trace.rs
Normal file
137
src/trace.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
use std::{fmt::Display, path::Path};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub struct Error {
|
||||
pub kind: ErrorKind,
|
||||
pub reason: String,
|
||||
pub backtrace: Vec<String>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(kind: ErrorKind, reason: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
reason: reason.into(),
|
||||
backtrace: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn msg(mut self, message: impl Into<String>) -> Self {
|
||||
self.reason = message.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compile_error(reason: impl Into<String>) -> Error {
|
||||
Error {
|
||||
kind: ErrorKind::Compilation,
|
||||
reason: reason.into(),
|
||||
backtrace: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_file(p: impl AsRef<Path>) -> Result<String> {
|
||||
std::fs::read_to_string(&p)
|
||||
.ctx(format!("Opening file: {}", p.as_ref().to_string_lossy()))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorKind {
|
||||
IO,
|
||||
Parsing,
|
||||
Compilation,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:?} error\nReason:\n\t{}\nBacktrace:\n",
|
||||
self.kind, self.reason
|
||||
)?;
|
||||
for s in self.backtrace.iter().rev() {
|
||||
write!(f, "\t{}\n", s)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self {
|
||||
kind: ErrorKind::IO,
|
||||
reason: format!("{}", value),
|
||||
backtrace: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<html_parser::Error> for Error {
|
||||
fn from(value: html_parser::Error) -> Self {
|
||||
match value {
|
||||
html_parser::Error::Parsing(e) => Self {
|
||||
kind: ErrorKind::Parsing,
|
||||
reason: e,
|
||||
backtrace: vec![],
|
||||
},
|
||||
html_parser::Error::Cli(e) => Self {
|
||||
kind: ErrorKind::Unknown,
|
||||
reason: e,
|
||||
backtrace: vec![],
|
||||
},
|
||||
html_parser::Error::IO(e) => Self {
|
||||
kind: ErrorKind::IO,
|
||||
reason: format!("{}", e),
|
||||
backtrace: vec![],
|
||||
},
|
||||
html_parser::Error::Serde(e) => Self {
|
||||
kind: ErrorKind::Unknown,
|
||||
reason: format!("{}", e),
|
||||
backtrace: vec![],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for Error {
|
||||
fn from(value: toml::de::Error) -> Self {
|
||||
Error::new(ErrorKind::Parsing, value.message())
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WithContext<T, S: Into<String>> {
|
||||
fn ctx(self, s: S) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<T, E, S> WithContext<T, S> for std::result::Result<T, E>
|
||||
where
|
||||
E: Into<Error>,
|
||||
S: Into<String>,
|
||||
{
|
||||
fn ctx(self, s: S) -> Result<T> {
|
||||
self.map_err(|e| e.into()).map_err(|mut e| {
|
||||
e.backtrace.push(s.into());
|
||||
e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> WithContext<T, S> for std::option::Option<T>
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
fn ctx(self, s: S) -> Result<T> {
|
||||
match self {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(Error::new(ErrorKind::Unknown, "Missing expected value")),
|
||||
}
|
||||
}
|
||||
}
|
8
style.css
Normal file
8
style.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
body {
|
||||
background-color: linen;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: maroon;
|
||||
margin-left: 40px;
|
||||
}
|
91
test.html
Normal file
91
test.html
Normal file
|
@ -0,0 +1,91 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="test_files/main-ad0a5132b4027392.css">
|
||||
|
||||
<link rel="preload" href="https://logan-gatlin.com/web-2abb0afbb41a1a01_bg.wasm" as="fetch" type="application/wasm"
|
||||
crossorigin="">
|
||||
<link rel="modulepreload" href="https://logan-gatlin.com/web-2abb0afbb41a1a01.js">
|
||||
<link
|
||||
href="data:text/css,%3Ais(%5Bid*%3D'google_ads_iframe'%5D%2C%5Bid*%3D'taboola-'%5D%2C.taboolaHeight%2C.taboola-placeholder%2C%23credential_picker_container%2C%23credentials-picker-container%2C%23credential_picker_iframe%2C%5Bid*%3D'google-one-tap-iframe'%5D%2C%23google-one-tap-popup-container%2C.google-one-tap-modal-div%2C%23amp_floatingAdDiv%2C%23ez-content-blocker-container)%20%7Bdisplay%3Anone!important%3Bmin-height%3A0!important%3Bheight%3A0!important%3B%7D"
|
||||
rel="stylesheet" type="text/css">
|
||||
<link
|
||||
href="data:text/css,%3Ais(%5Bid*%3D'google_ads_iframe'%5D%2C%5Bid*%3D'taboola-'%5D%2C.taboolaHeight%2C.taboola-placeholder%2C%23credential_picker_container%2C%23credentials-picker-container%2C%23credential_picker_iframe%2C%5Bid*%3D'google-one-tap-iframe'%5D%2C%23google-one-tap-popup-container%2C.google-one-tap-modal-div%2C%23amp_floatingAdDiv%2C%23ez-content-blocker-container)%20%7Bdisplay%3Anone!important%3Bmin-height%3A0!important%3Bheight%3A0!important%3B%7D"
|
||||
rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
||||
<script type="module">import init from '/web-2abb0afbb41a1a01.js'; init('/web-2abb0afbb41a1a01_bg.wasm');</script>
|
||||
<main class="container">
|
||||
<article>
|
||||
<header>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ul>
|
||||
<li><a href="https://logan-gatlin.com/">Home</a></li><!----><!---->
|
||||
<li><a class="contrast">Base Conversion</a></li><!----><!---->
|
||||
</ul>
|
||||
</nav>
|
||||
</header><!---->
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div><input type="text" placeholder="Decimal number" value="0"></div>
|
||||
<table role="grid">
|
||||
<tr>
|
||||
<td role="button" type="button" class="secondary">HEX</td><!---->
|
||||
<td>00<!----></td><!---->
|
||||
</tr>
|
||||
<tr>
|
||||
<td role="button" type="button" class="secondary contrast">DEC</td><!---->
|
||||
<td>0<!----></td><!---->
|
||||
</tr>
|
||||
<tr>
|
||||
<td role="button" type="button" class="secondary">OCT</td><!---->
|
||||
<td>000<!----></td><!---->
|
||||
</tr>
|
||||
<tr>
|
||||
<td role="button" type="button" class="secondary">BIN</td><!---->
|
||||
<td>0000 0000<!----></td><!---->
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td role="button" type="button" class="secondary contrast">BYTE</td><!---->
|
||||
<td role="button" type="button" class="secondary">WORD</td><!---->
|
||||
<td role="button" type="button" class="secondary">DWORD</td><!---->
|
||||
<td role="button" type="button" class="secondary">QWORD</td><!---->
|
||||
</tr>
|
||||
</table><!----><!---->
|
||||
</div>
|
||||
<div>
|
||||
<table class="binary">
|
||||
<tr>
|
||||
<td type="button" role="button" class="secondary">0<!----></td>
|
||||
<td type="button" role="button" class="secondary">0<!----></td>
|
||||
<td type="button" role="button" class="secondary">0<!----></td>
|
||||
<td type="button" role="button" class="secondary">0<!----></td>
|
||||
<td type="button" role="button" class="secondary">0<!----></td>
|
||||
<td type="button" role="button" class="secondary">0<!----></td>
|
||||
<td type="button" role="button" class="secondary">0<!----></td>
|
||||
<td type="button" role="button" class="secondary">0<!----></td><!---->
|
||||
</tr><!----><!----><!---->
|
||||
</table><!---->
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div><button type="button">Remember</button><!----><button type="button" class="secondary">Clear
|
||||
history</button><!---->
|
||||
<table><!----><!----></table><!---->
|
||||
</div>
|
||||
<footer><a href="https://github.com/Xterminate1818/based" target="_blank">View source code on GitHub</a><!---->
|
||||
</footer>
|
||||
</article><!----><!----><!---->
|
||||
</main><!----><!----><!---->
|
||||
</body>
|
||||
|
||||
</html>
|
4
test_files/main-ad0a5132b4027392.css
Normal file
4
test_files/main-ad0a5132b4027392.css
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue