refactor: cleanup parse function
This commit is contained in:
parent
c1154a1853
commit
c5b14256f0
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "orgize"
|
name = "orgize"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
authors = ["PoiScript <poiscript@gmail.com>"]
|
authors = ["PoiScript <poiscript@gmail.com>"]
|
||||||
description = "A Rust library for parsing orgmode files."
|
description = "A Rust library for parsing orgmode files."
|
||||||
repository = "https://github.com/PoiScript/orgize"
|
repository = "https://github.com/PoiScript/orgize"
|
||||||
|
|
33
examples/convert.rs
Normal file
33
examples/convert.rs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
use std::env;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use orgize::export::{HtmlHandler, Render};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
if args.len() < 2 {
|
||||||
|
println!("Usage: {} <org-file>", args[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = File::open(&args[1]).expect(&format!("file {} not found", &args[1]));
|
||||||
|
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)
|
||||||
|
.expect("something went wrong reading the file");
|
||||||
|
|
||||||
|
let cursor = Cursor::new(Vec::new());
|
||||||
|
let handler = HtmlHandler;
|
||||||
|
let mut render = Render::new(handler, cursor, &contents);
|
||||||
|
|
||||||
|
render
|
||||||
|
.render()
|
||||||
|
.expect("something went wrong rendering the file");
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
String::from_utf8(render.into_wirter().into_inner()).expect("invalid utf-8")
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,61 +1,61 @@
|
||||||
use crate::lines::Lines;
|
use crate::lines::Lines;
|
||||||
use memchr::memchr2;
|
use memchr::memchr2;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
// return (name, args, contents-begin, contents-end, end)
|
||||||
#[derive(Debug)]
|
#[inline]
|
||||||
pub struct Block;
|
pub fn parse(src: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> {
|
||||||
|
debug_assert!(src.starts_with("#+"));
|
||||||
|
|
||||||
impl Block {
|
if src[2..8].to_uppercase() != "BEGIN_" {
|
||||||
// return (name, args, contents-begin, contents-end, end)
|
return None;
|
||||||
pub fn parse(src: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> {
|
|
||||||
debug_assert!(src.starts_with("#+"));
|
|
||||||
|
|
||||||
if !src[2..8].eq_ignore_ascii_case("BEGIN_") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = memchr2(b' ', b'\n', src.as_bytes())
|
|
||||||
.filter(|&i| src.as_bytes()[8..i].iter().all(|c| c.is_ascii_alphabetic()))?;
|
|
||||||
let mut lines = Lines::new(src);
|
|
||||||
let (pre_cont_end, cont_beg, _) = lines.next()?;
|
|
||||||
let args = if pre_cont_end == name {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(&src[name..pre_cont_end])
|
|
||||||
};
|
|
||||||
let name = &src[8..name];
|
|
||||||
let end_line = format!(r"#+END_{}", name);
|
|
||||||
let mut pre_end = cont_beg;
|
|
||||||
|
|
||||||
for (_, end, line) in lines {
|
|
||||||
if line.trim().eq_ignore_ascii_case(&end_line) {
|
|
||||||
return Some((name, args, cont_beg, pre_end, end));
|
|
||||||
} else {
|
|
||||||
pre_end = end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name = memchr2(b' ', b'\n', src.as_bytes())
|
||||||
|
.filter(|&i| src.as_bytes()[8..i].iter().all(|c| c.is_ascii_alphabetic()))?;
|
||||||
|
let mut lines = Lines::new(src);
|
||||||
|
let (pre_cont_end, cont_beg, _) = lines.next()?;
|
||||||
|
let args = if pre_cont_end == name {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&src[name..pre_cont_end])
|
||||||
|
};
|
||||||
|
let name = &src[8..name];
|
||||||
|
let end_line = format!(r"#+END_{}", name.to_uppercase());
|
||||||
|
let mut pre_end = cont_beg;
|
||||||
|
|
||||||
|
for (_, end, line) in lines {
|
||||||
|
if line.trim() == end_line {
|
||||||
|
return Some((name, args, cont_beg, pre_end, end));
|
||||||
|
} else {
|
||||||
|
pre_end = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn parse() {
|
mod tests {
|
||||||
assert_eq!(
|
#[test]
|
||||||
Block::parse("#+BEGIN_SRC\n#+END_SRC"),
|
fn parse() {
|
||||||
Some(("SRC", None, 12, 12, 21))
|
use super::parse;
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Block::parse(
|
parse("#+BEGIN_SRC\n#+END_SRC"),
|
||||||
r#"#+BEGIN_SRC rust
|
Some(("SRC", None, 12, 12, 21))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse(
|
||||||
|
r#"#+BEGIN_SRC rust
|
||||||
fn main() {
|
fn main() {
|
||||||
// print "Hello World!" to the console
|
// print "Hello World!" to the console
|
||||||
println!("Hello World!");
|
println!("Hello World!");
|
||||||
}
|
}
|
||||||
#+END_SRC
|
#+END_SRC
|
||||||
"#
|
"#
|
||||||
),
|
),
|
||||||
Some(("SRC", Some(" rust"), 17, 104, 114))
|
Some(("SRC", Some(" rust"), 17, 104, 114))
|
||||||
);
|
);
|
||||||
// TODO: more testing
|
// TODO: more testing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,62 @@
|
||||||
use crate::lines::Lines;
|
use crate::lines::Lines;
|
||||||
use memchr::memchr2;
|
use memchr::memchr2;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
// return (name, parameters, contents-begin, contents-end, end)
|
||||||
#[derive(Debug)]
|
#[inline]
|
||||||
pub struct DynBlock;
|
pub fn parse(src: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> {
|
||||||
|
debug_assert!(src.starts_with("#+"));
|
||||||
|
|
||||||
impl DynBlock {
|
if !src[2..9].eq_ignore_ascii_case("BEGIN: ") {
|
||||||
// return (name, parameters, contents-begin, contents-end, end)
|
return None;
|
||||||
pub fn parse(src: &str) -> Option<(&str, Option<&str>, usize, usize, usize)> {
|
|
||||||
debug_assert!(src.starts_with("#+"));
|
|
||||||
|
|
||||||
if !src[2..9].eq_ignore_ascii_case("BEGIN: ") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bytes = src.as_bytes();
|
|
||||||
let args = eol!(src);
|
|
||||||
let name = memchr2(b' ', b'\n', &bytes[9..])
|
|
||||||
.map(|i| i + 9)
|
|
||||||
.filter(|&i| {
|
|
||||||
src.as_bytes()[9..i]
|
|
||||||
.iter()
|
|
||||||
.all(|&c| c.is_ascii_alphabetic())
|
|
||||||
})?;
|
|
||||||
let mut lines = Lines::new(src);
|
|
||||||
let (mut pre_cont_end, _, _) = lines.next()?;
|
|
||||||
|
|
||||||
for (cont_end, end, line) in lines {
|
|
||||||
if line.trim().eq_ignore_ascii_case("#+END:") {
|
|
||||||
return Some((
|
|
||||||
&src[8..name].trim(),
|
|
||||||
if name == args {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(&src[name..args].trim())
|
|
||||||
},
|
|
||||||
args,
|
|
||||||
pre_cont_end,
|
|
||||||
end,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
pre_cont_end = cont_end;
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bytes = src.as_bytes();
|
||||||
|
let args = eol!(src);
|
||||||
|
let name = memchr2(b' ', b'\n', &bytes[9..])
|
||||||
|
.map(|i| i + 9)
|
||||||
|
.filter(|&i| {
|
||||||
|
src.as_bytes()[9..i]
|
||||||
|
.iter()
|
||||||
|
.all(|&c| c.is_ascii_alphabetic())
|
||||||
|
})?;
|
||||||
|
let mut lines = Lines::new(src);
|
||||||
|
let (mut pre_cont_end, _, _) = lines.next()?;
|
||||||
|
|
||||||
|
for (cont_end, end, line) in lines {
|
||||||
|
if line.trim().eq_ignore_ascii_case("#+END:") {
|
||||||
|
return Some((
|
||||||
|
&src[8..name].trim(),
|
||||||
|
if name == args {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&src[name..args].trim())
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
pre_cont_end,
|
||||||
|
end,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
pre_cont_end = cont_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn parse() {
|
mod tests {
|
||||||
// TODO: testing
|
#[test]
|
||||||
assert_eq!(
|
fn parse() {
|
||||||
DynBlock::parse(
|
use super::parse;
|
||||||
r"#+BEGIN: clocktable :scope file
|
|
||||||
|
// TODO: testing
|
||||||
|
assert_eq!(
|
||||||
|
parse(
|
||||||
|
r"#+BEGIN: clocktable :scope file
|
||||||
CONTENTS
|
CONTENTS
|
||||||
#+END:
|
#+END:
|
||||||
"
|
"
|
||||||
),
|
),
|
||||||
Some(("clocktable", Some(":scope file"), 31, 40, 48))
|
Some(("clocktable", Some(":scope file"), 31, 40, 48))
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,54 @@
|
||||||
use memchr::memchr;
|
use memchr::memchr;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[inline]
|
||||||
#[derive(Debug)]
|
pub fn parse(src: &str) -> Option<(&str, &str, usize)> {
|
||||||
pub struct FnDef;
|
debug_assert!(src.starts_with("[fn:"));
|
||||||
|
|
||||||
impl FnDef {
|
let label = memchr(b']', src.as_bytes()).filter(|&i| {
|
||||||
pub fn parse(src: &str) -> Option<(&str, &str, usize)> {
|
i != 4
|
||||||
debug_assert!(src.starts_with("[fn:"));
|
&& src.as_bytes()[4..i]
|
||||||
|
.iter()
|
||||||
|
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
|
||||||
|
})?;
|
||||||
|
|
||||||
let label = memchr(b']', src.as_bytes()).filter(|&i| {
|
let end = eol!(src);
|
||||||
i != 4
|
|
||||||
&& src.as_bytes()[4..i]
|
|
||||||
.iter()
|
|
||||||
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let end = eol!(src);
|
Some((&src[4..label], &src[label + 1..end], end))
|
||||||
|
}
|
||||||
|
|
||||||
Some((&src[4..label], &src[label + 1..end], end))
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
use super::parse;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse("[fn:1] https://orgmode.org").unwrap(),
|
||||||
|
(
|
||||||
|
"1",
|
||||||
|
" https://orgmode.org",
|
||||||
|
"[fn:1] https://orgmode.org".len()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("[fn:word_1] https://orgmode.org").unwrap(),
|
||||||
|
(
|
||||||
|
"word_1",
|
||||||
|
" https://orgmode.org",
|
||||||
|
"[fn:word_1] https://orgmode.org".len()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("[fn:WORD-1] https://orgmode.org").unwrap(),
|
||||||
|
(
|
||||||
|
"WORD-1",
|
||||||
|
" https://orgmode.org",
|
||||||
|
"[fn:WORD-1] https://orgmode.org".len()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_eq!(parse("[fn:WORD]").unwrap(), ("WORD", "", "[fn:WORD]".len()));
|
||||||
|
assert!(parse("[fn:] https://orgmode.org").is_none());
|
||||||
|
assert!(parse("[fn:wor d] https://orgmode.org").is_none());
|
||||||
|
assert!(parse("[fn:WORD https://orgmode.org").is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
assert_eq!(
|
|
||||||
FnDef::parse("[fn:1] https://orgmode.org").unwrap(),
|
|
||||||
(
|
|
||||||
"1",
|
|
||||||
" https://orgmode.org",
|
|
||||||
"[fn:1] https://orgmode.org".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
FnDef::parse("[fn:word_1] https://orgmode.org").unwrap(),
|
|
||||||
(
|
|
||||||
"word_1",
|
|
||||||
" https://orgmode.org",
|
|
||||||
"[fn:word_1] https://orgmode.org".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
FnDef::parse("[fn:WORD-1] https://orgmode.org").unwrap(),
|
|
||||||
(
|
|
||||||
"WORD-1",
|
|
||||||
" https://orgmode.org",
|
|
||||||
"[fn:WORD-1] https://orgmode.org".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
FnDef::parse("[fn:WORD]").unwrap(),
|
|
||||||
("WORD", "", "[fn:WORD]".len())
|
|
||||||
);
|
|
||||||
assert!(FnDef::parse("[fn:] https://orgmode.org").is_none());
|
|
||||||
assert!(FnDef::parse("[fn:wor d] https://orgmode.org").is_none());
|
|
||||||
assert!(FnDef::parse("[fn:WORD https://orgmode.org").is_none());
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
use memchr::{memchr, memchr2};
|
use memchr::{memchr, memchr2};
|
||||||
|
|
||||||
pub struct Keyword;
|
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Key<'a> {
|
pub enum Key<'a> {
|
||||||
|
@ -24,114 +22,116 @@ pub enum Key<'a> {
|
||||||
Call,
|
Call,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Keyword {
|
pub fn parse(src: &str) -> Option<(Key<'_>, &str, usize)> {
|
||||||
// return (key, value, offset)
|
debug_assert!(src.starts_with("#+"));
|
||||||
pub fn parse(src: &str) -> Option<(Key<'_>, &str, usize)> {
|
|
||||||
debug_assert!(src.starts_with("#+"));
|
|
||||||
|
|
||||||
let bytes = src.as_bytes();
|
let bytes = src.as_bytes();
|
||||||
let key_end = memchr2(b':', b'[', bytes).filter(|&i| {
|
let key_end = memchr2(b':', b'[', bytes).filter(|&i| {
|
||||||
bytes[2..i]
|
bytes[2..i]
|
||||||
.iter()
|
.iter()
|
||||||
.all(|&c| c.is_ascii_alphabetic() || c == b'_')
|
.all(|&c| c.is_ascii_alphabetic() || c == b'_')
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let option = if bytes[key_end] == b'[' {
|
let option = if bytes[key_end] == b'[' {
|
||||||
let option =
|
let option =
|
||||||
memchr(b']', bytes).filter(|&i| bytes[key_end..i].iter().all(|&c| c != b'\n'))?;
|
memchr(b']', bytes).filter(|&i| bytes[key_end..i].iter().all(|&c| c != b'\n'))?;
|
||||||
expect!(src, option + 1, b':')?;
|
expect!(src, option + 1, b':')?;
|
||||||
option + 1
|
option + 1
|
||||||
} else {
|
} else {
|
||||||
key_end
|
key_end
|
||||||
};
|
};
|
||||||
|
|
||||||
// includes the eol character
|
// includes the eol character
|
||||||
let end = memchr::memchr(b'\n', src.as_bytes())
|
let end = memchr::memchr(b'\n', src.as_bytes())
|
||||||
.map(|i| i + 1)
|
.map(|i| i + 1)
|
||||||
.unwrap_or_else(|| src.len());
|
.unwrap_or_else(|| src.len());
|
||||||
|
|
||||||
Some((
|
Some((
|
||||||
match &src[2..key_end] {
|
match src[2..key_end].to_uppercase().as_str() {
|
||||||
key if key.eq_ignore_ascii_case("CAPTION") => Key::Caption {
|
"AUTHOR" => Key::Author,
|
||||||
option: if key_end == option {
|
"CALL" => Key::Call,
|
||||||
None
|
"DATE" => Key::Date,
|
||||||
} else {
|
"HEADER" => Key::Header,
|
||||||
Some(&src[key_end + 1..option - 1])
|
"NAME" => Key::Name,
|
||||||
},
|
"PLOT" => Key::Plot,
|
||||||
|
"TITLE" => Key::Title,
|
||||||
|
"RESULTS" => Key::Results {
|
||||||
|
option: if key_end == option {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&src[key_end + 1..option - 1])
|
||||||
},
|
},
|
||||||
key if key.eq_ignore_ascii_case("HEADER") => Key::Header,
|
|
||||||
key if key.eq_ignore_ascii_case("NAME") => Key::Name,
|
|
||||||
key if key.eq_ignore_ascii_case("PLOT") => Key::Plot,
|
|
||||||
key if key.eq_ignore_ascii_case("RESULTS") => Key::Results {
|
|
||||||
option: if key_end == option {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(&src[key_end + 1..option - 1])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
key if key.eq_ignore_ascii_case("AUTHOR") => Key::Author,
|
|
||||||
key if key.eq_ignore_ascii_case("DATE") => Key::Date,
|
|
||||||
key if key.eq_ignore_ascii_case("TITLE") => Key::Title,
|
|
||||||
key if key.eq_ignore_ascii_case("CALL") => Key::Call,
|
|
||||||
key if key.starts_with("ATTR_") => Key::Attr {
|
|
||||||
backend: &src["#+ATTR_".len()..key_end],
|
|
||||||
},
|
|
||||||
key => Key::Custom(key),
|
|
||||||
},
|
},
|
||||||
&src[option + 1..end].trim(),
|
"CAPTION" => Key::Caption {
|
||||||
end,
|
option: if key_end == option {
|
||||||
))
|
None
|
||||||
|
} else {
|
||||||
|
Some(&src[key_end + 1..option - 1])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
key if key.starts_with("ATTR_") => Key::Attr {
|
||||||
|
backend: &src["#+ATTR_".len()..key_end],
|
||||||
|
},
|
||||||
|
_ => Key::Custom(&src[2..key_end]),
|
||||||
|
},
|
||||||
|
&src[option + 1..end].trim(),
|
||||||
|
end,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse("#+KEY:"),
|
||||||
|
Some((Key::Custom("KEY"), "", "#+KEY:".len()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("#+KEY: VALUE"),
|
||||||
|
Some((Key::Custom("KEY"), "VALUE", "#+KEY: VALUE".len()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("#+K_E_Y: VALUE"),
|
||||||
|
Some((Key::Custom("K_E_Y"), "VALUE", "#+K_E_Y: VALUE".len()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("#+KEY:VALUE\n"),
|
||||||
|
Some((Key::Custom("KEY"), "VALUE", "#+KEY:VALUE\n".len()))
|
||||||
|
);
|
||||||
|
assert!(parse("#+KE Y: VALUE").is_none());
|
||||||
|
assert!(parse("#+ KEY: VALUE").is_none());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse("#+RESULTS:"),
|
||||||
|
Some((Key::Results { option: None }, "", "#+RESULTS:".len()))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse("#+ATTR_LATEX: :width 5cm"),
|
||||||
|
Some((
|
||||||
|
Key::Attr { backend: "LATEX" },
|
||||||
|
":width 5cm",
|
||||||
|
"#+ATTR_LATEX: :width 5cm".len()
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse("#+CALL: double(n=4)"),
|
||||||
|
Some((Key::Call, "double(n=4)", "#+CALL: double(n=4)".len()))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse("#+CAPTION[Short caption]: Longer caption."),
|
||||||
|
Some((
|
||||||
|
Key::Caption {
|
||||||
|
option: Some("Short caption")
|
||||||
|
},
|
||||||
|
"Longer caption.",
|
||||||
|
"#+CAPTION[Short caption]: Longer caption.".len()
|
||||||
|
))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
assert_eq!(
|
|
||||||
Keyword::parse("#+KEY:"),
|
|
||||||
Some((Key::Custom("KEY"), "", "#+KEY:".len()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Keyword::parse("#+KEY: VALUE"),
|
|
||||||
Some((Key::Custom("KEY"), "VALUE", "#+KEY: VALUE".len()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Keyword::parse("#+K_E_Y: VALUE"),
|
|
||||||
Some((Key::Custom("K_E_Y"), "VALUE", "#+K_E_Y: VALUE".len()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Keyword::parse("#+KEY:VALUE\n"),
|
|
||||||
Some((Key::Custom("KEY"), "VALUE", "#+KEY:VALUE\n".len()))
|
|
||||||
);
|
|
||||||
assert!(Keyword::parse("#+KE Y: VALUE").is_none());
|
|
||||||
assert!(Keyword::parse("#+ KEY: VALUE").is_none());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keyword::parse("#+RESULTS:"),
|
|
||||||
Some((Key::Results { option: None }, "", "#+RESULTS:".len()))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keyword::parse("#+ATTR_LATEX: :width 5cm"),
|
|
||||||
Some((
|
|
||||||
Key::Attr { backend: "LATEX" },
|
|
||||||
":width 5cm",
|
|
||||||
"#+ATTR_LATEX: :width 5cm".len()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keyword::parse("#+CALL: double(n=4)"),
|
|
||||||
Some((Key::Call, "double(n=4)", "#+CALL: double(n=4)".len()))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keyword::parse("#+CAPTION[Short caption]: Longer caption."),
|
|
||||||
Some((
|
|
||||||
Key::Caption {
|
|
||||||
option: Some("Short caption")
|
|
||||||
},
|
|
||||||
"Longer caption.",
|
|
||||||
"#+CAPTION[Short caption]: Longer caption.".len()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,157 +1,159 @@
|
||||||
use crate::lines::Lines;
|
use crate::lines::Lines;
|
||||||
|
|
||||||
pub struct List;
|
#[inline]
|
||||||
|
pub fn is_item(src: &str) -> (bool, bool) {
|
||||||
impl List {
|
if src.is_empty() {
|
||||||
#[inline]
|
return (false, false);
|
||||||
pub fn is_item(src: &str) -> (bool, bool) {
|
|
||||||
if src.is_empty() {
|
|
||||||
return (false, false);
|
|
||||||
}
|
|
||||||
let bytes = src.as_bytes();
|
|
||||||
let (i, ordered) = match bytes[0] {
|
|
||||||
b'*' | b'-' | b'+' => (1, false),
|
|
||||||
b'0'...b'9' => {
|
|
||||||
let i = bytes
|
|
||||||
.iter()
|
|
||||||
.position(|&c| !c.is_ascii_digit())
|
|
||||||
.unwrap_or_else(|| src.len() - 1);
|
|
||||||
let c = bytes[i];
|
|
||||||
if !(c == b'.' || c == b')') {
|
|
||||||
return (false, false);
|
|
||||||
}
|
|
||||||
(i + 1, true)
|
|
||||||
}
|
|
||||||
_ => return (false, false),
|
|
||||||
};
|
|
||||||
|
|
||||||
if i < src.len() {
|
|
||||||
// bullet is follwed by a space or line ending
|
|
||||||
(bytes[i] == b' ' || bytes[i] == b'\n', ordered)
|
|
||||||
} else {
|
|
||||||
(false, false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let bytes = src.as_bytes();
|
||||||
// returns (bullets, contents begin, contents end, end, has more)
|
let (i, ordered) = match bytes[0] {
|
||||||
pub fn parse(src: &str, ident: usize) -> (&str, usize, usize, usize, bool) {
|
b'*' | b'-' | b'+' => (1, false),
|
||||||
debug_assert!(Self::is_item(&src[ident..]).0);
|
b'0'...b'9' => {
|
||||||
debug_assert!(
|
let i = bytes
|
||||||
src[..ident].chars().all(|c| c == ' ' || c == '\t'),
|
.iter()
|
||||||
"{:?} doesn't starts with indentation {}",
|
.position(|&c| !c.is_ascii_digit())
|
||||||
src,
|
.unwrap_or_else(|| src.len() - 1);
|
||||||
ident
|
let c = bytes[i];
|
||||||
);
|
if !(c == b'.' || c == b')') {
|
||||||
|
return (false, false);
|
||||||
let mut lines = Lines::new(src);
|
|
||||||
let (mut pre_cont_end, mut pre_end, first_line) = lines.next().unwrap();
|
|
||||||
let beg = match memchr::memchr(b' ', &first_line.as_bytes()[ident..]) {
|
|
||||||
Some(i) => i + ident + 1,
|
|
||||||
None => {
|
|
||||||
let len = first_line.len();
|
|
||||||
return (
|
|
||||||
&first_line,
|
|
||||||
len,
|
|
||||||
len,
|
|
||||||
len,
|
|
||||||
Self::is_item(lines.next().unwrap().2).0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
(i + 1, true)
|
||||||
let bullet = &src[0..beg];
|
}
|
||||||
|
_ => return (false, false),
|
||||||
|
};
|
||||||
|
|
||||||
while let Some((mut cont_end, mut end, mut line)) = lines.next() {
|
if i < src.len() {
|
||||||
// this line is emtpy
|
// bullet is follwed by a space or line ending
|
||||||
if line.is_empty() {
|
(bytes[i] == b' ' || bytes[i] == b'\n', ordered)
|
||||||
if let Some((next_cont_end, next_end, next_line)) = lines.next() {
|
} else {
|
||||||
// next line is emtpy, too
|
(false, false)
|
||||||
if next_line.is_empty() {
|
}
|
||||||
return (bullet, beg, pre_cont_end, next_end, false);
|
}
|
||||||
} else {
|
|
||||||
// move to next line
|
// returns (bullets, contents begin, contents end, end, has more)
|
||||||
pre_end = end;
|
#[inline]
|
||||||
cont_end = next_cont_end;
|
pub fn parse(src: &str, ident: usize) -> (&str, usize, usize, usize, bool) {
|
||||||
end = next_end;
|
debug_assert!(is_item(&src[ident..]).0);
|
||||||
line = next_line;
|
debug_assert!(
|
||||||
}
|
src[..ident].chars().all(|c| c == ' ' || c == '\t'),
|
||||||
|
"{:?} doesn't starts with indentation {}",
|
||||||
|
src,
|
||||||
|
ident
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut lines = Lines::new(src);
|
||||||
|
let (mut pre_cont_end, mut pre_end, first_line) = lines.next().unwrap();
|
||||||
|
let beg = match memchr::memchr(b' ', &first_line.as_bytes()[ident..]) {
|
||||||
|
Some(i) => i + ident + 1,
|
||||||
|
None => {
|
||||||
|
let len = first_line.len();
|
||||||
|
return (
|
||||||
|
&first_line,
|
||||||
|
len,
|
||||||
|
len,
|
||||||
|
len,
|
||||||
|
is_item(lines.next().unwrap().2).0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let bullet = &src[0..beg];
|
||||||
|
|
||||||
|
while let Some((mut cont_end, mut end, mut line)) = lines.next() {
|
||||||
|
// this line is emtpy
|
||||||
|
if line.is_empty() {
|
||||||
|
if let Some((next_cont_end, next_end, next_line)) = lines.next() {
|
||||||
|
// next line is emtpy, too
|
||||||
|
if next_line.is_empty() {
|
||||||
|
return (bullet, beg, pre_cont_end, next_end, false);
|
||||||
} else {
|
} else {
|
||||||
return (bullet, beg, pre_cont_end, end, false);
|
// move to next line
|
||||||
|
pre_end = end;
|
||||||
|
cont_end = next_cont_end;
|
||||||
|
end = next_end;
|
||||||
|
line = next_line;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return (bullet, beg, pre_cont_end, end, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let line_ident = Self::ident(line);
|
|
||||||
|
|
||||||
if line_ident < ident {
|
|
||||||
return (bullet, beg, pre_cont_end, pre_end, false);
|
|
||||||
} else if line_ident == ident {
|
|
||||||
return (
|
|
||||||
bullet,
|
|
||||||
beg,
|
|
||||||
pre_cont_end,
|
|
||||||
pre_end,
|
|
||||||
Self::is_item(&line[ident..]).0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pre_end = end;
|
|
||||||
pre_cont_end = cont_end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(bullet, beg, src.len(), src.len(), false)
|
let line_ident = self::ident(line);
|
||||||
|
|
||||||
|
if line_ident < ident {
|
||||||
|
return (bullet, beg, pre_cont_end, pre_end, false);
|
||||||
|
} else if line_ident == ident {
|
||||||
|
return (
|
||||||
|
bullet,
|
||||||
|
beg,
|
||||||
|
pre_cont_end,
|
||||||
|
pre_end,
|
||||||
|
is_item(&line[ident..]).0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre_end = end;
|
||||||
|
pre_cont_end = cont_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ident(src: &str) -> usize {
|
(bullet, beg, src.len(), src.len(), false)
|
||||||
src.as_bytes()
|
}
|
||||||
.iter()
|
|
||||||
.position(|&c| c != b' ' && c != b'\t')
|
#[inline]
|
||||||
.unwrap_or(0)
|
fn ident(src: &str) -> usize {
|
||||||
|
src.as_bytes()
|
||||||
|
.iter()
|
||||||
|
.position(|&c| c != b' ' && c != b'\t')
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn is_item() {
|
||||||
|
use super::is_item;
|
||||||
|
|
||||||
|
assert_eq!(is_item("+ item"), (true, false));
|
||||||
|
assert_eq!(is_item("- item"), (true, false));
|
||||||
|
assert_eq!(is_item("10. item"), (true, true));
|
||||||
|
assert_eq!(is_item("10) item"), (true, true));
|
||||||
|
assert_eq!(is_item("1. item"), (true, true));
|
||||||
|
assert_eq!(is_item("1) item"), (true, true));
|
||||||
|
assert_eq!(is_item("10. "), (true, true));
|
||||||
|
assert_eq!(is_item("10.\n"), (true, true));
|
||||||
|
assert_eq!(is_item("10."), (false, false));
|
||||||
|
assert_eq!(is_item("+"), (false, false));
|
||||||
|
assert_eq!(is_item("-item"), (false, false));
|
||||||
|
assert_eq!(is_item("+item"), (false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
use super::parse;
|
||||||
|
|
||||||
|
assert_eq!(parse("+ item1\n+ item2\n+ item3", 0), ("+ ", 2, 7, 8, true));
|
||||||
|
assert_eq!(
|
||||||
|
parse("* item1\n\n* item2\n* item3", 0),
|
||||||
|
("* ", 2, 7, 9, true)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("- item1\n\n\n- item2\n- item3", 0),
|
||||||
|
("- ", 2, 7, 10, false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("1. item1\n\n\n\n2. item2\n3. item3", 0),
|
||||||
|
("1. ", 3, 8, 11, false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse(" + item1\n + item2\n+ item3", 2),
|
||||||
|
(" + ", 4, 21, 22, false)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse(" + item1\n + item2\n + item3", 2),
|
||||||
|
(" + ", 4, 9, 10, true)
|
||||||
|
);
|
||||||
|
assert_eq!(parse("+\n", 0), ("+", 1, 1, 1, false));
|
||||||
|
assert_eq!(parse("+\n+ item2\n+ item3", 0), ("+", 1, 1, 1, true));
|
||||||
|
assert_eq!(parse("1) item1", 0), ("1) ", 3, 8, 8, false));
|
||||||
|
assert_eq!(parse("1) item1\n", 0), ("1) ", 3, 8, 9, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_item() {
|
|
||||||
assert_eq!(List::is_item("+ item"), (true, false));
|
|
||||||
assert_eq!(List::is_item("- item"), (true, false));
|
|
||||||
assert_eq!(List::is_item("10. item"), (true, true));
|
|
||||||
assert_eq!(List::is_item("10) item"), (true, true));
|
|
||||||
assert_eq!(List::is_item("1. item"), (true, true));
|
|
||||||
assert_eq!(List::is_item("1) item"), (true, true));
|
|
||||||
assert_eq!(List::is_item("10. "), (true, true));
|
|
||||||
assert_eq!(List::is_item("10.\n"), (true, true));
|
|
||||||
assert_eq!(List::is_item("10."), (false, false));
|
|
||||||
assert_eq!(List::is_item("+"), (false, false));
|
|
||||||
assert_eq!(List::is_item("-item"), (false, false));
|
|
||||||
assert_eq!(List::is_item("+item"), (false, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
assert_eq!(
|
|
||||||
List::parse("+ item1\n+ item2\n+ item3", 0),
|
|
||||||
("+ ", 2, 7, 8, true)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
List::parse("* item1\n\n* item2\n* item3", 0),
|
|
||||||
("* ", 2, 7, 9, true)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
List::parse("- item1\n\n\n- item2\n- item3", 0),
|
|
||||||
("- ", 2, 7, 10, false)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
List::parse("1. item1\n\n\n\n2. item2\n3. item3", 0),
|
|
||||||
("1. ", 3, 8, 11, false)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
List::parse(" + item1\n + item2\n+ item3", 2),
|
|
||||||
(" + ", 4, 21, 22, false)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
List::parse(" + item1\n + item2\n + item3", 2),
|
|
||||||
(" + ", 4, 9, 10, true)
|
|
||||||
);
|
|
||||||
assert_eq!(List::parse("+\n", 0), ("+", 1, 1, 1, false));
|
|
||||||
assert_eq!(List::parse("+\n+ item2\n+ item3", 0), ("+", 1, 1, 1, true));
|
|
||||||
assert_eq!(List::parse("1) item1", 0), ("1) ", 3, 8, 8, false));
|
|
||||||
assert_eq!(List::parse("1) item1\n", 0), ("1) ", 3, 8, 9, false));
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,12 +5,7 @@ pub mod keyword;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod rule;
|
pub mod rule;
|
||||||
|
|
||||||
pub use self::block::Block;
|
pub use self::keyword::Key;
|
||||||
pub use self::dyn_block::DynBlock;
|
|
||||||
pub use self::fn_def::FnDef;
|
|
||||||
pub use self::keyword::{Key, Keyword};
|
|
||||||
pub use self::list::List;
|
|
||||||
pub use self::rule::Rule;
|
|
||||||
|
|
||||||
use memchr::memchr;
|
use memchr::memchr;
|
||||||
use memchr::memchr_iter;
|
use memchr::memchr_iter;
|
||||||
|
@ -118,9 +113,9 @@ impl<'a> Element<'a> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlike other element, footnote definition must starts at column 0
|
// Unlike other element, footnote def must starts at column 0
|
||||||
if bytes[pos..].starts_with(b"[fn:") {
|
if bytes[pos..].starts_with(b"[fn:") {
|
||||||
if let Some((label, cont, off)) = FnDef::parse(&src[pos..]) {
|
if let Some((label, cont, off)) = fn_def::parse(&src[pos..]) {
|
||||||
brk!(Element::FnDef { label, cont }, off + 1);
|
brk!(Element::FnDef { label, cont }, off + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +133,7 @@ impl<'a> Element<'a> {
|
||||||
|
|
||||||
pos = skip_space!(src, pos);
|
pos = skip_space!(src, pos);
|
||||||
|
|
||||||
let (is_item, ordered) = List::is_item(&src[pos..]);
|
let (is_item, ordered) = list::is_item(&src[pos..]);
|
||||||
if is_item {
|
if is_item {
|
||||||
let list = Element::List {
|
let list = Element::List {
|
||||||
ident: pos - line_beg,
|
ident: pos - line_beg,
|
||||||
|
@ -163,7 +158,7 @@ impl<'a> Element<'a> {
|
||||||
|
|
||||||
// Rule
|
// Rule
|
||||||
if bytes[pos] == b'-' {
|
if bytes[pos] == b'-' {
|
||||||
let off = Rule::parse(&src[pos..]);
|
let off = rule::parse(&src[pos..]);
|
||||||
if off != 0 {
|
if off != 0 {
|
||||||
brk!(Element::Rule, off);
|
brk!(Element::Rule, off);
|
||||||
}
|
}
|
||||||
|
@ -178,7 +173,7 @@ impl<'a> Element<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes[pos..].starts_with(b"#+") {
|
if bytes[pos..].starts_with(b"#+") {
|
||||||
if let Some((name, args, cont_beg, cont_end, end)) = Block::parse(&src[pos..]) {
|
if let Some((name, args, cont_beg, cont_end, end)) = block::parse(&src[pos..]) {
|
||||||
let cont = &src[pos + cont_beg..pos + cont_end];
|
let cont = &src[pos + cont_beg..pos + cont_end];
|
||||||
match name.to_uppercase().as_str() {
|
match name.to_uppercase().as_str() {
|
||||||
"COMMENT" => brk!(Element::CommentBlock { args, cont }, end),
|
"COMMENT" => brk!(Element::CommentBlock { args, cont }, end),
|
||||||
|
@ -214,7 +209,7 @@ impl<'a> Element<'a> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((name, args, cont_beg, cont_end, end)) = DynBlock::parse(&src[pos..]) {
|
if let Some((name, args, cont_beg, cont_end, end)) = dyn_block::parse(&src[pos..]) {
|
||||||
brk!(
|
brk!(
|
||||||
Element::DynBlock {
|
Element::DynBlock {
|
||||||
name,
|
name,
|
||||||
|
@ -226,7 +221,7 @@ impl<'a> Element<'a> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((key, value, off)) = Keyword::parse(&src[pos..]) {
|
if let Some((key, value, off)) = keyword::parse(&src[pos..]) {
|
||||||
brk!(
|
brk!(
|
||||||
if let Key::Call = key {
|
if let Key::Call = key {
|
||||||
Element::Call { value }
|
Element::Call { value }
|
||||||
|
|
|
@ -1,37 +1,37 @@
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[inline]
|
||||||
#[derive(Debug)]
|
pub fn parse(src: &str) -> usize {
|
||||||
pub struct Rule;
|
let end = memchr::memchr(b'\n', src.as_bytes())
|
||||||
|
.map(|i| i + 1)
|
||||||
impl Rule {
|
.unwrap_or_else(|| src.len());
|
||||||
pub fn parse(src: &str) -> usize {
|
let rules = &src[0..end].trim();
|
||||||
let end = memchr::memchr(b'\n', src.as_bytes())
|
if rules.len() >= 5 && rules.chars().all(|c| c == '-') {
|
||||||
.map(|i| i + 1)
|
end
|
||||||
.unwrap_or_else(|| src.len());
|
} else {
|
||||||
let rules = &src[0..end].trim();
|
0
|
||||||
if rules.len() >= 5 && rules.chars().all(|c| c == '-') {
|
|
||||||
end
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn parse() {
|
mod tests {
|
||||||
assert_eq!(Rule::parse("-----"), "-----".len());
|
#[test]
|
||||||
assert_eq!(Rule::parse("--------"), "--------".len());
|
fn parse() {
|
||||||
assert_eq!(Rule::parse(" -----"), " -----".len());
|
use super::parse;
|
||||||
assert_eq!(Rule::parse("\t\t-----"), "\t\t-----".len());
|
|
||||||
assert_eq!(Rule::parse("\t\t-----\n"), "\t\t-----\n".len());
|
assert_eq!(parse("-----"), "-----".len());
|
||||||
assert_eq!(Rule::parse("\t\t----- \n"), "\t\t----- \n".len());
|
assert_eq!(parse("--------"), "--------".len());
|
||||||
assert_eq!(Rule::parse(""), 0);
|
assert_eq!(parse(" -----"), " -----".len());
|
||||||
assert_eq!(Rule::parse("----"), 0);
|
assert_eq!(parse("\t\t-----"), "\t\t-----".len());
|
||||||
assert_eq!(Rule::parse(" ----"), 0);
|
assert_eq!(parse("\t\t-----\n"), "\t\t-----\n".len());
|
||||||
assert_eq!(Rule::parse(" 0----"), 0);
|
assert_eq!(parse("\t\t----- \n"), "\t\t----- \n".len());
|
||||||
assert_eq!(Rule::parse("0 ----"), 0);
|
assert_eq!(parse(""), 0);
|
||||||
assert_eq!(Rule::parse("0------"), 0);
|
assert_eq!(parse("----"), 0);
|
||||||
assert_eq!(Rule::parse("----0----"), 0);
|
assert_eq!(parse(" ----"), 0);
|
||||||
assert_eq!(Rule::parse("\t\t----"), 0);
|
assert_eq!(parse(" 0----"), 0);
|
||||||
assert_eq!(Rule::parse("------0"), 0);
|
assert_eq!(parse("0 ----"), 0);
|
||||||
assert_eq!(Rule::parse("----- 0"), 0);
|
assert_eq!(parse("0------"), 0);
|
||||||
|
assert_eq!(parse("----0----"), 0);
|
||||||
|
assert_eq!(parse("\t\t----"), 0);
|
||||||
|
assert_eq!(parse("------0"), 0);
|
||||||
|
assert_eq!(parse("----- 0"), 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,7 @@
|
||||||
use crate::elements::Key;
|
use crate::elements::Key;
|
||||||
use crate::export::Handler;
|
use crate::export::Handler;
|
||||||
use crate::headline::Headline;
|
use crate::headline::Headline;
|
||||||
use crate::objects::{
|
use crate::objects::Cookie;
|
||||||
Cookie, FnRef, InlineCall, InlineSrc, Link, Macros, RadioTarget, Snippet, Target,
|
|
||||||
};
|
|
||||||
use std::io::{Result, Write};
|
use std::io::{Result, Write};
|
||||||
|
|
||||||
pub struct HtmlHandler;
|
pub struct HtmlHandler;
|
||||||
|
@ -117,36 +115,49 @@ impl<W: Write> Handler<W> for HtmlHandler {
|
||||||
fn handle_cookie(&mut self, w: &mut W, cookie: Cookie) -> Result<()> {
|
fn handle_cookie(&mut self, w: &mut W, cookie: Cookie) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn handle_fn_ref(&mut self, w: &mut W, fn_ref: FnRef) -> Result<()> {
|
fn handle_fn_ref(&mut self, w: &mut W, label: Option<&str>, def: Option<&str>) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn handle_inline_call(&mut self, w: &mut W, inline_call: InlineCall) -> Result<()> {
|
fn handle_inline_call(
|
||||||
|
&mut self,
|
||||||
|
w: &mut W,
|
||||||
|
name: &str,
|
||||||
|
args: &str,
|
||||||
|
inside_header: Option<&str>,
|
||||||
|
end_header: Option<&str>,
|
||||||
|
) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn handle_inline_src(&mut self, w: &mut W, inline_src: InlineSrc) -> Result<()> {
|
fn handle_inline_src(
|
||||||
write!(w, "<code>{}</code>", inline_src.body)
|
&mut self,
|
||||||
|
w: &mut W,
|
||||||
|
lang: &str,
|
||||||
|
option: Option<&str>,
|
||||||
|
body: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
write!(w, "<code>{}</code>", body)
|
||||||
}
|
}
|
||||||
fn handle_link(&mut self, w: &mut W, link: Link) -> Result<()> {
|
fn handle_link(&mut self, w: &mut W, path: &str, desc: Option<&str>) -> Result<()> {
|
||||||
if let Some(desc) = link.desc {
|
if let Some(desc) = desc {
|
||||||
write!(w, r#"<a href="{}">{}</a>"#, link.path, desc)
|
write!(w, r#"<a href="{}">{}</a>"#, path, desc)
|
||||||
} else {
|
} else {
|
||||||
write!(w, r#"<a href="{0}">{0}</a>"#, link.path)
|
write!(w, r#"<a href="{0}">{0}</a>"#, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn handle_macros(&mut self, w: &mut W, macros: Macros) -> Result<()> {
|
fn handle_macros(&mut self, w: &mut W, name: &str, args: Option<&str>) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn handle_radio_target(&mut self, w: &mut W, target: RadioTarget) -> Result<()> {
|
fn handle_radio_target(&mut self, w: &mut W, target: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn handle_snippet(&mut self, w: &mut W, snippet: Snippet) -> Result<()> {
|
fn handle_snippet(&mut self, w: &mut W, name: &str, value: &str) -> Result<()> {
|
||||||
if snippet.name.eq_ignore_ascii_case("HTML") {
|
if name.eq_ignore_ascii_case("HTML") {
|
||||||
write!(w, "{}", snippet.value)
|
write!(w, "{}", value)
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn handle_target(&mut self, w: &mut W, target: Target) -> Result<()> {
|
fn handle_target(&mut self, w: &mut W, target: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn handle_bold_beg(&mut self, w: &mut W) -> Result<()> {
|
fn handle_bold_beg(&mut self, w: &mut W) -> Result<()> {
|
||||||
|
|
|
@ -4,9 +4,7 @@ pub use self::html::HtmlHandler;
|
||||||
|
|
||||||
use crate::elements::Key;
|
use crate::elements::Key;
|
||||||
use crate::headline::Headline;
|
use crate::headline::Headline;
|
||||||
use crate::objects::{
|
use crate::objects::Cookie;
|
||||||
Cookie, FnRef, InlineCall, InlineSrc, Link, Macros, RadioTarget, Snippet, Target,
|
|
||||||
};
|
|
||||||
use crate::parser::Parser;
|
use crate::parser::Parser;
|
||||||
use std::io::{Result, Write};
|
use std::io::{Result, Write};
|
||||||
|
|
||||||
|
@ -46,14 +44,27 @@ pub trait Handler<W: Write> {
|
||||||
fn handle_keyword(&mut self, w: &mut W, key: Key<'_>, value: &str) -> Result<()>;
|
fn handle_keyword(&mut self, w: &mut W, key: Key<'_>, value: &str) -> Result<()>;
|
||||||
fn handle_rule(&mut self, w: &mut W) -> Result<()>;
|
fn handle_rule(&mut self, w: &mut W) -> Result<()>;
|
||||||
fn handle_cookie(&mut self, w: &mut W, cookie: Cookie) -> Result<()>;
|
fn handle_cookie(&mut self, w: &mut W, cookie: Cookie) -> Result<()>;
|
||||||
fn handle_fn_ref(&mut self, w: &mut W, fn_ref: FnRef) -> Result<()>;
|
fn handle_fn_ref(&mut self, w: &mut W, label: Option<&str>, def: Option<&str>) -> Result<()>;
|
||||||
fn handle_inline_call(&mut self, w: &mut W, inline_call: InlineCall) -> Result<()>;
|
fn handle_inline_call(
|
||||||
fn handle_inline_src(&mut self, w: &mut W, inline_src: InlineSrc) -> Result<()>;
|
&mut self,
|
||||||
fn handle_link(&mut self, w: &mut W, link: Link) -> Result<()>;
|
w: &mut W,
|
||||||
fn handle_macros(&mut self, w: &mut W, macros: Macros) -> Result<()>;
|
name: &str,
|
||||||
fn handle_radio_target(&mut self, w: &mut W, target: RadioTarget) -> Result<()>;
|
args: &str,
|
||||||
fn handle_snippet(&mut self, w: &mut W, snippet: Snippet) -> Result<()>;
|
inside_header: Option<&str>,
|
||||||
fn handle_target(&mut self, w: &mut W, target: Target) -> Result<()>;
|
end_header: Option<&str>,
|
||||||
|
) -> Result<()>;
|
||||||
|
fn handle_inline_src(
|
||||||
|
&mut self,
|
||||||
|
w: &mut W,
|
||||||
|
lang: &str,
|
||||||
|
option: Option<&str>,
|
||||||
|
body: &str,
|
||||||
|
) -> Result<()>;
|
||||||
|
fn handle_link(&mut self, w: &mut W, path: &str, desc: Option<&str>) -> Result<()>;
|
||||||
|
fn handle_macros(&mut self, w: &mut W, name: &str, args: Option<&str>) -> Result<()>;
|
||||||
|
fn handle_radio_target(&mut self, w: &mut W, target: &str) -> Result<()>;
|
||||||
|
fn handle_snippet(&mut self, w: &mut W, name: &str, value: &str) -> Result<()>;
|
||||||
|
fn handle_target(&mut self, w: &mut W, target: &str) -> Result<()>;
|
||||||
fn handle_bold_beg(&mut self, w: &mut W) -> Result<()>;
|
fn handle_bold_beg(&mut self, w: &mut W) -> Result<()>;
|
||||||
fn handle_bold_end(&mut self, w: &mut W) -> Result<()>;
|
fn handle_bold_end(&mut self, w: &mut W) -> Result<()>;
|
||||||
fn handle_italic_beg(&mut self, w: &mut W) -> Result<()>;
|
fn handle_italic_beg(&mut self, w: &mut W) -> Result<()>;
|
||||||
|
@ -129,14 +140,19 @@ impl<'a, W: Write, H: Handler<W>> Render<'a, W, H> {
|
||||||
Keyword { key, value } => h.handle_keyword(w, key, value)?,
|
Keyword { key, value } => h.handle_keyword(w, key, value)?,
|
||||||
Rule => h.handle_rule(w)?,
|
Rule => h.handle_rule(w)?,
|
||||||
Cookie(cookie) => h.handle_cookie(w, cookie)?,
|
Cookie(cookie) => h.handle_cookie(w, cookie)?,
|
||||||
FnRef(fnref) => h.handle_fn_ref(w, fnref)?,
|
FnRef { label, def } => h.handle_fn_ref(w, label, def)?,
|
||||||
InlineCall(inlinecall) => h.handle_inline_call(w, inlinecall)?,
|
InlineSrc { lang, option, body } => h.handle_inline_src(w, lang, option, body)?,
|
||||||
InlineSrc(inlinesrc) => h.handle_inline_src(w, inlinesrc)?,
|
InlineCall {
|
||||||
Link(link) => h.handle_link(w, link)?,
|
name,
|
||||||
Macros(macros) => h.handle_macros(w, macros)?,
|
args,
|
||||||
RadioTarget(radiotarget) => h.handle_radio_target(w, radiotarget)?,
|
inside_header,
|
||||||
Snippet(snippet) => h.handle_snippet(w, snippet)?,
|
end_header,
|
||||||
Target(target) => h.handle_target(w, target)?,
|
} => h.handle_inline_call(w, name, args, inside_header, end_header)?,
|
||||||
|
Link { path, desc } => h.handle_link(w, path, desc)?,
|
||||||
|
Macros { name, args } => h.handle_macros(w, name, args)?,
|
||||||
|
RadioTarget { target } => h.handle_radio_target(w, target)?,
|
||||||
|
Snippet { name, value } => h.handle_snippet(w, name, value)?,
|
||||||
|
Target { target } => h.handle_target(w, target)?,
|
||||||
BoldBeg => h.handle_bold_beg(w)?,
|
BoldBeg => h.handle_bold_beg(w)?,
|
||||||
BoldEnd => h.handle_bold_end(w)?,
|
BoldEnd => h.handle_bold_end(w)?,
|
||||||
ItalicBeg => h.handle_italic_beg(w)?,
|
ItalicBeg => h.handle_italic_beg(w)?,
|
||||||
|
|
123
src/headline.rs
123
src/headline.rs
|
@ -1,10 +1,17 @@
|
||||||
|
//! Headline
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Headline<'a> {
|
pub struct Headline<'a> {
|
||||||
|
/// headline level, number of stars
|
||||||
pub level: usize,
|
pub level: usize,
|
||||||
|
/// priority cookie
|
||||||
pub priority: Option<char>,
|
pub priority: Option<char>,
|
||||||
|
/// headline tags, including the sparated colons
|
||||||
pub tags: Option<&'a str>,
|
pub tags: Option<&'a str>,
|
||||||
|
/// headline title
|
||||||
pub title: &'a str,
|
pub title: &'a str,
|
||||||
|
/// headline keyword
|
||||||
pub keyword: Option<&'a str>,
|
pub keyword: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +65,20 @@ impl<'a> Headline<'a> {
|
||||||
(None, src.len())
|
(None, src.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// parsing the input string and returning the parsed headline
|
||||||
|
/// and the content-begin and the end of headline container.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use orgize::headline::Headline;
|
||||||
|
///
|
||||||
|
/// let (hdl, _, _) = Headline::parse("* DONE [#A] COMMENT Title :tag:a2%:");
|
||||||
|
///
|
||||||
|
/// assert_eq!(hdl.level, 1);
|
||||||
|
/// assert_eq!(hdl.priority, Some('A'));
|
||||||
|
/// assert_eq!(hdl.tags, Some(":tag:a2%:"));
|
||||||
|
/// assert_eq!(hdl.title, "COMMENT Title");
|
||||||
|
/// assert_eq!(hdl.keyword, Some("DONE"));
|
||||||
|
/// ```
|
||||||
pub fn parse(src: &'a str) -> (Headline<'a>, usize, usize) {
|
pub fn parse(src: &'a str) -> (Headline<'a>, usize, usize) {
|
||||||
let mut level = 0;
|
let mut level = 0;
|
||||||
loop {
|
loop {
|
||||||
|
@ -69,7 +90,11 @@ impl<'a> Headline<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let eol = eol!(src);
|
let eol = eol!(src);
|
||||||
let end = Headline::find_level(&src[eol..], level) + eol;
|
let end = if eol == src.len() {
|
||||||
|
eol
|
||||||
|
} else {
|
||||||
|
Headline::find_level(&src[eol..], level) + eol
|
||||||
|
};
|
||||||
|
|
||||||
let mut title_start = skip_space!(src, level);
|
let mut title_start = skip_space!(src, level);
|
||||||
|
|
||||||
|
@ -89,88 +114,70 @@ impl<'a> Headline<'a> {
|
||||||
|
|
||||||
let (tags, title_off) = Headline::parse_tags(&src[title_start..eol]);
|
let (tags, title_off) = Headline::parse_tags(&src[title_start..eol]);
|
||||||
|
|
||||||
// println!("{:?} {:?} {:?}", keyword, priority, tags);
|
|
||||||
// println!("{:?} {}", title_start, title_off);
|
|
||||||
|
|
||||||
(
|
(
|
||||||
Headline::new(
|
Headline {
|
||||||
level,
|
level,
|
||||||
keyword,
|
keyword,
|
||||||
priority,
|
priority,
|
||||||
&src[title_start..title_start + title_off],
|
title: &src[title_start..title_start + title_off],
|
||||||
tags,
|
tags,
|
||||||
),
|
},
|
||||||
eol,
|
eol,
|
||||||
end,
|
end,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: optimize
|
|
||||||
pub fn find_level(src: &str, level: usize) -> usize {
|
pub fn find_level(src: &str, level: usize) -> usize {
|
||||||
let mut pos = 0;
|
use jetscii::ByteSubstring;
|
||||||
loop {
|
use memchr::memchr2;
|
||||||
if pos >= src.len() {
|
|
||||||
return src.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
if src.as_bytes()[pos] == b'*' && (pos == 0 || src.as_bytes()[pos - 1] == b'\n') {
|
let bytes = src.as_bytes();
|
||||||
let pos_ = pos;
|
if bytes[0] == b'*' {
|
||||||
loop {
|
if let Some(stars) = memchr2(b'\n', b' ', bytes) {
|
||||||
if pos >= src.len() {
|
if stars > 0 && stars <= level && bytes[0..stars].iter().all(|&c| c == b'*') {
|
||||||
return src.len();
|
return 0;
|
||||||
}
|
|
||||||
if src.as_bytes()[pos] == b'*' {
|
|
||||||
pos += 1;
|
|
||||||
} else if src.as_bytes()[pos] == b' ' && pos - pos_ <= level {
|
|
||||||
return pos_;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pos += 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut pos = 0;
|
||||||
|
while let Some(off) = ByteSubstring::new(b"\n*").find(&bytes[pos..]) {
|
||||||
|
pos += off + 1;
|
||||||
|
if let Some(stars) = memchr2(b'\n', b' ', &bytes[pos..]) {
|
||||||
|
if stars > 0 && stars <= level && bytes[pos..pos + stars].iter().all(|&c| c == b'*')
|
||||||
|
{
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
src.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// checks if this headline is "commented"
|
||||||
pub fn is_commented(&self) -> bool {
|
pub fn is_commented(&self) -> bool {
|
||||||
self.title.starts_with("COMMENT ")
|
self.title.starts_with("COMMENT ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// checks if this headline is "archived"
|
||||||
pub fn is_archived(&self) -> bool {
|
pub fn is_archived(&self) -> bool {
|
||||||
self.tags
|
self.tags
|
||||||
.map(|tags| tags[1..].split_terminator(':').any(|t| t == "ARCHIVE"))
|
.map(|tags| tags[1..].split_terminator(':').any(|t| t == "ARCHIVE"))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
level: usize,
|
|
||||||
keyword: Option<&'a str>,
|
|
||||||
priority: Option<char>,
|
|
||||||
title: &'a str,
|
|
||||||
tags: Option<&'a str>,
|
|
||||||
) -> Headline<'a> {
|
|
||||||
Headline {
|
|
||||||
level,
|
|
||||||
keyword,
|
|
||||||
priority,
|
|
||||||
title,
|
|
||||||
tags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse() {
|
fn parse() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Headline::parse("**** TODO [#A] COMMENT Title :tag:a2%:").0,
|
Headline::parse("**** TODO [#A] COMMENT Title :tag:a2%:").0,
|
||||||
Headline::new(
|
Headline {
|
||||||
4,
|
level: 4,
|
||||||
Some("TODO"),
|
priority: Some('A'),
|
||||||
Some('A'),
|
keyword: Some("TODO"),
|
||||||
"COMMENT Title",
|
title: "COMMENT Title",
|
||||||
Some(":tag:a2%:"),
|
tags: Some(":tag:a2%:"),
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Headline::parse("**** ToDO [#A] COMMENT Title :tag:a2%:").0,
|
Headline::parse("**** ToDO [#A] COMMENT Title :tag:a2%:").0,
|
||||||
|
@ -262,3 +269,17 @@ fn is_archived() {
|
||||||
assert!(!Headline::parse("* Title :ARCHIVES:").0.is_archived());
|
assert!(!Headline::parse("* Title :ARCHIVES:").0.is_archived());
|
||||||
assert!(!Headline::parse("* Title :archive:").0.is_archived());
|
assert!(!Headline::parse("* Title :archive:").0.is_archived());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_level() {
|
||||||
|
assert_eq!(
|
||||||
|
Headline::find_level(
|
||||||
|
r#"
|
||||||
|
** Title
|
||||||
|
* Title
|
||||||
|
** Title"#,
|
||||||
|
1
|
||||||
|
),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
57
src/lib.rs
57
src/lib.rs
|
@ -1,3 +1,58 @@
|
||||||
|
//! A Rust library for parsing orgmode files.
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use orgize::Parser;
|
||||||
|
//!
|
||||||
|
//! fn main() {
|
||||||
|
//! let parser = Parser::new(
|
||||||
|
//! r#"* Title 1
|
||||||
|
//! *Section 1*
|
||||||
|
//! ** Title 2
|
||||||
|
//! _Section 2_
|
||||||
|
//! * Title 3
|
||||||
|
//! /Section 3/
|
||||||
|
//! * Title 4
|
||||||
|
//! =Section 4="#,
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! for event in parser {
|
||||||
|
//! // handling the event
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Alternatively, you can use the built-in render.
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use orgize::export::{HtmlHandler, Render};
|
||||||
|
//! use std::io::Cursor;
|
||||||
|
//!
|
||||||
|
//! fn main() {
|
||||||
|
//! let contents = r#"* Title 1
|
||||||
|
//! *Section 1*
|
||||||
|
//! ** Title 2
|
||||||
|
//! _Section 2_
|
||||||
|
//! * Title 3
|
||||||
|
//! /Section 3/
|
||||||
|
//! * Title 4
|
||||||
|
//! =Section 4="#;
|
||||||
|
//!
|
||||||
|
//! let cursor = Cursor::new(Vec::new());
|
||||||
|
//! let mut render = Render::new(HtmlHandler, cursor, &contents);
|
||||||
|
//!
|
||||||
|
//! render
|
||||||
|
//! .render()
|
||||||
|
//! .expect("something went wrong rendering the file");
|
||||||
|
//!
|
||||||
|
//! println!(
|
||||||
|
//! "{}",
|
||||||
|
//! String::from_utf8(render.into_wirter().into_inner()).expect("invalid utf-8")
|
||||||
|
//! );
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
@ -7,3 +62,5 @@ pub mod headline;
|
||||||
mod lines;
|
mod lines;
|
||||||
pub mod objects;
|
pub mod objects;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
|
||||||
|
pub use parser::{Event, Parser};
|
||||||
|
|
|
@ -2,74 +2,50 @@ use memchr::{memchr, memchr2};
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Cookie<'a> {
|
pub enum Cookie<'a> {
|
||||||
value: &'a str,
|
Percent(&'a str),
|
||||||
|
Slash(&'a str, &'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Cookie<'a> {
|
#[inline]
|
||||||
pub fn parse(src: &'a str) -> Option<(Cookie<'a>, usize)> {
|
pub fn parse(src: &str) -> Option<(Cookie<'_>, usize)> {
|
||||||
debug_assert!(src.starts_with('['));
|
debug_assert!(src.starts_with('['));
|
||||||
|
|
||||||
let num1 = memchr2(b'%', b'/', src.as_bytes())
|
let bytes = src.as_bytes();
|
||||||
.filter(|&i| src.as_bytes()[1..i].iter().all(|c| c.is_ascii_digit()))?;
|
let num1 =
|
||||||
|
memchr2(b'%', b'/', bytes).filter(|&i| bytes[1..i].iter().all(|c| c.is_ascii_digit()))?;
|
||||||
|
|
||||||
if src.as_bytes()[num1] == b'%' && *src.as_bytes().get(num1 + 1)? == b']' {
|
if bytes[num1] == b'%' && *bytes.get(num1 + 1)? == b']' {
|
||||||
Some((
|
Some((Cookie::Percent(&src[1..num1]), num1 + 2))
|
||||||
Cookie {
|
} else {
|
||||||
value: &src[0..=num1 + 1],
|
let num2 = memchr(b']', bytes)
|
||||||
},
|
.filter(|&i| bytes[num1 + 1..i].iter().all(|c| c.is_ascii_digit()))?;
|
||||||
num1 + 2,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
let num2 = memchr(b']', src.as_bytes()).filter(|&i| {
|
|
||||||
src.as_bytes()[num1 + 1..i]
|
|
||||||
.iter()
|
|
||||||
.all(|c| c.is_ascii_digit())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Some((
|
Some((Cookie::Slash(&src[1..num1], &src[num1 + 1..num2]), num2 + 1))
|
||||||
Cookie {
|
|
||||||
value: &src[0..=num2],
|
|
||||||
},
|
|
||||||
num2 + 1,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn parse() {
|
mod tests {
|
||||||
assert_eq!(
|
#[test]
|
||||||
Cookie::parse("[1/10]").unwrap(),
|
fn parse() {
|
||||||
(Cookie { value: "[1/10]" }, "[1/10]".len())
|
use super::parse;
|
||||||
);
|
use super::Cookie::*;
|
||||||
assert_eq!(
|
|
||||||
Cookie::parse("[1/1000]").unwrap(),
|
|
||||||
(Cookie { value: "[1/1000]" }, "[1/1000]".len())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Cookie::parse("[10%]").unwrap(),
|
|
||||||
(Cookie { value: "[10%]" }, "[10%]".len())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Cookie::parse("[%]").unwrap(),
|
|
||||||
(Cookie { value: "[%]" }, "[%]".len())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Cookie::parse("[/]").unwrap(),
|
|
||||||
(Cookie { value: "[/]" }, "[/]".len())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Cookie::parse("[100/]").unwrap(),
|
|
||||||
(Cookie { value: "[100/]" }, "[100/]".len())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Cookie::parse("[/100]").unwrap(),
|
|
||||||
(Cookie { value: "[/100]" }, "[/100]".len())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(Cookie::parse("[10% ]").is_none(),);
|
assert_eq!(parse("[1/10]").unwrap(), (Slash("1", "10"), "[1/10]".len()));
|
||||||
assert!(Cookie::parse("[1//100]").is_none(),);
|
assert_eq!(
|
||||||
assert!(Cookie::parse("[1\\100]").is_none(),);
|
parse("[1/1000]").unwrap(),
|
||||||
assert!(Cookie::parse("[10%%]").is_none(),);
|
(Slash("1", "1000"), "[1/1000]".len())
|
||||||
|
);
|
||||||
|
assert_eq!(parse("[10%]").unwrap(), (Percent("10"), "[10%]".len()));
|
||||||
|
assert_eq!(parse("[%]").unwrap(), (Percent(""), "[%]".len()));
|
||||||
|
assert_eq!(parse("[/]").unwrap(), (Slash("", ""), "[/]".len()));
|
||||||
|
assert_eq!(parse("[100/]").unwrap(), (Slash("100", ""), "[100/]".len()));
|
||||||
|
assert_eq!(parse("[/100]").unwrap(), (Slash("", "100"), "[/100]".len()));
|
||||||
|
|
||||||
|
assert!(parse("[10% ]").is_none(),);
|
||||||
|
assert!(parse("[1//100]").is_none(),);
|
||||||
|
assert!(parse("[1\\100]").is_none(),);
|
||||||
|
assert!(parse("[10%%]").is_none(),);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,59 @@
|
||||||
use memchr::memchr;
|
use memchr::memchr;
|
||||||
|
|
||||||
pub struct Emphasis;
|
#[inline]
|
||||||
|
/// returns offset
|
||||||
|
pub fn parse(src: &str, marker: u8) -> Option<usize> {
|
||||||
|
debug_assert!(src.len() >= 3);
|
||||||
|
|
||||||
impl Emphasis {
|
let bytes = src.as_bytes();
|
||||||
// TODO: return usize instead of Option<usize>
|
|
||||||
pub fn parse(src: &str, marker: u8) -> Option<usize> {
|
|
||||||
expect!(src, 1, |c: u8| !c.is_ascii_whitespace())?;
|
|
||||||
|
|
||||||
let bytes = src.as_bytes();
|
if bytes[1].is_ascii_whitespace() {
|
||||||
let end = memchr(marker, &bytes[1..])
|
return None;
|
||||||
.map(|i| i + 1)
|
}
|
||||||
.filter(|&i| bytes[1..i].iter().filter(|&&c| c == b'\n').count() < 2)?;
|
|
||||||
|
|
||||||
expect!(src, end - 1, |c: u8| !c.is_ascii_whitespace())?;
|
let end = memchr(marker, &bytes[1..])
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.filter(|&i| bytes[1..i].iter().filter(|&&c| c == b'\n').count() < 2)?;
|
||||||
|
|
||||||
if end < src.len() - 1 {
|
if bytes[end - 1].is_ascii_whitespace() {
|
||||||
expect!(src, end + 1, |ch| ch == b' '
|
return None;
|
||||||
|| ch == b'-'
|
}
|
||||||
|| ch == b'.'
|
|
||||||
|| ch == b','
|
if end < src.len() - 1 {
|
||||||
|| ch == b':'
|
let post = bytes[end + 1];
|
||||||
|| ch == b'!'
|
if post == b' '
|
||||||
|| ch == b'?'
|
|| post == b'-'
|
||||||
|| ch == b'\''
|
|| post == b'.'
|
||||||
|| ch == b'\n'
|
|| post == b','
|
||||||
|| ch == b')'
|
|| post == b':'
|
||||||
|| ch == b'}')?;
|
|| post == b'!'
|
||||||
|
|| post == b'?'
|
||||||
|
|| post == b'\''
|
||||||
|
|| post == b'\n'
|
||||||
|
|| post == b')'
|
||||||
|
|| post == b'}'
|
||||||
|
{
|
||||||
|
Some(end)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
Some(end)
|
Some(end)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn parse() {
|
mod tests {
|
||||||
assert_eq!(Emphasis::parse("*bold*", b'*').unwrap(), "*bold".len());
|
#[test]
|
||||||
assert_eq!(Emphasis::parse("*bo\nld*", b'*').unwrap(), "*bo\nld".len());
|
fn parse() {
|
||||||
assert!(Emphasis::parse("*bold*a", b'*').is_none());
|
use super::parse;
|
||||||
assert!(Emphasis::parse("*bold*", b'/').is_none());
|
|
||||||
assert!(Emphasis::parse("*bold *", b'*').is_none());
|
assert_eq!(parse("*bold*", b'*').unwrap(), "*bold".len());
|
||||||
assert!(Emphasis::parse("* bold*", b'*').is_none());
|
assert_eq!(parse("*bo\nld*", b'*').unwrap(), "*bo\nld".len());
|
||||||
assert!(Emphasis::parse("*b\nol\nd*", b'*').is_none());
|
assert!(parse("*bold*a", b'*').is_none());
|
||||||
|
assert!(parse("*bold*", b'/').is_none());
|
||||||
|
assert!(parse("*bold *", b'*').is_none());
|
||||||
|
assert!(parse("* bold*", b'*').is_none());
|
||||||
|
assert!(parse("*b\nol\nd*", b'*').is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,106 +1,73 @@
|
||||||
use memchr::{memchr2, memchr2_iter};
|
use memchr::{memchr2, memchr2_iter};
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
/// returns (footnote reference label, footnote reference definition, offset)
|
||||||
#[derive(Debug)]
|
#[inline]
|
||||||
pub struct FnRef<'a> {
|
pub fn parse(src: &str) -> Option<(Option<&str>, Option<&str>, usize)> {
|
||||||
label: Option<&'a str>,
|
debug_assert!(src.starts_with("[fn:"));
|
||||||
definition: Option<&'a str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> FnRef<'a> {
|
let bytes = src.as_bytes();
|
||||||
pub fn parse(src: &'a str) -> Option<(FnRef<'a>, usize)> {
|
let label = memchr2(b']', b':', &bytes[4..])
|
||||||
debug_assert!(src.starts_with("[fn:"));
|
.map(|i| i + 4)
|
||||||
|
.filter(|&i| {
|
||||||
|
bytes[4..i]
|
||||||
|
.iter()
|
||||||
|
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
|
||||||
|
})?;
|
||||||
|
|
||||||
let bytes = src.as_bytes();
|
if bytes[label] == b':' {
|
||||||
let label = memchr2(b']', b':', &bytes[4..])
|
let mut pairs = 1;
|
||||||
.map(|i| i + 4)
|
let def = memchr2_iter(b'[', b']', &bytes[label..])
|
||||||
.filter(|&i| {
|
.map(|i| i + label)
|
||||||
bytes[4..i]
|
.find(|&i| {
|
||||||
.iter()
|
if bytes[i] == b'[' {
|
||||||
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
|
pairs += 1;
|
||||||
|
} else {
|
||||||
|
pairs -= 1;
|
||||||
|
}
|
||||||
|
pairs == 0
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if bytes[label] == b':' {
|
Some((
|
||||||
let mut pairs = 1;
|
if label == 4 {
|
||||||
let def = memchr2_iter(b'[', b']', &bytes[label..])
|
None
|
||||||
.map(|i| i + label)
|
} else {
|
||||||
.find(|&i| {
|
Some(&src[4..label])
|
||||||
if bytes[i] == b'[' {
|
},
|
||||||
pairs += 1;
|
Some(&src[label + 1..def]),
|
||||||
} else {
|
def + 1,
|
||||||
pairs -= 1;
|
))
|
||||||
}
|
} else {
|
||||||
pairs == 0
|
Some((
|
||||||
})?;
|
if label == 4 {
|
||||||
|
None
|
||||||
Some((
|
} else {
|
||||||
FnRef {
|
Some(&src[4..label])
|
||||||
label: if label == 4 {
|
},
|
||||||
None
|
None,
|
||||||
} else {
|
label + 1,
|
||||||
Some(&src[4..label])
|
))
|
||||||
},
|
|
||||||
definition: Some(&src[label + 1..def]),
|
|
||||||
},
|
|
||||||
def + 1,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Some((
|
|
||||||
FnRef {
|
|
||||||
label: if label == 4 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(&src[4..label])
|
|
||||||
},
|
|
||||||
definition: None,
|
|
||||||
},
|
|
||||||
label + 1,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn parse() {
|
mod tests {
|
||||||
assert_eq!(
|
#[test]
|
||||||
FnRef::parse("[fn:1]").unwrap(),
|
fn parse() {
|
||||||
(
|
use super::parse;
|
||||||
FnRef {
|
|
||||||
label: Some("1"),
|
assert_eq!(parse("[fn:1]").unwrap(), (Some("1"), None, "[fn:1]".len()));
|
||||||
definition: None,
|
assert_eq!(
|
||||||
},
|
parse("[fn:1:2]").unwrap(),
|
||||||
"[fn:1]".len()
|
(Some("1"), Some("2"), "[fn:1:2]".len())
|
||||||
)
|
);
|
||||||
);
|
assert_eq!(
|
||||||
assert_eq!(
|
parse("[fn::2]").unwrap(),
|
||||||
FnRef::parse("[fn:1:2]").unwrap(),
|
(None, Some("2"), "[fn::2]".len())
|
||||||
(
|
);
|
||||||
FnRef {
|
assert_eq!(
|
||||||
label: Some("1"),
|
parse("[fn::[]]").unwrap(),
|
||||||
definition: Some("2"),
|
(None, Some("[]"), "[fn::[]]".len())
|
||||||
},
|
);
|
||||||
"[fn:1:2]".len()
|
assert!(parse("[fn::[]").is_none());
|
||||||
)
|
}
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
FnRef::parse("[fn::2]").unwrap(),
|
|
||||||
(
|
|
||||||
FnRef {
|
|
||||||
label: None,
|
|
||||||
definition: Some("2"),
|
|
||||||
},
|
|
||||||
"[fn::2]".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
FnRef::parse("[fn::[]]").unwrap(),
|
|
||||||
(
|
|
||||||
FnRef {
|
|
||||||
label: None,
|
|
||||||
definition: Some("[]"),
|
|
||||||
},
|
|
||||||
"[fn::[]]".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert!(FnRef::parse("[fn::[]").is_none());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,115 +1,88 @@
|
||||||
use memchr::{memchr, memchr2};
|
use memchr::{memchr, memchr2};
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
/// returns (name, args, inside_header, end_header)
|
||||||
#[derive(Debug)]
|
#[inline]
|
||||||
pub struct InlineCall<'a> {
|
pub fn parse(src: &str) -> Option<(&str, &str, Option<&str>, Option<&str>, usize)> {
|
||||||
pub name: &'a str,
|
debug_assert!(src.starts_with("call_"));
|
||||||
pub args: &'a str,
|
|
||||||
// header args for block
|
// TODO: refactor
|
||||||
pub inside_header: Option<&'a str>,
|
let bytes = src.as_bytes();
|
||||||
// header args for call line
|
let mut pos =
|
||||||
pub end_header: Option<&'a str>,
|
memchr2(b'[', b'(', bytes).filter(|&i| bytes[5..i].iter().all(|c| c.is_ascii_graphic()))?;
|
||||||
|
let mut pos_;
|
||||||
|
|
||||||
|
let name = &src[5..pos];
|
||||||
|
|
||||||
|
let inside_header = if bytes[pos] == b'[' {
|
||||||
|
pos_ = pos;
|
||||||
|
pos = memchr(b']', &bytes[pos..])
|
||||||
|
.map(|i| i + pos)
|
||||||
|
.filter(|&i| bytes[pos..i].iter().all(|&c| c != b'\n'))?
|
||||||
|
+ 1;
|
||||||
|
expect!(src, pos, b'(')?;
|
||||||
|
Some(&src[pos_ + 1..pos - 1])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
pos_ = pos;
|
||||||
|
pos = memchr(b')', &bytes[pos..])
|
||||||
|
.map(|i| i + pos)
|
||||||
|
.filter(|&i| bytes[pos..i].iter().all(|&c| c != b'\n'))?;
|
||||||
|
let args = &src[pos_ + 1..pos];
|
||||||
|
|
||||||
|
let end_header = if src.len() > pos + 1 && src.as_bytes()[pos + 1] == b'[' {
|
||||||
|
pos_ = pos;
|
||||||
|
pos = memchr(b']', &bytes[pos_ + 1..])
|
||||||
|
.map(|i| i + pos_ + 1)
|
||||||
|
.filter(|&i| bytes[pos_ + 1..i].iter().all(|&c| c != b'\n' && c != b')'))?;
|
||||||
|
Some(&src[pos_ + 2..pos])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Some((name, args, inside_header, end_header, pos + 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> InlineCall<'a> {
|
#[cfg(test)]
|
||||||
pub fn parse(src: &'a str) -> Option<(InlineCall, usize)> {
|
mod tests {
|
||||||
debug_assert!(src.starts_with("call_"));
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
use super::parse;
|
||||||
|
|
||||||
let bytes = src.as_bytes();
|
assert_eq!(
|
||||||
let mut pos = memchr2(b'[', b'(', bytes)
|
parse("call_square(4)").unwrap(),
|
||||||
.filter(|&i| bytes[5..i].iter().all(|c| c.is_ascii_graphic()))?;
|
("square", "4", None, None, "call_square(4)".len())
|
||||||
let mut pos_;
|
);
|
||||||
|
assert_eq!(
|
||||||
let name = &src[5..pos];
|
parse("call_square[:results output](4)").unwrap(),
|
||||||
|
(
|
||||||
let inside_header = if bytes[pos] == b'[' {
|
"square",
|
||||||
pos_ = pos;
|
"4",
|
||||||
pos = memchr(b']', &bytes[pos..])
|
Some(":results output"),
|
||||||
.map(|i| i + pos)
|
None,
|
||||||
.filter(|&i| bytes[pos..i].iter().all(|&c| c != b'\n'))?
|
"call_square[:results output](4)".len()
|
||||||
+ 1;
|
)
|
||||||
expect!(src, pos, b'(')?;
|
);
|
||||||
Some(&src[pos_ + 1..pos - 1])
|
assert_eq!(
|
||||||
} else {
|
parse("call_square(4)[:results html]").unwrap(),
|
||||||
None
|
(
|
||||||
};
|
"square",
|
||||||
|
"4",
|
||||||
pos_ = pos;
|
None,
|
||||||
pos = memchr(b')', &bytes[pos..])
|
Some(":results html"),
|
||||||
.map(|i| i + pos)
|
"call_square(4)[:results html]".len()
|
||||||
.filter(|&i| bytes[pos..i].iter().all(|&c| c != b'\n'))?;
|
)
|
||||||
let args = &src[pos_ + 1..pos];
|
);
|
||||||
|
assert_eq!(
|
||||||
let end_header = if src.len() > pos + 1 && src.as_bytes()[pos + 1] == b'[' {
|
parse("call_square[:results output](4)[:results html]").unwrap(),
|
||||||
pos_ = pos;
|
(
|
||||||
pos = memchr(b']', &bytes[pos_ + 1..])
|
"square",
|
||||||
.map(|i| i + pos_ + 1)
|
"4",
|
||||||
.filter(|&i| bytes[pos_ + 1..i].iter().all(|&c| c != b'\n' && c != b')'))?;
|
Some(":results output"),
|
||||||
Some(&src[pos_ + 2..pos])
|
Some(":results html"),
|
||||||
} else {
|
"call_square[:results output](4)[:results html]".len()
|
||||||
None
|
)
|
||||||
};
|
);
|
||||||
|
|
||||||
Some((
|
|
||||||
InlineCall {
|
|
||||||
name,
|
|
||||||
inside_header,
|
|
||||||
args,
|
|
||||||
end_header,
|
|
||||||
},
|
|
||||||
pos + 1,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
assert_eq!(
|
|
||||||
InlineCall::parse("call_square(4)").unwrap(),
|
|
||||||
(
|
|
||||||
InlineCall {
|
|
||||||
name: "square",
|
|
||||||
args: "4",
|
|
||||||
inside_header: None,
|
|
||||||
end_header: None,
|
|
||||||
},
|
|
||||||
"call_square(4)".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
InlineCall::parse("call_square[:results output](4)").unwrap(),
|
|
||||||
(
|
|
||||||
InlineCall {
|
|
||||||
name: "square",
|
|
||||||
args: "4",
|
|
||||||
inside_header: Some(":results output"),
|
|
||||||
end_header: None,
|
|
||||||
},
|
|
||||||
"call_square[:results output](4)".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
InlineCall::parse("call_square(4)[:results html]").unwrap(),
|
|
||||||
(
|
|
||||||
InlineCall {
|
|
||||||
name: "square",
|
|
||||||
args: "4",
|
|
||||||
inside_header: None,
|
|
||||||
end_header: Some(":results html"),
|
|
||||||
},
|
|
||||||
"call_square(4)[:results html]".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
InlineCall::parse("call_square[:results output](4)[:results html]").unwrap(),
|
|
||||||
(
|
|
||||||
InlineCall {
|
|
||||||
name: "square",
|
|
||||||
args: "4",
|
|
||||||
inside_header: Some(":results output"),
|
|
||||||
end_header: Some(":results html"),
|
|
||||||
},
|
|
||||||
"call_square[:results output](4)[:results html]".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,77 +1,54 @@
|
||||||
use memchr::{memchr, memchr2};
|
use memchr::{memchr, memchr2};
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
/// returns (language, option, body, offset)
|
||||||
#[derive(Debug)]
|
#[inline]
|
||||||
pub struct InlineSrc<'a> {
|
pub fn parse(src: &str) -> Option<(&str, Option<&str>, &str, usize)> {
|
||||||
pub lang: &'a str,
|
debug_assert!(src.starts_with("src_"));
|
||||||
pub option: Option<&'a str>,
|
|
||||||
pub body: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> InlineSrc<'a> {
|
let bytes = src.as_bytes();
|
||||||
pub fn parse(src: &'a str) -> Option<(InlineSrc, usize)> {
|
let lang = memchr2(b'[', b'{', bytes)
|
||||||
debug_assert!(src.starts_with("src_"));
|
.filter(|&i| i != 4 && bytes[4..i].iter().all(|c| !c.is_ascii_whitespace()))?;
|
||||||
|
|
||||||
let bytes = src.as_bytes();
|
if bytes[lang] == b'[' {
|
||||||
let lang = memchr2(b'[', b'{', bytes)
|
let option = memchr(b']', bytes).filter(|&i| bytes[lang..i].iter().all(|c| *c != b'\n'))?;
|
||||||
.filter(|&i| i != 4 && bytes[4..i].iter().all(|c| !c.is_ascii_whitespace()))?;
|
let body = memchr(b'}', &bytes[option..])
|
||||||
|
.map(|i| i + option)
|
||||||
|
.filter(|&i| bytes[option..i].iter().all(|c| *c != b'\n'))?;
|
||||||
|
|
||||||
if bytes[lang] == b'[' {
|
Some((
|
||||||
let option =
|
&src[4..lang],
|
||||||
memchr(b']', bytes).filter(|&i| bytes[lang..i].iter().all(|c| *c != b'\n'))?;
|
Some(&src[lang + 1..option]),
|
||||||
let body = memchr(b'}', &bytes[option..])
|
&src[option + 2..body],
|
||||||
.map(|i| i + option)
|
body + 1,
|
||||||
.filter(|&i| bytes[option..i].iter().all(|c| *c != b'\n'))?;
|
))
|
||||||
|
} else {
|
||||||
|
let body = memchr(b'}', bytes).filter(|&i| bytes[lang..i].iter().all(|c| *c != b'\n'))?;
|
||||||
|
|
||||||
Some((
|
Some((&src[4..lang], None, &src[lang + 1..body], body + 1))
|
||||||
InlineSrc {
|
|
||||||
lang: &src[4..lang],
|
|
||||||
option: Some(&src[lang + 1..option]),
|
|
||||||
body: &src[option + 2..body],
|
|
||||||
},
|
|
||||||
body + 1,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
let body =
|
|
||||||
memchr(b'}', bytes).filter(|&i| bytes[lang..i].iter().all(|c| *c != b'\n'))?;
|
|
||||||
|
|
||||||
Some((
|
|
||||||
InlineSrc {
|
|
||||||
lang: &src[4..lang],
|
|
||||||
option: None,
|
|
||||||
body: &src[lang + 1..body],
|
|
||||||
},
|
|
||||||
body + 1,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn parse() {
|
mod tests {
|
||||||
assert_eq!(
|
#[test]
|
||||||
InlineSrc::parse("src_C{int a = 0;}").unwrap(),
|
fn parse() {
|
||||||
(
|
use super::parse;
|
||||||
InlineSrc {
|
|
||||||
lang: "C",
|
assert_eq!(
|
||||||
option: None,
|
parse("src_C{int a = 0;}").unwrap(),
|
||||||
body: "int a = 0;"
|
("C", None, "int a = 0;", "src_C{int a = 0;}".len())
|
||||||
},
|
);
|
||||||
"src_C{int a = 0;}".len()
|
assert_eq!(
|
||||||
)
|
parse("src_xml[:exports code]{<tag>text</tag>}").unwrap(),
|
||||||
);
|
(
|
||||||
assert_eq!(
|
"xml",
|
||||||
InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>}").unwrap(),
|
Some(":exports code"),
|
||||||
(
|
"<tag>text</tag>",
|
||||||
InlineSrc {
|
"src_xml[:exports code]{<tag>text</tag>}".len()
|
||||||
lang: "xml",
|
)
|
||||||
option: Some(":exports code"),
|
);
|
||||||
body: "<tag>text</tag>"
|
assert!(parse("src_xml[:exports code]{<tag>text</tag>").is_none());
|
||||||
},
|
assert!(parse("src_[:exports code]{<tag>text</tag>}").is_none());
|
||||||
"src_xml[:exports code]{<tag>text</tag>}".len()
|
assert!(parse("src_xml[:exports code]").is_none());
|
||||||
)
|
}
|
||||||
);
|
|
||||||
assert!(InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>").is_none());
|
|
||||||
assert!(InlineSrc::parse("src_[:exports code]{<tag>text</tag>}").is_none());
|
|
||||||
assert!(InlineSrc::parse("src_xml[:exports code]").is_none());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,71 +1,42 @@
|
||||||
use memchr::memchr;
|
use memchr::memchr;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
/// returns (link path, link description, offset)
|
||||||
#[derive(Debug)]
|
#[inline]
|
||||||
pub struct Link<'a> {
|
pub fn parse(src: &str) -> Option<(&str, Option<&str>, usize)> {
|
||||||
pub path: &'a str,
|
debug_assert!(src.starts_with("[["));
|
||||||
pub desc: Option<&'a str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Link<'a> {
|
let bytes = src.as_bytes();
|
||||||
pub fn parse(src: &'a str) -> Option<(Link<'a>, usize)> {
|
let path = memchr(b']', bytes).filter(|&i| {
|
||||||
debug_assert!(src.starts_with("[["));
|
bytes[2..i]
|
||||||
|
.iter()
|
||||||
|
.all(|&c| c != b'<' && c != b'>' && c != b'\n')
|
||||||
|
})?;
|
||||||
|
|
||||||
let bytes = src.as_bytes();
|
if *bytes.get(path + 1)? == b']' {
|
||||||
let path = memchr(b']', bytes).filter(|&i| {
|
Some((&src[2..path], None, path + 2))
|
||||||
bytes[2..i]
|
} else if bytes[path + 1] == b'[' {
|
||||||
.iter()
|
let desc = memchr(b']', &bytes[path + 2..])
|
||||||
.all(|&c| c != b'<' && c != b'>' && c != b'\n')
|
.map(|i| i + path + 2)
|
||||||
})?;
|
.filter(|&i| bytes[path + 2..i].iter().all(|&c| c != b'['))?;
|
||||||
|
expect!(src, desc + 1, b']')?;
|
||||||
|
|
||||||
if *bytes.get(path + 1)? == b']' {
|
Some((&src[2..path], Some(&src[path + 2..desc]), desc + 2))
|
||||||
Some((
|
} else {
|
||||||
Link {
|
None
|
||||||
path: &src[2..path],
|
|
||||||
desc: None,
|
|
||||||
},
|
|
||||||
path + 2,
|
|
||||||
))
|
|
||||||
} else if bytes[path + 1] == b'[' {
|
|
||||||
let desc = memchr(b']', &bytes[path + 2..])
|
|
||||||
.map(|i| i + path + 2)
|
|
||||||
.filter(|&i| bytes[path + 2..i].iter().all(|&c| c != b'['))?;
|
|
||||||
expect!(src, desc + 1, b']')?;
|
|
||||||
|
|
||||||
Some((
|
|
||||||
Link {
|
|
||||||
path: &src[2..path],
|
|
||||||
desc: Some(&src[path + 2..desc]),
|
|
||||||
},
|
|
||||||
desc + 2,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[cfg(test)]
|
||||||
fn parse() {
|
mod tests {
|
||||||
assert_eq!(
|
#[test]
|
||||||
Link::parse("[[#id]]").unwrap(),
|
fn parse() {
|
||||||
(
|
use super::parse;
|
||||||
Link {
|
|
||||||
path: "#id",
|
assert_eq!(parse("[[#id]]").unwrap(), ("#id", None, "[[#id]]".len()));
|
||||||
desc: None,
|
assert_eq!(
|
||||||
},
|
parse("[[#id][desc]]").unwrap(),
|
||||||
"[[#id]]".len()
|
("#id", Some("desc"), "[[#id][desc]]".len())
|
||||||
)
|
);
|
||||||
);
|
assert!(parse("[[#id][desc]").is_none());
|
||||||
assert_eq!(
|
}
|
||||||
Link::parse("[[#id][desc]]").unwrap(),
|
|
||||||
(
|
|
||||||
Link {
|
|
||||||
path: "#id",
|
|
||||||
desc: Some("desc"),
|
|
||||||
},
|
|
||||||
"[[#id][desc]]".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert!(Link::parse("[[#id][desc]").is_none());
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,90 +1,62 @@
|
||||||
use jetscii::Substring;
|
use jetscii::Substring;
|
||||||
use memchr::memchr2;
|
use memchr::memchr2;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
/// returns (macros name, macros arguments, offset)
|
||||||
#[derive(Debug)]
|
#[inline]
|
||||||
pub struct Macros<'a> {
|
pub fn parse(src: &str) -> Option<(&str, Option<&str>, usize)> {
|
||||||
pub name: &'a str,
|
debug_assert!(src.starts_with("{{{"));
|
||||||
pub args: Option<&'a str>,
|
|
||||||
|
expect!(src, 3, |c: u8| c.is_ascii_alphabetic())?;
|
||||||
|
|
||||||
|
let bytes = src.as_bytes();
|
||||||
|
let name = memchr2(b'}', b'(', bytes).filter(|&i| {
|
||||||
|
bytes[3..i]
|
||||||
|
.iter()
|
||||||
|
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some(if bytes[name] == b'}' {
|
||||||
|
expect!(src, name + 1, b'}')?;
|
||||||
|
expect!(src, name + 2, b'}')?;
|
||||||
|
(&src[3..name], None, name + 3)
|
||||||
|
} else {
|
||||||
|
let end = Substring::new(")}}}")
|
||||||
|
.find(&src[name..])
|
||||||
|
.map(|i| i + name)?;
|
||||||
|
(
|
||||||
|
&src[3..name],
|
||||||
|
if name == end {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&src[name + 1..end])
|
||||||
|
},
|
||||||
|
end + 4,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Macros<'a> {
|
#[cfg(test)]
|
||||||
pub fn parse(src: &'a str) -> Option<(Macros<'a>, usize)> {
|
mod tests {
|
||||||
debug_assert!(src.starts_with("{{{"));
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
use super::parse;
|
||||||
|
|
||||||
expect!(src, 3, |c: u8| c.is_ascii_alphabetic())?;
|
assert_eq!(
|
||||||
|
parse("{{{poem(red,blue)}}}"),
|
||||||
|
Some(("poem", Some("red,blue"), "{{{poem(red,blue)}}}".len()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("{{{poem())}}}"),
|
||||||
|
Some(("poem", Some(")"), "{{{poem())}}}".len()))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("{{{author}}}"),
|
||||||
|
Some(("author", None, "{{{author}}}".len()))
|
||||||
|
);
|
||||||
|
|
||||||
let bytes = src.as_bytes();
|
assert_eq!(parse("{{{0uthor}}}"), None);
|
||||||
let name = memchr2(b'}', b'(', bytes).filter(|&i| {
|
assert_eq!(parse("{{{author}}"), None);
|
||||||
bytes[3..i]
|
assert_eq!(parse("{{{poem(}}}"), None);
|
||||||
.iter()
|
assert_eq!(parse("{{{poem)}}}"), None);
|
||||||
.all(|&c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_')
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Some(if bytes[name] == b'}' {
|
|
||||||
expect!(src, name + 1, b'}')?;
|
|
||||||
expect!(src, name + 2, b'}')?;
|
|
||||||
(
|
|
||||||
Macros {
|
|
||||||
name: &src[3..name],
|
|
||||||
args: None,
|
|
||||||
},
|
|
||||||
name + 3,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let end = Substring::new(")}}}")
|
|
||||||
.find(&src[name..])
|
|
||||||
.map(|i| i + name)?;
|
|
||||||
(
|
|
||||||
Macros {
|
|
||||||
name: &src[3..name],
|
|
||||||
args: if name == end {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(&src[name + 1..end])
|
|
||||||
},
|
|
||||||
},
|
|
||||||
end + 4,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
assert_eq!(
|
|
||||||
Macros::parse("{{{poem(red,blue)}}}"),
|
|
||||||
Some((
|
|
||||||
Macros {
|
|
||||||
name: "poem",
|
|
||||||
args: Some("red,blue")
|
|
||||||
},
|
|
||||||
"{{{poem(red,blue)}}}".len()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Macros::parse("{{{poem())}}}"),
|
|
||||||
Some((
|
|
||||||
Macros {
|
|
||||||
name: "poem",
|
|
||||||
args: Some(")")
|
|
||||||
},
|
|
||||||
"{{{poem())}}}".len()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Macros::parse("{{{author}}}"),
|
|
||||||
Some((
|
|
||||||
Macros {
|
|
||||||
name: "author",
|
|
||||||
args: None
|
|
||||||
},
|
|
||||||
"{{{author}}}".len()
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(Macros::parse("{{{0uthor}}}"), None);
|
|
||||||
assert_eq!(Macros::parse("{{{author}}"), None);
|
|
||||||
assert_eq!(Macros::parse("{{{poem(}}}"), None);
|
|
||||||
assert_eq!(Macros::parse("{{{poem)}}}"), None);
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,37 +5,63 @@ mod inline_call;
|
||||||
mod inline_src;
|
mod inline_src;
|
||||||
mod link;
|
mod link;
|
||||||
mod macros;
|
mod macros;
|
||||||
|
mod radio_target;
|
||||||
mod snippet;
|
mod snippet;
|
||||||
mod target;
|
mod target;
|
||||||
|
|
||||||
pub use self::cookie::Cookie;
|
pub use self::cookie::Cookie;
|
||||||
pub use self::emphasis::Emphasis;
|
|
||||||
pub use self::fn_ref::FnRef;
|
|
||||||
pub use self::inline_call::InlineCall;
|
|
||||||
pub use self::inline_src::InlineSrc;
|
|
||||||
pub use self::link::Link;
|
|
||||||
pub use self::macros::Macros;
|
|
||||||
pub use self::snippet::Snippet;
|
|
||||||
pub use self::target::{RadioTarget, Target};
|
|
||||||
use jetscii::bytes;
|
use jetscii::bytes;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq, Debug))]
|
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||||
pub enum Object<'a> {
|
pub enum Object<'a> {
|
||||||
Cookie(Cookie<'a>),
|
Cookie(Cookie<'a>),
|
||||||
FnRef(FnRef<'a>),
|
FnRef {
|
||||||
InlineCall(InlineCall<'a>),
|
label: Option<&'a str>,
|
||||||
InlineSrc(InlineSrc<'a>),
|
def: Option<&'a str>,
|
||||||
Link(Link<'a>),
|
},
|
||||||
Macros(Macros<'a>),
|
InlineCall {
|
||||||
RadioTarget(RadioTarget<'a>),
|
name: &'a str,
|
||||||
Snippet(Snippet<'a>),
|
args: &'a str,
|
||||||
Target(Target<'a>),
|
inside_header: Option<&'a str>,
|
||||||
|
end_header: Option<&'a str>,
|
||||||
|
},
|
||||||
|
InlineSrc {
|
||||||
|
lang: &'a str,
|
||||||
|
option: Option<&'a str>,
|
||||||
|
body: &'a str,
|
||||||
|
},
|
||||||
|
Link {
|
||||||
|
path: &'a str,
|
||||||
|
desc: Option<&'a str>,
|
||||||
|
},
|
||||||
|
Macros {
|
||||||
|
name: &'a str,
|
||||||
|
args: Option<&'a str>,
|
||||||
|
},
|
||||||
|
RadioTarget {
|
||||||
|
target: &'a str,
|
||||||
|
},
|
||||||
|
Snippet {
|
||||||
|
name: &'a str,
|
||||||
|
value: &'a str,
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
target: &'a str,
|
||||||
|
},
|
||||||
|
|
||||||
// `end` indicates the position of the second marker
|
// `end` indicates the position of the second marker
|
||||||
Bold { end: usize },
|
Bold {
|
||||||
Italic { end: usize },
|
end: usize,
|
||||||
Strike { end: usize },
|
},
|
||||||
Underline { end: usize },
|
Italic {
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
Strike {
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
Underline {
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
|
||||||
Verbatim(&'a str),
|
Verbatim(&'a str),
|
||||||
Code(&'a str),
|
Code(&'a str),
|
||||||
|
@ -68,40 +94,40 @@ impl<'a> Object<'a> {
|
||||||
|
|
||||||
match bytes[pos] {
|
match bytes[pos] {
|
||||||
b'@' if bytes[pos + 1] == b'@' => {
|
b'@' if bytes[pos + 1] == b'@' => {
|
||||||
if let Some((snippet, off)) = Snippet::parse(&src[pos..]) {
|
if let Some((name, value, off)) = snippet::parse(&src[pos..]) {
|
||||||
brk!(Object::Snippet(snippet), off, pos);
|
brk!(Object::Snippet { name, value }, off, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'{' if bytes[pos + 1] == b'{' && bytes[pos + 2] == b'{' => {
|
b'{' if bytes[pos + 1] == b'{' && bytes[pos + 2] == b'{' => {
|
||||||
if let Some((macros, off)) = Macros::parse(&src[pos..]) {
|
if let Some((name, args, off)) = macros::parse(&src[pos..]) {
|
||||||
brk!(Object::Macros(macros), off, pos);
|
brk!(Object::Macros { name, args }, off, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'<' if bytes[pos + 1] == b'<' => {
|
b'<' if bytes[pos + 1] == b'<' => {
|
||||||
if bytes[pos + 2] == b'<' {
|
if bytes[pos + 2] == b'<' {
|
||||||
if let Some((target, off)) = RadioTarget::parse(&src[pos..]) {
|
if let Some((target, off)) = radio_target::parse(&src[pos..]) {
|
||||||
brk!(Object::RadioTarget(target), off, pos);
|
brk!(Object::RadioTarget { target }, off, pos);
|
||||||
}
|
}
|
||||||
} else if bytes[pos + 2] != b'\n' {
|
} else if bytes[pos + 2] != b'\n' {
|
||||||
if let Some((target, off)) = Target::parse(&src[pos..]) {
|
if let Some((target, off)) = target::parse(&src[pos..]) {
|
||||||
brk!(Object::Target(target), off, pos);
|
brk!(Object::Target { target }, off, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'[' => {
|
b'[' => {
|
||||||
if bytes[pos + 1..].starts_with(b"fn:") {
|
if bytes[pos + 1..].starts_with(b"fn:") {
|
||||||
if let Some((fn_ref, off)) = FnRef::parse(&src[pos..]) {
|
if let Some((label, def, off)) = fn_ref::parse(&src[pos..]) {
|
||||||
brk!(Object::FnRef(fn_ref), off, pos);
|
brk!(Object::FnRef { label, def }, off, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes[pos + 1] == b'[' {
|
if bytes[pos + 1] == b'[' {
|
||||||
if let Some((link, off)) = Link::parse(&src[pos..]) {
|
if let Some((path, desc, off)) = link::parse(&src[pos..]) {
|
||||||
brk!(Object::Link(link), off, pos);
|
brk!(Object::Link { path, desc }, off, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((cookie, off)) = Cookie::parse(&src[pos..]) {
|
if let Some((cookie, off)) = cookie::parse(&src[pos..]) {
|
||||||
brk!(Object::Cookie(cookie), off, pos);
|
brk!(Object::Cookie(cookie), off, pos);
|
||||||
}
|
}
|
||||||
// TODO: Timestamp
|
// TODO: Timestamp
|
||||||
|
@ -112,43 +138,54 @@ impl<'a> Object<'a> {
|
||||||
|
|
||||||
match bytes[pre] {
|
match bytes[pre] {
|
||||||
b'*' => {
|
b'*' => {
|
||||||
if let Some(end) = Emphasis::parse(&src[pre..], b'*') {
|
if let Some(end) = emphasis::parse(&src[pre..], b'*') {
|
||||||
brk!(Object::Bold { end }, 1, pre);
|
brk!(Object::Bold { end }, 1, pre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'+' => {
|
b'+' => {
|
||||||
if let Some(end) = Emphasis::parse(&src[pre..], b'+') {
|
if let Some(end) = emphasis::parse(&src[pre..], b'+') {
|
||||||
brk!(Object::Strike { end }, 1, pre);
|
brk!(Object::Strike { end }, 1, pre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'/' => {
|
b'/' => {
|
||||||
if let Some(end) = Emphasis::parse(&src[pre..], b'/') {
|
if let Some(end) = emphasis::parse(&src[pre..], b'/') {
|
||||||
brk!(Object::Italic { end }, 1, pre);
|
brk!(Object::Italic { end }, 1, pre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'_' => {
|
b'_' => {
|
||||||
if let Some(end) = Emphasis::parse(&src[pre..], b'_') {
|
if let Some(end) = emphasis::parse(&src[pre..], b'_') {
|
||||||
brk!(Object::Underline { end }, 1, pre);
|
brk!(Object::Underline { end }, 1, pre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'=' => {
|
b'=' => {
|
||||||
if let Some(end) = Emphasis::parse(&src[pre..], b'=') {
|
if let Some(end) = emphasis::parse(&src[pre..], b'=') {
|
||||||
brk!(Object::Verbatim(&src[pre + 1..pre + end]), end + 1, pre);
|
brk!(Object::Verbatim(&src[pre + 1..pre + end]), end + 1, pre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'~' => {
|
b'~' => {
|
||||||
if let Some(end) = Emphasis::parse(&src[pre..], b'~') {
|
if let Some(end) = emphasis::parse(&src[pre..], b'~') {
|
||||||
brk!(Object::Code(&src[pre + 1..pre + end]), end + 1, pre);
|
brk!(Object::Code(&src[pre + 1..pre + end]), end + 1, pre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'c' if src[pre..].starts_with("call_") => {
|
b'c' if src[pre..].starts_with("call_") => {
|
||||||
if let Some((call, off)) = InlineCall::parse(&src[pre..]) {
|
if let Some((name, args, inside_header, end_header, off)) =
|
||||||
brk!(Object::InlineCall(call), off, pre);
|
inline_call::parse(&src[pre..])
|
||||||
|
{
|
||||||
|
brk!(
|
||||||
|
Object::InlineCall {
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
inside_header,
|
||||||
|
end_header,
|
||||||
|
},
|
||||||
|
off,
|
||||||
|
pre
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b's' if src[pre..].starts_with("src_") => {
|
b's' if src[pre..].starts_with("src_") => {
|
||||||
if let Some((src, off)) = InlineSrc::parse(&src[pre..]) {
|
if let Some((lang, option, body, off)) = inline_src::parse(&src[pre..]) {
|
||||||
brk!(Object::InlineSrc(src), off, pre);
|
brk!(Object::InlineSrc { lang, option, body }, off, pre);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
|
44
src/objects/radio_target.rs
Normal file
44
src/objects/radio_target.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use jetscii::Substring;
|
||||||
|
|
||||||
|
// TODO: text-markup, entities, latex-fragments, subscript and superscript
|
||||||
|
#[inline]
|
||||||
|
pub fn parse(src: &str) -> Option<(&str, usize)> {
|
||||||
|
debug_assert!(src.starts_with("<<<"));
|
||||||
|
|
||||||
|
expect!(src, 3, |c| c != b' ')?;
|
||||||
|
|
||||||
|
let end = Substring::new(">>>").find(src).filter(|&i| {
|
||||||
|
src.as_bytes()[3..i]
|
||||||
|
.iter()
|
||||||
|
.all(|&c| c != b'<' && c != b'\n' && c != b'>')
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if src.as_bytes()[end - 1] == b' ' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((&src[3..end], end + 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
use super::parse;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse("<<<target>>>").unwrap(),
|
||||||
|
("target", "<<<target>>>".len())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse("<<<tar get>>>").unwrap(),
|
||||||
|
("tar get", "<<<tar get>>>".len())
|
||||||
|
);
|
||||||
|
assert_eq!(parse("<<<target >>>"), None);
|
||||||
|
assert_eq!(parse("<<< target>>>"), None);
|
||||||
|
assert_eq!(parse("<<<ta<get>>>"), None);
|
||||||
|
assert_eq!(parse("<<<ta>get>>>"), None);
|
||||||
|
assert_eq!(parse("<<<ta\nget>>>"), None);
|
||||||
|
assert_eq!(parse("<<<target>>"), None);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,71 +1,46 @@
|
||||||
use jetscii::Substring;
|
use jetscii::Substring;
|
||||||
use memchr::memchr;
|
use memchr::memchr;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
/// returns (snippet name, snippet value, offset)
|
||||||
#[derive(Debug)]
|
#[inline]
|
||||||
pub struct Snippet<'a> {
|
pub fn parse(src: &str) -> Option<(&str, &str, usize)> {
|
||||||
pub name: &'a str,
|
debug_assert!(src.starts_with("@@"));
|
||||||
pub value: &'a str,
|
|
||||||
|
let name = memchr(b':', src.as_bytes()).filter(|&i| {
|
||||||
|
i != 2
|
||||||
|
&& src.as_bytes()[2..i]
|
||||||
|
.iter()
|
||||||
|
.all(|&c| c.is_ascii_alphanumeric() || c == b'-')
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let end = Substring::new("@@")
|
||||||
|
.find(&src[name + 1..])
|
||||||
|
.map(|i| i + name + 1)?;
|
||||||
|
|
||||||
|
Some((&src[2..name], &src[name + 1..end], end + 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Snippet<'a> {
|
#[cfg(test)]
|
||||||
pub fn parse(src: &'a str) -> Option<(Snippet<'a>, usize)> {
|
mod tests {
|
||||||
debug_assert!(src.starts_with("@@"));
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
use super::parse;
|
||||||
|
|
||||||
let name = memchr(b':', src.as_bytes()).filter(|&i| {
|
assert_eq!(
|
||||||
i != 2
|
parse("@@html:<b>@@").unwrap(),
|
||||||
&& src.as_bytes()[2..i]
|
("html", "<b>", "@@html:<b>@@".len())
|
||||||
.iter()
|
);
|
||||||
.all(|&c| c.is_ascii_alphanumeric() || c == b'-')
|
assert_eq!(
|
||||||
})?;
|
parse("@@latex:any arbitrary LaTeX code@@").unwrap(),
|
||||||
|
(
|
||||||
let end = Substring::new("@@")
|
"latex",
|
||||||
.find(&src[name + 1..])
|
"any arbitrary LaTeX code",
|
||||||
.map(|i| i + name + 1)?;
|
"@@latex:any arbitrary LaTeX code@@".len()
|
||||||
|
)
|
||||||
Some((
|
);
|
||||||
Snippet {
|
assert_eq!(parse("@@html:@@").unwrap(), ("html", "", "@@html:@@".len()));
|
||||||
name: &src[2..name],
|
assert!(parse("@@html:<b>@").is_none());
|
||||||
value: &src[name + 1..end],
|
assert!(parse("@@html<b>@@").is_none());
|
||||||
},
|
assert!(parse("@@:<b>@@").is_none());
|
||||||
end + 2,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
assert_eq!(
|
|
||||||
Snippet::parse("@@html:<b>@@").unwrap(),
|
|
||||||
(
|
|
||||||
Snippet {
|
|
||||||
name: "html",
|
|
||||||
value: "<b>"
|
|
||||||
},
|
|
||||||
"@@html:<b>@@".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Snippet::parse("@@latex:any arbitrary LaTeX code@@").unwrap(),
|
|
||||||
(
|
|
||||||
Snippet {
|
|
||||||
name: "latex",
|
|
||||||
value: "any arbitrary LaTeX code"
|
|
||||||
},
|
|
||||||
"@@latex:any arbitrary LaTeX code@@".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Snippet::parse("@@html:@@").unwrap(),
|
|
||||||
(
|
|
||||||
Snippet {
|
|
||||||
name: "html",
|
|
||||||
value: ""
|
|
||||||
},
|
|
||||||
"@@html:@@".len()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
assert!(Snippet::parse("@@html:<b>@").is_none());
|
|
||||||
assert!(Snippet::parse("@@html<b>@@").is_none());
|
|
||||||
assert!(Snippet::parse("@@:<b>@@").is_none());
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,79 +1,40 @@
|
||||||
use jetscii::Substring;
|
use jetscii::Substring;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[inline]
|
||||||
#[derive(Debug)]
|
pub fn parse(src: &str) -> Option<(&str, usize)> {
|
||||||
// TODO: text-markup, entities, latex-fragments, subscript and superscript
|
debug_assert!(src.starts_with("<<"));
|
||||||
pub struct RadioTarget<'a>(&'a str);
|
|
||||||
|
|
||||||
impl<'a> RadioTarget<'a> {
|
expect!(src, 2, |c| c != b' ')?;
|
||||||
pub fn parse(src: &'a str) -> Option<(RadioTarget<'a>, usize)> {
|
|
||||||
debug_assert!(src.starts_with("<<<"));
|
|
||||||
|
|
||||||
expect!(src, 3, |c| c != b' ')?;
|
let end = Substring::new(">>").find(src).filter(|&i| {
|
||||||
|
src.as_bytes()[2..i]
|
||||||
|
.iter()
|
||||||
|
.all(|&c| c != b'<' && c != b'\n' && c != b'>')
|
||||||
|
})?;
|
||||||
|
|
||||||
let end = Substring::new(">>>").find(src).filter(|&i| {
|
if src.as_bytes()[end - 1] == b' ' {
|
||||||
src.as_bytes()[3..i]
|
return None;
|
||||||
.iter()
|
}
|
||||||
.all(|&c| c != b'<' && c != b'\n' && c != b'>')
|
|
||||||
})?;
|
|
||||||
|
|
||||||
expect!(src, end - 1, |c| c != b' ')?;
|
Some((&src[2..end], end + 2))
|
||||||
|
}
|
||||||
|
|
||||||
Some((RadioTarget(&src[3..end]), end + 3))
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn parse() {
|
||||||
|
use super::parse;
|
||||||
|
|
||||||
|
assert_eq!(parse("<<target>>").unwrap(), ("target", "<<target>>".len()));
|
||||||
|
assert_eq!(
|
||||||
|
parse("<<tar get>>").unwrap(),
|
||||||
|
("tar get", "<<tar get>>".len())
|
||||||
|
);
|
||||||
|
assert_eq!(parse("<<target >>"), None);
|
||||||
|
assert_eq!(parse("<< target>>"), None);
|
||||||
|
assert_eq!(parse("<<ta<get>>"), None);
|
||||||
|
assert_eq!(parse("<<ta>get>>"), None);
|
||||||
|
assert_eq!(parse("<<ta\nget>>"), None);
|
||||||
|
assert_eq!(parse("<<target>"), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Target<'a>(&'a str);
|
|
||||||
|
|
||||||
impl<'a> Target<'a> {
|
|
||||||
pub fn parse(src: &'a str) -> Option<(Target<'a>, usize)> {
|
|
||||||
debug_assert!(src.starts_with("<<"));
|
|
||||||
|
|
||||||
expect!(src, 2, |c| c != b' ')?;
|
|
||||||
|
|
||||||
let end = Substring::new(">>").find(src).filter(|&i| {
|
|
||||||
src.as_bytes()[2..i]
|
|
||||||
.iter()
|
|
||||||
.all(|&c| c != b'<' && c != b'\n' && c != b'>')
|
|
||||||
})?;
|
|
||||||
|
|
||||||
expect!(src, end - 1, |c| c != b' ')?;
|
|
||||||
|
|
||||||
Some((Target(&src[2..end]), end + 2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse() {
|
|
||||||
assert_eq!(
|
|
||||||
RadioTarget::parse("<<<target>>>").unwrap(),
|
|
||||||
(RadioTarget("target"), "<<<target>>>".len())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
RadioTarget::parse("<<<tar get>>>").unwrap(),
|
|
||||||
(RadioTarget("tar get"), "<<<tar get>>>".len())
|
|
||||||
);
|
|
||||||
assert_eq!(RadioTarget::parse("<<<target >>>"), None);
|
|
||||||
assert_eq!(RadioTarget::parse("<<< target>>>"), None);
|
|
||||||
assert_eq!(RadioTarget::parse("<<<ta<get>>>"), None);
|
|
||||||
assert_eq!(RadioTarget::parse("<<<ta>get>>>"), None);
|
|
||||||
assert_eq!(RadioTarget::parse("<<<ta\nget>>>"), None);
|
|
||||||
assert_eq!(RadioTarget::parse("<<<target>>"), None);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Target::parse("<<target>>").unwrap(),
|
|
||||||
(Target("target"), "<<target>>".len())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Target::parse("<<tar get>>").unwrap(),
|
|
||||||
(Target("tar get"), "<<tar get>>".len())
|
|
||||||
);
|
|
||||||
assert_eq!(Target::parse("<<target >>"), None);
|
|
||||||
assert_eq!(Target::parse("<< target>>"), None);
|
|
||||||
assert_eq!(Target::parse("<<ta<get>>"), None);
|
|
||||||
assert_eq!(Target::parse("<<ta>get>>"), None);
|
|
||||||
assert_eq!(Target::parse("<<ta\nget>>"), None);
|
|
||||||
assert_eq!(Target::parse("<<target>"), None);
|
|
||||||
}
|
|
||||||
|
|
109
src/parser.rs
109
src/parser.rs
|
@ -1,10 +1,12 @@
|
||||||
|
//! Parser
|
||||||
|
|
||||||
use crate::elements::*;
|
use crate::elements::*;
|
||||||
use crate::headline::*;
|
use crate::headline::*;
|
||||||
use crate::objects::*;
|
use crate::objects::*;
|
||||||
|
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub enum Container {
|
enum Container {
|
||||||
Headline {
|
Headline {
|
||||||
beg: usize,
|
beg: usize,
|
||||||
end: usize,
|
end: usize,
|
||||||
|
@ -143,14 +145,39 @@ pub enum Event<'a> {
|
||||||
Rule,
|
Rule,
|
||||||
|
|
||||||
Cookie(Cookie<'a>),
|
Cookie(Cookie<'a>),
|
||||||
FnRef(FnRef<'a>),
|
FnRef {
|
||||||
InlineCall(InlineCall<'a>),
|
label: Option<&'a str>,
|
||||||
InlineSrc(InlineSrc<'a>),
|
def: Option<&'a str>,
|
||||||
Link(Link<'a>),
|
},
|
||||||
Macros(Macros<'a>),
|
InlineCall {
|
||||||
RadioTarget(RadioTarget<'a>),
|
name: &'a str,
|
||||||
Snippet(Snippet<'a>),
|
args: &'a str,
|
||||||
Target(Target<'a>),
|
inside_header: Option<&'a str>,
|
||||||
|
end_header: Option<&'a str>,
|
||||||
|
},
|
||||||
|
InlineSrc {
|
||||||
|
lang: &'a str,
|
||||||
|
option: Option<&'a str>,
|
||||||
|
body: &'a str,
|
||||||
|
},
|
||||||
|
Link {
|
||||||
|
path: &'a str,
|
||||||
|
desc: Option<&'a str>,
|
||||||
|
},
|
||||||
|
Macros {
|
||||||
|
name: &'a str,
|
||||||
|
args: Option<&'a str>,
|
||||||
|
},
|
||||||
|
RadioTarget {
|
||||||
|
target: &'a str,
|
||||||
|
},
|
||||||
|
Snippet {
|
||||||
|
name: &'a str,
|
||||||
|
value: &'a str,
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
target: &'a str,
|
||||||
|
},
|
||||||
|
|
||||||
BoldBeg,
|
BoldBeg,
|
||||||
BoldEnd,
|
BoldEnd,
|
||||||
|
@ -176,6 +203,7 @@ pub struct Parser<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Parser<'a> {
|
impl<'a> Parser<'a> {
|
||||||
|
/// creates a new parser from string
|
||||||
pub fn new(text: &'a str) -> Parser<'a> {
|
pub fn new(text: &'a str) -> Parser<'a> {
|
||||||
Parser {
|
Parser {
|
||||||
text,
|
text,
|
||||||
|
@ -187,10 +215,12 @@ impl<'a> Parser<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns current offset
|
||||||
pub fn offset(&self) -> usize {
|
pub fn offset(&self) -> usize {
|
||||||
self.off
|
self.off
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns current stack depth
|
||||||
pub fn stack_depth(&self) -> usize {
|
pub fn stack_depth(&self) -> usize {
|
||||||
self.stack.len()
|
self.stack.len()
|
||||||
}
|
}
|
||||||
|
@ -359,16 +389,26 @@ impl<'a> Parser<'a> {
|
||||||
Object::Bold { .. } => Event::BoldBeg,
|
Object::Bold { .. } => Event::BoldBeg,
|
||||||
Object::Code(c) => Event::Code(c),
|
Object::Code(c) => Event::Code(c),
|
||||||
Object::Cookie(c) => Event::Cookie(c),
|
Object::Cookie(c) => Event::Cookie(c),
|
||||||
Object::FnRef(f) => Event::FnRef(f),
|
Object::FnRef { label, def } => Event::FnRef { label, def },
|
||||||
Object::InlineCall(i) => Event::InlineCall(i),
|
Object::InlineCall {
|
||||||
Object::InlineSrc(i) => Event::InlineSrc(i),
|
name,
|
||||||
|
args,
|
||||||
|
inside_header,
|
||||||
|
end_header,
|
||||||
|
} => Event::InlineCall {
|
||||||
|
name,
|
||||||
|
args,
|
||||||
|
inside_header,
|
||||||
|
end_header,
|
||||||
|
},
|
||||||
|
Object::InlineSrc { lang, option, body } => Event::InlineSrc { lang, option, body },
|
||||||
Object::Italic { .. } => Event::ItalicBeg,
|
Object::Italic { .. } => Event::ItalicBeg,
|
||||||
Object::Link(l) => Event::Link(l),
|
Object::Link { path, desc } => Event::Link { path, desc },
|
||||||
Object::Macros(m) => Event::Macros(m),
|
Object::Macros { name, args } => Event::Macros { name, args },
|
||||||
Object::RadioTarget(r) => Event::RadioTarget(r),
|
Object::RadioTarget { target } => Event::RadioTarget { target },
|
||||||
Object::Snippet(s) => Event::Snippet(s),
|
Object::Snippet { name, value } => Event::Snippet { name, value },
|
||||||
Object::Strike { .. } => Event::StrikeBeg,
|
Object::Strike { .. } => Event::StrikeBeg,
|
||||||
Object::Target(t) => Event::Target(t),
|
Object::Target { target } => Event::Target { target },
|
||||||
Object::Text(t) => Event::Text(t),
|
Object::Text(t) => Event::Text(t),
|
||||||
Object::Underline { .. } => Event::UnderlineBeg,
|
Object::Underline { .. } => Event::UnderlineBeg,
|
||||||
Object::Verbatim(v) => Event::Verbatim(v),
|
Object::Verbatim(v) => Event::Verbatim(v),
|
||||||
|
@ -376,7 +416,7 @@ impl<'a> Parser<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_list_item(&mut self, ident: usize, end: usize) -> Event<'a> {
|
fn next_list_item(&mut self, ident: usize, end: usize) -> Event<'a> {
|
||||||
let (bullet, off, cont_end, end, has_more) = List::parse(&self.text[self.off..end], ident);
|
let (bullet, off, cont_end, end, has_more) = list::parse(&self.text[self.off..end], ident);
|
||||||
self.stack.push(Container::ListItem {
|
self.stack.push(Container::ListItem {
|
||||||
cont_end: self.off + cont_end,
|
cont_end: self.off + cont_end,
|
||||||
end: self.off + end,
|
end: self.off + end,
|
||||||
|
@ -386,6 +426,7 @@ impl<'a> Parser<'a> {
|
||||||
Event::ListItemBeg { bullet }
|
Event::ListItemBeg { bullet }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
fn end(&mut self) -> Event<'a> {
|
fn end(&mut self) -> Event<'a> {
|
||||||
match self.stack.pop().unwrap() {
|
match self.stack.pop().unwrap() {
|
||||||
Container::Bold { .. } => Event::BoldEnd,
|
Container::Bold { .. } => Event::BoldEnd,
|
||||||
|
@ -482,7 +523,13 @@ fn parse() {
|
||||||
use self::Event::*;
|
use self::Event::*;
|
||||||
|
|
||||||
let expected = vec![
|
let expected = vec![
|
||||||
HeadlineBeg(Headline::new(1, None, None, "Title 1", None)),
|
HeadlineBeg(Headline {
|
||||||
|
level: 1,
|
||||||
|
priority: None,
|
||||||
|
keyword: None,
|
||||||
|
title: "Title 1",
|
||||||
|
tags: None,
|
||||||
|
}),
|
||||||
SectionBeg,
|
SectionBeg,
|
||||||
ParagraphBeg,
|
ParagraphBeg,
|
||||||
BoldBeg,
|
BoldBeg,
|
||||||
|
@ -490,7 +537,13 @@ fn parse() {
|
||||||
BoldEnd,
|
BoldEnd,
|
||||||
ParagraphEnd,
|
ParagraphEnd,
|
||||||
SectionEnd,
|
SectionEnd,
|
||||||
HeadlineBeg(Headline::new(2, None, None, "Title 2", None)),
|
HeadlineBeg(Headline {
|
||||||
|
level: 2,
|
||||||
|
priority: None,
|
||||||
|
keyword: None,
|
||||||
|
title: "Title 2",
|
||||||
|
tags: None,
|
||||||
|
}),
|
||||||
SectionBeg,
|
SectionBeg,
|
||||||
ParagraphBeg,
|
ParagraphBeg,
|
||||||
UnderlineBeg,
|
UnderlineBeg,
|
||||||
|
@ -500,7 +553,13 @@ fn parse() {
|
||||||
SectionEnd,
|
SectionEnd,
|
||||||
HeadlineEnd,
|
HeadlineEnd,
|
||||||
HeadlineEnd,
|
HeadlineEnd,
|
||||||
HeadlineBeg(Headline::new(1, None, None, "Title 3", None)),
|
HeadlineBeg(Headline {
|
||||||
|
level: 1,
|
||||||
|
priority: None,
|
||||||
|
keyword: None,
|
||||||
|
title: "Title 3",
|
||||||
|
tags: None,
|
||||||
|
}),
|
||||||
SectionBeg,
|
SectionBeg,
|
||||||
ParagraphBeg,
|
ParagraphBeg,
|
||||||
ItalicBeg,
|
ItalicBeg,
|
||||||
|
@ -509,7 +568,13 @@ fn parse() {
|
||||||
ParagraphEnd,
|
ParagraphEnd,
|
||||||
SectionEnd,
|
SectionEnd,
|
||||||
HeadlineEnd,
|
HeadlineEnd,
|
||||||
HeadlineBeg(Headline::new(1, None, None, "Title 4", None)),
|
HeadlineBeg(Headline {
|
||||||
|
level: 1,
|
||||||
|
priority: None,
|
||||||
|
keyword: None,
|
||||||
|
title: "Title 4",
|
||||||
|
tags: None,
|
||||||
|
}),
|
||||||
SectionBeg,
|
SectionBeg,
|
||||||
ParagraphBeg,
|
ParagraphBeg,
|
||||||
Verbatim("Section 4"),
|
Verbatim("Section 4"),
|
||||||
|
|
Loading…
Reference in a new issue