documents: the drawing's reading rules are public, for exports to share
Shortcodes, inline_tag, html_block_text, alert, is_toc and headings are what the drawing itself calls, so an export that calls them shows the same. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8e5dad8177
commit
97fa047d18
5 files changed
+159
-102
No files matched your search
@@ -57,6 +57,10 @@ pub use templates::{Template, TemplatePicker};
|
||||
pub use thumbs::PageThumbnailList;
|
||||
pub use todo::{Checklist, TodoItem};
|
||||
pub use toolbar::FixedFormatToolbar;
|
||||
pub use tree::{
|
||||
AlertKind, Inline, InlineTag, Shortcodes, Style, alert, headings, html_block_text, inline_tag,
|
||||
is_toc,
|
||||
};
|
||||
pub(crate) use viewer::step as zoom_step;
|
||||
pub use viewer::{DocPage, DocumentViewer, PageHit, PageNote};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use markdown::mdast;
|
||||
|
||||
use super::{Node, Reader, Run, html, text_block};
|
||||
use crate::documents::markdown::span;
|
||||
use super::{Inline, Node, Reader, Run, html};
|
||||
use crate::documents::markdown::{file_tree, span};
|
||||
|
||||
/// A heading anywhere in the file.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -58,9 +58,7 @@ impl Reader<'_> {
|
||||
if let Some(alert) = self.alert(node) {
|
||||
return out.push(alert);
|
||||
}
|
||||
let toc = matches!(node, mdast::Node::Paragraph(_))
|
||||
&& self.source[span(node)].trim().eq_ignore_ascii_case("[toc]");
|
||||
match toc {
|
||||
match is_toc(node, self.source) {
|
||||
true => out.push(Node::Toc(self.headings.clone())),
|
||||
false => self.block(node, out),
|
||||
}
|
||||
@@ -71,87 +69,74 @@ impl Reader<'_> {
|
||||
let mdast::Node::Blockquote(quote) = node else {
|
||||
return None;
|
||||
};
|
||||
let [mdast::Node::Paragraph(paragraph), rest @ ..] = "e.children[..] else {
|
||||
return None;
|
||||
};
|
||||
let (kind, after) = marker(¶graph.children)?;
|
||||
if after.is_empty() && rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (kind, parts) = alert(quote)?;
|
||||
let mut body = Vec::new();
|
||||
if !after.is_empty() {
|
||||
let kept = mdast::Paragraph {
|
||||
children: after,
|
||||
position: paragraph.position.clone(),
|
||||
};
|
||||
self.block(&mdast::Node::Paragraph(kept), &mut body);
|
||||
}
|
||||
self.blocks(rest, &mut body);
|
||||
self.blocks(&parts, &mut body);
|
||||
Some(Node::Alert { kind, body })
|
||||
}
|
||||
}
|
||||
|
||||
/// An HTML block's text, as a browser shows it.
|
||||
pub(super) fn html_block(&self, written: &str, out: &mut Vec<Node>) {
|
||||
let mut run = Run::default();
|
||||
let mut rest = written;
|
||||
while let Some(at) = rest.find('<') {
|
||||
run.words(&rest[..at]);
|
||||
rest = &rest[at..];
|
||||
if let Some(comment) = rest.strip_prefix("<!--") {
|
||||
rest = comment.split_once("-->").map_or("", |(_, after)| after);
|
||||
run.shortcodes.split();
|
||||
continue;
|
||||
}
|
||||
let opens =
|
||||
rest[1..].starts_with(|ch: char| ch.is_ascii_alphabetic() || "/!?".contains(ch));
|
||||
if !opens {
|
||||
run.words("<");
|
||||
rest = &rest[1..];
|
||||
continue;
|
||||
}
|
||||
// An unclosed tag drops the rest, as HTML5 does.
|
||||
let Some(end) = html::tag_end(rest) else {
|
||||
rest = "";
|
||||
break;
|
||||
};
|
||||
let (written, after) = rest.split_at(end);
|
||||
rest = after;
|
||||
match html::tag(written) {
|
||||
Some(tag) if html::filtered(&tag.name) => run.words(written),
|
||||
// A block closes what opened inside it.
|
||||
Some(tag) if html::breaks(&tag.name) => {
|
||||
run.line();
|
||||
match tag.closing {
|
||||
true => run.end(&tag.name),
|
||||
false => {
|
||||
run.implied(&tag.name);
|
||||
match tag.name == "hr" {
|
||||
true => run.shortcodes.split(),
|
||||
false => run.start(&tag.name, None, None),
|
||||
}
|
||||
/// An HTML block's text, as a browser shows it.
|
||||
pub fn html_block_text(written: &str) -> Inline {
|
||||
let mut run = Run::default();
|
||||
let mut rest = written;
|
||||
while let Some(at) = rest.find('<') {
|
||||
run.words(&rest[..at]);
|
||||
rest = &rest[at..];
|
||||
if let Some(comment) = rest.strip_prefix("<!--") {
|
||||
rest = comment.split_once("-->").map_or("", |(_, after)| after);
|
||||
run.shortcodes.split();
|
||||
continue;
|
||||
}
|
||||
let opens =
|
||||
rest[1..].starts_with(|ch: char| ch.is_ascii_alphabetic() || "/!?".contains(ch));
|
||||
if !opens {
|
||||
run.words("<");
|
||||
rest = &rest[1..];
|
||||
continue;
|
||||
}
|
||||
// An unclosed tag drops the rest, as HTML5 does.
|
||||
let Some(end) = html::tag_end(rest) else {
|
||||
rest = "";
|
||||
break;
|
||||
};
|
||||
let (written, after) = rest.split_at(end);
|
||||
rest = after;
|
||||
match html::tag(written) {
|
||||
Some(tag) if html::filtered(&tag.name) => run.words(written),
|
||||
// A block closes what opened inside it.
|
||||
Some(tag) if html::breaks(&tag.name) => {
|
||||
run.line();
|
||||
match tag.closing {
|
||||
true => run.end(&tag.name),
|
||||
false => {
|
||||
run.implied(&tag.name);
|
||||
match tag.name == "hr" {
|
||||
true => run.shortcodes.split(),
|
||||
false => run.start(&tag.name, None, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
// It ends a `p` but lays no block.
|
||||
Some(tag) if !tag.closing && html::closes_p(&tag.name) => {
|
||||
run.implied(&tag.name);
|
||||
run.html(written);
|
||||
}
|
||||
// A line's end drops its space.
|
||||
Some(tag) if tag.closing || tag.name == "br" => {
|
||||
run.owed = run.owed.filter(|_| tag.name != "br");
|
||||
run.html(written);
|
||||
}
|
||||
Some(_) => {
|
||||
run.pay();
|
||||
run.html(written);
|
||||
}
|
||||
None => run.shortcodes.split(),
|
||||
}
|
||||
// It ends a `p` but lays no block.
|
||||
Some(tag) if !tag.closing && html::closes_p(&tag.name) => {
|
||||
run.implied(&tag.name);
|
||||
run.html(written);
|
||||
}
|
||||
// A line's end drops its space.
|
||||
Some(tag) if tag.closing || tag.name == "br" => {
|
||||
run.owed = run.owed.filter(|_| tag.name != "br");
|
||||
run.html(written);
|
||||
}
|
||||
Some(_) => {
|
||||
run.pay();
|
||||
run.html(written);
|
||||
}
|
||||
None => run.shortcodes.split(),
|
||||
}
|
||||
run.words(rest);
|
||||
text_block(run.cut(), out);
|
||||
}
|
||||
run.words(rest);
|
||||
run.cut()
|
||||
}
|
||||
|
||||
impl Run {
|
||||
@@ -223,6 +208,35 @@ impl Run {
|
||||
}
|
||||
}
|
||||
|
||||
/// A quote's alert kind and its blocks after the marker.
|
||||
pub fn alert(quote: &mdast::Blockquote) -> Option<(AlertKind, Vec<mdast::Node>)> {
|
||||
let [mdast::Node::Paragraph(paragraph), rest @ ..] = "e.children[..] else {
|
||||
return None;
|
||||
};
|
||||
let (kind, after) = marker(¶graph.children)?;
|
||||
if after.is_empty() && rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let head = (!after.is_empty()).then(|| {
|
||||
mdast::Node::Paragraph(mdast::Paragraph {
|
||||
children: after,
|
||||
position: paragraph.position.clone(),
|
||||
})
|
||||
});
|
||||
Some((kind, head.into_iter().chain(rest.iter().cloned()).collect()))
|
||||
}
|
||||
|
||||
/// A paragraph that is `[toc]` alone, as Typora reads it.
|
||||
pub fn is_toc(node: &mdast::Node, source: &str) -> bool {
|
||||
matches!(node, mdast::Node::Paragraph(_))
|
||||
&& source[span(node)].trim().eq_ignore_ascii_case("[toc]")
|
||||
}
|
||||
|
||||
/// Every heading in `text`, as `[toc]` lists them.
|
||||
pub fn headings(text: &str) -> Vec<DocumentHeading> {
|
||||
Reader::new(text, &file_tree(text)).headings
|
||||
}
|
||||
|
||||
/// A plain first line naming a kind, and what follows.
|
||||
fn marker(children: &[mdast::Node]) -> Option<(AlertKind, Vec<mdast::Node>)> {
|
||||
let mut line = String::new();
|
||||
|
||||
@@ -49,7 +49,7 @@ fn candidate(text: &str) -> Option<usize> {
|
||||
|
||||
/// One DOM text node's shortcodes, as GitHub reads them.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Shortcodes {
|
||||
pub struct Shortcodes {
|
||||
/// Settled by the node's first `:name:`.
|
||||
reads: Option<bool>,
|
||||
/// Text before it held more than white space.
|
||||
@@ -59,12 +59,12 @@ pub(crate) struct Shortcodes {
|
||||
|
||||
impl Shortcodes {
|
||||
/// A new DOM text node starts.
|
||||
pub(crate) fn split(&mut self) {
|
||||
pub fn split(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
/// Blank or spaced text before the first reads them all.
|
||||
pub(crate) fn read(&mut self, text: &str) -> String {
|
||||
pub fn read(&mut self, text: &str) -> String {
|
||||
if self.reads.is_none() {
|
||||
let at = candidate(text);
|
||||
let lead = &text[..at.unwrap_or(text.len())];
|
||||
|
||||
@@ -96,6 +96,52 @@ pub(crate) fn breaks(name: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// What one inline HTML tag does, as GitHub shows it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum InlineTag {
|
||||
/// Opens `name`; an anchor styles only with an address.
|
||||
Open {
|
||||
name: String,
|
||||
style: Option<Style>,
|
||||
href: Option<String>,
|
||||
},
|
||||
Close(String),
|
||||
Break,
|
||||
/// A filtered tag, shown as its text.
|
||||
Text(String),
|
||||
/// Gone; the text around it stays.
|
||||
Gone,
|
||||
}
|
||||
|
||||
/// What the inline HTML `written` does.
|
||||
pub fn inline_tag(written: &str) -> InlineTag {
|
||||
let Some(tag) = tag(written) else {
|
||||
return InlineTag::Gone;
|
||||
};
|
||||
if filtered(&tag.name) {
|
||||
return InlineTag::Text(text(written));
|
||||
}
|
||||
if tag.name == "br" {
|
||||
return InlineTag::Break;
|
||||
}
|
||||
let Some(style) = inline_style(&tag.name) else {
|
||||
return InlineTag::Gone;
|
||||
};
|
||||
if tag.closing {
|
||||
return InlineTag::Close(tag.name);
|
||||
}
|
||||
let href = match style {
|
||||
Style::Link => tag.attribute("href").map(str::to_string),
|
||||
_ => None,
|
||||
};
|
||||
let style = (style != Style::Link || href.is_some()).then_some(style);
|
||||
InlineTag::Open {
|
||||
name: tag.name,
|
||||
style,
|
||||
href,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start tags that end an open `p`, as HTML5 lists them.
|
||||
pub(crate) fn closes_p(name: &str) -> bool {
|
||||
matches!(
|
||||
|
||||
+16
-23
@@ -8,7 +8,9 @@ mod blocks;
|
||||
mod emoji;
|
||||
mod html;
|
||||
|
||||
pub use blocks::{AlertKind, DocumentHeading};
|
||||
pub use blocks::{AlertKind, DocumentHeading, alert, headings, html_block_text, is_toc};
|
||||
pub use emoji::Shortcodes;
|
||||
pub use html::{InlineTag, inline_tag};
|
||||
|
||||
/// How a stretch of inline text prints.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -156,28 +158,19 @@ impl Run {
|
||||
|
||||
/// Opens or closes an element for an inline HTML tag.
|
||||
fn html(&mut self, written: &str) {
|
||||
let Some(tag) = html::tag(written) else {
|
||||
return self.shortcodes.split();
|
||||
};
|
||||
if html::filtered(&tag.name) {
|
||||
return self.push_text(&html::text(written));
|
||||
match html::inline_tag(written) {
|
||||
InlineTag::Text(shown) => self.push_text(&shown),
|
||||
InlineTag::Open { name, style, href } => self.start(&name, style, href),
|
||||
InlineTag::Close(name) => {
|
||||
self.shortcodes.split();
|
||||
self.end(&name);
|
||||
}
|
||||
InlineTag::Break => {
|
||||
self.shortcodes.split();
|
||||
self.text.text.push('\n');
|
||||
}
|
||||
InlineTag::Gone => self.shortcodes.split(),
|
||||
}
|
||||
self.shortcodes.split();
|
||||
if tag.name == "br" {
|
||||
return self.text.text.push('\n');
|
||||
}
|
||||
let Some(style) = html::inline_style(&tag.name) else {
|
||||
return;
|
||||
};
|
||||
if tag.closing {
|
||||
return self.end(&tag.name);
|
||||
}
|
||||
let url = match style {
|
||||
Style::Link => tag.attribute("href").map(str::to_string),
|
||||
_ => None,
|
||||
};
|
||||
let style = (style != Style::Link || url.is_some()).then_some(style);
|
||||
self.start(&tag.name, style, url);
|
||||
}
|
||||
|
||||
/// Opens `name`; a new `a` ends the open one.
|
||||
@@ -366,7 +359,7 @@ impl<'a> Reader<'a> {
|
||||
},
|
||||
body: self.nested(¬e.children),
|
||||
}),
|
||||
mdast::Node::Html(html) => self.html_block(&html.value, out),
|
||||
mdast::Node::Html(html) => text_block(html_block_text(&html.value), out),
|
||||
mdast::Node::Yaml(yaml) => out.push(Node::FrontMatter(yaml.value.clone())),
|
||||
mdast::Node::Toml(toml) => out.push(Node::FrontMatter(toml.value.clone())),
|
||||
mdast::Node::Definition(_) => {}
|
||||
|
||||
Reference in new issue
Block a user