dynamic loading
This commit is contained in:
parent
b28a4f5b9a
commit
aa91046bc5
|
@ -166,4 +166,4 @@
|
||||||
- breaking
|
- breaking
|
||||||
|
|
||||||
% content.link = "secret"
|
% content.link = "secret"
|
||||||
- this block includes another block's content
|
+ this block includes another block's content
|
||||||
|
|
|
@ -18,3 +18,21 @@ impl<'a> Display for EscapeAttribute<'a> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct EscapeHtml<'a>(&'a str);
|
||||||
|
|
||||||
|
impl<'a> Display for EscapeHtml<'a> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
for c in self.0.chars() {
|
||||||
|
match c {
|
||||||
|
'<' => f.write_str("<")?,
|
||||||
|
'>' => f.write_str(">")?,
|
||||||
|
'&' => f.write_str("&")?,
|
||||||
|
'\'' => f.write_str("'")?,
|
||||||
|
'"' => f.write_str(""")?,
|
||||||
|
_ => f.write_char(c)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,76 +8,37 @@ use crate::{
|
||||||
state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse},
|
state::{toml_error_to_diagnostic, FileId, TomlError, Treehouse},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{attributes::Attributes, markdown};
|
use super::{
|
||||||
|
attributes::{Attributes, Content},
|
||||||
|
markdown, EscapeHtml,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId, branch: &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 attributes = parse_attributes(treehouse, file_id, branch);
|
||||||
|
|
||||||
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());
|
|
||||||
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.)
|
// Reborrow because the closure requires unique access (it adds a new diagnostic.)
|
||||||
let source = treehouse.get_source(file_id);
|
let source = treehouse.get_source(file_id);
|
||||||
|
|
||||||
let class = if !branch.children.is_empty() {
|
let has_children =
|
||||||
"branch"
|
!branch.children.is_empty() || matches!(attributes.content, Content::Link(_));
|
||||||
|
|
||||||
|
let class = if has_children { "branch" } else { "leaf" };
|
||||||
|
let linked_branch = if let Content::Link(link) = &attributes.content {
|
||||||
|
format!(
|
||||||
|
" is=\"th-linked-branch\" data-th-link=\"{}\"",
|
||||||
|
EscapeHtml(link)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
"leaf"
|
String::new()
|
||||||
};
|
};
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"<li class=\"{class}\" id=\"{}\">",
|
"<li class=\"{class}\" id=\"{}\"{linked_branch}>",
|
||||||
EscapeAttribute(&attributes.id)
|
EscapeAttribute(&attributes.id)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
{
|
{
|
||||||
if !branch.children.is_empty() {
|
if has_children {
|
||||||
s.push_str(match branch.kind {
|
s.push_str(match branch.kind {
|
||||||
BranchKind::Expanded => "<details open>",
|
BranchKind::Expanded => "<details open>",
|
||||||
BranchKind::Collapsed => "<details>",
|
BranchKind::Collapsed => "<details>",
|
||||||
|
@ -110,18 +71,37 @@ pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId
|
||||||
});
|
});
|
||||||
markdown::push_html(s, markdown_parser);
|
markdown::push_html(s, markdown_parser);
|
||||||
|
|
||||||
s.push_str("<th-bb>");
|
if let Content::Link(link) = &attributes.content {
|
||||||
{
|
|
||||||
write!(
|
write!(
|
||||||
s,
|
s,
|
||||||
"<a class=\"branch-link\" href=\"#{}\" title=\"permalink\"></a>",
|
"<noscript><a class=\"navigate icon-go\" href=\"{}.html\">Go to linked tree: <code>{}</code></a></noscript>",
|
||||||
|
EscapeAttribute(link),
|
||||||
|
EscapeHtml(link),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
s.push_str("<th-bb>");
|
||||||
|
{
|
||||||
|
if let Content::Link(link) = &attributes.content {
|
||||||
|
write!(
|
||||||
|
s,
|
||||||
|
"<a class=\"icon icon-go\" href=\"{}.html\" title=\"linked tree\"></a>",
|
||||||
|
EscapeAttribute(link),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(
|
||||||
|
s,
|
||||||
|
"<a class=\"icon icon-permalink\" href=\"#{}\" title=\"permalink\"></a>",
|
||||||
EscapeAttribute(&attributes.id)
|
EscapeAttribute(&attributes.id)
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
s.push_str("</th-bb>");
|
s.push_str("</th-bb>");
|
||||||
|
|
||||||
if !branch.children.is_empty() {
|
if has_children {
|
||||||
s.push_str("</summary>");
|
s.push_str("</summary>");
|
||||||
branches_to_html(s, treehouse, file_id, &branch.children);
|
branches_to_html(s, treehouse, file_id, &branch.children);
|
||||||
s.push_str("</details>");
|
s.push_str("</details>");
|
||||||
|
@ -132,6 +112,82 @@ pub fn branch_to_html(s: &mut String, treehouse: &mut Treehouse, file_id: FileId
|
||||||
s.push_str("</li>");
|
s.push_str("</li>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_attributes(treehouse: &mut Treehouse, file_id: usize, branch: &Branch) -> Attributes {
|
||||||
|
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 {
|
||||||
|
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.get_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(
|
pub fn branches_to_html(
|
||||||
s: &mut String,
|
s: &mut String,
|
||||||
treehouse: &mut Treehouse,
|
treehouse: &mut Treehouse,
|
||||||
|
|
|
@ -97,8 +97,7 @@
|
||||||
opacity: 100%;
|
opacity: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree .branch-link {
|
.tree .icon {
|
||||||
background-image: url("../svg/link.svg");
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 50% 50%;
|
background-position: 50% 50%;
|
||||||
opacity: 35%;
|
opacity: 35%;
|
||||||
|
@ -106,3 +105,24 @@
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tree .icon-permalink {
|
||||||
|
background-image: url("../svg/permalink.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree .icon-go {
|
||||||
|
background-image: url("../svg/go.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree a.navigate {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 50%;
|
||||||
|
opacity: 50%;
|
||||||
|
color: #000;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree .link-loading {
|
||||||
|
padding-left: 24px;
|
||||||
|
opacity: 50%;
|
||||||
|
}
|
||||||
|
|
60
static/js/tree.js
Normal file
60
static/js/tree.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
class LinkedBranch extends HTMLLIElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.linkedTree = this.getAttribute("data-th-link");
|
||||||
|
|
||||||
|
this.details = this.childNodes[0];
|
||||||
|
this.innerUL = this.details.childNodes[1];
|
||||||
|
|
||||||
|
this.state = "notloaded";
|
||||||
|
|
||||||
|
this.loadingText = document.createElement("p");
|
||||||
|
{
|
||||||
|
this.loadingText.className = "link-loading";
|
||||||
|
let linkedTreeName = document.createElement("code");
|
||||||
|
linkedTreeName.innerText = this.linkedTree;
|
||||||
|
this.loadingText.append("Loading ", linkedTreeName, "...");
|
||||||
|
}
|
||||||
|
this.innerUL.appendChild(this.loadingText);
|
||||||
|
|
||||||
|
// This produces a warning during static generation but we still want to handle that
|
||||||
|
// correctly. Having an expanded-by-default linked block can be useful in development.
|
||||||
|
if (this.details.open) {
|
||||||
|
this.loadTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.details.addEventListener("toggle", event => {
|
||||||
|
if (this.details.open) {
|
||||||
|
this.loadTree();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTree() {
|
||||||
|
if (this.state == "notloaded") {
|
||||||
|
this.state = "loading";
|
||||||
|
|
||||||
|
fetch(`/${this.linkedTree}.html`)
|
||||||
|
.then(request => request.text())
|
||||||
|
.then(text => {
|
||||||
|
let parser = new DOMParser();
|
||||||
|
let linkedDocument = parser.parseFromString(text, "text/html");
|
||||||
|
let main = linkedDocument.getElementsByTagName("main")[0];
|
||||||
|
let ul /*: Element */ = main.getElementsByTagName("ul")[0];
|
||||||
|
console.log(ul);
|
||||||
|
|
||||||
|
this.loadingText.remove();
|
||||||
|
|
||||||
|
for (let i = 0; i < ul.childNodes.length; ++i) {
|
||||||
|
this.innerUL.appendChild(ul.childNodes[i]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.loadingText.innerText = error.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("th-linked-branch", LinkedBranch, { extends: "li" });
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
@ -11,6 +11,7 @@
|
||||||
<link rel="stylesheet" href="{{ site }}/static/css/tree.css">
|
<link rel="stylesheet" href="{{ site }}/static/css/tree.css">
|
||||||
<link rel="stylesheet" href="{{ site }}/static/font/font.css">
|
<link rel="stylesheet" href="{{ site }}/static/font/font.css">
|
||||||
|
|
||||||
|
<script type="module" src="{{ site }}/static/js/tree.js"></script>
|
||||||
<script type="module" src="{{ site }}/static/js/usability.js"></script>
|
<script type="module" src="{{ site }}/static/js/usability.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue