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:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+593
View File
@@ -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()
}
+501
View File
@@ -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
View File
@@ -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());
}
}
+195
View File
@@ -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);
}
+2605
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+389
View File
@@ -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)
}
}
+285
View File
@@ -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())
}
}
+150
View File
@@ -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(),
}
}
}