feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
+2158
File diff suppressed because it is too large.
Load diff
+2711
File diff suppressed because it is too large.
Load diff
Vendored
+593
@@ -0,0 +1,593 @@
|
||||
// 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 crate::strings::CowStr;
|
||||
use crate::Event::*;
|
||||
use crate::{Alignment, BlockQuoteKind, CodeBlockKind, Event, LinkType, Tag, TagEnd};
|
||||
use pulldown_cmark_escape::{
|
||||
escape_href, escape_html, escape_html_body_text, FmtWriter, IoWriter, StrWrite,
|
||||
};
|
||||
|
||||
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,
|
||||
|
||||
/// Whether if inside a metadata block (text should not be written)
|
||||
in_non_writing_block: 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,
|
||||
in_non_writing_block: false,
|
||||
table_state: TableState::Head,
|
||||
table_alignments: vec![],
|
||||
table_cell_index: 0,
|
||||
numbers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a new line.
|
||||
#[inline]
|
||||
fn write_newline(&mut self) -> Result<(), W::Error> {
|
||||
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) -> Result<(), W::Error> {
|
||||
self.writer.write_str(s)?;
|
||||
|
||||
if !s.is_empty() {
|
||||
self.end_newline = s.ends_with('\n');
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(mut self) -> Result<(), W::Error> {
|
||||
while let Some(event) = self.iter.next() {
|
||||
match event {
|
||||
Start(tag) => {
|
||||
self.start_tag(tag)?;
|
||||
}
|
||||
End(tag) => {
|
||||
self.end_tag(tag)?;
|
||||
}
|
||||
Text(text) => {
|
||||
if !self.in_non_writing_block {
|
||||
escape_html_body_text(&mut self.writer, &text)?;
|
||||
self.end_newline = text.ends_with('\n');
|
||||
}
|
||||
}
|
||||
Code(text) => {
|
||||
self.write("<code>")?;
|
||||
escape_html_body_text(&mut self.writer, &text)?;
|
||||
self.write("</code>")?;
|
||||
}
|
||||
InlineMath(text) => {
|
||||
self.write(r#"<span class="math math-inline">"#)?;
|
||||
escape_html(&mut self.writer, &text)?;
|
||||
self.write("</span>")?;
|
||||
}
|
||||
DisplayMath(text) => {
|
||||
self.write(r#"<span class="math math-display">"#)?;
|
||||
escape_html(&mut self.writer, &text)?;
|
||||
self.write("</span>")?;
|
||||
}
|
||||
Html(html) | InlineHtml(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>) -> Result<(), W::Error> {
|
||||
match tag {
|
||||
Tag::HtmlBlock => Ok(()),
|
||||
Tag::Paragraph => {
|
||||
if self.end_newline {
|
||||
self.write("<p>")
|
||||
} else {
|
||||
self.write("\n<p>")
|
||||
}
|
||||
}
|
||||
Tag::Heading {
|
||||
level,
|
||||
id,
|
||||
classes,
|
||||
attrs,
|
||||
} => {
|
||||
if self.end_newline {
|
||||
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("\"")?;
|
||||
}
|
||||
for (attr, value) in attrs {
|
||||
self.write(" ")?;
|
||||
escape_html(&mut self.writer, &attr)?;
|
||||
if let Some(val) = value {
|
||||
self.write("=\"")?;
|
||||
escape_html(&mut self.writer, &val)?;
|
||||
self.write("\"")?;
|
||||
} else {
|
||||
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(kind) => {
|
||||
let class_str = match kind {
|
||||
None => "",
|
||||
Some(kind) => match kind {
|
||||
BlockQuoteKind::Note => " class=\"markdown-alert-note\"",
|
||||
BlockQuoteKind::Tip => " class=\"markdown-alert-tip\"",
|
||||
BlockQuoteKind::Important => " class=\"markdown-alert-important\"",
|
||||
BlockQuoteKind::Warning => " class=\"markdown-alert-warning\"",
|
||||
BlockQuoteKind::Caution => " class=\"markdown-alert-caution\"",
|
||||
},
|
||||
};
|
||||
if self.end_newline {
|
||||
self.write(&format!("<blockquote{}>\n", class_str))
|
||||
} else {
|
||||
self.write(&format!("\n<blockquote{}>\n", class_str))
|
||||
}
|
||||
}
|
||||
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 {
|
||||
link_type: LinkType::Email,
|
||||
dest_url,
|
||||
title,
|
||||
id: _,
|
||||
} => {
|
||||
self.write("<a href=\"mailto:")?;
|
||||
escape_href(&mut self.writer, &dest_url)?;
|
||||
if !title.is_empty() {
|
||||
self.write("\" title=\"")?;
|
||||
escape_html(&mut self.writer, &title)?;
|
||||
}
|
||||
self.write("\">")
|
||||
}
|
||||
Tag::Link {
|
||||
link_type: _,
|
||||
dest_url,
|
||||
title,
|
||||
id: _,
|
||||
} => {
|
||||
self.write("<a href=\"")?;
|
||||
escape_href(&mut self.writer, &dest_url)?;
|
||||
if !title.is_empty() {
|
||||
self.write("\" title=\"")?;
|
||||
escape_html(&mut self.writer, &title)?;
|
||||
}
|
||||
self.write("\">")
|
||||
}
|
||||
Tag::Image {
|
||||
link_type: _,
|
||||
dest_url,
|
||||
title,
|
||||
id: _,
|
||||
} => {
|
||||
self.write("<img src=\"")?;
|
||||
escape_href(&mut self.writer, &dest_url)?;
|
||||
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>")
|
||||
}
|
||||
Tag::MetadataBlock(_) => {
|
||||
self.in_non_writing_block = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn end_tag(&mut self, tag: TagEnd) -> Result<(), W::Error> {
|
||||
match tag {
|
||||
TagEnd::HtmlBlock => {}
|
||||
TagEnd::Paragraph => {
|
||||
self.write("</p>\n")?;
|
||||
}
|
||||
TagEnd::Heading(level) => {
|
||||
self.write("</")?;
|
||||
write!(&mut self.writer, "{}", level)?;
|
||||
self.write(">\n")?;
|
||||
}
|
||||
TagEnd::Table => {
|
||||
self.write("</tbody></table>\n")?;
|
||||
}
|
||||
TagEnd::TableHead => {
|
||||
self.write("</tr></thead><tbody>\n")?;
|
||||
self.table_state = TableState::Body;
|
||||
}
|
||||
TagEnd::TableRow => {
|
||||
self.write("</tr>\n")?;
|
||||
}
|
||||
TagEnd::TableCell => {
|
||||
match self.table_state {
|
||||
TableState::Head => {
|
||||
self.write("</th>")?;
|
||||
}
|
||||
TableState::Body => {
|
||||
self.write("</td>")?;
|
||||
}
|
||||
}
|
||||
self.table_cell_index += 1;
|
||||
}
|
||||
TagEnd::BlockQuote => {
|
||||
self.write("</blockquote>\n")?;
|
||||
}
|
||||
TagEnd::CodeBlock => {
|
||||
self.write("</code></pre>\n")?;
|
||||
}
|
||||
TagEnd::List(true) => {
|
||||
self.write("</ol>\n")?;
|
||||
}
|
||||
TagEnd::List(false) => {
|
||||
self.write("</ul>\n")?;
|
||||
}
|
||||
TagEnd::Item => {
|
||||
self.write("</li>\n")?;
|
||||
}
|
||||
TagEnd::Emphasis => {
|
||||
self.write("</em>")?;
|
||||
}
|
||||
TagEnd::Strong => {
|
||||
self.write("</strong>")?;
|
||||
}
|
||||
TagEnd::Strikethrough => {
|
||||
self.write("</del>")?;
|
||||
}
|
||||
TagEnd::Link => {
|
||||
self.write("</a>")?;
|
||||
}
|
||||
TagEnd::Image => (), // shouldn't happen, handled in start
|
||||
TagEnd::FootnoteDefinition => {
|
||||
self.write("</div>\n")?;
|
||||
}
|
||||
TagEnd::MetadataBlock(_) => {
|
||||
self.in_non_writing_block = false;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// run raw text, consuming end tag
|
||||
fn raw_text(&mut self) -> Result<(), W::Error> {
|
||||
let mut nest = 0;
|
||||
while let Some(event) = self.iter.next() {
|
||||
match event {
|
||||
Start(_) => nest += 1,
|
||||
End(_) => {
|
||||
if nest == 0 {
|
||||
break;
|
||||
}
|
||||
nest -= 1;
|
||||
}
|
||||
Html(_) => {}
|
||||
InlineHtml(text) | Code(text) | Text(text) => {
|
||||
// Don't use escape_html_body_text here.
|
||||
// The output of this function is used in the `alt` attribute.
|
||||
escape_html(&mut self.writer, &text)?;
|
||||
self.end_newline = text.ends_with('\n');
|
||||
}
|
||||
InlineMath(text) => {
|
||||
self.write("$")?;
|
||||
escape_html(&mut self.writer, &text)?;
|
||||
self.write("$")?;
|
||||
}
|
||||
DisplayMath(text) => {
|
||||
self.write("$$")?;
|
||||
escape_html(&mut self.writer, &text)?;
|
||||
self.write("$$")?;
|
||||
}
|
||||
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>>,
|
||||
{
|
||||
write_html_fmt(s, iter).unwrap()
|
||||
}
|
||||
|
||||
/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
|
||||
/// write it out to an I/O stream.
|
||||
///
|
||||
/// **Note**: using this function with an unbuffered writer like a file or socket
|
||||
/// will result in poor performance. Wrap these in a
|
||||
/// [`BufWriter`](https://doc.rust-lang.org/std/io/struct.BufWriter.html) to
|
||||
/// prevent unnecessary slowdowns.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pulldown_cmark::{html, Parser};
|
||||
/// use std::io::Cursor;
|
||||
///
|
||||
/// let markdown_str = r#"
|
||||
/// hello
|
||||
/// =====
|
||||
///
|
||||
/// * alpha
|
||||
/// * beta
|
||||
/// "#;
|
||||
/// let mut bytes = Vec::new();
|
||||
/// let parser = Parser::new(markdown_str);
|
||||
///
|
||||
/// html::write_html_io(Cursor::new(&mut bytes), parser);
|
||||
///
|
||||
/// assert_eq!(&String::from_utf8_lossy(&bytes)[..], r#"<h1>hello</h1>
|
||||
/// <ul>
|
||||
/// <li>alpha</li>
|
||||
/// <li>beta</li>
|
||||
/// </ul>
|
||||
/// "#);
|
||||
/// ```
|
||||
pub fn write_html_io<'a, I, W>(writer: W, iter: I) -> std::io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = Event<'a>>,
|
||||
W: std::io::Write,
|
||||
{
|
||||
HtmlWriter::new(iter, IoWriter(writer)).run()
|
||||
}
|
||||
|
||||
/// Iterate over an `Iterator` of `Event`s, generate HTML for each `Event`, and
|
||||
/// write it into Unicode-accepting buffer or stream.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use pulldown_cmark::{html, Parser};
|
||||
///
|
||||
/// let markdown_str = r#"
|
||||
/// hello
|
||||
/// =====
|
||||
///
|
||||
/// * alpha
|
||||
/// * beta
|
||||
/// "#;
|
||||
/// let mut buf = String::new();
|
||||
/// let parser = Parser::new(markdown_str);
|
||||
///
|
||||
/// html::write_html_fmt(&mut buf, parser);
|
||||
///
|
||||
/// assert_eq!(buf, r#"<h1>hello</h1>
|
||||
/// <ul>
|
||||
/// <li>alpha</li>
|
||||
/// <li>beta</li>
|
||||
/// </ul>
|
||||
/// "#);
|
||||
/// ```
|
||||
pub fn write_html_fmt<'a, I, W>(writer: W, iter: I) -> std::fmt::Result
|
||||
where
|
||||
I: Iterator<Item = Event<'a>>,
|
||||
W: std::fmt::Write,
|
||||
{
|
||||
HtmlWriter::new(iter, FmtWriter(writer)).run()
|
||||
}
|
||||
Vendored
+501
@@ -0,0 +1,501 @@
|
||||
// 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.
|
||||
|
||||
//! Pull parser for [CommonMark](https://commonmark.org). This crate provides a [Parser](struct.Parser.html) struct
|
||||
//! which is an iterator over [Event](enum.Event.html)s. This iterator can be used
|
||||
//! directly, or to output HTML using the [HTML module](html/index.html).
|
||||
//!
|
||||
//! By default, only CommonMark features are enabled. To use extensions like tables,
|
||||
//! footnotes or task lists, enable them by setting the corresponding flags in the
|
||||
//! [Options](struct.Options.html) struct.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```rust
|
||||
//! use pulldown_cmark::{Parser, Options};
|
||||
//!
|
||||
//! let markdown_input = "Hello world, this is a ~~complicated~~ *very simple* example.";
|
||||
//!
|
||||
//! // Set up options and parser. Strikethroughs are not part of the CommonMark standard
|
||||
//! // and we therefore must enable it explicitly.
|
||||
//! let mut options = Options::empty();
|
||||
//! options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
//! let parser = Parser::new_ext(markdown_input, options);
|
||||
//!
|
||||
//! # #[cfg(feature = "html")] {
|
||||
//! // Write to String buffer.
|
||||
//! let mut html_output = String::new();
|
||||
//! pulldown_cmark::html::push_html(&mut html_output, parser);
|
||||
//!
|
||||
//! // Check that the output is what we expected.
|
||||
//! let expected_html = "<p>Hello world, this is a <del>complicated</del> <em>very simple</em> example.</p>\n";
|
||||
//! assert_eq!(expected_html, &html_output);
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Note that consecutive text events can happen due to the manner in which the
|
||||
//! parser evaluates the source. A utility `TextMergeStream` exists to improve
|
||||
//! the comfort of iterating the events:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pulldown_cmark::{Event, Parser, TextMergeStream};
|
||||
//!
|
||||
//! let markdown_input = "Hello world, this is a ~~complicated~~ *very simple* example.";
|
||||
//!
|
||||
//! let iterator = TextMergeStream::new(Parser::new(markdown_input));
|
||||
//!
|
||||
//! for event in iterator {
|
||||
//! match event {
|
||||
//! Event::Text(text) => println!("{}", text),
|
||||
//! _ => {}
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
// When compiled for the rustc compiler itself we want to make sure that this is
|
||||
// an unstable crate.
|
||||
#![cfg_attr(rustbuild, feature(staged_api, rustc_private))]
|
||||
#![cfg_attr(rustbuild, unstable(feature = "rustc_private", issue = "27812"))]
|
||||
// Forbid unsafe code unless the SIMD feature is enabled.
|
||||
#![cfg_attr(not(feature = "simd"), forbid(unsafe_code))]
|
||||
#![warn(missing_debug_implementations)]
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(feature = "html")]
|
||||
pub mod html;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
mod entities;
|
||||
mod firstpass;
|
||||
mod linklabel;
|
||||
mod parse;
|
||||
mod puncttable;
|
||||
mod scanners;
|
||||
mod strings;
|
||||
mod tree;
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
pub use crate::parse::{
|
||||
BrokenLink, BrokenLinkCallback, DefaultBrokenLinkCallback, OffsetIter, Parser, RefDefs,
|
||||
};
|
||||
pub use crate::strings::{CowStr, InlineStr};
|
||||
pub use crate::utils::*;
|
||||
|
||||
/// Codeblock kind.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum CodeBlockKind<'a> {
|
||||
Indented,
|
||||
/// The value contained in the tag describes the language of the code, which may be empty.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
Fenced(CowStr<'a>),
|
||||
}
|
||||
|
||||
impl<'a> CodeBlockKind<'a> {
|
||||
pub fn is_indented(&self) -> bool {
|
||||
matches!(*self, CodeBlockKind::Indented)
|
||||
}
|
||||
|
||||
pub fn is_fenced(&self) -> bool {
|
||||
matches!(*self, CodeBlockKind::Fenced(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// BlockQuote kind (Note, Tip, Important, Warning, Caution).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum BlockQuoteKind {
|
||||
Note,
|
||||
Tip,
|
||||
Important,
|
||||
Warning,
|
||||
Caution,
|
||||
}
|
||||
|
||||
/// Metadata block kind.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum MetadataBlockKind {
|
||||
YamlStyle,
|
||||
PlusesStyle,
|
||||
}
|
||||
|
||||
/// Tags for elements that can contain other elements.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum Tag<'a> {
|
||||
/// A paragraph of text and other inline elements.
|
||||
Paragraph,
|
||||
|
||||
/// A heading, with optional identifier, classes and custom attributes.
|
||||
/// The identifier is prefixed with `#` and the last one in the attributes
|
||||
/// list is chosen, classes are prefixed with `.` and custom attributes
|
||||
/// have no prefix and can optionally have a value (`myattr` or `myattr=myvalue`).
|
||||
Heading {
|
||||
level: HeadingLevel,
|
||||
id: Option<CowStr<'a>>,
|
||||
classes: Vec<CowStr<'a>>,
|
||||
/// The first item of the tuple is the attr and second one the value.
|
||||
attrs: Vec<(CowStr<'a>, Option<CowStr<'a>>)>,
|
||||
},
|
||||
|
||||
BlockQuote(Option<BlockQuoteKind>),
|
||||
/// A code block.
|
||||
CodeBlock(CodeBlockKind<'a>),
|
||||
|
||||
/// A HTML block.
|
||||
HtmlBlock,
|
||||
|
||||
/// A list. If the list is ordered the field indicates the number of the first item.
|
||||
/// Contains only list items.
|
||||
List(Option<u64>), // TODO: add delim and tight for ast (not needed for html)
|
||||
/// A list item.
|
||||
Item,
|
||||
/// A footnote definition. The value contained is the footnote's label by which it can
|
||||
/// be referred to.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
FootnoteDefinition(CowStr<'a>),
|
||||
|
||||
/// A table. Contains a vector describing the text-alignment for each of its columns.
|
||||
Table(Vec<Alignment>),
|
||||
/// A table header. Contains only `TableCell`s. Note that the table body starts immediately
|
||||
/// after the closure of the `TableHead` tag. There is no `TableBody` tag.
|
||||
TableHead,
|
||||
/// A table row. Is used both for header rows as body rows. Contains only `TableCell`s.
|
||||
TableRow,
|
||||
TableCell,
|
||||
|
||||
// span-level tags
|
||||
Emphasis,
|
||||
Strong,
|
||||
Strikethrough,
|
||||
|
||||
/// A link.
|
||||
Link {
|
||||
link_type: LinkType,
|
||||
dest_url: CowStr<'a>,
|
||||
title: CowStr<'a>,
|
||||
/// Identifier of reference links, e.g. `world` in the link `[hello][world]`.
|
||||
id: CowStr<'a>,
|
||||
},
|
||||
|
||||
/// An image. The first field is the link type, the second the destination URL and the third is a title,
|
||||
/// the fourth is the link identifier.
|
||||
Image {
|
||||
link_type: LinkType,
|
||||
dest_url: CowStr<'a>,
|
||||
title: CowStr<'a>,
|
||||
/// Identifier of reference links, e.g. `world` in the link `[hello][world]`.
|
||||
id: CowStr<'a>,
|
||||
},
|
||||
|
||||
/// A metadata block.
|
||||
MetadataBlock(MetadataBlockKind),
|
||||
}
|
||||
|
||||
impl<'a> Tag<'a> {
|
||||
pub fn to_end(&self) -> TagEnd {
|
||||
match self {
|
||||
Tag::Paragraph => TagEnd::Paragraph,
|
||||
Tag::Heading { level, .. } => TagEnd::Heading(*level),
|
||||
Tag::BlockQuote(_) => TagEnd::BlockQuote,
|
||||
Tag::CodeBlock(_) => TagEnd::CodeBlock,
|
||||
Tag::HtmlBlock => TagEnd::HtmlBlock,
|
||||
Tag::List(number) => TagEnd::List(number.is_some()),
|
||||
Tag::Item => TagEnd::Item,
|
||||
Tag::FootnoteDefinition(_) => TagEnd::FootnoteDefinition,
|
||||
Tag::Table(_) => TagEnd::Table,
|
||||
Tag::TableHead => TagEnd::TableHead,
|
||||
Tag::TableRow => TagEnd::TableRow,
|
||||
Tag::TableCell => TagEnd::TableCell,
|
||||
Tag::Emphasis => TagEnd::Emphasis,
|
||||
Tag::Strong => TagEnd::Strong,
|
||||
Tag::Strikethrough => TagEnd::Strikethrough,
|
||||
Tag::Link { .. } => TagEnd::Link,
|
||||
Tag::Image { .. } => TagEnd::Image,
|
||||
Tag::MetadataBlock(kind) => TagEnd::MetadataBlock(*kind),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The end of a `Tag`.
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum TagEnd {
|
||||
Paragraph,
|
||||
Heading(HeadingLevel),
|
||||
|
||||
BlockQuote,
|
||||
CodeBlock,
|
||||
|
||||
HtmlBlock,
|
||||
|
||||
/// A list, `true` for ordered lists.
|
||||
List(bool),
|
||||
Item,
|
||||
FootnoteDefinition,
|
||||
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
|
||||
Emphasis,
|
||||
Strong,
|
||||
Strikethrough,
|
||||
|
||||
Link,
|
||||
Image,
|
||||
|
||||
MetadataBlock(MetadataBlockKind),
|
||||
}
|
||||
|
||||
/// Make sure `TagEnd` is no more than two bytes in size.
|
||||
/// This is why it's used instead of just using `Tag`.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
const _STATIC_ASSERT_TAG_END_SIZE: [(); 2] = [(); std::mem::size_of::<TagEnd>()];
|
||||
|
||||
impl<'a> From<Tag<'a>> for TagEnd {
|
||||
fn from(value: Tag) -> Self {
|
||||
value.to_end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum HeadingLevel {
|
||||
H1 = 1,
|
||||
H2,
|
||||
H3,
|
||||
H4,
|
||||
H5,
|
||||
H6,
|
||||
}
|
||||
|
||||
impl Display for HeadingLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::H1 => write!(f, "h1"),
|
||||
Self::H2 => write!(f, "h2"),
|
||||
Self::H3 => write!(f, "h3"),
|
||||
Self::H4 => write!(f, "h4"),
|
||||
Self::H5 => write!(f, "h5"),
|
||||
Self::H6 => write!(f, "h6"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returned when trying to convert a `usize` into a `Heading` but it fails
|
||||
/// because the usize isn't a valid heading level
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct InvalidHeadingLevel(usize);
|
||||
|
||||
impl TryFrom<usize> for HeadingLevel {
|
||||
type Error = InvalidHeadingLevel;
|
||||
|
||||
fn try_from(value: usize) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
1 => Ok(Self::H1),
|
||||
2 => Ok(Self::H2),
|
||||
3 => Ok(Self::H3),
|
||||
4 => Ok(Self::H4),
|
||||
5 => Ok(Self::H5),
|
||||
6 => Ok(Self::H6),
|
||||
_ => Err(InvalidHeadingLevel(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type specifier for inline links. See [the Tag::Link](enum.Tag.html#variant.Link) for more information.
|
||||
#[derive(Clone, Debug, PartialEq, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum LinkType {
|
||||
/// Inline link like `[foo](bar)`
|
||||
Inline,
|
||||
/// Reference link like `[foo][bar]`
|
||||
Reference,
|
||||
/// Reference without destination in the document, but resolved by the broken_link_callback
|
||||
ReferenceUnknown,
|
||||
/// Collapsed link like `[foo][]`
|
||||
Collapsed,
|
||||
/// Collapsed link without destination in the document, but resolved by the broken_link_callback
|
||||
CollapsedUnknown,
|
||||
/// Shortcut link like `[foo]`
|
||||
Shortcut,
|
||||
/// Shortcut without destination in the document, but resolved by the broken_link_callback
|
||||
ShortcutUnknown,
|
||||
/// Autolink like `<http://foo.bar/baz>`
|
||||
Autolink,
|
||||
/// Email address in autolink like `<john@example.org>`
|
||||
Email,
|
||||
}
|
||||
|
||||
impl LinkType {
|
||||
/// Map the link type to an equivalent _Unknown link type.
|
||||
fn to_unknown(self) -> Self {
|
||||
match self {
|
||||
LinkType::Reference => LinkType::ReferenceUnknown,
|
||||
LinkType::Collapsed => LinkType::CollapsedUnknown,
|
||||
LinkType::Shortcut => LinkType::ShortcutUnknown,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Markdown events that are generated in a preorder traversal of the document
|
||||
/// tree, with additional `End` events whenever all of an inner node's children
|
||||
/// have been visited.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum Event<'a> {
|
||||
/// Start of a tagged element. Events that are yielded after this event
|
||||
/// and before its corresponding `End` event are inside this element.
|
||||
/// Start and end events are guaranteed to be balanced.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
Start(Tag<'a>),
|
||||
/// End of a tagged element.
|
||||
End(TagEnd),
|
||||
/// A text node.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
Text(CowStr<'a>),
|
||||
/// An inline code node.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
Code(CowStr<'a>),
|
||||
/// An inline math environment node.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
InlineMath(CowStr<'a>),
|
||||
/// A display math environment node.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
DisplayMath(CowStr<'a>),
|
||||
/// An HTML node.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
Html(CowStr<'a>),
|
||||
/// An inline HTML node.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
InlineHtml(CowStr<'a>),
|
||||
/// A reference to a footnote with given label, which may or may not be defined
|
||||
/// by an event with a `Tag::FootnoteDefinition` tag. Definitions and references to them may
|
||||
/// occur in any order.
|
||||
#[cfg_attr(feature = "serde", serde(borrow))]
|
||||
FootnoteReference(CowStr<'a>),
|
||||
/// A soft line break.
|
||||
SoftBreak,
|
||||
/// A hard line break.
|
||||
HardBreak,
|
||||
/// A horizontal ruler.
|
||||
Rule,
|
||||
/// A task list marker, rendered as a checkbox in HTML. Contains a true when it is checked.
|
||||
TaskListMarker(bool),
|
||||
}
|
||||
|
||||
/// Table column text alignment.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
|
||||
pub enum Alignment {
|
||||
/// Default text alignment.
|
||||
None,
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// Option struct containing flags for enabling extra features
|
||||
/// that are not part of the CommonMark spec.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Options: u32 {
|
||||
const ENABLE_TABLES = 1 << 1;
|
||||
/// GitHub-compatible footnote syntax.
|
||||
///
|
||||
/// Footnotes are referenced with the syntax `[^IDENT]`,
|
||||
/// and defined with an identifier followed by a colon at top level.
|
||||
///
|
||||
/// ---
|
||||
///
|
||||
/// ```markdown
|
||||
/// Footnote referenced [^1].
|
||||
///
|
||||
/// [^1]: footnote defined
|
||||
/// ```
|
||||
///
|
||||
/// Footnote referenced [^1].
|
||||
///
|
||||
/// [^1]: footnote defined
|
||||
const ENABLE_FOOTNOTES = 1 << 2;
|
||||
const ENABLE_STRIKETHROUGH = 1 << 3;
|
||||
const ENABLE_TASKLISTS = 1 << 4;
|
||||
const ENABLE_SMART_PUNCTUATION = 1 << 5;
|
||||
/// Extension to allow headings to have ID and classes.
|
||||
///
|
||||
/// `# text { #id .class1 .class2 myattr other_attr=myvalue }`
|
||||
/// is interpreted as a level 1 heading
|
||||
/// with the content `text`, ID `id`, classes `class1` and `class2` and
|
||||
/// custom attributes `myattr` (without value) and
|
||||
/// `other_attr` with value `myvalue`.
|
||||
/// Note that ID, classes, and custom attributes should be space-separated.
|
||||
const ENABLE_HEADING_ATTRIBUTES = 1 << 6;
|
||||
/// Metadata blocks in YAML style, i.e.:
|
||||
/// - starting with a `---` line
|
||||
/// - ending with a `---` or `...` line
|
||||
const ENABLE_YAML_STYLE_METADATA_BLOCKS = 1 << 7;
|
||||
/// Metadata blocks delimited by:
|
||||
/// - `+++` line at start
|
||||
/// - `+++` line at end
|
||||
const ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS = 1 << 8;
|
||||
/// Older footnote syntax. This flag implies `ENABLE_FOOTNOTES`, changing it to use an
|
||||
/// older syntax instead of the new, default, GitHub-compatible syntax.
|
||||
///
|
||||
/// New syntax is different from the old syntax regarding
|
||||
/// indentation, nesting, and footnote references with no definition:
|
||||
///
|
||||
/// ```markdown
|
||||
/// [^1]: In new syntax, this is two footnote definitions.
|
||||
/// [^2]: In old syntax, this is a single footnote definition with two lines.
|
||||
///
|
||||
/// [^3]:
|
||||
///
|
||||
/// In new syntax, this is a footnote with two paragraphs.
|
||||
///
|
||||
/// In old syntax, this is a footnote followed by a code block.
|
||||
///
|
||||
/// In new syntax, this undefined footnote definition renders as
|
||||
/// literal text [^4]. In old syntax, it creates a dangling link.
|
||||
/// ```
|
||||
const ENABLE_OLD_FOOTNOTES = (1 << 9) | (1 << 2);
|
||||
/// With this feature enabled, two events `Event::InlineMath` and `Event::DisplayMath`
|
||||
/// are emitted that conventionally contain TeX formulas.
|
||||
const ENABLE_MATH = 1 << 10;
|
||||
/// Misc GitHub Flavored Markdown features not supported in CommonMark.
|
||||
/// The following features are currently behind this tag:
|
||||
/// - Blockquote tags ([!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION]).
|
||||
const ENABLE_GFM = 1 << 11;
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
pub(crate) fn has_gfm_footnotes(&self) -> bool {
|
||||
self.contains(Options::ENABLE_FOOTNOTES) && !self.contains(Options::ENABLE_OLD_FOOTNOTES)
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
//! Link label parsing and matching.
|
||||
|
||||
use unicase::UniCase;
|
||||
|
||||
use crate::scanners::{is_ascii_whitespace, scan_eol, is_ascii_punctuation};
|
||||
use crate::strings::CowStr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum ReferenceLabel<'a> {
|
||||
Link(CowStr<'a>),
|
||||
Footnote(CowStr<'a>),
|
||||
}
|
||||
|
||||
pub(crate) type LinkLabel<'a> = UniCase<CowStr<'a>>;
|
||||
|
||||
pub(crate) type FootnoteLabel<'a> = UniCase<CowStr<'a>>;
|
||||
|
||||
/// Assumes the opening bracket has already been scanned.
|
||||
/// The line break handler determines what happens when a linebreak
|
||||
/// is found. It is passed the bytes following the line break and
|
||||
/// either returns `Some(k)`, where `k` is the number of bytes to skip,
|
||||
/// or `None` to abort parsing the label.
|
||||
/// Returns the number of bytes read (including closing bracket) and label on success.
|
||||
pub(crate) fn scan_link_label_rest<'t>(
|
||||
text: &'t str,
|
||||
linebreak_handler: &dyn Fn(&[u8]) -> Option<usize>,
|
||||
is_in_table: bool,
|
||||
) -> Option<(usize, CowStr<'t>)> {
|
||||
let bytes = text.as_bytes();
|
||||
let mut ix = 0;
|
||||
let mut only_white_space = true;
|
||||
let mut codepoints = 0;
|
||||
// no worries, doesn't allocate until we push things onto it
|
||||
let mut label = String::new();
|
||||
let mut mark = 0;
|
||||
|
||||
loop {
|
||||
if codepoints >= 1000 {
|
||||
return None;
|
||||
}
|
||||
match *bytes.get(ix)? {
|
||||
b'[' => return None,
|
||||
b']' => break,
|
||||
// Backslash escapes in link references are normally untouched, but
|
||||
// tables are an exception, because they're parsed as-if the tables
|
||||
// were parsed in a discrete pass, changing `\|` to `|`, and then
|
||||
// passing the changed string to the inline parser.
|
||||
b'|' if is_in_table && ix != 0 && bytes.get(ix - 1) == Some(&b'\\') => {
|
||||
// only way to reach this spot is to have `\\|` (even number of `\` before `|`)
|
||||
label.push_str(&text[mark..ix - 1]);
|
||||
label.push('|');
|
||||
ix += 1;
|
||||
only_white_space = false;
|
||||
mark = ix;
|
||||
}
|
||||
b'\\' if is_in_table && bytes.get(ix + 1) == Some(&b'|') => {
|
||||
// only way to reach this spot is to have `\|` (odd number of `\` before `|`)
|
||||
label.push_str(&text[mark..ix]);
|
||||
label.push('|');
|
||||
ix += 2;
|
||||
codepoints += 1;
|
||||
only_white_space = false;
|
||||
mark = ix;
|
||||
}
|
||||
b'\\' if is_ascii_punctuation(*bytes.get(ix + 1)?) => {
|
||||
ix += 2;
|
||||
codepoints += 2;
|
||||
only_white_space = false;
|
||||
}
|
||||
b if is_ascii_whitespace(b) => {
|
||||
// normalize labels by collapsing whitespaces, including linebreaks
|
||||
let mut whitespaces = 0;
|
||||
let mut linebreaks = 0;
|
||||
let whitespace_start = ix;
|
||||
|
||||
while ix < bytes.len() && is_ascii_whitespace(bytes[ix]) {
|
||||
if let Some(eol_bytes) = scan_eol(&bytes[ix..]) {
|
||||
linebreaks += 1;
|
||||
if linebreaks > 1 {
|
||||
return None;
|
||||
}
|
||||
ix += eol_bytes;
|
||||
ix += linebreak_handler(&bytes[ix..])?;
|
||||
whitespaces += 2; // indicate that we need to replace
|
||||
} else {
|
||||
whitespaces += if bytes[ix] == b' ' { 1 } else { 2 };
|
||||
ix += 1;
|
||||
}
|
||||
}
|
||||
if whitespaces > 1 {
|
||||
label.push_str(&text[mark..whitespace_start]);
|
||||
label.push(' ');
|
||||
mark = ix;
|
||||
codepoints += ix - whitespace_start;
|
||||
} else {
|
||||
codepoints += 1;
|
||||
}
|
||||
}
|
||||
b => {
|
||||
only_white_space = false;
|
||||
ix += 1;
|
||||
if b & 0b1000_0000 != 0 {
|
||||
codepoints += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if only_white_space {
|
||||
None
|
||||
} else {
|
||||
let cow = if mark == 0 {
|
||||
let asciiws = &[' ', '\r', '\n', '\t'][..];
|
||||
text[..ix].trim_matches(asciiws).into()
|
||||
} else {
|
||||
label.push_str(&text[mark..ix]);
|
||||
while matches!(label.as_bytes().last(), Some(&b' ' | &b'\r' | &b'\n' | &b'\t')) {
|
||||
label.pop();
|
||||
}
|
||||
while matches!(label.as_bytes().first(), Some(&b' ' | &b'\r' | &b'\n' | &b'\t')) {
|
||||
label.remove(0);
|
||||
}
|
||||
label.into()
|
||||
};
|
||||
Some((ix + 1, cow))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::scan_link_label_rest;
|
||||
|
||||
#[test]
|
||||
fn whitespace_normalization() {
|
||||
let input = "«\t\tBlurry Eyes\t\t»][blurry_eyes]";
|
||||
let expected_output = "« Blurry Eyes »"; // regular spaces!
|
||||
|
||||
let (_bytes, normalized_label) = scan_link_label_rest(input, &|_| None, false).unwrap();
|
||||
assert_eq!(expected_output, normalized_label.as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_carriage_linefeed_ok() {
|
||||
let input = "hello\r\nworld\r\n]";
|
||||
assert!(scan_link_label_rest(input, &|_| Some(0), false).is_some());
|
||||
}
|
||||
}
|
||||
Vendored
+195
@@ -0,0 +1,195 @@
|
||||
// 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.
|
||||
|
||||
//! Command line tool to exercise pulldown-cmark.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use pulldown_cmark::{html, BrokenLink, Options, Parser};
|
||||
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn dry_run(text: &str, opts: Options, broken_links: &mut Vec<BrokenLink<'static>>) {
|
||||
let p = Parser::new_with_broken_link_callback(
|
||||
text,
|
||||
opts,
|
||||
Some(|link: BrokenLink<'_>| {
|
||||
broken_links.push(link.into_static());
|
||||
None
|
||||
}),
|
||||
);
|
||||
let count = p.count();
|
||||
println!("{} events", count);
|
||||
}
|
||||
|
||||
fn print_events(text: &str, opts: Options, broken_links: &mut Vec<BrokenLink<'static>>) {
|
||||
let parser = Parser::new_with_broken_link_callback(
|
||||
text,
|
||||
opts,
|
||||
Some(|link: BrokenLink<'_>| {
|
||||
broken_links.push(link.into_static());
|
||||
None
|
||||
}),
|
||||
)
|
||||
.into_offset_iter();
|
||||
for (event, range) in parser {
|
||||
println!("{:?}: {:?}", range, event);
|
||||
}
|
||||
println!("EOF");
|
||||
}
|
||||
|
||||
fn brief(program: &str) -> String {
|
||||
format!(
|
||||
"Usage: {} [options]\n\n{}",
|
||||
program, "Reads markdown from file or standard input and emits HTML.",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn main() -> std::io::Result<()> {
|
||||
let args: Vec<_> = env::args().collect();
|
||||
let mut opts = getopts::Options::new();
|
||||
opts.optflag("h", "help", "this help message");
|
||||
opts.optflag("d", "dry-run", "dry run, produce no output");
|
||||
opts.optflag("e", "events", "print event sequence instead of rendering");
|
||||
opts.optflag("T", "enable-tables", "enable GitHub-style tables");
|
||||
opts.optflag("m", "enable-math", "enable LaTeX-style math");
|
||||
opts.optflag("F", "enable-footnotes", "enable GitHub-style footnotes");
|
||||
opts.optflag("", "enable-old-footnotes", "enable Hoedown-style footnotes");
|
||||
opts.optflag(
|
||||
"S",
|
||||
"enable-strikethrough",
|
||||
"enable GitHub-style strikethrough",
|
||||
);
|
||||
opts.optflag("L", "enable-tasklists", "enable GitHub-style task lists");
|
||||
opts.optflag("P", "enable-smart-punctuation", "enable smart punctuation");
|
||||
opts.optflag(
|
||||
"H",
|
||||
"enable-heading-attributes",
|
||||
"enable heading attributes",
|
||||
);
|
||||
opts.optflag("M", "enable-metadata-blocks", "enable metadata blocks");
|
||||
opts.optflag(
|
||||
"R",
|
||||
"reject-broken-links",
|
||||
"fail if input file has broken links",
|
||||
);
|
||||
opts.optflag("G", "enable-gfm", "enable misc GFM features");
|
||||
|
||||
let matches = match opts.parse(&args[1..]) {
|
||||
Ok(m) => m,
|
||||
Err(f) => {
|
||||
eprintln!("{}\n{}", f, opts.usage(&brief(&args[0])));
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
if matches.opt_present("help") {
|
||||
println!("{}", opts.usage(&brief(&args[0])));
|
||||
return Ok(());
|
||||
}
|
||||
let mut opts = Options::empty();
|
||||
if matches.opt_present("enable-tables") {
|
||||
opts.insert(Options::ENABLE_TABLES);
|
||||
}
|
||||
if matches.opt_present("enable-math") {
|
||||
opts.insert(Options::ENABLE_MATH);
|
||||
}
|
||||
if matches.opt_present("enable-footnotes") {
|
||||
opts.insert(Options::ENABLE_FOOTNOTES);
|
||||
}
|
||||
if matches.opt_present("enable-old-footnotes") {
|
||||
opts.insert(Options::ENABLE_OLD_FOOTNOTES);
|
||||
}
|
||||
if matches.opt_present("enable-strikethrough") {
|
||||
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
}
|
||||
if matches.opt_present("enable-tasklists") {
|
||||
opts.insert(Options::ENABLE_TASKLISTS);
|
||||
}
|
||||
if matches.opt_present("enable-smart-punctuation") {
|
||||
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||
}
|
||||
if matches.opt_present("enable-heading-attributes") {
|
||||
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
||||
}
|
||||
if matches.opt_present("enable-metadata-blocks") {
|
||||
opts.insert(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
|
||||
opts.insert(Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS);
|
||||
}
|
||||
if matches.opt_present("enable-gfm") {
|
||||
opts.insert(Options::ENABLE_GFM);
|
||||
}
|
||||
|
||||
let mut input = String::new();
|
||||
let mut broken_links = vec![];
|
||||
if !&matches.free.is_empty() {
|
||||
for filename in &matches.free {
|
||||
let real_path = PathBuf::from(filename);
|
||||
let mut f = File::open(&real_path).expect("file not found");
|
||||
f.read_to_string(&mut input)
|
||||
.expect("something went wrong reading the file");
|
||||
if matches.opt_present("events") {
|
||||
print_events(&input, opts, &mut broken_links);
|
||||
} else if matches.opt_present("dry-run") {
|
||||
dry_run(&input, opts, &mut broken_links);
|
||||
} else {
|
||||
pulldown_cmark(&input, opts, &mut broken_links);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = io::stdin().lock().read_to_string(&mut input);
|
||||
if matches.opt_present("events") {
|
||||
print_events(&input, opts, &mut broken_links);
|
||||
} else if matches.opt_present("dry-run") {
|
||||
dry_run(&input, opts, &mut broken_links);
|
||||
} else {
|
||||
pulldown_cmark(&input, opts, &mut broken_links);
|
||||
}
|
||||
}
|
||||
|
||||
if matches.opt_present("reject-broken-links") && !broken_links.is_empty() {
|
||||
eprintln!("Error: {} broken links:", broken_links.len());
|
||||
for link in broken_links {
|
||||
let start = link.span.start;
|
||||
let end = link.span.end;
|
||||
let reference = link.reference;
|
||||
eprintln!("[{start}-{end}]: {reference}");
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pulldown_cmark(input: &str, opts: Options, broken_links: &mut Vec<BrokenLink<'static>>) {
|
||||
let mut p = Parser::new_with_broken_link_callback(
|
||||
input,
|
||||
opts,
|
||||
Some(|link: BrokenLink<'_>| {
|
||||
broken_links.push(link.into_static());
|
||||
None
|
||||
}),
|
||||
);
|
||||
let stdio = io::stdout();
|
||||
let buffer = std::io::BufWriter::with_capacity(1024 * 1024, stdio.lock());
|
||||
let _ = html::write_html_io(buffer, &mut p);
|
||||
}
|
||||
Vendored
+2605
File diff suppressed because it is too large.
Load diff
+1541
File diff suppressed because it is too large.
Load diff
+1522
File diff suppressed because it is too large.
Load diff
Vendored
+389
@@ -0,0 +1,389 @@
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
use std::str::from_utf8;
|
||||
|
||||
const MAX_INLINE_STR_LEN: usize = 3 * std::mem::size_of::<isize>() - 2;
|
||||
|
||||
/// Returned when trying to convert a `&str` into a `InlineStr`
|
||||
/// but it fails because it doesn't fit.
|
||||
#[derive(Debug)]
|
||||
pub struct StringTooLongError;
|
||||
|
||||
/// An inline string that can contain almost three words
|
||||
/// of utf-8 text.
|
||||
#[derive(Debug, Clone, Copy, Eq)]
|
||||
pub struct InlineStr {
|
||||
inner: [u8; MAX_INLINE_STR_LEN],
|
||||
len: u8,
|
||||
}
|
||||
|
||||
impl AsRef<str> for InlineStr {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for InlineStr {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.deref().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl From<char> for InlineStr {
|
||||
fn from(c: char) -> Self {
|
||||
let mut inner = [0u8; MAX_INLINE_STR_LEN];
|
||||
c.encode_utf8(&mut inner);
|
||||
let len = c.len_utf8() as u8;
|
||||
Self { inner, len }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::cmp::PartialEq<InlineStr> for InlineStr {
|
||||
fn eq(&self, other: &InlineStr) -> bool {
|
||||
self.deref() == other.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for InlineStr {
|
||||
type Error = StringTooLongError;
|
||||
|
||||
fn try_from(s: &str) -> Result<InlineStr, StringTooLongError> {
|
||||
let len = s.len();
|
||||
if len <= MAX_INLINE_STR_LEN {
|
||||
let mut inner = [0u8; MAX_INLINE_STR_LEN];
|
||||
inner[..len].copy_from_slice(s.as_bytes());
|
||||
let len = len as u8;
|
||||
Ok(Self { inner, len })
|
||||
} else {
|
||||
Err(StringTooLongError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for InlineStr {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &str {
|
||||
let len = self.len as usize;
|
||||
from_utf8(&self.inner[..len]).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InlineStr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// A copy-on-write string that can be owned, borrowed
|
||||
/// or inlined.
|
||||
///
|
||||
/// It is three words long.
|
||||
#[derive(Debug, Eq)]
|
||||
pub enum CowStr<'a> {
|
||||
/// An owned, immutable string.
|
||||
Boxed(Box<str>),
|
||||
/// A borrowed string.
|
||||
Borrowed(&'a str),
|
||||
/// A short inline string.
|
||||
Inlined(InlineStr),
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_impl {
|
||||
use super::CowStr;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
|
||||
impl<'a> Serialize for CowStr<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
struct CowStrVisitor;
|
||||
|
||||
impl<'de> de::Visitor<'de> for CowStrVisitor {
|
||||
type Value = CowStr<'de>;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a string")
|
||||
}
|
||||
|
||||
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(CowStr::Borrowed(v))
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match v.try_into() {
|
||||
Ok(it) => Ok(CowStr::Inlined(it)),
|
||||
Err(_) => Ok(CowStr::Boxed(String::from(v).into_boxed_str())),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
Ok(CowStr::Boxed(v.into_boxed_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'de: 'a> Deserialize<'de> for CowStr<'a> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(CowStrVisitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<str> for CowStr<'a> {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Hash for CowStr<'a> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.deref().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::clone::Clone for CowStr<'a> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
CowStr::Boxed(s) => match InlineStr::try_from(&**s) {
|
||||
Ok(inline) => CowStr::Inlined(inline),
|
||||
Err(..) => CowStr::Boxed(s.clone()),
|
||||
},
|
||||
CowStr::Borrowed(s) => CowStr::Borrowed(s),
|
||||
CowStr::Inlined(s) => CowStr::Inlined(*s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::cmp::PartialEq<CowStr<'a>> for CowStr<'a> {
|
||||
fn eq(&self, other: &CowStr<'_>) -> bool {
|
||||
self.deref() == other.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for CowStr<'a> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
CowStr::Borrowed(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for CowStr<'a> {
|
||||
fn from(s: String) -> Self {
|
||||
CowStr::Boxed(s.into_boxed_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<char> for CowStr<'a> {
|
||||
fn from(c: char) -> Self {
|
||||
CowStr::Inlined(c.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for CowStr<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Self {
|
||||
match s {
|
||||
Cow::Borrowed(s) => CowStr::Borrowed(s),
|
||||
Cow::Owned(s) => CowStr::Boxed(s.into_boxed_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<CowStr<'a>> for Cow<'a, str> {
|
||||
fn from(s: CowStr<'a>) -> Self {
|
||||
match s {
|
||||
CowStr::Boxed(s) => Cow::Owned(s.to_string()),
|
||||
CowStr::Inlined(s) => Cow::Owned(s.to_string()),
|
||||
CowStr::Borrowed(s) => Cow::Borrowed(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, char>> for CowStr<'a> {
|
||||
fn from(s: Cow<'a, char>) -> Self {
|
||||
CowStr::Inlined(InlineStr::from(*s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for CowStr<'a> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &str {
|
||||
match self {
|
||||
CowStr::Boxed(ref b) => b,
|
||||
CowStr::Borrowed(b) => b,
|
||||
CowStr::Inlined(ref s) => s.deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Borrow<str> for CowStr<'a> {
|
||||
fn borrow(&self) -> &str {
|
||||
self.deref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CowStr<'a> {
|
||||
pub fn into_string(self) -> String {
|
||||
match self {
|
||||
CowStr::Boxed(b) => b.into(),
|
||||
CowStr::Borrowed(b) => b.to_owned(),
|
||||
CowStr::Inlined(s) => s.deref().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for CowStr<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_special_string {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn inlinestr_ascii() {
|
||||
let s: InlineStr = 'a'.into();
|
||||
assert_eq!("a", s.deref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inlinestr_unicode() {
|
||||
let s: InlineStr = '🍔'.into();
|
||||
assert_eq!("🍔", s.deref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cowstr_size() {
|
||||
let size = std::mem::size_of::<CowStr>();
|
||||
let word_size = std::mem::size_of::<isize>();
|
||||
assert_eq!(3 * word_size, size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cowstr_char_to_string() {
|
||||
let c = '藏';
|
||||
let smort: CowStr = c.into();
|
||||
let owned: String = smort.to_string();
|
||||
let expected = "藏".to_owned();
|
||||
assert_eq!(expected, owned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_inline_str_len_atleast_four() {
|
||||
// we need 4 bytes to store a char
|
||||
assert!(MAX_INLINE_STR_LEN >= 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn inlinestr_fits_twentytwo() {
|
||||
let s = "0123456789abcdefghijkl";
|
||||
let stack_str = InlineStr::try_from(s).unwrap();
|
||||
assert_eq!(stack_str.deref(), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn inlinestr_not_fits_twentythree() {
|
||||
let s = "0123456789abcdefghijklm";
|
||||
let _stack_str = InlineStr::try_from(s).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
fn small_boxed_str_clones_to_stack() {
|
||||
let s = "0123456789abcde".to_owned();
|
||||
let smort: CowStr = s.into();
|
||||
let smort_clone = smort.clone();
|
||||
|
||||
if let CowStr::Inlined(..) = smort_clone {
|
||||
} else {
|
||||
panic!("Expected a Inlined variant!");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cow_to_cow_str() {
|
||||
let s = "some text";
|
||||
let cow = Cow::Borrowed(s);
|
||||
let actual = CowStr::from(cow);
|
||||
let expected = CowStr::Borrowed(s);
|
||||
assert_eq!(actual, expected);
|
||||
assert!(variant_eq(&actual, &expected));
|
||||
|
||||
let s = "some text".to_string();
|
||||
let cow: Cow<str> = Cow::Owned(s.clone());
|
||||
let actual = CowStr::from(cow);
|
||||
let expected = CowStr::Boxed(s.into_boxed_str());
|
||||
assert_eq!(actual, expected);
|
||||
assert!(variant_eq(&actual, &expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cow_str_to_cow() {
|
||||
let s = "some text";
|
||||
let cow_str = CowStr::Borrowed(s);
|
||||
let actual = Cow::from(cow_str);
|
||||
let expected = Cow::Borrowed(s);
|
||||
assert_eq!(actual, expected);
|
||||
assert!(variant_eq(&actual, &expected));
|
||||
|
||||
let s = "s";
|
||||
let inline_str: InlineStr = InlineStr::try_from(s).unwrap();
|
||||
let cow_str = CowStr::Inlined(inline_str);
|
||||
let actual = Cow::from(cow_str);
|
||||
let expected: Cow<str> = Cow::Owned(s.to_string());
|
||||
assert_eq!(actual, expected);
|
||||
assert!(variant_eq(&actual, &expected));
|
||||
|
||||
let s = "s";
|
||||
let cow_str = CowStr::Boxed(s.to_string().into_boxed_str());
|
||||
let actual = Cow::from(cow_str);
|
||||
let expected: Cow<str> = Cow::Owned(s.to_string());
|
||||
assert_eq!(actual, expected);
|
||||
assert!(variant_eq(&actual, &expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cow_char_to_cow_str() {
|
||||
let c = 'c';
|
||||
let cow: Cow<char> = Cow::Owned(c);
|
||||
let actual = CowStr::from(cow);
|
||||
let expected = CowStr::Inlined(InlineStr::from(c));
|
||||
assert_eq!(actual, expected);
|
||||
assert!(variant_eq(&actual, &expected));
|
||||
|
||||
let c = 'c';
|
||||
let cow: Cow<char> = Cow::Borrowed(&c);
|
||||
let actual = CowStr::from(cow);
|
||||
let expected = CowStr::Inlined(InlineStr::from(c));
|
||||
assert_eq!(actual, expected);
|
||||
assert!(variant_eq(&actual, &expected));
|
||||
}
|
||||
|
||||
fn variant_eq<T>(a: &T, b: &T) -> bool {
|
||||
std::mem::discriminant(a) == std::mem::discriminant(b)
|
||||
}
|
||||
}
|
||||
Vendored
+285
@@ -0,0 +1,285 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
//! A Vec-based container for a tree structure.
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::{Add, Sub};
|
||||
|
||||
use crate::parse::{Item, ItemBody};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, PartialOrd)]
|
||||
pub(crate) struct TreeIndex(NonZeroUsize);
|
||||
|
||||
impl TreeIndex {
|
||||
fn new(i: usize) -> Self {
|
||||
TreeIndex(NonZeroUsize::new(i).unwrap())
|
||||
}
|
||||
|
||||
pub fn get(self) -> usize {
|
||||
self.0.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<usize> for TreeIndex {
|
||||
type Output = TreeIndex;
|
||||
|
||||
fn add(self, rhs: usize) -> Self {
|
||||
let inner = self.0.get() + rhs;
|
||||
TreeIndex::new(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<usize> for TreeIndex {
|
||||
type Output = TreeIndex;
|
||||
|
||||
fn sub(self, rhs: usize) -> Self {
|
||||
let inner = self.0.get().checked_sub(rhs).unwrap();
|
||||
TreeIndex::new(inner)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Node<T> {
|
||||
pub child: Option<TreeIndex>,
|
||||
pub next: Option<TreeIndex>,
|
||||
pub item: T,
|
||||
}
|
||||
|
||||
/// A tree abstraction, intended for fast building as a preorder traversal.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Tree<T> {
|
||||
nodes: Vec<Node<T>>,
|
||||
spine: Vec<TreeIndex>, // indices of nodes on path to current node
|
||||
cur: Option<TreeIndex>,
|
||||
}
|
||||
|
||||
impl<T: Default> Tree<T> {
|
||||
// Indices start at one, so we place a dummy value at index zero.
|
||||
// The alternative would be subtracting one from every TreeIndex
|
||||
// every time we convert it to usize to index our nodes.
|
||||
pub(crate) fn with_capacity(cap: usize) -> Tree<T> {
|
||||
let mut nodes = Vec::with_capacity(cap);
|
||||
nodes.push(Node {
|
||||
child: None,
|
||||
next: None,
|
||||
item: <T as Default>::default(),
|
||||
});
|
||||
Tree {
|
||||
nodes,
|
||||
spine: Vec::new(),
|
||||
cur: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the index of the element currently in focus.
|
||||
pub(crate) fn cur(&self) -> Option<TreeIndex> {
|
||||
self.cur
|
||||
}
|
||||
|
||||
/// Append one item to the current position in the tree.
|
||||
pub(crate) fn append(&mut self, item: T) -> TreeIndex {
|
||||
let ix = self.create_node(item);
|
||||
let this = Some(ix);
|
||||
|
||||
if let Some(ix) = self.cur {
|
||||
self[ix].next = this;
|
||||
} else if let Some(&parent) = self.spine.last() {
|
||||
self[parent].child = this;
|
||||
}
|
||||
self.cur = this;
|
||||
ix
|
||||
}
|
||||
|
||||
/// Create an isolated node.
|
||||
pub(crate) fn create_node(&mut self, item: T) -> TreeIndex {
|
||||
let this = self.nodes.len();
|
||||
self.nodes.push(Node {
|
||||
child: None,
|
||||
next: None,
|
||||
item,
|
||||
});
|
||||
TreeIndex::new(this)
|
||||
}
|
||||
|
||||
/// Push down one level, so that new items become children of the current node.
|
||||
/// The new focus index is returned.
|
||||
pub(crate) fn push(&mut self) -> TreeIndex {
|
||||
let cur_ix = self.cur.unwrap();
|
||||
self.spine.push(cur_ix);
|
||||
self.cur = self[cur_ix].child;
|
||||
cur_ix
|
||||
}
|
||||
|
||||
/// Pop back up a level.
|
||||
pub(crate) fn pop(&mut self) -> Option<TreeIndex> {
|
||||
let ix = Some(self.spine.pop()?);
|
||||
self.cur = ix;
|
||||
ix
|
||||
}
|
||||
|
||||
/// Remove the last node, as `pop` but removing it.
|
||||
pub(crate) fn remove_node(&mut self) -> Option<TreeIndex> {
|
||||
let ix = self.spine.pop()?;
|
||||
self.cur = Some(ix);
|
||||
self.nodes.pop()?;
|
||||
self[ix].child = None;
|
||||
Some(ix)
|
||||
}
|
||||
|
||||
/// Look at the parent node.
|
||||
pub(crate) fn peek_up(&self) -> Option<TreeIndex> {
|
||||
self.spine.last().copied()
|
||||
}
|
||||
|
||||
/// Look at grandparent node.
|
||||
pub(crate) fn peek_grandparent(&self) -> Option<TreeIndex> {
|
||||
if self.spine.len() >= 2 {
|
||||
Some(self.spine[self.spine.len() - 2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when there are no nodes other than the root node
|
||||
/// in the tree, false otherwise.
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.nodes.len() <= 1
|
||||
}
|
||||
|
||||
/// Returns the length of the spine.
|
||||
pub(crate) fn spine_len(&self) -> usize {
|
||||
self.spine.len()
|
||||
}
|
||||
|
||||
/// Resets the focus to the first node added to the tree, if it exists.
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.cur = if self.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(TreeIndex::new(1))
|
||||
};
|
||||
self.spine.clear();
|
||||
}
|
||||
|
||||
/// Walks the spine from a root node up to, but not including, the current node.
|
||||
pub(crate) fn walk_spine(&self) -> impl std::iter::DoubleEndedIterator<Item = &TreeIndex> {
|
||||
self.spine.iter()
|
||||
}
|
||||
|
||||
/// Moves focus to the next sibling of the given node.
|
||||
pub(crate) fn next_sibling(&mut self, cur_ix: TreeIndex) -> Option<TreeIndex> {
|
||||
self.cur = self[cur_ix].next;
|
||||
self.cur
|
||||
}
|
||||
}
|
||||
|
||||
impl Tree<Item> {
|
||||
/// Truncates the preceding siblings to the given end position,
|
||||
/// and returns the new current node.
|
||||
pub(crate) fn truncate_siblings(&mut self, end_byte_ix: usize) {
|
||||
let parent_ix = self.peek_up().unwrap();
|
||||
let mut next_child_ix = self[parent_ix].child;
|
||||
let mut prev_child_ix = None;
|
||||
|
||||
// drop or truncate children based on its range
|
||||
while let Some(child_ix) = next_child_ix {
|
||||
let child_end = self[child_ix].item.end;
|
||||
if child_end < end_byte_ix {
|
||||
// preserve this node, and go to the next
|
||||
prev_child_ix = Some(child_ix);
|
||||
next_child_ix = self[child_ix].next;
|
||||
continue;
|
||||
} else if child_end == end_byte_ix {
|
||||
// this will be the last node
|
||||
self[child_ix].next = None;
|
||||
// focus to the new last child (this node)
|
||||
self.cur = Some(child_ix);
|
||||
} else if self[child_ix].item.start == end_byte_ix {
|
||||
// check whether the previous character is a backslash
|
||||
let is_previous_char_backslash_escape = match self[child_ix].item.body {
|
||||
ItemBody::Text { backslash_escaped } => backslash_escaped,
|
||||
_ => false,
|
||||
};
|
||||
if is_previous_char_backslash_escape {
|
||||
// rescue the backslash as a plain text content
|
||||
let last_byte_ix = end_byte_ix - 1;
|
||||
self[child_ix].item.start = last_byte_ix;
|
||||
self[child_ix].item.end = end_byte_ix;
|
||||
self.cur = Some(child_ix);
|
||||
} else if let Some(prev_child_ix) = prev_child_ix {
|
||||
// the node will become empty. drop the node
|
||||
// a preceding sibling exists
|
||||
self[prev_child_ix].next = None;
|
||||
self.cur = Some(prev_child_ix);
|
||||
} else {
|
||||
// no preceding siblings. remove the node from the parent
|
||||
self[parent_ix].child = None;
|
||||
self.cur = None;
|
||||
}
|
||||
} else {
|
||||
debug_assert!(self[child_ix].item.start < end_byte_ix);
|
||||
debug_assert!(end_byte_ix < child_end);
|
||||
// truncate the node
|
||||
self[child_ix].item.end = end_byte_ix;
|
||||
self[child_ix].next = None;
|
||||
// focus to the new last child
|
||||
self.cur = Some(child_ix);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for Tree<T>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fn debug_tree<T>(
|
||||
tree: &Tree<T>,
|
||||
cur: TreeIndex,
|
||||
indent: usize,
|
||||
f: &mut std::fmt::Formatter<'_>,
|
||||
) -> std::fmt::Result
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
for _ in 0..indent {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
writeln!(f, "{:?}", &tree[cur].item)?;
|
||||
if let Some(child_ix) = tree[cur].child {
|
||||
debug_tree(tree, child_ix, indent + 1, f)?;
|
||||
}
|
||||
if let Some(next_ix) = tree[cur].next {
|
||||
debug_tree(tree, next_ix, indent, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if self.nodes.len() > 1 {
|
||||
let cur = TreeIndex(NonZeroUsize::new(1).unwrap());
|
||||
debug_tree(self, cur, 0, f)
|
||||
} else {
|
||||
write!(f, "Empty tree")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Index<TreeIndex> for Tree<T> {
|
||||
type Output = Node<T>;
|
||||
|
||||
fn index(&self, ix: TreeIndex) -> &Self::Output {
|
||||
self.nodes.index(ix.get())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::IndexMut<TreeIndex> for Tree<T> {
|
||||
fn index_mut(&mut self, ix: TreeIndex) -> &mut Node<T> {
|
||||
self.nodes.index_mut(ix.get())
|
||||
}
|
||||
}
|
||||
Vendored
+150
@@ -0,0 +1,150 @@
|
||||
//! Miscellaneous utilities to increase comfort.
|
||||
//! Special thanks to:
|
||||
//!
|
||||
//! - <https://github.com/BenjaminRi/Redwood-Wiki/blob/master/src/markdown_utils.rs>.
|
||||
//! Its author authorized the use of this GPL code in this project in
|
||||
//! <https://github.com/raphlinus/pulldown-cmark/issues/507>.
|
||||
//!
|
||||
//! - <https://gist.github.com/rambip/a507c312ed61c99c24b2a54f98325721>.
|
||||
//! Its author proposed the solution in
|
||||
//! <https://github.com/raphlinus/pulldown-cmark/issues/708>.
|
||||
|
||||
use crate::{
|
||||
BrokenLinkCallback, CowStr, DefaultBrokenLinkCallback, Event, OffsetIter, Options, Parser,
|
||||
};
|
||||
use std::{iter::Peekable, ops::Range};
|
||||
|
||||
/// Merge consecutive `Event::Text` events into only one.
|
||||
#[derive(Debug)]
|
||||
pub struct TextMergeStream<'a, I> {
|
||||
iter: I,
|
||||
last_event: Option<Event<'a>>,
|
||||
}
|
||||
|
||||
impl<'a, I> TextMergeStream<'a, I>
|
||||
where
|
||||
I: Iterator<Item = Event<'a>>,
|
||||
{
|
||||
pub fn new(iter: I) -> Self {
|
||||
Self {
|
||||
iter,
|
||||
last_event: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, I> Iterator for TextMergeStream<'a, I>
|
||||
where
|
||||
I: Iterator<Item = Event<'a>>,
|
||||
{
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.last_event.take(), self.iter.next()) {
|
||||
(Some(Event::Text(last_text)), Some(Event::Text(next_text))) => {
|
||||
// We need to start merging consecutive text events together into one
|
||||
let mut string_buf: String = last_text.into_string();
|
||||
string_buf.push_str(&next_text);
|
||||
loop {
|
||||
// Avoid recursion to avoid stack overflow and to optimize concatenation
|
||||
match self.iter.next() {
|
||||
Some(Event::Text(next_text)) => {
|
||||
string_buf.push_str(&next_text);
|
||||
}
|
||||
next_event => {
|
||||
self.last_event = next_event;
|
||||
if string_buf.is_empty() {
|
||||
// Discard text event(s) altogether if there is no text
|
||||
break self.next();
|
||||
} else {
|
||||
break Some(Event::Text(CowStr::Boxed(
|
||||
string_buf.into_boxed_str(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(next_event)) => {
|
||||
// This only happens once during the first iteration and if there are items
|
||||
self.last_event = Some(next_event);
|
||||
self.next()
|
||||
}
|
||||
(None, None) => {
|
||||
// This happens when the iterator is depleted
|
||||
None
|
||||
}
|
||||
(last_event, next_event) => {
|
||||
// The ordinary case, emit one event after the other without modification
|
||||
self.last_event = next_event;
|
||||
last_event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge consecutive `Event::Text` events into only one with offsets.
|
||||
#[derive(Debug)]
|
||||
pub struct TextMergeWithOffset<'input, F = DefaultBrokenLinkCallback>
|
||||
where
|
||||
F: BrokenLinkCallback<'input>,
|
||||
{
|
||||
source: &'input str,
|
||||
parser: Peekable<OffsetIter<'input, F>>,
|
||||
}
|
||||
|
||||
impl<'input, F> TextMergeWithOffset<'input, F>
|
||||
where
|
||||
F: BrokenLinkCallback<'input>,
|
||||
{
|
||||
pub fn new_ext(source: &'input str, options: Options) -> Self {
|
||||
Self {
|
||||
source,
|
||||
parser: Parser::new_with_broken_link_callback(source, options, None)
|
||||
.into_offset_iter()
|
||||
.peekable(),
|
||||
}
|
||||
}
|
||||
pub fn new_ext_with_broken_link_callback(
|
||||
source: &'input str,
|
||||
options: Options,
|
||||
callback: Option<F>,
|
||||
) -> Self {
|
||||
Self {
|
||||
source,
|
||||
parser: Parser::new_with_broken_link_callback(source, options, callback)
|
||||
.into_offset_iter()
|
||||
.peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'input, F> Iterator for TextMergeWithOffset<'input, F>
|
||||
where
|
||||
F: BrokenLinkCallback<'input>,
|
||||
{
|
||||
type Item = (Event<'input>, Range<usize>);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let is_empty_text = |x: Option<&(Event<'input>, Range<usize>)>| match x {
|
||||
Some(e) => matches!(&e.0, Event::Text(t) if t.is_empty()),
|
||||
None => false,
|
||||
};
|
||||
|
||||
while is_empty_text(self.parser.peek()) {
|
||||
self.parser.next();
|
||||
}
|
||||
|
||||
match self.parser.peek()? {
|
||||
(Event::Text(_), range) => {
|
||||
let start = range.start;
|
||||
let mut end = range.end;
|
||||
while let Some((Event::Text(_), _)) = self.parser.peek() {
|
||||
end = self.parser.next().unwrap().1.end;
|
||||
}
|
||||
|
||||
Some((Event::Text(self.source[start..end].into()), start..end))
|
||||
}
|
||||
_ => self.parser.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user