mirror of
https://github.com/zyllian/webdog.git
synced 2025-07-04 12:21:59 -07:00
Compare commits
No commits in common. "1d502881f6f88df5c5f022d576261ed24476370f" and "71bd753b05adbe5a87841cd364a136adad66a17b" have entirely different histories.
1d502881f6
...
71bd753b05
10 changed files with 375 additions and 734 deletions
756
Cargo.lock
generated
756
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ name = "webdog"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = {version = "4", features = ["derive"]}
|
clap = { version = "4", features = ["derive"] }
|
||||||
color-eyre = {version = "0.6", optional = true}
|
color-eyre = {version = "0.6", optional = true}
|
||||||
extract-frontmatter = "4"
|
extract-frontmatter = "4"
|
||||||
eyre = "0.6"
|
eyre = "0.6"
|
||||||
|
@ -12,10 +12,8 @@ fs_extra = "1.2"
|
||||||
futures = {version = "0.3", optional = true}
|
futures = {version = "0.3", optional = true}
|
||||||
grass = {version = "0.13", default-features = false}
|
grass = {version = "0.13", default-features = false}
|
||||||
hotwatch = {version = "0.5", optional = true}
|
hotwatch = {version = "0.5", optional = true}
|
||||||
html5ever = "0.29"
|
|
||||||
include_dir = "0.7"
|
include_dir = "0.7"
|
||||||
itertools = "0.14"
|
itertools = "0.13"
|
||||||
kuchikiki = "0.8.6-speedreader"
|
|
||||||
lol_html = "2"
|
lol_html = "2"
|
||||||
minifier = {version = "0.3", features = ["html"]}
|
minifier = {version = "0.3", features = ["html"]}
|
||||||
percent-encoding = {version = "2", optional = true}
|
percent-encoding = {version = "2", optional = true}
|
||||||
|
@ -30,7 +28,7 @@ serde_yml = "0.0.12"
|
||||||
syntect = "5"
|
syntect = "5"
|
||||||
tera = "1"
|
tera = "1"
|
||||||
time = {version = "0.3", features = ["serde-human-readable"]}
|
time = {version = "0.3", features = ["serde-human-readable"]}
|
||||||
tokio = {version = "1", features = [
|
tokio = {version = "1.10", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
], optional = true}
|
], optional = true}
|
||||||
|
|
|
@ -20,10 +20,6 @@ learn about standard webdog pages here
|
||||||
|
|
||||||
learn how to use webdog templates
|
learn how to use webdog templates
|
||||||
|
|
||||||
## <a href="webdog-html">webdog html</a>
|
|
||||||
|
|
||||||
learn about webdog html extensions
|
|
||||||
|
|
||||||
## <a href="styling">styling</a>
|
## <a href="styling">styling</a>
|
||||||
|
|
||||||
learn about webdog styling
|
learn about webdog styling
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
---
|
|
||||||
title: webdog html
|
|
||||||
template: docs.tera
|
|
||||||
---
|
|
||||||
|
|
||||||
# webdog html
|
|
||||||
|
|
||||||
webdog adds some extensions to html to make building your site easier.
|
|
||||||
|
|
||||||
## `wd-partial`
|
|
||||||
|
|
||||||
any template can be used as a partial in another template or page.
|
|
||||||
|
|
||||||
in a template like this:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<p>this is a partial.</p>
|
|
||||||
<p>the hi argument is {{ userdata.hi }}</p>
|
|
||||||
<p>the hello argument is {{ userdata.hello }}</p>
|
|
||||||
<div>
|
|
||||||
<p>and here's the inner content:
|
|
||||||
{{ page | safe }}
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
simply include the `wd-partial` html tag like so:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<wd-partial t="template-to-use.tera" hello="hi" hi="hello">
|
|
||||||
hiiiiiiiiiii~
|
|
||||||
</wd-partial>
|
|
||||||
```
|
|
||||||
|
|
||||||
a `wd-partial` tag consists of the `t` attribute to determine the template and any number of additional arguments, which are passed on to the partial template.
|
|
|
@ -12,7 +12,6 @@
|
||||||
{{ self::docLink(text="configuration", href="config") }}
|
{{ self::docLink(text="configuration", href="config") }}
|
||||||
{{ self::docLink(text="pages", href="pages") }}
|
{{ self::docLink(text="pages", href="pages") }}
|
||||||
{{ self::docLink(text="templates", href="templates") }}
|
{{ self::docLink(text="templates", href="templates") }}
|
||||||
{{ self::docLink(text="webdog html", href="webdog-html") }}
|
|
||||||
{{ self::docLink(text="styling", href="styling") }}
|
{{ self::docLink(text="styling", href="styling") }}
|
||||||
{{ self::docLink(text="resources", href="resources") }}
|
{{ self::docLink(text="resources", href="resources") }}
|
||||||
{{ self::docLink(text="webdog assumptions", href="wd-assumptions") }}
|
{{ self::docLink(text="webdog assumptions", href="wd-assumptions") }}
|
||||||
|
|
270
src/builder.rs
270
src/builder.rs
|
@ -163,164 +163,106 @@ impl SiteBuilder {
|
||||||
head: &Option<String>,
|
head: &Option<String>,
|
||||||
scripts: &[String],
|
scripts: &[String],
|
||||||
styles: &[String],
|
styles: &[String],
|
||||||
is_partial: bool,
|
|
||||||
) -> eyre::Result<String> {
|
) -> eyre::Result<String> {
|
||||||
use kuchikiki::traits::*;
|
let mut output = Vec::new();
|
||||||
|
let mut rewriter = HtmlRewriter::new(
|
||||||
let html = {
|
Settings {
|
||||||
let document = kuchikiki::parse_html().one(html.clone()).document_node;
|
element_content_handlers: vec![
|
||||||
let mut needs_reserialized = false;
|
element!("body", |el| {
|
||||||
|
if self.serving {
|
||||||
while let Ok(el) = document.select_first("wd-partial") {
|
el.set_attribute("class", "debug")?;
|
||||||
needs_reserialized = true;
|
}
|
||||||
let attr_map = el.attributes.borrow();
|
Ok(())
|
||||||
let template = attr_map
|
}),
|
||||||
.get("t")
|
element!("head", |el| {
|
||||||
.ok_or_eyre("missing t attribute on wd-partial")?;
|
el.prepend(r#"<meta charset="utf-8">"#, ContentType::Html);
|
||||||
let attr_map: HashMap<_, _> = attr_map
|
el.append(&format!("<title>{title}</title>"), ContentType::Html);
|
||||||
.map
|
if let Some(head) = head {
|
||||||
.iter()
|
el.append(head, ContentType::Html);
|
||||||
.map(|(k, v)| (k.local.to_string(), &v.value))
|
}
|
||||||
.collect();
|
for script in scripts {
|
||||||
let mut html_buf = Vec::new();
|
|
||||||
for child in el.as_node().children() {
|
|
||||||
child.serialize(&mut html_buf)?;
|
|
||||||
}
|
|
||||||
let html = String::from_utf8(html_buf)?;
|
|
||||||
let new_html = self.build_page_raw(
|
|
||||||
PageMetadata {
|
|
||||||
template: Some(template.to_string()),
|
|
||||||
userdata: serde_yml::to_value(attr_map)?,
|
|
||||||
is_partial: true,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&html,
|
|
||||||
(),
|
|
||||||
)?;
|
|
||||||
let new_doc = kuchikiki::parse_html()
|
|
||||||
.one(new_html)
|
|
||||||
.document_node
|
|
||||||
.select_first("body")
|
|
||||||
.map(|b| b.as_node().children())
|
|
||||||
.expect("should never fail");
|
|
||||||
for child in new_doc {
|
|
||||||
el.as_node().insert_before(child);
|
|
||||||
}
|
|
||||||
el.as_node().detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
if needs_reserialized {
|
|
||||||
let mut html = Vec::new();
|
|
||||||
document.serialize(&mut html)?;
|
|
||||||
String::from_utf8(html)?
|
|
||||||
} else {
|
|
||||||
html
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = if is_partial {
|
|
||||||
html
|
|
||||||
} else {
|
|
||||||
let mut output = Vec::new();
|
|
||||||
let mut rewriter = HtmlRewriter::new(
|
|
||||||
Settings {
|
|
||||||
element_content_handlers: vec![
|
|
||||||
element!("body", |el| {
|
|
||||||
if self.serving {
|
|
||||||
el.set_attribute("class", "debug")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}),
|
|
||||||
element!("head", |el| {
|
|
||||||
el.prepend(r#"<meta charset="utf-8">"#, ContentType::Html);
|
|
||||||
el.append(&format!("<title>{title}</title>"), ContentType::Html);
|
|
||||||
if let Some(head) = head {
|
|
||||||
el.append(head, ContentType::Html);
|
|
||||||
}
|
|
||||||
for script in scripts {
|
|
||||||
el.append(
|
|
||||||
&format!(
|
|
||||||
r#"<script type="text/javascript" src="{script}" defer></script>"#
|
|
||||||
),
|
|
||||||
ContentType::Html,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for style in styles {
|
|
||||||
el.append(
|
|
||||||
&format!(r#"<link rel="stylesheet" href="/styles/{style}">"#),
|
|
||||||
ContentType::Html,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
el.append(
|
el.append(
|
||||||
r#"<script type="text/javascript" src="/webdog/webdog.js" defer></script>"#,
|
&format!(
|
||||||
|
r#"<script type="text/javascript" src="{script}" defer></script>"#
|
||||||
|
),
|
||||||
ContentType::Html,
|
ContentType::Html,
|
||||||
);
|
);
|
||||||
if self.serving {
|
}
|
||||||
el.append(r#"<script src="/_dev.js"></script>"#, ContentType::Html);
|
for style in styles {
|
||||||
}
|
el.append(
|
||||||
|
&format!(r#"<link rel="stylesheet" href="/styles/{style}">"#),
|
||||||
|
ContentType::Html,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
el.append(
|
||||||
|
r#"<script type="text/javascript" src="/webdog/webdog.js" defer></script>"#,
|
||||||
|
ContentType::Html,
|
||||||
|
);
|
||||||
|
if self.serving {
|
||||||
|
el.append(r#"<script src="/_dev.js"></script>"#, ContentType::Html);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}),
|
}),
|
||||||
element!("a", |el| {
|
element!("a", |el| {
|
||||||
if let Some(mut href) = el.get_attribute("href") {
|
if let Some(mut href) = el.get_attribute("href") {
|
||||||
if let Some((command, mut new_href)) = href.split_once('$') {
|
if let Some((command, mut new_href)) = href.split_once('$') {
|
||||||
#[allow(clippy::single_match)]
|
#[allow(clippy::single_match)]
|
||||||
match command {
|
match command {
|
||||||
"me" => {
|
"me" => {
|
||||||
el.set_attribute(
|
|
||||||
"rel",
|
|
||||||
&(el.get_attribute("rel").unwrap_or_default()
|
|
||||||
+ " me"),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
new_href = &href;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
href = new_href.to_string();
|
|
||||||
el.set_attribute("href", &href)?;
|
|
||||||
}
|
|
||||||
if let Ok(url) = Url::parse(&href) {
|
|
||||||
if url.host().is_some() {
|
|
||||||
// Make external links open in new tabs without referral information
|
|
||||||
el.set_attribute(
|
el.set_attribute(
|
||||||
"rel",
|
"rel",
|
||||||
(el.get_attribute("rel").unwrap_or_default()
|
&(el.get_attribute("rel").unwrap_or_default() + " me"),
|
||||||
+ " noopener noreferrer")
|
|
||||||
.trim(),
|
|
||||||
)?;
|
)?;
|
||||||
el.set_attribute("target", "_blank")?;
|
}
|
||||||
|
_ => {
|
||||||
|
new_href = &href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
href = new_href.to_string();
|
||||||
|
el.set_attribute("href", &href)?;
|
||||||
|
}
|
||||||
|
if let Ok(url) = Url::parse(&href) {
|
||||||
|
if url.host().is_some() {
|
||||||
|
// Make external links open in new tabs without referral information
|
||||||
|
el.set_attribute(
|
||||||
|
"rel",
|
||||||
|
(el.get_attribute("rel").unwrap_or_default()
|
||||||
|
+ " noopener noreferrer")
|
||||||
|
.trim(),
|
||||||
|
)?;
|
||||||
|
el.set_attribute("target", "_blank")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
|
element!("md", |el| {
|
||||||
|
el.remove();
|
||||||
|
let class = el.get_attribute("class");
|
||||||
|
|
||||||
|
let md_type = el
|
||||||
|
.get_attribute("type")
|
||||||
|
.ok_or_eyre("missing type attribute on markdown tag")?;
|
||||||
|
|
||||||
|
if md_type == "blog-image" {
|
||||||
|
let mut src = el
|
||||||
|
.get_attribute("src")
|
||||||
|
.ok_or_eyre("missing src attribute")?;
|
||||||
|
|
||||||
|
if src.starts_with("cdn$") {
|
||||||
|
src = self.site.config.cdn_url(&src[4..])?.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
let class = format!("image {}", class.unwrap_or_default());
|
||||||
}),
|
let content = el
|
||||||
element!("md", |el| {
|
.get_attribute("content")
|
||||||
el.remove();
|
.ok_or_eyre("missing content attribute")?;
|
||||||
let class = el.get_attribute("class");
|
|
||||||
|
|
||||||
let md_type = el
|
el.replace(
|
||||||
.get_attribute("type")
|
&format!(
|
||||||
.ok_or_eyre("missing type attribute on markdown tag")?;
|
r#"
|
||||||
|
|
||||||
if md_type == "blog-image" {
|
|
||||||
let mut src = el
|
|
||||||
.get_attribute("src")
|
|
||||||
.ok_or_eyre("missing src attribute")?;
|
|
||||||
|
|
||||||
if src.starts_with("cdn$") {
|
|
||||||
src = self.site.config.cdn_url(&src[4..])?.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let class = format!("image {}", class.unwrap_or_default());
|
|
||||||
let content = el
|
|
||||||
.get_attribute("content")
|
|
||||||
.ok_or_eyre("missing content attribute")?;
|
|
||||||
|
|
||||||
el.replace(
|
|
||||||
&format!(
|
|
||||||
r#"
|
|
||||||
<div class="{class}">
|
<div class="{class}">
|
||||||
<a href="{src}">
|
<a href="{src}">
|
||||||
<img src="{src}">
|
<img src="{src}">
|
||||||
|
@ -328,29 +270,26 @@ impl SiteBuilder {
|
||||||
<span>{content}</span>
|
<span>{content}</span>
|
||||||
</div>
|
</div>
|
||||||
"#
|
"#
|
||||||
),
|
),
|
||||||
ContentType::Html,
|
ContentType::Html,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Err(eyre!("unknown markdown tag type: {md_type}").into());
|
return Err(eyre!("unknown markdown tag type: {md_type}").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
strict: true,
|
strict: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|c: &[u8]| output.extend_from_slice(c),
|
|c: &[u8]| output.extend_from_slice(c),
|
||||||
);
|
);
|
||||||
|
|
||||||
rewriter.write(html.as_bytes())?;
|
rewriter.write(html.as_bytes())?;
|
||||||
rewriter.end()?;
|
rewriter.end()?;
|
||||||
|
|
||||||
String::from_utf8(output)?
|
Ok(String::from_utf8(output)?)
|
||||||
};
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to build a page without writing it to disk.
|
/// Helper to build a page without writing it to disk.
|
||||||
|
@ -395,7 +334,6 @@ impl SiteBuilder {
|
||||||
&head,
|
&head,
|
||||||
&page_metadata.scripts,
|
&page_metadata.scripts,
|
||||||
&page_metadata.styles,
|
&page_metadata.styles,
|
||||||
page_metadata.is_partial,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if let Some(data) = extra {
|
if let Some(data) = extra {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
let socket;
|
let socket;
|
||||||
|
|
||||||
function start(reload) {
|
function start(reload) {
|
||||||
socket = new WebSocket(`ws://${location.host}`);
|
socket = new WebSocket("ws://127.0.0.1:8080");
|
||||||
let reloading = false;
|
let reloading = false;
|
||||||
socket.onmessage = function (ev) {
|
socket.onmessage = function (ev) {
|
||||||
if (ev.data === "reload") {
|
if (ev.data === "reload") {
|
||||||
|
|
|
@ -12,18 +12,6 @@ pub struct FrontMatter<T> {
|
||||||
pub data: Option<T>,
|
pub data: Option<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> FrontMatter<T> {
|
|
||||||
/// Creates a new front matter.
|
|
||||||
pub fn new(data: Option<T>, content: String) -> Self {
|
|
||||||
Self { data, content }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new front matter without content.
|
|
||||||
pub fn new_empty(data: Option<T>) -> Self {
|
|
||||||
Self::new(data, String::new())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> FrontMatter<T>
|
impl<T> FrontMatter<T>
|
||||||
where
|
where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
|
@ -75,16 +63,6 @@ where
|
||||||
pub struct FrontMatterRequired<T>(FrontMatter<T>);
|
pub struct FrontMatterRequired<T>(FrontMatter<T>);
|
||||||
|
|
||||||
impl<T> FrontMatterRequired<T> {
|
impl<T> FrontMatterRequired<T> {
|
||||||
/// Creates a new front matter.
|
|
||||||
pub fn new(data: T, content: String) -> Self {
|
|
||||||
Self(FrontMatter::new(Some(data), content))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new front matter without content.
|
|
||||||
pub fn new_empty(data: T) -> Self {
|
|
||||||
Self(FrontMatter::new_empty(Some(data)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a reference to the front matter's data.
|
/// Gets a reference to the front matter's data.
|
||||||
pub fn data(&self) -> &T {
|
pub fn data(&self) -> &T {
|
||||||
self.0.data.as_ref().expect("missing front matter data")
|
self.0.data.as_ref().expect("missing front matter data")
|
||||||
|
|
|
@ -130,9 +130,6 @@ pub struct PageMetadata {
|
||||||
/// Custom values passed to the base template.
|
/// Custom values passed to the base template.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub userdata: serde_yml::Value,
|
pub userdata: serde_yml::Value,
|
||||||
/// Whether this page being rendered is a partial. Set by the builder, not your page metadata.
|
|
||||||
#[serde(skip)]
|
|
||||||
pub is_partial: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Struct containing information about the site.
|
/// Struct containing information about the site.
|
||||||
|
|
|
@ -256,8 +256,11 @@ fn main() -> eyre::Result<()> {
|
||||||
title,
|
title,
|
||||||
template,
|
template,
|
||||||
} => {
|
} => {
|
||||||
let page_path = cli.site.join(webdog::PAGES_PATH).join(&id).with_extension("md");
|
let page_path = cli
|
||||||
let dir = page_path.parent().expect("should never fail");
|
.site
|
||||||
|
.join(webdog::PAGES_PATH)
|
||||||
|
.join(&id)
|
||||||
|
.with_extension("md");
|
||||||
if page_path.exists() {
|
if page_path.exists() {
|
||||||
eprintln!("page already exists!");
|
eprintln!("page already exists!");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -270,7 +273,7 @@ fn main() -> eyre::Result<()> {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
std::fs::create_dir_all(dir)?;
|
std::fs::create_dir_all(page_path.parent().expect("should never fail"))?;
|
||||||
std::fs::write(&page_path, fm.format()?)?;
|
std::fs::write(&page_path, fm.format()?)?;
|
||||||
|
|
||||||
println!("Page created! Edit at {:?}.", page_path);
|
println!("Page created! Edit at {:?}.", page_path);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue