making it look better

This commit is contained in:
liquidex 2023-08-18 17:04:12 +02:00
parent ad84a79335
commit 30255be018
22 changed files with 2567 additions and 72 deletions

12
.editorconfig Normal file
View 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

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,4 @@ resolver = "2"
[workspace.dependencies]
log = "0.4.20"
treehouse-format = { path = "crates/treehouse-format" }

View file

@ -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
- ie. a public braindump
- ie. a public braindump adsadasdsad
- since you're here, you're probably just setting up
@ -12,7 +12,7 @@
- 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
@ -28,10 +28,35 @@
- 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
- 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!)
- .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
```

View file

@ -5,4 +5,3 @@ edition = "2021"
[dependencies]
thiserror = "1.0.47"
log = { workspace = true }

View file

@ -25,6 +25,7 @@ impl Roots {
#[derive(Debug, Clone)]
pub struct Branch {
pub indent_level: usize,
pub attributes: Range<usize>,
pub kind: BranchKind,
pub kind_span: Range<usize>,
@ -35,6 +36,7 @@ pub struct Branch {
impl From<BranchEvent> for Branch {
fn from(branch: BranchEvent) -> Self {
Self {
indent_level: branch.indent_level,
attributes: branch.attributes,
kind: branch.kind,
kind_span: branch.kind_span,

View file

@ -4,9 +4,17 @@ version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.75"
axum = "0.6.20"
codespan-reporting = "0.11.1"
copy_dir = "0.1.3"
handlebars = "4.3.7"
pulldown-cmark = { version = "0.9.3", default-features = false }
serde = { version = "1.0.183", features = ["derive"] }
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 }
watchexec = "2.3.0"

View 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();
}

View file

@ -0,0 +1,2 @@
mod markdown;
pub mod tree;

View file

@ -1,5 +1,7 @@
use treehouse_format::{ast::Branch, pull::BranchKind};
use super::markdown;
pub fn branch_to_html(s: &mut String, branch: &Branch, source: &str) {
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(&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() {
s.push_str("</summary>");
branches_to_html(s, &branch.children, source);

View file

@ -1,46 +1,33 @@
mod tree_html;
mod html;
use axum::Router;
use codespan_reporting::{
diagnostic::{Diagnostic, Label, LabelStyle, Severity},
files::SimpleFile,
term::termcolor::{ColorChoice, StandardStream},
};
use tree_html::branches_to_html;
use treehouse_format::{
ast::{Branch, Roots},
pull::Parser,
};
use copy_dir::copy_dir;
use handlebars::Handlebars;
use html::tree::branches_to_html;
use serde::Serialize;
use tower_http::services::ServeDir;
use tower_livereload::LiveReloadLayer;
use treehouse_format::{ast::Roots, pull::Parser};
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("treehouse parsing error: {0}")]
Parse(#[from] treehouse_format::ParseError),
#[derive(Serialize)]
pub struct TemplateData {
pub tree: String,
}
fn print_branch(branch: &Branch, source: &str) {
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>> {
fn regenerate() -> anyhow::Result<()> {
let _ = std::fs::remove_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 parse_result = Roots::parse(&mut Parser {
input: &root_file,
@ -49,13 +36,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
match parse_result {
Ok(roots) => {
let mut html = String::from("<!DOCTYPE html><html><head></head><body>");
for root in &roots.branches {
print_branch(root, &root_file);
}
branches_to_html(&mut html, &roots.branches, &root_file);
std::fs::write("target/site/index.html", &html)?;
html.push_str("</body></html>")
let mut tree = String::new();
branches_to_html(&mut tree, &roots.branches, &root_file);
let index_html = handlebars.render("template/index.hbs", &TemplateData { tree })?;
std::fs::write("target/site/index.html", index_html)?;
}
Err(error) => {
let writer = StandardStream::stderr(ColorChoice::Auto);
@ -77,29 +63,43 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
// let mut parser = treehouse_format::Parser {
// input: &root_file,
// position: 0,
// };
// let mut generator = HtmlGenerator::default();
// while let Some(branch) = parser.next_branch()? {
// for _ in 0..branch.indent_level {
// print!(" ");
// }
// println!(
// "{} {:?}",
// branch.kind.char(),
// &root_file[branch.content.clone()]
// );
// generator.add(&root_file, &branch);
// }
// std::fs::write(
// "target/site/index.html",
// format!(
// "<!DOCTYPE html><html><head></head><body>{}</body></html>",
// generator.finish()
// ),
// )?;
Ok(())
}
fn regenerate_or_report_error() {
eprintln!("regenerating");
match regenerate() {
Ok(_) => (),
Err(error) => eprintln!("error: {error:?}"),
}
}
async fn web_server() -> anyhow::Result<()> {
let app = Router::new().nest_service("/", ServeDir::new("target/site"));
#[cfg(debug_assertions)]
let app = app.layer(LiveReloadLayer::new());
Ok(axum::Server::bind(&([0, 0, 0, 0], 8080).into())
.serve(app.into_make_service())
.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(())
}

75
static/css/main.css Normal file
View 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;
}

59
static/font/font.css Normal file
View 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
View 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();
}
})

View file

View file

@ -3,12 +3,20 @@
<html>
<head>
<meta charset="UTF-8">
<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>
<body>
<main class="tree">
{{{ tree }}}
</main>
</body>
</html>