mirror of
https://github.com/zyllian/webdog.git
synced 2025-01-17 19:22:21 -08:00
Compare commits
5 commits
71bd753b05
...
1d502881f6
Author | SHA1 | Date | |
---|---|---|---|
1d502881f6 | |||
fd40a5d31a | |||
9608925572 | |||
4265317edd | |||
bba26401ba |
10 changed files with 735 additions and 376 deletions
754
Cargo.lock
generated
754
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,8 +12,10 @@ 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.13"
|
itertools = "0.14"
|
||||||
|
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}
|
||||||
|
@ -28,7 +30,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.10", features = [
|
tokio = {version = "1", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
], optional = true}
|
], optional = true}
|
||||||
|
|
|
@ -20,6 +20,10 @@ 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
|
||||||
|
|
34
site/pages/docs/webdog-html.md
Normal file
34
site/pages/docs/webdog-html.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
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,6 +12,7 @@
|
||||||
{{ 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") }}
|
||||||
|
|
274
src/builder.rs
274
src/builder.rs
|
@ -163,106 +163,164 @@ 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> {
|
||||||
let mut output = Vec::new();
|
use kuchikiki::traits::*;
|
||||||
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(
|
|
||||||
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(())
|
let html = {
|
||||||
}),
|
let document = kuchikiki::parse_html().one(html.clone()).document_node;
|
||||||
element!("a", |el| {
|
let mut needs_reserialized = false;
|
||||||
if let Some(mut href) = el.get_attribute("href") {
|
|
||||||
if let Some((command, mut new_href)) = href.split_once('$') {
|
while let Ok(el) = document.select_first("wd-partial") {
|
||||||
#[allow(clippy::single_match)]
|
needs_reserialized = true;
|
||||||
match command {
|
let attr_map = el.attributes.borrow();
|
||||||
"me" => {
|
let template = attr_map
|
||||||
|
.get("t")
|
||||||
|
.ok_or_eyre("missing t attribute on wd-partial")?;
|
||||||
|
let attr_map: HashMap<_, _> = attr_map
|
||||||
|
.map
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (k.local.to_string(), &v.value))
|
||||||
|
.collect();
|
||||||
|
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(
|
||||||
|
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(())
|
||||||
|
}),
|
||||||
|
element!("a", |el| {
|
||||||
|
if let Some(mut href) = el.get_attribute("href") {
|
||||||
|
if let Some((command, mut new_href)) = href.split_once('$') {
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match command {
|
||||||
|
"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() + " me"),
|
(el.get_attribute("rel").unwrap_or_default()
|
||||||
|
+ " 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() {
|
Ok(())
|
||||||
// Make external links open in new tabs without referral information
|
}),
|
||||||
el.set_attribute(
|
element!("md", |el| {
|
||||||
"rel",
|
el.remove();
|
||||||
(el.get_attribute("rel").unwrap_or_default()
|
let class = el.get_attribute("class");
|
||||||
+ " noopener noreferrer")
|
|
||||||
.trim(),
|
let md_type = el
|
||||||
)?;
|
.get_attribute("type")
|
||||||
el.set_attribute("target", "_blank")?;
|
.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}">
|
||||||
|
@ -270,26 +328,29 @@ 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()?;
|
||||||
|
|
||||||
Ok(String::from_utf8(output)?)
|
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.
|
||||||
|
@ -334,6 +395,7 @@ 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://127.0.0.1:8080");
|
socket = new WebSocket(`ws://${location.host}`);
|
||||||
let reloading = false;
|
let reloading = false;
|
||||||
socket.onmessage = function (ev) {
|
socket.onmessage = function (ev) {
|
||||||
if (ev.data === "reload") {
|
if (ev.data === "reload") {
|
||||||
|
|
|
@ -12,6 +12,18 @@ 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,
|
||||||
|
@ -63,6 +75,16 @@ 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,6 +130,9 @@ 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,11 +256,8 @@ fn main() -> eyre::Result<()> {
|
||||||
title,
|
title,
|
||||||
template,
|
template,
|
||||||
} => {
|
} => {
|
||||||
let page_path = cli
|
let page_path = cli.site.join(webdog::PAGES_PATH).join(&id).with_extension("md");
|
||||||
.site
|
let dir = page_path.parent().expect("should never fail");
|
||||||
.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(());
|
||||||
|
@ -273,7 +270,7 @@ fn main() -> eyre::Result<()> {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
std::fs::create_dir_all(page_path.parent().expect("should never fail"))?;
|
std::fs::create_dir_all(dir)?;
|
||||||
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
Reference in a new issue