generate ids

This commit is contained in:
liquidex 2023-08-20 12:15:48 +02:00
parent f99738d031
commit 1a92f85c83
13 changed files with 297 additions and 109 deletions

14
Cargo.lock generated
View file

@ -944,6 +944,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1102,6 +1111,9 @@ name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
@ -1110,6 +1122,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]

View file

@ -164,3 +164,6 @@
% id = "01H87RD70VNVQ75DCWW5FQG9AR"
- breaking
% content.link = "secret"
- this block includes another block's content

View file

@ -18,7 +18,7 @@ handlebars = "4.3.7"
pulldown-cmark = { version = "0.9.3", default-features = false }
serde = { version = "1.0.183", features = ["derive"] }
tokio = { version = "1.32.0", features = ["full"] }
toml_edit = "0.19.14"
toml_edit = { version = "0.19.14", features = ["serde"] }
tower-http = { version = "0.4.3", features = ["fs"] }
tower-livereload = "0.8.0"
walkdir = "2.3.3"

View file

@ -1,42 +0,0 @@
use anyhow::Context;
use codespan_reporting::{
diagnostic::Diagnostic,
files::SimpleFiles,
term::termcolor::{ColorChoice, StandardStream},
};
pub type Files = SimpleFiles<String, String>;
pub type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId;
pub struct Diagnosis {
pub files: Files,
pub diagnostics: Vec<Diagnostic<FileId>>,
}
impl Diagnosis {
pub fn new() -> Self {
Self {
files: Files::new(),
diagnostics: vec![],
}
}
/// Get the source code of a file, assuming it was previously registered.
pub fn get_source(&self, file_id: FileId) -> &str {
self.files
.get(file_id)
.expect("file should have been registered previously")
.source()
}
pub fn report(&self) -> anyhow::Result<()> {
let writer = StandardStream::stderr(ColorChoice::Auto);
let config = codespan_reporting::term::Config::default();
for diagnostic in &self.diagnostics {
codespan_reporting::term::emit(&mut writer.lock(), &config, &self.files, diagnostic)
.context("could not emit diagnostic")?;
}
Ok(())
}
}

View file

@ -3,8 +3,9 @@ use std::ops::Range;
use anyhow::Context;
use treehouse_format::ast::Branch;
use crate::state::{FileId, Treehouse};
use super::{
diagnostics::{Diagnosis, FileId},
parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics},
FixArgs,
};
@ -19,7 +20,7 @@ struct State {
fixes: Vec<Fix>,
}
fn dfs_fix_branch(diagnosis: &mut Diagnosis, file_id: FileId, state: &mut State, branch: &Branch) {
fn dfs_fix_branch(treehouse: &mut Treehouse, file_id: FileId, state: &mut State, branch: &Branch) {
let mut rng = rand::thread_rng();
let ulid = ulid::Generator::new()
.generate_with_source(&mut rng)
@ -31,7 +32,7 @@ fn dfs_fix_branch(diagnosis: &mut Diagnosis, file_id: FileId, state: &mut State,
// the top-level table. Then we also need to pretty-print everything to match the right
// indentation level.
if let Ok(mut toml) =
parse_toml_with_diagnostics(diagnosis, file_id, attributes.data.clone())
parse_toml_with_diagnostics(treehouse, file_id, attributes.data.clone())
{
if !toml.contains_key("id") {
toml["id"] = toml_edit::value(ulid.to_string());
@ -68,7 +69,7 @@ fn dfs_fix_branch(diagnosis: &mut Diagnosis, file_id: FileId, state: &mut State,
// Then we fix child branches.
for child in &branch.children {
dfs_fix_branch(diagnosis, file_id, state, child);
dfs_fix_branch(treehouse, file_id, state, child);
}
}
@ -100,15 +101,15 @@ fn fix_indent_in_generated_toml(toml: &str, min_indent_level: usize) -> String {
}
pub fn fix_file(
diagnosis: &mut Diagnosis,
treehouse: &mut Treehouse,
file_id: FileId,
) -> Result<String, parse::ErrorsEmitted> {
parse_tree_with_diagnostics(diagnosis, file_id).map(|roots| {
let mut source = diagnosis.get_source(file_id).to_owned();
parse_tree_with_diagnostics(treehouse, file_id).map(|roots| {
let mut source = treehouse.get_source(file_id).to_owned();
let mut state = State::default();
for branch in &roots.branches {
dfs_fix_branch(diagnosis, file_id, &mut state, branch);
dfs_fix_branch(treehouse, file_id, &mut state, branch);
}
// Doing a depth-first search of the branches yields fixes from the beginning of the file
@ -127,15 +128,15 @@ pub fn fix_file_cli(fix_args: FixArgs) -> anyhow::Result<()> {
let utf8_filename = fix_args.file.to_string_lossy().into_owned();
let file = std::fs::read_to_string(&fix_args.file).context("cannot read file to fix")?;
let mut diagnosis = Diagnosis::new();
let file_id = diagnosis.files.add(utf8_filename, file);
let mut treehouse = Treehouse::new();
let file_id = treehouse.files.add(utf8_filename, file);
if let Ok(fixed) = fix_file(&mut diagnosis, file_id) {
if let Ok(fixed) = fix_file(&mut treehouse, file_id) {
if fix_args.apply {
// Try to write the backup first. If writing that fails, bail out without overwriting
// the source file.
if let Some(backup_path) = fix_args.backup {
std::fs::write(backup_path, diagnosis.get_source(file_id))
std::fs::write(backup_path, treehouse.get_source(file_id))
.context("cannot write backup; original file will not be overwritten")?;
}
std::fs::write(&fix_args.file, fixed).context("cannot overwrite original file")?;
@ -143,7 +144,7 @@ pub fn fix_file_cli(fix_args: FixArgs) -> anyhow::Result<()> {
println!("{fixed}");
}
} else {
diagnosis.report()?;
treehouse.report_diagnostics()?;
}
Ok(())

View file

@ -1,4 +1,3 @@
pub mod diagnostics;
pub mod fix;
mod parse;
pub mod regenerate;

View file

@ -3,17 +3,17 @@ use std::{ops::Range, str::FromStr};
use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity};
use treehouse_format::ast::Roots;
use super::diagnostics::{Diagnosis, FileId};
use crate::state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse};
pub struct ErrorsEmitted;
pub fn parse_tree_with_diagnostics(
diagnosis: &mut Diagnosis,
treehouse: &mut Treehouse,
file_id: FileId,
) -> Result<Roots, ErrorsEmitted> {
let input = diagnosis.get_source(file_id);
let input = treehouse.get_source(file_id);
Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| {
diagnosis.diagnostics.push(Diagnostic {
treehouse.diagnostics.push(Diagnostic {
severity: Severity::Error,
code: Some("tree".into()),
message: error.kind.to_string(),
@ -30,28 +30,20 @@ pub fn parse_tree_with_diagnostics(
}
pub fn parse_toml_with_diagnostics(
diagnosis: &mut Diagnosis,
treehouse: &mut Treehouse,
file_id: FileId,
range: Range<usize>,
) -> Result<toml_edit::Document, ErrorsEmitted> {
let input = &diagnosis.get_source(file_id)[range.clone()];
let input = &treehouse.get_source(file_id)[range.clone()];
toml_edit::Document::from_str(input).map_err(|error| {
diagnosis.diagnostics.push(Diagnostic {
severity: Severity::Error,
code: Some("toml".into()),
message: error.message().to_owned(),
labels: error
.span()
.map(|span| Label {
style: LabelStyle::Primary,
file_id,
range: range.start + span.start..range.start + span.end,
message: String::new(),
})
.into_iter()
.collect(),
notes: vec![],
});
treehouse
.diagnostics
.push(toml_error_to_diagnostic(TomlError {
message: error.message().to_owned(),
span: error.span(),
file_id,
input_range: range.clone(),
}));
ErrorsEmitted
})
}

View file

@ -19,7 +19,7 @@ use walkdir::WalkDir;
use crate::{cli::parse::parse_tree_with_diagnostics, html::tree::branches_to_html};
use super::diagnostics::{Diagnosis, FileId};
use crate::state::{FileId, Treehouse};
#[derive(Default)]
struct Generator {
@ -39,23 +39,23 @@ impl Generator {
fn register_template(
handlebars: &mut Handlebars<'_>,
diagnosis: &mut Diagnosis,
treehouse: &mut Treehouse,
name: &str,
path: &Path,
) -> anyhow::Result<FileId> {
let source = std::fs::read_to_string(path)
.with_context(|| format!("cannot read template file {path:?}"))?;
let file_id = diagnosis
let file_id = treehouse
.files
.add(path.to_string_lossy().into_owned(), source);
let file = diagnosis
let file = treehouse
.files
.get(file_id)
.expect("file was just added to the list");
let source = file.source();
if let Err(error) = handlebars.register_template_string(name, source) {
Self::wrangle_handlebars_error_into_diagnostic(
diagnosis,
treehouse,
file_id,
error.line_no,
error.column_no,
@ -66,18 +66,18 @@ impl Generator {
}
fn wrangle_handlebars_error_into_diagnostic(
diagnosis: &mut Diagnosis,
treehouse: &mut Treehouse,
file_id: FileId,
line: Option<usize>,
column: Option<usize>,
message: String,
) -> anyhow::Result<()> {
if let (Some(line), Some(column)) = (line, column) {
let line_range = diagnosis
let line_range = treehouse
.files
.line_range(file_id, line)
.expect("file was added to the list");
diagnosis.diagnostics.push(Diagnostic {
treehouse.diagnostics.push(Diagnostic {
severity: Severity::Error,
code: Some("template".into()),
message,
@ -90,7 +90,7 @@ impl Generator {
notes: vec![],
})
} else {
let file = diagnosis
let file = treehouse
.files
.get(file_id)
.expect("file should already be in the list");
@ -99,13 +99,13 @@ impl Generator {
Ok(())
}
fn generate_all_files(&self, dirs: &Dirs<'_>) -> anyhow::Result<Diagnosis> {
let mut diagnosis = Diagnosis::new();
fn generate_all_files(&self, dirs: &Dirs<'_>) -> anyhow::Result<Treehouse> {
let mut treehouse = Treehouse::new();
let mut handlebars = Handlebars::new();
let tree_template = Self::register_template(
&mut handlebars,
&mut diagnosis,
&mut treehouse,
"tree",
&dirs.template_dir.join("tree.hbs"),
)?;
@ -123,7 +123,7 @@ impl Generator {
let source = match std::fs::read_to_string(path) {
Ok(source) => source,
Err(error) => {
diagnosis.diagnostics.push(Diagnostic {
treehouse.diagnostics.push(Diagnostic {
severity: Severity::Error,
code: None,
message: format!("{utf8_filename}: cannot read file: {error}"),
@ -133,19 +133,18 @@ impl Generator {
continue;
}
};
let file_id = diagnosis.files.add(utf8_filename.into_owned(), source);
let file_id = treehouse.files.add(utf8_filename.into_owned(), source);
if let Ok(roots) = parse_tree_with_diagnostics(&mut diagnosis, file_id) {
if let Ok(roots) = parse_tree_with_diagnostics(&mut treehouse, file_id) {
let mut tree = String::new();
let source = diagnosis.get_source(file_id);
branches_to_html(&mut tree, &roots.branches, source);
branches_to_html(&mut tree, &mut treehouse, file_id, &roots.branches);
let template_data = TemplateData { tree };
let templated_html = match handlebars.render("tree", &template_data) {
Ok(html) => html,
Err(error) => {
Self::wrangle_handlebars_error_into_diagnostic(
&mut diagnosis,
&mut treehouse,
tree_template,
error.line_no,
error.column_no,
@ -159,7 +158,7 @@ impl Generator {
}
}
Ok(diagnosis)
Ok(treehouse)
}
}
@ -187,9 +186,9 @@ pub fn regenerate(dirs: &Dirs<'_>) -> anyhow::Result<()> {
info!("generating standalone pages");
let mut generator = Generator::default();
generator.add_directory_rec(dirs.content_dir)?;
let diagnosis = generator.generate_all_files(dirs)?;
let treehouse = generator.generate_all_files(dirs)?;
diagnosis.report()?;
treehouse.report_diagnostics()?;
Ok(())
}

View file

@ -0,0 +1,40 @@
use serde::Deserialize;
/// Branch attributes.
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
pub struct Attributes {
/// Unique identifier of the branch.
///
/// Note that this must be unique to the _entire_ site, not just a single tree.
/// This is because trees may be embedded within each other using [`Content::Link`].
#[serde(default)]
pub id: String,
/// Controls how the block should be presented.
#[serde(default)]
pub content: Content,
}
/// Controls for block content presentation.
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Content {
/// Children are stored inline in the block. Nothing special happens.
#[default]
Inline,
/// Link to another tree.
///
/// When JavaScript is enabled, the tree's roots will be embedded inline into the branch and
/// loaded lazily.
///
/// Without JavaScript, the tree will be linked with an `<a>` element.
///
/// The string provided as an argument is relative to the `content` root and should not contain
/// any file extensions. For example, to link to `content/my-article.tree`,
/// use `content.link = "my-article"`.
///
/// Note that `Link` branches must not contain any children. If a `Link` branch does contain
/// children, an `attribute`-type error is raised.
Link(String),
}

View file

@ -1,2 +1,20 @@
use std::fmt::{self, Display, Write};
pub mod attributes;
mod markdown;
pub mod tree;
pub struct EscapeAttribute<'a>(&'a str);
impl<'a> Display for EscapeAttribute<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for c in self.0.chars() {
if c == '"' {
f.write_str("&quot;")?;
} else {
f.write_char(c)?;
}
}
Ok(())
}
}

View file

@ -1,13 +1,81 @@
use std::fmt::Write;
use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity};
use treehouse_format::{ast::Branch, pull::BranchKind};
use super::markdown;
use crate::{
html::EscapeAttribute,
state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse},
};
pub fn branch_to_html(s: &mut String, branch: &Branch, source: &str) {
s.push_str(if !branch.children.is_empty() {
"<li class=\"branch\">"
use super::{attributes::Attributes, markdown};
pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId, branch: &Branch) {
let source = treehouse.get_source(file_id);
let mut successfully_parsed = true;
let mut attributes = if let Some(attributes) = &branch.attributes {
toml_edit::de::from_str(&source[attributes.data.clone()]).unwrap_or_else(|error| {
treehouse
.diagnostics
.push(toml_error_to_diagnostic(TomlError {
message: error.message().to_owned(),
span: error.span(),
file_id,
input_range: attributes.data.clone(),
}));
successfully_parsed = false;
Attributes::default()
})
} else {
"<li class=\"leaf\">"
});
Attributes::default()
};
let successfully_parsed = successfully_parsed;
// Only check for attribute validity if the attributes were parsed successfully.
if successfully_parsed {
let attribute_warning_span = branch
.attributes
.as_ref()
.map(|attributes| attributes.percent.clone())
.unwrap_or(branch.kind_span.clone());
if attributes.id.is_empty() {
attributes.id = format!("treehouse-missingno-{}", treehouse.next_missingno());
treehouse.diagnostics.push(Diagnostic {
severity: Severity::Warning,
code: Some("attr".into()),
message: "branch does not have an `id` attribute".into(),
labels: vec![Label {
style: LabelStyle::Primary,
file_id,
range: attribute_warning_span,
message: String::new(),
}],
notes: vec![
format!(
"note: a generated id `{}` will be used, but this id is unstable and will not persist across generations",
attributes.id
),
format!("help: run `treehouse fix {}` to add missing ids to branches", treehouse.get_filename(file_id)),
],
});
}
}
// Reborrow because the closure requires unique access (it adds a new diagnostic.)
let source = treehouse.get_source(file_id);
let class = if !branch.children.is_empty() {
"branch"
} else {
"leaf"
};
write!(
s,
"<li class=\"{class}\" id=\"{}\">",
EscapeAttribute(&attributes.id)
)
.unwrap();
{
if !branch.children.is_empty() {
s.push_str(match branch.kind {
@ -42,17 +110,22 @@ pub fn branch_to_html(s: &mut String, branch: &Branch, source: &str) {
if !branch.children.is_empty() {
s.push_str("</summary>");
branches_to_html(s, &branch.children, source);
branches_to_html(s, treehouse, file_id, &branch.children);
s.push_str("</details>");
}
}
s.push_str("</li>");
}
pub fn branches_to_html(s: &mut String, branches: &[Branch], source: &str) {
pub fn branches_to_html(
s: &mut String,
treehouse: &mut Treehouse,
file_id: FileId,
branches: &[Branch],
) {
s.push_str("<ul>");
for child in branches {
branch_to_html(s, child, source);
branch_to_html(s, treehouse, file_id, child);
}
s.push_str("</ul>");
}

View file

@ -10,6 +10,7 @@ use log::{error, info};
mod cli;
mod html;
mod state;
async fn fallible_main() -> anyhow::Result<()> {
let args = ProgramArgs::parse();

View file

@ -0,0 +1,90 @@
use std::ops::Range;
use anyhow::Context;
use codespan_reporting::{
diagnostic::{Diagnostic, Label, LabelStyle, Severity},
files::SimpleFiles,
term::termcolor::{ColorChoice, StandardStream},
};
use ulid::Ulid;
pub type Files = SimpleFiles<String, String>;
pub type FileId = <Files as codespan_reporting::files::Files<'static>>::FileId;
/// Treehouse compilation context.
pub struct Treehouse {
pub files: Files,
pub diagnostics: Vec<Diagnostic<FileId>>,
missingno_generator: ulid::Generator,
}
impl Treehouse {
pub fn new() -> Self {
Self {
files: Files::new(),
diagnostics: vec![],
missingno_generator: ulid::Generator::new(),
}
}
/// Get the source code of a file, assuming it was previously registered.
pub fn get_source(&self, file_id: FileId) -> &str {
self.files
.get(file_id)
.expect("file should have been registered previously")
.source()
}
/// Get the name of a file, assuming it was previously registered.
pub fn get_filename(&self, file_id: FileId) -> &str {
self.files
.get(file_id)
.expect("file should have been registered previously")
.name()
}
pub fn report_diagnostics(&self) -> anyhow::Result<()> {
let writer = StandardStream::stderr(ColorChoice::Auto);
let config = codespan_reporting::term::Config::default();
for diagnostic in &self.diagnostics {
codespan_reporting::term::emit(&mut writer.lock(), &config, &self.files, diagnostic)
.context("could not emit diagnostic")?;
}
Ok(())
}
pub fn next_missingno(&mut self) -> Ulid {
self.missingno_generator
.generate()
.expect("just how much disk space do you have?")
}
}
pub struct TomlError {
pub message: String,
pub span: Option<Range<usize>>,
pub file_id: FileId,
pub input_range: Range<usize>,
}
pub fn toml_error_to_diagnostic(error: TomlError) -> Diagnostic<FileId> {
Diagnostic {
severity: Severity::Error,
code: Some("toml".into()),
message: error.message,
labels: error
.span
.map(|span| Label {
style: LabelStyle::Primary,
file_id: error.file_id,
range: error.input_range.start + span.start..error.input_range.start + span.end,
message: String::new(),
})
.into_iter()
.collect(),
notes: vec![],
}
}