making it look better

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

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