making it look better
This commit is contained in:
parent
ad84a79335
commit
30255be018
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
1849
Cargo.lock
generated
1849
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -4,6 +4,4 @@ resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
||||||
log = "0.4.20"
|
|
||||||
|
|
||||||
treehouse-format = { path = "crates/treehouse-format" }
|
treehouse-format = { path = "crates/treehouse-format" }
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
- treehouse is a brand new static website generator, inspired by the likes of Jekyll and Hugo, but offering a writing experience more close to Logseq
|
- treehouse is a brand new static website generator, inspired by the likes of Jekyll and Hugo, but offering a writing experience more close to Logseq
|
||||||
|
|
||||||
- ie. a public braindump
|
- ie. a public braindump adsadasdsad
|
||||||
|
|
||||||
- since you're here, you're probably just setting up
|
- since you're here, you're probably just setting up
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
- this special file is almost like your index.html
|
- this special file is almost like your index.html
|
||||||
|
|
||||||
+ the syntax is pretty simple
|
+ the .tree syntax is pretty simple
|
||||||
|
|
||||||
- separate blocks are delimited with a blank line
|
- separate blocks are delimited with a blank line
|
||||||
|
|
||||||
|
@ -28,10 +28,35 @@
|
||||||
|
|
||||||
- a plus `+` means that the block is hidden by default
|
- a plus `+` means that the block is hidden by default
|
||||||
|
|
||||||
- before the block content, there can be an arbitrary amount of TOML specifying the block config
|
- before the block content, there can be an arbitrary amount of TOML pecifying the block attributes
|
||||||
|
|
||||||
- many keys are available but they aren't really documented outside of code
|
- many keys are available but they aren't really documented outside of code
|
||||||
|
|
||||||
- blocks can span multiple lines as long as they are not broken apart with a blank line
|
- blocks can span multiple lines as long as they are not broken apart with a blank line
|
||||||
|
|
||||||
- that means each block can contain at most one paragraph, unless you use dirty HTML hacks (cheater!)
|
- that means each block can contain at most one paragraph, unless you use dirty HTML hacks (cheater!)
|
||||||
|
|
||||||
|
- .tree composes together with Markdown to let you format text however you want
|
||||||
|
|
||||||
|
- here's a bunch of stuff formatted
|
||||||
|
|
||||||
|
- # heading 1
|
||||||
|
|
||||||
|
- ## heading 2
|
||||||
|
|
||||||
|
- ### heading 3
|
||||||
|
headings lower than this aren't really supported because c'mon who would be this crazy
|
||||||
|
|
||||||
|
- <https://liquidev.net>
|
||||||
|
|
||||||
|
- here is my favorite fluffy boy ![ralsei with a hat](https://liquidev.net/syf/art/20230723_ralsei_hat.png)
|
||||||
|
|
||||||
|
- also a block quote
|
||||||
|
|
||||||
|
- > Enough You Foolish Children
|
||||||
|
|
||||||
|
- yes i will totally abuse you with deltarune references and you cannot stop me
|
||||||
|
|
||||||
|
- ```
|
||||||
|
this is some block of code it looks pretty cool doesn't it
|
||||||
|
```
|
||||||
|
|
|
@ -5,4 +5,3 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0.47"
|
thiserror = "1.0.47"
|
||||||
log = { workspace = true }
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ impl Roots {
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Branch {
|
pub struct Branch {
|
||||||
|
pub indent_level: usize,
|
||||||
pub attributes: Range<usize>,
|
pub attributes: Range<usize>,
|
||||||
pub kind: BranchKind,
|
pub kind: BranchKind,
|
||||||
pub kind_span: Range<usize>,
|
pub kind_span: Range<usize>,
|
||||||
|
@ -35,6 +36,7 @@ pub struct Branch {
|
||||||
impl From<BranchEvent> for Branch {
|
impl From<BranchEvent> for Branch {
|
||||||
fn from(branch: BranchEvent) -> Self {
|
fn from(branch: BranchEvent) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
indent_level: branch.indent_level,
|
||||||
attributes: branch.attributes,
|
attributes: branch.attributes,
|
||||||
kind: branch.kind,
|
kind: branch.kind,
|
||||||
kind_span: branch.kind_span,
|
kind_span: branch.kind_span,
|
||||||
|
|
|
@ -4,9 +4,17 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.75"
|
||||||
|
axum = "0.6.20"
|
||||||
codespan-reporting = "0.11.1"
|
codespan-reporting = "0.11.1"
|
||||||
|
copy_dir = "0.1.3"
|
||||||
handlebars = "4.3.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"] }
|
||||||
thiserror = "1.0.47"
|
thiserror = "1.0.47"
|
||||||
|
tokio = "1.32.0"
|
||||||
|
tower-http = { version = "0.4.3", features = ["fs"] }
|
||||||
|
tower-livereload = "0.8.0"
|
||||||
treehouse-format = { workspace = true }
|
treehouse-format = { workspace = true }
|
||||||
|
watchexec = "2.3.0"
|
||||||
|
|
||||||
|
|
439
crates/treehouse/src/html/markdown.rs
Normal file
439
crates/treehouse/src/html/markdown.rs
Normal file
|
@ -0,0 +1,439 @@
|
||||||
|
// NOTE: This code is pasted pretty much verbatim from pulldown-cmark but tweaked to have my own
|
||||||
|
// cool additions.
|
||||||
|
|
||||||
|
// Copyright 2015 Google Inc. All rights reserved.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
//! HTML renderer that takes an iterator of events as input.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use pulldown_cmark::escape::{escape_href, escape_html, StrWrite};
|
||||||
|
use pulldown_cmark::{Alignment, CodeBlockKind, Event, LinkType, Tag};
|
||||||
|
use pulldown_cmark::{CowStr, Event::*};
|
||||||
|
|
||||||
|
enum TableState {
|
||||||
|
Head,
|
||||||
|
Body,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HtmlWriter<'a, I, W> {
|
||||||
|
/// Iterator supplying events.
|
||||||
|
iter: I,
|
||||||
|
|
||||||
|
/// Writer to write to.
|
||||||
|
writer: W,
|
||||||
|
|
||||||
|
/// Whether or not the last write wrote a newline.
|
||||||
|
end_newline: bool,
|
||||||
|
|
||||||
|
table_state: TableState,
|
||||||
|
table_alignments: Vec<Alignment>,
|
||||||
|
table_cell_index: usize,
|
||||||
|
numbers: HashMap<CowStr<'a>, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, I, W> HtmlWriter<'a, I, W>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = Event<'a>>,
|
||||||
|
W: StrWrite,
|
||||||
|
{
|
||||||
|
fn new(iter: I, writer: W) -> Self {
|
||||||
|
Self {
|
||||||
|
iter,
|
||||||
|
writer,
|
||||||
|
end_newline: true,
|
||||||
|
table_state: TableState::Head,
|
||||||
|
table_alignments: vec![],
|
||||||
|
table_cell_index: 0,
|
||||||
|
numbers: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a new line.
|
||||||
|
fn write_newline(&mut self) -> io::Result<()> {
|
||||||
|
self.end_newline = true;
|
||||||
|
self.writer.write_str("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a buffer, and tracks whether or not a newline was written.
|
||||||
|
#[inline]
|
||||||
|
fn write(&mut self, s: &str) -> io::Result<()> {
|
||||||
|
self.writer.write_str(s)?;
|
||||||
|
|
||||||
|
if !s.is_empty() {
|
||||||
|
self.end_newline = s.ends_with('\n');
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(mut self) -> io::Result<()> {
|
||||||
|
while let Some(event) = self.iter.next() {
|
||||||
|
match event {
|
||||||
|
Start(tag) => {
|
||||||
|
self.start_tag(tag)?;
|
||||||
|
}
|
||||||
|
End(tag) => {
|
||||||
|
self.end_tag(tag)?;
|
||||||
|
}
|
||||||
|
Text(text) => {
|
||||||
|
escape_html(&mut self.writer, &text)?;
|
||||||
|
self.end_newline = text.ends_with('\n');
|
||||||
|
}
|
||||||
|
Code(text) => {
|
||||||
|
self.write("<code>")?;
|
||||||
|
escape_html(&mut self.writer, &text)?;
|
||||||
|
self.write("</code>")?;
|
||||||
|
}
|
||||||
|
Html(html) => {
|
||||||
|
self.write(&html)?;
|
||||||
|
}
|
||||||
|
SoftBreak => {
|
||||||
|
self.write_newline()?;
|
||||||
|
}
|
||||||
|
HardBreak => {
|
||||||
|
self.write("<br />\n")?;
|
||||||
|
}
|
||||||
|
Rule => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.write("<hr />\n")?;
|
||||||
|
} else {
|
||||||
|
self.write("\n<hr />\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FootnoteReference(name) => {
|
||||||
|
let len = self.numbers.len() + 1;
|
||||||
|
self.write("<sup class=\"footnote-reference\"><a href=\"#")?;
|
||||||
|
escape_html(&mut self.writer, &name)?;
|
||||||
|
self.write("\">")?;
|
||||||
|
let number = *self.numbers.entry(name).or_insert(len);
|
||||||
|
write!(&mut self.writer, "{}", number)?;
|
||||||
|
self.write("</a></sup>")?;
|
||||||
|
}
|
||||||
|
TaskListMarker(true) => {
|
||||||
|
self.write("<input disabled=\"\" type=\"checkbox\" checked=\"\"/>\n")?;
|
||||||
|
}
|
||||||
|
TaskListMarker(false) => {
|
||||||
|
self.write("<input disabled=\"\" type=\"checkbox\"/>\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the start of an HTML tag.
|
||||||
|
fn start_tag(&mut self, tag: Tag<'a>) -> io::Result<()> {
|
||||||
|
match tag {
|
||||||
|
Tag::Paragraph => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.write("<p>")
|
||||||
|
} else {
|
||||||
|
self.write("\n<p>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::Heading(level, id, classes) => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.end_newline = false;
|
||||||
|
self.write("<")?;
|
||||||
|
} else {
|
||||||
|
self.write("\n<")?;
|
||||||
|
}
|
||||||
|
write!(&mut self.writer, "{}", level)?;
|
||||||
|
if let Some(id) = id {
|
||||||
|
self.write(" id=\"")?;
|
||||||
|
escape_html(&mut self.writer, id)?;
|
||||||
|
self.write("\"")?;
|
||||||
|
}
|
||||||
|
let mut classes = classes.iter();
|
||||||
|
if let Some(class) = classes.next() {
|
||||||
|
self.write(" class=\"")?;
|
||||||
|
escape_html(&mut self.writer, class)?;
|
||||||
|
for class in classes {
|
||||||
|
self.write(" ")?;
|
||||||
|
escape_html(&mut self.writer, class)?;
|
||||||
|
}
|
||||||
|
self.write("\"")?;
|
||||||
|
}
|
||||||
|
self.write(">")
|
||||||
|
}
|
||||||
|
Tag::Table(alignments) => {
|
||||||
|
self.table_alignments = alignments;
|
||||||
|
self.write("<table>")
|
||||||
|
}
|
||||||
|
Tag::TableHead => {
|
||||||
|
self.table_state = TableState::Head;
|
||||||
|
self.table_cell_index = 0;
|
||||||
|
self.write("<thead><tr>")
|
||||||
|
}
|
||||||
|
Tag::TableRow => {
|
||||||
|
self.table_cell_index = 0;
|
||||||
|
self.write("<tr>")
|
||||||
|
}
|
||||||
|
Tag::TableCell => {
|
||||||
|
match self.table_state {
|
||||||
|
TableState::Head => {
|
||||||
|
self.write("<th")?;
|
||||||
|
}
|
||||||
|
TableState::Body => {
|
||||||
|
self.write("<td")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match self.table_alignments.get(self.table_cell_index) {
|
||||||
|
Some(&Alignment::Left) => self.write(" style=\"text-align: left\">"),
|
||||||
|
Some(&Alignment::Center) => self.write(" style=\"text-align: center\">"),
|
||||||
|
Some(&Alignment::Right) => self.write(" style=\"text-align: right\">"),
|
||||||
|
_ => self.write(">"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::BlockQuote => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.write("<blockquote>\n")
|
||||||
|
} else {
|
||||||
|
self.write("\n<blockquote>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::CodeBlock(info) => {
|
||||||
|
if !self.end_newline {
|
||||||
|
self.write_newline()?;
|
||||||
|
}
|
||||||
|
match info {
|
||||||
|
CodeBlockKind::Fenced(info) => {
|
||||||
|
let lang = info.split(' ').next().unwrap();
|
||||||
|
if lang.is_empty() {
|
||||||
|
self.write("<pre><code>")
|
||||||
|
} else {
|
||||||
|
self.write("<pre><code class=\"language-")?;
|
||||||
|
escape_html(&mut self.writer, lang)?;
|
||||||
|
self.write("\">")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CodeBlockKind::Indented => self.write("<pre><code>"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::List(Some(1)) => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.write("<ol>\n")
|
||||||
|
} else {
|
||||||
|
self.write("\n<ol>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::List(Some(start)) => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.write("<ol start=\"")?;
|
||||||
|
} else {
|
||||||
|
self.write("\n<ol start=\"")?;
|
||||||
|
}
|
||||||
|
write!(&mut self.writer, "{}", start)?;
|
||||||
|
self.write("\">\n")
|
||||||
|
}
|
||||||
|
Tag::List(None) => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.write("<ul>\n")
|
||||||
|
} else {
|
||||||
|
self.write("\n<ul>\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::Item => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.write("<li>")
|
||||||
|
} else {
|
||||||
|
self.write("\n<li>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::Emphasis => self.write("<em>"),
|
||||||
|
Tag::Strong => self.write("<strong>"),
|
||||||
|
Tag::Strikethrough => self.write("<del>"),
|
||||||
|
Tag::Link(LinkType::Email, dest, title) => {
|
||||||
|
self.write("<a href=\"mailto:")?;
|
||||||
|
escape_href(&mut self.writer, &dest)?;
|
||||||
|
if !title.is_empty() {
|
||||||
|
self.write("\" title=\"")?;
|
||||||
|
escape_html(&mut self.writer, &title)?;
|
||||||
|
}
|
||||||
|
self.write("\">")
|
||||||
|
}
|
||||||
|
Tag::Link(_link_type, dest, title) => {
|
||||||
|
self.write("<a href=\"")?;
|
||||||
|
escape_href(&mut self.writer, &dest)?;
|
||||||
|
if !title.is_empty() {
|
||||||
|
self.write("\" title=\"")?;
|
||||||
|
escape_html(&mut self.writer, &title)?;
|
||||||
|
}
|
||||||
|
self.write("\">")
|
||||||
|
}
|
||||||
|
Tag::Image(_link_type, dest, title) => {
|
||||||
|
self.write("<img src=\"")?;
|
||||||
|
escape_href(&mut self.writer, &dest)?;
|
||||||
|
self.write("\" alt=\"")?;
|
||||||
|
self.raw_text()?;
|
||||||
|
if !title.is_empty() {
|
||||||
|
self.write("\" title=\"")?;
|
||||||
|
escape_html(&mut self.writer, &title)?;
|
||||||
|
}
|
||||||
|
self.write("\" />")
|
||||||
|
}
|
||||||
|
Tag::FootnoteDefinition(name) => {
|
||||||
|
if self.end_newline {
|
||||||
|
self.write("<div class=\"footnote-definition\" id=\"")?;
|
||||||
|
} else {
|
||||||
|
self.write("\n<div class=\"footnote-definition\" id=\"")?;
|
||||||
|
}
|
||||||
|
escape_html(&mut self.writer, &name)?;
|
||||||
|
self.write("\"><sup class=\"footnote-definition-label\">")?;
|
||||||
|
let len = self.numbers.len() + 1;
|
||||||
|
let number = *self.numbers.entry(name).or_insert(len);
|
||||||
|
write!(&mut self.writer, "{}", number)?;
|
||||||
|
self.write("</sup>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_tag(&mut self, tag: Tag) -> io::Result<()> {
|
||||||
|
match tag {
|
||||||
|
Tag::Paragraph => {
|
||||||
|
self.write("</p>\n")?;
|
||||||
|
}
|
||||||
|
Tag::Heading(level, _id, _classes) => {
|
||||||
|
self.write("</")?;
|
||||||
|
write!(&mut self.writer, "{}", level)?;
|
||||||
|
self.write(">\n")?;
|
||||||
|
}
|
||||||
|
Tag::Table(_) => {
|
||||||
|
self.write("</tbody></table>\n")?;
|
||||||
|
}
|
||||||
|
Tag::TableHead => {
|
||||||
|
self.write("</tr></thead><tbody>\n")?;
|
||||||
|
self.table_state = TableState::Body;
|
||||||
|
}
|
||||||
|
Tag::TableRow => {
|
||||||
|
self.write("</tr>\n")?;
|
||||||
|
}
|
||||||
|
Tag::TableCell => {
|
||||||
|
match self.table_state {
|
||||||
|
TableState::Head => {
|
||||||
|
self.write("</th>")?;
|
||||||
|
}
|
||||||
|
TableState::Body => {
|
||||||
|
self.write("</td>")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.table_cell_index += 1;
|
||||||
|
}
|
||||||
|
Tag::BlockQuote => {
|
||||||
|
self.write("</blockquote>\n")?;
|
||||||
|
}
|
||||||
|
Tag::CodeBlock(_) => {
|
||||||
|
self.write("</code></pre>\n")?;
|
||||||
|
}
|
||||||
|
Tag::List(Some(_)) => {
|
||||||
|
self.write("</ol>\n")?;
|
||||||
|
}
|
||||||
|
Tag::List(None) => {
|
||||||
|
self.write("</ul>\n")?;
|
||||||
|
}
|
||||||
|
Tag::Item => {
|
||||||
|
self.write("</li>\n")?;
|
||||||
|
}
|
||||||
|
Tag::Emphasis => {
|
||||||
|
self.write("</em>")?;
|
||||||
|
}
|
||||||
|
Tag::Strong => {
|
||||||
|
self.write("</strong>")?;
|
||||||
|
}
|
||||||
|
Tag::Strikethrough => {
|
||||||
|
self.write("</del>")?;
|
||||||
|
}
|
||||||
|
Tag::Link(_, _, _) => {
|
||||||
|
self.write("</a>")?;
|
||||||
|
}
|
||||||
|
Tag::Image(_, _, _) => (), // shouldn't happen, handled in start
|
||||||
|
Tag::FootnoteDefinition(_) => {
|
||||||
|
self.write("</div>\n")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// run raw text, consuming end tag
|
||||||
|
fn raw_text(&mut self) -> io::Result<()> {
|
||||||
|
let mut nest = 0;
|
||||||
|
while let Some(event) = self.iter.next() {
|
||||||
|
match event {
|
||||||
|
Start(_) => nest += 1,
|
||||||
|
End(_) => {
|
||||||
|
if nest == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nest -= 1;
|
||||||
|
}
|
||||||
|
Html(text) | Code(text) | Text(text) => {
|
||||||
|
escape_html(&mut self.writer, &text)?;
|
||||||
|
self.end_newline = text.ends_with('\n');
|
||||||
|
}
|
||||||
|
SoftBreak | HardBreak | Rule => {
|
||||||
|
self.write(" ")?;
|
||||||
|
}
|
||||||
|
FootnoteReference(name) => {
|
||||||
|
let len = self.numbers.len() + 1;
|
||||||
|
let number = *self.numbers.entry(name).or_insert(len);
|
||||||
|
write!(&mut self.writer, "[{}]", number)?;
|
||||||
|
}
|
||||||
|
TaskListMarker(true) => self.write("[x]")?,
|
||||||
|
TaskListMarker(false) => self.write("[ ]")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
|
||||||
|
/// push it to a `String`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use pulldown_cmark::{html, Parser};
|
||||||
|
///
|
||||||
|
/// let markdown_str = r#"
|
||||||
|
/// hello
|
||||||
|
/// =====
|
||||||
|
///
|
||||||
|
/// * alpha
|
||||||
|
/// * beta
|
||||||
|
/// "#;
|
||||||
|
/// let parser = Parser::new(markdown_str);
|
||||||
|
///
|
||||||
|
/// let mut html_buf = String::new();
|
||||||
|
/// html::push_html(&mut html_buf, parser);
|
||||||
|
///
|
||||||
|
/// assert_eq!(html_buf, r#"<h1>hello</h1>
|
||||||
|
/// <ul>
|
||||||
|
/// <li>alpha</li>
|
||||||
|
/// <li>beta</li>
|
||||||
|
/// </ul>
|
||||||
|
/// "#);
|
||||||
|
/// ```
|
||||||
|
pub fn push_html<'a, I>(s: &mut String, iter: I)
|
||||||
|
where
|
||||||
|
I: Iterator<Item = Event<'a>>,
|
||||||
|
{
|
||||||
|
HtmlWriter::new(iter, s).run().unwrap();
|
||||||
|
}
|
2
crates/treehouse/src/html/mod.rs
Normal file
2
crates/treehouse/src/html/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mod markdown;
|
||||||
|
pub mod tree;
|
|
@ -1,5 +1,7 @@
|
||||||
use treehouse_format::{ast::Branch, pull::BranchKind};
|
use treehouse_format::{ast::Branch, pull::BranchKind};
|
||||||
|
|
||||||
|
use super::markdown;
|
||||||
|
|
||||||
pub fn branch_to_html(s: &mut String, branch: &Branch, source: &str) {
|
pub fn branch_to_html(s: &mut String, branch: &Branch, source: &str) {
|
||||||
s.push_str("<li>");
|
s.push_str("<li>");
|
||||||
{
|
{
|
||||||
|
@ -10,7 +12,18 @@ pub fn branch_to_html(s: &mut String, branch: &Branch, source: &str) {
|
||||||
});
|
});
|
||||||
s.push_str("<summary>");
|
s.push_str("<summary>");
|
||||||
}
|
}
|
||||||
s.push_str(&source[branch.content.clone()]);
|
|
||||||
|
let raw_block_content = &source[branch.content.clone()];
|
||||||
|
let mut unindented_block_content = String::with_capacity(raw_block_content.len());
|
||||||
|
let indent = " ".repeat(branch.indent_level);
|
||||||
|
for line in raw_block_content.lines() {
|
||||||
|
unindented_block_content.push_str(line.strip_prefix(&indent).unwrap_or(line));
|
||||||
|
unindented_block_content.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
let markdown_parser = pulldown_cmark::Parser::new(&unindented_block_content);
|
||||||
|
markdown::push_html(s, markdown_parser);
|
||||||
|
|
||||||
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, &branch.children, source);
|
|
@ -1,46 +1,33 @@
|
||||||
mod tree_html;
|
mod html;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
use codespan_reporting::{
|
use codespan_reporting::{
|
||||||
diagnostic::{Diagnostic, Label, LabelStyle, Severity},
|
diagnostic::{Diagnostic, Label, LabelStyle, Severity},
|
||||||
files::SimpleFile,
|
files::SimpleFile,
|
||||||
term::termcolor::{ColorChoice, StandardStream},
|
term::termcolor::{ColorChoice, StandardStream},
|
||||||
};
|
};
|
||||||
use tree_html::branches_to_html;
|
use copy_dir::copy_dir;
|
||||||
use treehouse_format::{
|
use handlebars::Handlebars;
|
||||||
ast::{Branch, Roots},
|
use html::tree::branches_to_html;
|
||||||
pull::Parser,
|
use serde::Serialize;
|
||||||
};
|
use tower_http::services::ServeDir;
|
||||||
|
use tower_livereload::LiveReloadLayer;
|
||||||
|
use treehouse_format::{ast::Roots, pull::Parser};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Serialize)]
|
||||||
enum Error {
|
pub struct TemplateData {
|
||||||
#[error("I/O error: {0}")]
|
pub tree: String,
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("treehouse parsing error: {0}")]
|
|
||||||
Parse(#[from] treehouse_format::ParseError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_branch(branch: &Branch, source: &str) {
|
fn regenerate() -> anyhow::Result<()> {
|
||||||
fn inner(branch: &Branch, source: &str, indent_level: usize) {
|
|
||||||
for _ in 0..indent_level {
|
|
||||||
print!(" ");
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"{} {:?}",
|
|
||||||
branch.kind.char(),
|
|
||||||
&source[branch.content.clone()]
|
|
||||||
);
|
|
||||||
for child in &branch.children {
|
|
||||||
inner(child, source, indent_level + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inner(branch, source, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let _ = std::fs::remove_dir_all("target/site");
|
let _ = std::fs::remove_dir_all("target/site");
|
||||||
std::fs::create_dir_all("target/site")?;
|
std::fs::create_dir_all("target/site")?;
|
||||||
|
|
||||||
|
copy_dir("static", "target/site/static")?;
|
||||||
|
|
||||||
|
let mut handlebars = Handlebars::new();
|
||||||
|
handlebars.register_template_file("template/index.hbs", "template/index.hbs")?;
|
||||||
|
|
||||||
let root_file = std::fs::read_to_string("content/tree/root.tree")?;
|
let root_file = std::fs::read_to_string("content/tree/root.tree")?;
|
||||||
let parse_result = Roots::parse(&mut Parser {
|
let parse_result = Roots::parse(&mut Parser {
|
||||||
input: &root_file,
|
input: &root_file,
|
||||||
|
@ -49,13 +36,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
match parse_result {
|
match parse_result {
|
||||||
Ok(roots) => {
|
Ok(roots) => {
|
||||||
let mut html = String::from("<!DOCTYPE html><html><head></head><body>");
|
let mut tree = String::new();
|
||||||
for root in &roots.branches {
|
branches_to_html(&mut tree, &roots.branches, &root_file);
|
||||||
print_branch(root, &root_file);
|
|
||||||
}
|
let index_html = handlebars.render("template/index.hbs", &TemplateData { tree })?;
|
||||||
branches_to_html(&mut html, &roots.branches, &root_file);
|
|
||||||
std::fs::write("target/site/index.html", &html)?;
|
std::fs::write("target/site/index.html", index_html)?;
|
||||||
html.push_str("</body></html>")
|
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let writer = StandardStream::stderr(ColorChoice::Auto);
|
let writer = StandardStream::stderr(ColorChoice::Auto);
|
||||||
|
@ -77,29 +63,43 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// let mut parser = treehouse_format::Parser {
|
Ok(())
|
||||||
// input: &root_file,
|
}
|
||||||
// position: 0,
|
|
||||||
// };
|
fn regenerate_or_report_error() {
|
||||||
// let mut generator = HtmlGenerator::default();
|
eprintln!("regenerating");
|
||||||
// while let Some(branch) = parser.next_branch()? {
|
|
||||||
// for _ in 0..branch.indent_level {
|
match regenerate() {
|
||||||
// print!(" ");
|
Ok(_) => (),
|
||||||
// }
|
Err(error) => eprintln!("error: {error:?}"),
|
||||||
// println!(
|
}
|
||||||
// "{} {:?}",
|
}
|
||||||
// branch.kind.char(),
|
|
||||||
// &root_file[branch.content.clone()]
|
async fn web_server() -> anyhow::Result<()> {
|
||||||
// );
|
let app = Router::new().nest_service("/", ServeDir::new("target/site"));
|
||||||
// generator.add(&root_file, &branch);
|
|
||||||
// }
|
#[cfg(debug_assertions)]
|
||||||
// std::fs::write(
|
let app = app.layer(LiveReloadLayer::new());
|
||||||
// "target/site/index.html",
|
|
||||||
// format!(
|
Ok(axum::Server::bind(&([0, 0, 0, 0], 8080).into())
|
||||||
// "<!DOCTYPE html><html><head></head><body>{}</body></html>",
|
.serve(app.into_make_service())
|
||||||
// generator.finish()
|
.await?)
|
||||||
// ),
|
}
|
||||||
// )?;
|
|
||||||
|
async fn fallible_main() -> anyhow::Result<()> {
|
||||||
|
regenerate_or_report_error();
|
||||||
|
|
||||||
|
web_server().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
match fallible_main().await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(error) => eprintln!("fatal: {error:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
75
static/css/main.css
Normal file
75
static/css/main.css
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/* Choose more pretty colors than vanilla HTML */
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: rgb(255, 253, 246);
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set up fonts */
|
||||||
|
|
||||||
|
body,
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
font-family: 'RecVar', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--recursive-mono: 0.0;
|
||||||
|
--recursive-casl: 1.0;
|
||||||
|
--recursive-wght: 400;
|
||||||
|
--recursive-slnt: -2.0;
|
||||||
|
--recursive-crsv: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after {
|
||||||
|
font-variation-settings:
|
||||||
|
"MONO" var(--recursive-mono),
|
||||||
|
"CASL" var(--recursive-casl),
|
||||||
|
"wght" var(--recursive-wght),
|
||||||
|
"slnt" var(--recursive-slnt),
|
||||||
|
"CRSV" var(--recursive-crsv);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
--recursive-slnt: 0.0;
|
||||||
|
--recursive-casl: 0.0;
|
||||||
|
--recursive-crsv: 0.0;
|
||||||
|
--recursive-wght: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
--recursive-mono: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the tree have + and - instead of the default details/summary arrow */
|
||||||
|
|
||||||
|
.tree details>summary {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree details::before {
|
||||||
|
content: '+';
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
padding-right: 8px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
|
||||||
|
--recursive-mono: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree details[open]::before {
|
||||||
|
content: '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree details *:first-child {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
BIN
static/font/Recursive_VF_1.085--subset_range_english_basic.woff2
Normal file
BIN
static/font/Recursive_VF_1.085--subset_range_english_basic.woff2
Normal file
Binary file not shown.
BIN
static/font/Recursive_VF_1.085--subset_range_latin_1.woff2
Normal file
BIN
static/font/Recursive_VF_1.085--subset_range_latin_1.woff2
Normal file
Binary file not shown.
BIN
static/font/Recursive_VF_1.085--subset_range_latin_1_punc.woff2
Normal file
BIN
static/font/Recursive_VF_1.085--subset_range_latin_1_punc.woff2
Normal file
Binary file not shown.
BIN
static/font/Recursive_VF_1.085--subset_range_latin_ext.woff2
Normal file
BIN
static/font/Recursive_VF_1.085--subset_range_latin_ext.woff2
Normal file
Binary file not shown.
BIN
static/font/Recursive_VF_1.085--subset_range_remaining.woff2
Normal file
BIN
static/font/Recursive_VF_1.085--subset_range_remaining.woff2
Normal file
Binary file not shown.
BIN
static/font/Recursive_VF_1.085--subset_range_vietnamese.woff2
Normal file
BIN
static/font/Recursive_VF_1.085--subset_range_vietnamese.woff2
Normal file
Binary file not shown.
59
static/font/font.css
Normal file
59
static/font/font.css
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/* The bare minimum English subset, plus copyright & arrows (← ↑ → ↓) & quotes (“ ” ‘ ’) & bullet (•) */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'RecVar';
|
||||||
|
font-style: oblique 0deg 15deg;
|
||||||
|
font-weight: 300 1000;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Recursive_VF_1.085--subset_range_english_basic.woff2') format('woff2');
|
||||||
|
unicode-range: U+0020-007F, U+00A9, U+2190-2193, U+2018, U+2019, U+201C, U+201D, U+2022;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* unicode latin-1 letters, basic european diacritics */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'RecVar';
|
||||||
|
font-style: oblique 0deg 15deg;
|
||||||
|
font-weight: 300 1000;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Recursive_VF_1.085--subset_range_latin_1.woff2') format('woff2');
|
||||||
|
unicode-range: U+00C0-00FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* unicode latin-1, punc/symbols & arrows (↔ ↕ ↖ ↗ ↘ ↙) */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'RecVar';
|
||||||
|
font-style: oblique 0deg 15deg;
|
||||||
|
font-weight: 300 1000;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Recursive_VF_1.085--subset_range_latin_1_punc.woff2') format('woff2');
|
||||||
|
unicode-range: U+00A0-00A8, U+00AA-00BF, U+2194-2199;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* unicode latin A extended */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'RecVar';
|
||||||
|
font-style: oblique 0deg 15deg;
|
||||||
|
font-weight: 300 1000;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Recursive_VF_1.085--subset_range_latin_ext.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-017F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* unicodes for vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'RecVar';
|
||||||
|
font-style: oblique 0deg 15deg;
|
||||||
|
font-weight: 300 1000;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Recursive_VF_1.085--subset_range_vietnamese.woff2') format('woff2');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* remaining Unicodes */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'RecVar';
|
||||||
|
font-style: oblique 0deg 15deg;
|
||||||
|
font-weight: 300 1000;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('./Recursive_VF_1.085--subset_range_remaining.woff2') format('woff2');
|
||||||
|
unicode-range: U+2007, U+2008, U+2009, U+200A, U+200B, U+D, U+2010, U+2012, U+2013, U+2014, U+2015, U+201A, U+201E, U+2020, U+2021, U+2026, U+2030, U+2032, U+2033, U+2039, U+203A, U+203E, U+2044, U+2052, U+2070, U+2074, U+2075, U+2076, U+2077, U+2078, U+2079, U+207B, U+2080, U+2081, U+2082, U+2083, U+2084, U+2085, U+2086, U+2087, U+2088, U+2089, U+20A1, U+20A6, U+20A8, U+20A9, U+20AA, U+20AC, U+20AD, U+20B1, U+20B2, U+20B4, U+20B5, U+20B8, U+20B9, U+20BA, U+20BC, U+20BD, U+20BF, U+F8FF, U+2113, U+2116, U+2122, U+2126, U+212E, U+E132, U+E133, U+2153, U+2154, U+215B, U+215C, U+215D, U+215E, U+18F, U+192, U+19D, U+1C4, U+1C5, U+1C6, U+1C7, U+1C8, U+1C9, U+1CA, U+1CB, U+1CC, U+1E6, U+1E7, U+1EA, U+1EB, U+1F1, U+1F2, U+1F3, U+1FA, U+1FB, U+1FC, U+1FD, U+1FE, U+1FF, U+200, U+201, U+202, U+203, U+204, U+205, U+206, U+207, U+208, U+209, U+20A, U+20B, U+20C, U+20D, U+20E, U+20F, U+210, U+211, U+212, U+213, U+214, U+215, U+216, U+217, U+218, U+219, U+21A, U+21B, U+2215, U+2219, U+221E, U+221A, U+22A, U+22B, U+22C, U+22D, U+222B, U+230, U+231, U+232, U+233, U+2236, U+237, U+2248, U+259, U+2260, U+2261, U+2264, U+2265, U+272, U+2B9, U+2BA, U+2BB, U+2BC, U+2BE, U+2BF, U+2C6, U+2C7, U+2C8, U+2C9, U+2CA, U+2CB, U+2D8, U+2D9, U+2DA, U+2DB, U+2DC, U+2DD, U+300, U+301, U+FB02, U+FB03, U+302, U+303, U+304, U+FB01, U+306, U+307, U+308, U+309, U+30A, U+30B, U+30C, U+30F, U+311, U+312, U+315, U+31B, U+2202, U+323, U+324, U+325, U+326, U+327, U+328, U+329, U+2205, U+32E, U+2206, U+331, U+335, U+220F, U+2211, U+2212, U+391, U+392, U+393, U+394, U+398, U+39B, U+39C, U+39D, U+3A0, U+3A6, U+3B1, U+3B2, U+3B3, U+3B4, U+3B8, U+3BB, U+3BC, U+3BD, U+3C0, U+3C6, U+25A0, U+25A1, U+25B2, U+25B3, U+25B6, U+25B7, U+25BC, U+25BD, U+25C0, U+25C1, U+25C6, U+25C7, U+25CA, U+1E08, U+1E09, U+1E0C, U+1E0D, U+1E0E, U+1E0F, U+2610, U+2611, U+1E14, U+1E15, U+1E16, U+1E17, U+1E1C, U+1E1D, U+1E20, U+1E21, U+1E24, U+1E25, U+1E2A, U+1E2B, U+1E2E, U+1E2F, U+1E36, U+1E37, U+1E3A, U+1E3B, U+E3F, U+1E42, U+1E43, U+1E44, U+1E45, U+1E46, U+1E47, U+1E48, U+1E49, U+1E4C, U+1E4D, U+1E4E, U+1E4F, U+1E50, U+1E51, U+1E52, U+1E53, U+1E5A, U+1E5B, U+1E5E, U+1E5F, U+1E60, U+2661, U+1E61, U+1E62, U+1E63, U+1E64, U+1E65, U+1E66, U+1E67, U+1E68, U+1E69, U+2665, U+1E6C, U+1E6D, U+1E6E, U+1E6F, U+1E78, U+1E79, U+1E7A, U+1E7B, U+1E80, U+1E81, U+1E82, U+1E83, U+1E84, U+1E85, U+1E8E, U+1E8F, U+1E92, U+1E93, U+1E97, U+1E9E, U+2713, U+27E8, U+27E9;
|
||||||
|
}
|
10
static/js/usability.js
Normal file
10
static/js/usability.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// Bits and pieces to make vanilla HTML just a bit more usable.
|
||||||
|
|
||||||
|
// We want to let the user have a selection on collapsible blocks without collapsing them when
|
||||||
|
// the user finishes marking their selection.
|
||||||
|
document.addEventListener("click", event => {
|
||||||
|
console.log(getSelection());
|
||||||
|
if (getSelection().type == "Range") {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
})
|
|
@ -3,12 +3,20 @@
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<title>{{ config.user.title }}</title>
|
<title>{{ config.user.title }}</title>
|
||||||
<link rel="stylesheet" href="{{ local 'static/main.css' }}">
|
|
||||||
|
<link rel="stylesheet" href="{{ site }}/static/css/main.css">
|
||||||
|
<link rel="stylesheet" href="{{ site }}/static/font/font.css">
|
||||||
|
|
||||||
|
<script type="module" src="{{ site }}/static/js/usability.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<main class="tree">
|
||||||
{{{ tree }}}
|
{{{ tree }}}
|
||||||
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue