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 thumbs::PageThumbnailList;
|
||||||
pub use todo::{Checklist, TodoItem};
|
pub use todo::{Checklist, TodoItem};
|
||||||
pub use toolbar::FixedFormatToolbar;
|
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(crate) use viewer::step as zoom_step;
|
||||||
pub use viewer::{DocPage, DocumentViewer, PageHit, PageNote};
|
pub use viewer::{DocPage, DocumentViewer, PageHit, PageNote};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use markdown::mdast;
|
use markdown::mdast;
|
||||||
|
|
||||||
use super::{Node, Reader, Run, html, text_block};
|
use super::{Inline, Node, Reader, Run, html};
|
||||||
use crate::documents::markdown::span;
|
use crate::documents::markdown::{file_tree, span};
|
||||||
|
|
||||||
/// A heading anywhere in the file.
|
/// A heading anywhere in the file.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -58,9 +58,7 @@ impl Reader<'_> {
|
|||||||
if let Some(alert) = self.alert(node) {
|
if let Some(alert) = self.alert(node) {
|
||||||
return out.push(alert);
|
return out.push(alert);
|
||||||
}
|
}
|
||||||
let toc = matches!(node, mdast::Node::Paragraph(_))
|
match is_toc(node, self.source) {
|
||||||
&& self.source[span(node)].trim().eq_ignore_ascii_case("[toc]");
|
|
||||||
match toc {
|
|
||||||
true => out.push(Node::Toc(self.headings.clone())),
|
true => out.push(Node::Toc(self.headings.clone())),
|
||||||
false => self.block(node, out),
|
false => self.block(node, out),
|
||||||
}
|
}
|
||||||
@@ -71,87 +69,74 @@ impl Reader<'_> {
|
|||||||
let mdast::Node::Blockquote(quote) = node else {
|
let mdast::Node::Blockquote(quote) = node else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let [mdast::Node::Paragraph(paragraph), rest @ ..] = "e.children[..] else {
|
let (kind, parts) = alert(quote)?;
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let (kind, after) = marker(¶graph.children)?;
|
|
||||||
if after.is_empty() && rest.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut body = Vec::new();
|
let mut body = Vec::new();
|
||||||
if !after.is_empty() {
|
self.blocks(&parts, &mut body);
|
||||||
let kept = mdast::Paragraph {
|
|
||||||
children: after,
|
|
||||||
position: paragraph.position.clone(),
|
|
||||||
};
|
|
||||||
self.block(&mdast::Node::Paragraph(kept), &mut body);
|
|
||||||
}
|
|
||||||
self.blocks(rest, &mut body);
|
|
||||||
Some(Node::Alert { kind, body })
|
Some(Node::Alert { kind, body })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An HTML block's text, as a browser shows it.
|
/// An HTML block's text, as a browser shows it.
|
||||||
pub(super) fn html_block(&self, written: &str, out: &mut Vec<Node>) {
|
pub fn html_block_text(written: &str) -> Inline {
|
||||||
let mut run = Run::default();
|
let mut run = Run::default();
|
||||||
let mut rest = written;
|
let mut rest = written;
|
||||||
while let Some(at) = rest.find('<') {
|
while let Some(at) = rest.find('<') {
|
||||||
run.words(&rest[..at]);
|
run.words(&rest[..at]);
|
||||||
rest = &rest[at..];
|
rest = &rest[at..];
|
||||||
if let Some(comment) = rest.strip_prefix("<!--") {
|
if let Some(comment) = rest.strip_prefix("<!--") {
|
||||||
rest = comment.split_once("-->").map_or("", |(_, after)| after);
|
rest = comment.split_once("-->").map_or("", |(_, after)| after);
|
||||||
run.shortcodes.split();
|
run.shortcodes.split();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let opens =
|
let opens =
|
||||||
rest[1..].starts_with(|ch: char| ch.is_ascii_alphabetic() || "/!?".contains(ch));
|
rest[1..].starts_with(|ch: char| ch.is_ascii_alphabetic() || "/!?".contains(ch));
|
||||||
if !opens {
|
if !opens {
|
||||||
run.words("<");
|
run.words("<");
|
||||||
rest = &rest[1..];
|
rest = &rest[1..];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// An unclosed tag drops the rest, as HTML5 does.
|
// An unclosed tag drops the rest, as HTML5 does.
|
||||||
let Some(end) = html::tag_end(rest) else {
|
let Some(end) = html::tag_end(rest) else {
|
||||||
rest = "";
|
rest = "";
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
let (written, after) = rest.split_at(end);
|
let (written, after) = rest.split_at(end);
|
||||||
rest = after;
|
rest = after;
|
||||||
match html::tag(written) {
|
match html::tag(written) {
|
||||||
Some(tag) if html::filtered(&tag.name) => run.words(written),
|
Some(tag) if html::filtered(&tag.name) => run.words(written),
|
||||||
// A block closes what opened inside it.
|
// A block closes what opened inside it.
|
||||||
Some(tag) if html::breaks(&tag.name) => {
|
Some(tag) if html::breaks(&tag.name) => {
|
||||||
run.line();
|
run.line();
|
||||||
match tag.closing {
|
match tag.closing {
|
||||||
true => run.end(&tag.name),
|
true => run.end(&tag.name),
|
||||||
false => {
|
false => {
|
||||||
run.implied(&tag.name);
|
run.implied(&tag.name);
|
||||||
match tag.name == "hr" {
|
match tag.name == "hr" {
|
||||||
true => run.shortcodes.split(),
|
true => run.shortcodes.split(),
|
||||||
false => run.start(&tag.name, None, None),
|
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 {
|
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.
|
/// A plain first line naming a kind, and what follows.
|
||||||
fn marker(children: &[mdast::Node]) -> Option<(AlertKind, Vec<mdast::Node>)> {
|
fn marker(children: &[mdast::Node]) -> Option<(AlertKind, Vec<mdast::Node>)> {
|
||||||
let mut line = String::new();
|
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.
|
/// One DOM text node's shortcodes, as GitHub reads them.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct Shortcodes {
|
pub struct Shortcodes {
|
||||||
/// Settled by the node's first `:name:`.
|
/// Settled by the node's first `:name:`.
|
||||||
reads: Option<bool>,
|
reads: Option<bool>,
|
||||||
/// Text before it held more than white space.
|
/// Text before it held more than white space.
|
||||||
@@ -59,12 +59,12 @@ pub(crate) struct Shortcodes {
|
|||||||
|
|
||||||
impl Shortcodes {
|
impl Shortcodes {
|
||||||
/// A new DOM text node starts.
|
/// A new DOM text node starts.
|
||||||
pub(crate) fn split(&mut self) {
|
pub fn split(&mut self) {
|
||||||
*self = Self::default();
|
*self = Self::default();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Blank or spaced text before the first reads them all.
|
/// 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() {
|
if self.reads.is_none() {
|
||||||
let at = candidate(text);
|
let at = candidate(text);
|
||||||
let lead = &text[..at.unwrap_or(text.len())];
|
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.
|
/// Start tags that end an open `p`, as HTML5 lists them.
|
||||||
pub(crate) fn closes_p(name: &str) -> bool {
|
pub(crate) fn closes_p(name: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
|
|||||||
+16
-23
@@ -8,7 +8,9 @@ mod blocks;
|
|||||||
mod emoji;
|
mod emoji;
|
||||||
mod html;
|
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.
|
/// How a stretch of inline text prints.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -156,28 +158,19 @@ impl Run {
|
|||||||
|
|
||||||
/// Opens or closes an element for an inline HTML tag.
|
/// Opens or closes an element for an inline HTML tag.
|
||||||
fn html(&mut self, written: &str) {
|
fn html(&mut self, written: &str) {
|
||||||
let Some(tag) = html::tag(written) else {
|
match html::inline_tag(written) {
|
||||||
return self.shortcodes.split();
|
InlineTag::Text(shown) => self.push_text(&shown),
|
||||||
};
|
InlineTag::Open { name, style, href } => self.start(&name, style, href),
|
||||||
if html::filtered(&tag.name) {
|
InlineTag::Close(name) => {
|
||||||
return self.push_text(&html::text(written));
|
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.
|
/// Opens `name`; a new `a` ends the open one.
|
||||||
@@ -366,7 +359,7 @@ impl<'a> Reader<'a> {
|
|||||||
},
|
},
|
||||||
body: self.nested(¬e.children),
|
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::Yaml(yaml) => out.push(Node::FrontMatter(yaml.value.clone())),
|
||||||
mdast::Node::Toml(toml) => out.push(Node::FrontMatter(toml.value.clone())),
|
mdast::Node::Toml(toml) => out.push(Node::FrontMatter(toml.value.clone())),
|
||||||
mdast::Node::Definition(_) => {}
|
mdast::Node::Definition(_) => {}
|
||||||
|
|||||||
Reference in new issue
Block a user