init
This commit is contained in:
70
src/doc.rs
Normal file
70
src/doc.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Document<'a> {
|
||||
pub title: Text<'a>,
|
||||
pub depth: usize,
|
||||
pub link: &'a str,
|
||||
pub sections: Vec<Section<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Section<'a> {
|
||||
Paragraph(Text<'a>),
|
||||
List(Vec<Text<'a>>),
|
||||
Code(&'a str, Text<'a>),
|
||||
Quote(&'a str, &'a str, Text<'a>),
|
||||
Ext(&'a str, &'a str),
|
||||
Document(Document<'a>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Text<'a>(pub Vec<Component<'a>>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Component<'a> {
|
||||
Char(char),
|
||||
Text(&'a str),
|
||||
Note(Text<'a>),
|
||||
Wrong(Text<'a>),
|
||||
Special(Text<'a>),
|
||||
Emphasis(Text<'a>),
|
||||
Important(Text<'a>),
|
||||
Link(&'a str, Text<'a>),
|
||||
Ext(&'a str, &'a str),
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.iter().any(|c| c.is_empty())
|
||||
}
|
||||
}
|
||||
impl<'a> Component<'a> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Component::Char(_) => false,
|
||||
Component::Text(t) => t.is_empty(),
|
||||
Component::Note(t)
|
||||
| Component::Wrong(t)
|
||||
| Component::Special(t)
|
||||
| Component::Emphasis(t)
|
||||
| Component::Important(t) => t.is_empty(),
|
||||
Component::Link(target, t) => target.is_empty() && t.is_empty(),
|
||||
Component::Ext(_, _) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Document<'a> {
|
||||
pub fn find_link_target(&'a self, link: &str) -> Option<&'a Document<'a>> {
|
||||
if self.link == link {
|
||||
return Some(self);
|
||||
}
|
||||
for child in &self.sections {
|
||||
if let Section::Document(child) = child
|
||||
&& let Some(target) = child.find_link_target(link)
|
||||
{
|
||||
return Some(target);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
141
src/main.rs
Normal file
141
src/main.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
//! # mharkup
|
||||
//!
|
||||
//! another markup language
|
||||
//!
|
||||
//! ## sections
|
||||
//!
|
||||
//! Sections are delimited by double newlines (\n\n).
|
||||
//! A double newline can be included in any section by surrounding
|
||||
//! the section with at least one set of square brackets.
|
||||
//!
|
||||
//! ## input
|
||||
//!
|
||||
//! Must be utf-8 and must end with a linebreak.
|
||||
//! Linebreaks are \n (no \r).
|
||||
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::Shutdown,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use crate::{doc::Document, parse::parse};
|
||||
|
||||
mod doc;
|
||||
mod parse;
|
||||
mod to;
|
||||
|
||||
fn main() {
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: <filename>.mharkup <format> [specs...]");
|
||||
eprintln!("output formats and (available specs)");
|
||||
eprintln!("` html: a static html+css document (html, texm)");
|
||||
eprintln!("` html-http: html with auto-refresh");
|
||||
return;
|
||||
}
|
||||
let specs = &args[2..];
|
||||
let src = || std::fs::read_to_string(&args[0]).unwrap();
|
||||
match args[1].to_lowercase().trim() {
|
||||
"html" => println!(
|
||||
"{}",
|
||||
to::html::page(&parse(&src()).unwrap(), specs, &mut to::html::State::new())
|
||||
),
|
||||
"html-http" => {
|
||||
let addr = std::env::var("ADDR");
|
||||
let addr = addr.as_ref().map_or("localhost:8000", |v| v.as_str());
|
||||
let wait = std::env::var("WAIT")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse().ok())
|
||||
.unwrap_or(100);
|
||||
eprintln!();
|
||||
eprintln!("Binding to {addr} (ADDR env var)");
|
||||
eprintln!("Checking file mtime every {wait}ms while handling a request (WAIT env var)");
|
||||
let listener = std::net::TcpListener::bind(addr).unwrap();
|
||||
eprintln!();
|
||||
eprintln!("With your browser, open http://{addr}/");
|
||||
eprintln!("Without javascript, use http://{addr}/s");
|
||||
eprintln!("For a static page, open http://{addr}/1");
|
||||
let mut ptime = SystemTime::UNIX_EPOCH + Duration::from_mins(9);
|
||||
loop {
|
||||
let mut state = to::html::State::new();
|
||||
let mut buf1 = vec![String::new(); 1024];
|
||||
let mut buf2 = vec![None; 1024];
|
||||
let mut i = 0;
|
||||
// connection might time out after 30 seconds,
|
||||
// so send a 304 before that can happen.
|
||||
let maxchecks = 25 * 1000 / wait;
|
||||
'accept_connection: while i < 1024 {
|
||||
let (mut connection, _) = listener.accept().unwrap();
|
||||
let mut buf = [0u8; 8];
|
||||
if connection.read_exact(&mut buf).is_err() {
|
||||
continue;
|
||||
}
|
||||
let allow_delay = if let Ok(req) = str::from_utf8(&buf) {
|
||||
if req.contains(" /u") {
|
||||
state.head_to_body = "</head>\n<body>";
|
||||
true
|
||||
} else if req.contains(" /1") {
|
||||
state.head_to_body = "</head>\n<body>";
|
||||
false
|
||||
} else if req.contains(" /s") {
|
||||
state.head_to_body =
|
||||
"<meta http-equiv=\"refresh\" content=\"1\">\n</head>\n<body>";
|
||||
true
|
||||
} else {
|
||||
state.head_to_body = r#"<script>
|
||||
const BODY_REGEX = new RegExp("<bo"+"dy>.*<\\/bo"+"dy>", "misv");
|
||||
(async () => { while (true) {
|
||||
let res = await fetch("/u");
|
||||
if (!res.ok) continue;
|
||||
let str = (await res.text()).match(BODY_REGEX)[0];
|
||||
document.body.outerHTML = str;
|
||||
} })();
|
||||
</script>
|
||||
</head>
|
||||
<body>"#;
|
||||
false
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
if allow_delay {
|
||||
'delay_connection: {
|
||||
for _ in 0..maxchecks {
|
||||
let mtime =
|
||||
std::fs::metadata(&args[0]).unwrap().modified().unwrap();
|
||||
if mtime != ptime {
|
||||
ptime = mtime;
|
||||
break 'delay_connection;
|
||||
} else {
|
||||
std::thread::sleep(std::time::Duration::from_millis(wait));
|
||||
}
|
||||
}
|
||||
connection
|
||||
.write_all(
|
||||
b"HTTP/1.1 304 Not Modified\r\nContent-Length: 0\r\n\r\n",
|
||||
)
|
||||
.ok();
|
||||
connection.shutdown(Shutdown::Both).ok();
|
||||
continue 'accept_connection;
|
||||
}
|
||||
}
|
||||
buf1[i] = src();
|
||||
let src = unsafe { &*((&buf1[i]) as *const String) };
|
||||
buf2[i] = Some(parse(src).unwrap());
|
||||
let doc = unsafe { &*(buf2[i].as_ref().unwrap() as *const Document<'_>) };
|
||||
i += 1;
|
||||
let mut http =
|
||||
"HTTP/1.1 200 Ok\r\nContent-Length: 0000000000\r\n\r\n".to_owned();
|
||||
let len = http.len();
|
||||
to::html::page_to(doc, specs, &mut state, &mut http);
|
||||
let content_length = format!("{}", http.len() - len);
|
||||
http.replace_range(len - 4 - content_length.len()..len - 4, &content_length);
|
||||
connection.write_all(http.as_bytes()).ok();
|
||||
connection.shutdown(Shutdown::Both).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => eprintln!("Unknown format, run without arguments for help"),
|
||||
}
|
||||
}
|
||||
519
src/parse.rs
Normal file
519
src/parse.rs
Normal file
@@ -0,0 +1,519 @@
|
||||
use crate::doc::{Component, Document, Section, Text};
|
||||
|
||||
pub fn parse<'a>(mut src: &'a str) -> Result<Document<'a>, ParseError<'a>> {
|
||||
let doc = parse_document(&mut src, 0)?.unwrap();
|
||||
assert!(src.trim().is_empty());
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseError<'a>(ParseErr<'a>, usize);
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub enum ParseErr<'a> {
|
||||
InvalidSectionHash,
|
||||
InvalidHeadingDepth,
|
||||
BracketsAtStartOfSectionWithNoLinebreakAfter,
|
||||
BracketedSectionNotTerminated,
|
||||
BracketedSectionTerminatedByTooManyBrackets,
|
||||
IncompleteBacktick,
|
||||
MissingClosing(char),
|
||||
InvalidBacktickBracketCountSpecifier,
|
||||
InvalidBacktickModifier,
|
||||
SubsectionNotAllowedHere,
|
||||
FileTitleWithLink,
|
||||
r#__Zzzz(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ParseTextOpts<'a> {
|
||||
closing: Option<(char, &'a str)>,
|
||||
remove_linebreaks: bool,
|
||||
remove_indentation: (usize, usize),
|
||||
}
|
||||
#[allow(dead_code)]
|
||||
impl<'a> ParseTextOpts<'a> {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
closing: None,
|
||||
remove_linebreaks: false,
|
||||
remove_indentation: (0, 0),
|
||||
}
|
||||
}
|
||||
pub fn default(self) -> Self {
|
||||
Self::new()
|
||||
}
|
||||
fn closing(mut self, char: char, str: &'a str) -> Self {
|
||||
self.closing = Some((char, str));
|
||||
self
|
||||
}
|
||||
pub fn remove_linebreaks(mut self) -> Self {
|
||||
self.remove_linebreaks = true;
|
||||
self
|
||||
}
|
||||
pub fn remove_all_indentation(mut self, depth: usize) -> Self {
|
||||
self.remove_indentation = (depth, depth);
|
||||
self
|
||||
}
|
||||
pub fn remove_all_indentation_2(
|
||||
mut self,
|
||||
depth_initial: usize,
|
||||
depth_linebreak: usize,
|
||||
) -> Self {
|
||||
self.remove_indentation = (depth_initial, depth_linebreak);
|
||||
self
|
||||
}
|
||||
pub fn remove_only_initial_indentation(mut self, depth: usize) -> Self {
|
||||
self.remove_indentation = (depth, 0);
|
||||
self
|
||||
}
|
||||
pub fn remove_only_linebreak_indentation(mut self, depth: usize) -> Self {
|
||||
self.remove_indentation = (0, depth);
|
||||
self
|
||||
}
|
||||
}
|
||||
pub fn parse_text<'a>(
|
||||
src: &mut &'a str,
|
||||
opts: impl FnOnce(ParseTextOpts<'static>) -> ParseTextOpts<'static>,
|
||||
) -> Result<Text<'a>, ParseError<'a>> {
|
||||
parse_text_impl(src, opts(ParseTextOpts::new()))
|
||||
}
|
||||
pub fn parse_text_impl<'a>(
|
||||
src: &mut &'a str,
|
||||
ptopts: ParseTextOpts<'_>,
|
||||
) -> Result<Text<'a>, ParseError<'a>> {
|
||||
let closing = ptopts.closing;
|
||||
let remove_linebreaks = ptopts.remove_linebreaks;
|
||||
let remove_indentation = ptopts.remove_indentation;
|
||||
let mut components = Vec::new();
|
||||
let len = src.len();
|
||||
let pos = |src: &str| len - src.len();
|
||||
let indentation_bytes = src
|
||||
.chars()
|
||||
.take(remove_indentation.0)
|
||||
.take_while(|ch| ch.is_whitespace() && *ch != '\n')
|
||||
.map(|ch| ch.len_utf8())
|
||||
.sum();
|
||||
*src = &src[indentation_bytes..];
|
||||
while let Some(i) = src.find(['`', '\n']) {
|
||||
if let Some((_, closing)) = closing
|
||||
&& let Some(c) = src[..i].find(closing)
|
||||
{
|
||||
if c > 0 {
|
||||
components.push(Component::Text(&src[..c]));
|
||||
}
|
||||
*src = &src[c + closing.len()..];
|
||||
return Ok(Text(components));
|
||||
}
|
||||
let is_linebreak = src[i..].starts_with('\n');
|
||||
if is_linebreak {
|
||||
if remove_linebreaks {
|
||||
if src[i + 1..].starts_with('\n') {
|
||||
// keep one of the two linebreaks
|
||||
components.push(Component::Text(&src[..i + 1]));
|
||||
*src = &src[i + 2..];
|
||||
} else {
|
||||
// remove the linebreak, put a space
|
||||
components.push(Component::Text(&src[..i]));
|
||||
components.push(Component::Char(' '));
|
||||
*src = &src[i + 1..];
|
||||
}
|
||||
} else {
|
||||
// keep the linebreak
|
||||
components.push(Component::Text(&src[..i + 1]));
|
||||
*src = &src[i + 1..];
|
||||
}
|
||||
let indentation_bytes = src
|
||||
.chars()
|
||||
.take(remove_indentation.1)
|
||||
.take_while(|ch| ch.is_whitespace() && *ch != '\n')
|
||||
.map(|ch| ch.len_utf8())
|
||||
.sum();
|
||||
*src = &src[indentation_bytes..];
|
||||
continue;
|
||||
}
|
||||
if i > 0 {
|
||||
components.push(Component::Text(&src[..i]));
|
||||
}
|
||||
*src = &src[i + 1..];
|
||||
match src
|
||||
.chars()
|
||||
.next()
|
||||
.ok_or(ParseError(ParseErr::IncompleteBacktick, pos(src)))?
|
||||
{
|
||||
'`' => {
|
||||
*src = &src[1..];
|
||||
components.push(Component::Char('`'));
|
||||
}
|
||||
' ' => return Err(ParseError(ParseErr::IncompleteBacktick, pos(src))),
|
||||
_ => {
|
||||
let i = src
|
||||
.chars()
|
||||
.take_while(|&ch| {
|
||||
!(ch.is_whitespace()
|
||||
|| closing.is_some_and(|(c, _)| c == ch)
|
||||
|| matches!(ch, '(' | '[' | '{' | '<'))
|
||||
})
|
||||
.map(|ch| ch.len_utf8())
|
||||
.sum();
|
||||
if let Some(bracket) = src[i..]
|
||||
.chars()
|
||||
.next()
|
||||
.filter(|&ch| !(ch.is_whitespace() || closing.is_some_and(|(c, _)| c == ch)))
|
||||
{
|
||||
let mut opts = &src[..i];
|
||||
*src = &src[i + bracket.len_utf8()..];
|
||||
let numbrackets = {
|
||||
let i = opts
|
||||
.find(|ch: char| ch.is_ascii_punctuation())
|
||||
.unwrap_or(opts.len());
|
||||
if i == 0 {
|
||||
1
|
||||
} else if let Some(num) = opts[..i].parse().ok().filter(|n| *n > 0) {
|
||||
opts = &opts[i..];
|
||||
num
|
||||
} else {
|
||||
return Err(ParseError(
|
||||
ParseErr::InvalidBacktickBracketCountSpecifier,
|
||||
pos(src) - opts.len(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let pos = pos(src);
|
||||
let (closing, raw) = match bracket {
|
||||
'(' => (')', false),
|
||||
'[' => (']', false),
|
||||
'{' => ('}', true),
|
||||
'<' => ('>', true),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut pat = closing.to_string();
|
||||
if numbrackets > 1 {
|
||||
pat = pat.repeat(numbrackets);
|
||||
}
|
||||
let mut inner = if raw {
|
||||
if let Some(i) = src.find(&pat) {
|
||||
let text = &src[..i];
|
||||
*src = &src[i + pat.len()..];
|
||||
Ok(Component::Text(text))
|
||||
} else {
|
||||
return Err(ParseError(ParseErr::MissingClosing(closing), pos));
|
||||
}
|
||||
} else {
|
||||
Err(parse_text_impl(src, ptopts.closing(closing, &pat))
|
||||
.map_err(|ParseError(e, i)| ParseError(e, i + pos))?)
|
||||
};
|
||||
while let Some(ch) = opts.chars().next() {
|
||||
opts = &opts[ch.len_utf8()..];
|
||||
let opt = if let Some(i) = opts.find(|ch: char| ch.is_ascii_punctuation()) {
|
||||
let opt = &opts[..i];
|
||||
opts = &opts[i..];
|
||||
opt
|
||||
} else {
|
||||
let opt = opts;
|
||||
opts = "";
|
||||
opt
|
||||
};
|
||||
match ch {
|
||||
'-' if opt.is_empty() => {
|
||||
inner = Ok(text(inner, Component::Note));
|
||||
}
|
||||
'~' if opt.is_empty() => {
|
||||
inner = Ok(text(inner, Component::Wrong));
|
||||
}
|
||||
'_' if opt.is_empty() => {
|
||||
inner = Ok(text(inner, Component::Special));
|
||||
}
|
||||
'+' if opt.is_empty() => {
|
||||
inner = Ok(text(inner, Component::Emphasis));
|
||||
}
|
||||
'*' if opt.is_empty() => {
|
||||
inner = Ok(text(inner, Component::Important));
|
||||
}
|
||||
'/' if !opt.is_empty() => {
|
||||
inner = Ok(text(inner, |v| Component::Link(opt, v)));
|
||||
}
|
||||
'\\' if raw && !opt.is_empty() => {
|
||||
let mut inner = inner.as_mut().expect("because we are in raw mode");
|
||||
loop {
|
||||
match inner {
|
||||
Component::Text(raw) => {
|
||||
*inner = Component::Ext(opt, raw);
|
||||
break;
|
||||
}
|
||||
Component::Link(_, text)
|
||||
| Component::Note(text)
|
||||
| Component::Wrong(text)
|
||||
| Component::Special(text)
|
||||
| Component::Emphasis(text)
|
||||
| Component::Important(text) => inner = &mut text.0[0],
|
||||
Component::Char(_) | Component::Ext(_, _) => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(ParseError(
|
||||
ParseErr::InvalidBacktickModifier,
|
||||
pos - opts.len(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn text<'a>(
|
||||
inner: Result<Component<'a>, Text<'a>>,
|
||||
f: impl FnOnce(Text<'a>) -> Component<'a>,
|
||||
) -> Component<'a> {
|
||||
match inner {
|
||||
Ok(component) => f(Text(vec![component])),
|
||||
Err(text) => f(text),
|
||||
}
|
||||
}
|
||||
match inner {
|
||||
Ok(component) => components.push(component),
|
||||
Err(text) => components.extend(text.0),
|
||||
}
|
||||
} else {
|
||||
return Err(ParseError(ParseErr::IncompleteBacktick, pos(src)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((ch, closing)) = closing {
|
||||
if let Some(c) = src.find(closing) {
|
||||
if c > 0 {
|
||||
components.push(Component::Text(&src[..c]));
|
||||
}
|
||||
*src = &src[c + closing.len()..];
|
||||
} else {
|
||||
return Err(ParseError(ParseErr::MissingClosing(ch), pos(src)));
|
||||
}
|
||||
} else if !src.is_empty() {
|
||||
components.push(Component::Text(&src[..]));
|
||||
*src = "";
|
||||
}
|
||||
Ok(Text(components))
|
||||
}
|
||||
|
||||
// NOTE: for a `depth` of 0, this never returns `Ok(Err(_))`.
|
||||
pub fn parse_document<'a>(
|
||||
src: &mut &'a str,
|
||||
min_depth: usize,
|
||||
) -> Result<Result<Document<'a>, (usize, bool)>, ParseError<'a>> {
|
||||
let mut src = Parser::new(src);
|
||||
let (title_section, pos) = src.take_section()?;
|
||||
let depth = match title_section.chars().take_while(|ch| *ch == '#').count() {
|
||||
d if d >= min_depth => d,
|
||||
d if d != 0 && d < min_depth => {
|
||||
return Ok(Err(
|
||||
if title_section[d..].starts_with(char::is_whitespace) {
|
||||
// has a title, so go to parent of the depth we want
|
||||
(d - 1, false)
|
||||
} else {
|
||||
// has no title, so continue using old section of that depth
|
||||
(d, true)
|
||||
},
|
||||
));
|
||||
}
|
||||
_ => return Err(ParseError(ParseErr::InvalidHeadingDepth, pos)),
|
||||
};
|
||||
if let Some(mut title) = if depth == 0 {
|
||||
Some(title_section)
|
||||
} else {
|
||||
title_section[depth..].strip_prefix(char::is_whitespace)
|
||||
} {
|
||||
let link = if let Some(i) = title
|
||||
.starts_with('/')
|
||||
.then_some(1)
|
||||
.or_else(|| title.rfind(" /").map(|i| i + 2))
|
||||
&& let prelink = &title[..i.saturating_sub(2)]
|
||||
&& let link = title[i..].trim_end()
|
||||
&& !link.contains(char::is_whitespace)
|
||||
{
|
||||
if depth == 0 {
|
||||
return Err(ParseError(ParseErr::FileTitleWithLink, pos));
|
||||
}
|
||||
title = prelink;
|
||||
link
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let sections = match parse_sections(&mut src, depth, true) {
|
||||
Ok(Ok(sections)) => sections,
|
||||
Ok(Err(info)) => return Ok(Err(info)),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
Ok(Ok(Document {
|
||||
title: parse_text(&mut title, |o| o.remove_linebreaks())
|
||||
.map_err(|ParseError(e, i)| ParseError(e, i + pos + depth + 1))?,
|
||||
depth,
|
||||
link,
|
||||
sections,
|
||||
}))
|
||||
} else {
|
||||
Err(ParseError(ParseErr::InvalidSectionHash, pos))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_sections<'a>(
|
||||
src: &mut Parser<'a, '_>,
|
||||
depth: usize,
|
||||
document: bool,
|
||||
) -> Result<Result<Vec<Section<'a>>, (usize, bool)>, ParseError<'a>> {
|
||||
let mut sections = Vec::new();
|
||||
'sections: while let (mut section, pos) = src.take_section()?
|
||||
&& !section.is_empty()
|
||||
{
|
||||
if section.starts_with('#') {
|
||||
let posthash = section.trim_start_matches('#');
|
||||
match posthash.chars().next() {
|
||||
// => section hashes
|
||||
Some(' ' | '\n') | None => {
|
||||
if !document {
|
||||
return Err(ParseError(ParseErr::SubsectionNotAllowedHere, pos));
|
||||
}
|
||||
src.reset_to(pos);
|
||||
match parse_document(src.get(), depth + 1)
|
||||
.map_err(|ParseError(e, i)| ParseError(e, i + pos))?
|
||||
{
|
||||
Ok(child_document) => {
|
||||
sections.push(Section::Document(child_document));
|
||||
}
|
||||
Err((d, notitle)) => {
|
||||
// a section of the same depth
|
||||
if d == depth && notitle {
|
||||
// terminated a subsection,
|
||||
// continue parsing here
|
||||
} else {
|
||||
// pass to parent for parsing
|
||||
src.reset_to(pos);
|
||||
break 'sections;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some('\\') => {
|
||||
let (spec, inner) = posthash[1..].split_once('\n').unwrap_or((posthash, ""));
|
||||
sections.push(Section::Ext(spec.strip_prefix(' ').unwrap_or(spec), inner));
|
||||
}
|
||||
Some('!') => {
|
||||
let (lang, mut inner) =
|
||||
posthash[1..].split_once('\n').unwrap_or((posthash, ""));
|
||||
sections.push(Section::Code(
|
||||
lang.strip_prefix(' ').unwrap_or(lang),
|
||||
parse_text(&mut inner, |o| o.default())
|
||||
.map_err(|ParseError(e, i)| ParseError(e, i + pos))?,
|
||||
));
|
||||
}
|
||||
Some('@') => {
|
||||
let (attributed, mut inner) =
|
||||
posthash[1..].split_once('\n').unwrap_or((posthash, ""));
|
||||
let attributed = attributed.strip_prefix(' ').unwrap_or(attributed);
|
||||
let (attributed, context) =
|
||||
attributed.rsplit_once('@').unwrap_or((attributed, ""));
|
||||
sections.push(Section::Quote(
|
||||
attributed.strip_suffix(' ').unwrap_or(attributed),
|
||||
context.strip_prefix(' ').unwrap_or(context),
|
||||
parse_text(&mut inner, |o| o.remove_linebreaks())
|
||||
.map_err(|ParseError(e, i)| ParseError(e, i + pos))?,
|
||||
));
|
||||
}
|
||||
_ => return Err(ParseError(ParseErr::InvalidSectionHash, pos)),
|
||||
}
|
||||
} else if let Some(contents) = section.strip_prefix("` ") {
|
||||
let mut pos = pos;
|
||||
sections.push(Section::List(
|
||||
contents
|
||||
.split("\n` ")
|
||||
.map(|mut elem| {
|
||||
pos += 2;
|
||||
let p = pos;
|
||||
pos += elem.len();
|
||||
parse_text(&mut elem, |o| {
|
||||
o.remove_linebreaks().remove_only_linebreak_indentation(2)
|
||||
})
|
||||
.map_err(|ParseError(e, i)| ParseError(e, i + p))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
));
|
||||
} else {
|
||||
sections.push(Section::Paragraph(
|
||||
parse_text(&mut section, |o| o.remove_linebreaks())
|
||||
.map_err(|ParseError(e, i)| ParseError(e, i + pos))?,
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Ok(sections))
|
||||
}
|
||||
|
||||
struct Parser<'a, 'b>(&'b mut &'a str, &'a str);
|
||||
impl<'a, 'b> Parser<'a, 'b> {
|
||||
fn new(src: &'b mut &'a str) -> Self {
|
||||
Self(src, src)
|
||||
}
|
||||
fn pos(&self) -> usize {
|
||||
self.1.len() - self.0.len()
|
||||
}
|
||||
fn reset_to(&mut self, pos: usize) {
|
||||
*self.0 = &self.1[pos..];
|
||||
}
|
||||
fn get<'c>(&'c mut self) -> &'c mut &'a str {
|
||||
self.0
|
||||
}
|
||||
fn trim_empty_lines(&mut self) {
|
||||
*self.0 = self.0.trim_start_matches('\n');
|
||||
}
|
||||
fn take_section(&mut self) -> Result<(&'a str, usize), ParseError<'a>> {
|
||||
self.trim_empty_lines();
|
||||
let start = self.pos();
|
||||
let brackets = self.0.chars().take_while(|ch| *ch == '[').count();
|
||||
if brackets > 0 {
|
||||
if !self.0[brackets..].starts_with('\n') {
|
||||
return Err(ParseError(
|
||||
ParseErr::BracketsAtStartOfSectionWithNoLinebreakAfter,
|
||||
start,
|
||||
));
|
||||
}
|
||||
*self.0 = &self.0[brackets..];
|
||||
self.trim_empty_lines();
|
||||
}
|
||||
self.take_rest_of_section(brackets)
|
||||
.map(|section| (section, start))
|
||||
.map_err(|e| ParseError(e, start))
|
||||
}
|
||||
fn take_rest_of_section(&mut self, brackets: usize) -> Result<&'a str, ParseErr<'a>> {
|
||||
if brackets == 0 {
|
||||
if let Some(i) = self.0.find("\n\n") {
|
||||
let out = &self.0[0..i];
|
||||
*self.0 = &self.0[i + 2..];
|
||||
Ok(out)
|
||||
} else {
|
||||
let out = &self.0[..];
|
||||
*self.0 = "";
|
||||
Ok(out)
|
||||
}
|
||||
} else {
|
||||
let pat = "]".repeat(brackets) + "\n";
|
||||
let mut k = 0;
|
||||
while let Some(i) = self.0[k..].find(&pat).map(|i| i + k) {
|
||||
if self.0[0..i].ends_with('\n') && self.0[i + pat.len()..].starts_with('\n') {
|
||||
let out = &self.0[0..i - 1];
|
||||
*self.0 = &self.0[i + pat.len() + 1..];
|
||||
return Ok(out);
|
||||
} else if self.0[0..i].ends_with('\n') && self.0[i + pat.len()..].is_empty() {
|
||||
let out = &self.0[0..i - 1];
|
||||
*self.0 = &self.0[i + pat.len()..];
|
||||
return Ok(out);
|
||||
} else if self.0[0..i].ends_with(']')
|
||||
&& self.0[0..i].trim_end_matches(']').ends_with('\n')
|
||||
{
|
||||
return Err(ParseErr::BracketedSectionTerminatedByTooManyBrackets);
|
||||
} else {
|
||||
k = i + pat.len();
|
||||
}
|
||||
}
|
||||
Err(ParseErr::BracketedSectionNotTerminated)
|
||||
}
|
||||
}
|
||||
}
|
||||
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