Something went wrong. Try again.
Markdown parser fork with extended syntax for personal use.
Something went wrong. Try again.
6.5 kB · 251 lines
Rust
at hack
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252//! Deal with bytes, chars, and kinds.
use crate::util::unicode::PUNCTUATION;use alloc::{format, string::String};use core::str;
/// Character kinds.#[derive(Debug, PartialEq, Eq)]pub enum Kind { /// Whitespace. /// /// ## Example /// /// ```markdown /// > | **a_b_ c**. /// ^ ^ ^ /// ``` Whitespace, /// Punctuation. /// /// ## Example /// /// ```markdown /// > | **a_b_ c**. /// ^^ ^ ^ ^ /// ``` Punctuation, /// Everything else. /// /// ## Example /// /// ```markdown /// > | **a_b_ c**. /// ^ ^ ^ /// ``` Other,}
/// Get a [`char`][] right before `index` in bytes (`&[u8]`).////// In most cases, markdown operates on ASCII bytes./// In a few cases, it is unicode aware, so we need to find an actual char.pub fn before_index(bytes: &[u8], index: usize) -> Option<char> { let start = if index < 4 { 0 } else { index - 4 }; String::from_utf8_lossy(&bytes[start..index]).chars().last()}
/// Get a [`char`][] right at `index` in bytes (`&[u8]`).////// In most cases, markdown operates on ASCII bytes./// In a few cases, it is unicode aware, so we need to find an actual char.pub fn after_index(bytes: &[u8], index: usize) -> Option<char> { let end = if index + 4 > bytes.len() { bytes.len() } else { index + 4 }; String::from_utf8_lossy(&bytes[index..end]).chars().next()}
/// Classify a char at `index` in bytes (`&[u8]`).pub fn kind_after_index(bytes: &[u8], index: usize) -> Kind { if index == bytes.len() { Kind::Whitespace } else { let byte = bytes[index]; if byte.is_ascii_whitespace() { Kind::Whitespace } else if byte.is_ascii_punctuation() { Kind::Punctuation } else if byte.is_ascii_alphanumeric() { Kind::Other } else { // Otherwise: seems to be an ASCII control, so it seems to be a // non-ASCII `char`. classify_opt(after_index(bytes, index)) } }}
/// Classify whether a `char` represents whitespace, punctuation, or something/// else.////// Used for attention (emphasis, strong), whose sequences can open or close/// based on the class of surrounding characters.////// ## References////// * [`micromark-util-classify-character` in `micromark`](https://github.com/micromark/micromark/blob/main/packages/micromark-util-classify-character/dev/index.js)pub fn classify(char: char) -> Kind { // Unicode whitespace. if char.is_whitespace() { Kind::Whitespace } // Unicode punctuation. else if char.is_ascii_punctuation() || PUNCTUATION.contains(&char) { Kind::Punctuation } // Everything else. else { Kind::Other }}
/// Like [`classify`], but supports eof as whitespace.pub fn classify_opt(char_opt: Option<char>) -> Kind { char_opt.map_or(Kind::Whitespace, classify)}
/// Format an optional `char` (`none` means eof).pub fn format_opt(char: Option<char>) -> String { char.map_or("end of file".into(), |char| { format!("character {}", format(char)) })}
/// Format an optional `byte` (`none` means eof).#[cfg(feature = "log")]pub fn format_byte_opt(byte: Option<u8>) -> String { byte.map_or("end of file".into(), |byte| { format!("byte {}", format_byte(byte)) })}
/// Format a `char`.pub fn format(char: char) -> String { let representation = format!("U+{:>04X}", char as u32); let printable = match char { '`' => Some("`` ` ``".into()), '!'..='~' => Some(format!("`{}`", char)), _ => None, };
if let Some(char) = printable { format!("{} ({})", char, representation) } else { representation }}
/// Format a byte (`u8`).pub fn format_byte(byte: u8) -> String { let representation = format!("U+{:>04X}", byte); let printable = match byte { b'`' => Some("`` ` ``".into()), b'!'..=b'~' => Some(format!("`{}`", str::from_utf8(&[byte]).unwrap())), _ => None, };
if let Some(char) = printable { format!("{} ({})", char, representation) } else { representation }}
#[cfg(test)]mod tests { use super::*; use alloc::string::ToString;
#[test] fn test_classify() { assert_eq!( classify(' '), Kind::Whitespace, "should classify whitespace" );
assert_eq!( classify('.'), Kind::Punctuation, "should classify punctuation" );
assert_eq!(classify('a'), Kind::Other, "should classify other"); }
#[test] fn test_format_opt() { assert_eq!( format_opt(None), "end of file".to_string(), "should format an optional char: none -> eof" );
assert_eq!( format_opt(Some('!')), "character `!` (U+0021)".to_string(), "should format an optional char: char -> pretty" ); }
#[test] #[cfg(feature = "log")] fn test_format_byte_opt() { assert_eq!( format_byte_opt(None), "end of file".to_string(), "should format an optional byte: none -> eof" );
assert_eq!( format_byte_opt(Some(b'!')), "byte `!` (U+0021)".to_string(), "should format an optional byte: char -> pretty" ); }
#[test] fn test_format() { assert_eq!( format('`'), "`` ` `` (U+0060)".to_string(), "should format a char: grave accent" );
assert_eq!( format('!'), "`!` (U+0021)".to_string(), "should format a char: regular" );
assert_eq!( format(' '), "U+0020".to_string(), "should format a char: unprintable" ); }
#[test] fn test_format_byte() { assert_eq!( format_byte(b'`'), "`` ` `` (U+0060)".to_string(), "should format a byte: grave accent" );
assert_eq!( format_byte(b'!'), "`!` (U+0021)".to_string(), "should format a byte: regular" );
assert_eq!( format_byte(b' '), "U+0020".to_string(), "should format a byte: unprintable" ); }}