mirror of
https://github.com/zyllian/webdog.git
synced 2025-05-10 02:26:42 -07:00
introduce webdog cli tool
This commit is contained in:
parent
8b7620b0bd
commit
399a0c1b87
26 changed files with 528 additions and 108 deletions
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
|
@ -40,7 +40,7 @@ jobs:
|
|||
# Runs a single command using the runners shell
|
||||
- name: Build site
|
||||
run: |
|
||||
cargo run --no-default-features --release
|
||||
cargo run --no-default-features --release -- build --site-path site
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
/target
|
||||
/site/build
|
||||
.DS_Store
|
||||
/sitetest
|
||||
|
|
40
Cargo.lock
generated
40
Cargo.lock
generated
|
@ -280,6 +280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -294,6 +295,18 @@ dependencies = [
|
|||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.2"
|
||||
|
@ -937,6 +950,12 @@ dependencies = [
|
|||
"http 0.2.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
|
@ -1216,6 +1235,25 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
|
||||
dependencies = [
|
||||
"include_dir_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "include_dir_macros"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
|
@ -2838,6 +2876,7 @@ checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
|
|||
name = "webdog"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"color-eyre",
|
||||
"extract-frontmatter",
|
||||
"eyre",
|
||||
|
@ -2845,6 +2884,7 @@ dependencies = [
|
|||
"futures",
|
||||
"grass",
|
||||
"hotwatch",
|
||||
"include_dir",
|
||||
"itertools",
|
||||
"lol_html",
|
||||
"minifier",
|
||||
|
|
|
@ -4,6 +4,7 @@ name = "webdog"
|
|||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
color-eyre = {version = "0.6", optional = true}
|
||||
extract-frontmatter = "4"
|
||||
eyre = "0.6"
|
||||
|
@ -11,6 +12,7 @@ fs_extra = "1.2"
|
|||
futures = {version = "0.3", optional = true}
|
||||
grass = {version = "0.13", default-features = false}
|
||||
hotwatch = {version = "0.5", optional = true}
|
||||
include_dir = "0.7"
|
||||
itertools = "0.13"
|
||||
lol_html = "2"
|
||||
minifier = {version = "0.3", features = ["html"]}
|
||||
|
|
|
@ -10,10 +10,10 @@ resources:
|
|||
source_path: blog
|
||||
output_path_short: blog
|
||||
output_path_long: blog
|
||||
resource_template: blog-post.tera
|
||||
resource_list_template: blog-list.tera
|
||||
resource_template: blog/blog.tera
|
||||
resource_list_template: blog/list.tera
|
||||
tag_list_template: basic-link-list.tera
|
||||
rss_template: rss/blog-post.tera
|
||||
rss_template: blog/rss.tera
|
||||
rss_title: webdog blog
|
||||
rss_description: feed of recent webdog blog posts.
|
||||
list_title: blog
|
||||
|
|
|
@ -46,7 +46,7 @@ pub struct SiteBuilder {
|
|||
/// The path to the build directory.
|
||||
pub build_path: PathBuf,
|
||||
/// Whether the site is going to be served locally with the dev server.
|
||||
serving: bool,
|
||||
pub serving: bool,
|
||||
|
||||
/// The resource builders available to the builder.
|
||||
pub resource_builders: HashMap<String, ResourceBuilder>,
|
||||
|
@ -105,7 +105,7 @@ impl SiteBuilder {
|
|||
std::fs::create_dir(&webdog_path)?;
|
||||
std::fs::write(
|
||||
webdog_path.join("webdog.js"),
|
||||
include_str!("./js/webdog.js"),
|
||||
include_str!("./embedded/js/webdog.js"),
|
||||
)?;
|
||||
|
||||
let root_path = self.site.site_path.join(ROOT_PATH);
|
||||
|
|
1
src/embedded/default_site/.gitignore
vendored
Normal file
1
src/embedded/default_site/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
7
src/embedded/default_site/pages/404.md
Normal file
7
src/embedded/default_site/pages/404.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Not Found
|
||||
---
|
||||
|
||||
# 404 Not Found
|
||||
|
||||
The page or resource requested does not exist. [Back to home](/)
|
1
src/embedded/default_site/pages/index.md
Normal file
1
src/embedded/default_site/pages/index.md
Normal file
|
@ -0,0 +1 @@
|
|||
hello from webdog! awruff!!
|
4
src/embedded/default_site/sass/index.scss
Normal file
4
src/embedded/default_site/sass/index.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
.page {
|
||||
background-color: #bbb;
|
||||
padding: 8px;
|
||||
}
|
29
src/embedded/default_site/templates/base.tera
Normal file
29
src/embedded/default_site/templates/base.tera
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<link rel="stylesheet" href="/styles/index.css">
|
||||
|
||||
<title>{{ title }}</title>
|
||||
{# header information from webdog #}
|
||||
{{ head | safe }}
|
||||
{# include scripts defined in the page frontmatter #}
|
||||
{% for script in scripts %}
|
||||
<script type="text/javascript" src="{{script}}" defer></script>
|
||||
{% endfor %}
|
||||
{# include styles defined in the page frontmatter #}
|
||||
{% for style in styles %}
|
||||
<link rel="stylesheet" href="/styles/{{style}}">
|
||||
{% endfor %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1><a href="/">webdog site</a></h1>
|
||||
<main class="page">
|
||||
{% block content %}{{ page | safe }}{% endblock content %}
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
9
src/embedded/default_site/templates/basic-link-list.tera
Normal file
9
src/embedded/default_site/templates/basic-link-list.tera
Normal file
|
@ -0,0 +1,9 @@
|
|||
{% extends "base.tera" %}
|
||||
{% block content %}
|
||||
<h1>{{ title }}</h1>
|
||||
<ul>
|
||||
{% for link in links %}
|
||||
<li><a href="{{link.link}}">{{ link.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock content %}
|
23
src/embedded/resource-template/list.tera
Normal file
23
src/embedded/resource-template/list.tera
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.tera" %}
|
||||
{% block content %}
|
||||
{% if tag %}
|
||||
<h1>!!RESOURCE_NAME_PLURAL!! tagged {{ tag }}</h1>
|
||||
<p><a href="/!!RESOURCE_TYPE!!/">View all !!RESOURCE_NAME_PLURAL_LOWERCASE!!</a></p>
|
||||
{% else %}
|
||||
<h1>!!RESOURCE_NAME_PLURAL!!</h1>
|
||||
<p><a href="tags">view !!RESOURCE_NAME!! tags</a></p>
|
||||
<p><a href="rss.xml">rss feed</a></p>
|
||||
{% endif %}
|
||||
<h1>Page {{ page }}/{{ page_max }}</h1>
|
||||
{% if previous %}
|
||||
<a href="./{{previous}}">previous page</a>
|
||||
{% endif %}
|
||||
{% if next %}
|
||||
<a href="./{{next}}">next page</a>
|
||||
{% endif %}
|
||||
<div>
|
||||
{% for resource in resources %}
|
||||
<p><a href="/!!RESOURCE_TYPE!!/{{resource.id}}">{{ resource.title }}</a></p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
21
src/embedded/resource-template/resource.tera
Normal file
21
src/embedded/resource-template/resource.tera
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% extends "base.tera" %}
|
||||
{% block content %}
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
<span>published {{ timestamp }}</span>
|
||||
{% if draft %}
|
||||
<h2>DRAFT</h2>
|
||||
{% endif %}
|
||||
<p>{{ desc }}</p>
|
||||
<div>
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
<hr />
|
||||
<h3>tags</h3>
|
||||
<div>
|
||||
{% for tag in tags %}
|
||||
<a href="/!!RESOURCE_TYPE!!/tag/{{tag}}">{{ tag }}</a>{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
1
src/embedded/resource-template/rss.tera
Normal file
1
src/embedded/resource-template/rss.tera
Normal file
|
@ -0,0 +1 @@
|
|||
<div>{{ desc | safe }}</div>
|
|
@ -104,7 +104,7 @@ fn resource_list_outside(
|
|||
.map(|(id, v)| ResourceTemplateData {
|
||||
resource: v,
|
||||
id: id.clone(),
|
||||
timestamp: v.timestamp,
|
||||
timestamp: v.data().timestamp,
|
||||
})
|
||||
.collect(),
|
||||
})?,
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::ops::Deref;
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
/// Very basic YAML front matter parser.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FrontMatter<T> {
|
||||
/// The content past the front matter.
|
||||
pub content: String,
|
||||
/// The front matter found, if any.
|
||||
#[serde(flatten)]
|
||||
pub data: Option<T>,
|
||||
}
|
||||
|
||||
|
@ -54,3 +57,46 @@ where
|
|||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around `FrontMatter` to only function when the data is present.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FrontMatterRequired<T>(FrontMatter<T>);
|
||||
|
||||
impl<T> FrontMatterRequired<T> {
|
||||
/// Gets a reference to the front matter's data.
|
||||
pub fn data(&self) -> &T {
|
||||
self.0.data.as_ref().expect("missing front matter data")
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the front matter's data.
|
||||
pub fn data_mut(&mut self) -> &mut T {
|
||||
self.0.data.as_mut().expect("missing front matter data")
|
||||
}
|
||||
|
||||
/// Gets a mutable reference to the front matter's content.
|
||||
pub fn content_mut(&mut self) -> &mut String {
|
||||
&mut self.0.content
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FrontMatterRequired<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
/// Parses the given input for front matter, failing if missing.
|
||||
pub fn parse(input: String) -> eyre::Result<Self> {
|
||||
let fm = FrontMatter::parse(input)?;
|
||||
if fm.data.is_none() {
|
||||
eyre::bail!("missing frontmatter!");
|
||||
}
|
||||
Ok(Self(fm))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for FrontMatterRequired<T> {
|
||||
type Target = FrontMatter<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
|
47
src/lib.rs
47
src/lib.rs
|
@ -1,8 +1,8 @@
|
|||
mod builder;
|
||||
mod extras;
|
||||
mod frontmatter;
|
||||
pub mod frontmatter;
|
||||
mod link_list;
|
||||
mod resource;
|
||||
pub mod resource;
|
||||
#[cfg(feature = "serve")]
|
||||
pub mod serving;
|
||||
mod util;
|
||||
|
@ -16,19 +16,25 @@ use extras::ExtraData;
|
|||
use eyre::Context;
|
||||
use rayon::prelude::*;
|
||||
use resource::{EmbedMetadata, ResourceBuilderConfig};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use builder::SiteBuilder;
|
||||
|
||||
const PAGES_PATH: &str = "pages";
|
||||
const TEMPLATES_PATH: &str = "templates";
|
||||
const SASS_PATH: &str = "sass";
|
||||
const ROOT_PATH: &str = "root";
|
||||
/// Source base path for normal site pages.
|
||||
pub const PAGES_PATH: &str = "pages";
|
||||
/// Source base path for site templates.
|
||||
pub const TEMPLATES_PATH: &str = "templates";
|
||||
/// Source base path for SASS stylesheets.
|
||||
pub const SASS_PATH: &str = "sass";
|
||||
/// Source base path for files which will be copied to the site root.
|
||||
pub const ROOT_PATH: &str = "root";
|
||||
/// Source base path for resources.
|
||||
pub const RESOURCES_PATH: &str = "resources";
|
||||
|
||||
/// Struct for the site's configuration.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SiteConfig {
|
||||
/// The location the site is at.
|
||||
pub base_url: Url,
|
||||
|
@ -53,16 +59,18 @@ pub struct SiteConfig {
|
|||
}
|
||||
|
||||
impl SiteConfig {
|
||||
/// The filename for site config files.
|
||||
pub const FILENAME: &str = "config.yaml";
|
||||
|
||||
/// Creates a new site config from the given title.
|
||||
pub fn new(title: String) -> Self {
|
||||
let url: Url = "/".parse().expect("should never fail");
|
||||
pub fn new(base_url: Url, cdn_url: Url, title: String) -> Self {
|
||||
Self {
|
||||
base_url: url.clone(),
|
||||
base_url,
|
||||
title,
|
||||
description: Default::default(),
|
||||
build: None,
|
||||
sass_styles: vec!["index.scss".into()],
|
||||
cdn_url: url,
|
||||
cdn_url,
|
||||
code_theme: "base16-ocean.dark".to_string(),
|
||||
resources: Default::default(),
|
||||
}
|
||||
|
@ -83,6 +91,15 @@ impl SiteConfig {
|
|||
.ok_or_else(|| eyre::eyre!("missing code theme: {}", self.code_theme))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper to read the site config from the given path.
|
||||
pub fn read(site_path: &Path) -> eyre::Result<Self> {
|
||||
let config_path = site_path.join(SiteConfig::FILENAME);
|
||||
if !config_path.exists() {
|
||||
eyre::bail!("no site config found!");
|
||||
}
|
||||
Ok(serde_yml::from_str(&std::fs::read_to_string(config_path)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct for the front matter in templates. (nothing here yet)
|
||||
|
@ -124,11 +141,7 @@ pub struct Site {
|
|||
impl Site {
|
||||
/// Creates a new site from the given path.
|
||||
pub fn new(site_path: &Path) -> eyre::Result<Self> {
|
||||
let config: SiteConfig = serde_yml::from_str(
|
||||
&std::fs::read_to_string(site_path.join("config.yaml"))
|
||||
.wrap_err("Failed to read site config")?,
|
||||
)
|
||||
.wrap_err("Failed to parse site config")?;
|
||||
let config = SiteConfig::read(site_path)?;
|
||||
|
||||
let mut page_index = HashMap::new();
|
||||
let pages_path = site_path.join(PAGES_PATH);
|
||||
|
|
309
src/main.rs
309
src/main.rs
|
@ -1,64 +1,281 @@
|
|||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use webdog::Site;
|
||||
use clap::{Parser, Subcommand};
|
||||
use include_dir::{include_dir, Dir};
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
use url::Url;
|
||||
use webdog::{
|
||||
frontmatter::FrontMatter,
|
||||
resource::{ResourceBuilderConfig, ResourceMetadata},
|
||||
Site, SiteConfig,
|
||||
};
|
||||
|
||||
#[cfg(feature = "serve")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum Mode {
|
||||
Build,
|
||||
Serve,
|
||||
Now,
|
||||
/// The default project to use when creating a new one, embedded into the binary.
|
||||
static DEFAULT_PROJECT: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/embedded/default_site");
|
||||
/// The default resource template, embedded into the binary.
|
||||
static DEFAULT_RESOURCE_TEMPLATES: Dir =
|
||||
include_dir!("$CARGO_MANIFEST_DIR/src/embedded/resource-template");
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
/// The path to the site.
|
||||
#[arg(global = true, long, default_value = ".")]
|
||||
site_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Create a new webdog site.
|
||||
Create {
|
||||
/// The site's base URL.
|
||||
base_url: Url,
|
||||
/// The site's title.
|
||||
title: String,
|
||||
/// The site's CDN URL. (defaults to the base URL)
|
||||
#[arg(long)]
|
||||
cdn_url: Option<Url>,
|
||||
},
|
||||
/// Builds the site.
|
||||
Build {},
|
||||
/// Serves the site for locally viewing edits made before publishing.
|
||||
#[cfg(feature = "serve")]
|
||||
Serve {
|
||||
/// The IP address to bind to.
|
||||
#[arg(long, default_value = "127.0.0.1")]
|
||||
ip: String,
|
||||
/// The port to bind to.
|
||||
#[arg(short, long, default_value = "8080")]
|
||||
port: u16,
|
||||
},
|
||||
/// Helper to get the current timestamp.
|
||||
Now,
|
||||
/// For dealing with site resources.
|
||||
Resource {
|
||||
#[clap(subcommand)]
|
||||
command: ResourceCommands,
|
||||
},
|
||||
/// Creates a new resource of the given type.
|
||||
New {
|
||||
/// The type of resource to create.
|
||||
resource_type: String,
|
||||
/// The resource's ID.
|
||||
id: String,
|
||||
/// The resource's title.
|
||||
title: String,
|
||||
/// The resource's tags.
|
||||
#[arg(short, long = "tag")]
|
||||
tags: Vec<String>,
|
||||
/// The resource's description.
|
||||
#[arg(short, long)]
|
||||
description: Option<String>,
|
||||
/// Whether to skip setting the resource as a draft or not.
|
||||
#[arg(long, default_value = "false")]
|
||||
skip_draft: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum ResourceCommands {
|
||||
/// Creates a new resource type.
|
||||
Create {
|
||||
/// The resource type's ID.
|
||||
id: String,
|
||||
/// The name of the resource type to create.
|
||||
name: String,
|
||||
/// The name of the resource type, but plural.
|
||||
plural: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "serve")]
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||
|
||||
#[cfg(feature = "color-eyre")]
|
||||
color_eyre::install()?;
|
||||
|
||||
let site = Site::new(&Path::new("site").canonicalize()?)?;
|
||||
let cli = Cli::parse();
|
||||
|
||||
let mut mode = Mode::Build;
|
||||
for arg in std::env::args() {
|
||||
if arg == "serve" {
|
||||
mode = Mode::Serve;
|
||||
break;
|
||||
} else if arg == "now" {
|
||||
mode = Mode::Now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let site = || -> eyre::Result<Site> { Site::new(&Path::new(&cli.site_path).canonicalize()?) };
|
||||
|
||||
match cli.command {
|
||||
Commands::Create {
|
||||
base_url,
|
||||
cdn_url,
|
||||
title,
|
||||
} => {
|
||||
if cli.site_path.exists() {
|
||||
eprintln!("content exists in the given path! canceling!");
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(&cli.site_path)?;
|
||||
let config = SiteConfig::new(base_url.clone(), cdn_url.unwrap_or(base_url), title);
|
||||
std::fs::write(
|
||||
cli.site_path.join(SiteConfig::FILENAME),
|
||||
serde_yml::to_string(&config)?,
|
||||
)?;
|
||||
DEFAULT_PROJECT.extract(&cli.site_path)?;
|
||||
std::fs::create_dir(cli.site_path.join(webdog::ROOT_PATH))?;
|
||||
|
||||
match mode {
|
||||
Mode::Build => {
|
||||
build(site)?;
|
||||
}
|
||||
Mode::Serve => site.serve("127.0.0.1:8080").await?,
|
||||
Mode::Now => {
|
||||
let time = OffsetDateTime::now_utc();
|
||||
println!(
|
||||
"{}",
|
||||
time.format(&Rfc3339)
|
||||
.expect("failed to format the current time")
|
||||
"Base site created at {:?}! Ready for editing, woof!",
|
||||
cli.site_path
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Commands::Build {} => {
|
||||
println!("Building site...");
|
||||
let now = std::time::Instant::now();
|
||||
site()?.build_once()?;
|
||||
println!("Build completed in {:?}", now.elapsed());
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "serve")]
|
||||
Commands::Serve { ip, port } => {
|
||||
let site = site()?;
|
||||
site.serve(&format!("{}:{}", ip, port)).await
|
||||
}
|
||||
Commands::Now => {
|
||||
let time = OffsetDateTime::now_utc();
|
||||
println!("{}", time.format(&Rfc3339)?);
|
||||
Ok(())
|
||||
}
|
||||
Commands::Resource { command } => match command {
|
||||
ResourceCommands::Create { id, name, plural } => {
|
||||
let config_path = cli.site_path.join(SiteConfig::FILENAME);
|
||||
let mut config = SiteConfig::read(&cli.site_path)?;
|
||||
if config.resources.contains_key(&id) {
|
||||
eprintln!("resource type {id} already exists, canceling");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let resource_template_path = cli.site_path.join(webdog::TEMPLATES_PATH).join(&id);
|
||||
if resource_template_path.exists() {
|
||||
eprintln!(
|
||||
"path for resource already exists at {resource_template_path:?}, canceling"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
std::fs::create_dir_all(&resource_template_path)?;
|
||||
for file in DEFAULT_RESOURCE_TEMPLATES.files() {
|
||||
let resource_path = resource_template_path.join(file.path());
|
||||
if let Some(contents) = file.contents_utf8() {
|
||||
let mut contents = contents.to_owned();
|
||||
contents = contents.replace("!!RESOURCE_TYPE!!", &id);
|
||||
contents = contents.replace("!!RESOURCE_NAME!!", &name);
|
||||
contents =
|
||||
contents.replace("!!RESOURCE_NAME_LOWERCASE!!", &name.to_lowercase());
|
||||
contents = contents.replace("!!RESOURCE_NAME_PLURAL!!", &plural);
|
||||
contents = contents
|
||||
.replace("!!RESOURCE_NAME_PLURAL_LOWERCASE!!", &plural.to_lowercase());
|
||||
std::fs::write(resource_path, contents)?;
|
||||
} else {
|
||||
std::fs::write(resource_path, file.contents())?;
|
||||
}
|
||||
}
|
||||
|
||||
let resource_config = ResourceBuilderConfig {
|
||||
source_path: id.clone(),
|
||||
output_path_short: id.clone(),
|
||||
output_path_long: id.clone(),
|
||||
resource_template: format!("{id}/resource.tera"),
|
||||
resource_list_template: format!("{id}/list.tera"),
|
||||
tag_list_template: "basic-link-list.tera".to_string(),
|
||||
rss_template: format!("{id}/rss.tera"),
|
||||
rss_title: id.clone(),
|
||||
rss_description: Default::default(),
|
||||
list_title: name.clone(),
|
||||
tag_list_title: format!("{name} tags"),
|
||||
resource_name_plural: plural,
|
||||
resources_per_page: 3,
|
||||
};
|
||||
|
||||
config.resources.insert(id.clone(), resource_config);
|
||||
|
||||
std::fs::write(config_path, serde_yml::to_string(&config)?)?;
|
||||
|
||||
let resource_path = cli.site_path.join(webdog::RESOURCES_PATH).join(&id);
|
||||
std::fs::create_dir_all(&resource_path)?;
|
||||
|
||||
create_resource(
|
||||
&resource_path.join("first.md"),
|
||||
&ResourceMetadata {
|
||||
title: format!("First {name}"),
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
tags: vec!["first".to_string()],
|
||||
cdn_file: None,
|
||||
desc: Some(format!("This is the first {name} :)")),
|
||||
inner: serde_yml::Value::Null,
|
||||
draft: true,
|
||||
},
|
||||
)?;
|
||||
|
||||
println!("Created the new resource type {id}! The first resource of this time is available at {:?}.", resource_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
Commands::New {
|
||||
resource_type,
|
||||
id,
|
||||
title,
|
||||
tags,
|
||||
description,
|
||||
skip_draft,
|
||||
} => {
|
||||
let config = SiteConfig::read(&cli.site_path)?;
|
||||
if let Some(resource) = config.resources.get(&resource_type) {
|
||||
let resource_path = cli
|
||||
.site_path
|
||||
.join(webdog::RESOURCES_PATH)
|
||||
.join(&resource.source_path)
|
||||
.join(&id)
|
||||
.with_extension("md");
|
||||
|
||||
if resource_path.exists() {
|
||||
eprintln!(
|
||||
"A {resource_type} resource of the ID {id} already exists, canceling!"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
create_resource(
|
||||
&resource_path,
|
||||
&ResourceMetadata {
|
||||
title,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
tags,
|
||||
cdn_file: None,
|
||||
desc: description,
|
||||
inner: serde_yml::Value::Null,
|
||||
draft: !skip_draft,
|
||||
},
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"Created the new {resource_type} resource {id}! Available at {:?}",
|
||||
resource_path
|
||||
);
|
||||
} else {
|
||||
eprintln!("no resource of type {resource_type}, canceling");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "serve"))]
|
||||
fn main() -> eyre::Result<()> {
|
||||
let site = Site::new(&Path::new("site").canonicalize()?)?;
|
||||
build(site)
|
||||
}
|
||||
|
||||
fn build(site: Site) -> eyre::Result<()> {
|
||||
println!("Building site...");
|
||||
let now = std::time::Instant::now();
|
||||
site.build_once()?;
|
||||
println!("Build completed in {:?}", now.elapsed());
|
||||
/// Creates a new resource from the given metadata.
|
||||
fn create_resource(resource_path: &Path, metadata: &ResourceMetadata) -> eyre::Result<()> {
|
||||
std::fs::write(
|
||||
resource_path,
|
||||
FrontMatter {
|
||||
content: "hello world :)".to_string(),
|
||||
data: Some(metadata),
|
||||
}
|
||||
.format()?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -10,10 +10,9 @@ use rss::{validation::Validate, ChannelBuilder, ItemBuilder};
|
|||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use time::{format_description::well_known::Rfc2822, OffsetDateTime};
|
||||
|
||||
use crate::{builder::SiteBuilder, frontmatter::FrontMatter, link_list::Link, PageMetadata};
|
||||
|
||||
/// Source base path for resources.
|
||||
pub const RESOURCES_PATH: &str = "resources";
|
||||
use crate::{
|
||||
builder::SiteBuilder, frontmatter::FrontMatterRequired, link_list::Link, PageMetadata,
|
||||
};
|
||||
|
||||
/// Metadata for resources.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@ -35,16 +34,13 @@ pub struct ResourceMetadata {
|
|||
/// Whether the resource is a draft. Drafts can be committed without being published to the live site.
|
||||
#[serde(default)]
|
||||
pub draft: bool,
|
||||
/// The resource's content. Defaults to nothing until loaded in another step.
|
||||
#[serde(default)]
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ResourceTemplateData<'r> {
|
||||
/// The resource's metadata.
|
||||
#[serde(flatten)]
|
||||
pub resource: &'r ResourceMetadata,
|
||||
pub resource: &'r FrontMatterRequired<ResourceMetadata>,
|
||||
/// The resource's ID.
|
||||
pub id: String,
|
||||
/// The resource's timestamp. Duplicated to change serialization method.
|
||||
|
@ -129,7 +125,7 @@ struct ResourceListTemplateData<'r> {
|
|||
}
|
||||
|
||||
/// Config for the resource builder.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ResourceBuilderConfig {
|
||||
/// Path to where the resources should be loaded from.
|
||||
pub source_path: String,
|
||||
|
@ -165,7 +161,7 @@ pub struct ResourceBuilder {
|
|||
/// The builder's config.
|
||||
pub config: ResourceBuilderConfig,
|
||||
/// The currently loaded resource metadata.
|
||||
pub loaded_metadata: Vec<(String, ResourceMetadata)>,
|
||||
pub loaded_metadata: Vec<(String, FrontMatterRequired<ResourceMetadata>)>,
|
||||
}
|
||||
|
||||
impl ResourceBuilder {
|
||||
|
@ -187,26 +183,27 @@ impl ResourceBuilder {
|
|||
}
|
||||
|
||||
/// Loads resource metadata from the given path.
|
||||
fn load(builder: &SiteBuilder, path: &Path) -> eyre::Result<(String, ResourceMetadata)> {
|
||||
fn load(
|
||||
builder: &SiteBuilder,
|
||||
path: &Path,
|
||||
) -> eyre::Result<(String, FrontMatterRequired<ResourceMetadata>)> {
|
||||
let id = Self::get_id(path);
|
||||
|
||||
let input = std::fs::read_to_string(path)?;
|
||||
let page = FrontMatter::<ResourceMetadata>::parse(input)
|
||||
let mut page = FrontMatterRequired::<ResourceMetadata>::parse(input)
|
||||
.wrap_err_with(|| eyre::eyre!("Failed to parse resource front matter"))?;
|
||||
|
||||
let parser = Parser::new_ext(&page.content, Options::all());
|
||||
let mut html = String::new();
|
||||
pulldown_cmark::html::push_html(&mut html, parser);
|
||||
*page.content_mut() = html;
|
||||
|
||||
let mut data = page
|
||||
.data
|
||||
.ok_or_else(|| eyre::eyre!("missing front matter for file at {path:?}"))?;
|
||||
data.content = html;
|
||||
if let Some(cdn_file) = data.cdn_file {
|
||||
data.cdn_file = Some(builder.site.config.cdn_url(&cdn_file)?.to_string());
|
||||
let data = page.data_mut();
|
||||
if let Some(cdn_file) = &data.cdn_file {
|
||||
data.cdn_file = Some(builder.site.config.cdn_url(cdn_file)?.to_string());
|
||||
}
|
||||
|
||||
Ok((id, data))
|
||||
Ok((id, page))
|
||||
}
|
||||
|
||||
/// Loads all resource metadata from the given config.
|
||||
|
@ -216,20 +213,26 @@ impl ResourceBuilder {
|
|||
for e in builder
|
||||
.site
|
||||
.site_path
|
||||
.join(RESOURCES_PATH)
|
||||
.join(crate::RESOURCES_PATH)
|
||||
.join(&self.config.source_path)
|
||||
.read_dir()?
|
||||
{
|
||||
let p = e?.path();
|
||||
if let Some("md") = p.extension().and_then(|e| e.to_str()) {
|
||||
let (id, metadata) = Self::load(builder, &p)?;
|
||||
if cfg!(not(debug_assertions)) && metadata.draft {
|
||||
if !builder.serving && metadata.data.as_ref().expect("should never fail").draft {
|
||||
continue;
|
||||
}
|
||||
lmd.push((id, metadata));
|
||||
}
|
||||
}
|
||||
lmd.sort_by(|a, b| b.1.timestamp.cmp(&a.1.timestamp));
|
||||
lmd.sort_by(|a, b| {
|
||||
b.1.data
|
||||
.as_ref()
|
||||
.expect("should never fail")
|
||||
.timestamp
|
||||
.cmp(&a.1.data.as_ref().expect("should never fail").timestamp)
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -246,19 +249,20 @@ impl ResourceBuilder {
|
|||
&self,
|
||||
builder: &SiteBuilder,
|
||||
id: String,
|
||||
resource: &ResourceMetadata,
|
||||
resource: &FrontMatterRequired<ResourceMetadata>,
|
||||
) -> eyre::Result<()> {
|
||||
let out_path = self.build_path(&builder.build_path, &id);
|
||||
|
||||
let data = resource.data();
|
||||
let out = builder.build_page_raw(
|
||||
PageMetadata {
|
||||
template: Some(self.config.resource_template.clone()),
|
||||
title: Some(resource.title.clone()),
|
||||
title: Some(data.title.clone()),
|
||||
embed: Some(EmbedMetadata {
|
||||
title: resource.title.clone(),
|
||||
title: data.title.clone(),
|
||||
site_name: builder.site.config.title.clone(),
|
||||
description: resource.desc.clone(),
|
||||
image: if let Some(cdn_file) = &resource.cdn_file {
|
||||
description: data.desc.clone(),
|
||||
image: if let Some(cdn_file) = &data.cdn_file {
|
||||
Some(builder.site.config.cdn_url(cdn_file)?.to_string())
|
||||
} else {
|
||||
None
|
||||
|
@ -273,7 +277,7 @@ impl ResourceBuilder {
|
|||
ResourceTemplateData {
|
||||
resource,
|
||||
id,
|
||||
timestamp: resource.timestamp,
|
||||
timestamp: resource.data.as_ref().expect("should never fail").timestamp,
|
||||
},
|
||||
)?;
|
||||
std::fs::write(out_path, out)?;
|
||||
|
@ -303,7 +307,7 @@ impl ResourceBuilder {
|
|||
data.push(ResourceTemplateData {
|
||||
resource,
|
||||
id: id.clone(),
|
||||
timestamp: resource.timestamp,
|
||||
timestamp: resource.data().timestamp,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -368,7 +372,7 @@ impl ResourceBuilder {
|
|||
// Build resource lists by tag
|
||||
let mut tags: BTreeMap<String, Vec<&ResourceTemplateData>> = BTreeMap::new();
|
||||
for resource in &data {
|
||||
for tag in resource.resource.tags.iter().cloned() {
|
||||
for tag in resource.resource.data().tags.iter().cloned() {
|
||||
tags.entry(tag).or_default().push(resource);
|
||||
}
|
||||
}
|
||||
|
@ -416,7 +420,7 @@ impl ResourceBuilder {
|
|||
for resource in data {
|
||||
items.push(
|
||||
ItemBuilder::default()
|
||||
.title(Some(resource.resource.title.to_owned()))
|
||||
.title(Some(resource.resource.data().title.to_owned()))
|
||||
.link(Some(
|
||||
builder
|
||||
.site
|
||||
|
@ -428,7 +432,7 @@ impl ResourceBuilder {
|
|||
))?
|
||||
.to_string(),
|
||||
))
|
||||
.description(resource.resource.desc.clone())
|
||||
.description(resource.resource.data().desc.clone())
|
||||
.pub_date(Some(resource.timestamp.format(&Rfc2822)?))
|
||||
.content(Some(builder.tera.render(
|
||||
&self.config.rss_template,
|
||||
|
|
|
@ -19,7 +19,7 @@ use warp::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
resource::RESOURCES_PATH, Site, SiteBuilder, PAGES_PATH, ROOT_PATH, SASS_PATH, TEMPLATES_PATH,
|
||||
Site, SiteBuilder, SiteConfig, PAGES_PATH, RESOURCES_PATH, ROOT_PATH, SASS_PATH, TEMPLATES_PATH,
|
||||
};
|
||||
|
||||
/// Helper to get the "name" of a path.
|
||||
|
@ -94,7 +94,7 @@ fn create(
|
|||
builder.site.build_all_pages(builder)?;
|
||||
builder.build_all_resources()?;
|
||||
}
|
||||
} else if relative_path.display().to_string() == "config.yaml" {
|
||||
} else if relative_path.display().to_string() == SiteConfig::FILENAME {
|
||||
let new_config = serde_yml::from_str(&std::fs::read_to_string(path)?)?;
|
||||
builder.site.config = new_config;
|
||||
builder.reload()?;
|
||||
|
@ -277,8 +277,9 @@ impl Site {
|
|||
.expect("Failed to decode URL");
|
||||
|
||||
if p == "_dev.js" {
|
||||
let res =
|
||||
Response::new(include_str!("./js/refresh_websocket.js").into());
|
||||
let res = Response::new(
|
||||
include_str!("./embedded/js/refresh_websocket.js").into(),
|
||||
);
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue