diff --git a/crates/treehouse/src/cli/regenerate.rs b/crates/treehouse/src/cli/regenerate.rs index 5711d59..8ae097d 100644 --- a/crates/treehouse/src/cli/regenerate.rs +++ b/crates/treehouse/src/cli/regenerate.rs @@ -17,7 +17,9 @@ use tower_http::services::ServeDir; use tower_livereload::LiveReloadLayer; 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, tree::SemaRoots, +}; use crate::state::{FileId, Treehouse}; @@ -132,6 +134,8 @@ impl Generator { ); if let Ok(roots) = parse_tree_with_diagnostics(&mut treehouse, file_id) { + let roots = SemaRoots::from_roots(&mut treehouse, file_id, roots); + let mut tree = String::new(); branches_to_html(&mut tree, &mut treehouse, file_id, &roots.branches); diff --git a/crates/treehouse/src/html/mod.rs b/crates/treehouse/src/html/mod.rs index eec01a6..f9cd40e 100644 --- a/crates/treehouse/src/html/mod.rs +++ b/crates/treehouse/src/html/mod.rs @@ -1,6 +1,5 @@ use std::fmt::{self, Display, Write}; -pub mod attributes; mod markdown; pub mod tree; diff --git a/crates/treehouse/src/html/tree.rs b/crates/treehouse/src/html/tree.rs index 95a8835..4892cfc 100644 --- a/crates/treehouse/src/html/tree.rs +++ b/crates/treehouse/src/html/tree.rs @@ -1,43 +1,37 @@ use std::fmt::Write; -use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity}; -use treehouse_format::{ast::Branch, pull::BranchKind}; +use pulldown_cmark::{BrokenLink, LinkType}; +use treehouse_format::pull::BranchKind; use crate::{ html::EscapeAttribute, - state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse}, + state::{FileId, Treehouse}, + tree::{attributes::Content, SemaBranchId}, }; -use super::{ - attributes::{Attributes, Content}, - markdown, EscapeHtml, -}; - -pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId, branch: &Branch) { - let attributes = parse_attributes(treehouse, file_id, branch); +use super::{markdown, EscapeHtml}; +pub fn branch_to_html( + s: &mut String, + treehouse: &mut Treehouse, + file_id: FileId, + branch_id: SemaBranchId, +) { // Reborrow because the closure requires unique access (it adds a new diagnostic.) let source = treehouse.source(file_id); + let branch = treehouse.tree.branch(branch_id); let has_children = - !branch.children.is_empty() || matches!(attributes.content, Content::Link(_)); - - let id = format!( - "{}:{}", - treehouse - .tree_path(file_id) - .expect("file should have a tree path"), - attributes.id - ); + !branch.children.is_empty() || matches!(branch.attributes.content, Content::Link(_)); let class = if has_children { "branch" } else { "leaf" }; - let component = if let Content::Link(_) = attributes.content { + let component = if let Content::Link(_) = branch.attributes.content { "th-b-linked" } else { "th-b" }; - let linked_branch = if let Content::Link(link) = &attributes.content { + let linked_branch = if let Content::Link(link) = &branch.attributes.content { format!(" data-th-link=\"{}\"", EscapeHtml(link)) } else { String::new() @@ -46,7 +40,7 @@ pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId write!( s, "
  • ", - EscapeAttribute(&id) + EscapeAttribute(&branch.html_id) ) .unwrap(); { @@ -77,13 +71,38 @@ pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId unindented_block_content.push('\n'); } - let markdown_parser = pulldown_cmark::Parser::new_ext(&unindented_block_content, { - use pulldown_cmark::Options; - Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES - }); + let broken_link_callback = &mut |broken_link: BrokenLink<'_>| { + if let LinkType::Reference | LinkType::Shortcut = broken_link.link_type { + broken_link + .reference + .split_once(':') + .and_then(|(kind, linked)| match kind { + "branch" => treehouse + .branches_by_named_id + .get(linked) + .map(|&branch_id| { + ( + format!("#{}", treehouse.tree.branch(branch_id).html_id).into(), + "".into(), + ) + }), + _ => None, + }) + } else { + None + } + }; + let markdown_parser = pulldown_cmark::Parser::new_with_broken_link_callback( + &unindented_block_content, + { + use pulldown_cmark::Options; + Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TABLES + }, + Some(broken_link_callback), + ); markdown::push_html(s, markdown_parser); - if let Content::Link(link) = &attributes.content { + if let Content::Link(link) = &branch.attributes.content { write!( s, "", @@ -95,7 +114,7 @@ pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId s.push_str(""); { - if let Content::Link(link) = &attributes.content { + if let Content::Link(link) = &branch.attributes.content { write!( s, "", @@ -107,7 +126,7 @@ pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId write!( s, "", - EscapeAttribute(&id) + EscapeAttribute(&branch.html_id) ) .unwrap(); } @@ -115,7 +134,15 @@ pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId if has_children { s.push_str(""); - branches_to_html(s, treehouse, file_id, &branch.children); + { + s.push_str(""); + } s.push_str(""); } else { s.push_str(""); @@ -124,90 +151,14 @@ pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId s.push_str("
  • "); } -fn parse_attributes(treehouse: &mut Treehouse, file_id: usize, branch: &Branch) -> Attributes { - let source = treehouse.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 { - 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()); - - // Check that every block has an ID. - 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.clone(), - 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.filename(file_id)), - ], - }); - } - - // Check that link-type blocks are `+`-type to facilitate lazy loading. - if let Content::Link(_) = &attributes.content { - if branch.kind == BranchKind::Expanded { - treehouse.diagnostics.push(Diagnostic { - severity: Severity::Warning, - code: Some("attr".into()), - message: "`content.link` branch is expanded by default".into(), - labels: vec![Label { - style: LabelStyle::Primary, - file_id, - range: branch.kind_span.clone(), - message: String::new(), - }], - notes: vec![ - "note: `content.link` branches should normally be collapsed to allow for lazy loading".into(), - ], - }); - } - } - } - attributes -} - pub fn branches_to_html( s: &mut String, treehouse: &mut Treehouse, file_id: FileId, - branches: &[Branch], + branches: &[SemaBranchId], ) { s.push_str(""); diff --git a/crates/treehouse/src/main.rs b/crates/treehouse/src/main.rs index 324758a..c74243e 100644 --- a/crates/treehouse/src/main.rs +++ b/crates/treehouse/src/main.rs @@ -12,6 +12,7 @@ mod cli; mod html; mod paths; mod state; +mod tree; async fn fallible_main() -> anyhow::Result<()> { let args = ProgramArgs::parse(); diff --git a/crates/treehouse/src/state.rs b/crates/treehouse/src/state.rs index f739862..fb9bc36 100644 --- a/crates/treehouse/src/state.rs +++ b/crates/treehouse/src/state.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::{collections::HashMap, ops::Range}; use anyhow::Context; use codespan_reporting::{ @@ -8,6 +8,8 @@ use codespan_reporting::{ }; use ulid::Ulid; +use crate::tree::{SemaBranchId, SemaTree}; + pub type Files = SimpleFiles; pub type FileId = >::FileId; @@ -16,18 +18,31 @@ pub struct Treehouse { pub files: Files, pub diagnostics: Vec>, + pub tree: SemaTree, + pub branches_by_named_id: HashMap, + // Bit of a hack because I don't wanna write my own `Files`. tree_paths: Vec>, missingno_generator: ulid::Generator, } +#[derive(Debug, Clone)] +pub struct BranchRef { + pub html_id: String, + pub file_id: FileId, + pub kind_span: Range, +} + impl Treehouse { pub fn new() -> Self { Self { files: Files::new(), diagnostics: vec![], + tree: SemaTree::default(), + branches_by_named_id: HashMap::new(), + tree_paths: vec![], missingno_generator: ulid::Generator::new(), diff --git a/crates/treehouse/src/html/attributes.rs b/crates/treehouse/src/tree/attributes.rs similarity index 100% rename from crates/treehouse/src/html/attributes.rs rename to crates/treehouse/src/tree/attributes.rs diff --git a/crates/treehouse/src/tree/mod.rs b/crates/treehouse/src/tree/mod.rs new file mode 100644 index 0000000..844e9d7 --- /dev/null +++ b/crates/treehouse/src/tree/mod.rs @@ -0,0 +1,205 @@ +pub mod attributes; + +use std::ops::Range; + +use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle, Severity}; +use treehouse_format::{ + ast::{Branch, Roots}, + pull::BranchKind, +}; + +use crate::{ + state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse}, + tree::attributes::{Attributes, Content}, +}; + +#[derive(Debug, Default, Clone)] +pub struct SemaTree { + branches: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SemaBranchId(usize); + +impl SemaTree { + pub fn add_branch(&mut self, branch: SemaBranch) -> SemaBranchId { + let id = self.branches.len(); + self.branches.push(branch); + SemaBranchId(id) + } + + pub fn branch(&self, id: SemaBranchId) -> &SemaBranch { + &self.branches[id.0] + } +} + +#[derive(Debug, Clone)] +pub struct SemaRoots { + pub branches: Vec, +} + +impl SemaRoots { + pub fn from_roots(treehouse: &mut Treehouse, file_id: FileId, roots: Roots) -> Self { + Self { + branches: roots + .branches + .into_iter() + .map(|branch| SemaBranch::from_branch(treehouse, file_id, branch)) + .collect(), + } + } +} + +/// Analyzed branch. +#[derive(Debug, Clone)] +pub struct SemaBranch { + pub file_id: FileId, + + pub indent_level: usize, + pub raw_attributes: Option, + pub kind: BranchKind, + pub kind_span: Range, + pub content: Range, + + pub html_id: String, + pub attributes: Attributes, + pub children: Vec, +} + +impl SemaBranch { + pub fn from_branch(treehouse: &mut Treehouse, file_id: FileId, branch: Branch) -> SemaBranchId { + let attributes = Self::parse_attributes(treehouse, file_id, &branch); + + let named_id = attributes.id.clone(); + let html_id = format!( + "{}:{}", + treehouse + .tree_path(file_id) + .expect("file should have a tree path"), + attributes.id + ); + + let branch = Self { + file_id, + indent_level: branch.indent_level, + raw_attributes: branch.attributes, + kind: branch.kind, + kind_span: branch.kind_span, + content: branch.content, + html_id, + attributes, + children: branch + .children + .into_iter() + .map(|child| Self::from_branch(treehouse, file_id, child)) + .collect(), + }; + let new_branch_id = treehouse.tree.add_branch(branch); + + if let Some(old_branch_id) = treehouse + .branches_by_named_id + .insert(named_id.clone(), new_branch_id) + { + let new_branch = treehouse.tree.branch(new_branch_id); + let old_branch = treehouse.tree.branch(old_branch_id); + + treehouse.diagnostics.push( + Diagnostic::warning() + .with_code("sema") + .with_message(format!("two branches share the same id `{}`", named_id)) + .with_labels(vec![ + Label { + style: LabelStyle::Primary, + file_id, + range: new_branch.kind_span.clone(), + message: String::new(), + }, + Label { + style: LabelStyle::Primary, + file_id: old_branch.file_id, + range: old_branch.kind_span.clone(), + message: String::new(), + }, + ]), + ) + } + + new_branch_id + } + + fn parse_attributes(treehouse: &mut Treehouse, file_id: FileId, branch: &Branch) -> Attributes { + let source = treehouse.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 { + 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()); + + // Check that every block has an ID. + 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.clone(), + 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.filename(file_id)), + ], + }); + } + + // Check that link-type blocks are `+`-type to facilitate lazy loading. + if let Content::Link(_) = &attributes.content { + if branch.kind == BranchKind::Expanded { + treehouse.diagnostics.push(Diagnostic { + severity: Severity::Warning, + code: Some("attr".into()), + message: "`content.link` branch is expanded by default".into(), + labels: vec![Label { + style: LabelStyle::Primary, + file_id, + range: branch.kind_span.clone(), + message: String::new(), + }], + notes: vec![ + "note: `content.link` branches should normally be collapsed to allow for lazy loading".into(), + ], + }); + } + } + } + attributes + } +}