Initial commit
This commit is contained in:
commit
28d7f26db6
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
45
.vscode/launch.json
vendored
Normal file
45
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug executable 'roxy'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=roxy",
|
||||
"--package=roxy"
|
||||
],
|
||||
"filter": {
|
||||
"name": "roxy",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug unit tests in executable 'roxy'",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"test",
|
||||
"--no-run",
|
||||
"--bin=roxy",
|
||||
"--package=roxy"
|
||||
],
|
||||
"filter": {
|
||||
"name": "roxy",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "roxy"
|
||||
version = "0.1.0"
|
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "roxy"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
1
fixtures/index.html
Normal file
1
fixtures/index.html
Normal file
@ -0,0 +1 @@
|
||||
<body><h1>Hello <a>link</a> there</h1><input type="email" name="hello" disabled /><!-- hello --></body>
|
1
fixtures/style.css
Normal file
1
fixtures/style.css
Normal file
@ -0,0 +1 @@
|
||||
h1 { color: #00ff00; } .big {font-size: 12px; } h1.some#chain { width: auto; }
|
106
src/dom.rs
Normal file
106
src/dom.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use core::fmt;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AttrValue {
|
||||
Text(String),
|
||||
Implicit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AttrMap(pub HashMap<String, AttrValue>);
|
||||
|
||||
impl fmt::Display for AttrMap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let i = self
|
||||
.0
|
||||
.iter()
|
||||
.map(|(key, value)| match &value {
|
||||
AttrValue::Text(text) => format!("{}=\"{}\"", key, text),
|
||||
AttrValue::Implicit => format!("{}", key),
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
write!(f, "{}", i.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ElementData {
|
||||
tag_name: String,
|
||||
attributes: AttrMap,
|
||||
child_nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NodeType {
|
||||
ElementNode(ElementData),
|
||||
TextNode(String),
|
||||
CommentNode(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Node {
|
||||
node_type: NodeType,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn pretty_print(&self, f: &mut fmt::Formatter<'_>, indent: usize) {
|
||||
let prepadding = " ".repeat(indent);
|
||||
match &self.node_type {
|
||||
NodeType::ElementNode(data) => {
|
||||
write!(f, "{}<{}", prepadding, data.tag_name).unwrap();
|
||||
|
||||
if data.attributes.0.len() > 0 {
|
||||
write!(f, " {}", data.attributes).unwrap();
|
||||
}
|
||||
|
||||
if data.child_nodes.len() == 0 {
|
||||
writeln!(f, "></{}>", data.tag_name).unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(f, ">").unwrap();
|
||||
|
||||
let _ = &data
|
||||
.child_nodes
|
||||
.iter()
|
||||
.for_each(|node| node.pretty_print(f, indent + 1));
|
||||
|
||||
writeln!(f, "{}</{}>", prepadding, data.tag_name).unwrap();
|
||||
}
|
||||
NodeType::TextNode(text) => {
|
||||
writeln!(f, "{}{}", prepadding, text).unwrap();
|
||||
}
|
||||
NodeType::CommentNode(text) => writeln!(f, "{}<!-- {} -->", prepadding, text).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Node {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.pretty_print(f, 0);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(data: String) -> Node {
|
||||
Node {
|
||||
node_type: NodeType::TextNode(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element(name: String, attrs: AttrMap, children: Vec<Node>) -> Node {
|
||||
Node {
|
||||
node_type: NodeType::ElementNode(ElementData {
|
||||
tag_name: name,
|
||||
attributes: attrs,
|
||||
child_nodes: children,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn comment(text: String) -> Node {
|
||||
Node {
|
||||
node_type: NodeType::CommentNode(text),
|
||||
}
|
||||
}
|
174
src/html.rs
Normal file
174
src/html.rs
Normal file
@ -0,0 +1,174 @@
|
||||
#![allow(dead_code)]
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::dom::{comment, element, text, AttrMap, AttrValue, Node};
|
||||
|
||||
struct Parser {
|
||||
pos: usize,
|
||||
input: String,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn next_char(&self) -> char {
|
||||
self.input[self.pos..].chars().next().unwrap()
|
||||
}
|
||||
|
||||
fn starts_with(&self, s: &str) -> bool {
|
||||
self.input[self.pos..].starts_with(s)
|
||||
}
|
||||
|
||||
fn eof(&self) -> bool {
|
||||
self.pos >= self.input.len()
|
||||
}
|
||||
|
||||
fn consume_char(&mut self) -> char {
|
||||
let mut iter = self.input[self.pos..].char_indices();
|
||||
let (_, cur_char) = iter.next().unwrap();
|
||||
let (next_pos, _) = iter.next().unwrap_or((1, ' '));
|
||||
self.pos += next_pos;
|
||||
return cur_char;
|
||||
}
|
||||
|
||||
fn consume_while<F>(&mut self, test: F) -> String
|
||||
where
|
||||
F: Fn(char) -> bool,
|
||||
{
|
||||
let mut result = String::new();
|
||||
while !self.eof() && test(self.next_char()) {
|
||||
result.push(self.consume_char());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn consume_whitespace(&mut self) {
|
||||
self.consume_while(|c| c.is_whitespace());
|
||||
}
|
||||
|
||||
fn parse_tag_name(&mut self) -> String {
|
||||
self.consume_while(|c| c.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
fn parse_node(&mut self) -> Node {
|
||||
if self.starts_with("<!--") {
|
||||
return self.parse_comment();
|
||||
}
|
||||
|
||||
match self.next_char() {
|
||||
'<' => self.parse_element(),
|
||||
_ => self.parse_text(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_text(&mut self) -> Node {
|
||||
text(self.consume_while(|c| c != '<'))
|
||||
}
|
||||
|
||||
fn parse_element(&mut self) -> Node {
|
||||
// Opening tag.
|
||||
assert!(self.consume_char() == '<');
|
||||
let tag_name = self.parse_tag_name();
|
||||
let attrs = self.parse_attributes();
|
||||
|
||||
// Optional self-closing
|
||||
let next_char = self.consume_char();
|
||||
if next_char == '/' {
|
||||
assert!(self.consume_char() == '>');
|
||||
return element(tag_name, attrs, vec![]);
|
||||
}
|
||||
assert!(next_char == '>');
|
||||
|
||||
// Contents.
|
||||
let children = self.parse_nodes();
|
||||
|
||||
// Closing tag.
|
||||
assert!(self.consume_char() == '<');
|
||||
assert!(self.consume_char() == '/');
|
||||
assert!(self.parse_tag_name() == tag_name);
|
||||
assert!(self.consume_char() == '>');
|
||||
|
||||
return element(tag_name, attrs, children);
|
||||
}
|
||||
|
||||
fn parse_comment(&mut self) -> Node {
|
||||
assert!(self.starts_with("<!--"));
|
||||
assert!(self.consume_char() == '<');
|
||||
assert!(self.consume_char() == '!');
|
||||
assert!(self.consume_char() == '-');
|
||||
assert!(self.consume_char() == '-');
|
||||
let mut result = String::new();
|
||||
loop {
|
||||
let c = self.consume_char();
|
||||
if c == '-' && self.starts_with("->") {
|
||||
break;
|
||||
}
|
||||
result.push(c);
|
||||
}
|
||||
assert!(self.consume_char() == '-');
|
||||
assert!(self.consume_char() == '>');
|
||||
comment(result)
|
||||
}
|
||||
|
||||
fn parse_attributes(&mut self) -> AttrMap {
|
||||
let mut attributes = HashMap::new();
|
||||
loop {
|
||||
self.consume_whitespace();
|
||||
|
||||
let next_char = self.next_char();
|
||||
if next_char == '>' || next_char == '/' {
|
||||
break;
|
||||
}
|
||||
|
||||
let (name, value) = self.parse_attr();
|
||||
if value.len() > 0 {
|
||||
attributes.insert(name, AttrValue::Text(value));
|
||||
} else {
|
||||
attributes.insert(name, AttrValue::Implicit);
|
||||
}
|
||||
}
|
||||
return AttrMap(attributes);
|
||||
}
|
||||
|
||||
fn parse_attr_value(&mut self) -> String {
|
||||
let open_quote = self.consume_char();
|
||||
assert!(open_quote == '"' || open_quote == '\'');
|
||||
let value = self.consume_while(|c| c != open_quote);
|
||||
assert!(self.consume_char() == open_quote);
|
||||
return value;
|
||||
}
|
||||
|
||||
fn parse_attr(&mut self) -> (String, String) {
|
||||
let name = self.parse_tag_name();
|
||||
if self.consume_char() == '=' {
|
||||
let value = self.parse_attr_value();
|
||||
return (name, value);
|
||||
}
|
||||
|
||||
return (name, String::new());
|
||||
}
|
||||
|
||||
fn parse_nodes(&mut self) -> Vec<Node> {
|
||||
let mut nodes = Vec::new();
|
||||
loop {
|
||||
self.consume_whitespace();
|
||||
if self.eof() || self.starts_with("</") {
|
||||
break;
|
||||
}
|
||||
nodes.push(self.parse_node());
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(source: String) -> Node {
|
||||
let mut parser = Parser {
|
||||
pos: 0,
|
||||
input: source,
|
||||
};
|
||||
let mut nodes = parser.parse_nodes();
|
||||
|
||||
if nodes.len() == 1 {
|
||||
nodes.swap_remove(0)
|
||||
} else {
|
||||
element("html".into(), AttrMap::default(), nodes)
|
||||
}
|
||||
}
|
335
src/main.rs
Normal file
335
src/main.rs
Normal file
@ -0,0 +1,335 @@
|
||||
use core::fmt;
|
||||
use std::io;
|
||||
|
||||
mod dom;
|
||||
mod html;
|
||||
|
||||
struct Parser {
|
||||
pos: usize,
|
||||
input: String,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn eof(&self) -> bool {
|
||||
self.pos >= self.input.len()
|
||||
}
|
||||
|
||||
fn consume_while<F>(&mut self, test: F) -> String
|
||||
where
|
||||
F: Fn(char) -> bool,
|
||||
{
|
||||
let mut result = String::new();
|
||||
while !self.eof() && test(self.next_char()) {
|
||||
result.push(self.consume_char());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn next_char(&self) -> char {
|
||||
self.input[self.pos..].chars().next().unwrap()
|
||||
}
|
||||
|
||||
fn consume_char(&mut self) -> char {
|
||||
let mut iter = self.input[self.pos..].char_indices();
|
||||
let (_, cur_char) = iter.next().unwrap();
|
||||
let (next_pos, _) = iter.next().unwrap_or((1, ' '));
|
||||
self.pos += next_pos;
|
||||
return cur_char;
|
||||
}
|
||||
|
||||
fn consume_whitespace(&mut self) {
|
||||
self.consume_while(|c| c.is_whitespace());
|
||||
}
|
||||
|
||||
fn parse_single_selector(&mut self) -> SingleSelector {
|
||||
let mut selector = SingleSelector::default();
|
||||
while !self.eof() {
|
||||
self.consume_whitespace();
|
||||
match self.next_char() {
|
||||
'#' => {
|
||||
self.consume_char();
|
||||
assert!(self.next_char().is_ascii_alphanumeric());
|
||||
selector.id = Some(self.parse_identifier());
|
||||
}
|
||||
'.' => {
|
||||
self.consume_char();
|
||||
assert!(self.next_char().is_ascii_alphanumeric());
|
||||
selector.classes.push(self.parse_identifier());
|
||||
}
|
||||
'*' => {
|
||||
self.consume_char();
|
||||
assert!(self.next_char().is_ascii_whitespace());
|
||||
}
|
||||
'{' => {
|
||||
break;
|
||||
}
|
||||
c => {
|
||||
if c.is_ascii_alphanumeric() == false {
|
||||
break;
|
||||
}
|
||||
selector.tag_name = Some(self.parse_identifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
return selector;
|
||||
}
|
||||
|
||||
fn parse_identifier(&mut self) -> String {
|
||||
self.consume_while(|c| (c.is_ascii_alphanumeric() || c == '-'))
|
||||
}
|
||||
|
||||
fn parse_rule(&mut self) -> Rule {
|
||||
Rule {
|
||||
selectors: self.parse_selectors(),
|
||||
declarations: self.parse_declarations(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_selectors(&mut self) -> Vec<Selector> {
|
||||
let mut selectors = Vec::new();
|
||||
loop {
|
||||
selectors.push(Selector::Single(self.parse_single_selector()));
|
||||
self.consume_whitespace();
|
||||
match self.next_char() {
|
||||
',' => {
|
||||
self.consume_char();
|
||||
self.consume_whitespace();
|
||||
}
|
||||
'{' => break,
|
||||
c => panic!("Unexpected character {} in selector list", c),
|
||||
}
|
||||
}
|
||||
return selectors;
|
||||
}
|
||||
|
||||
fn parse_declarations(&mut self) -> Vec<Declaration> {
|
||||
assert!(self.consume_char() == '{');
|
||||
let mut result = Vec::new();
|
||||
while !self.eof() {
|
||||
self.consume_whitespace();
|
||||
if self.next_char() == '}' {
|
||||
self.consume_char();
|
||||
break;
|
||||
}
|
||||
let identifier = self.parse_identifier();
|
||||
self.consume_whitespace();
|
||||
assert!(self.consume_char() == ':');
|
||||
self.consume_whitespace();
|
||||
let value = self.parse_declaration_value();
|
||||
result.push(Declaration {
|
||||
name: identifier,
|
||||
value: value,
|
||||
});
|
||||
self.consume_whitespace();
|
||||
assert!(self.consume_char() == ';');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn parse_declaration_value(&mut self) -> Value {
|
||||
match self.next_char() {
|
||||
'0'..='9' => self.parse_length(),
|
||||
'#' => self.parse_color(),
|
||||
_ => Value::Keyword(self.parse_identifier()),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hex_pair(&mut self) -> u8 {
|
||||
let s = &self.input[self.pos..self.pos + 2];
|
||||
self.pos += 2;
|
||||
u8::from_str_radix(s, 16).unwrap()
|
||||
}
|
||||
|
||||
fn parse_color(&mut self) -> Value {
|
||||
assert_eq!(self.consume_char(), '#');
|
||||
Value::Color(ColorValue::RGBA(
|
||||
self.parse_hex_pair(),
|
||||
self.parse_hex_pair(),
|
||||
self.parse_hex_pair(),
|
||||
255,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_length(&mut self) -> Value {
|
||||
Value::Length(self.parse_float(), self.parse_unit())
|
||||
}
|
||||
|
||||
fn parse_float(&mut self) -> f32 {
|
||||
let s = self.consume_while(|c| match c {
|
||||
'0'..='9' | '.' => true,
|
||||
_ => false,
|
||||
});
|
||||
s.parse().unwrap()
|
||||
}
|
||||
|
||||
fn parse_unit(&mut self) -> Unit {
|
||||
match &*self.parse_identifier().to_ascii_lowercase() {
|
||||
"px" => Unit::Px,
|
||||
_ => panic!("unrecognized unit"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_rules(&mut self) -> Vec<Rule> {
|
||||
let mut rules = Vec::new();
|
||||
while !self.eof() {
|
||||
rules.push(self.parse_rule());
|
||||
self.consume_whitespace();
|
||||
}
|
||||
rules
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(input: String) -> StyleSheet {
|
||||
let mut parser = Parser {
|
||||
pos: 0,
|
||||
input: input,
|
||||
};
|
||||
StyleSheet {
|
||||
rules: parser.parse_rules(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct SingleSelector {
|
||||
tag_name: Option<String>,
|
||||
id: Option<String>,
|
||||
classes: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Selector {
|
||||
Single(SingleSelector),
|
||||
}
|
||||
|
||||
impl fmt::Display for Selector {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::Single(selector) => {
|
||||
if let Some(tag_name) = &selector.tag_name {
|
||||
write!(f, "{}", tag_name).unwrap();
|
||||
}
|
||||
if selector.classes.len() > 0 {
|
||||
write!(f, ".{}", selector.classes.join(".")).unwrap();
|
||||
}
|
||||
if let Some(id) = &selector.id {
|
||||
write!(f, "#{}", id).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Value {
|
||||
Keyword(String),
|
||||
Length(f32, Unit),
|
||||
Color(ColorValue),
|
||||
}
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::Keyword(keyword) => {
|
||||
write!(f, "{}", keyword)
|
||||
}
|
||||
Self::Color(color) => {
|
||||
write!(f, "{}", color)
|
||||
}
|
||||
Self::Length(amount, unit) => {
|
||||
write!(f, "{}{}", amount, unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ColorValue {
|
||||
RGBA(u8, u8, u8, u8),
|
||||
}
|
||||
|
||||
impl fmt::Display for ColorValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::RGBA(r, g, b, a) => write!(f, "rgba({}, {}, {}, {})", r, g, b, a),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Unit {
|
||||
Px,
|
||||
}
|
||||
|
||||
impl fmt::Display for Unit {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::Px => write!(f, "px"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Declaration {
|
||||
name: String,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
impl fmt::Display for Declaration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}: {};", self.name, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Rule {
|
||||
selectors: Vec<Selector>,
|
||||
declarations: Vec<Declaration>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Rule {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let prepadding = " ";
|
||||
let selectors = self
|
||||
.selectors
|
||||
.iter()
|
||||
.map(|d| format!("{d}"))
|
||||
.collect::<Vec<String>>();
|
||||
writeln!(f, "{} {{", selectors.join(", ")).unwrap();
|
||||
let declarations = self
|
||||
.declarations
|
||||
.iter()
|
||||
.map(|d| format!("{d}"))
|
||||
.collect::<Vec<String>>();
|
||||
writeln!(f, "{}{}", prepadding, declarations.join("")).unwrap();
|
||||
writeln!(f, "}}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StyleSheet {
|
||||
rules: Vec<Rule>,
|
||||
}
|
||||
|
||||
impl fmt::Display for StyleSheet {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.rules.len() > 0 {
|
||||
let rules = self
|
||||
.rules
|
||||
.iter()
|
||||
.map(|r| format!("{r}"))
|
||||
.collect::<Vec<String>>();
|
||||
writeln!(f, "{}", rules.join("\n")).unwrap()
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut input = String::new();
|
||||
let stdin = io::stdin();
|
||||
stdin.read_line(&mut input).unwrap();
|
||||
// let nodes = html::parse(input);
|
||||
let nodes = parse(input);
|
||||
println!("{nodes}");
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user