Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

html_support #43

Merged
merged 8 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 313 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions bslive.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<style>
p {
color: red;
}

abc-element {
color: green;
}

</style>

<abc-element></abc-element>

<script type="module">
class BSlive extends HTMLElement {
connectedCallback() {
this.innerHTML = "Hello worlds";
}
}

customElements.define("abc-element", BSlive);

</script>
2 changes: 2 additions & 0 deletions crates/bsnext_dto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ pub enum InputErrorDTO {
DirError(String),
YamlError(String),
MarkdownError(String),
HtmlError(String),
Io(String),
UnsupportedExtension(String),
MissingExtension(String),
Expand All @@ -305,6 +306,7 @@ impl From<&InputError> for InputErrorDTO {
e @ InputError::PortError(_) => InputErrorDTO::PortError(e.to_string()),
e @ InputError::DirError(_) => InputErrorDTO::DirError(e.to_string()),
e @ InputError::MarkdownError(_) => InputErrorDTO::MarkdownError(e.to_string()),
e @ InputError::HtmlError(_) => InputErrorDTO::HtmlError(e.to_string()),
e @ InputError::YamlError(_) => InputErrorDTO::YamlError(e.to_string()),
e @ InputError::Io(_) => InputErrorDTO::Io(e.to_string()),
e @ InputError::UnsupportedExtension(_) => {
Expand Down
1 change: 1 addition & 0 deletions crates/bsnext_example/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ edition = "2021"
[dependencies]
bsnext_input = { path = "../bsnext_input" }
bsnext_md = { path = "../bsnext_md" }
bsnext_html = { path = "../bsnext_html" }
clap = { workspace = true }
7 changes: 4 additions & 3 deletions crates/bsnext_example/src/md.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use bsnext_input::server_config::ServerIdentity;
use bsnext_input::{InputSource, InputSourceKind};
use bsnext_md::md_to_input;
use bsnext_input::{InputCreation, InputSource, InputSourceKind};
use bsnext_md::md_fs::MdFs;

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct MdExample;

impl InputSource for MdExample {
fn into_input(self, identity: Option<ServerIdentity>) -> InputSourceKind {
let input_str = include_str!("../../../examples/markdown/single.md");
let mut input = md_to_input(input_str).expect("example cannot fail?");
let mut input =
MdFs::from_input_str(input_str, &Default::default()).expect("example cannot fail?");
let server = input
.servers
.first_mut()
Expand Down
8 changes: 4 additions & 4 deletions crates/bsnext_example/src/playground.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use bsnext_html::HtmlFs;
use bsnext_input::server_config::ServerIdentity;
use bsnext_input::{InputCreation, InputSource, InputSourceKind};
use bsnext_md::md_fs::MdFs;
use bsnext_input::{InputCreation, InputCtx, InputSource, InputSourceKind};

#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PlaygroundExample;

impl InputSource for PlaygroundExample {
fn into_input(self, identity: Option<ServerIdentity>) -> InputSourceKind {
let input_str = include_str!("../../../examples/markdown/playground.md");
let mut input = MdFs::from_input_str(input_str).unwrap();
let input_str = include_str!("../../../examples/html/playground.html");
let mut input = HtmlFs::from_input_str(input_str, &InputCtx::default()).unwrap();

// update the server identity if it was provided
if let (Some(server), Some(identity)) = (input.servers.get_mut(0), identity) {
Expand Down
14 changes: 14 additions & 0 deletions crates/bsnext_html/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "bsnext_html"
version = "0.1.0"
edition = "2021"

[dependencies]
bsnext_input = { path = "../bsnext_input" }
unindent = "0.2.3"
indent = "0.1.1"
scraper = "0.21.0"

[dev-dependencies]
anyhow = { workspace = true }
insta = { workspace = true }
54 changes: 54 additions & 0 deletions crates/bsnext_html/src/html_writer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use bsnext_input::playground::Playground;
use bsnext_input::server_config::ServerConfig;
use bsnext_input::{Input, InputWriter};

pub struct HtmlWriter;

impl InputWriter for HtmlWriter {
fn input_to_str(&self, input: &Input) -> String {
if input.servers.is_empty() {
todo!("html requires at least 1 server definition")
}
if input.servers.len() > 1 {
todo!("more than 1 server not supported yet")
}
let server = input.servers.first().expect("must access first");
let Some(playground) = &server.playground else {
todo!("only playground is supported in HTML for now")
};
let mut blocks: Vec<String> = vec![];
if let Some(css) = &playground.css {
blocks.push("<style>".into());
let indented = indent::indent_all_by(4, css);
blocks.push(indented);
blocks.push("</style>".into());
}
blocks.push(playground.html.clone());
if let Some(js) = &playground.js {
blocks.push("<script type=\"module\">".into());
let indented = indent::indent_all_by(4, js);
blocks.push(indented);
blocks.push("</script>".into());
}
blocks.join("\n")
}
}

#[test]
fn test_html_writer_for_playground() {
let css = r#"body {
background: red;
}"#;
let js = r#"console.log("hello world!")"#;
let playground = Playground {
html: "<p>Hello world</p>".to_string(),
js: Some(js.to_string()),
css: Some(css.to_string()),
};
let mut input = Input::default();
let mut server = ServerConfig::default();
server.playground = Some(playground);
input.servers.push(server);
let output = HtmlWriter.input_to_str(&input);
insta::assert_snapshot!(output);
}
96 changes: 96 additions & 0 deletions crates/bsnext_html/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use bsnext_input::playground::Playground;
use bsnext_input::server_config::{ServerConfig, ServerIdentity};
use bsnext_input::{Input, InputCreation, InputCtx, InputError};
use std::fs::read_to_string;
use std::path::Path;

pub mod html_writer;

pub struct HtmlFs;

impl InputCreation for HtmlFs {
fn from_input_path<P: AsRef<Path>>(path: P, ctx: &InputCtx) -> Result<Input, Box<InputError>> {
let str = read_to_string(path).map_err(|e| Box::new(e.into()))?;
let input = playground_html_str_to_input(&str, ctx)
.map_err(|e| Box::new(InputError::HtmlError(e.to_string())))?;
Ok(input)
}

fn from_input_str<P: AsRef<str>>(content: P, ctx: &InputCtx) -> Result<Input, Box<InputError>> {
let input = playground_html_str_to_input(&content.as_ref(), ctx)
.map_err(|e| Box::new(InputError::HtmlError(e.to_string())))?;
Ok(input)
}
}

fn playground_html_str_to_input(html: &str, ctx: &InputCtx) -> Result<Input, Box<InputError>> {
use unindent::unindent;

// parse the HTML
let mut document = scraper::Html::parse_fragment(html);

let style = scraper::Selector::parse("style:first-of-type").unwrap();
let script = scraper::Selector::parse("script:first-of-type").unwrap();

let mut style_elems = document.select(&style);
let mut script_elems = document.select(&script);
let mut node_ids_to_remove = vec![];

// start an empty playground
let mut playground = Playground {
html: "".to_string(),
css: None,
js: None,
};

if let Some(style) = style_elems.next() {
node_ids_to_remove.push(style.id());
let t = style.text().nth(0).unwrap();
let unindented = unindent(t);
playground.css = Some(unindented);
}

if let Some(script) = script_elems.next() {
node_ids_to_remove.push(script.id());
let t = script.text().nth(0).unwrap();
let unindented = unindent(t);
playground.js = Some(unindented);
}

for node_id in node_ids_to_remove {
document.tree.get_mut(node_id).unwrap().detach();
}

// grab the HTML
let as_html = document.html();
let trimmed = as_html
.strip_prefix("<html>")
.unwrap()
.strip_suffix("</html>")
.unwrap();
let un_indented = unindent(trimmed);
playground.html = un_indented;

// Now start to build up the input
let mut input = Input::default();

// 1: first try prev
// 2: next try if 'port' was provided
// 3: finally, make one up
let iden = ctx
.first_id()
.or_else(|| ServerIdentity::from_port_or_named(ctx.port()).ok())
.unwrap_or_default();

// Create the server
let server = ServerConfig {
identity: iden,
playground: Some(playground),
..Default::default()
};

// Add it to the input
input.servers.push(server);

Ok(input)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: crates/bsnext_html/src/html_writer.rs
expression: output
---
<style>
body {
background: red;
}
</style>
<p>Hello world</p>
<script type="module">
console.log("hello world!")
</script>
82 changes: 82 additions & 0 deletions crates/bsnext_html/tests/html_playground.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use bsnext_html::HtmlFs;
use bsnext_input::server_config::ServerIdentity;
use bsnext_input::{InputArgs, InputCreation, InputCtx};
use insta::assert_debug_snapshot;

const INPUT: &str = r#"
<style>
p {
color: red;
}

abc-shane {
color: red;
}
</style>
<main>
<h1>Test!</h1>
<abc-shane></abc-shane>
</main>
<script type="module">
class BSlive extends HTMLElement {
connectedCallback() {
this.innerHTML = "Hello world";
}
}

customElements.define("abc-shane", BSlive);
</script>"#;

#[test]
fn test_html_playground_content() -> anyhow::Result<()> {
let idens = vec![];
let ctx = InputCtx::new(&idens, None);
let as_input = HtmlFs::from_input_str(INPUT, &ctx)?;
let Some(server) = as_input.servers.get(0) else {
return Err(anyhow::anyhow!("no server"));
};
let routes = server.routes();
let html = routes.get(0).unwrap();
let js = routes.get(1).unwrap();
let css = routes.get(2).unwrap();
assert_debug_snapshot!(html.kind);
assert_debug_snapshot!(js.kind);
assert_debug_snapshot!(css.kind);
Ok(())
}
#[test]
fn test_html_playground_without_server_id() -> anyhow::Result<()> {
let idens = vec![];
let ctx = InputCtx::new(&idens, None);
let as_input = HtmlFs::from_input_str(INPUT, &ctx)?;
assert_eq!(as_input.servers.len(), 1);
let first = as_input.servers.get(0).unwrap();
let is_named = matches!(first.identity, ServerIdentity::Named { .. });
assert_eq!(is_named, true);
Ok(())
}
#[test]
fn test_html_playground_with_server_id() -> anyhow::Result<()> {
let ident = ServerIdentity::Address {
bind_address: String::from("127.0.0.1:8080"),
};
let ctx = InputCtx::new(&[ident.clone()], None);
let as_input = HtmlFs::from_input_str(INPUT, &ctx)?;

assert_eq!(as_input.servers.len(), 1);
let first = as_input.servers.get(0).unwrap();
assert_eq!(ident, first.identity);
Ok(())
}
#[test]
fn test_html_playground_with_port() -> anyhow::Result<()> {
let ident = ServerIdentity::Address {
bind_address: String::from("0.0.0.0:8080"),
};
let input_args = InputArgs { port: Some(8080) };
let ctx = InputCtx::new(&[], Some(input_args));
let as_input = HtmlFs::from_input_str(INPUT, &ctx)?;
let first = as_input.servers.get(0).unwrap();
assert_eq!(first.identity, ident);
Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: crates/bsnext_html/tests/html_playground.rs
expression: js.kind
---
Raw(
Raw {
raw: "class BSlive extends HTMLElement {\n connectedCallback() {\n this.innerHTML = \"Hello world\";\n }\n}\n\ncustomElements.define(\"abc-shane\", BSlive);\n",
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: crates/bsnext_html/tests/html_playground.rs
expression: css.kind
---
Raw(
Raw {
raw: "p {\n color: red;\n}\n\nabc-shane {\n color: red;\n}\n",
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
source: crates/bsnext_html/tests/html_playground.rs
expression: html.kind
---
Raw(
Html {
html: "\n<main>\n <h1>Test!</h1>\n <abc-shane></abc-shane>\n</main>\n",
},
)
Loading