#[cfg(feature = "html")]
pub mod html;
mod attr;
mod block;
mod inline;
mod lex;
pub use attr::AttributeKind;
pub use attr::AttributeValue;
pub use attr::AttributeValueParts;
pub use attr::Attributes;
pub use attr::ParseAttributesError;
type CowStr<'s> = std::borrow::Cow<'s, str>;
pub trait Render<'s> {
fn push_event<W>(&mut self, event: Event<'s>, out: W) -> std::fmt::Result
where
W: std::fmt::Write;
fn write_event<W>(&mut self, event: Event<'s>, out: W) -> std::io::Result<()>
where
W: std::io::Write,
{
struct WriteAdapter<T: std::io::Write> {
inner: T,
error: std::io::Result<()>,
}
impl<T: std::io::Write> std::fmt::Write for WriteAdapter<T> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.inner.write_all(s.as_bytes()).map_err(|e| {
self.error = Err(e);
std::fmt::Error
})
}
}
let mut out = WriteAdapter {
inner: out,
error: Ok(()),
};
self.push_event(event, &mut out)
.map_err(|_| match out.error {
Err(e) => e,
_ => std::io::Error::other("formatter error"),
})
}
fn push_events<I, W>(&mut self, mut events: I, mut out: W) -> std::fmt::Result
where
I: Iterator<Item = Event<'s>>,
W: std::fmt::Write,
{
events.try_for_each(|e| self.push_event(e, &mut out))
}
fn write_events<I, W>(&mut self, mut events: I, mut out: W) -> std::io::Result<()>
where
I: Iterator<Item = Event<'s>>,
W: std::io::Write,
{
events.try_for_each(|e| self.write_event(e, &mut out))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event<'s> {
Start(Container<'s>, Attributes<'s>),
End(Container<'s>),
Str(CowStr<'s>),
FootnoteReference(CowStr<'s>),
Symbol(CowStr<'s>),
LeftSingleQuote,
RightSingleQuote,
LeftDoubleQuote,
RightDoubleQuote,
Ellipsis,
EnDash,
EmDash,
NonBreakingSpace,
Softbreak,
Hardbreak,
Escape,
Blankline,
ThematicBreak(Attributes<'s>),
Attributes(Attributes<'s>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Container<'s> {
Document,
Blockquote,
List { kind: ListKind, tight: bool },
ListItem,
TaskListItem { checked: bool },
DescriptionList,
DescriptionDetails,
Footnote { label: CowStr<'s> },
Table,
TableRow { head: bool },
Section { id: CowStr<'s> },
Div { class: CowStr<'s> },
Paragraph,
Heading {
level: u16,
has_section: bool,
id: CowStr<'s>,
},
TableCell { alignment: Alignment, head: bool },
Caption,
DescriptionTerm,
LinkDefinition { label: CowStr<'s> },
RawBlock { format: CowStr<'s> },
CodeBlock { language: CowStr<'s> },
Span,
Link(CowStr<'s>, LinkType),
Image(CowStr<'s>, SpanLinkType),
Verbatim,
Math { display: bool },
RawInline { format: CowStr<'s> },
Subscript,
Superscript,
Insert,
Delete,
Strong,
Emphasis,
Mark,
}
impl Container<'_> {
#[must_use]
pub fn is_block(&self) -> bool {
match self {
Self::Blockquote
| Self::List { .. }
| Self::ListItem
| Self::TaskListItem { .. }
| Self::DescriptionList
| Self::DescriptionDetails
| Self::Footnote { .. }
| Self::Table
| Self::TableRow { .. }
| Self::Section { .. }
| Self::Div { .. }
| Self::Paragraph
| Self::Heading { .. }
| Self::TableCell { .. }
| Self::Caption
| Self::DescriptionTerm
| Self::LinkDefinition { .. }
| Self::RawBlock { .. }
| Self::CodeBlock { .. } => true,
Self::Document
| Self::Span
| Self::Link(..)
| Self::Image(..)
| Self::Verbatim
| Self::Math { .. }
| Self::RawInline { .. }
| Self::Subscript
| Self::Superscript
| Self::Insert
| Self::Delete
| Self::Strong
| Self::Emphasis
| Self::Mark => false,
}
}
#[must_use]
pub fn is_block_container(&self) -> bool {
match self {
Self::Blockquote
| Self::List { .. }
| Self::ListItem
| Self::TaskListItem { .. }
| Self::DescriptionList
| Self::DescriptionDetails
| Self::Footnote { .. }
| Self::Table
| Self::TableRow { .. }
| Self::Section { .. }
| Self::Div { .. } => true,
Self::Document
| Self::Paragraph
| Self::Heading { .. }
| Self::TableCell { .. }
| Self::Caption
| Self::DescriptionTerm
| Self::LinkDefinition { .. }
| Self::RawBlock { .. }
| Self::CodeBlock { .. }
| Self::Span
| Self::Link(..)
| Self::Image(..)
| Self::Verbatim
| Self::Math { .. }
| Self::RawInline { .. }
| Self::Subscript
| Self::Superscript
| Self::Insert
| Self::Delete
| Self::Strong
| Self::Emphasis
| Self::Mark => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Unspecified,
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanLinkType {
Inline,
Reference,
Unresolved,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkType {
Span(SpanLinkType),
AutoLink,
Email,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListBulletType {
Dash,
Star,
Plus,
}
impl TryFrom<u8> for ListBulletType {
type Error = ();
fn try_from(c: u8) -> Result<Self, Self::Error> {
match c {
b'-' => Ok(Self::Dash),
b'*' => Ok(Self::Star),
b'+' => Ok(Self::Plus),
_ => Err(()),
}
}
}
impl TryFrom<char> for ListBulletType {
type Error = ();
fn try_from(c: char) -> Result<Self, Self::Error> {
u8::try_from(u32::from(c))
.map_err(|_| ())
.and_then(Self::try_from)
}
}
impl From<ListBulletType> for u8 {
fn from(t: ListBulletType) -> Self {
match t {
ListBulletType::Dash => b'-',
ListBulletType::Star => b'*',
ListBulletType::Plus => b'+',
}
}
}
impl From<ListBulletType> for char {
fn from(t: ListBulletType) -> Self {
u8::from(t).into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ListKind {
Unordered(ListBulletType),
Ordered {
numbering: OrderedListNumbering,
style: OrderedListStyle,
start: u64,
},
Task(ListBulletType),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderedListNumbering {
Decimal,
AlphaLower,
AlphaUpper,
RomanLower,
RomanUpper,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderedListStyle {
Period,
Paren,
ParenParen,
}
impl OrderedListNumbering {
fn parse_number(self, n: &str) -> u64 {
match self {
Self::Decimal => n.parse().unwrap(),
Self::AlphaLower | Self::AlphaUpper => {
let d0 = u64::from(if matches!(self, Self::AlphaLower) {
b'a'
} else {
b'A'
});
let weights = (1..=n.len()).scan(1, |a, _| {
let prev = *a;
*a *= 26;
Some(prev)
});
n.as_bytes()
.iter()
.rev()
.copied()
.map(u64::from)
.zip(weights)
.map(|(d, w)| w * (d - d0 + 1))
.sum()
}
Self::RomanLower | Self::RomanUpper => {
fn value(d: char) -> u64 {
match d {
'i' | 'I' => 1,
'v' | 'V' => 5,
'x' | 'X' => 10,
'l' | 'L' => 50,
'c' | 'C' => 100,
'd' | 'D' => 500,
'm' | 'M' => 1000,
_ => panic!(),
}
}
let mut prev = 0;
let mut sum = 0;
for d in n.chars().rev() {
let v = value(d);
if v < prev {
sum -= v;
} else {
sum += v;
}
prev = v;
}
sum
}
}
}
}
impl OrderedListStyle {
fn number(self, marker: &str) -> &str {
&marker[usize::from(matches!(self, Self::ParenParen))..marker.len() - 1]
}
}
#[cfg(not(feature = "deterministic"))]
type Map<K, V> = std::collections::HashMap<K, V>;
#[cfg(feature = "deterministic")]
type Map<K, V> = std::collections::BTreeMap<K, V>;
#[cfg(not(feature = "deterministic"))]
type Set<T> = std::collections::HashSet<T>;
#[cfg(feature = "deterministic")]
type Set<T> = std::collections::BTreeSet<T>;
#[derive(Clone)]
pub struct Parser<'s> {
src: &'s str,
blocks: std::iter::Peekable<std::vec::IntoIter<block::Event<'s>>>,
pre_pass: PrePass<'s>,
block_attributes: Option<(Attributes<'s>, std::ops::Range<usize>)>,
table_head_row: bool,
verbatim: bool,
inline_parser: inline::Parser<'s>,
}
#[derive(Clone)]
struct Heading {
location: u32,
id_auto: String,
text: String,
id_override: Option<String>,
}
#[derive(Clone)]
struct PrePass<'s> {
link_definitions: Map<&'s str, (CowStr<'s>, attr::Attributes<'s>)>,
headings: Vec<Heading>,
headings_lex: Vec<usize>,
}
impl<'s> PrePass<'s> {
#[must_use]
fn new(
src: &'s str,
mut blocks: std::slice::Iter<block::Event<'s>>,
inline_parser: &mut inline::Parser<'s>,
) -> Self {
use std::fmt::Write;
let mut link_definitions = Map::new();
let mut headings: Vec<Heading> = Vec::new();
let mut used_ids: Set<String> = Set::new();
let mut attr_prev: Vec<std::ops::Range<usize>> = Vec::new();
while let Some(e) = blocks.next() {
match e.kind {
block::EventKind::Enter(block::Node::Leaf(block::Leaf::LinkDefinition {
label,
})) => {
let attrs = attr_prev
.iter()
.flat_map(|sp| {
Attributes::try_from(&src[sp.clone()]).expect("should be valid")
})
.collect::<Attributes>();
let url = if let Some(block::Event {
kind: block::EventKind::Inline,
span,
}) = blocks.next()
{
let start =
src[span.clone()].trim_matches(|c: char| c.is_ascii_whitespace());
if let Some(block::Event {
kind: block::EventKind::Inline,
span,
}) = blocks.next()
{
let mut url = start.to_string();
url.push_str(
src[span.clone()].trim_matches(|c: char| c.is_ascii_whitespace()),
);
while let Some(block::Event {
kind: block::EventKind::Inline,
span,
}) = blocks.next()
{
url.push_str(
src[span.clone()]
.trim_matches(|c: char| c.is_ascii_whitespace()),
);
}
url.into() } else {
start.into() }
} else {
"".into() };
link_definitions.insert(label, (url, attrs));
}
block::EventKind::Enter(block::Node::Leaf(block::Leaf::Heading { .. })) => {
let attrs = attr_prev
.iter()
.flat_map(|sp| {
Attributes::try_from(&src[sp.clone()]).expect("should be valid")
})
.collect::<Attributes>();
let id_override = attrs.get_value("id").map(|s| s.to_string());
let mut id_auto = String::new();
let mut text = String::new();
let mut last_whitespace = true;
inline_parser.reset();
let mut last_end = 0;
loop {
let span_inline = blocks.next().and_then(|e| {
if matches!(e.kind, block::EventKind::Inline) {
last_end = e.span.end;
Some(e.span.clone())
} else {
None
}
});
inline_parser.feed_line(
span_inline.clone().unwrap_or(last_end..last_end),
span_inline.is_none(),
);
inline_parser.for_each(|ev| match ev.kind {
inline::EventKind::Str => {
text.push_str(&src[ev.span.clone()]);
let mut chars = src[ev.span].chars().peekable();
while let Some(c) = chars.next() {
if c.is_ascii_whitespace() {
while chars.peek().is_some_and(char::is_ascii_whitespace) {
chars.next();
}
if !last_whitespace {
last_whitespace = true;
id_auto.push('-');
}
} else if !c.is_ascii_punctuation() || matches!(c, '-' | '_') {
id_auto.push(c);
last_whitespace = false;
}
}
}
inline::EventKind::Atom(inline::Atom::Softbreak) => {
text.push(' ');
id_auto.push('-');
}
_ => {}
});
if span_inline.is_none() {
break;
}
}
id_auto.drain(id_auto.trim_end_matches('-').len()..);
if used_ids.contains::<str>(&id_auto) || id_auto.is_empty() {
if id_auto.is_empty() {
id_auto.push('s');
}
let mut num = 1;
id_auto.push('-');
let i_num = id_auto.len();
write!(id_auto, "{num}").unwrap();
while used_ids.contains::<str>(&id_auto) {
num += 1;
id_auto.drain(i_num..);
write!(id_auto, "{num}").unwrap();
}
}
used_ids.insert(id_auto.clone());
headings.push(Heading {
location: e.span.start as u32,
id_auto,
text,
id_override,
});
}
block::EventKind::Atom(block::Atom::Attributes) => {
attr_prev.push(e.span.clone());
}
block::EventKind::Enter(..)
| block::EventKind::Exit(block::Node::Container(block::Container::Section {
..
})) => {}
_ => {
attr_prev = Vec::new();
}
}
}
let mut headings_lex = (0..headings.len()).collect::<Vec<_>>();
headings_lex.sort_by_key(|i| &headings[*i].text);
Self {
link_definitions,
headings,
headings_lex,
}
}
fn heading_id(&self, i: usize) -> &str {
let h = &self.headings[i];
h.id_override.as_ref().unwrap_or(&h.id_auto)
}
fn heading_id_by_location(&self, location: u32) -> Option<&str> {
self.headings
.binary_search_by_key(&location, |h| h.location)
.ok()
.map(|i| self.heading_id(i))
}
fn heading_id_by_tag(&self, tag: &str) -> Option<&str> {
self.headings_lex
.binary_search_by_key(&tag, |i| &self.headings[*i].text)
.ok()
.map(|i| self.heading_id(self.headings_lex[i]))
}
}
impl<'s> Parser<'s> {
#[must_use]
pub fn new(src: &'s str) -> Self {
let blocks = block::parse(src);
let mut inline_parser = inline::Parser::new(src);
let pre_pass = PrePass::new(src, blocks.iter(), &mut inline_parser);
Self {
src,
blocks: blocks.into_iter().peekable(),
pre_pass,
block_attributes: None,
table_head_row: false,
verbatim: false,
inline_parser,
}
}
#[must_use]
pub fn into_offset_iter(self) -> OffsetIter<'s> {
OffsetIter { parser: self }
}
fn inline(&mut self) -> Option<(Event<'s>, std::ops::Range<usize>)> {
let next = self.inline_parser.next()?;
let (inline, mut attributes) = match next {
inline::Event {
kind: inline::EventKind::Attributes { attrs, .. },
..
} => (
self.inline_parser.next(),
self.inline_parser.store_attributes[attrs as usize].clone(),
),
inline => (Some(inline), Attributes::new()),
};
let event = inline.map(|inline| {
let enter = matches!(inline.kind, inline::EventKind::Enter(_));
let event = match inline.kind {
inline::EventKind::Enter(c) | inline::EventKind::Exit(c) => {
let t = match c {
inline::Container::Span => Container::Span,
inline::Container::Verbatim => Container::Verbatim,
inline::Container::InlineMath => Container::Math { display: false },
inline::Container::DisplayMath => Container::Math { display: true },
inline::Container::RawFormat { format } => Container::RawInline {
format: format.into(),
},
inline::Container::Subscript => Container::Subscript,
inline::Container::Superscript => Container::Superscript,
inline::Container::Insert => Container::Insert,
inline::Container::Delete => Container::Delete,
inline::Container::Emphasis => Container::Emphasis,
inline::Container::Strong => Container::Strong,
inline::Container::Mark => Container::Mark,
inline::Container::InlineLink(url) => Container::Link(
self.inline_parser.store_cowstrs[url as usize].clone(),
LinkType::Span(SpanLinkType::Inline),
),
inline::Container::InlineImage(url) => Container::Image(
self.inline_parser.store_cowstrs[url as usize].clone(),
SpanLinkType::Inline,
),
inline::Container::ReferenceLink(tag)
| inline::Container::ReferenceImage(tag) => {
let tag = &self.inline_parser.store_cowstrs[tag as usize];
let link_def =
self.pre_pass.link_definitions.get(tag.as_ref()).cloned();
let (url_or_tag, ty) = if let Some((url, mut attrs_def)) = link_def {
if enter {
attrs_def.append(&mut attributes);
attributes = attrs_def;
}
(url, SpanLinkType::Reference)
} else {
self.pre_pass.heading_id_by_tag(tag.as_ref()).map_or_else(
|| (tag.clone(), SpanLinkType::Unresolved),
|id| (format!("#{id}").into(), SpanLinkType::Reference),
)
};
if matches!(c, inline::Container::ReferenceLink(..)) {
Container::Link(url_or_tag, LinkType::Span(ty))
} else {
Container::Image(url_or_tag, ty)
}
}
inline::Container::Autolink(url) => {
let ty = if url.contains('@') {
LinkType::Email
} else {
LinkType::AutoLink
};
Container::Link(url.into(), ty)
}
};
if enter {
Event::Start(t, attributes.take())
} else {
Event::End(t)
}
}
inline::EventKind::Atom(a) => match a {
inline::Atom::FootnoteReference { label } => {
Event::FootnoteReference(label.into())
}
inline::Atom::Symbol(sym) => Event::Symbol(sym.into()),
inline::Atom::Quote { ty, left } => match (ty, left) {
(inline::QuoteType::Single, true) => Event::LeftSingleQuote,
(inline::QuoteType::Single, false) => Event::RightSingleQuote,
(inline::QuoteType::Double, true) => Event::LeftDoubleQuote,
(inline::QuoteType::Double, false) => Event::RightDoubleQuote,
},
inline::Atom::Ellipsis => Event::Ellipsis,
inline::Atom::EnDash => Event::EnDash,
inline::Atom::EmDash => Event::EmDash,
inline::Atom::Nbsp => Event::NonBreakingSpace,
inline::Atom::Softbreak => Event::Softbreak,
inline::Atom::Hardbreak => Event::Hardbreak,
inline::Atom::Escape => Event::Escape,
},
inline::EventKind::Empty => {
debug_assert!(!attributes.is_empty());
Event::Attributes(attributes.take())
}
inline::EventKind::Str => Event::Str(self.src[inline.span.clone()].into()),
inline::EventKind::Attributes { .. } | inline::EventKind::Placeholder => {
panic!("{inline:?}")
}
};
(event, inline.span)
});
debug_assert!(
attributes.is_empty(),
"unhandled attributes: {attributes:?}",
);
event
}
fn block(&mut self) -> Option<(Event<'s>, std::ops::Range<usize>)> {
while let Some(ev) = self.blocks.peek() {
let mut ev_span = ev.span.clone();
let mut pop = true;
let event = match ev.kind {
block::EventKind::Atom(a) => match a {
block::Atom::Blankline => {
debug_assert_eq!(self.block_attributes, None);
Event::Blankline
}
block::Atom::ThematicBreak => {
let attrs = if let Some((attrs, span)) = self.block_attributes.take() {
ev_span.start = span.start;
attrs
} else {
Attributes::new()
};
Event::ThematicBreak(attrs)
}
block::Atom::Attributes => {
let (mut attrs, mut span) = self
.block_attributes
.take()
.unwrap_or_else(|| (Attributes::new(), ev_span.clone()));
attrs
.parse(&self.src[ev_span.clone()])
.expect("should be valid");
span.end = ev_span.end;
self.blocks.next().unwrap();
if matches!(
self.blocks.peek().map(|e| &e.kind),
Some(block::EventKind::Atom(block::Atom::Blankline))
) {
return Some((Event::Attributes(attrs), span));
}
self.block_attributes = Some((attrs, span));
continue;
}
},
block::EventKind::Enter(c) | block::EventKind::Exit(c) => {
let enter = matches!(ev.kind, block::EventKind::Enter(..));
let cont = match c {
block::Node::Leaf(l) => {
self.inline_parser.reset();
match l {
block::Leaf::Paragraph => Container::Paragraph,
block::Leaf::Heading {
level,
has_section,
pos,
} => Container::Heading {
level,
has_section,
id: self
.pre_pass
.heading_id_by_location(pos)
.unwrap_or_default()
.to_string()
.into(),
},
block::Leaf::DescriptionTerm => Container::DescriptionTerm,
block::Leaf::CodeBlock { language } => {
self.verbatim = enter;
if let Some(format) = language.strip_prefix('=') {
Container::RawBlock {
format: format.into(),
}
} else {
Container::CodeBlock {
language: language.into(),
}
}
}
block::Leaf::TableCell(alignment) => Container::TableCell {
alignment,
head: self.table_head_row,
},
block::Leaf::Caption => Container::Caption,
block::Leaf::LinkDefinition { label } => {
self.verbatim = enter;
Container::LinkDefinition {
label: label.into(),
}
}
}
}
block::Node::Container(c) => match c {
block::Container::Document => Container::Document,
block::Container::Blockquote => Container::Blockquote,
block::Container::Div { class } => Container::Div {
class: class.into(),
},
block::Container::Footnote { label } => Container::Footnote {
label: label.into(),
},
block::Container::List { ty, tight } => {
if matches!(ty, block::ListType::Description) {
Container::DescriptionList
} else {
let kind = match ty {
block::ListType::Unordered(c) => ListKind::Unordered(
c.try_into().expect("should be bullet character"),
),
block::ListType::Task(c) => ListKind::Task(
c.try_into().expect("should be bullet character"),
),
block::ListType::Ordered(
block::ListNumber { numbering, value },
style,
) => ListKind::Ordered {
numbering,
style,
start: value,
},
block::ListType::Description => unreachable!(),
};
Container::List { kind, tight }
}
}
block::Container::ListItem(kind) => match kind {
block::ListItemKind::Task { checked } => {
Container::TaskListItem { checked }
}
block::ListItemKind::Description => Container::DescriptionDetails,
block::ListItemKind::List => Container::ListItem,
},
block::Container::Table => Container::Table,
block::Container::TableRow { head } => {
if enter {
self.table_head_row = head;
}
Container::TableRow { head }
}
block::Container::Section { pos } => Container::Section {
id: self
.pre_pass
.heading_id_by_location(pos)
.unwrap_or_default()
.to_string()
.into(),
},
},
};
if enter {
let attrs = if let Some((attrs, span)) = self.block_attributes.take() {
ev_span.start = span.start;
attrs
} else {
Attributes::new()
};
Event::Start(cont, attrs)
} else if let Some((attrs, sp)) = self.block_attributes.take() {
pop = false;
ev_span = sp;
Event::Attributes(attrs)
} else {
Event::End(cont)
}
}
block::EventKind::Inline => {
if self.verbatim {
if ev_span.is_empty() {
self.blocks.next().unwrap();
continue;
}
Event::Str(self.src[ev_span.clone()].into())
} else {
self.blocks.next().unwrap();
self.inline_parser.feed_line(
ev_span.clone(),
!matches!(
self.blocks.peek().map(|e| &e.kind),
Some(block::EventKind::Inline),
),
);
return self.next_span();
}
}
block::EventKind::Stale => {
self.blocks.next().unwrap();
continue;
}
};
if pop {
self.blocks.next().unwrap();
}
return Some((event, ev_span));
}
None
}
fn next_span(&mut self) -> Option<(Event<'s>, std::ops::Range<usize>)> {
self.inline().or_else(|| self.block()).or_else(|| {
self.block_attributes
.take()
.map(|(attrs, span)| (Event::Attributes(attrs), span))
})
}
}
impl<'s> Iterator for Parser<'s> {
type Item = Event<'s>;
fn next(&mut self) -> Option<Self::Item> {
self.next_span().map(|(e, _)| e)
}
}
pub struct OffsetIter<'s> {
parser: Parser<'s>,
}
impl<'s> Iterator for OffsetIter<'s> {
type Item = (Event<'s>, std::ops::Range<usize>);
fn next(&mut self) -> Option<Self::Item> {
self.parser.next_span()
}
}
#[cfg(test)]
mod test {
use super::OrderedListNumbering::*;
#[test]
fn numbering_alpha() {
assert_eq!(AlphaLower.parse_number("a"), 1);
assert_eq!(AlphaUpper.parse_number("B"), 2);
assert_eq!(AlphaUpper.parse_number("Z"), 26);
assert_eq!(AlphaLower.parse_number("aa"), 27);
}
}