mirror of
https://github.com/zyllian/webdog.git
synced 2025-01-17 19:22:21 -08:00
Compare commits
7 commits
1d502881f6
...
855d29d9c4
Author | SHA1 | Date | |
---|---|---|---|
855d29d9c4 | |||
8685e51d0d | |||
3d20c3721c | |||
727e68c817 | |||
c27ac9c6d9 | |||
e44b219335 | |||
cb0c4e7391 |
8 changed files with 109 additions and 99 deletions
|
@ -1,6 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
|
description = "static site generator fit for a dog"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
homepage = "https://webdog.zyl.gay"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
name = "webdog"
|
name = "webdog"
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://github.com/zyllian/webdog"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
20
README.md
20
README.md
|
@ -1,3 +1,21 @@
|
||||||
# webdog
|
# webdog
|
||||||
|
|
||||||
Source for webdog, the static site generator fit for a dog. See our website at https://webdog.zyl.gay for more details.
|
webdog, the static site generator fit for a dog :3
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo install webdog
|
||||||
|
```
|
||||||
|
|
||||||
|
after installing, you can create your first site:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
webdog create https://example.com "My First Site!" --site my-site
|
||||||
|
cd my-site
|
||||||
|
webdog serve # your site is now running at http://127.0.0.1:8080 🥳
|
||||||
|
```
|
||||||
|
|
||||||
|
from there, you can start editing your site and adding pages or [more advanced things](https://webdog.zyl.gay/docs/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
webdog page new my-first-page "My First Page"
|
||||||
|
```
|
||||||
|
|
|
@ -5,7 +5,7 @@ welcome to webdog, the static site generator fit for a dog :3
|
||||||
if you have [rust](https://rust-lang.org) installed, all you need to do to install webdog is run the following command:
|
if you have [rust](https://rust-lang.org) installed, all you need to do to install webdog is run the following command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install webdog --git https://github.com/zyllian/webdog
|
cargo install webdog
|
||||||
```
|
```
|
||||||
|
|
||||||
then you can make your first webdog site!
|
then you can make your first webdog site!
|
||||||
|
|
|
@ -183,6 +183,7 @@ abbr {
|
||||||
|
|
||||||
.wd-codeblock {
|
.wd-codeblock {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
tab-size: 2;
|
||||||
|
|
||||||
.copy {
|
.copy {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -206,6 +207,6 @@ abbr {
|
||||||
|
|
||||||
& > pre {
|
& > pre {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
115
src/builder.rs
115
src/builder.rs
|
@ -4,7 +4,6 @@ use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
use eyre::{eyre, Context, OptionExt};
|
use eyre::{eyre, Context, OptionExt};
|
||||||
use lol_html::{element, html_content::ContentType, HtmlRewriter, Settings};
|
use lol_html::{element, html_content::ContentType, HtmlRewriter, Settings};
|
||||||
use pulldown_cmark::{Options, Parser};
|
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
|
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
|
||||||
|
@ -261,10 +260,28 @@ impl SiteBuilder {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}),
|
}),
|
||||||
|
element!("img", |el| {
|
||||||
|
if let Some(mut src) = el.get_attribute("src") {
|
||||||
|
if let Some((command, new_src)) = src.split_once('$') {
|
||||||
|
let mut new_src = new_src.to_string();
|
||||||
|
#[allow(clippy::single_match)]
|
||||||
|
match command {
|
||||||
|
"cdn" => {
|
||||||
|
new_src = self.site.config.cdn_url(&new_src)?.to_string();
|
||||||
|
}
|
||||||
|
_ => new_src = src,
|
||||||
|
}
|
||||||
|
src = new_src;
|
||||||
|
el.set_attribute("src", &src)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, new_href)) = href.split_once('$') {
|
||||||
#[allow(clippy::single_match)]
|
let mut new_href = new_href.to_string();
|
||||||
match command {
|
match command {
|
||||||
"me" => {
|
"me" => {
|
||||||
el.set_attribute(
|
el.set_attribute(
|
||||||
|
@ -273,11 +290,14 @@ impl SiteBuilder {
|
||||||
+ " me"),
|
+ " me"),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
"cdn" => {
|
||||||
|
new_href = self.site.config.cdn_url(&new_href)?.to_string();
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
new_href = &href;
|
new_href = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
href = new_href.to_string();
|
href = new_href;
|
||||||
el.set_attribute("href", &href)?;
|
el.set_attribute("href", &href)?;
|
||||||
}
|
}
|
||||||
if let Ok(url) = Url::parse(&href) {
|
if let Ok(url) = Url::parse(&href) {
|
||||||
|
@ -294,47 +314,6 @@ impl SiteBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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}">
|
|
||||||
<a href="{src}">
|
|
||||||
<img src="{src}">
|
|
||||||
</a>
|
|
||||||
<span>{content}</span>
|
|
||||||
</div>
|
|
||||||
"#
|
|
||||||
),
|
|
||||||
ContentType::Html,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Err(eyre!("unknown markdown tag type: {md_type}").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -419,49 +398,7 @@ impl SiteBuilder {
|
||||||
.with_context(|| format!("Failed to read page at {}", page_path.display()))?;
|
.with_context(|| format!("Failed to read page at {}", page_path.display()))?;
|
||||||
let page = crate::frontmatter::FrontMatter::parse(input)?;
|
let page = crate::frontmatter::FrontMatter::parse(input)?;
|
||||||
|
|
||||||
let mut language = None;
|
let page_html = util::render_markdown(self, &page.content)?;
|
||||||
let parser = Parser::new_ext(&page.content, Options::all()).filter_map(|event| {
|
|
||||||
// syntax highlighting for code blocks
|
|
||||||
match event {
|
|
||||||
pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(
|
|
||||||
pulldown_cmark::CodeBlockKind::Fenced(name),
|
|
||||||
)) => {
|
|
||||||
language = Some(name);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
pulldown_cmark::Event::Text(code) => {
|
|
||||||
if let Some(language) = language.take() {
|
|
||||||
let syntax_reference = self
|
|
||||||
.syntax_set
|
|
||||||
.find_syntax_by_token(&language)
|
|
||||||
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
|
|
||||||
let html = format!(
|
|
||||||
r#"<div class="wd-codeblock">
|
|
||||||
<button class="copy">Copy</button>
|
|
||||||
{}
|
|
||||||
</div>"#,
|
|
||||||
syntect::html::highlighted_html_for_string(
|
|
||||||
&code,
|
|
||||||
&self.syntax_set,
|
|
||||||
syntax_reference,
|
|
||||||
self.theme_set
|
|
||||||
.themes
|
|
||||||
.get(&self.site.config.code_theme)
|
|
||||||
.as_ref()
|
|
||||||
.expect("should never fail"),
|
|
||||||
)
|
|
||||||
.expect("failed to highlight syntax")
|
|
||||||
);
|
|
||||||
Some(pulldown_cmark::Event::Html(html.into()))
|
|
||||||
} else {
|
|
||||||
Some(pulldown_cmark::Event::Text(code))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Some(event),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let mut page_html = String::new();
|
|
||||||
pulldown_cmark::html::push_html(&mut page_html, parser);
|
|
||||||
|
|
||||||
let out = self.build_page_raw(page.data.unwrap_or_default(), &page_html, ())?;
|
let out = self.build_page_raw(page.data.unwrap_or_default(), &page_html, ())?;
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
.wd-codeblock {
|
.wd-codeblock {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
tab-size: 2;
|
||||||
|
|
||||||
.copy {
|
.copy {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -28,6 +29,6 @@
|
||||||
|
|
||||||
& > pre {
|
& > pre {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,13 @@ use std::{
|
||||||
|
|
||||||
use eyre::Context;
|
use eyre::Context;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use pulldown_cmark::{Options, Parser};
|
|
||||||
use rss::{validation::Validate, ChannelBuilder, ItemBuilder};
|
use rss::{validation::Validate, ChannelBuilder, ItemBuilder};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::{format_description::well_known::Rfc2822, OffsetDateTime};
|
use time::{format_description::well_known::Rfc2822, OffsetDateTime};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builder::SiteBuilder, frontmatter::FrontMatterRequired, link_list::Link,
|
builder::SiteBuilder, frontmatter::FrontMatterRequired, link_list::Link,
|
||||||
util::format_timestamp, PageMetadata,
|
util::{self, format_timestamp}, PageMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Metadata for resources.
|
/// Metadata for resources.
|
||||||
|
@ -182,10 +181,7 @@ impl ResourceBuilder {
|
||||||
let mut page = FrontMatterRequired::<ResourceMetadata>::parse(input)
|
let mut page = FrontMatterRequired::<ResourceMetadata>::parse(input)
|
||||||
.wrap_err_with(|| eyre::eyre!("Failed to parse resource front matter"))?;
|
.wrap_err_with(|| eyre::eyre!("Failed to parse resource front matter"))?;
|
||||||
|
|
||||||
let parser = Parser::new_ext(&page.content, Options::all());
|
*page.content_mut() = util::render_markdown(builder, &page.content)?;
|
||||||
let mut html = String::new();
|
|
||||||
pulldown_cmark::html::push_html(&mut html, parser);
|
|
||||||
*page.content_mut() = html;
|
|
||||||
|
|
||||||
let data = page.data_mut();
|
let data = page.data_mut();
|
||||||
if let Some(cdn_file) = &data.cdn_file {
|
if let Some(cdn_file) = &data.cdn_file {
|
||||||
|
|
52
src/util.rs
52
src/util.rs
|
@ -2,8 +2,11 @@
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use pulldown_cmark::{Options, Parser};
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::builder::SiteBuilder;
|
||||||
|
|
||||||
/// Simple helper to remove the contents of a directory without removing the directory itself.
|
/// Simple helper to remove the contents of a directory without removing the directory itself.
|
||||||
pub fn remove_dir_contents(path: &Path) -> eyre::Result<()> {
|
pub fn remove_dir_contents(path: &Path) -> eyre::Result<()> {
|
||||||
for entry in path.read_dir()? {
|
for entry in path.read_dir()? {
|
||||||
|
@ -24,3 +27,52 @@ pub fn format_timestamp(ts: OffsetDateTime, format: &str) -> eyre::Result<String
|
||||||
let fmt = time::format_description::parse_borrowed::<2>(format)?;
|
let fmt = time::format_description::parse_borrowed::<2>(format)?;
|
||||||
Ok(ts.format(&fmt)?)
|
Ok(ts.format(&fmt)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper to render markdown.
|
||||||
|
pub fn render_markdown(builder: &SiteBuilder, input: &str) -> eyre::Result<String> {
|
||||||
|
let mut language = None;
|
||||||
|
let parser = Parser::new_ext(input, Options::all()).filter_map(|event| {
|
||||||
|
// syntax highlighting for code blocks
|
||||||
|
match event {
|
||||||
|
pulldown_cmark::Event::Start(pulldown_cmark::Tag::CodeBlock(
|
||||||
|
pulldown_cmark::CodeBlockKind::Fenced(name),
|
||||||
|
)) => {
|
||||||
|
language = Some(name);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
pulldown_cmark::Event::Text(code) => {
|
||||||
|
if let Some(language) = language.take() {
|
||||||
|
let syntax_reference = builder
|
||||||
|
.syntax_set
|
||||||
|
.find_syntax_by_token(&language)
|
||||||
|
.unwrap_or_else(|| builder.syntax_set.find_syntax_plain_text());
|
||||||
|
let html = format!(
|
||||||
|
r#"<div class="wd-codeblock">
|
||||||
|
<button class="copy">Copy</button>
|
||||||
|
{}
|
||||||
|
</div>"#,
|
||||||
|
syntect::html::highlighted_html_for_string(
|
||||||
|
&code,
|
||||||
|
&builder.syntax_set,
|
||||||
|
syntax_reference,
|
||||||
|
builder.theme_set
|
||||||
|
.themes
|
||||||
|
.get(&builder.site.config.code_theme)
|
||||||
|
.as_ref()
|
||||||
|
.expect("should never fail"),
|
||||||
|
)
|
||||||
|
.expect("failed to highlight syntax")
|
||||||
|
);
|
||||||
|
Some(pulldown_cmark::Event::Html(html.into()))
|
||||||
|
} else {
|
||||||
|
Some(pulldown_cmark::Event::Text(code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Some(event),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let mut page_html = String::new();
|
||||||
|
pulldown_cmark::html::push_html(&mut page_html, parser);
|
||||||
|
|
||||||
|
Ok(page_html)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue