init
This commit is contained in:
543
src/to/html.rs
Normal file
543
src/to/html.rs
Normal file
@@ -0,0 +1,543 @@
|
||||
use std::{borrow::Cow, collections::BTreeMap, process::Command};
|
||||
|
||||
use crate::{
|
||||
doc::{Component, Document, Section, Text},
|
||||
to::{Spec, SpecsExt},
|
||||
};
|
||||
|
||||
pub struct State<'a> {
|
||||
pub head_to_body: &'a str,
|
||||
cache_docs: BTreeMap<&'a Document<'a>, String>,
|
||||
cache_secs: BTreeMap<&'a Section<'a>, String>,
|
||||
}
|
||||
impl<'a> State<'a> {
|
||||
pub fn new() -> Self {
|
||||
State {
|
||||
head_to_body: "</head>\n<body>",
|
||||
cache_docs: Default::default(),
|
||||
cache_secs: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page<'a>(doc: &'a Document<'a>, specs: &[impl Spec], state: &mut State<'a>) -> String {
|
||||
let mut out = String::new();
|
||||
page_to(doc, specs, state, &mut out);
|
||||
out
|
||||
}
|
||||
pub fn page_to<'a>(
|
||||
doc: &'a Document<'a>,
|
||||
specs: &[impl Spec],
|
||||
state: &mut State<'a>,
|
||||
out: &mut String,
|
||||
) {
|
||||
let doc_start = format!(
|
||||
r##"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{}</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: light) {{ :root {{ --bg: #FFF; --bg2: #FFF4F4; --fg: #000; }} }}
|
||||
@media (prefers-color-scheme: dark) {{ :root {{ --bg: #070002; --bg2: #070010; --fg: #E0B8A0; }} }}
|
||||
:root {{ color: var(--fg); background: var(--bg);
|
||||
--fgl: color-mix(var(--fg), var(--bg) 50%); --fgd: color-mix(var(--fg), var(--bg) 65%); --fgt: color-mix(var(--fg), var(--bg) 80%);
|
||||
--fsmall: 66%; --fless: 77%; --fmore: 110%; --ftitle: 132%; }}
|
||||
/*
|
||||
.x: actual content (incl. titles)
|
||||
.y: section elements except sub and title
|
||||
.z: section elements
|
||||
*/
|
||||
.doc.d0 {{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 90%;
|
||||
width: fit-content;
|
||||
}}
|
||||
.z {{
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
}}
|
||||
|
||||
.z {{
|
||||
margin-bottom: 0.5em;
|
||||
}}
|
||||
.sub > .doc, .doc > .doc {{
|
||||
margin-left: 0.15em;
|
||||
border-left: 2pt solid;
|
||||
padding-left: 0.6em;
|
||||
border-image: linear-gradient(to bottom, var(--fgl), var(--fgt)) 1;
|
||||
}}
|
||||
.doc:has(> .doc) {{
|
||||
border-image: linear-gradient(to bottom, var(--fgd), transparent) 1;
|
||||
}}
|
||||
|
||||
.y * {{
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}}
|
||||
.x {{
|
||||
white-space: pre-wrap;
|
||||
}}
|
||||
.doc {{
|
||||
white-space: normal;
|
||||
}}
|
||||
|
||||
.y.d0 .x {{
|
||||
font-size: var(--fless);
|
||||
}}
|
||||
.z.title.d0 .x {{
|
||||
font-size: var(--fmore);
|
||||
}}
|
||||
.z.title:not(.d0) .x {{
|
||||
font-size: var(--ftitle);
|
||||
}}
|
||||
.z.sub {{
|
||||
margin-top: 1em;
|
||||
}}
|
||||
|
||||
.z.title .lnpt {{
|
||||
color: var(--fgl);
|
||||
font-size: var(--fsmall);
|
||||
}}
|
||||
.z.title .lnpt::before {{
|
||||
font-size: var(--fless);
|
||||
content: " / ";
|
||||
}}
|
||||
.y.list ul {{
|
||||
padding-left: 1em;
|
||||
}}
|
||||
|
||||
.y.code .head {{
|
||||
color: var(--fgl);
|
||||
font-size: var(--fsmall);
|
||||
}}
|
||||
.y.code .head::before {{
|
||||
content: "#! ";
|
||||
font-size: var(--fless);
|
||||
}}
|
||||
|
||||
.y.quote .head {{
|
||||
color: var(--fgl);
|
||||
font-size: var(--fless);
|
||||
}}
|
||||
.y.quote .tail {{
|
||||
color: var(--fgl);
|
||||
font-size: var(--fsmall);
|
||||
text-align: right;
|
||||
}}
|
||||
.y.quote .head::before {{
|
||||
content: "@ ";
|
||||
font-size: var(--fless);
|
||||
}}
|
||||
.y.quote:not(.c-) .tail::before {{
|
||||
content: " (";
|
||||
font-size: var(--fless);
|
||||
}}
|
||||
.y.quote:not(.c-) .tail::after {{
|
||||
content: ") ";
|
||||
font-size: var(--fless);
|
||||
}}
|
||||
.y.quote.c- .x::after {{
|
||||
color: var(--fgl);
|
||||
font-size: var(--fsmall);
|
||||
content: "”";
|
||||
}}
|
||||
|
||||
.y.ext.spec .head {{
|
||||
color: var(--fgl);
|
||||
}}
|
||||
.y.ext.nospec .head {{
|
||||
color: red;
|
||||
}}
|
||||
.y.ext .head {{
|
||||
font-size: var(--fsmall);
|
||||
}}
|
||||
.y.ext .head::before {{
|
||||
content: "#/ ";
|
||||
font-size: var(--fless);
|
||||
}}
|
||||
.y.ext.spec.e-html .head {{
|
||||
display: none;
|
||||
}}
|
||||
.y.ext.spec.e-texm .head {{
|
||||
display: none;
|
||||
}}
|
||||
|
||||
.link {{
|
||||
color: var(--fg);
|
||||
text-decoration: underline var(--fgl);
|
||||
position: relative;
|
||||
}}
|
||||
.linkwpreview:has(> .link:hover) .lnpreview {{
|
||||
display: block;
|
||||
}}
|
||||
.lnpreview:hover {{
|
||||
display: block;
|
||||
}}
|
||||
.linkwpreview:has(> .link:active) .lnpreview {{
|
||||
display: none;
|
||||
}}
|
||||
.lnpreview {{
|
||||
display: none;
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
max-width: 70%;
|
||||
max-height: 40%;
|
||||
overflow-y: hidden;
|
||||
background: var(--bg2);
|
||||
padding: 1em;
|
||||
border: 2pt solid var(--fgd);
|
||||
}}
|
||||
.note, .lnref {{
|
||||
font-size: var(--fsmall);
|
||||
}}
|
||||
|
||||
</style>
|
||||
{}
|
||||
"##,
|
||||
escape(&crate::to::plain::text_minimal(&doc.title)),
|
||||
state.head_to_body,
|
||||
);
|
||||
if out.is_empty() {
|
||||
*out = doc_start;
|
||||
} else {
|
||||
out.push_str(&doc_start);
|
||||
}
|
||||
doc_to(doc, specs, out, state);
|
||||
out.push_str("</body>\n</html>\n");
|
||||
}
|
||||
|
||||
struct DocArgs<'a, 'b> {
|
||||
state: &'b mut State<'a>,
|
||||
root: &'a Document<'a>,
|
||||
depth: usize,
|
||||
max_len: Option<usize>,
|
||||
}
|
||||
fn doc_to<'a>(doc: &'a Document<'a>, specs: &[impl Spec], out: &mut String, state: &mut State<'a>) {
|
||||
doc_to_impl(
|
||||
doc,
|
||||
specs,
|
||||
out,
|
||||
&mut DocArgs {
|
||||
state,
|
||||
root: doc,
|
||||
depth: 0,
|
||||
max_len: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
fn doc_preview_to<'a>(
|
||||
doc: &'a Document<'a>,
|
||||
root: &'a Document<'a>,
|
||||
specs: &[impl Spec],
|
||||
out: &mut String,
|
||||
state: &mut State<'a>,
|
||||
) {
|
||||
doc_to_impl(
|
||||
doc,
|
||||
specs,
|
||||
out,
|
||||
&mut DocArgs {
|
||||
state,
|
||||
root,
|
||||
depth: doc.depth,
|
||||
max_len: Some(1024),
|
||||
},
|
||||
)
|
||||
}
|
||||
fn doc_to_impl<'a>(
|
||||
doc: &'a Document<'a>,
|
||||
specs: &[impl Spec],
|
||||
out: &mut String,
|
||||
doc_args: &mut DocArgs<'a, '_>,
|
||||
) {
|
||||
if let Some(prev) = doc_args.state.cache_docs.get(doc) {
|
||||
out.push_str(prev);
|
||||
return;
|
||||
}
|
||||
let out_start = out.len();
|
||||
let depth = doc_args.depth;
|
||||
let end_at = doc_args.max_len.map(|len| out.len() + len);
|
||||
for d in depth..=doc.depth {
|
||||
out.push_str(&format!(r#"<div class="doc d{d}">{}"#, '\n'));
|
||||
}
|
||||
if !doc.title.is_empty() {
|
||||
let a = if !doc.link.is_empty() && doc_args.max_len.is_none() {
|
||||
let link = doc.link.replace(char::is_whitespace, "-");
|
||||
let link = escape_quot(&link);
|
||||
format!(
|
||||
r##"<a class="lnpt" name="{}" href="#{}">{}</a>"##,
|
||||
link,
|
||||
link,
|
||||
escape(doc.link),
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
out.push_str(&format!(
|
||||
r#"<div class="z d{} title"><span class="x">"#,
|
||||
doc.depth,
|
||||
));
|
||||
text_to(&doc.title, specs, out, doc_args);
|
||||
out.push_str(&format!("</span>{a}</div>\n"));
|
||||
}
|
||||
for section in &doc.sections {
|
||||
if let Some(prev) = doc_args.state.cache_secs.get(section) {
|
||||
out.push_str(prev);
|
||||
continue;
|
||||
}
|
||||
let section_start = out.len();
|
||||
if end_at.is_some_and(|len| out.len() > len) {
|
||||
break;
|
||||
}
|
||||
out.push_str(&format!(
|
||||
r#"<div class="z d{} {}"#,
|
||||
doc.depth,
|
||||
match section {
|
||||
Section::Paragraph(..) => "y par",
|
||||
Section::List(..) => "y list",
|
||||
Section::Code(..) => "y code",
|
||||
Section::Quote(..) => "y quote",
|
||||
Section::Ext(..) => "y ext",
|
||||
Section::Document(..) => "sub",
|
||||
},
|
||||
));
|
||||
match section {
|
||||
Section::Paragraph(text) => {
|
||||
out.push_str(r#""><div class="x">"#);
|
||||
text_to(text, specs, out, doc_args);
|
||||
out.push_str("</div>");
|
||||
}
|
||||
Section::List(list) => {
|
||||
out.push_str(r#""><ul class="x">"#);
|
||||
for elem in list {
|
||||
out.push_str("<li>");
|
||||
text_to(elem, specs, out, doc_args);
|
||||
out.push_str("</li>");
|
||||
}
|
||||
out.push_str("</ul>");
|
||||
}
|
||||
Section::Code(lang, text) => {
|
||||
out.push_str(&format!(
|
||||
r#" l-{}"><div class="head">{}</div><pre class="x">"#,
|
||||
escape_quot(&lang.replace(char::is_whitespace, "-")),
|
||||
escape(lang),
|
||||
));
|
||||
text_to(text, specs, out, doc_args);
|
||||
out.push_str("</pre>");
|
||||
}
|
||||
Section::Quote(attributed, context, text) => {
|
||||
out.push_str(&format!(
|
||||
r#" a-{} c-{}"><div class="head">{}</div><div class="x">"#,
|
||||
escape_quot(&attributed.replace(char::is_whitespace, "-")),
|
||||
escape_quot(&context.replace(char::is_whitespace, "-")),
|
||||
escape(attributed),
|
||||
));
|
||||
text_to(text, specs, out, doc_args);
|
||||
out.push_str(&format!(
|
||||
r#"</div><div class="tail">{}</div>"#,
|
||||
escape(context),
|
||||
));
|
||||
}
|
||||
Section::Ext(spec @ "html", source) if specs.has(spec) => {
|
||||
out.push_str(&format!(
|
||||
r#" s3 spec e-{}"><div class="head">{}</div><div class="x">"#,
|
||||
escape_quot(spec),
|
||||
escape(spec),
|
||||
));
|
||||
out.push_str(source);
|
||||
out.push_str("</div>");
|
||||
}
|
||||
Section::Ext(spec @ "texm", source) if specs.has(spec) => {
|
||||
let l = out.len() + 2;
|
||||
out.push_str(&format!(
|
||||
r#" s0 spec e-{}"><div class="head">{}</div><div class="x">"#,
|
||||
escape_quot(spec),
|
||||
escape(spec),
|
||||
));
|
||||
let status = texm_to(source, out);
|
||||
out.replace_range(l..=l, status);
|
||||
out.push_str("</div>");
|
||||
}
|
||||
Section::Ext(spec, source) => {
|
||||
out.push_str(&format!(
|
||||
r#" nospec e-{}"><div class="head">{}</div><div class="x">"#,
|
||||
escape_quot(spec),
|
||||
escape(spec),
|
||||
));
|
||||
out.push_str(&escape(source));
|
||||
out.push_str("</div>");
|
||||
}
|
||||
Section::Document(inner) => {
|
||||
out.push_str(r#"">"#);
|
||||
let mut args = DocArgs {
|
||||
state: doc_args.state,
|
||||
root: doc_args.root,
|
||||
max_len: doc_args.max_len.map(|v| v / 2),
|
||||
depth: doc.depth + 1,
|
||||
};
|
||||
doc_to_impl(inner, specs, out, &mut args);
|
||||
}
|
||||
}
|
||||
out.push_str("</div>\n");
|
||||
doc_args
|
||||
.state
|
||||
.cache_secs
|
||||
.insert(section, out[section_start..].to_owned());
|
||||
}
|
||||
for _ in depth..=doc.depth {
|
||||
out.push_str("</div>\n");
|
||||
}
|
||||
doc_args
|
||||
.state
|
||||
.cache_docs
|
||||
.insert(doc, out[out_start..].to_owned());
|
||||
}
|
||||
|
||||
fn text_to<'a: 'b, 'b>(
|
||||
text: &'a Text<'a>,
|
||||
specs: &[impl Spec],
|
||||
out: &mut String,
|
||||
args: &mut DocArgs<'a, 'b>,
|
||||
) {
|
||||
for component in &text.0 {
|
||||
match component {
|
||||
Component::Char(ch) => escape_to(*ch, out),
|
||||
Component::Text(text) => out.push_str(escape(text).as_ref()),
|
||||
Component::Note(text) => {
|
||||
out.push_str(r#"<small class="note">"#);
|
||||
text_to(text, specs, out, args);
|
||||
out.push_str("</small>");
|
||||
}
|
||||
Component::Wrong(text) => {
|
||||
out.push_str(r#"<del class="wrong">"#);
|
||||
text_to(text, specs, out, args);
|
||||
out.push_str("</del>");
|
||||
}
|
||||
Component::Special(text) => {
|
||||
out.push_str(r#"<u class="special">"#);
|
||||
text_to(text, specs, out, args);
|
||||
out.push_str("</u>");
|
||||
}
|
||||
Component::Emphasis(text) => {
|
||||
out.push_str(r#"<em class="emphasis">"#);
|
||||
text_to(text, specs, out, args);
|
||||
out.push_str("</em>");
|
||||
}
|
||||
Component::Important(text) => {
|
||||
out.push_str(r#"<strong class="important">"#);
|
||||
text_to(text, specs, out, args);
|
||||
out.push_str("</strong>");
|
||||
}
|
||||
Component::Link(target, text) => {
|
||||
let link_target: Option<&'a Document<'a>> = args
|
||||
.max_len
|
||||
.is_none()
|
||||
.then(|| args.root.find_link_target(target))
|
||||
.flatten();
|
||||
if link_target.is_some() {
|
||||
out.push_str(r#"<span class="linkwpreview">"#);
|
||||
}
|
||||
out.push_str(&format!(
|
||||
r##"<a class="link" href="#{}">"##,
|
||||
escape_quot(&target.replace(char::is_whitespace, "-")),
|
||||
));
|
||||
text_to(text, specs, out, args);
|
||||
out.push_str(&format!(
|
||||
r#"<small class="lnref">[{}]</small>"#,
|
||||
escape(target),
|
||||
));
|
||||
out.push_str("</a>");
|
||||
if let Some(doc) = link_target {
|
||||
out.push_str(r#"<div class="lnpreview">"#);
|
||||
doc_preview_to(doc, args.root, specs, out, args.state);
|
||||
out.push_str("</div></span>");
|
||||
}
|
||||
}
|
||||
Component::Ext(spec, source) => match if specs.has(spec) { *spec } else { "" } {
|
||||
"html" => {
|
||||
out.push_str(&format!(
|
||||
r#"<span class="s3 spec e-{}">"#,
|
||||
escape_quot(spec)
|
||||
));
|
||||
out.push_str(source);
|
||||
out.push_str("</span>");
|
||||
}
|
||||
"texm" => {
|
||||
out.push_str(r#"<span class="s"#);
|
||||
let l = out.len();
|
||||
out.push_str(&format!(r#"0 spec e-{}">"#, escape_quot(spec)));
|
||||
let status = texm_to(source, out);
|
||||
out.replace_range(l..=l, status);
|
||||
out.push_str("</span>");
|
||||
}
|
||||
_ => {
|
||||
out.push_str("?[");
|
||||
out.push_str(&escape(spec));
|
||||
out.push(':');
|
||||
out.push_str(source);
|
||||
out.push_str(&escape(source));
|
||||
out.push_str("]?");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_to(ch: char, out: &mut String) {
|
||||
match ch {
|
||||
'&' => out.push_str("&"),
|
||||
'<' => out.push_str("<"),
|
||||
'>' => out.push_str(">"),
|
||||
ch => out.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
fn escape(str: &str) -> Cow<'_, str> {
|
||||
let mut out = Cow::Borrowed(str);
|
||||
if out.contains('&') {
|
||||
out = Cow::Owned(out.replace('&', "&"));
|
||||
}
|
||||
if out.contains('<') {
|
||||
out = Cow::Owned(out.replace('<', "<"));
|
||||
}
|
||||
if out.contains('>') {
|
||||
out = Cow::Owned(out.replace('<', ">"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn escape_quot(str: &str) -> Cow<'_, str> {
|
||||
let mut out = Cow::Borrowed(str);
|
||||
if out.contains('"') {
|
||||
out = Cow::Owned(out.replace('"', "\\\""));
|
||||
}
|
||||
if out.contains('\'') {
|
||||
out = Cow::Owned(out.replace('\'', "\\'"));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn texm_to(source: &str, out: &mut String) -> &'static str {
|
||||
if let Ok(res) = Command::new("latex2mathml").args(["-t", source]).output() {
|
||||
if res.status.success() {
|
||||
out.push_str(String::from_utf8_lossy(&res.stdout).trim());
|
||||
"5"
|
||||
} else {
|
||||
out.push('$');
|
||||
out.push_str(&escape(source));
|
||||
out.push_str("$[");
|
||||
out.push_str(&escape(String::from_utf8_lossy(&res.stderr).trim()));
|
||||
out.push(']');
|
||||
"1"
|
||||
}
|
||||
} else {
|
||||
out.push('$');
|
||||
out.push_str(&escape(source));
|
||||
out.push('$');
|
||||
"1"
|
||||
}
|
||||
}
|
||||
13
src/to/mod.rs
Normal file
13
src/to/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod html;
|
||||
pub mod plain;
|
||||
|
||||
pub trait Spec: AsRef<str> {}
|
||||
pub trait SpecsExt {
|
||||
fn has(&self, spec: &str) -> bool;
|
||||
}
|
||||
impl<T: Spec> SpecsExt for [T] {
|
||||
fn has(&self, spec: &str) -> bool {
|
||||
self.iter().any(|s| s.as_ref() == spec)
|
||||
}
|
||||
}
|
||||
impl<T: AsRef<str>> Spec for T {}
|
||||
98
src/to/plain.rs
Normal file
98
src/to/plain.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
use crate::{
|
||||
doc::{Component, Text},
|
||||
to::{Spec, SpecsExt},
|
||||
};
|
||||
|
||||
// pub fn page(doc: &Document<'_>, specs: &Specs<impl Spec>) -> String {
|
||||
// }
|
||||
|
||||
// fn doc_to(doc: &Document<'_>, depth: usize, specs: &Specs<impl Spec>, out: &mut String) {
|
||||
// }
|
||||
|
||||
pub fn text_minimal(text: &Text<'_>) -> String {
|
||||
let mut out = String::new();
|
||||
let specs: &'static [&'static str] = &[];
|
||||
text_to(text, false, specs, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
fn text_to(text: &Text<'_>, complete: bool, specs: &[impl Spec], out: &mut String) {
|
||||
for component in &text.0 {
|
||||
match component {
|
||||
Component::Char(ch) => out.push(*ch),
|
||||
Component::Text(text) => out.push_str(text),
|
||||
Component::Note(_) => {
|
||||
if complete {
|
||||
out.push('-');
|
||||
text_to(text, complete, specs, out);
|
||||
out.push('-');
|
||||
}
|
||||
}
|
||||
Component::Wrong(text) => {
|
||||
if complete {
|
||||
out.push('~');
|
||||
text_to(text, complete, specs, out);
|
||||
out.push('~');
|
||||
}
|
||||
}
|
||||
Component::Special(text) => {
|
||||
if complete {
|
||||
out.push('_');
|
||||
}
|
||||
text_to(text, complete, specs, out);
|
||||
if complete {
|
||||
out.push('_');
|
||||
}
|
||||
}
|
||||
Component::Emphasis(text) => {
|
||||
if complete {
|
||||
out.push('*');
|
||||
}
|
||||
text_to(text, complete, specs, out);
|
||||
if complete {
|
||||
out.push('*');
|
||||
}
|
||||
}
|
||||
Component::Important(text) => {
|
||||
if complete {
|
||||
out.push_str("**");
|
||||
} else {
|
||||
out.push('*');
|
||||
}
|
||||
text_to(text, complete, specs, out);
|
||||
if complete {
|
||||
out.push_str("**");
|
||||
} else {
|
||||
out.push('*');
|
||||
}
|
||||
}
|
||||
Component::Link(target, text) => {
|
||||
if complete {
|
||||
out.push('_');
|
||||
}
|
||||
text_to(text, complete, specs, out);
|
||||
if complete {
|
||||
out.push_str("_ (→ ");
|
||||
out.push_str(target);
|
||||
out.push(')');
|
||||
}
|
||||
}
|
||||
Component::Ext(spec, source) => {
|
||||
if complete {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
match if specs.has(spec) { *spec } else { "" } {
|
||||
_ => {
|
||||
out.push_str("?[");
|
||||
out.push_str(spec);
|
||||
out.push(':');
|
||||
out.push_str(source);
|
||||
out.push_str("]?");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.push('…');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user