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