generate ids
This commit is contained in:
parent
f99738d031
commit
1a92f85c83
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -944,6 +944,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -1102,6 +1111,9 @@ name = "toml_datetime"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
|
@ -1110,6 +1122,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
|
|
|
@ -164,3 +164,6 @@
|
||||||
|
|
||||||
% id = "01H87RD70VNVQ75DCWW5FQG9AR"
|
% id = "01H87RD70VNVQ75DCWW5FQG9AR"
|
||||||
- breaking
|
- breaking
|
||||||
|
|
||||||
|
% content.link = "secret"
|
||||||
|
- this block includes another block's content
|
||||||
|
|
|
@ -18,7 +18,7 @@ handlebars = "4.3.7"
|
||||||
pulldown-cmark = { version = "0.9.3", default-features = false }
|
pulldown-cmark = { version = "0.9.3", default-features = false }
|
||||||
serde = { version = "1.0.183", features = ["derive"] }
|
serde = { version = "1.0.183", features = ["derive"] }
|
||||||
tokio = { version = "1.32.0", features = ["full"] }
|
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-http = { version = "0.4.3", features = ["fs"] }
|
||||||
tower-livereload = "0.8.0"
|
tower-livereload = "0.8.0"
|
||||||
walkdir = "2.3.3"
|
walkdir = "2.3.3"
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,8 +3,9 @@ use std::ops::Range;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use treehouse_format::ast::Branch;
|
use treehouse_format::ast::Branch;
|
||||||
|
|
||||||
|
use crate::state::{FileId, Treehouse};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
diagnostics::{Diagnosis, FileId},
|
|
||||||
parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics},
|
parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics},
|
||||||
FixArgs,
|
FixArgs,
|
||||||
};
|
};
|
||||||
|
@ -19,7 +20,7 @@ struct State {
|
||||||
fixes: Vec<Fix>,
|
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 mut rng = rand::thread_rng();
|
||||||
let ulid = ulid::Generator::new()
|
let ulid = ulid::Generator::new()
|
||||||
.generate_with_source(&mut rng)
|
.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
|
// the top-level table. Then we also need to pretty-print everything to match the right
|
||||||
// indentation level.
|
// indentation level.
|
||||||
if let Ok(mut toml) =
|
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") {
|
if !toml.contains_key("id") {
|
||||||
toml["id"] = toml_edit::value(ulid.to_string());
|
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.
|
// Then we fix child branches.
|
||||||
for child in &branch.children {
|
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(
|
pub fn fix_file(
|
||||||
diagnosis: &mut Diagnosis,
|
treehouse: &mut Treehouse,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
) -> Result<String, parse::ErrorsEmitted> {
|
) -> Result<String, parse::ErrorsEmitted> {
|
||||||
parse_tree_with_diagnostics(diagnosis, file_id).map(|roots| {
|
parse_tree_with_diagnostics(treehouse, file_id).map(|roots| {
|
||||||
let mut source = diagnosis.get_source(file_id).to_owned();
|
let mut source = treehouse.get_source(file_id).to_owned();
|
||||||
let mut state = State::default();
|
let mut state = State::default();
|
||||||
|
|
||||||
for branch in &roots.branches {
|
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
|
// 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 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 file = std::fs::read_to_string(&fix_args.file).context("cannot read file to fix")?;
|
||||||
|
|
||||||
let mut diagnosis = Diagnosis::new();
|
let mut treehouse = Treehouse::new();
|
||||||
let file_id = diagnosis.files.add(utf8_filename, file);
|
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 {
|
if fix_args.apply {
|
||||||
// Try to write the backup first. If writing that fails, bail out without overwriting
|
// Try to write the backup first. If writing that fails, bail out without overwriting
|
||||||
// the source file.
|
// the source file.
|
||||||
if let Some(backup_path) = fix_args.backup {
|
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")?;
|
.context("cannot write backup; original file will not be overwritten")?;
|
||||||
}
|
}
|
||||||
std::fs::write(&fix_args.file, fixed).context("cannot overwrite original file")?;
|
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}");
|
println!("{fixed}");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
diagnosis.report()?;
|
treehouse.report_diagnostics()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
pub mod diagnostics;
|
|
||||||
pub mod fix;
|
pub mod fix;
|
||||||
mod parse;
|
mod parse;
|
||||||
pub mod regenerate;
|
pub mod regenerate;
|
||||||
|
|
|
@ -3,17 +3,17 @@ use std::{ops::Range, str::FromStr};
|
||||||
use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity};
|
use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity};
|
||||||
use treehouse_format::ast::Roots;
|
use treehouse_format::ast::Roots;
|
||||||
|
|
||||||
use super::diagnostics::{Diagnosis, FileId};
|
use crate::state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse};
|
||||||
|
|
||||||
pub struct ErrorsEmitted;
|
pub struct ErrorsEmitted;
|
||||||
|
|
||||||
pub fn parse_tree_with_diagnostics(
|
pub fn parse_tree_with_diagnostics(
|
||||||
diagnosis: &mut Diagnosis,
|
treehouse: &mut Treehouse,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
) -> Result<Roots, ErrorsEmitted> {
|
) -> 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| {
|
Roots::parse(&mut treehouse_format::pull::Parser { input, position: 0 }).map_err(|error| {
|
||||||
diagnosis.diagnostics.push(Diagnostic {
|
treehouse.diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
code: Some("tree".into()),
|
code: Some("tree".into()),
|
||||||
message: error.kind.to_string(),
|
message: error.kind.to_string(),
|
||||||
|
@ -30,28 +30,20 @@ pub fn parse_tree_with_diagnostics(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_toml_with_diagnostics(
|
pub fn parse_toml_with_diagnostics(
|
||||||
diagnosis: &mut Diagnosis,
|
treehouse: &mut Treehouse,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
) -> Result<toml_edit::Document, ErrorsEmitted> {
|
) -> 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| {
|
toml_edit::Document::from_str(input).map_err(|error| {
|
||||||
diagnosis.diagnostics.push(Diagnostic {
|
treehouse
|
||||||
severity: Severity::Error,
|
.diagnostics
|
||||||
code: Some("toml".into()),
|
.push(toml_error_to_diagnostic(TomlError {
|
||||||
message: error.message().to_owned(),
|
message: error.message().to_owned(),
|
||||||
labels: error
|
span: error.span(),
|
||||||
.span()
|
|
||||||
.map(|span| Label {
|
|
||||||
style: LabelStyle::Primary,
|
|
||||||
file_id,
|
file_id,
|
||||||
range: range.start + span.start..range.start + span.end,
|
input_range: range.clone(),
|
||||||
message: String::new(),
|
}));
|
||||||
})
|
|
||||||
.into_iter()
|
|
||||||
.collect(),
|
|
||||||
notes: vec![],
|
|
||||||
});
|
|
||||||
ErrorsEmitted
|
ErrorsEmitted
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ use walkdir::WalkDir;
|
||||||
|
|
||||||
use crate::{cli::parse::parse_tree_with_diagnostics, html::tree::branches_to_html};
|
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)]
|
#[derive(Default)]
|
||||||
struct Generator {
|
struct Generator {
|
||||||
|
@ -39,23 +39,23 @@ impl Generator {
|
||||||
|
|
||||||
fn register_template(
|
fn register_template(
|
||||||
handlebars: &mut Handlebars<'_>,
|
handlebars: &mut Handlebars<'_>,
|
||||||
diagnosis: &mut Diagnosis,
|
treehouse: &mut Treehouse,
|
||||||
name: &str,
|
name: &str,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> anyhow::Result<FileId> {
|
) -> anyhow::Result<FileId> {
|
||||||
let source = std::fs::read_to_string(path)
|
let source = std::fs::read_to_string(path)
|
||||||
.with_context(|| format!("cannot read template file {path:?}"))?;
|
.with_context(|| format!("cannot read template file {path:?}"))?;
|
||||||
let file_id = diagnosis
|
let file_id = treehouse
|
||||||
.files
|
.files
|
||||||
.add(path.to_string_lossy().into_owned(), source);
|
.add(path.to_string_lossy().into_owned(), source);
|
||||||
let file = diagnosis
|
let file = treehouse
|
||||||
.files
|
.files
|
||||||
.get(file_id)
|
.get(file_id)
|
||||||
.expect("file was just added to the list");
|
.expect("file was just added to the list");
|
||||||
let source = file.source();
|
let source = file.source();
|
||||||
if let Err(error) = handlebars.register_template_string(name, source) {
|
if let Err(error) = handlebars.register_template_string(name, source) {
|
||||||
Self::wrangle_handlebars_error_into_diagnostic(
|
Self::wrangle_handlebars_error_into_diagnostic(
|
||||||
diagnosis,
|
treehouse,
|
||||||
file_id,
|
file_id,
|
||||||
error.line_no,
|
error.line_no,
|
||||||
error.column_no,
|
error.column_no,
|
||||||
|
@ -66,18 +66,18 @@ impl Generator {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wrangle_handlebars_error_into_diagnostic(
|
fn wrangle_handlebars_error_into_diagnostic(
|
||||||
diagnosis: &mut Diagnosis,
|
treehouse: &mut Treehouse,
|
||||||
file_id: FileId,
|
file_id: FileId,
|
||||||
line: Option<usize>,
|
line: Option<usize>,
|
||||||
column: Option<usize>,
|
column: Option<usize>,
|
||||||
message: String,
|
message: String,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if let (Some(line), Some(column)) = (line, column) {
|
if let (Some(line), Some(column)) = (line, column) {
|
||||||
let line_range = diagnosis
|
let line_range = treehouse
|
||||||
.files
|
.files
|
||||||
.line_range(file_id, line)
|
.line_range(file_id, line)
|
||||||
.expect("file was added to the list");
|
.expect("file was added to the list");
|
||||||
diagnosis.diagnostics.push(Diagnostic {
|
treehouse.diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
code: Some("template".into()),
|
code: Some("template".into()),
|
||||||
message,
|
message,
|
||||||
|
@ -90,7 +90,7 @@ impl Generator {
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let file = diagnosis
|
let file = treehouse
|
||||||
.files
|
.files
|
||||||
.get(file_id)
|
.get(file_id)
|
||||||
.expect("file should already be in the list");
|
.expect("file should already be in the list");
|
||||||
|
@ -99,13 +99,13 @@ impl Generator {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_all_files(&self, dirs: &Dirs<'_>) -> anyhow::Result<Diagnosis> {
|
fn generate_all_files(&self, dirs: &Dirs<'_>) -> anyhow::Result<Treehouse> {
|
||||||
let mut diagnosis = Diagnosis::new();
|
let mut treehouse = Treehouse::new();
|
||||||
|
|
||||||
let mut handlebars = Handlebars::new();
|
let mut handlebars = Handlebars::new();
|
||||||
let tree_template = Self::register_template(
|
let tree_template = Self::register_template(
|
||||||
&mut handlebars,
|
&mut handlebars,
|
||||||
&mut diagnosis,
|
&mut treehouse,
|
||||||
"tree",
|
"tree",
|
||||||
&dirs.template_dir.join("tree.hbs"),
|
&dirs.template_dir.join("tree.hbs"),
|
||||||
)?;
|
)?;
|
||||||
|
@ -123,7 +123,7 @@ impl Generator {
|
||||||
let source = match std::fs::read_to_string(path) {
|
let source = match std::fs::read_to_string(path) {
|
||||||
Ok(source) => source,
|
Ok(source) => source,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
diagnosis.diagnostics.push(Diagnostic {
|
treehouse.diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
code: None,
|
code: None,
|
||||||
message: format!("{utf8_filename}: cannot read file: {error}"),
|
message: format!("{utf8_filename}: cannot read file: {error}"),
|
||||||
|
@ -133,19 +133,18 @@ impl Generator {
|
||||||
continue;
|
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 mut tree = String::new();
|
||||||
let source = diagnosis.get_source(file_id);
|
branches_to_html(&mut tree, &mut treehouse, file_id, &roots.branches);
|
||||||
branches_to_html(&mut tree, &roots.branches, source);
|
|
||||||
|
|
||||||
let template_data = TemplateData { tree };
|
let template_data = TemplateData { tree };
|
||||||
let templated_html = match handlebars.render("tree", &template_data) {
|
let templated_html = match handlebars.render("tree", &template_data) {
|
||||||
Ok(html) => html,
|
Ok(html) => html,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
Self::wrangle_handlebars_error_into_diagnostic(
|
Self::wrangle_handlebars_error_into_diagnostic(
|
||||||
&mut diagnosis,
|
&mut treehouse,
|
||||||
tree_template,
|
tree_template,
|
||||||
error.line_no,
|
error.line_no,
|
||||||
error.column_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");
|
info!("generating standalone pages");
|
||||||
let mut generator = Generator::default();
|
let mut generator = Generator::default();
|
||||||
generator.add_directory_rec(dirs.content_dir)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
40
crates/treehouse/src/html/attributes.rs
Normal file
40
crates/treehouse/src/html/attributes.rs
Normal 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),
|
||||||
|
}
|
|
@ -1,2 +1,20 @@
|
||||||
|
use std::fmt::{self, Display, Write};
|
||||||
|
|
||||||
|
pub mod attributes;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
pub mod tree;
|
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(""")?;
|
||||||
|
} else {
|
||||||
|
f.write_char(c)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,81 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity};
|
||||||
use treehouse_format::{ast::Branch, pull::BranchKind};
|
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) {
|
use super::{attributes::Attributes, markdown};
|
||||||
s.push_str(if !branch.children.is_empty() {
|
|
||||||
"<li class=\"branch\">"
|
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 {
|
} 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() {
|
if !branch.children.is_empty() {
|
||||||
s.push_str(match branch.kind {
|
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() {
|
if !branch.children.is_empty() {
|
||||||
s.push_str("</summary>");
|
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("</details>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.push_str("</li>");
|
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>");
|
s.push_str("<ul>");
|
||||||
for child in branches {
|
for child in branches {
|
||||||
branch_to_html(s, child, source);
|
branch_to_html(s, treehouse, file_id, child);
|
||||||
}
|
}
|
||||||
s.push_str("</ul>");
|
s.push_str("</ul>");
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ use log::{error, info};
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod html;
|
mod html;
|
||||||
|
mod state;
|
||||||
|
|
||||||
async fn fallible_main() -> anyhow::Result<()> {
|
async fn fallible_main() -> anyhow::Result<()> {
|
||||||
let args = ProgramArgs::parse();
|
let args = ProgramArgs::parse();
|
||||||
|
|
90
crates/treehouse/src/state.rs
Normal file
90
crates/treehouse/src/state.rs
Normal 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![],
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue