chore: prepare for v0.10.0-alpha.1
This commit is contained in:
parent
9d7852c4f9
commit
af7c305c9e
111 changed files with 9132 additions and 9148 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[registries.crates-io]
|
||||
protocol = "sparse"
|
||||
55
Cargo.toml
55
Cargo.toml
|
|
@ -1,13 +1,13 @@
|
|||
[package]
|
||||
name = "orgize"
|
||||
version = "0.9.0"
|
||||
version = "0.10.0-alpha.1"
|
||||
authors = ["PoiScript <poiscript@gmail.com>"]
|
||||
description = "A Rust library for parsing orgmode files."
|
||||
description = "A Rust library for parsing org-mode files."
|
||||
repository = "https://github.com/PoiScript/orgize"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
keywords = ["orgmode", "emacs", "parser"]
|
||||
keywords = ["orgmode", "org-mode", "emacs", "parser"]
|
||||
exclude = ["/wasm", "/.github"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
|
@ -16,36 +16,39 @@ all-features = true
|
|||
[badges]
|
||||
travis-ci = { repository = "PoiScript/orgize" }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
opt-level = "s"
|
||||
|
||||
[features]
|
||||
default = ["ser"]
|
||||
wasm = ["serde-wasm-bindgen", "wasm-bindgen", "wee_alloc"]
|
||||
ser = ["serde", "serde_indextree", "indexmap/serde-1"]
|
||||
default = []
|
||||
indexmap = ["dep:indexmap"]
|
||||
chrono = ["dep:chrono"]
|
||||
|
||||
[workspace]
|
||||
members = [".", "./wasm"]
|
||||
|
||||
[dependencies]
|
||||
bytecount = "0.6"
|
||||
chrono = { version = "0.4", optional = true }
|
||||
indextree = "4.3"
|
||||
indexmap = { version = "1.9", optional = true }
|
||||
jetscii = "0.5"
|
||||
lazy_static = "1.4"
|
||||
memchr = "2.4"
|
||||
nom = { version = "7.0", default-features = false, features = ["std"] }
|
||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||
serde_indextree = { version = "0.2", optional = true }
|
||||
syntect = { version = "4.6", optional = true }
|
||||
indexmap = { version = "1.7", features = ["serde-1"], optional = true }
|
||||
# wasm stuff
|
||||
serde-wasm-bindgen = { version = "0.3", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
wee_alloc = { version = "0.4", optional = true }
|
||||
memchr = "2.5"
|
||||
nom = { version = "7.1", default-features = false, features = ["std"] }
|
||||
rowan = "0.15"
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.0"
|
||||
criterion = "0.4"
|
||||
pretty_assertions = "1.3"
|
||||
insta = "1.29"
|
||||
serde_json = "1.0"
|
||||
slugify = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
||||
|
||||
[profile.dev.package]
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[profile.bench]
|
||||
debug = true
|
||||
|
||||
[[bench]]
|
||||
name = "parse"
|
||||
harness = false
|
||||
|
|
|
|||
240
README.md
240
README.md
|
|
@ -1,210 +1,66 @@
|
|||
# Orgize
|
||||
A Rust library for parsing org-mode files.
|
||||
|
||||
[](https://travis-ci.org/PoiScript/orgize)
|
||||
[](https://crates.io/crates/orgize)
|
||||
[](https://docs.rs/orgize)
|
||||
[](https://docs.rs/orgize)
|
||||

|
||||
|
||||
A Rust library for parsing orgmode files.
|
||||
# Parse
|
||||
|
||||
[Live demo](https://orgize.herokuapp.com/)
|
||||
To parse a org-mode string, simply invoking the `Org::parse` function:
|
||||
|
||||
## Parse
|
||||
```rust
|
||||
use orgize::{Org, rowan::ast::AstNode};
|
||||
|
||||
To parse a orgmode string, simply invoking the `Org::parse` function:
|
||||
let org = Org::parse("* DONE Title :tag:");
|
||||
assert_eq!(
|
||||
format!("{:#?}", org.document().syntax()),
|
||||
r#"DOCUMENT@0..18
|
||||
HEADLINE@0..18
|
||||
HEADLINE_STARS@0..1 "*"
|
||||
WHITESPACE@1..2 " "
|
||||
HEADLINE_KEYWORD@2..6 "DONE"
|
||||
WHITESPACE@6..7 " "
|
||||
HEADLINE_TITLE@7..13
|
||||
TEXT@7..13 "Title "
|
||||
HEADLINE_TAGS@13..18
|
||||
COLON@13..14 ":"
|
||||
TEXT@14..17 "tag"
|
||||
COLON@17..18 ":"
|
||||
"#);
|
||||
```
|
||||
|
||||
use `ParseConfig::parse` to specific a custom parse config
|
||||
|
||||
```rust
|
||||
use orgize::{Org, ParseConfig, ast::Headline};
|
||||
|
||||
let config = ParseConfig {
|
||||
// custom todo keywords
|
||||
todo_keywords: (vec!["TASK".to_string()], vec![]),
|
||||
..Default::default()
|
||||
};
|
||||
let org = config.parse("* TASK Title 1");
|
||||
let hdl = org.first_node::<Headline>().unwrap();
|
||||
assert_eq!(hdl.keyword().unwrap().text(), "TASK");
|
||||
```
|
||||
|
||||
# Render to html
|
||||
|
||||
Call the `Org::to_html` function to export org element tree to html:
|
||||
|
||||
```rust
|
||||
use orgize::Org;
|
||||
|
||||
Org::parse("* DONE Title :tag:");
|
||||
```
|
||||
|
||||
or `Org::parse_custom`:
|
||||
|
||||
``` rust
|
||||
use orgize::{Org, ParseConfig};
|
||||
|
||||
Org::parse_custom(
|
||||
"* TASK Title 1",
|
||||
&ParseConfig {
|
||||
// custom todo keywords
|
||||
todo_keywords: (vec!["TASK".to_string()], vec![]),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Iter
|
||||
|
||||
`Org::iter` function will returns an iterator of `Event`s, which is
|
||||
a simple wrapper of `Element`.
|
||||
|
||||
```rust
|
||||
use orgize::Org;
|
||||
|
||||
for event in Org::parse("* DONE Title :tag:").iter() {
|
||||
// handling the event
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: whether an element is container or not, it will appears twice in one loop.
|
||||
One as `Event::Start(element)`, one as `Event::End(element)`.
|
||||
|
||||
## Render html
|
||||
|
||||
You can call the `Org::write_html` function to generate html directly, which
|
||||
uses the `DefaultHtmlHandler` internally:
|
||||
|
||||
```rust
|
||||
use orgize::Org;
|
||||
|
||||
let mut writer = Vec::new();
|
||||
Org::parse("* title\n*section*").write_html(&mut writer).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(writer).unwrap(),
|
||||
Org::parse("* title\n*section*").to_html(),
|
||||
"<main><h1>title</h1><section><p><b>section</b></p></section></main>"
|
||||
);
|
||||
```
|
||||
|
||||
## Render html with custom `HtmlHandler`
|
||||
Checkout `examples/html-slugify.rs` on how to customizing html export process.
|
||||
|
||||
To customize html rendering, simply implementing `HtmlHandler` trait and passing
|
||||
it to the `Org::wirte_html_custom` function.
|
||||
# Features
|
||||
|
||||
The following code demonstrates how to add a id for every headline and return
|
||||
own error type while rendering.
|
||||
- `chrono`: adds the ability to convert `Timestamp` into `chrono::NaiveDateTime`, disabled by default.
|
||||
|
||||
```rust
|
||||
use std::convert::From;
|
||||
use std::io::{Error as IOError, Write};
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use orgize::export::{DefaultHtmlHandler, HtmlHandler};
|
||||
use orgize::{Element, Org};
|
||||
use slugify::slugify;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MyError {
|
||||
IO(IOError),
|
||||
Heading,
|
||||
Utf8(FromUtf8Error),
|
||||
}
|
||||
|
||||
// From<std::io::Error> trait is required for custom error type
|
||||
impl From<IOError> for MyError {
|
||||
fn from(err: IOError) -> Self {
|
||||
MyError::IO(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for MyError {
|
||||
fn from(err: FromUtf8Error) -> Self {
|
||||
MyError::Utf8(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyHtmlHandler(DefaultHtmlHandler);
|
||||
|
||||
impl HtmlHandler<MyError> for MyHtmlHandler {
|
||||
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
|
||||
if let Element::Title(title) = element {
|
||||
if title.level > 6 {
|
||||
return Err(MyError::Heading);
|
||||
} else {
|
||||
write!(
|
||||
w,
|
||||
"<h{0}><a id=\"{1}\" href=\"#{1}\">",
|
||||
title.level,
|
||||
slugify!(&title.raw),
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
// fallthrough to default handler
|
||||
self.0.start(w, element)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
|
||||
if let Element::Title(title) = element {
|
||||
write!(w, "</a></h{}>", title.level)?;
|
||||
} else {
|
||||
self.0.end(w, element)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), MyError> {
|
||||
let mut writer = Vec::new();
|
||||
let mut handler = MyHtmlHandler::default();
|
||||
Org::parse("* title\n*section*").wirte_html_custom(&mut writer, &mut handler)?;
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(writer)?,
|
||||
"<main><h1><a id=\"title\" href=\"#title\">title</a></h1>\
|
||||
<section><p><b>section</b></p></section></main>"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: as I mentioned above, each element will appears two times while iterating.
|
||||
And handler will silently ignores all end events from non-container elements.
|
||||
|
||||
So if you want to change how a non-container element renders, just redefine the `start`
|
||||
function and leave the `end` function unchanged.
|
||||
|
||||
## Serde
|
||||
|
||||
`Org` struct have already implemented serde's `Serialize` trait. It means you can
|
||||
serialize it into any format supported by serde, such as json:
|
||||
|
||||
```rust
|
||||
use orgize::Org;
|
||||
use serde_json::{json, to_string};
|
||||
|
||||
let org = Org::parse("I 'm *bold*.");
|
||||
println!("{}", to_string(&org).unwrap());
|
||||
|
||||
// {
|
||||
// "type": "document",
|
||||
// "children": [{
|
||||
// "type": "section",
|
||||
// "children": [{
|
||||
// "type": "paragraph",
|
||||
// "children":[{
|
||||
// "type": "text",
|
||||
// "value":"I 'm "
|
||||
// }, {
|
||||
// "type": "bold",
|
||||
// "children":[{
|
||||
// "type": "text",
|
||||
// "value": "bold"
|
||||
// }]
|
||||
// }, {
|
||||
// "type":"text",
|
||||
// "value":"."
|
||||
// }]
|
||||
// }]
|
||||
// }]
|
||||
// }
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
By now, orgize provides four features:
|
||||
|
||||
+ `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default.
|
||||
|
||||
+ `chrono`: adds the ability to convert `Datetime` into `chrono` structs, disabled by default.
|
||||
|
||||
+ `syntect`: provides `SyntectHtmlHandler` for highlighting code block, disabled by default.
|
||||
|
||||
+ `indexmap`: Uses `IndexMap` instead of `HashMap` for properties to preserve their order, disabled by default.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
- `indexmap`: adds the ability to convert `PropertyDrawer` properties into `IndexMap`, disabled by default.
|
||||
|
|
|
|||
|
|
@ -1,30 +1,24 @@
|
|||
#![feature(test)]
|
||||
|
||||
extern crate test;
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
use orgize::Org;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn org_syntax(b: &mut Bencher) {
|
||||
// wget https://orgmode.org/worg/sources/dev/org-syntax.org
|
||||
b.iter(|| {
|
||||
Org::parse(include_str!("org-syntax.org"));
|
||||
})
|
||||
const INPUT: &[(&str, &str)] = &[
|
||||
// ("org-syntax.org", include_str!("./org-syntax.org")),
|
||||
("doc.org", include_str!("./doc.org")),
|
||||
("org-faq.org", include_str!("./org-faq.org")),
|
||||
];
|
||||
|
||||
pub fn bench_parse(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Parse");
|
||||
|
||||
for (id, json) in INPUT.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("Rowan", id), json, |b, i| {
|
||||
b.iter(|| Org::parse(i))
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn doc(b: &mut Bencher) {
|
||||
// wget https://orgmode.org/worg/sources/doc.org
|
||||
b.iter(|| {
|
||||
Org::parse(include_str!("doc.org"));
|
||||
})
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn org_faq(b: &mut Bencher) {
|
||||
// wget https://orgmode.org/worg/sources/org-faq.org
|
||||
b.iter(|| {
|
||||
Org::parse(include_str!("org-faq.org"));
|
||||
})
|
||||
}
|
||||
criterion_group!(benches, bench_parse);
|
||||
criterion_main!(benches);
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
use std::convert::From;
|
||||
use std::env::args;
|
||||
use std::fs;
|
||||
use std::io::{Error as IOError, Write};
|
||||
use std::result::Result;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
use orgize::export::{DefaultHtmlHandler, HtmlHandler};
|
||||
use orgize::{Element, Org};
|
||||
use slugify::slugify;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MyError {
|
||||
IO(IOError),
|
||||
Heading,
|
||||
Utf8(FromUtf8Error),
|
||||
}
|
||||
|
||||
// From<std::io::Error> trait is required for custom error type
|
||||
impl From<IOError> for MyError {
|
||||
fn from(err: IOError) -> Self {
|
||||
MyError::IO(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for MyError {
|
||||
fn from(err: FromUtf8Error) -> Self {
|
||||
MyError::Utf8(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyHtmlHandler(DefaultHtmlHandler);
|
||||
|
||||
impl HtmlHandler<MyError> for MyHtmlHandler {
|
||||
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
|
||||
if let Element::Title(title) = element {
|
||||
if title.level > 6 {
|
||||
return Err(MyError::Heading);
|
||||
} else {
|
||||
write!(
|
||||
w,
|
||||
"<h{0}><a id=\"{1}\" href=\"#{1}\">",
|
||||
title.level,
|
||||
slugify!(&title.raw),
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
// fallthrough to default handler
|
||||
self.0.start(w, element)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
|
||||
if let Element::Title(title) = element {
|
||||
write!(w, "</a></h{}>", title.level)?;
|
||||
} else {
|
||||
self.0.end(w, element)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), MyError> {
|
||||
let args: Vec<_> = args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} <org-file>", args[0]);
|
||||
} else {
|
||||
let contents = String::from_utf8(fs::read(&args[1])?)?;
|
||||
|
||||
let mut writer = Vec::new();
|
||||
let mut handler = MyHtmlHandler::default();
|
||||
Org::parse(&contents).write_html_custom(&mut writer, &mut handler)?;
|
||||
|
||||
println!("{}", String::from_utf8(writer)?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
64
examples/html-slugify.rs
Normal file
64
examples/html-slugify.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//! ```bash
|
||||
//! cargo run --example html-slugify '* hello world!'
|
||||
//! ```
|
||||
|
||||
use orgize::{
|
||||
ast::HeadlineTitle,
|
||||
export::{HtmlExport, TraversalContext, Traverser},
|
||||
forward_handler,
|
||||
rowan::{ast::AstNode, WalkEvent},
|
||||
Org,
|
||||
};
|
||||
use slugify::slugify;
|
||||
use std::cmp::min;
|
||||
use std::env::args;
|
||||
|
||||
#[derive(Default)]
|
||||
struct MyHtmlHandler(pub HtmlExport);
|
||||
|
||||
// AsMut trait is required for using forward_handler macros
|
||||
impl AsMut<HtmlExport> for MyHtmlHandler {
|
||||
fn as_mut(&mut self) -> &mut HtmlExport {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Traverser for MyHtmlHandler {
|
||||
fn headline_title(&mut self, event: WalkEvent<&HeadlineTitle>, _ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
WalkEvent::Enter(title) => {
|
||||
let level = title.headline().and_then(|h| h.level()).unwrap_or(1);
|
||||
let level = min(level, 6);
|
||||
let raw = title.syntax().to_string();
|
||||
self.0.output += &format!("<h{level}><a id=\"{0}\" href=\"#{0}\">", slugify!(&raw));
|
||||
}
|
||||
WalkEvent::Leave(title) => {
|
||||
let level = title.headline().and_then(|h| h.level()).unwrap_or(1);
|
||||
let level = min(level, 6);
|
||||
self.0.output += &format!("</a></h{level}>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
forward_handler! {
|
||||
HtmlExport,
|
||||
link text document headline paragraph section rule comment
|
||||
inline_src inline_call code bold verbatim italic strike underline list list_item list_item_tag
|
||||
special_block quote_block center_block verse_block comment_block example_block export_block
|
||||
source_block babel_call clock cookie radio_target drawer dyn_block fn_def fn_ref macros
|
||||
snippet timestamp target fixed_width org_table org_table_row org_table_cell list_item_content
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<_> = args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} <org-mode-string>", args[0]);
|
||||
} else {
|
||||
let mut handler = MyHtmlHandler::default();
|
||||
Org::parse(&args[1]).traverse(&mut handler);
|
||||
|
||||
println!("{}", handler.0.finish());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
use orgize::Org;
|
||||
use std::env::args;
|
||||
use std::fs;
|
||||
use std::io::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} <org-file>", args[0]);
|
||||
} else {
|
||||
let contents = String::from_utf8(fs::read(&args[1])?).unwrap();
|
||||
|
||||
for event in Org::parse(&contents).iter() {
|
||||
println!("{:?}", event);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
use orgize::Org;
|
||||
use serde_json::to_string;
|
||||
use std::env::args;
|
||||
use std::fs;
|
||||
use std::io::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = args().collect();
|
||||
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} <org-file>", args[0]);
|
||||
} else {
|
||||
let contents = String::from_utf8(fs::read(&args[1])?).unwrap();
|
||||
println!("{}", to_string(&Org::parse(&contents)).unwrap());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
1
fuzz/.gitignore
vendored
1
fuzz/.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
[package]
|
||||
name = "orgize-fuzz"
|
||||
version = "0.0.1"
|
||||
authors = ["Automatically generated"]
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer-sys.git" }
|
||||
orgize = { path = ".." }
|
||||
libfuzzer-sys = "0.4"
|
||||
|
||||
[dependencies.orgize]
|
||||
path = ".."
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_target_1"
|
||||
path = "fuzz_targets/fuzz_target_1.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
#![no_main]
|
||||
|
||||
#[macro_use]
|
||||
extern crate libfuzzer_sys;
|
||||
extern crate orgize;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use orgize::syntax::{HtmlHandler, Org};
|
||||
use std::str;
|
||||
|
||||
use orgize::Org;
|
||||
|
||||
#[cfg_attr(rustfmt, rustfmt_skip)]
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
let _ = Org::parse(s);
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(utf8) = str::from_utf8(data) {
|
||||
let _ = Org::parse(utf8);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
67
src/ast/drawer.rs
Normal file
67
src/ast/drawer.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use super::{filter_token, SyntaxKind::*};
|
||||
use crate::{ast::PropertyDrawer, syntax::SyntaxToken};
|
||||
|
||||
impl PropertyDrawer {
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::PropertyDrawer};
|
||||
///
|
||||
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:ID: id\n:END:");
|
||||
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
|
||||
/// assert_eq!(drawer.iter().count(), 2);
|
||||
/// ```
|
||||
pub fn iter(&self) -> impl Iterator<Item = (SyntaxToken, SyntaxToken)> {
|
||||
self.node_properties().filter_map(|property| {
|
||||
let mut texts = property
|
||||
.syntax
|
||||
.children_with_tokens()
|
||||
.filter_map(filter_token(TEXT));
|
||||
|
||||
Some((texts.next()?, texts.next()?))
|
||||
})
|
||||
}
|
||||
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::PropertyDrawer};
|
||||
///
|
||||
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:ID: id\n:END:");
|
||||
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
|
||||
/// assert_eq!(drawer.get("CUSTOM_ID").unwrap().text(), "someid");
|
||||
/// assert_eq!(drawer.get("ID").unwrap().text(), "id");
|
||||
/// ```
|
||||
pub fn get(&self, key: &str) -> Option<SyntaxToken> {
|
||||
self.iter()
|
||||
.find_map(|(k, v)| (k.text() == key).then_some(v))
|
||||
}
|
||||
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::PropertyDrawer};
|
||||
///
|
||||
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:CUSTOM_ID: id\n:END:");
|
||||
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
|
||||
/// let map = drawer.to_hash_map();
|
||||
/// assert_eq!(map.len(), 1);
|
||||
/// assert_eq!(map.get("CUSTOM_ID").unwrap(), "id");
|
||||
/// ```
|
||||
pub fn to_hash_map(&self) -> HashMap<String, String> {
|
||||
self.iter()
|
||||
.map(|(k, v)| (k.text().into(), v.text().into()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexmap")]
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::PropertyDrawer};
|
||||
///
|
||||
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:ID: id\n:END:");
|
||||
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
|
||||
/// let map = drawer.to_index_map();
|
||||
/// assert_eq!(map.get_index(1).unwrap(), (&"ID".to_string(), &"id".to_string()));
|
||||
/// ```
|
||||
pub fn to_index_map(&self) -> indexmap::IndexMap<String, String> {
|
||||
self.iter()
|
||||
.map(|(k, v)| (k.text().into(), v.text().into()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
362
src/ast/generate.js
Normal file
362
src/ast/generate.js
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
const nodes = [
|
||||
{
|
||||
struct: "Document",
|
||||
kind: ["DOCUMENT"],
|
||||
pre_blank: true,
|
||||
first_child: [
|
||||
["section", "Section"],
|
||||
["first_headline", "Headline"],
|
||||
],
|
||||
last_child: [["last_headline", "Headline"]],
|
||||
children: [["headlines", "Headline"]],
|
||||
},
|
||||
{
|
||||
struct: "Section",
|
||||
kind: ["SECTION"],
|
||||
post_blank: true,
|
||||
},
|
||||
{
|
||||
struct: "Paragraph",
|
||||
kind: ["PARAGRAPH"],
|
||||
post_blank: true,
|
||||
},
|
||||
{
|
||||
struct: "Headline",
|
||||
kind: ["HEADLINE"],
|
||||
first_child: [
|
||||
["title", "HeadlineTitle"],
|
||||
["section", "Section"],
|
||||
["tags", "HeadlineTags"],
|
||||
["planning", "Planning"],
|
||||
["priority", "HeadlinePriority"],
|
||||
],
|
||||
children: [["headlines", "Headline"]],
|
||||
token: [
|
||||
["stars", "HEADLINE_STARS"],
|
||||
["keyword", "HEADLINE_KEYWORD"],
|
||||
],
|
||||
post_blank: true,
|
||||
},
|
||||
{
|
||||
struct: "HeadlineStars",
|
||||
kind: ["HEADLINE_STARS"],
|
||||
parent: [["headline", "Headline"]],
|
||||
},
|
||||
{
|
||||
struct: "HeadlineTitle",
|
||||
kind: ["HEADLINE_TITLE"],
|
||||
parent: [["headline", "Headline"]],
|
||||
},
|
||||
{
|
||||
struct: "HeadlineKeyword",
|
||||
kind: ["HEADLINE_KEYWORD"],
|
||||
parent: [["headline", "Headline"]],
|
||||
},
|
||||
{
|
||||
struct: "HeadlinePriority",
|
||||
kind: ["HEADLINE_PRIORITY"],
|
||||
parent: [["headline", "Headline"]],
|
||||
token: [["text", "TEXT"]],
|
||||
},
|
||||
{
|
||||
struct: "HeadlineTags",
|
||||
kind: ["HEADLINE_TAGS"],
|
||||
parent: [["headline", "Headline"]],
|
||||
},
|
||||
{
|
||||
struct: "PropertyDrawer",
|
||||
kind: ["PROPERTY_DRAWER"],
|
||||
children: [["node_properties", "NodeProperty"]],
|
||||
},
|
||||
{
|
||||
struct: "NodeProperty",
|
||||
kind: ["NODE_PROPERTY"],
|
||||
},
|
||||
{
|
||||
struct: "Planning",
|
||||
kind: ["PLANNING"],
|
||||
last_child: [
|
||||
["deadline", "PlanningDeadline"],
|
||||
["scheduled", "PlanningScheduled"],
|
||||
["closed", "PlanningClosed"],
|
||||
],
|
||||
},
|
||||
{
|
||||
struct: "PlanningDeadline",
|
||||
kind: ["PLANNING_DEADLINE"],
|
||||
},
|
||||
{
|
||||
struct: "PlanningScheduled",
|
||||
kind: ["PLANNING_SCHEDULED"],
|
||||
},
|
||||
{
|
||||
struct: "PlanningClosed",
|
||||
kind: ["PLANNING_CLOSED"],
|
||||
},
|
||||
{
|
||||
struct: "OrgTable",
|
||||
kind: ["ORG_TABLE"],
|
||||
post_blank: true,
|
||||
},
|
||||
{
|
||||
struct: "OrgTableRow",
|
||||
kind: ["ORG_TABLE_RULE_ROW", "ORG_TABLE_STANDARD_ROW"],
|
||||
},
|
||||
{
|
||||
struct: "OrgTableCell",
|
||||
kind: ["ORG_TABLE_CELL"],
|
||||
},
|
||||
{
|
||||
struct: "List",
|
||||
kind: ["LIST"],
|
||||
children: [["items", "ListItem"]],
|
||||
},
|
||||
{
|
||||
struct: "ListItem",
|
||||
kind: ["LIST_ITEM"],
|
||||
first_child: [["content", "ListItemContent"]],
|
||||
token: [
|
||||
["indent", "LIST_ITEM_INDENT"],
|
||||
["bullet", "LIST_ITEM_BULLET"],
|
||||
],
|
||||
},
|
||||
{
|
||||
struct: "ListItemIndent",
|
||||
kind: ["LIST_ITEM_INDENT"],
|
||||
},
|
||||
{
|
||||
struct: "ListItemTag",
|
||||
kind: ["LIST_ITEM_TAG"],
|
||||
},
|
||||
{
|
||||
struct: "ListItemBullet",
|
||||
kind: ["LIST_ITEM_BULLET"],
|
||||
},
|
||||
{
|
||||
struct: "ListItemContent",
|
||||
kind: ["LIST_ITEM_CONTENT"],
|
||||
},
|
||||
{
|
||||
struct: "Drawer",
|
||||
kind: ["DRAWER"],
|
||||
},
|
||||
{
|
||||
struct: "DynBlock",
|
||||
kind: ["DYN_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "Keyword",
|
||||
kind: ["KEYWORD"],
|
||||
},
|
||||
{
|
||||
struct: "BabelCall",
|
||||
kind: ["BABEL_CALL"],
|
||||
},
|
||||
{
|
||||
struct: "TableEl",
|
||||
kind: ["TABLE_EL"],
|
||||
post_blank: true,
|
||||
},
|
||||
{
|
||||
struct: "Clock",
|
||||
kind: ["CLOCK"],
|
||||
post_blank: true,
|
||||
},
|
||||
{
|
||||
struct: "FnDef",
|
||||
kind: ["FN_DEF"],
|
||||
post_blank: true,
|
||||
},
|
||||
{
|
||||
struct: "Comment",
|
||||
kind: ["COMMENT"],
|
||||
post_blank: true,
|
||||
token: [["text", "TEXT"]],
|
||||
},
|
||||
{
|
||||
struct: "Rule",
|
||||
kind: ["RULE"],
|
||||
post_blank: true,
|
||||
},
|
||||
{
|
||||
struct: "FixedWidth",
|
||||
kind: ["FIXED_WIDTH"],
|
||||
post_blank: true,
|
||||
token: [["text", "TEXT"]],
|
||||
},
|
||||
{
|
||||
struct: "SpecialBlock",
|
||||
kind: ["SPECIAL_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "QuoteBlock",
|
||||
kind: ["QUOTE_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "CenterBlock",
|
||||
kind: ["CENTER_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "VerseBlock",
|
||||
kind: ["VERSE_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "CommentBlock",
|
||||
kind: ["COMMENT_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "ExampleBlock",
|
||||
kind: ["EXAMPLE_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "ExportBlock",
|
||||
kind: ["EXPORT_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "SourceBlock",
|
||||
kind: ["SOURCE_BLOCK"],
|
||||
},
|
||||
{
|
||||
struct: "InlineCall",
|
||||
kind: ["INLINE_CALL"],
|
||||
},
|
||||
{
|
||||
struct: "InlineSrc",
|
||||
kind: ["INLINE_SRC"],
|
||||
},
|
||||
{
|
||||
struct: "Link",
|
||||
kind: ["LINK"],
|
||||
token: [["path", "LINK_PATH"]],
|
||||
},
|
||||
{
|
||||
struct: "Cookie",
|
||||
kind: ["COOKIE"],
|
||||
},
|
||||
{
|
||||
struct: "RadioTarget",
|
||||
kind: ["RADIO_TARGET"],
|
||||
},
|
||||
{
|
||||
struct: "FnRef",
|
||||
kind: ["FN_REF"],
|
||||
},
|
||||
{
|
||||
struct: "LatexEnvironment",
|
||||
kind: ["LATEX_ENVIRONMENT"],
|
||||
},
|
||||
{
|
||||
struct: "Macros",
|
||||
kind: ["MACROS"],
|
||||
},
|
||||
{
|
||||
struct: "MacrosArgument",
|
||||
kind: ["MACROS_ARGUMENT"],
|
||||
},
|
||||
{
|
||||
struct: "Snippet",
|
||||
kind: ["SNIPPET"],
|
||||
token: [["name", "TEXT"]],
|
||||
},
|
||||
{
|
||||
struct: "Target",
|
||||
kind: ["TARGET"],
|
||||
},
|
||||
{
|
||||
struct: "Bold",
|
||||
kind: ["BOLD"],
|
||||
},
|
||||
{
|
||||
struct: "Strike",
|
||||
kind: ["STRIKE"],
|
||||
},
|
||||
{
|
||||
struct: "Italic",
|
||||
kind: ["ITALIC"],
|
||||
},
|
||||
{
|
||||
struct: "Underline",
|
||||
kind: ["UNDERLINE"],
|
||||
},
|
||||
{
|
||||
struct: "Verbatim",
|
||||
kind: ["VERBATIM"],
|
||||
},
|
||||
{
|
||||
struct: "Code",
|
||||
kind: ["CODE"],
|
||||
token: [["text", "TEXT"]],
|
||||
},
|
||||
{
|
||||
struct: "Timestamp",
|
||||
kind: ["TIMESTAMP_ACTIVE", "TIMESTAMP_INACTIVE", "TIMESTAMP_DIARY"],
|
||||
token: [
|
||||
["year_start", "TIMESTAMP_YEAR"],
|
||||
["month_start", "TIMESTAMP_MONTH"],
|
||||
["day_start", "TIMESTAMP_DAY"],
|
||||
["hour_start", "TIMESTAMP_HOUR"],
|
||||
["minute_start", "TIMESTAMP_MINUTE"],
|
||||
],
|
||||
last_token: [
|
||||
["year_end", "TIMESTAMP_YEAR"],
|
||||
["month_end", "TIMESTAMP_MONTH"],
|
||||
["day_end", "TIMESTAMP_DAY"],
|
||||
["hour_end", "TIMESTAMP_HOUR"],
|
||||
["minute_end", "TIMESTAMP_MINUTE"],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let content = `//! generated file, do not modify it directly
|
||||
#![allow(clippy::all)]
|
||||
#![allow(unused)]
|
||||
|
||||
use rowan::ast::{support, AstChildren, AstNode};
|
||||
use crate::syntax::{OrgLanguage, SyntaxKind, SyntaxKind::*, SyntaxNode, SyntaxToken};
|
||||
`;
|
||||
|
||||
for (const node of nodes) {
|
||||
content += `
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ${node.struct} {
|
||||
pub(crate) syntax: SyntaxNode,
|
||||
}
|
||||
impl AstNode for ${node.struct} {
|
||||
type Language = OrgLanguage;
|
||||
fn can_cast(kind: SyntaxKind) -> bool { ${node.kind
|
||||
.map((k) => `kind == ${k}`)
|
||||
.join(" || ")} }
|
||||
fn cast(node: SyntaxNode) -> Option<${
|
||||
node.struct
|
||||
}> { Self::can_cast(node.kind()).then(|| ${node.struct} { syntax: node }) }
|
||||
fn syntax(&self) -> &SyntaxNode { &self.syntax }
|
||||
}
|
||||
impl ${node.struct} {\n`;
|
||||
for (const [method, kind] of node.token || []) {
|
||||
content += ` pub fn ${method}(&self) -> Option<SyntaxToken> { support::token(&self.syntax, ${kind}) }\n`;
|
||||
}
|
||||
for (const [method, kind] of node.last_token || []) {
|
||||
content += ` pub fn ${method}(&self) -> Option<SyntaxToken> { super::last_token(&self.syntax, ${kind}) }\n`;
|
||||
}
|
||||
for (const [method, kind] of node.parent || []) {
|
||||
content += ` pub fn ${method}(&self) -> Option<${kind}> { self.syntax.parent().and_then(${kind}::cast) }\n`;
|
||||
}
|
||||
for (const [method, kind] of node.first_child || []) {
|
||||
content += ` pub fn ${method}(&self) -> Option<${kind}> { support::child(&self.syntax) }\n`;
|
||||
}
|
||||
for (const [method, kind] of node.last_child || []) {
|
||||
content += ` pub fn ${method}(&self) -> Option<${kind}> { super::last_child(&self.syntax) }\n`;
|
||||
}
|
||||
for (const [method, kind] of node.children || []) {
|
||||
content += ` pub fn ${method}(&self) -> AstChildren<${kind}> { support::children(&self.syntax) }\n`;
|
||||
}
|
||||
if (node.post_blank) {
|
||||
content += ` pub fn post_blank(&self) -> usize { super::blank_lines(&self.syntax) }\n`;
|
||||
}
|
||||
if (node.pre_blank) {
|
||||
content += ` pub fn pre_blank(&self) -> usize { super::blank_lines(&self.syntax) }\n`;
|
||||
}
|
||||
content += `}\n`;
|
||||
}
|
||||
|
||||
require("fs").writeFileSync(__dirname + "/generated.rs", content);
|
||||
1266
src/ast/generated.rs
Normal file
1266
src/ast/generated.rs
Normal file
File diff suppressed because it is too large
Load diff
358
src/ast/headline.rs
Normal file
358
src/ast/headline.rs
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
use rowan::ast::support;
|
||||
|
||||
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxToken};
|
||||
|
||||
use super::{filter_token, Headline, HeadlinePriority, HeadlineTags, Timestamp};
|
||||
|
||||
impl Headline {
|
||||
/// Return level of this headline
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Headline};
|
||||
///
|
||||
/// let hdl = Org::parse("* ").first_node::<Headline>().unwrap();
|
||||
/// assert_eq!(hdl.level(), Some(1));
|
||||
/// let hdl = Org::parse("****** hello").first_node::<Headline>().unwrap();
|
||||
/// assert_eq!(hdl.level(), Some(6));
|
||||
/// ```
|
||||
pub fn level(&self) -> Option<usize> {
|
||||
self.stars().map(|stars| stars.text().len())
|
||||
}
|
||||
|
||||
/// Return `true` if this headline contains a COMMENT keyword
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Headline};
|
||||
///
|
||||
/// let hdl = Org::parse("* COMMENT").first_node::<Headline>().unwrap();
|
||||
/// assert!(hdl.is_commented());
|
||||
/// let hdl = Org::parse("* COMMENT hello").first_node::<Headline>().unwrap();
|
||||
/// assert!(hdl.is_commented());
|
||||
/// let hdl = Org::parse("* hello").first_node::<Headline>().unwrap();
|
||||
/// assert!(!hdl.is_commented());
|
||||
/// ```
|
||||
pub fn is_commented(&self) -> bool {
|
||||
self.title()
|
||||
.and_then(|title| title.syntax.first_token())
|
||||
.map(|title| {
|
||||
let text = title.text();
|
||||
title.kind() == SyntaxKind::TEXT
|
||||
&& text.starts_with("COMMENT")
|
||||
&& (text.len() == 7 || text[7..].starts_with(char::is_whitespace))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Return `true` if this headline contains an archive tag
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Headline};
|
||||
///
|
||||
/// let hdl = Org::parse("* hello :ARCHIVE:").first_node::<Headline>().unwrap();
|
||||
/// assert!(hdl.is_archived());
|
||||
/// let hdl = Org::parse("* hello :ARCHIVED:").first_node::<Headline>().unwrap();
|
||||
/// assert!(!hdl.is_archived());
|
||||
/// ```
|
||||
pub fn is_archived(&self) -> bool {
|
||||
self.tags()
|
||||
.map(|tags| {
|
||||
tags.syntax
|
||||
.children_with_tokens()
|
||||
.any(|elem| matches!(elem, SyntaxElement::Token(t) if t.text() == "ARCHIVE"))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns this headline's closed timestamp, or `None` if not set.
|
||||
pub fn closed(&self) -> Option<Timestamp> {
|
||||
self.planning()
|
||||
.and_then(|planning| planning.closed())
|
||||
.and_then(|node| support::child::<Timestamp>(&node.syntax))
|
||||
}
|
||||
|
||||
/// Returns this headline's scheduled timestamp, or `None` if not set.
|
||||
pub fn scheduled(&self) -> Option<Timestamp> {
|
||||
self.planning()
|
||||
.and_then(|planning| planning.scheduled())
|
||||
.and_then(|node| support::child::<Timestamp>(&node.syntax))
|
||||
}
|
||||
|
||||
/// Returns this headline's deadline timestamp, or `None` if not set.
|
||||
pub fn deadline(&self) -> Option<Timestamp> {
|
||||
self.planning()
|
||||
.and_then(|planning| planning.deadline())
|
||||
.and_then(|node| support::child::<Timestamp>(&node.syntax))
|
||||
}
|
||||
}
|
||||
|
||||
// pub enum DocumentOrHeadline {
|
||||
// Document(Document),
|
||||
// Headline(Headline),
|
||||
// }
|
||||
|
||||
// impl From<Document> for DocumentOrHeadline {
|
||||
// fn from(value: Document) -> Self {
|
||||
// DocumentOrHeadline::Document(value)
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl From<Headline> for DocumentOrHeadline {
|
||||
// fn from(value: Headline) -> Self {
|
||||
// DocumentOrHeadline::Headline(value)
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl DocumentOrHeadline {
|
||||
// pub fn section(&self) -> Option<Section> {
|
||||
// match self {
|
||||
// DocumentOrHeadline::Document(v) => v.section(),
|
||||
// DocumentOrHeadline::Headline(v) => v.section(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Org {
|
||||
// /// set the title of this headline
|
||||
// ///
|
||||
// /// ```rust
|
||||
// /// use orgize::Org;
|
||||
// ///
|
||||
// /// let mut org = Org::parse("* [#A]");
|
||||
// /// let hdl = org.document().first_headline().unwrap();
|
||||
// /// org.set_title(hdl, "world");
|
||||
// /// assert_eq!(org.to_org(), "* [#A] world");
|
||||
// /// let hdl = org.document().first_headline().unwrap();
|
||||
// /// org.set_title(hdl, "world!");
|
||||
// /// assert_eq!(org.to_org(), "* [#A] world!");
|
||||
// /// ```
|
||||
// pub fn set_title(&mut self, headline: Headline, title: &str) -> Option<HeadlineTitle> {
|
||||
// let bytes = title.as_bytes();
|
||||
// let title = match memchr(b'\n', bytes) {
|
||||
// Some(i) if i > 0 && bytes[i] == b'\r' => &title[0..i - 1],
|
||||
// Some(i) => &title[0..i],
|
||||
// _ => title,
|
||||
// };
|
||||
// let new_title = node(HEADLINE_TITLE, object_nodes(self.create_input(title)));
|
||||
|
||||
// if let Some(title) = headline.title() {
|
||||
// self.green = title.syntax.replace_with(new_title.into_node().unwrap());
|
||||
|
||||
// return Some(title);
|
||||
// }
|
||||
|
||||
// let mut child: Vec<_> = headline
|
||||
// .syntax
|
||||
// .green()
|
||||
// .children()
|
||||
// .map(|ch| ch.to_owned())
|
||||
// .collect();
|
||||
|
||||
// let index = support::child
|
||||
// .iter()
|
||||
// .enumerate()
|
||||
// .filter_map(|(idx, it)| {
|
||||
// if it.kind() == HEADLINE_STARS.into()
|
||||
// || it.kind() == HEADLINE_KEYWORD.into()
|
||||
// || it.kind() == HEADLINE_PRIORITY.into()
|
||||
// {
|
||||
// Some(idx + 1)
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// })
|
||||
// .last()
|
||||
// .unwrap_or_default();
|
||||
|
||||
// if index == child.len() {
|
||||
// child.push(token(WHITESPACE, " "));
|
||||
// child.push(new_title);
|
||||
// } else if child[index].kind() != WHITESPACE.into() {
|
||||
// child.insert(index, token(WHITESPACE, " "));
|
||||
// child.insert(index + 1, new_title);
|
||||
// } else {
|
||||
// child.insert(index, new_title);
|
||||
// }
|
||||
|
||||
// self.green = headline
|
||||
// .syntax
|
||||
// .replace_with(node(HEADLINE, child).into_node().unwrap());
|
||||
|
||||
// None
|
||||
// }
|
||||
|
||||
// /// set the section of this document or headline
|
||||
// ///
|
||||
// /// ```rust
|
||||
// /// use orgize::Org;
|
||||
// ///
|
||||
// /// let mut org = Org::parse("* hello");
|
||||
// ///
|
||||
// /// let hdl = org.document().first_headline().unwrap();
|
||||
// /// org.set_section(hdl, "world");
|
||||
// /// assert_eq!(org.to_org(), "* hello\nworld\n");
|
||||
// ///
|
||||
// /// let hdl = org.document().first_headline().unwrap();
|
||||
// /// org.set_section(hdl, "world!");
|
||||
// /// assert_eq!(org.to_org(), "* hello\nworld!\n");
|
||||
// ///
|
||||
// /// let doc = org.document();
|
||||
// /// org.set_section(doc, "doc");
|
||||
// /// assert_eq!(org.to_org(), "doc\n* hello\nworld!\n");
|
||||
// /// ```
|
||||
// pub fn set_section(
|
||||
// &mut self,
|
||||
// document_or_headline: impl Into<DocumentOrHeadline>,
|
||||
// section: &str,
|
||||
// ) -> Option<Section> {
|
||||
// let document_or_headline = document_or_headline.into();
|
||||
|
||||
// let section = section_text(self.create_input(section)).ok()?.1.as_str();
|
||||
|
||||
// let section = if section.ends_with('\n') {
|
||||
// section_node(self.create_input(section)).map(|(_, s)| s)
|
||||
// } else {
|
||||
// section_node(self.create_input(&format!("{section}\n"))).map(|(_, s)| s)
|
||||
// }
|
||||
// .ok()?;
|
||||
|
||||
// if let Some(old) = document_or_headline.section() {
|
||||
// self.green = old.syntax.replace_with(section.into_node().unwrap());
|
||||
|
||||
// return Some(old);
|
||||
// }
|
||||
|
||||
// match document_or_headline {
|
||||
// DocumentOrHeadline::Document(document) => {
|
||||
// let mut child: Vec<_> = document
|
||||
// .syntax
|
||||
// .green()
|
||||
// .children()
|
||||
// .map(|ch| ch.to_owned())
|
||||
// .collect();
|
||||
|
||||
// let headline_idx = child.iter().position(|it| it.kind() == HEADLINE.into());
|
||||
|
||||
// if let Some(idx) = headline_idx {
|
||||
// child.insert(idx, section);
|
||||
// } else {
|
||||
// child.push(section);
|
||||
// }
|
||||
|
||||
// self.green = document
|
||||
// .syntax
|
||||
// .replace_with(GreenNode::new(DOCUMENT.into(), child));
|
||||
|
||||
// None
|
||||
// }
|
||||
// DocumentOrHeadline::Headline(headline) => {
|
||||
// let mut child: Vec<_> = headline
|
||||
// .syntax
|
||||
// .green()
|
||||
// .children()
|
||||
// .map(|ch| ch.to_owned())
|
||||
// .collect();
|
||||
|
||||
// let new_line_idx = support::child
|
||||
// .iter()
|
||||
// .position(|it| it.kind() == NEW_LINE.into());
|
||||
|
||||
// if let Some(idx) = new_line_idx {
|
||||
// // add section *after* newline
|
||||
// if idx < support::child.len() {
|
||||
// support::child.insert(idx, section);
|
||||
// } else {
|
||||
// support::child.push(section);
|
||||
// }
|
||||
// } else {
|
||||
// support::child.push(token(NEW_LINE, "\n"));
|
||||
// support::child.push(section);
|
||||
// }
|
||||
|
||||
// self.green = headline
|
||||
// .syntax
|
||||
// .replace_with(GreenNode::new(HEADLINE.into(), support::child));
|
||||
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// set the level of this headline
|
||||
// ///
|
||||
// /// ```rust
|
||||
// /// use orgize::Org;
|
||||
// ///
|
||||
// /// let mut org = Org::parse("** 1\n** 2");
|
||||
// ///
|
||||
// /// let hdl = org.document().last_headline().unwrap();
|
||||
// /// org.set_level(hdl, 1);
|
||||
// /// assert_eq!(org.to_org(), "** 1\n* 2");
|
||||
// ///
|
||||
// /// let hdl = org.document().last_headline().unwrap();
|
||||
// /// org.set_level(hdl, 3);
|
||||
// /// assert_eq!(org.to_org(), "** 1\n* 2");
|
||||
// /// ```
|
||||
// pub fn set_level(&mut self, headline: Headline, level: usize) {
|
||||
// if level == 0 {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let min_level_in_siblings = headline
|
||||
// .syntax
|
||||
// .siblings(rowan::Direction::Next)
|
||||
// .chain(headline.syntax.siblings(rowan::Direction::Prev))
|
||||
// .filter_map(Headline::cast)
|
||||
// .filter_map(|headline| headline.level())
|
||||
// .min()
|
||||
// .unwrap_or(1);
|
||||
|
||||
// if level <= min_level_in_siblings {
|
||||
// if let Some(stars) = headline.stars() {
|
||||
// self.green = stars.replace_with(GreenToken::new(
|
||||
// SyntaxKind::HEADLINE_STARS.into(),
|
||||
// "*".repeat(level).as_str(),
|
||||
// ));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
impl HeadlineTags {
|
||||
/// Returns an iterator of text token in this tags
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::HeadlineTags};
|
||||
///
|
||||
/// let tags_vec = |input: &str| {
|
||||
/// let tags = Org::parse(input).first_node::<HeadlineTags>().unwrap();
|
||||
/// let tags: Vec<_> = tags.iter().map(|t| t.to_string()).collect();
|
||||
/// tags
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(tags_vec("* :tag:"), vec!["tag".to_string()]);
|
||||
/// assert_eq!(tags_vec("* [#A] :::::a2%:"), vec!["a2%".to_string()]);
|
||||
/// assert_eq!(tags_vec("* TODO :tag: :a2%:"), vec!["tag".to_string(), "a2%".to_string()]);
|
||||
/// assert_eq!(tags_vec("* title :tag:a2%:"), vec!["tag".to_string(), "a2%".to_string()]);
|
||||
/// ```
|
||||
pub fn iter(&self) -> impl Iterator<Item = SyntaxToken> {
|
||||
self.syntax
|
||||
.children_with_tokens()
|
||||
.filter_map(filter_token(SyntaxKind::TEXT))
|
||||
}
|
||||
}
|
||||
|
||||
impl HeadlinePriority {
|
||||
/// Returns priority text
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::HeadlinePriority};
|
||||
///
|
||||
/// let priority = Org::parse("* [#A]").first_node::<HeadlinePriority>().unwrap();
|
||||
/// assert_eq!(priority.text_string().unwrap(), "A".to_string());
|
||||
/// let priority = Org::parse("* [#破]").first_node::<HeadlinePriority>().unwrap();
|
||||
/// assert_eq!(priority.text_string().unwrap(), "破".to_string());
|
||||
/// ```
|
||||
pub fn text_string(&self) -> Option<String> {
|
||||
self.text().map(|tk| tk.to_string())
|
||||
}
|
||||
}
|
||||
22
src/ast/inline_call.rs
Normal file
22
src/ast/inline_call.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxToken};
|
||||
|
||||
use super::InlineCall;
|
||||
|
||||
impl InlineCall {
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::InlineCall};
|
||||
///
|
||||
/// let call = Org::parse("call_square(4)").first_node::<InlineCall>().unwrap();
|
||||
/// assert_eq!(call.call().unwrap().text(), "square");
|
||||
/// ```
|
||||
pub fn call(&self) -> Option<SyntaxToken> {
|
||||
self.syntax
|
||||
.children_with_tokens()
|
||||
.filter_map(|it| match it {
|
||||
SyntaxElement::Token(t) if t.kind() == SyntaxKind::TEXT => Some(t),
|
||||
_ => None,
|
||||
})
|
||||
.nth(1)
|
||||
}
|
||||
}
|
||||
41
src/ast/link.rs
Normal file
41
src/ast/link.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
use rowan::ast::{support, AstNode};
|
||||
|
||||
use super::Link;
|
||||
use crate::syntax::SyntaxKind;
|
||||
|
||||
impl Link {
|
||||
/// Returns `true` if link contains description
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Link};
|
||||
///
|
||||
/// let link = Org::parse("[[https://google.com]]").first_node::<Link>().unwrap();
|
||||
/// assert!(!link.has_description());
|
||||
/// let link = Org::parse("[[https://google.com][Google]]").first_node::<Link>().unwrap();
|
||||
/// assert!(link.has_description());
|
||||
/// ```
|
||||
pub fn has_description(&self) -> bool {
|
||||
support::token(self.syntax(), SyntaxKind::TEXT).is_some()
|
||||
}
|
||||
|
||||
/// Returns `true` if link is an image link
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Link};
|
||||
///
|
||||
/// let link = Org::parse("[[file:/home/dominik/images/jupiter.jpg]]").first_node::<Link>().unwrap();
|
||||
/// assert!(link.is_image());
|
||||
/// ```
|
||||
pub fn is_image(&self) -> bool {
|
||||
const IMAGE_SUFFIX: &[&str] = &[
|
||||
// https://github.com/bzg/org-mode/blob/7de1e818d5fbe6a05c6b1a007eed07dc27e7246b/lisp/ox.el#L253
|
||||
".png", ".jpeg", ".jpg", ".gif", ".tiff", ".tif", ".xbm", ".xpm", ".pbm", ".pgm",
|
||||
".ppm", ".webp", ".avif", ".svg",
|
||||
];
|
||||
|
||||
self.path()
|
||||
.map(|path| IMAGE_SUFFIX.iter().any(|e| path.text().ends_with(e)))
|
||||
.unwrap_or_default()
|
||||
&& !self.has_description()
|
||||
}
|
||||
}
|
||||
47
src/ast/list.rs
Normal file
47
src/ast/list.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use super::List;
|
||||
use crate::syntax::SyntaxKind;
|
||||
|
||||
impl List {
|
||||
/// Returns `true` if this list is an ordered link
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::List};
|
||||
///
|
||||
/// let list = Org::parse("+ 1").first_node::<List>().unwrap();
|
||||
/// assert!(!list.is_ordered());
|
||||
///
|
||||
/// let list = Org::parse("1. 1").first_node::<List>().unwrap();
|
||||
/// assert!(list.is_ordered());
|
||||
///
|
||||
/// let list = Org::parse("1) 1\n- 2\n3. 3").first_node::<List>().unwrap();
|
||||
/// assert!(list.is_ordered());
|
||||
/// ```
|
||||
pub fn is_ordered(&self) -> bool {
|
||||
self.items()
|
||||
.next()
|
||||
.and_then(|item| item.bullet())
|
||||
.map(|bullet| bullet.text().starts_with(|c: char| c.is_ascii_digit()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns `true` if this list contains a TAG
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::List};
|
||||
///
|
||||
/// let list = Org::parse("- some tag :: item 2.1").first_node::<List>().unwrap();
|
||||
/// assert!(list.is_descriptive());
|
||||
/// let list = Org::parse("2. [X] item 2").first_node::<List>().unwrap();
|
||||
/// assert!(!list.is_descriptive());
|
||||
/// ```
|
||||
pub fn is_descriptive(&self) -> bool {
|
||||
self.items()
|
||||
.next()
|
||||
.map(|item| {
|
||||
item.syntax
|
||||
.children()
|
||||
.any(|it| it.kind() == SyntaxKind::LIST_ITEM_TAG)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
49
src/ast/mod.rs
Normal file
49
src/ast/mod.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#[rustfmt::skip]
|
||||
mod generated;
|
||||
|
||||
|
||||
mod drawer;
|
||||
mod headline;
|
||||
mod inline_call;
|
||||
mod link;
|
||||
mod list;
|
||||
mod snippet;
|
||||
mod table;
|
||||
mod timestamp;
|
||||
|
||||
pub use generated::*;
|
||||
pub use rowan::ast::support::*;
|
||||
|
||||
use crate::syntax::{SyntaxKind, SyntaxNode};
|
||||
use rowan::{ast::AstNode, Language, NodeOrToken};
|
||||
|
||||
pub fn blank_lines(parent: &SyntaxNode) -> usize {
|
||||
parent
|
||||
.children()
|
||||
.filter(|n| n.kind() == SyntaxKind::BLANK_LINE)
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn last_child<N: AstNode>(parent: &rowan::SyntaxNode<N::Language>) -> Option<N> {
|
||||
parent.children().filter_map(N::cast).last()
|
||||
}
|
||||
|
||||
pub fn last_token<L: Language>(
|
||||
parent: &rowan::SyntaxNode<L>,
|
||||
kind: L::Kind,
|
||||
) -> Option<rowan::SyntaxToken<L>> {
|
||||
parent
|
||||
.children_with_tokens()
|
||||
.filter_map(filter_token(kind))
|
||||
.last()
|
||||
}
|
||||
|
||||
pub fn filter_token<L: Language>(
|
||||
kind: L::Kind,
|
||||
) -> impl Fn(NodeOrToken<rowan::SyntaxNode<L>, rowan::SyntaxToken<L>>) -> Option<rowan::SyntaxToken<L>>
|
||||
{
|
||||
move |elem| match elem {
|
||||
NodeOrToken::Token(tk) if tk.kind() == kind => Some(tk),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
18
src/ast/snippet.rs
Normal file
18
src/ast/snippet.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use crate::syntax::{SyntaxKind, SyntaxToken};
|
||||
|
||||
use super::{filter_token, Snippet};
|
||||
|
||||
impl Snippet {
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Snippet};
|
||||
///
|
||||
/// let snippet = Org::parse("@@BACKEND:VALUE@@").first_node::<Snippet>().unwrap();
|
||||
/// assert_eq!(snippet.value().unwrap().text(), "VALUE");
|
||||
/// ```
|
||||
pub fn value(&self) -> Option<SyntaxToken> {
|
||||
self.syntax
|
||||
.children_with_tokens()
|
||||
.filter_map(filter_token(SyntaxKind::TEXT))
|
||||
.nth(1)
|
||||
}
|
||||
}
|
||||
30
src/ast/table.rs
Normal file
30
src/ast/table.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use super::OrgTableRow;
|
||||
use crate::syntax::SyntaxKind;
|
||||
|
||||
impl OrgTableRow {
|
||||
/// Returns `true` if this row is a rule
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::OrgTableRow};
|
||||
///
|
||||
/// let org = Org::parse("|----|----|\n|Foo |Bar |");
|
||||
/// let row = org.first_node::<OrgTableRow>().unwrap();
|
||||
/// assert!(row.is_rule());
|
||||
/// ```
|
||||
pub fn is_rule(&self) -> bool {
|
||||
self.syntax.kind() == SyntaxKind::ORG_TABLE_RULE_ROW
|
||||
}
|
||||
|
||||
/// Returns `true` if this row is a standard row
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::OrgTableRow};
|
||||
///
|
||||
/// let org = Org::parse("|Foo |Bar |\n|----|----|");
|
||||
/// let row = org.first_node::<OrgTableRow>().unwrap();
|
||||
/// assert!(row.is_standard());
|
||||
/// ```
|
||||
pub fn is_standard(&self) -> bool {
|
||||
self.syntax.kind() == SyntaxKind::ORG_TABLE_STANDARD_ROW
|
||||
}
|
||||
}
|
||||
112
src/ast/timestamp.rs
Normal file
112
src/ast/timestamp.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use super::{filter_token, Timestamp};
|
||||
use crate::syntax::SyntaxKind;
|
||||
|
||||
impl Timestamp {
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Timestamp};
|
||||
///
|
||||
/// let ts = Org::parse("<2003-09-16 Tue 09:39-10:39>").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_active());
|
||||
/// let ts = Org::parse("<2003-09-16 Tue 09:39>--<2003-09-16 Tue 10:39>").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_active());
|
||||
/// let ts = Org::parse("<2003-09-16 Tue 09:39>").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_active());
|
||||
/// ```
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.syntax.kind() == SyntaxKind::TIMESTAMP_ACTIVE
|
||||
}
|
||||
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Timestamp};
|
||||
///
|
||||
/// let ts = Org::parse("[2003-09-16 Tue 09:39-10:39]").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_inactive());
|
||||
/// let ts = Org::parse("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_inactive());
|
||||
/// let ts = Org::parse("[2003-09-16 Tue 09:39]").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_inactive());
|
||||
/// ```
|
||||
pub fn is_inactive(&self) -> bool {
|
||||
self.syntax.kind() == SyntaxKind::TIMESTAMP_INACTIVE
|
||||
}
|
||||
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Timestamp};
|
||||
///
|
||||
/// let ts = Org::parse("<%%(org-calendar-holiday)>").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_diary());
|
||||
/// ```
|
||||
pub fn is_diary(&self) -> bool {
|
||||
self.syntax.kind() == SyntaxKind::TIMESTAMP_DIARY
|
||||
}
|
||||
|
||||
/// Returns `true` if this timestamp has a range
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Timestamp};
|
||||
///
|
||||
/// let ts = Org::parse("[2003-09-16 Tue 09:39-10:39]").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_range());
|
||||
/// let ts = Org::parse("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(ts.is_range());
|
||||
/// let ts = Org::parse("[2003-09-16 Tue 09:39]").first_node::<Timestamp>().unwrap();
|
||||
/// assert!(!ts.is_range());
|
||||
/// ```
|
||||
pub fn is_range(&self) -> bool {
|
||||
self.syntax
|
||||
.children_with_tokens()
|
||||
.filter_map(filter_token(SyntaxKind::MINUS))
|
||||
.count()
|
||||
> 2
|
||||
}
|
||||
|
||||
/// Converts timestamp start to chrono NaiveDateTime
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Timestamp};
|
||||
/// use chrono::NaiveDateTime;
|
||||
///
|
||||
/// let ts = Org::parse("[2003-09-16 Tue 09:39-10:39]").first_node::<Timestamp>().unwrap();
|
||||
/// assert_eq!(ts.start_to_chrono().unwrap(), "2003-09-16T09:39:00".parse::<NaiveDateTime>().unwrap());
|
||||
/// ```
|
||||
#[cfg(feature = "chrono")]
|
||||
pub fn start_to_chrono(&self) -> Option<chrono::NaiveDateTime> {
|
||||
Some(chrono::NaiveDateTime::new(
|
||||
chrono::NaiveDate::from_ymd_opt(
|
||||
self.year_start()?.text().parse().ok()?,
|
||||
self.month_start()?.text().parse().ok()?,
|
||||
self.day_start()?.text().parse().ok()?,
|
||||
)?,
|
||||
chrono::NaiveTime::from_hms_opt(
|
||||
self.hour_start()?.text().parse().ok()?,
|
||||
self.minute_start()?.text().parse().ok()?,
|
||||
0,
|
||||
)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Converts timestamp end to chrono NaiveDateTime
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{Org, ast::Timestamp};
|
||||
/// use chrono::NaiveDateTime;
|
||||
///
|
||||
/// let ts = Org::parse("[2003-09-16 Tue 09:39-10:39]").first_node::<Timestamp>().unwrap();
|
||||
/// assert_eq!(ts.end_to_chrono().unwrap(), "2003-09-16T10:39:00".parse::<NaiveDateTime>().unwrap());
|
||||
/// ```
|
||||
#[cfg(feature = "chrono")]
|
||||
pub fn end_to_chrono(&self) -> Option<chrono::NaiveDateTime> {
|
||||
Some(chrono::NaiveDateTime::new(
|
||||
chrono::NaiveDate::from_ymd_opt(
|
||||
self.year_end()?.text().parse().ok()?,
|
||||
self.month_end()?.text().parse().ok()?,
|
||||
self.day_end()?.text().parse().ok()?,
|
||||
)?,
|
||||
chrono::NaiveTime::from_hms_opt(
|
||||
self.hour_end()?.text().parse().ok()?,
|
||||
self.minute_end()?.text().parse().ok()?,
|
||||
0,
|
||||
)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +1,56 @@
|
|||
use crate::syntax::document::document_node;
|
||||
use crate::Org;
|
||||
|
||||
/// Parse configuration
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParseConfig {
|
||||
/// Headline's todo keywords
|
||||
pub todo_keywords: (Vec<String>, Vec<String>),
|
||||
|
||||
pub dual_keywords: Vec<String>,
|
||||
|
||||
pub parsed_keywords: Vec<String>,
|
||||
|
||||
/// Affiliated keywords
|
||||
///
|
||||
/// Equivalent to [`org-element-affiliated-keywords`](https://git.sr.ht/~bzg/org-mode/tree/6f960f3c6a4dfe137fbd33fef9f7dadfd229600c/item/lisp/org-element.el#L331)
|
||||
pub affiliated_keywords: Vec<String>,
|
||||
}
|
||||
|
||||
impl ParseConfig {
|
||||
/// Parses input with current config
|
||||
pub fn parse(self, input: impl AsRef<str>) -> Org {
|
||||
let input = (input.as_ref(), &self).into();
|
||||
let node = document_node(input).unwrap().1;
|
||||
|
||||
Org {
|
||||
config: self,
|
||||
green: node.into_node().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ParseConfig {
|
||||
fn default() -> Self {
|
||||
ParseConfig {
|
||||
todo_keywords: (vec![String::from("TODO")], vec![String::from("DONE")]),
|
||||
todo_keywords: (vec!["TODO".into()], vec!["DONE".into()]),
|
||||
dual_keywords: vec!["CAPTION".into(), "RESULTS".into()],
|
||||
parsed_keywords: vec!["CAPTION".into()],
|
||||
affiliated_keywords: vec![
|
||||
"CAPTION".into(),
|
||||
"DATA".into(),
|
||||
"HEADER".into(),
|
||||
"HEADERS".into(),
|
||||
"LABEL".into(),
|
||||
"NAME".into(),
|
||||
"PLOT".into(),
|
||||
"RESNAME".into(),
|
||||
"RESULT".into(),
|
||||
"RESULTS".into(),
|
||||
"SOURCE".into(),
|
||||
"SRCNAME".into(),
|
||||
"TBLNAME".into(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref DEFAULT_CONFIG: ParseConfig = ParseConfig::default();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,408 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::tag_no_case,
|
||||
character::complete::{alpha1, space0},
|
||||
sequence::preceded,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::elements::Element;
|
||||
use crate::parse::combinators::{blank_lines_count, line, lines_till};
|
||||
|
||||
/// Special Block Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct SpecialBlock<'a> {
|
||||
/// Block parameters
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub parameters: Option<Cow<'a, str>>,
|
||||
/// Block name
|
||||
pub name: Cow<'a, str>,
|
||||
/// Numbers of blank lines between first block's line and next non-blank
|
||||
/// line
|
||||
pub pre_blank: usize,
|
||||
/// Numbers of blank lines between last block's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl SpecialBlock<'_> {
|
||||
pub fn into_owned(self) -> SpecialBlock<'static> {
|
||||
SpecialBlock {
|
||||
name: self.name.into_owned().into(),
|
||||
parameters: self.parameters.map(Into::into).map(Cow::Owned),
|
||||
pre_blank: self.pre_blank,
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Quote Block Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct QuoteBlock<'a> {
|
||||
/// Optional block parameters
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub parameters: Option<Cow<'a, str>>,
|
||||
/// Numbers of blank lines between first block's line and next non-blank
|
||||
/// line
|
||||
pub pre_blank: usize,
|
||||
/// Numbers of blank lines between last block's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl QuoteBlock<'_> {
|
||||
pub fn into_owned(self) -> QuoteBlock<'static> {
|
||||
QuoteBlock {
|
||||
parameters: self.parameters.map(Into::into).map(Cow::Owned),
|
||||
pre_blank: self.pre_blank,
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Center Block Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct CenterBlock<'a> {
|
||||
/// Optional block parameters
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub parameters: Option<Cow<'a, str>>,
|
||||
/// Numbers of blank lines between first block's line and next non-blank
|
||||
/// line
|
||||
pub pre_blank: usize,
|
||||
/// Numbers of blank lines between last block's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl CenterBlock<'_> {
|
||||
pub fn into_owned(self) -> CenterBlock<'static> {
|
||||
CenterBlock {
|
||||
parameters: self.parameters.map(Into::into).map(Cow::Owned),
|
||||
pre_blank: self.pre_blank,
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Verse Block Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct VerseBlock<'a> {
|
||||
/// Optional block parameters
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub parameters: Option<Cow<'a, str>>,
|
||||
/// Numbers of blank lines between first block's line and next non-blank
|
||||
/// line
|
||||
pub pre_blank: usize,
|
||||
/// Numbers of blank lines between last block's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl VerseBlock<'_> {
|
||||
pub fn into_owned(self) -> VerseBlock<'static> {
|
||||
VerseBlock {
|
||||
parameters: self.parameters.map(Into::into).map(Cow::Owned),
|
||||
pre_blank: self.pre_blank,
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Comment Block Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct CommentBlock<'a> {
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub data: Option<Cow<'a, str>>,
|
||||
/// Comment block contents
|
||||
pub contents: Cow<'a, str>,
|
||||
/// Numbers of blank lines between last block's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl CommentBlock<'_> {
|
||||
pub fn into_owned(self) -> CommentBlock<'static> {
|
||||
CommentBlock {
|
||||
data: self.data.map(Into::into).map(Cow::Owned),
|
||||
contents: self.contents.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Example Block Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct ExampleBlock<'a> {
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub data: Option<Cow<'a, str>>,
|
||||
/// Block contents
|
||||
pub contents: Cow<'a, str>,
|
||||
/// Numbers of blank lines between last block's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl ExampleBlock<'_> {
|
||||
pub fn into_owned(self) -> ExampleBlock<'static> {
|
||||
ExampleBlock {
|
||||
data: self.data.map(Into::into).map(Cow::Owned),
|
||||
contents: self.contents.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Export Block Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct ExportBlock<'a> {
|
||||
pub data: Cow<'a, str>,
|
||||
/// Block contents
|
||||
pub contents: Cow<'a, str>,
|
||||
/// Numbers of blank lines between last block's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl ExportBlock<'_> {
|
||||
pub fn into_owned(self) -> ExportBlock<'static> {
|
||||
ExportBlock {
|
||||
data: self.data.into_owned().into(),
|
||||
contents: self.contents.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Src Block Element
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct SourceBlock<'a> {
|
||||
/// Block contents
|
||||
pub contents: Cow<'a, str>,
|
||||
/// Language of the code in the block
|
||||
pub language: Cow<'a, str>,
|
||||
pub arguments: Cow<'a, str>,
|
||||
/// Numbers of blank lines between last block's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl SourceBlock<'_> {
|
||||
pub fn into_owned(self) -> SourceBlock<'static> {
|
||||
SourceBlock {
|
||||
language: self.language.into_owned().into(),
|
||||
arguments: self.arguments.into_owned().into(),
|
||||
contents: self.contents.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fn number_lines() -> Some(New) | Some(Continued) | None { }
|
||||
// TODO: fn preserve_indent() -> bool { }
|
||||
// TODO: fn use_labels() -> bool { }
|
||||
// TODO: fn label_fmt() -> Option<String> { }
|
||||
// TODO: fn retain_labels() -> bool { }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub(crate) struct RawBlock<'a> {
|
||||
pub name: &'a str,
|
||||
pub arguments: &'a str,
|
||||
|
||||
pub pre_blank: usize,
|
||||
pub contents: &'a str,
|
||||
pub contents_without_blank_lines: &'a str,
|
||||
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl<'a> RawBlock<'a> {
|
||||
pub fn parse(input: &str) -> Option<(&str, RawBlock)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_element(self) -> (Element<'a>, &'a str) {
|
||||
let RawBlock {
|
||||
name,
|
||||
contents,
|
||||
arguments,
|
||||
pre_blank,
|
||||
contents_without_blank_lines,
|
||||
post_blank,
|
||||
} = self;
|
||||
|
||||
let arguments: Option<Cow<'a, str>> = if arguments.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(arguments.into())
|
||||
};
|
||||
|
||||
let element = match &*name.to_uppercase() {
|
||||
"CENTER" => CenterBlock {
|
||||
parameters: arguments,
|
||||
pre_blank,
|
||||
post_blank,
|
||||
}
|
||||
.into(),
|
||||
"QUOTE" => QuoteBlock {
|
||||
parameters: arguments,
|
||||
pre_blank,
|
||||
post_blank,
|
||||
}
|
||||
.into(),
|
||||
"VERSE" => VerseBlock {
|
||||
parameters: arguments,
|
||||
pre_blank,
|
||||
post_blank,
|
||||
}
|
||||
.into(),
|
||||
"COMMENT" => CommentBlock {
|
||||
data: arguments,
|
||||
contents: contents.into(),
|
||||
post_blank,
|
||||
}
|
||||
.into(),
|
||||
"EXAMPLE" => ExampleBlock {
|
||||
data: arguments,
|
||||
contents: contents.into(),
|
||||
post_blank,
|
||||
}
|
||||
.into(),
|
||||
"EXPORT" => ExportBlock {
|
||||
data: arguments.unwrap_or_default(),
|
||||
contents: contents.into(),
|
||||
post_blank,
|
||||
}
|
||||
.into(),
|
||||
"SRC" => {
|
||||
let (language, arguments) = match &arguments {
|
||||
Some(Cow::Borrowed(args)) => {
|
||||
let (language, arguments) =
|
||||
args.split_at(args.find(' ').unwrap_or_else(|| args.len()));
|
||||
(language.into(), arguments.into())
|
||||
}
|
||||
None => (Cow::Borrowed(""), Cow::Borrowed("")),
|
||||
_ => unreachable!(
|
||||
"`parse_block_element` returns `Some(Cow::Borrowed)` or `None`"
|
||||
),
|
||||
};
|
||||
SourceBlock {
|
||||
arguments,
|
||||
language,
|
||||
contents: contents.into(),
|
||||
post_blank,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
_ => SpecialBlock {
|
||||
parameters: arguments,
|
||||
name: name.into(),
|
||||
pre_blank,
|
||||
post_blank,
|
||||
}
|
||||
.into(),
|
||||
};
|
||||
|
||||
(element, contents_without_blank_lines)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_internal(input: &str) -> IResult<&str, RawBlock, ()> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, name) = preceded(tag_no_case("#+BEGIN_"), alpha1)(input)?;
|
||||
let (input, arguments) = line(input)?;
|
||||
let end_line = format!("#+END_{}", name);
|
||||
let (input, contents) = lines_till(|line| line.trim().eq_ignore_ascii_case(&end_line))(input)?;
|
||||
let (contents_without_blank_lines, pre_blank) = blank_lines_count(contents)?;
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
RawBlock {
|
||||
name,
|
||||
contents,
|
||||
arguments: arguments.trim(),
|
||||
pre_blank,
|
||||
contents_without_blank_lines,
|
||||
post_blank,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
RawBlock::parse(
|
||||
r#"#+BEGIN_SRC
|
||||
#+END_SRC"#
|
||||
),
|
||||
Some((
|
||||
"",
|
||||
RawBlock {
|
||||
contents: "",
|
||||
contents_without_blank_lines: "",
|
||||
pre_blank: 0,
|
||||
post_blank: 0,
|
||||
name: "SRC".into(),
|
||||
arguments: ""
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RawBlock::parse(
|
||||
r#"#+begin_src
|
||||
#+end_src"#
|
||||
),
|
||||
Some((
|
||||
"",
|
||||
RawBlock {
|
||||
contents: "",
|
||||
contents_without_blank_lines: "",
|
||||
pre_blank: 0,
|
||||
post_blank: 0,
|
||||
name: "src".into(),
|
||||
arguments: ""
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RawBlock::parse(
|
||||
r#"#+BEGIN_SRC javascript
|
||||
console.log('Hello World!');
|
||||
#+END_SRC
|
||||
|
||||
"#
|
||||
),
|
||||
Some((
|
||||
"",
|
||||
RawBlock {
|
||||
contents: "console.log('Hello World!');\n",
|
||||
contents_without_blank_lines: "console.log('Hello World!');\n",
|
||||
pre_blank: 0,
|
||||
post_blank: 1,
|
||||
name: "SRC".into(),
|
||||
arguments: "javascript"
|
||||
}
|
||||
))
|
||||
);
|
||||
// TODO: more testing
|
||||
}
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::tag,
|
||||
character::complete::{char, digit1, space0},
|
||||
combinator::recognize,
|
||||
sequence::separated_pair,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::elements::timestamp::{parse_inactive, Datetime, Timestamp};
|
||||
use crate::parse::combinators::{blank_lines_count, eol};
|
||||
|
||||
/// Clock Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(untagged))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Clock<'a> {
|
||||
/// Closed Clock
|
||||
Closed {
|
||||
/// Time start
|
||||
start: Datetime<'a>,
|
||||
/// Time end
|
||||
end: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
/// Clock duration
|
||||
duration: Cow<'a, str>,
|
||||
/// Numbers of blank lines between the clock line and next non-blank
|
||||
/// line or buffer's end
|
||||
post_blank: usize,
|
||||
},
|
||||
/// Running Clock
|
||||
Running {
|
||||
/// Time start
|
||||
start: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
/// Numbers of blank lines between the clock line and next non-blank
|
||||
/// line or buffer's end
|
||||
post_blank: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clock<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Clock)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_onwed(self) -> Clock<'static> {
|
||||
match self {
|
||||
Clock::Closed {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
duration,
|
||||
post_blank,
|
||||
} => Clock::Closed {
|
||||
start: start.into_owned(),
|
||||
end: end.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
duration: duration.into_owned().into(),
|
||||
post_blank,
|
||||
},
|
||||
Clock::Running {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
post_blank,
|
||||
} => Clock::Running {
|
||||
start: start.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
post_blank,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the clock is running.
|
||||
pub fn is_running(&self) -> bool {
|
||||
match self {
|
||||
Clock::Closed { .. } => false,
|
||||
Clock::Running { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the clock is closed.
|
||||
pub fn is_closed(&self) -> bool {
|
||||
match self {
|
||||
Clock::Closed { .. } => true,
|
||||
Clock::Running { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns clock duration, or `None` if it's running.
|
||||
pub fn duration(&self) -> Option<&str> {
|
||||
match self {
|
||||
Clock::Closed { duration, .. } => Some(duration),
|
||||
Clock::Running { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a timestamp from the clock.
|
||||
pub fn value(&self) -> Timestamp {
|
||||
match &*self {
|
||||
Clock::Closed {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
..
|
||||
} => Timestamp::InactiveRange {
|
||||
start: start.clone(),
|
||||
end: end.clone(),
|
||||
repeater: repeater.clone(),
|
||||
delay: delay.clone(),
|
||||
},
|
||||
Clock::Running {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
..
|
||||
} => Timestamp::Inactive {
|
||||
start: start.clone(),
|
||||
repeater: repeater.clone(),
|
||||
delay: delay.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_internal(input: &str) -> IResult<&str, Clock, ()> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = tag("CLOCK:")(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, timestamp) = parse_inactive(input)?;
|
||||
|
||||
match timestamp {
|
||||
Timestamp::InactiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
} => {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = tag("=>")(input)?;
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, duration) = recognize(separated_pair(digit1, char(':'), digit1))(input)?;
|
||||
let (input, _) = eol(input)?;
|
||||
let (input, blank) = blank_lines_count(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Clock::Closed {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
duration: duration.into(),
|
||||
post_blank: blank,
|
||||
},
|
||||
))
|
||||
}
|
||||
Timestamp::Inactive {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
} => {
|
||||
let (input, _) = eol(input)?;
|
||||
let (input, blank) = blank_lines_count(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Clock::Running {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
post_blank: blank,
|
||||
},
|
||||
))
|
||||
}
|
||||
_ => unreachable!(
|
||||
"`parse_inactive` only returns `Timestamp::InactiveRange` or `Timestamp::Inactive`."
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]"),
|
||||
Some((
|
||||
"",
|
||||
Clock::Running {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(9),
|
||||
minute: Some(39)
|
||||
},
|
||||
repeater: None,
|
||||
delay: None,
|
||||
post_blank: 0,
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00\n\n"),
|
||||
Some((
|
||||
"",
|
||||
Clock::Closed {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(9),
|
||||
minute: Some(39)
|
||||
},
|
||||
end: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(10),
|
||||
minute: Some(39)
|
||||
},
|
||||
repeater: None,
|
||||
delay: None,
|
||||
duration: "1:00".into(),
|
||||
post_blank: 1,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
error::{make_error, ErrorKind},
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
use crate::parse::combinators::{blank_lines_count, lines_while};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct Comment<'a> {
|
||||
/// Comments value, with pound signs
|
||||
pub value: Cow<'a, str>,
|
||||
/// Numbers of blank lines between last comment's line and next non-blank
|
||||
/// line or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl Comment<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Comment)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Comment<'static> {
|
||||
Comment {
|
||||
value: self.value.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_internal(input: &str) -> IResult<&str, Comment, ()> {
|
||||
let (input, value) = lines_while(|line| {
|
||||
let line = line.trim_start();
|
||||
line == "#" || line.starts_with("# ")
|
||||
})(input)?;
|
||||
|
||||
if value.is_empty() {
|
||||
// TODO: better error kind
|
||||
return Err(Err::Error(make_error(input, ErrorKind::Many0)));
|
||||
}
|
||||
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Comment {
|
||||
value: value.into(),
|
||||
post_blank,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag,
|
||||
character::complete::digit0,
|
||||
combinator::recognize,
|
||||
sequence::{delimited, pair, separated_pair},
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Statistics Cookie Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cookie<'a> {
|
||||
/// Full cookie value
|
||||
pub value: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Cookie<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Cookie)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Cookie<'static> {
|
||||
Cookie {
|
||||
value: self.value.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, Cookie, ()> {
|
||||
let (input, value) = recognize(delimited(
|
||||
tag("["),
|
||||
alt((
|
||||
separated_pair(digit0, tag("/"), digit0),
|
||||
pair(digit0, tag("%")),
|
||||
)),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Cookie {
|
||||
value: value.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
Cookie::parse("[1/10]"),
|
||||
Some((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[1/10]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Cookie::parse("[1/1000]"),
|
||||
Some((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[1/1000]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Cookie::parse("[10%]"),
|
||||
Some((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[10%]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Cookie::parse("[%]"),
|
||||
Some((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[%]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Cookie::parse("[/]"),
|
||||
Some((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[/]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Cookie::parse("[100/]"),
|
||||
Some((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[100/]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Cookie::parse("[/100]"),
|
||||
Some((
|
||||
"",
|
||||
Cookie {
|
||||
value: "[/100]".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert!(Cookie::parse("[10% ]").is_none());
|
||||
assert!(Cookie::parse("[1//100]").is_none());
|
||||
assert!(Cookie::parse("[1\\100]").is_none());
|
||||
assert!(Cookie::parse("[10%%]").is_none());
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while1},
|
||||
character::complete::space0,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::parse::combinators::{blank_lines_count, eol, lines_till};
|
||||
|
||||
/// Drawer Element
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct Drawer<'a> {
|
||||
/// Drawer name
|
||||
pub name: Cow<'a, str>,
|
||||
/// Numbers of blank lines between first drawer's line and next non-blank
|
||||
/// line
|
||||
pub pre_blank: usize,
|
||||
/// Numbers of blank lines between last drawer's line and next non-blank
|
||||
/// line or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl Drawer<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, (Drawer, &str))> {
|
||||
parse_drawer(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Drawer<'static> {
|
||||
Drawer {
|
||||
name: self.name.into_owned().into(),
|
||||
pre_blank: self.pre_blank,
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn parse_drawer(input: &str) -> IResult<&str, (Drawer, &str), ()> {
|
||||
let (input, (mut drawer, content)) = parse_drawer_without_blank(input)?;
|
||||
|
||||
let (content, blank) = blank_lines_count(content)?;
|
||||
drawer.pre_blank = blank;
|
||||
|
||||
let (input, blank) = blank_lines_count(input)?;
|
||||
drawer.post_blank = blank;
|
||||
|
||||
Ok((input, (drawer, content)))
|
||||
}
|
||||
|
||||
pub fn parse_drawer_without_blank(input: &str) -> IResult<&str, (Drawer, &str), ()> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, name) = delimited(
|
||||
tag(":"),
|
||||
take_while1(|c: char| c.is_ascii_alphabetic() || c == '-' || c == '_'),
|
||||
tag(":"),
|
||||
)(input)?;
|
||||
let (input, _) = eol(input)?;
|
||||
let (input, contents) = lines_till(|line| line.trim().eq_ignore_ascii_case(":END:"))(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
Drawer {
|
||||
name: name.into(),
|
||||
pre_blank: 0,
|
||||
post_blank: 0,
|
||||
},
|
||||
contents,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
parse_drawer(
|
||||
r#":PROPERTIES:
|
||||
:CUSTOM_ID: id
|
||||
:END:"#
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Drawer {
|
||||
name: "PROPERTIES".into(),
|
||||
pre_blank: 0,
|
||||
post_blank: 0
|
||||
},
|
||||
" :CUSTOM_ID: id\n"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_drawer(
|
||||
r#":PROPERTIES:
|
||||
|
||||
|
||||
:END:
|
||||
|
||||
"#
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Drawer {
|
||||
name: "PROPERTIES".into(),
|
||||
pre_blank: 2,
|
||||
post_blank: 1,
|
||||
},
|
||||
""
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
// https://github.com/PoiScript/orgize/issues/9
|
||||
assert!(parse_drawer(":SPAGHETTI:\n").is_err());
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::tag_no_case,
|
||||
character::complete::{alpha1, space0, space1},
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::parse::combinators::{blank_lines_count, line, lines_till};
|
||||
|
||||
/// Dynamic Block Element
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct DynBlock<'a> {
|
||||
/// Block name
|
||||
pub block_name: Cow<'a, str>,
|
||||
/// Block argument
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub arguments: Option<Cow<'a, str>>,
|
||||
/// Numbers of blank lines between first block's line and next non-blank
|
||||
/// line
|
||||
pub pre_blank: usize,
|
||||
/// Numbers of blank lines between last drawer's line and next non-blank
|
||||
/// line or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl DynBlock<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, (DynBlock, &str))> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> DynBlock<'static> {
|
||||
DynBlock {
|
||||
block_name: self.block_name.into_owned().into(),
|
||||
arguments: self.arguments.map(Into::into).map(Cow::Owned),
|
||||
pre_blank: self.pre_blank,
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, (DynBlock, &str), ()> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = tag_no_case("#+BEGIN:")(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, name) = alpha1(input)?;
|
||||
let (input, args) = line(input)?;
|
||||
let (input, contents) = lines_till(|line| line.trim().eq_ignore_ascii_case("#+END:"))(input)?;
|
||||
let (contents, pre_blank) = blank_lines_count(contents)?;
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
DynBlock {
|
||||
block_name: name.into(),
|
||||
arguments: if args.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(args.trim().into())
|
||||
},
|
||||
pre_blank,
|
||||
post_blank,
|
||||
},
|
||||
contents,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
// TODO: testing
|
||||
assert_eq!(
|
||||
DynBlock::parse(
|
||||
r#"#+BEGIN: clocktable :scope file
|
||||
|
||||
|
||||
CONTENTS
|
||||
#+END:
|
||||
|
||||
"#
|
||||
),
|
||||
Some((
|
||||
"",
|
||||
(
|
||||
DynBlock {
|
||||
block_name: "clocktable".into(),
|
||||
arguments: Some(":scope file".into()),
|
||||
pre_blank: 2,
|
||||
post_blank: 1,
|
||||
},
|
||||
"CONTENTS\n"
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
use bytecount::count;
|
||||
use memchr::memchr_iter;
|
||||
|
||||
use crate::elements::Element;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub(crate) struct Emphasis<'a> {
|
||||
marker: u8,
|
||||
contents: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Emphasis<'a> {
|
||||
pub fn parse(text: &str, marker: u8) -> Option<(&str, Emphasis)> {
|
||||
if text.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bytes = text.as_bytes();
|
||||
|
||||
if bytes[1].is_ascii_whitespace() {
|
||||
return None;
|
||||
}
|
||||
|
||||
for i in memchr_iter(marker, bytes).skip(1) {
|
||||
// contains at least one character
|
||||
if i == 1 {
|
||||
continue;
|
||||
} else if count(&bytes[1..i], b'\n') >= 2 {
|
||||
break;
|
||||
} else if validate_marker(i, text) {
|
||||
return Some((
|
||||
&text[i + 1..],
|
||||
Emphasis {
|
||||
marker,
|
||||
contents: &text[1..i],
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn into_element(self) -> (Element<'a>, &'a str) {
|
||||
let Emphasis { marker, contents } = self;
|
||||
let element = match marker {
|
||||
b'*' => Element::Bold,
|
||||
b'+' => Element::Strike,
|
||||
b'/' => Element::Italic,
|
||||
b'_' => Element::Underline,
|
||||
b'=' => Element::Verbatim {
|
||||
value: contents.into(),
|
||||
},
|
||||
b'~' => Element::Code {
|
||||
value: contents.into(),
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
(element, contents)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_marker(pos: usize, text: &str) -> bool {
|
||||
if text.as_bytes()[pos - 1].is_ascii_whitespace() {
|
||||
false
|
||||
} else if let Some(&post) = text.as_bytes().get(pos + 1) {
|
||||
match post {
|
||||
b' ' | b'-' | b'.' | b',' | b':' | b'!' | b'?' | b'\'' | b'\n' | b')' | b'}' => true,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
Emphasis::parse("*bold*", b'*'),
|
||||
Some((
|
||||
"",
|
||||
Emphasis {
|
||||
contents: "bold",
|
||||
marker: b'*'
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Emphasis::parse("*bo*ld*", b'*'),
|
||||
Some((
|
||||
"",
|
||||
Emphasis {
|
||||
contents: "bo*ld",
|
||||
marker: b'*'
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Emphasis::parse("*bo\nld*", b'*'),
|
||||
Some((
|
||||
"",
|
||||
Emphasis {
|
||||
contents: "bo\nld",
|
||||
marker: b'*'
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(Emphasis::parse("*bold*a", b'*'), None);
|
||||
assert_eq!(Emphasis::parse("*bold*", b'/'), None);
|
||||
assert_eq!(Emphasis::parse("*bold *", b'*'), None);
|
||||
assert_eq!(Emphasis::parse("* bold*", b'*'), None);
|
||||
assert_eq!(Emphasis::parse("*b\nol\nd*", b'*'), None);
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
error::{make_error, ErrorKind},
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
use crate::parse::combinators::{blank_lines_count, lines_while};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct FixedWidth<'a> {
|
||||
/// Fixed width value
|
||||
pub value: Cow<'a, str>,
|
||||
/// Numbers of blank lines between last fixed width's line and next
|
||||
/// non-blank line or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl FixedWidth<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, FixedWidth)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> FixedWidth<'static> {
|
||||
FixedWidth {
|
||||
value: self.value.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_internal(input: &str) -> IResult<&str, FixedWidth, ()> {
|
||||
let (input, value) = lines_while(|line| {
|
||||
let line = line.trim_start();
|
||||
line == ":" || line.starts_with(": ")
|
||||
})(input)?;
|
||||
|
||||
if value.is_empty() {
|
||||
// TODO: better error kind
|
||||
return Err(Err::Error(make_error(input, ErrorKind::Many0)));
|
||||
}
|
||||
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
FixedWidth {
|
||||
value: value.into(),
|
||||
post_blank,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
FixedWidth::parse(
|
||||
r#": A
|
||||
:
|
||||
: B
|
||||
: C
|
||||
|
||||
"#
|
||||
),
|
||||
Some((
|
||||
"",
|
||||
FixedWidth {
|
||||
value: r#": A
|
||||
:
|
||||
: B
|
||||
: C
|
||||
"#
|
||||
.into(),
|
||||
post_blank: 1
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while1},
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::parse::combinators::{blank_lines_count, line};
|
||||
|
||||
/// Footnote Definition Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct FnDef<'a> {
|
||||
/// Footnote label, used for reference
|
||||
pub label: Cow<'a, str>,
|
||||
/// Numbers of blank lines between last footnote definition's line and next
|
||||
/// non-blank line or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl FnDef<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, (FnDef, &str))> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> FnDef<'static> {
|
||||
FnDef {
|
||||
label: self.label.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_internal(input: &str) -> IResult<&str, (FnDef, &str), ()> {
|
||||
let (input, label) = delimited(
|
||||
tag("[fn:"),
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||
tag("]"),
|
||||
)(input)?;
|
||||
|
||||
let (input, content) = line(input)?;
|
||||
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
FnDef {
|
||||
label: label.into(),
|
||||
post_blank,
|
||||
},
|
||||
content,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
FnDef::parse("[fn:1] https://orgmode.org"),
|
||||
Some((
|
||||
"",
|
||||
(
|
||||
FnDef {
|
||||
label: "1".into(),
|
||||
post_blank: 0
|
||||
},
|
||||
" https://orgmode.org"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
FnDef::parse("[fn:word_1] https://orgmode.org"),
|
||||
Some((
|
||||
"",
|
||||
(
|
||||
FnDef {
|
||||
label: "word_1".into(),
|
||||
post_blank: 0,
|
||||
},
|
||||
" https://orgmode.org"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
FnDef::parse("[fn:WORD-1] https://orgmode.org"),
|
||||
Some((
|
||||
"",
|
||||
(
|
||||
FnDef {
|
||||
label: "WORD-1".into(),
|
||||
post_blank: 0,
|
||||
},
|
||||
" https://orgmode.org"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
FnDef::parse("[fn:WORD]"),
|
||||
Some((
|
||||
"",
|
||||
(
|
||||
FnDef {
|
||||
label: "WORD".into(),
|
||||
post_blank: 0,
|
||||
},
|
||||
""
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
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,111 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use memchr::memchr2_iter;
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::opt,
|
||||
error::{make_error, ErrorKind},
|
||||
sequence::preceded,
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
/// Footnote Reference Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FnRef<'a> {
|
||||
/// Footnote label
|
||||
pub label: Cow<'a, str>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub definition: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl FnRef<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, FnRef)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> FnRef<'static> {
|
||||
FnRef {
|
||||
label: self.label.into_owned().into(),
|
||||
definition: self.definition.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, FnRef, ()> {
|
||||
let (input, _) = tag("[fn:")(input)?;
|
||||
let (input, label) =
|
||||
take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_')(input)?;
|
||||
let (input, definition) = opt(preceded(tag(":"), balanced_brackets))(input)?;
|
||||
let (input, _) = tag("]")(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
FnRef {
|
||||
label: label.into(),
|
||||
definition: definition.map(Into::into),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn balanced_brackets(input: &str) -> IResult<&str, &str, ()> {
|
||||
let mut pairs = 1;
|
||||
for i in memchr2_iter(b'[', b']', input.as_bytes()) {
|
||||
if input.as_bytes()[i] == b'[' {
|
||||
pairs += 1;
|
||||
} else if pairs != 1 {
|
||||
pairs -= 1;
|
||||
} else {
|
||||
return Ok((&input[i..], &input[0..i]));
|
||||
}
|
||||
}
|
||||
Err(Err::Error(make_error(input, ErrorKind::Tag)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
FnRef::parse("[fn:1]"),
|
||||
Some((
|
||||
"",
|
||||
FnRef {
|
||||
label: "1".into(),
|
||||
definition: None
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
FnRef::parse("[fn:1:2]"),
|
||||
Some((
|
||||
"",
|
||||
FnRef {
|
||||
label: "1".into(),
|
||||
definition: Some("2".into())
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
FnRef::parse("[fn::2]"),
|
||||
Some((
|
||||
"",
|
||||
FnRef {
|
||||
label: "".into(),
|
||||
definition: Some("2".into())
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
FnRef::parse("[fn::[]]"),
|
||||
Some((
|
||||
"",
|
||||
FnRef {
|
||||
label: "".into(),
|
||||
definition: Some("[]".into())
|
||||
},
|
||||
))
|
||||
);
|
||||
|
||||
assert!(FnRef::parse("[fn::[]").is_none());
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_till},
|
||||
combinator::opt,
|
||||
sequence::{delimited, preceded},
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Inline Babel Call Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct InlineCall<'a> {
|
||||
/// Called code block name
|
||||
pub name: Cow<'a, str>,
|
||||
/// Header arguments applied to the code block
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub inside_header: Option<Cow<'a, str>>,
|
||||
/// Argument passed to the code block
|
||||
pub arguments: Cow<'a, str>,
|
||||
/// Header arguments applied to the calling instance
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub end_header: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl InlineCall<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, InlineCall)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> InlineCall<'static> {
|
||||
InlineCall {
|
||||
name: self.name.into_owned().into(),
|
||||
arguments: self.arguments.into_owned().into(),
|
||||
inside_header: self.inside_header.map(Into::into).map(Cow::Owned),
|
||||
end_header: self.end_header.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, InlineCall, ()> {
|
||||
let (input, name) = preceded(
|
||||
tag("call_"),
|
||||
take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')'),
|
||||
)(input)?;
|
||||
let (input, inside_header) = opt(delimited(
|
||||
tag("["),
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
let (input, arguments) =
|
||||
delimited(tag("("), take_till(|c| c == ')' || c == '\n'), tag(")"))(input)?;
|
||||
let (input, end_header) = opt(delimited(
|
||||
tag("["),
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
InlineCall {
|
||||
name: name.into(),
|
||||
arguments: arguments.into(),
|
||||
inside_header: inside_header.map(Into::into),
|
||||
end_header: end_header.map(Into::into),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
InlineCall::parse("call_square(4)"),
|
||||
Some((
|
||||
"",
|
||||
InlineCall {
|
||||
name: "square".into(),
|
||||
arguments: "4".into(),
|
||||
inside_header: None,
|
||||
end_header: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
InlineCall::parse("call_square[:results output](4)"),
|
||||
Some((
|
||||
"",
|
||||
InlineCall {
|
||||
name: "square".into(),
|
||||
arguments: "4".into(),
|
||||
inside_header: Some(":results output".into()),
|
||||
end_header: None,
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
InlineCall::parse("call_square(4)[:results html]"),
|
||||
Some((
|
||||
"",
|
||||
InlineCall {
|
||||
name: "square".into(),
|
||||
arguments: "4".into(),
|
||||
inside_header: None,
|
||||
end_header: Some(":results html".into()),
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
InlineCall::parse("call_square[:results output](4)[:results html]"),
|
||||
Some((
|
||||
"",
|
||||
InlineCall {
|
||||
name: "square".into(),
|
||||
arguments: "4".into(),
|
||||
inside_header: Some(":results output".into()),
|
||||
end_header: Some(":results html".into()),
|
||||
},
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_till, take_while1},
|
||||
combinator::opt,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Inline Src Block Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InlineSrc<'a> {
|
||||
/// Language of the code
|
||||
pub lang: Cow<'a, str>,
|
||||
/// Optional header arguments
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub options: Option<Cow<'a, str>>,
|
||||
/// Source code
|
||||
pub body: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl InlineSrc<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, InlineSrc)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> InlineSrc<'static> {
|
||||
InlineSrc {
|
||||
lang: self.lang.into_owned().into(),
|
||||
options: self.options.map(Into::into).map(Cow::Owned),
|
||||
body: self.body.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, InlineSrc, ()> {
|
||||
let (input, _) = tag("src_")(input)?;
|
||||
let (input, lang) =
|
||||
take_while1(|c: char| !c.is_ascii_whitespace() && c != '[' && c != '{')(input)?;
|
||||
let (input, options) = opt(delimited(
|
||||
tag("["),
|
||||
take_till(|c| c == '\n' || c == ']'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
let (input, body) = delimited(tag("{"), take_till(|c| c == '\n' || c == '}'), tag("}"))(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
InlineSrc {
|
||||
lang: lang.into(),
|
||||
options: options.map(Into::into),
|
||||
body: body.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
InlineSrc::parse("src_C{int a = 0;}"),
|
||||
Some((
|
||||
"",
|
||||
InlineSrc {
|
||||
lang: "C".into(),
|
||||
options: None,
|
||||
body: "int a = 0;".into()
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>}"),
|
||||
Some((
|
||||
"",
|
||||
InlineSrc {
|
||||
lang: "xml".into(),
|
||||
options: Some(":exports code".into()),
|
||||
body: "<tag>text</tag>".into(),
|
||||
},
|
||||
))
|
||||
);
|
||||
|
||||
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,230 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_till},
|
||||
character::complete::space0,
|
||||
combinator::opt,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::elements::Element;
|
||||
use crate::parse::combinators::{blank_lines_count, line};
|
||||
|
||||
/// Keyword Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Keyword<'a> {
|
||||
/// Keyword name
|
||||
pub key: Cow<'a, str>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub optional: Option<Cow<'a, str>>,
|
||||
/// Keyword value
|
||||
pub value: Cow<'a, str>,
|
||||
/// Numbers of blank lines between keyword line and next non-blank line or
|
||||
/// buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl Keyword<'_> {
|
||||
pub fn into_owned(self) -> Keyword<'static> {
|
||||
Keyword {
|
||||
key: self.key.into_owned().into(),
|
||||
optional: self.optional.map(Into::into).map(Cow::Owned),
|
||||
value: self.value.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Babel Call Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct BabelCall<'a> {
|
||||
/// Babel call value
|
||||
pub value: Cow<'a, str>,
|
||||
/// Numbers of blank lines between babel call line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl BabelCall<'_> {
|
||||
pub fn into_owned(self) -> BabelCall<'static> {
|
||||
BabelCall {
|
||||
value: self.value.into_owned().into(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
pub(crate) struct RawKeyword<'a> {
|
||||
pub key: &'a str,
|
||||
pub value: &'a str,
|
||||
pub optional: Option<&'a str>,
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl<'a> RawKeyword<'a> {
|
||||
pub fn parse(input: &str) -> Option<(&str, RawKeyword)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_element(self) -> Element<'a> {
|
||||
let RawKeyword {
|
||||
key,
|
||||
value,
|
||||
optional,
|
||||
post_blank,
|
||||
} = self;
|
||||
|
||||
if (&*key).eq_ignore_ascii_case("CALL") {
|
||||
BabelCall {
|
||||
value: value.into(),
|
||||
post_blank,
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
Keyword {
|
||||
key: key.into(),
|
||||
optional: optional.map(Into::into),
|
||||
value: value.into(),
|
||||
post_blank,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_internal(input: &str) -> IResult<&str, RawKeyword, ()> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = tag("#+")(input)?;
|
||||
let (input, key) = take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '[')(input)?;
|
||||
let (input, optional) = opt(delimited(
|
||||
tag("["),
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
let (input, _) = tag(":")(input)?;
|
||||
let (input, value) = line(input)?;
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
RawKeyword {
|
||||
key,
|
||||
optional,
|
||||
value: value.trim(),
|
||||
post_blank,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
RawKeyword::parse("#+KEY:"),
|
||||
Some((
|
||||
"",
|
||||
RawKeyword {
|
||||
key: "KEY",
|
||||
optional: None,
|
||||
value: "",
|
||||
post_blank: 0
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
RawKeyword::parse("#+KEY: VALUE"),
|
||||
Some((
|
||||
"",
|
||||
RawKeyword {
|
||||
key: "KEY",
|
||||
optional: None,
|
||||
value: "VALUE",
|
||||
post_blank: 0
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
RawKeyword::parse("#+K_E_Y: VALUE"),
|
||||
Some((
|
||||
"",
|
||||
RawKeyword {
|
||||
key: "K_E_Y",
|
||||
optional: None,
|
||||
value: "VALUE",
|
||||
post_blank: 0
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
RawKeyword::parse("#+KEY:VALUE\n"),
|
||||
Some((
|
||||
"",
|
||||
RawKeyword {
|
||||
key: "KEY",
|
||||
optional: None,
|
||||
value: "VALUE",
|
||||
post_blank: 0
|
||||
}
|
||||
))
|
||||
);
|
||||
assert!(RawKeyword::parse("#+KE Y: VALUE").is_none());
|
||||
assert!(RawKeyword::parse("#+ KEY: VALUE").is_none());
|
||||
|
||||
assert_eq!(
|
||||
RawKeyword::parse("#+RESULTS:"),
|
||||
Some((
|
||||
"",
|
||||
RawKeyword {
|
||||
key: "RESULTS",
|
||||
optional: None,
|
||||
value: "",
|
||||
post_blank: 0
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RawKeyword::parse("#+ATTR_LATEX: :width 5cm\n"),
|
||||
Some((
|
||||
"",
|
||||
RawKeyword {
|
||||
key: "ATTR_LATEX",
|
||||
optional: None,
|
||||
value: ":width 5cm",
|
||||
post_blank: 0
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RawKeyword::parse("#+CALL: double(n=4)"),
|
||||
Some((
|
||||
"",
|
||||
RawKeyword {
|
||||
key: "CALL",
|
||||
optional: None,
|
||||
value: "double(n=4)",
|
||||
post_blank: 0
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RawKeyword::parse("#+CAPTION[Short caption]: Longer caption."),
|
||||
Some((
|
||||
"",
|
||||
RawKeyword {
|
||||
key: "CAPTION",
|
||||
optional: Some("Short caption"),
|
||||
value: "Longer caption.",
|
||||
post_blank: 0
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::opt,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Link Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Link<'a> {
|
||||
/// Link destination
|
||||
pub path: Cow<'a, str>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub desc: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl Link<'_> {
|
||||
#[inline]
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Link)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Link<'static> {
|
||||
Link {
|
||||
path: self.path.into_owned().into(),
|
||||
desc: self.desc.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, Link, ()> {
|
||||
let (input, path) = delimited(
|
||||
tag("[["),
|
||||
take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'),
|
||||
tag("]"),
|
||||
)(input)?;
|
||||
let (input, desc) = opt(delimited(
|
||||
tag("["),
|
||||
take_while(|c: char| c != '[' && c != ']'),
|
||||
tag("]"),
|
||||
))(input)?;
|
||||
let (input, _) = tag("]")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Link {
|
||||
path: path.into(),
|
||||
desc: desc.map(Into::into),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
Link::parse("[[#id]]"),
|
||||
Some((
|
||||
"",
|
||||
Link {
|
||||
path: "#id".into(),
|
||||
desc: None
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Link::parse("[[#id][desc]]"),
|
||||
Some((
|
||||
"",
|
||||
Link {
|
||||
path: "#id".into(),
|
||||
desc: Some("desc".into())
|
||||
}
|
||||
))
|
||||
);
|
||||
assert!(Link::parse("[[#id][desc]").is_none());
|
||||
}
|
||||
|
|
@ -1,316 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
use std::iter::once;
|
||||
|
||||
use memchr::{memchr, memchr_iter};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag,
|
||||
character::complete::{digit1, space0},
|
||||
combinator::{map, recognize},
|
||||
sequence::terminated,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Plain List Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct List {
|
||||
/// List indent, number of whitespaces
|
||||
pub indent: usize,
|
||||
/// List's type, determined by the first item of this list
|
||||
pub ordered: bool,
|
||||
/// Numbers of blank lines between last list's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
/// List Item Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ListItem<'a> {
|
||||
/// List item bullet
|
||||
pub bullet: Cow<'a, str>,
|
||||
/// List item indent, number of whitespaces
|
||||
pub indent: usize,
|
||||
/// List item type
|
||||
pub ordered: bool,
|
||||
// TODO checkbox
|
||||
// TODO counter
|
||||
// TODO tag
|
||||
}
|
||||
|
||||
impl ListItem<'_> {
|
||||
#[inline]
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, (ListItem, &str))> {
|
||||
list_item(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> ListItem<'static> {
|
||||
ListItem {
|
||||
bullet: self.bullet.into_owned().into(),
|
||||
indent: self.indent,
|
||||
ordered: self.ordered,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_item(input: &str) -> IResult<&str, (ListItem, &str), ()> {
|
||||
let (input, indent) = map(space0, |s: &str| s.len())(input)?;
|
||||
let (input, bullet) = recognize(alt((
|
||||
tag("+ "),
|
||||
tag("* "),
|
||||
tag("- "),
|
||||
terminated(digit1, tag(". ")),
|
||||
)))(input)?;
|
||||
let (input, contents) = list_item_contents(input, indent);
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
ListItem {
|
||||
bullet: bullet.into(),
|
||||
indent,
|
||||
ordered: bullet.starts_with(|c: char| c.is_ascii_digit()),
|
||||
},
|
||||
contents,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn list_item_contents(input: &str, indent: usize) -> (&str, &str) {
|
||||
let mut last_end = memchr(b'\n', input.as_bytes())
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or_else(|| input.len());
|
||||
|
||||
for i in memchr_iter(b'\n', input.as_bytes())
|
||||
.map(|i| i + 1)
|
||||
.chain(once(input.len()))
|
||||
.skip(1)
|
||||
{
|
||||
if input[last_end..i]
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.all(u8::is_ascii_whitespace)
|
||||
{
|
||||
let x = memchr(b'\n', &input[i..].as_bytes())
|
||||
.map(|ii| i + ii + 1)
|
||||
.unwrap_or_else(|| input.len());
|
||||
|
||||
// two consecutive empty lines
|
||||
if input[i..x].as_bytes().iter().all(u8::is_ascii_whitespace) {
|
||||
return (&input[x..], &input[0..x]);
|
||||
}
|
||||
}
|
||||
|
||||
// line less or equally indented than the starting line
|
||||
if input[last_end..i]
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.take(indent + 1)
|
||||
.any(|c| !c.is_ascii_whitespace())
|
||||
{
|
||||
return (&input[last_end..], &input[0..last_end]);
|
||||
}
|
||||
|
||||
last_end = i;
|
||||
}
|
||||
|
||||
("", input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#"+ item1
|
||||
+ item2"#
|
||||
),
|
||||
Ok((
|
||||
"+ item2",
|
||||
(
|
||||
ListItem {
|
||||
bullet: "+ ".into(),
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
r#"item1
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#"* item1
|
||||
|
||||
* item2"#
|
||||
),
|
||||
Ok((
|
||||
"* item2",
|
||||
(
|
||||
ListItem {
|
||||
bullet: "* ".into(),
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
r#"item1
|
||||
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#"* item1
|
||||
|
||||
|
||||
* item2"#
|
||||
),
|
||||
Ok((
|
||||
"* item2",
|
||||
(
|
||||
ListItem {
|
||||
bullet: "* ".into(),
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
r#"item1
|
||||
|
||||
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#"* item1
|
||||
|
||||
"#
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
ListItem {
|
||||
bullet: "* ".into(),
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
r#"item1
|
||||
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#"+ item1
|
||||
+ item2
|
||||
"#
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
ListItem {
|
||||
bullet: "+ ".into(),
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
r#"item1
|
||||
+ item2
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#"+ item1
|
||||
|
||||
+ item2
|
||||
|
||||
+ item 3"#
|
||||
),
|
||||
Ok((
|
||||
"+ item 3",
|
||||
(
|
||||
ListItem {
|
||||
bullet: "+ ".into(),
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
r#"item1
|
||||
|
||||
+ item2
|
||||
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#" + item1
|
||||
|
||||
+ item2"#
|
||||
),
|
||||
Ok((
|
||||
" + item2",
|
||||
(
|
||||
ListItem {
|
||||
bullet: "+ ".into(),
|
||||
indent: 2,
|
||||
ordered: false,
|
||||
},
|
||||
r#"item1
|
||||
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#" 1. item1
|
||||
2. item2
|
||||
3. item3"#
|
||||
),
|
||||
Ok((
|
||||
r#"2. item2
|
||||
3. item3"#,
|
||||
(
|
||||
ListItem {
|
||||
bullet: "1. ".into(),
|
||||
indent: 2,
|
||||
ordered: true,
|
||||
},
|
||||
r#"item1
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
list_item(
|
||||
r#"+ 1
|
||||
|
||||
- 2
|
||||
|
||||
- 3
|
||||
|
||||
+ 4"#
|
||||
),
|
||||
Ok((
|
||||
"+ 4",
|
||||
(
|
||||
ListItem {
|
||||
bullet: "+ ".into(),
|
||||
indent: 0,
|
||||
ordered: false,
|
||||
},
|
||||
r#"1
|
||||
|
||||
- 2
|
||||
|
||||
- 3
|
||||
|
||||
"#
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take, take_until, take_while1},
|
||||
combinator::{opt, verify},
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Macro Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Macros<'a> {
|
||||
/// Macro name
|
||||
pub name: Cow<'a, str>,
|
||||
/// Arguments passed to the macro
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub arguments: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl Macros<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Macros)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Macros<'static> {
|
||||
Macros {
|
||||
name: self.name.into_owned().into(),
|
||||
arguments: self.arguments.map(Into::into).map(Cow::Owned),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, Macros, ()> {
|
||||
let (input, _) = tag("{{{")(input)?;
|
||||
let (input, name) = verify(
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||
|s: &str| s.starts_with(|c: char| c.is_ascii_alphabetic()),
|
||||
)(input)?;
|
||||
let (input, arguments) = opt(delimited(tag("("), take_until(")}}}"), take(1usize)))(input)?;
|
||||
let (input, _) = tag("}}}")(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Macros {
|
||||
name: name.into(),
|
||||
arguments: arguments.map(Into::into),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
assert_eq!(
|
||||
Macros::parse("{{{poem(red,blue)}}}"),
|
||||
Some((
|
||||
"",
|
||||
Macros {
|
||||
name: "poem".into(),
|
||||
arguments: Some("red,blue".into())
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Macros::parse("{{{poem())}}}"),
|
||||
Some((
|
||||
"",
|
||||
Macros {
|
||||
name: "poem".into(),
|
||||
arguments: Some(")".into())
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Macros::parse("{{{author}}}"),
|
||||
Some((
|
||||
"",
|
||||
Macros {
|
||||
name: "author".into(),
|
||||
arguments: None
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert!(Macros::parse("{{{0uthor}}}").is_none());
|
||||
assert!(Macros::parse("{{{author}}").is_none());
|
||||
assert!(Macros::parse("{{{poem(}}}").is_none());
|
||||
assert!(Macros::parse("{{{poem)}}}").is_none());
|
||||
}
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
//! Org-mode elements
|
||||
|
||||
pub(crate) mod block;
|
||||
pub(crate) mod clock;
|
||||
pub(crate) mod comment;
|
||||
pub(crate) mod cookie;
|
||||
pub(crate) mod drawer;
|
||||
pub(crate) mod dyn_block;
|
||||
pub(crate) mod emphasis;
|
||||
pub(crate) mod fixed_width;
|
||||
pub(crate) mod fn_def;
|
||||
pub(crate) mod fn_ref;
|
||||
pub(crate) mod inline_call;
|
||||
pub(crate) mod inline_src;
|
||||
pub(crate) mod keyword;
|
||||
pub(crate) mod link;
|
||||
pub(crate) mod list;
|
||||
pub(crate) mod macros;
|
||||
pub(crate) mod planning;
|
||||
pub(crate) mod radio_target;
|
||||
pub(crate) mod rule;
|
||||
pub(crate) mod snippet;
|
||||
pub(crate) mod table;
|
||||
pub(crate) mod target;
|
||||
pub(crate) mod timestamp;
|
||||
pub(crate) mod title;
|
||||
|
||||
pub use self::{
|
||||
block::{
|
||||
CenterBlock, CommentBlock, ExampleBlock, ExportBlock, QuoteBlock, SourceBlock,
|
||||
SpecialBlock, VerseBlock,
|
||||
},
|
||||
clock::Clock,
|
||||
comment::Comment,
|
||||
cookie::Cookie,
|
||||
drawer::Drawer,
|
||||
dyn_block::DynBlock,
|
||||
fixed_width::FixedWidth,
|
||||
fn_def::FnDef,
|
||||
fn_ref::FnRef,
|
||||
inline_call::InlineCall,
|
||||
inline_src::InlineSrc,
|
||||
keyword::{BabelCall, Keyword},
|
||||
link::Link,
|
||||
list::{List, ListItem},
|
||||
macros::Macros,
|
||||
planning::Planning,
|
||||
rule::Rule,
|
||||
snippet::Snippet,
|
||||
table::{Table, TableCell, TableRow},
|
||||
target::Target,
|
||||
timestamp::{Datetime, Timestamp},
|
||||
title::{PropertiesMap, Title},
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Element Enum
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "type", rename_all = "kebab-case"))]
|
||||
pub enum Element<'a> {
|
||||
SpecialBlock(SpecialBlock<'a>),
|
||||
QuoteBlock(QuoteBlock<'a>),
|
||||
CenterBlock(CenterBlock<'a>),
|
||||
VerseBlock(VerseBlock<'a>),
|
||||
CommentBlock(CommentBlock<'a>),
|
||||
ExampleBlock(ExampleBlock<'a>),
|
||||
ExportBlock(ExportBlock<'a>),
|
||||
SourceBlock(SourceBlock<'a>),
|
||||
BabelCall(BabelCall<'a>),
|
||||
Section,
|
||||
Clock(Clock<'a>),
|
||||
Cookie(Cookie<'a>),
|
||||
RadioTarget,
|
||||
Drawer(Drawer<'a>),
|
||||
Document { pre_blank: usize },
|
||||
DynBlock(DynBlock<'a>),
|
||||
FnDef(FnDef<'a>),
|
||||
FnRef(FnRef<'a>),
|
||||
Headline { level: usize },
|
||||
InlineCall(InlineCall<'a>),
|
||||
InlineSrc(InlineSrc<'a>),
|
||||
Keyword(Keyword<'a>),
|
||||
Link(Link<'a>),
|
||||
List(List),
|
||||
ListItem(ListItem<'a>),
|
||||
Macros(Macros<'a>),
|
||||
Snippet(Snippet<'a>),
|
||||
Text { value: Cow<'a, str> },
|
||||
Paragraph { post_blank: usize },
|
||||
Rule(Rule),
|
||||
Timestamp(Timestamp<'a>),
|
||||
Target(Target<'a>),
|
||||
Bold,
|
||||
Strike,
|
||||
Italic,
|
||||
Underline,
|
||||
Verbatim { value: Cow<'a, str> },
|
||||
Code { value: Cow<'a, str> },
|
||||
Comment(Comment<'a>),
|
||||
FixedWidth(FixedWidth<'a>),
|
||||
Title(Title<'a>),
|
||||
Table(Table<'a>),
|
||||
TableRow(TableRow),
|
||||
TableCell(TableCell),
|
||||
}
|
||||
|
||||
impl Element<'_> {
|
||||
pub fn is_container(&self) -> bool {
|
||||
match self {
|
||||
Element::SpecialBlock(_)
|
||||
| Element::QuoteBlock(_)
|
||||
| Element::CenterBlock(_)
|
||||
| Element::VerseBlock(_)
|
||||
| Element::Bold
|
||||
| Element::Document { .. }
|
||||
| Element::DynBlock(_)
|
||||
| Element::Headline { .. }
|
||||
| Element::Italic
|
||||
| Element::List(_)
|
||||
| Element::ListItem(_)
|
||||
| Element::Paragraph { .. }
|
||||
| Element::Section
|
||||
| Element::Strike
|
||||
| Element::Underline
|
||||
| Element::Title(_)
|
||||
| Element::Table(_)
|
||||
| Element::TableRow(TableRow::Header)
|
||||
| Element::TableRow(TableRow::Body)
|
||||
| Element::TableCell(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Element<'static> {
|
||||
use Element::*;
|
||||
|
||||
match self {
|
||||
SpecialBlock(e) => SpecialBlock(e.into_owned()),
|
||||
QuoteBlock(e) => QuoteBlock(e.into_owned()),
|
||||
CenterBlock(e) => CenterBlock(e.into_owned()),
|
||||
VerseBlock(e) => VerseBlock(e.into_owned()),
|
||||
CommentBlock(e) => CommentBlock(e.into_owned()),
|
||||
ExampleBlock(e) => ExampleBlock(e.into_owned()),
|
||||
ExportBlock(e) => ExportBlock(e.into_owned()),
|
||||
SourceBlock(e) => SourceBlock(e.into_owned()),
|
||||
BabelCall(e) => BabelCall(e.into_owned()),
|
||||
Section => Section,
|
||||
Clock(e) => Clock(e.into_onwed()),
|
||||
Cookie(e) => Cookie(e.into_owned()),
|
||||
RadioTarget => RadioTarget,
|
||||
Drawer(e) => Drawer(e.into_owned()),
|
||||
Document { pre_blank } => Document { pre_blank },
|
||||
DynBlock(e) => DynBlock(e.into_owned()),
|
||||
FnDef(e) => FnDef(e.into_owned()),
|
||||
FnRef(e) => FnRef(e.into_owned()),
|
||||
Headline { level } => Headline { level },
|
||||
InlineCall(e) => InlineCall(e.into_owned()),
|
||||
InlineSrc(e) => InlineSrc(e.into_owned()),
|
||||
Keyword(e) => Keyword(e.into_owned()),
|
||||
Link(e) => Link(e.into_owned()),
|
||||
List(e) => List(e),
|
||||
ListItem(e) => ListItem(e.into_owned()),
|
||||
Macros(e) => Macros(e.into_owned()),
|
||||
Snippet(e) => Snippet(e.into_owned()),
|
||||
Text { value } => Text {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
Paragraph { post_blank } => Paragraph { post_blank },
|
||||
Rule(e) => Rule(e),
|
||||
Timestamp(e) => Timestamp(e.into_owned()),
|
||||
Target(e) => Target(e.into_owned()),
|
||||
Bold => Bold,
|
||||
Strike => Strike,
|
||||
Italic => Italic,
|
||||
Underline => Underline,
|
||||
Verbatim { value } => Verbatim {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
Code { value } => Code {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
Comment(e) => Comment(e.into_owned()),
|
||||
FixedWidth(e) => FixedWidth(e.into_owned()),
|
||||
Title(e) => Title(e.into_owned()),
|
||||
Table(e) => Table(e.into_owned()),
|
||||
TableRow(e) => TableRow(e),
|
||||
TableCell(e) => TableCell(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
($($ele0:ident),*; $($ele1:ident),*) => {
|
||||
$(
|
||||
impl<'a> From<$ele0<'a>> for Element<'a> {
|
||||
fn from(ele: $ele0<'a>) -> Element<'a> {
|
||||
Element::$ele0(ele)
|
||||
}
|
||||
}
|
||||
)*
|
||||
$(
|
||||
impl<'a> From<$ele1> for Element<'a> {
|
||||
fn from(ele: $ele1) -> Element<'a> {
|
||||
Element::$ele1(ele)
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_from!(
|
||||
BabelCall,
|
||||
CenterBlock,
|
||||
Clock,
|
||||
Comment,
|
||||
CommentBlock,
|
||||
Cookie,
|
||||
Drawer,
|
||||
DynBlock,
|
||||
ExampleBlock,
|
||||
ExportBlock,
|
||||
FixedWidth,
|
||||
FnDef,
|
||||
FnRef,
|
||||
InlineCall,
|
||||
InlineSrc,
|
||||
Keyword,
|
||||
Link,
|
||||
ListItem,
|
||||
Macros,
|
||||
QuoteBlock,
|
||||
Snippet,
|
||||
SourceBlock,
|
||||
SpecialBlock,
|
||||
Table,
|
||||
Target,
|
||||
Timestamp,
|
||||
Title,
|
||||
VerseBlock;
|
||||
List,
|
||||
Rule,
|
||||
TableRow
|
||||
);
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
use memchr::memchr;
|
||||
|
||||
use crate::elements::Timestamp;
|
||||
|
||||
/// Planning element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Planning<'a> {
|
||||
/// Timestamp associated to deadline keyword
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub deadline: Option<Timestamp<'a>>,
|
||||
/// Timestamp associated to scheduled keyword
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub scheduled: Option<Timestamp<'a>>,
|
||||
/// Timestamp associated to closed keyword
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub closed: Option<Timestamp<'a>>,
|
||||
}
|
||||
|
||||
impl Planning<'_> {
|
||||
#[inline]
|
||||
pub(crate) fn parse(text: &str) -> Option<(&str, Planning)> {
|
||||
let (mut deadline, mut scheduled, mut closed) = (None, None, None);
|
||||
let (mut tail, off) = memchr(b'\n', text.as_bytes())
|
||||
.map(|i| (text[..i].trim(), i + 1))
|
||||
.unwrap_or_else(|| (text.trim(), text.len()));
|
||||
|
||||
while let Some(i) = memchr(b' ', tail.as_bytes()) {
|
||||
let next = &tail[i + 1..].trim_start();
|
||||
|
||||
macro_rules! set_timestamp {
|
||||
($timestamp:expr) => {{
|
||||
let (new_tail, timestamp) =
|
||||
Timestamp::parse_active(next).or(Timestamp::parse_inactive(next))?;
|
||||
$timestamp = Some(timestamp);
|
||||
tail = new_tail.trim_start();
|
||||
}};
|
||||
}
|
||||
|
||||
match &tail[..i] {
|
||||
"DEADLINE:" if deadline.is_none() => set_timestamp!(deadline),
|
||||
"SCHEDULED:" if scheduled.is_none() => set_timestamp!(scheduled),
|
||||
"CLOSED:" if closed.is_none() => set_timestamp!(closed),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
if deadline.is_none() && scheduled.is_none() && closed.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&text[off..],
|
||||
Planning {
|
||||
deadline,
|
||||
scheduled,
|
||||
closed,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Planning<'static> {
|
||||
Planning {
|
||||
deadline: self.deadline.map(|x| x.into_owned()),
|
||||
scheduled: self.scheduled.map(|x| x.into_owned()),
|
||||
closed: self.closed.map(|x| x.into_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prase() {
|
||||
use crate::elements::Datetime;
|
||||
|
||||
assert_eq!(
|
||||
Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"),
|
||||
Some((
|
||||
"",
|
||||
Planning {
|
||||
scheduled: Some(Timestamp::Active {
|
||||
start: Datetime {
|
||||
year: 2019,
|
||||
month: 4,
|
||||
day: 8,
|
||||
dayname: "Mon".into(),
|
||||
hour: None,
|
||||
minute: None
|
||||
},
|
||||
repeater: None,
|
||||
delay: None
|
||||
}),
|
||||
deadline: None,
|
||||
closed: None,
|
||||
}
|
||||
))
|
||||
)
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::verify,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
// TODO: text-markup, entities, latex-fragments, subscript and superscript
|
||||
|
||||
#[inline]
|
||||
pub fn parse_radio_target(input: &str) -> Option<(&str, &str)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, &str, ()> {
|
||||
let (input, contents) = delimited(
|
||||
tag("<<<"),
|
||||
verify(
|
||||
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|
||||
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
|
||||
),
|
||||
tag(">>>"),
|
||||
)(input)?;
|
||||
|
||||
Ok((input, contents))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(parse_radio_target("<<<target>>>"), Some(("", "target")));
|
||||
assert_eq!(parse_radio_target("<<<tar get>>>"), Some(("", "tar get")));
|
||||
|
||||
assert!(parse_radio_target("<<<target >>>").is_none());
|
||||
assert!(parse_radio_target("<<< target>>>").is_none());
|
||||
assert!(parse_radio_target("<<<ta<get>>>").is_none());
|
||||
assert!(parse_radio_target("<<<ta>get>>>").is_none());
|
||||
assert!(parse_radio_target("<<<ta\nget>>>").is_none());
|
||||
assert!(parse_radio_target("<<<target>>").is_none());
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
use nom::{bytes::complete::take_while_m_n, character::complete::space0, IResult};
|
||||
|
||||
use crate::parse::combinators::{blank_lines_count, eol};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct Rule {
|
||||
/// Numbers of blank lines between rule line and next non-blank line or
|
||||
/// buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl Rule {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Rule)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_internal(input: &str) -> IResult<&str, Rule, ()> {
|
||||
let (input, _) = space0(input)?;
|
||||
let (input, _) = take_while_m_n(5, usize::max_value(), |c| c == '-')(input)?;
|
||||
let (input, _) = eol(input)?;
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
Ok((input, Rule { post_blank }))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(Rule::parse("-----"), Some(("", Rule { post_blank: 0 })));
|
||||
assert_eq!(Rule::parse("--------"), Some(("", Rule { post_blank: 0 })));
|
||||
assert_eq!(
|
||||
Rule::parse("-----\n\n\n"),
|
||||
Some(("", Rule { post_blank: 2 }))
|
||||
);
|
||||
assert_eq!(Rule::parse("----- \n"), Some(("", Rule { post_blank: 0 })));
|
||||
|
||||
assert!(Rule::parse("").is_none());
|
||||
assert!(Rule::parse("----").is_none());
|
||||
assert!(Rule::parse("----").is_none());
|
||||
assert!(Rule::parse("None----").is_none());
|
||||
assert!(Rule::parse("None ----").is_none());
|
||||
assert!(Rule::parse("None------").is_none());
|
||||
assert!(Rule::parse("----None----").is_none());
|
||||
assert!(Rule::parse("\t\t----").is_none());
|
||||
assert!(Rule::parse("------None").is_none());
|
||||
assert!(Rule::parse("----- None").is_none());
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take, take_until, take_while1},
|
||||
sequence::{delimited, separated_pair},
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Export Snippet Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Snippet<'a> {
|
||||
/// Back-end name
|
||||
pub name: Cow<'a, str>,
|
||||
/// Export code
|
||||
pub value: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Snippet<'_> {
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Snippet)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Snippet<'static> {
|
||||
Snippet {
|
||||
name: self.name.into_owned().into(),
|
||||
value: self.value.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, Snippet, ()> {
|
||||
let (input, (name, value)) = delimited(
|
||||
tag("@@"),
|
||||
separated_pair(
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-'),
|
||||
tag(":"),
|
||||
take_until("@@"),
|
||||
),
|
||||
take(2usize),
|
||||
)(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Snippet {
|
||||
name: name.into(),
|
||||
value: value.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
Snippet::parse("@@html:<b>@@"),
|
||||
Some((
|
||||
"",
|
||||
Snippet {
|
||||
name: "html".into(),
|
||||
value: "<b>".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Snippet::parse("@@latex:any arbitrary LaTeX code@@"),
|
||||
Some((
|
||||
"",
|
||||
Snippet {
|
||||
name: "latex".into(),
|
||||
value: "any arbitrary LaTeX code".into(),
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Snippet::parse("@@html:@@"),
|
||||
Some((
|
||||
"",
|
||||
Snippet {
|
||||
name: "html".into(),
|
||||
value: "".into(),
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Snippet::parse("@@html:<p>@</p>@@"),
|
||||
Some((
|
||||
"",
|
||||
Snippet {
|
||||
name: "html".into(),
|
||||
value: "<p>@</p>".into(),
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert!(Snippet::parse("@@html:<b>@").is_none());
|
||||
assert!(Snippet::parse("@@html<b>@@").is_none());
|
||||
assert!(Snippet::parse("@@:<b>@@").is_none());
|
||||
}
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
error::{make_error, ErrorKind},
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
use crate::parse::combinators::{blank_lines_count, line, lines_while};
|
||||
|
||||
/// Table Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "table_type"))]
|
||||
pub enum Table<'a> {
|
||||
/// "org" type table
|
||||
#[cfg_attr(feature = "ser", serde(rename = "org"))]
|
||||
Org {
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
tblfm: Option<Cow<'a, str>>,
|
||||
/// Numbers of blank lines between last table's line and next non-blank
|
||||
/// line or buffer's end
|
||||
post_blank: usize,
|
||||
has_header: bool,
|
||||
},
|
||||
/// "table.el" type table
|
||||
#[cfg_attr(feature = "ser", serde(rename = "table.el"))]
|
||||
TableEl {
|
||||
value: Cow<'a, str>,
|
||||
/// Numbers of blank lines between last table's line and next non-blank
|
||||
/// line or buffer's end
|
||||
post_blank: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Table<'_> {
|
||||
pub fn parse_table_el(input: &str) -> Option<(&str, Table)> {
|
||||
Self::parse_table_el_internal(input).ok()
|
||||
}
|
||||
|
||||
fn parse_table_el_internal(input: &str) -> IResult<&str, Table, ()> {
|
||||
let (_, first_line) = line(input)?;
|
||||
|
||||
let first_line = first_line.trim();
|
||||
|
||||
// Table.el tables start at lines beginning with "+-" string and followed by plus or minus signs
|
||||
if !first_line.starts_with("+-")
|
||||
|| first_line
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.any(|&c| c != b'+' && c != b'-')
|
||||
{
|
||||
// TODO: better error kind
|
||||
return Err(Err::Error(make_error(input, ErrorKind::Many0)));
|
||||
}
|
||||
|
||||
// Table.el tables end at the first line not starting with either a vertical line or a plus sign.
|
||||
let (input, content) = lines_while(|line| {
|
||||
let line = line.trim_start();
|
||||
line.starts_with('|') || line.starts_with('+')
|
||||
})(input)?;
|
||||
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Table::TableEl {
|
||||
value: content.into(),
|
||||
post_blank,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Table<'static> {
|
||||
match self {
|
||||
Table::Org {
|
||||
tblfm,
|
||||
post_blank,
|
||||
has_header,
|
||||
} => Table::Org {
|
||||
tblfm: tblfm.map(Into::into).map(Cow::Owned),
|
||||
post_blank,
|
||||
has_header,
|
||||
},
|
||||
Table::TableEl { value, post_blank } => Table::TableEl {
|
||||
value: value.into_owned().into(),
|
||||
post_blank,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table Row Element
|
||||
///
|
||||
/// # Syntax
|
||||
///
|
||||
/// ```text
|
||||
/// | 0 | 1 | 2 | <- TableRow::Body
|
||||
/// | 0 | 1 | 2 | <- TableRow::Body
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// |-----+-----+-----| <- ignores
|
||||
/// | 0 | 1 | 2 | <- TableRow::Header
|
||||
/// | 0 | 1 | 2 | <- TableRow::Header
|
||||
/// |-----+-----+-----| <- TableRow::HeaderRule
|
||||
/// | 0 | 1 | 2 | <- TableRow::Body
|
||||
/// |-----+-----+-----| <- TableRow::BodyRule
|
||||
/// | 0 | 1 | 2 | <- TableRow::Body
|
||||
/// |-----+-----+-----| <- TableRow::BodyRule
|
||||
/// |-----+-----+-----| <- TableRow::BodyRule
|
||||
/// | 0 | 1 | 2 | <- TableRow::Body
|
||||
/// |-----+-----+-----| <- ignores
|
||||
/// ```
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "table_row_type"))]
|
||||
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
|
||||
pub enum TableRow {
|
||||
/// This row is part of table header
|
||||
Header,
|
||||
/// This row is part of table body
|
||||
Body,
|
||||
/// This row is between table header and body
|
||||
HeaderRule,
|
||||
/// This row is between table body and next body
|
||||
BodyRule,
|
||||
}
|
||||
|
||||
/// Table Cell Element
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "table_cell_type"))]
|
||||
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
|
||||
pub enum TableCell {
|
||||
/// Header cell
|
||||
Header,
|
||||
/// Body cell, or standard cell
|
||||
Body,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_table_el_() {
|
||||
assert_eq!(
|
||||
Table::parse_table_el(
|
||||
r#" +---+
|
||||
| |
|
||||
+---+
|
||||
|
||||
"#
|
||||
),
|
||||
Some((
|
||||
"",
|
||||
Table::TableEl {
|
||||
value: r#" +---+
|
||||
| |
|
||||
+---+
|
||||
"#
|
||||
.into(),
|
||||
post_blank: 1
|
||||
}
|
||||
))
|
||||
);
|
||||
assert!(Table::parse_table_el("").is_none());
|
||||
assert!(Table::parse_table_el("+----|---").is_none());
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::verify,
|
||||
sequence::delimited,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Target Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Target<'a> {
|
||||
/// Target ID
|
||||
pub target: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl Target<'_> {
|
||||
#[inline]
|
||||
pub(crate) fn parse(input: &str) -> Option<(&str, Target)> {
|
||||
parse_internal(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Target<'static> {
|
||||
Target {
|
||||
target: self.target.into_owned().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_internal(input: &str) -> IResult<&str, Target, ()> {
|
||||
let (input, target) = delimited(
|
||||
tag("<<"),
|
||||
verify(
|
||||
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|
||||
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
|
||||
),
|
||||
tag(">>"),
|
||||
)(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Target {
|
||||
target: target.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
Target::parse("<<target>>"),
|
||||
Some((
|
||||
"",
|
||||
Target {
|
||||
target: "target".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
Target::parse("<<tar get>>"),
|
||||
Some((
|
||||
"",
|
||||
Target {
|
||||
target: "tar get".into()
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
assert!(Target::parse("<<target >>").is_none());
|
||||
assert!(Target::parse("<< target>>").is_none());
|
||||
assert!(Target::parse("<<ta<get>>").is_none());
|
||||
assert!(Target::parse("<<ta>get>>").is_none());
|
||||
assert!(Target::parse("<<ta\nget>>").is_none());
|
||||
assert!(Target::parse("<<target>").is_none());
|
||||
}
|
||||
|
|
@ -1,482 +0,0 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use nom::{
|
||||
bytes::complete::{tag, take, take_till, take_while, take_while_m_n},
|
||||
character::complete::{space0, space1},
|
||||
combinator::{map, map_res, opt},
|
||||
sequence::preceded,
|
||||
IResult,
|
||||
};
|
||||
|
||||
/// Datetime Struct
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Datetime<'a> {
|
||||
pub year: u16,
|
||||
pub month: u8,
|
||||
pub day: u8,
|
||||
pub dayname: Cow<'a, str>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub hour: Option<u8>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub minute: Option<u8>,
|
||||
}
|
||||
|
||||
impl Datetime<'_> {
|
||||
pub fn into_owned(self) -> Datetime<'static> {
|
||||
Datetime {
|
||||
year: self.year,
|
||||
month: self.month,
|
||||
day: self.day,
|
||||
dayname: self.dayname.into_owned().into(),
|
||||
hour: self.hour,
|
||||
minute: self.minute,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
mod chrono {
|
||||
use super::Datetime;
|
||||
use chrono::*;
|
||||
|
||||
impl Into<NaiveDate> for Datetime<'_> {
|
||||
fn into(self) -> NaiveDate {
|
||||
(&self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveTime> for Datetime<'_> {
|
||||
fn into(self) -> NaiveTime {
|
||||
(&self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveDateTime> for Datetime<'_> {
|
||||
fn into(self) -> NaiveDateTime {
|
||||
(&self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<DateTime<Utc>> for Datetime<'_> {
|
||||
fn into(self) -> DateTime<Utc> {
|
||||
(&self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveDate> for &Datetime<'_> {
|
||||
fn into(self) -> NaiveDate {
|
||||
NaiveDate::from_ymd(self.year.into(), self.month.into(), self.day.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveTime> for &Datetime<'_> {
|
||||
fn into(self) -> NaiveTime {
|
||||
NaiveTime::from_hms(
|
||||
self.hour.unwrap_or_default().into(),
|
||||
self.minute.unwrap_or_default().into(),
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NaiveDateTime> for &Datetime<'_> {
|
||||
fn into(self) -> NaiveDateTime {
|
||||
NaiveDateTime::new(self.into(), self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<DateTime<Utc>> for &Datetime<'_> {
|
||||
fn into(self) -> DateTime<Utc> {
|
||||
DateTime::from_utc(self.into(), Utc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Timestamp Object
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
|
||||
#[cfg_attr(feature = "ser", serde(tag = "timestamp_type"))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Timestamp<'a> {
|
||||
Active {
|
||||
start: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
Inactive {
|
||||
start: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
ActiveRange {
|
||||
start: Datetime<'a>,
|
||||
end: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
InactiveRange {
|
||||
start: Datetime<'a>,
|
||||
end: Datetime<'a>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
repeater: Option<Cow<'a, str>>,
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
delay: Option<Cow<'a, str>>,
|
||||
},
|
||||
Diary {
|
||||
value: Cow<'a, str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Timestamp<'_> {
|
||||
pub(crate) fn parse_active(input: &str) -> Option<(&str, Timestamp)> {
|
||||
parse_active(input).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_inactive(input: &str) -> Option<(&str, Timestamp)> {
|
||||
parse_inactive(input).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn parse_diary(input: &str) -> Option<(&str, Timestamp)> {
|
||||
parse_diary(input).ok()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Timestamp<'static> {
|
||||
match self {
|
||||
Timestamp::Active {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::Active {
|
||||
start: start.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Timestamp::Inactive {
|
||||
start,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::Inactive {
|
||||
start: start.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Timestamp::ActiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::ActiveRange {
|
||||
start: start.into_owned(),
|
||||
end: end.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Timestamp::InactiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater,
|
||||
delay,
|
||||
} => Timestamp::InactiveRange {
|
||||
start: start.into_owned(),
|
||||
end: end.into_owned(),
|
||||
repeater: repeater.map(Into::into).map(Cow::Owned),
|
||||
delay: delay.map(Into::into).map(Cow::Owned),
|
||||
},
|
||||
Timestamp::Diary { value } => Timestamp::Diary {
|
||||
value: value.into_owned().into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_active(input: &str) -> IResult<&str, Timestamp, ()> {
|
||||
let (input, _) = tag("<")(input)?;
|
||||
let (input, start) = parse_datetime(input)?;
|
||||
|
||||
if input.starts_with('-') {
|
||||
let (input, (hour, minute)) = parse_time(&input[1..])?;
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag(">")(input)?;
|
||||
let mut end = start.clone();
|
||||
end.hour = Some(hour);
|
||||
end.minute = Some(minute);
|
||||
return Ok((
|
||||
input,
|
||||
Timestamp::ActiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag(">")(input)?;
|
||||
|
||||
if input.starts_with("--<") {
|
||||
let (input, end) = parse_datetime(&input["--<".len()..])?;
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag(">")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::ActiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::Active {
|
||||
start,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_inactive(input: &str) -> IResult<&str, Timestamp, ()> {
|
||||
let (input, _) = tag("[")(input)?;
|
||||
let (input, start) = parse_datetime(input)?;
|
||||
|
||||
if input.starts_with('-') {
|
||||
let (input, (hour, minute)) = parse_time(&input[1..])?;
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag("]")(input)?;
|
||||
let mut end = start.clone();
|
||||
end.hour = Some(hour);
|
||||
end.minute = Some(minute);
|
||||
return Ok((
|
||||
input,
|
||||
Timestamp::InactiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag("]")(input)?;
|
||||
|
||||
if input.starts_with("--[") {
|
||||
let (input, end) = parse_datetime(&input["--[".len()..])?;
|
||||
let (input, _) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, _) = tag("]")(input)?;
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::InactiveRange {
|
||||
start,
|
||||
end,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
} else {
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::Inactive {
|
||||
start,
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_diary(input: &str) -> IResult<&str, Timestamp, ()> {
|
||||
let (input, _) = tag("<%%(")(input)?;
|
||||
let (input, value) = take_till(|c| c == ')' || c == '>' || c == '\n')(input)?;
|
||||
let (input, _) = tag(")>")(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Timestamp::Diary {
|
||||
value: value.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_time(input: &str) -> IResult<&str, (u8, u8), ()> {
|
||||
let (input, hour) = map_res(take_while_m_n(1, 2, |c: char| c.is_ascii_digit()), |num| {
|
||||
u8::from_str_radix(num, 10)
|
||||
})(input)?;
|
||||
let (input, _) = tag(":")(input)?;
|
||||
let (input, minute) = map_res(take(2usize), |num| u8::from_str_radix(num, 10))(input)?;
|
||||
Ok((input, (hour, minute)))
|
||||
}
|
||||
|
||||
fn parse_datetime(input: &str) -> IResult<&str, Datetime, ()> {
|
||||
let parse_u8 = |num| u8::from_str_radix(num, 10);
|
||||
|
||||
let (input, year) = map_res(take(4usize), |num| u16::from_str_radix(num, 10))(input)?;
|
||||
let (input, _) = tag("-")(input)?;
|
||||
let (input, month) = map_res(take(2usize), parse_u8)(input)?;
|
||||
let (input, _) = tag("-")(input)?;
|
||||
let (input, day) = map_res(take(2usize), parse_u8)(input)?;
|
||||
let (input, _) = space1(input)?;
|
||||
let (input, dayname) = take_while(|c: char| {
|
||||
!c.is_ascii_whitespace()
|
||||
&& !c.is_ascii_digit()
|
||||
&& c != '+'
|
||||
&& c != '-'
|
||||
&& c != ']'
|
||||
&& c != '>'
|
||||
})(input)?;
|
||||
let (input, (hour, minute)) = map(opt(preceded(space1, parse_time)), |time| {
|
||||
(time.map(|t| t.0), time.map(|t| t.1))
|
||||
})(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
Datetime {
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
dayname: dayname.into(),
|
||||
hour,
|
||||
minute,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// TODO
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub enum RepeaterType {
|
||||
// Cumulate,
|
||||
// CatchUp,
|
||||
// Restart,
|
||||
// }
|
||||
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub enum DelayType {
|
||||
// All,
|
||||
// First,
|
||||
// }
|
||||
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub enum TimeUnit {
|
||||
// Hour,
|
||||
// Day,
|
||||
// Week,
|
||||
// Month,
|
||||
// Year,
|
||||
// }
|
||||
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub struct Repeater {
|
||||
// pub ty: RepeaterType,
|
||||
// pub value: usize,
|
||||
// pub unit: TimeUnit,
|
||||
// }
|
||||
|
||||
// #[cfg_attr(test, derive(PartialEq))]
|
||||
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
// #[derive(Debug, Copy, Clone)]
|
||||
// pub struct Delay {
|
||||
// pub ty: DelayType,
|
||||
// pub value: usize,
|
||||
// pub unit: TimeUnit,
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
parse_inactive("[2003-09-16 Tue]"),
|
||||
Ok((
|
||||
"",
|
||||
Timestamp::Inactive {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: None,
|
||||
minute: None
|
||||
},
|
||||
repeater: None,
|
||||
delay: None,
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_inactive("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]"),
|
||||
Ok((
|
||||
"",
|
||||
Timestamp::InactiveRange {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(9),
|
||||
minute: Some(39)
|
||||
},
|
||||
end: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(10),
|
||||
minute: Some(39),
|
||||
},
|
||||
repeater: None,
|
||||
delay: None
|
||||
},
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_active("<2003-09-16 Tue 09:39-10:39>"),
|
||||
Ok((
|
||||
"",
|
||||
Timestamp::ActiveRange {
|
||||
start: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(9),
|
||||
minute: Some(39),
|
||||
},
|
||||
end: Datetime {
|
||||
year: 2003,
|
||||
month: 9,
|
||||
day: 16,
|
||||
dayname: "Tue".into(),
|
||||
hour: Some(10),
|
||||
minute: Some(39),
|
||||
},
|
||||
repeater: None,
|
||||
delay: None
|
||||
},
|
||||
))
|
||||
);
|
||||
}
|
||||
|
|
@ -1,551 +0,0 @@
|
|||
//! Headline Title
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::{borrow::Cow, iter::FromIterator};
|
||||
|
||||
use memchr::memrchr2;
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take_until, take_while},
|
||||
character::complete::{anychar, line_ending, space1},
|
||||
combinator::{map, opt, verify},
|
||||
error::{make_error, ErrorKind},
|
||||
multi::fold_many0,
|
||||
sequence::{delimited, preceded},
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::ParseConfig,
|
||||
elements::{drawer::parse_drawer_without_blank, Planning, Timestamp},
|
||||
parse::combinators::{blank_lines_count, line, one_word},
|
||||
};
|
||||
|
||||
/// Title Element
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Title<'a> {
|
||||
/// Headline level, number of stars
|
||||
pub level: usize,
|
||||
/// Headline priority cookie
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub priority: Option<char>,
|
||||
/// Headline title tags
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Vec::is_empty"))]
|
||||
pub tags: Vec<Cow<'a, str>>,
|
||||
/// Headline todo keyword
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub keyword: Option<Cow<'a, str>>,
|
||||
/// Raw headline's text, without the stars and the tags
|
||||
pub raw: Cow<'a, str>,
|
||||
/// Planning element associated to this headline
|
||||
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub planning: Option<Box<Planning<'a>>>,
|
||||
/// Property drawer associated to this headline
|
||||
#[cfg_attr(
|
||||
feature = "ser",
|
||||
serde(skip_serializing_if = "PropertiesMap::is_empty")
|
||||
)]
|
||||
pub properties: PropertiesMap<'a>,
|
||||
/// Numbers of blank lines between last title's line and next non-blank line
|
||||
/// or buffer's end
|
||||
pub post_blank: usize,
|
||||
}
|
||||
|
||||
impl Title<'_> {
|
||||
pub(crate) fn parse<'a>(
|
||||
input: &'a str,
|
||||
config: &ParseConfig,
|
||||
) -> Option<(&'a str, (Title<'a>, &'a str))> {
|
||||
parse_title(input, config).ok()
|
||||
}
|
||||
|
||||
// TODO: fn is_quoted(&self) -> bool { }
|
||||
// TODO: fn is_footnote_section(&self) -> bool { }
|
||||
|
||||
/// Returns this headline's closed timestamp, or `None` if not set.
|
||||
pub fn closed(&self) -> Option<&Timestamp> {
|
||||
self.planning.as_ref().and_then(|p| p.closed.as_ref())
|
||||
}
|
||||
|
||||
/// Returns this headline's scheduled timestamp, or `None` if not set.
|
||||
pub fn scheduled(&self) -> Option<&Timestamp> {
|
||||
self.planning.as_ref().and_then(|p| p.scheduled.as_ref())
|
||||
}
|
||||
|
||||
/// Returns this headline's deadline timestamp, or `None` if not set.
|
||||
pub fn deadline(&self) -> Option<&Timestamp> {
|
||||
self.planning.as_ref().and_then(|p| p.deadline.as_ref())
|
||||
}
|
||||
|
||||
/// Returns `true` if this headline is archived
|
||||
pub fn is_archived(&self) -> bool {
|
||||
self.tags.iter().any(|tag| tag == "ARCHIVE")
|
||||
}
|
||||
|
||||
/// Returns `true` if this headline is commented
|
||||
pub fn is_commented(&self) -> bool {
|
||||
self.raw.starts_with("COMMENT")
|
||||
&& (self.raw.len() == 7 || self.raw[7..].starts_with(char::is_whitespace))
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> Title<'static> {
|
||||
Title {
|
||||
level: self.level,
|
||||
priority: self.priority,
|
||||
tags: self
|
||||
.tags
|
||||
.into_iter()
|
||||
.map(|s| s.into_owned().into())
|
||||
.collect(),
|
||||
keyword: self.keyword.map(Into::into).map(Cow::Owned),
|
||||
raw: self.raw.into_owned().into(),
|
||||
planning: self.planning.map(|p| Box::new(p.into_owned())),
|
||||
properties: self.properties.into_owned(),
|
||||
post_blank: self.post_blank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Title<'_> {
|
||||
fn default() -> Title<'static> {
|
||||
Title {
|
||||
level: 1,
|
||||
priority: None,
|
||||
tags: Vec::new(),
|
||||
keyword: None,
|
||||
raw: Cow::Borrowed(""),
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq))]
|
||||
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
|
||||
pub struct PropertiesMap<'a> {
|
||||
pub pairs: Vec<(Cow<'a, str>, Cow<'a, str>)>,
|
||||
}
|
||||
|
||||
impl<'a> PropertiesMap<'a> {
|
||||
pub fn new() -> Self {
|
||||
PropertiesMap { pairs: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.pairs.is_empty()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &(Cow<'a, str>, Cow<'a, str>)> {
|
||||
self.pairs.iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut (Cow<'a, str>, Cow<'a, str>)> {
|
||||
self.pairs.iter_mut()
|
||||
}
|
||||
|
||||
pub fn into_iter(self) -> impl Iterator<Item = (Cow<'a, str>, Cow<'a, str>)> {
|
||||
self.pairs.into_iter()
|
||||
}
|
||||
|
||||
pub fn into_hash_map(self) -> HashMap<Cow<'a, str>, Cow<'a, str>> {
|
||||
self.pairs.into_iter().collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "indexmap")]
|
||||
pub fn into_index_map(self) -> indexmap::IndexMap<Cow<'a, str>, Cow<'a, str>> {
|
||||
self.pairs.into_iter().collect()
|
||||
}
|
||||
|
||||
pub fn into_owned(self) -> PropertiesMap<'static> {
|
||||
self.pairs
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.into_owned().into(), v.into_owned().into()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<(Cow<'a, str>, Cow<'a, str>)> for PropertiesMap<'a> {
|
||||
fn from_iter<T: IntoIterator<Item = (Cow<'a, str>, Cow<'a, str>)>>(iter: T) -> Self {
|
||||
let mut map = PropertiesMap::new();
|
||||
map.pairs.extend(iter);
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
fn white_spaces_or_eol(input: &str) -> IResult<&str, &str, ()> {
|
||||
alt((space1, line_ending))(input)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_title<'a>(
|
||||
input: &'a str,
|
||||
config: &ParseConfig,
|
||||
) -> IResult<&'a str, (Title<'a>, &'a str), ()> {
|
||||
let (input, level) = map(take_while(|c: char| c == '*'), |s: &str| s.len())(input)?;
|
||||
|
||||
debug_assert!(level > 0);
|
||||
|
||||
let (input, keyword) = opt(preceded(
|
||||
space1,
|
||||
verify(one_word, |s: &str| {
|
||||
config.todo_keywords.0.iter().any(|x| x == s)
|
||||
|| config.todo_keywords.1.iter().any(|x| x == s)
|
||||
}),
|
||||
))(input)?;
|
||||
|
||||
let (input, priority) = opt(delimited(
|
||||
space1,
|
||||
delimited(
|
||||
tag("[#"),
|
||||
verify(anychar, |c: &char| c.is_ascii_uppercase()),
|
||||
tag("]"),
|
||||
),
|
||||
white_spaces_or_eol,
|
||||
))(input)?;
|
||||
let (input, tail) = line(input)?;
|
||||
let tail = tail.trim();
|
||||
|
||||
// tags can be separated by space or \t
|
||||
let (raw, tags) = memrchr2(b' ', b'\t', tail.as_bytes())
|
||||
.map(|i| (tail[0..i].trim(), &tail[i + 1..]))
|
||||
.filter(|(_, x)| is_tag_line(x))
|
||||
.unwrap_or((tail, ""));
|
||||
|
||||
let tags = tags
|
||||
.split(':')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
let (input, planning) = Planning::parse(input)
|
||||
.map(|(input, planning)| (input, Some(Box::new(planning))))
|
||||
.unwrap_or((input, None));
|
||||
|
||||
let (input, properties) = opt(parse_properties_drawer)(input)?;
|
||||
let (input, post_blank) = blank_lines_count(input)?;
|
||||
|
||||
Ok((
|
||||
input,
|
||||
(
|
||||
Title {
|
||||
properties: properties.unwrap_or_default(),
|
||||
level,
|
||||
keyword: keyword.map(Into::into),
|
||||
priority,
|
||||
tags,
|
||||
raw: raw.into(),
|
||||
planning,
|
||||
post_blank,
|
||||
},
|
||||
raw,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn is_tag_line(input: &str) -> bool {
|
||||
input.len() > 2
|
||||
&& input.starts_with(':')
|
||||
&& input.ends_with(':')
|
||||
&& input.chars().all(|ch| {
|
||||
ch.is_alphanumeric() || ch == '_' || ch == '@' || ch == '#' || ch == '%' || ch == ':'
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_properties_drawer(input: &str) -> IResult<&str, PropertiesMap<'_>, ()> {
|
||||
let (input, (drawer, content)) = parse_drawer_without_blank(input.trim_start())?;
|
||||
if drawer.name != "PROPERTIES" {
|
||||
return Err(Err::Error(make_error(input, ErrorKind::Tag)));
|
||||
}
|
||||
let (_, map) = fold_many0(
|
||||
parse_node_property,
|
||||
PropertiesMap::new,
|
||||
|mut acc: PropertiesMap, (name, value)| {
|
||||
acc.pairs.push((name.into(), value.into()));
|
||||
acc
|
||||
},
|
||||
)(content)?;
|
||||
Ok((input, map))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_node_property(input: &str) -> IResult<&str, (&str, &str), ()> {
|
||||
let (input, _) = blank_lines_count(input)?;
|
||||
let input = input.trim_start();
|
||||
let (input, name) = map(delimited(tag(":"), take_until(":"), tag(":")), |s: &str| {
|
||||
s.trim_end_matches('+')
|
||||
})(input)?;
|
||||
let (input, value) = line(input)?;
|
||||
Ok((input, (name, value.trim())))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_title_() {
|
||||
use crate::config::DEFAULT_CONFIG;
|
||||
|
||||
assert_eq!(
|
||||
parse_title("**** DONE [#A] COMMENT Title :tag:a2%:", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: Some("DONE".into()),
|
||||
priority: Some('A'),
|
||||
raw: "COMMENT Title".into(),
|
||||
tags: vec!["tag".into(), "a2%".into()],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title("**** ToDO [#A] COMMENT Title", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "ToDO [#A] COMMENT Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"ToDO [#A] COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title("**** T0DO [#A] COMMENT Title", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "T0DO [#A] COMMENT Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"T0DO [#A] COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title("**** DONE [#1] COMMENT Title", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: Some("DONE".into()),
|
||||
priority: None,
|
||||
raw: "[#1] COMMENT Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"[#1] COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title("**** DONE [#a] COMMENT Title", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: Some("DONE".into()),
|
||||
priority: None,
|
||||
raw: "[#a] COMMENT Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"[#a] COMMENT Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
// https://github.com/PoiScript/orgize/issues/20
|
||||
assert_eq!(
|
||||
parse_title("** DONE [#B]::", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 2,
|
||||
keyword: Some("DONE".into()),
|
||||
priority: None,
|
||||
raw: "[#B]::".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"[#B]::"
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_title("**** Title :tag:a2%", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "Title :tag:a2%".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"Title :tag:a2%"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title("**** Title tag:a2%:", &DEFAULT_CONFIG),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "Title tag:a2%:".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"Title tag:a2%:"
|
||||
)
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_title(
|
||||
"**** DONE Title",
|
||||
&ParseConfig {
|
||||
todo_keywords: (vec![], vec![]),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: None,
|
||||
priority: None,
|
||||
raw: "DONE Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"DONE Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_title(
|
||||
"**** TASK [#A] Title",
|
||||
&ParseConfig {
|
||||
todo_keywords: (vec!["TASK".to_string()], vec![]),
|
||||
..Default::default()
|
||||
}
|
||||
),
|
||||
Ok((
|
||||
"",
|
||||
(
|
||||
Title {
|
||||
level: 4,
|
||||
keyword: Some("TASK".into()),
|
||||
priority: Some('A'),
|
||||
raw: "Title".into(),
|
||||
tags: vec![],
|
||||
planning: None,
|
||||
properties: PropertiesMap::new(),
|
||||
post_blank: 0,
|
||||
},
|
||||
"Title"
|
||||
)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_properties_drawer_() {
|
||||
assert_eq!(
|
||||
parse_properties_drawer(" :PROPERTIES:\n :CUSTOM_ID: id\n :END:"),
|
||||
Ok((
|
||||
"",
|
||||
vec![("CUSTOM_ID".into(), "id".into())]
|
||||
.into_iter()
|
||||
.collect::<PropertiesMap>()
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "indexmap")]
|
||||
fn preserve_properties_drawer_order() {
|
||||
let mut vec = Vec::default();
|
||||
// Use a large number of properties to reduce false pass rate, since HashMap
|
||||
// is non-deterministic. There are roughly 10^18 possible derangements of this sequence.
|
||||
for i in 0..20 {
|
||||
// Avoid alphabetic or numeric order.
|
||||
let j = (i + 7) % 20;
|
||||
vec.push((
|
||||
Cow::Owned(format!(
|
||||
"{}{}",
|
||||
if i % 3 == 0 {
|
||||
"FOO"
|
||||
} else if i % 3 == 1 {
|
||||
"QUX"
|
||||
} else {
|
||||
"BAR"
|
||||
},
|
||||
j
|
||||
)),
|
||||
Cow::Owned(i.to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let mut s = String::default();
|
||||
|
||||
for (k, v) in &vec {
|
||||
s += &format!(" :{}: {}\n", k, v);
|
||||
}
|
||||
|
||||
let drawer = format!(" :PROPERTIES:\n{}:END:\n", &s);
|
||||
|
||||
let map = parse_properties_drawer(&drawer).unwrap().1.into_index_map();
|
||||
|
||||
// indexmap should be in the same order as vector
|
||||
for (left, right) in vec.iter().zip(map) {
|
||||
assert_eq!(left, &right);
|
||||
}
|
||||
}
|
||||
210
src/export/forward.rs
Normal file
210
src/export/forward.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/// Forward handler method implement to other handler
|
||||
///
|
||||
/// This macros is commonly used if you want to extend
|
||||
/// some builtin handlers like HtmlExport.
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::{
|
||||
/// ast::HeadlineTitle,
|
||||
/// export::{HtmlExport, TraversalContext, Traverser},
|
||||
/// forward_handler,
|
||||
/// rowan::{ast::AstNode, WalkEvent},
|
||||
/// Org,
|
||||
/// };
|
||||
/// use slugify::slugify;
|
||||
/// use std::cmp::min;
|
||||
///
|
||||
/// #[derive(Default)]
|
||||
/// struct SlugifyTitleHandler(pub HtmlExport);
|
||||
///
|
||||
/// // AsMut trait is required
|
||||
/// impl AsMut<HtmlExport> for SlugifyTitleHandler {
|
||||
/// fn as_mut(&mut self) -> &mut HtmlExport {
|
||||
/// &mut self.0
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl Traverser for SlugifyTitleHandler {
|
||||
/// fn headline_title(&mut self, event: WalkEvent<&HeadlineTitle>, _ctx: &mut TraversalContext) {
|
||||
/// match event {
|
||||
/// WalkEvent::Enter(title) => {
|
||||
/// let level = title.headline().and_then(|h| h.level()).unwrap_or(1);
|
||||
/// let level = min(level, 6);
|
||||
/// let raw = title.syntax().to_string();
|
||||
/// self.0.output += &format!("<h{level}><a id=\"{0}\" href=\"#{0}\">", slugify!(&raw));
|
||||
/// }
|
||||
/// WalkEvent::Leave(title) => {
|
||||
/// let level = title.headline().and_then(|h| h.level()).unwrap_or(1);
|
||||
/// let level = min(level, 6);
|
||||
/// self.0.output += &format!("</a></h{level}>");
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// forward_handler! {
|
||||
/// HtmlExport,
|
||||
/// link text document headline paragraph section rule comment
|
||||
/// inline_src inline_call code bold verbatim italic strike underline list list_item list_item_tag
|
||||
/// special_block quote_block center_block verse_block comment_block example_block export_block
|
||||
/// source_block babel_call clock cookie radio_target drawer dyn_block fn_def fn_ref macros
|
||||
/// snippet timestamp target fixed_width org_table org_table_row org_table_cell list_item_content
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let mut handler = SlugifyTitleHandler::default();
|
||||
/// Org::parse("* hello world!").traverse(&mut handler);
|
||||
/// assert_eq!(handler.0.finish(), r##"<main><h1><a id="hello-world" href="#hello-world">hello world!</a></h1></main>"##);
|
||||
/// ```
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! forward_handler {
|
||||
($handler:ty, $($func:ident)*) => {
|
||||
$(
|
||||
forward_handler!(@method $handler, $func);
|
||||
)*
|
||||
};
|
||||
|
||||
(@method $handler:ty, text) => {
|
||||
forward_handler!(@method $handler, text, ::orgize::SyntaxToken);
|
||||
};
|
||||
(@method $handler:ty, document) => {
|
||||
forward_handler!(@method $handler, document, WalkEvent<&::orgize::ast::Document>);
|
||||
};
|
||||
(@method $handler:ty, headline) => {
|
||||
forward_handler!(@method $handler, headline, WalkEvent<&::orgize::ast::Headline>);
|
||||
};
|
||||
(@method $handler:ty, paragraph) => {
|
||||
forward_handler!(@method $handler, paragraph, WalkEvent<&::orgize::ast::Paragraph>);
|
||||
};
|
||||
(@method $handler:ty, section) => {
|
||||
forward_handler!(@method $handler, section, WalkEvent<&::orgize::ast::Section>);
|
||||
};
|
||||
(@method $handler:ty, rule) => {
|
||||
forward_handler!(@method $handler, rule, WalkEvent<&::orgize::ast::Rule>);
|
||||
};
|
||||
(@method $handler:ty, comment) => {
|
||||
forward_handler!(@method $handler, comment, WalkEvent<&::orgize::ast::Comment>);
|
||||
};
|
||||
(@method $handler:ty, inline_src) => {
|
||||
forward_handler!(@method $handler, inline_src, WalkEvent<&::orgize::ast::InlineSrc>);
|
||||
};
|
||||
(@method $handler:ty, inline_call) => {
|
||||
forward_handler!(@method $handler, inline_call, WalkEvent<&::orgize::ast::InlineCall>);
|
||||
};
|
||||
(@method $handler:ty, code) => {
|
||||
forward_handler!(@method $handler, code, WalkEvent<&::orgize::ast::Code>);
|
||||
};
|
||||
(@method $handler:ty, bold) => {
|
||||
forward_handler!(@method $handler, bold, WalkEvent<&::orgize::ast::Bold>);
|
||||
};
|
||||
(@method $handler:ty, verbatim) => {
|
||||
forward_handler!(@method $handler, verbatim, WalkEvent<&::orgize::ast::Verbatim>);
|
||||
};
|
||||
(@method $handler:ty, italic) => {
|
||||
forward_handler!(@method $handler, italic, WalkEvent<&::orgize::ast::Italic>);
|
||||
};
|
||||
(@method $handler:ty, strike) => {
|
||||
forward_handler!(@method $handler, strike, WalkEvent<&::orgize::ast::Strike>);
|
||||
};
|
||||
(@method $handler:ty, underline) => {
|
||||
forward_handler!(@method $handler, underline, WalkEvent<&::orgize::ast::Underline>);
|
||||
};
|
||||
(@method $handler:ty, list) => {
|
||||
forward_handler!(@method $handler, list, WalkEvent<&::orgize::ast::List>);
|
||||
};
|
||||
(@method $handler:ty, list_item) => {
|
||||
forward_handler!(@method $handler, list_item, WalkEvent<&::orgize::ast::ListItem>);
|
||||
};
|
||||
(@method $handler:ty, list_item_tag) => {
|
||||
forward_handler!(@method $handler, list_item_tag, WalkEvent<&::orgize::ast::ListItemTag>);
|
||||
};
|
||||
(@method $handler:ty, list_item_content) => {
|
||||
forward_handler!(@method $handler, list_item_content, WalkEvent<&::orgize::ast::ListItemContent>);
|
||||
};
|
||||
(@method $handler:ty, special_block) => {
|
||||
forward_handler!(@method $handler, special_block, WalkEvent<&::orgize::ast::SpecialBlock>);
|
||||
};
|
||||
(@method $handler:ty, quote_block) => {
|
||||
forward_handler!(@method $handler, quote_block, WalkEvent<&::orgize::ast::QuoteBlock>);
|
||||
};
|
||||
(@method $handler:ty, center_block) => {
|
||||
forward_handler!(@method $handler, center_block, WalkEvent<&::orgize::ast::CenterBlock>);
|
||||
};
|
||||
(@method $handler:ty, verse_block) => {
|
||||
forward_handler!(@method $handler, verse_block, WalkEvent<&::orgize::ast::VerseBlock>);
|
||||
};
|
||||
(@method $handler:ty, comment_block) => {
|
||||
forward_handler!(@method $handler, comment_block, WalkEvent<&::orgize::ast::CommentBlock>);
|
||||
};
|
||||
(@method $handler:ty, example_block) => {
|
||||
forward_handler!(@method $handler, example_block, WalkEvent<&::orgize::ast::ExampleBlock>);
|
||||
};
|
||||
(@method $handler:ty, export_block) => {
|
||||
forward_handler!(@method $handler, export_block, WalkEvent<&::orgize::ast::ExportBlock>);
|
||||
};
|
||||
(@method $handler:ty, source_block) => {
|
||||
forward_handler!(@method $handler, source_block, WalkEvent<&::orgize::ast::SourceBlock>);
|
||||
};
|
||||
(@method $handler:ty, babel_call) => {
|
||||
forward_handler!(@method $handler, babel_call, WalkEvent<&::orgize::ast::BabelCall>);
|
||||
};
|
||||
(@method $handler:ty, clock) => {
|
||||
forward_handler!(@method $handler, clock, WalkEvent<&::orgize::ast::Clock>);
|
||||
};
|
||||
(@method $handler:ty, cookie) => {
|
||||
forward_handler!(@method $handler, cookie, WalkEvent<&::orgize::ast::Cookie>);
|
||||
};
|
||||
(@method $handler:ty, radio_target) => {
|
||||
forward_handler!(@method $handler, radio_target, WalkEvent<&::orgize::ast::RadioTarget>);
|
||||
};
|
||||
(@method $handler:ty, drawer) => {
|
||||
forward_handler!(@method $handler, drawer, WalkEvent<&::orgize::ast::Drawer>);
|
||||
};
|
||||
(@method $handler:ty, dyn_block) => {
|
||||
forward_handler!(@method $handler, dyn_block, WalkEvent<&::orgize::ast::DynBlock>);
|
||||
};
|
||||
(@method $handler:ty, fn_def) => {
|
||||
forward_handler!(@method $handler, fn_def, WalkEvent<&::orgize::ast::FnDef>);
|
||||
};
|
||||
(@method $handler:ty, fn_ref) => {
|
||||
forward_handler!(@method $handler, fn_ref, WalkEvent<&::orgize::ast::FnRef>);
|
||||
};
|
||||
(@method $handler:ty, macros) => {
|
||||
forward_handler!(@method $handler, macros, WalkEvent<&::orgize::ast::Macros>);
|
||||
};
|
||||
(@method $handler:ty, snippet) => {
|
||||
forward_handler!(@method $handler, snippet, WalkEvent<&::orgize::ast::Snippet>);
|
||||
};
|
||||
(@method $handler:ty, timestamp) => {
|
||||
forward_handler!(@method $handler, timestamp, WalkEvent<&::orgize::ast::Timestamp>);
|
||||
};
|
||||
(@method $handler:ty, target) => {
|
||||
forward_handler!(@method $handler, target, WalkEvent<&::orgize::ast::Target>);
|
||||
};
|
||||
(@method $handler:ty, fixed_width) => {
|
||||
forward_handler!(@method $handler, fixed_width, WalkEvent<&::orgize::ast::FixedWidth>);
|
||||
};
|
||||
(@method $handler:ty, headline_title) => {
|
||||
forward_handler!(@method $handler, headline_title, WalkEvent<&::orgize::ast::HeadlineTitle>);
|
||||
};
|
||||
(@method $handler:ty, org_table) => {
|
||||
forward_handler!(@method $handler, org_table, WalkEvent<&::orgize::ast::OrgTable>);
|
||||
};
|
||||
(@method $handler:ty, org_table_row) => {
|
||||
forward_handler!(@method $handler, org_table_row, WalkEvent<&::orgize::ast::OrgTableRow>);
|
||||
};
|
||||
(@method $handler:ty, org_table_cell) => {
|
||||
forward_handler!(@method $handler, org_table_cell, WalkEvent<&::orgize::ast::OrgTableCell>);
|
||||
};
|
||||
(@method $handler:ty, link) => {
|
||||
forward_handler!(@method $handler, link, WalkEvent<&::orgize::ast::Link>);
|
||||
};
|
||||
(@method $handler:ty, $x:ident) => {
|
||||
std::compile_error!(std::concat!(std::stringify!($x), " is not a method"));
|
||||
};
|
||||
|
||||
(@method $handler:ty, $name:ident, $type:ty) => {
|
||||
fn $name(&mut self, item: $type, ctx: &mut ::orgize::export::TraversalContext) {
|
||||
<Self as AsMut<$handler>>::as_mut(self).$name(item, ctx)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
use rowan::WalkEvent;
|
||||
use std::fmt;
|
||||
use std::io::{Error, Result as IOResult, Write};
|
||||
|
||||
use jetscii::{bytes, BytesConst};
|
||||
|
||||
use crate::elements::{Element, Table, TableCell, TableRow, Timestamp};
|
||||
use crate::export::write_datetime;
|
||||
use super::TraversalContext;
|
||||
use super::Traverser;
|
||||
use crate::ast::*;
|
||||
use crate::syntax::SyntaxToken;
|
||||
|
||||
/// A wrapper for escaping sensitive characters in html.
|
||||
///
|
||||
|
|
@ -26,11 +26,7 @@ impl<S: AsRef<str>> fmt::Display for HtmlEscape<S> {
|
|||
let content = self.0.as_ref();
|
||||
let bytes = content.as_bytes();
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref ESCAPE_BYTES: BytesConst = bytes!(b'<', b'>', b'&', b'\'', b'"');
|
||||
}
|
||||
|
||||
while let Some(off) = ESCAPE_BYTES.find(&bytes[pos..]) {
|
||||
while let Some(off) = jetscii::bytes!(b'<', b'>', b'&', b'\'', b'"').find(&bytes[pos..]) {
|
||||
write!(f, "{}", &content[pos..pos + off])?;
|
||||
|
||||
pos += off + 1;
|
||||
|
|
@ -49,349 +45,359 @@ impl<S: AsRef<str>> fmt::Display for HtmlEscape<S> {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait HtmlHandler<E: From<Error>>: Default {
|
||||
fn start<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E>;
|
||||
fn end<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E>;
|
||||
}
|
||||
|
||||
/// Default Html Handler
|
||||
#[derive(Default)]
|
||||
pub struct DefaultHtmlHandler;
|
||||
pub struct HtmlExport {
|
||||
pub output: String,
|
||||
in_descriptive_list: Vec<bool>,
|
||||
}
|
||||
|
||||
impl HtmlHandler<Error> for DefaultHtmlHandler {
|
||||
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> IOResult<()> {
|
||||
match element {
|
||||
// container elements
|
||||
Element::SpecialBlock(_) => (),
|
||||
Element::QuoteBlock(_) => write!(w, "<blockquote>")?,
|
||||
Element::CenterBlock(_) => write!(w, "<div class=\"center\">")?,
|
||||
Element::VerseBlock(_) => write!(w, "<p class=\"verse\">")?,
|
||||
Element::Bold => write!(w, "<b>")?,
|
||||
Element::Document { .. } => write!(w, "<main>")?,
|
||||
Element::DynBlock(_dyn_block) => (),
|
||||
Element::Headline { .. } => (),
|
||||
Element::List(list) => {
|
||||
if list.ordered {
|
||||
write!(w, "<ol>")?;
|
||||
} else {
|
||||
write!(w, "<ul>")?;
|
||||
}
|
||||
}
|
||||
Element::Italic => write!(w, "<i>")?,
|
||||
Element::ListItem(_) => write!(w, "<li>")?,
|
||||
Element::Paragraph { .. } => write!(w, "<p>")?,
|
||||
Element::Section => write!(w, "<section>")?,
|
||||
Element::Strike => write!(w, "<s>")?,
|
||||
Element::Underline => write!(w, "<u>")?,
|
||||
// non-container elements
|
||||
Element::CommentBlock(_) => (),
|
||||
Element::ExampleBlock(block) => write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
HtmlEscape(&block.contents)
|
||||
)?,
|
||||
Element::ExportBlock(block) => {
|
||||
if block.data.eq_ignore_ascii_case("HTML") {
|
||||
write!(w, "{}", block.contents)?
|
||||
}
|
||||
}
|
||||
Element::SourceBlock(block) => {
|
||||
if block.language.is_empty() {
|
||||
write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
HtmlEscape(&block.contents)
|
||||
)?;
|
||||
} else {
|
||||
write!(
|
||||
w,
|
||||
"<div class=\"org-src-container\"><pre class=\"src src-{}\">{}</pre></div>",
|
||||
block.language,
|
||||
HtmlEscape(&block.contents)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Element::BabelCall(_) => (),
|
||||
Element::InlineSrc(inline_src) => write!(
|
||||
w,
|
||||
"<code class=\"src src-{}\">{}</code>",
|
||||
inline_src.lang,
|
||||
HtmlEscape(&inline_src.body)
|
||||
)?,
|
||||
Element::Code { value } => write!(w, "<code>{}</code>", HtmlEscape(value))?,
|
||||
Element::FnRef(_fn_ref) => (),
|
||||
Element::InlineCall(_) => (),
|
||||
Element::Link(link) => write!(
|
||||
w,
|
||||
"<a href=\"{}\">{}</a>",
|
||||
HtmlEscape(&link.path),
|
||||
HtmlEscape(link.desc.as_ref().unwrap_or(&link.path)),
|
||||
)?,
|
||||
Element::Macros(_macros) => (),
|
||||
Element::RadioTarget => (),
|
||||
Element::Snippet(snippet) => {
|
||||
if snippet.name.eq_ignore_ascii_case("HTML") {
|
||||
write!(w, "{}", snippet.value)?;
|
||||
}
|
||||
}
|
||||
Element::Target(_target) => (),
|
||||
Element::Text { value } => write!(w, "{}", HtmlEscape(value))?,
|
||||
Element::Timestamp(timestamp) => {
|
||||
write!(
|
||||
&mut w,
|
||||
"<span class=\"timestamp-wrapper\"><span class=\"timestamp\">"
|
||||
)?;
|
||||
|
||||
match timestamp {
|
||||
Timestamp::Active { start, .. } => {
|
||||
write_datetime(&mut w, "<", start, ">")?;
|
||||
}
|
||||
Timestamp::Inactive { start, .. } => {
|
||||
write_datetime(&mut w, "[", start, "]")?;
|
||||
}
|
||||
Timestamp::ActiveRange { start, end, .. } => {
|
||||
write_datetime(&mut w, "<", start, ">–")?;
|
||||
write_datetime(&mut w, "<", end, ">")?;
|
||||
}
|
||||
Timestamp::InactiveRange { start, end, .. } => {
|
||||
write_datetime(&mut w, "[", start, "]–")?;
|
||||
write_datetime(&mut w, "[", end, "]")?;
|
||||
}
|
||||
Timestamp::Diary { value } => {
|
||||
write!(&mut w, "<%%({})>", HtmlEscape(value))?
|
||||
}
|
||||
}
|
||||
|
||||
write!(&mut w, "</span></span>")?;
|
||||
}
|
||||
Element::Verbatim { value } => write!(&mut w, "<code>{}</code>", HtmlEscape(value))?,
|
||||
Element::FnDef(_fn_def) => (),
|
||||
Element::Clock(_clock) => (),
|
||||
Element::Comment(_) => (),
|
||||
Element::FixedWidth(fixed_width) => write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
HtmlEscape(&fixed_width.value)
|
||||
)?,
|
||||
Element::Keyword(_keyword) => (),
|
||||
Element::Drawer(_drawer) => (),
|
||||
Element::Rule(_) => write!(w, "<hr>")?,
|
||||
Element::Cookie(cookie) => write!(w, "<code>{}</code>", cookie.value)?,
|
||||
Element::Title(title) => {
|
||||
write!(w, "<h{}>", if title.level <= 6 { title.level } else { 6 })?;
|
||||
}
|
||||
Element::Table(Table::TableEl { .. }) => (),
|
||||
Element::Table(Table::Org { has_header, .. }) => {
|
||||
write!(w, "<table>")?;
|
||||
if *has_header {
|
||||
write!(w, "<thead>")?;
|
||||
} else {
|
||||
write!(w, "<tbody>")?;
|
||||
}
|
||||
}
|
||||
Element::TableRow(row) => match row {
|
||||
TableRow::Body => write!(w, "<tr>")?,
|
||||
TableRow::BodyRule => write!(w, "</tbody><tbody>")?,
|
||||
TableRow::Header => write!(w, "<tr>")?,
|
||||
TableRow::HeaderRule => write!(w, "</thead><tbody>")?,
|
||||
},
|
||||
Element::TableCell(cell) => match cell {
|
||||
TableCell::Body => write!(w, "<td>")?,
|
||||
TableCell::Header => write!(w, "<th>")?,
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> IOResult<()> {
|
||||
match element {
|
||||
// container elements
|
||||
Element::SpecialBlock(_) => (),
|
||||
Element::QuoteBlock(_) => write!(w, "</blockquote>")?,
|
||||
Element::CenterBlock(_) => write!(w, "</div>")?,
|
||||
Element::VerseBlock(_) => write!(w, "</p>")?,
|
||||
Element::Bold => write!(w, "</b>")?,
|
||||
Element::Document { .. } => write!(w, "</main>")?,
|
||||
Element::DynBlock(_dyn_block) => (),
|
||||
Element::Headline { .. } => (),
|
||||
Element::List(list) => {
|
||||
if list.ordered {
|
||||
write!(w, "</ol>")?;
|
||||
} else {
|
||||
write!(w, "</ul>")?;
|
||||
}
|
||||
}
|
||||
Element::Italic => write!(w, "</i>")?,
|
||||
Element::ListItem(_) => write!(w, "</li>")?,
|
||||
Element::Paragraph { .. } => write!(w, "</p>")?,
|
||||
Element::Section => write!(w, "</section>")?,
|
||||
Element::Strike => write!(w, "</s>")?,
|
||||
Element::Underline => write!(w, "</u>")?,
|
||||
Element::Title(title) => {
|
||||
write!(w, "</h{}>", if title.level <= 6 { title.level } else { 6 })?
|
||||
}
|
||||
Element::Table(Table::TableEl { .. }) => (),
|
||||
Element::Table(Table::Org { .. }) => {
|
||||
write!(w, "</tbody></table>")?;
|
||||
}
|
||||
Element::TableRow(TableRow::Body) | Element::TableRow(TableRow::Header) => {
|
||||
write!(w, "</tr>")?;
|
||||
}
|
||||
Element::TableCell(cell) => match cell {
|
||||
TableCell::Body => write!(w, "</td>")?,
|
||||
TableCell::Header => write!(w, "</th>")?,
|
||||
},
|
||||
// non-container elements
|
||||
_ => debug_assert!(!element.is_container()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
impl HtmlExport {
|
||||
pub fn finish(self) -> String {
|
||||
self.output
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
mod syntect_handler {
|
||||
use super::*;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use syntect::{
|
||||
easy::HighlightLines,
|
||||
highlighting::ThemeSet,
|
||||
html::{styled_line_to_highlighted_html, IncludeBackground},
|
||||
parsing::SyntaxSet,
|
||||
};
|
||||
|
||||
/// Syntect Html Handler
|
||||
///
|
||||
/// Simple Usage:
|
||||
///
|
||||
/// ```rust
|
||||
/// use orgize::Org;
|
||||
/// use orgize::export::{DefaultHtmlHandler, SyntectHtmlHandler};
|
||||
///
|
||||
/// let mut handler = SyntectHtmlHandler::new(DefaultHtmlHandler);
|
||||
/// let org = Org::parse("src_rust{println!(\"Hello\")}");
|
||||
///
|
||||
/// let mut vec = vec![];
|
||||
///
|
||||
/// org.write_html_custom(&mut vec, &mut handler).unwrap();
|
||||
/// ```
|
||||
///
|
||||
/// Customize:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// // orgize has re-exported the whole syntect crate
|
||||
/// use orgize::syntect::parsing::SyntaxSet;
|
||||
/// use orgize::export::{DefaultHtmlHandler, SyntectHtmlHandler};
|
||||
///
|
||||
/// let mut handler = SyntectHtmlHandler {
|
||||
/// syntax_set: {
|
||||
/// let set = SyntaxSet::load_defaults_newlines();
|
||||
/// let mut builder = set.into_builder();
|
||||
/// // add extra language syntax
|
||||
/// builder.add_from_folder("path/to/syntax/dir", true).unwrap();
|
||||
/// builder.build()
|
||||
/// },
|
||||
/// // specify theme
|
||||
/// theme: String::from("Solarized (dark)"),
|
||||
/// inner: DefaultHtmlHandler,
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
///
|
||||
/// // Make sure to check if theme presents or it will panic at runtime
|
||||
/// if handler.theme_set.themes.contains_key("dont-exists") {
|
||||
///
|
||||
/// }
|
||||
/// ```
|
||||
pub struct SyntectHtmlHandler<E: From<Error>, H: HtmlHandler<E>> {
|
||||
/// syntax set, default is `SyntaxSet::load_defaults_newlines()`
|
||||
pub syntax_set: SyntaxSet,
|
||||
/// theme set, default is `ThemeSet::load_defaults()`
|
||||
pub theme_set: ThemeSet,
|
||||
/// theme used for highlighting, default is `"InspiredGitHub"`
|
||||
pub theme: String,
|
||||
/// inner html handler
|
||||
pub inner: H,
|
||||
/// background color, default is `IncludeBackground::No`
|
||||
pub background: IncludeBackground,
|
||||
/// handler error type
|
||||
pub error_type: PhantomData<E>,
|
||||
impl Traverser for HtmlExport {
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn text(&mut self, token: SyntaxToken, _ctx: &mut TraversalContext) {
|
||||
self.output += &HtmlEscape(token.text()).to_string();
|
||||
}
|
||||
|
||||
impl<E: From<Error>, H: HtmlHandler<E>> SyntectHtmlHandler<E, H> {
|
||||
pub fn new(inner: H) -> Self {
|
||||
SyntectHtmlHandler {
|
||||
inner,
|
||||
..Default::default()
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn document(&mut self, event: WalkEvent<&Document>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<main>",
|
||||
WalkEvent::Leave(_) => "</main>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn list(&mut self, event: WalkEvent<&List>, _ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
WalkEvent::Enter(list) => {
|
||||
self.output += if list.is_ordered() {
|
||||
self.in_descriptive_list.push(false);
|
||||
"<ol>"
|
||||
} else if list.is_descriptive() {
|
||||
self.in_descriptive_list.push(true);
|
||||
"<dl>"
|
||||
} else {
|
||||
self.in_descriptive_list.push(false);
|
||||
"<ul>"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight(&self, language: Option<&str>, content: &str) -> String {
|
||||
let mut highlighter = HighlightLines::new(
|
||||
language
|
||||
.and_then(|lang| self.syntax_set.find_syntax_by_token(lang))
|
||||
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()),
|
||||
&self.theme_set.themes[&self.theme],
|
||||
);
|
||||
let regions = highlighter.highlight(content, &self.syntax_set);
|
||||
styled_line_to_highlighted_html(®ions[..], self.background)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: From<Error>, H: HtmlHandler<E>> Default for SyntectHtmlHandler<E, H> {
|
||||
fn default() -> Self {
|
||||
SyntectHtmlHandler {
|
||||
syntax_set: SyntaxSet::load_defaults_newlines(),
|
||||
theme_set: ThemeSet::load_defaults(),
|
||||
theme: String::from("InspiredGitHub"),
|
||||
inner: H::default(),
|
||||
background: IncludeBackground::No,
|
||||
error_type: PhantomData,
|
||||
WalkEvent::Leave(list) => {
|
||||
self.output += if list.is_ordered() {
|
||||
"</ol>"
|
||||
} else if let Some(true) = self.in_descriptive_list.last() {
|
||||
"</dl>"
|
||||
} else {
|
||||
"</ul>"
|
||||
};
|
||||
self.in_descriptive_list.pop();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn list_item(&mut self, event: WalkEvent<&ListItem>, _ctx: &mut TraversalContext) {
|
||||
if !self.in_descriptive_list.last().copied().unwrap_or_default() {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<li>",
|
||||
WalkEvent::Leave(_) => "</li>",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: From<Error>, H: HtmlHandler<E>> HtmlHandler<E> for SyntectHtmlHandler<E, H> {
|
||||
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), E> {
|
||||
match element {
|
||||
Element::InlineSrc(inline_src) => write!(
|
||||
w,
|
||||
"<code>{}</code>",
|
||||
self.highlight(Some(&inline_src.lang), &inline_src.body)
|
||||
)?,
|
||||
Element::SourceBlock(block) => {
|
||||
if block.language.is_empty() {
|
||||
write!(w, "<pre class=\"example\">{}</pre>", block.contents)?;
|
||||
} else {
|
||||
write!(
|
||||
w,
|
||||
"<div class=\"org-src-container\"><pre class=\"src src-{}\">{}</pre></div>",
|
||||
block.language,
|
||||
self.highlight(Some(&block.language), &block.contents)
|
||||
)?;
|
||||
}
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn list_item_content(
|
||||
&mut self,
|
||||
event: WalkEvent<&ListItemContent>,
|
||||
_ctx: &mut TraversalContext,
|
||||
) {
|
||||
if self.in_descriptive_list.last().copied().unwrap_or_default() {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<dd>",
|
||||
WalkEvent::Leave(_) => "</dd>",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn list_item_tag(&mut self, event: WalkEvent<&ListItemTag>, _ctx: &mut TraversalContext) {
|
||||
if self.in_descriptive_list.last().copied().unwrap_or_default() {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<dt>",
|
||||
WalkEvent::Leave(_) => "</dt>",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn paragraph(&mut self, event: WalkEvent<&Paragraph>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<p>",
|
||||
WalkEvent::Leave(_) => "</p>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn section(&mut self, event: WalkEvent<&Section>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<section>",
|
||||
WalkEvent::Leave(_) => "</section>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn fixed_width(&mut self, event: WalkEvent<&FixedWidth>, _ctx: &mut TraversalContext) {
|
||||
if let WalkEvent::Enter(_f) = event {
|
||||
// self.output += f.text();
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, ctx))]
|
||||
fn snippet(&mut self, event: WalkEvent<&Snippet>, ctx: &mut TraversalContext) {
|
||||
if let WalkEvent::Enter(snippet) = event {
|
||||
if matches!(snippet.name(), Some(name) if name.text().eq_ignore_ascii_case("html")) {
|
||||
if let Some(value) = snippet.value() {
|
||||
self.output += value.text()
|
||||
}
|
||||
Element::FixedWidth(fixed_width) => write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
self.highlight(None, &fixed_width.value)
|
||||
)?,
|
||||
Element::ExampleBlock(block) => write!(
|
||||
w,
|
||||
"<pre class=\"example\">{}</pre>",
|
||||
self.highlight(None, &block.contents)
|
||||
)?,
|
||||
_ => self.inner.start(w, element)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
return ctx.skip();
|
||||
};
|
||||
}
|
||||
|
||||
fn end<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E> {
|
||||
self.inner.end(w, element)
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn headline_title(&mut self, event: WalkEvent<&HeadlineTitle>, _ctx: &mut TraversalContext) {
|
||||
self.output += &match event {
|
||||
WalkEvent::Enter(title) => {
|
||||
let level = title
|
||||
.headline()
|
||||
.and_then(|hdl| hdl.level())
|
||||
.map(|lvl| std::cmp::min(lvl, 6))
|
||||
.unwrap_or(1);
|
||||
format!("<h{level}>")
|
||||
}
|
||||
WalkEvent::Leave(title) => {
|
||||
let level = title
|
||||
.headline()
|
||||
.and_then(|hdl| hdl.level())
|
||||
.map(|lvl| std::cmp::min(lvl, 6))
|
||||
.unwrap_or(1);
|
||||
format!("</h{level}>")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn italic(&mut self, event: WalkEvent<&Italic>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<i>",
|
||||
WalkEvent::Leave(_) => "</i>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn bold(&mut self, event: WalkEvent<&Bold>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<b>",
|
||||
WalkEvent::Leave(_) => "</b>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn strike(&mut self, event: WalkEvent<&Strike>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<s>",
|
||||
WalkEvent::Leave(_) => "</s>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn underline(&mut self, event: WalkEvent<&Underline>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<u>",
|
||||
WalkEvent::Leave(_) => "</u>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn verbatim(&mut self, event: WalkEvent<&Verbatim>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<code>",
|
||||
WalkEvent::Leave(_) => "</code>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn code(&mut self, event: WalkEvent<&Code>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<code>",
|
||||
WalkEvent::Leave(_) => "</code>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, ctx))]
|
||||
fn rule(&mut self, event: WalkEvent<&Rule>, ctx: &mut TraversalContext) {
|
||||
if let WalkEvent::Enter(_) = event {
|
||||
self.output += "<hr/>"
|
||||
};
|
||||
ctx.skip()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, ctx))]
|
||||
fn link(&mut self, event: WalkEvent<&Link>, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
WalkEvent::Enter(link) => {
|
||||
let path = link.path();
|
||||
let path = path.as_ref().map(|path| path.text()).unwrap_or_default();
|
||||
|
||||
if link.is_image() {
|
||||
self.output += &format!(r#"<img src="{}">"#, HtmlEscape(path));
|
||||
return ctx.skip();
|
||||
}
|
||||
|
||||
self.output += &format!(r#"<a href="{}">"#, HtmlEscape(path));
|
||||
|
||||
if !link.has_description() {
|
||||
self.output += &HtmlEscape(path).to_string();
|
||||
self.output += "</a>";
|
||||
return ctx.skip();
|
||||
}
|
||||
}
|
||||
WalkEvent::Leave(_) => {
|
||||
self.output += "</a>";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
pub use syntect_handler::SyntectHtmlHandler;
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn quote_block(&mut self, event: WalkEvent<&QuoteBlock>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<blockquote>",
|
||||
WalkEvent::Leave(_) => "</blockquote>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn verse_block(&mut self, event: WalkEvent<&VerseBlock>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<p class=\"verse\">",
|
||||
WalkEvent::Leave(_) => "</p>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn example_block(&mut self, event: WalkEvent<&ExampleBlock>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<pre class=\"example\">",
|
||||
WalkEvent::Leave(_) => "</pre>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn center_block(&mut self, event: WalkEvent<&CenterBlock>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<div class=\"center\">",
|
||||
WalkEvent::Leave(_) => "</div>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn org_table(&mut self, event: WalkEvent<&OrgTable>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<table><tbody>",
|
||||
WalkEvent::Leave(_) => "</tbody></table>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, ctx))]
|
||||
fn org_table_row(&mut self, event: WalkEvent<&OrgTableRow>, ctx: &mut TraversalContext) {
|
||||
if match event {
|
||||
WalkEvent::Enter(n) | WalkEvent::Leave(n) => n.is_rule(),
|
||||
} {
|
||||
return ctx.skip();
|
||||
}
|
||||
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<tr>",
|
||||
WalkEvent::Leave(_) => "</tr>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn org_table_cell(&mut self, event: WalkEvent<&OrgTableCell>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<td>",
|
||||
WalkEvent::Leave(_) => "</td>",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn comment(&mut self, event: WalkEvent<&Comment>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<!--",
|
||||
WalkEvent::Leave(_) => "-->",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn comment_block(&mut self, event: WalkEvent<&CommentBlock>, _ctx: &mut TraversalContext) {
|
||||
self.output += match event {
|
||||
WalkEvent::Enter(_) => "<!--",
|
||||
WalkEvent::Leave(_) => "-->",
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn headline(&mut self, _event: WalkEvent<&Headline>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn inline_src(&mut self, _event: WalkEvent<&InlineSrc>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn inline_call(&mut self, _event: WalkEvent<&InlineCall>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn special_block(&mut self, _event: WalkEvent<&SpecialBlock>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn export_block(&mut self, _event: WalkEvent<&ExportBlock>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn source_block(&mut self, _event: WalkEvent<&SourceBlock>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn babel_call(&mut self, _event: WalkEvent<&BabelCall>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn clock(&mut self, _event: WalkEvent<&Clock>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn cookie(&mut self, _event: WalkEvent<&Cookie>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn radio_target(&mut self, _event: WalkEvent<&RadioTarget>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn drawer(&mut self, _event: WalkEvent<&Drawer>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn dyn_block(&mut self, _event: WalkEvent<&DynBlock>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn fn_def(&mut self, _event: WalkEvent<&FnDef>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn fn_ref(&mut self, _event: WalkEvent<&FnRef>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn macros(&mut self, _event: WalkEvent<&Macros>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn timestamp(&mut self, _event: WalkEvent<&Timestamp>, _ctx: &mut TraversalContext) {}
|
||||
|
||||
#[tracing::instrument(skip(self, _ctx))]
|
||||
fn target(&mut self, _event: WalkEvent<&Target>, _ctx: &mut TraversalContext) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,8 @@
|
|||
//! Export `Org` struct to various formats.
|
||||
|
||||
mod forward;
|
||||
mod html;
|
||||
mod org;
|
||||
mod traverse;
|
||||
|
||||
#[cfg(feature = "syntect")]
|
||||
pub use html::SyntectHtmlHandler;
|
||||
pub use html::{DefaultHtmlHandler, HtmlEscape, HtmlHandler};
|
||||
pub use org::{DefaultOrgHandler, OrgHandler};
|
||||
|
||||
use std::io::{Error, Write};
|
||||
|
||||
use crate::elements::Datetime;
|
||||
|
||||
pub(crate) fn write_datetime<W: Write>(
|
||||
mut w: W,
|
||||
start: &str,
|
||||
datetime: &Datetime,
|
||||
end: &str,
|
||||
) -> Result<(), Error> {
|
||||
write!(w, "{}", start)?;
|
||||
write!(
|
||||
w,
|
||||
"{}-{:02}-{:02} {}",
|
||||
datetime.year, datetime.month, datetime.day, datetime.dayname
|
||||
)?;
|
||||
if let (Some(hour), Some(minute)) = (datetime.hour, datetime.minute) {
|
||||
write!(w, " {:02}:{:02}", hour, minute)?;
|
||||
}
|
||||
write!(w, "{}", end)
|
||||
}
|
||||
pub use html::{HtmlEscape, HtmlExport};
|
||||
pub use traverse::{TraversalContext, Traverser};
|
||||
|
|
|
|||
|
|
@ -1,321 +0,0 @@
|
|||
use std::io::{Error, Result as IOResult, Write};
|
||||
|
||||
use crate::elements::{Clock, Element, Table, Timestamp};
|
||||
use crate::export::write_datetime;
|
||||
|
||||
pub trait OrgHandler<E: From<Error>>: Default {
|
||||
fn start<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E>;
|
||||
fn end<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DefaultOrgHandler;
|
||||
|
||||
impl OrgHandler<Error> for DefaultOrgHandler {
|
||||
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> IOResult<()> {
|
||||
match element {
|
||||
// container elements
|
||||
Element::SpecialBlock(block) => {
|
||||
writeln!(w, "#+BEGIN_{}", block.name)?;
|
||||
write_blank_lines(&mut w, block.pre_blank)?;
|
||||
}
|
||||
Element::QuoteBlock(block) => {
|
||||
writeln!(&mut w, "#+BEGIN_QUOTE")?;
|
||||
write_blank_lines(&mut w, block.pre_blank)?;
|
||||
}
|
||||
Element::CenterBlock(block) => {
|
||||
writeln!(&mut w, "#+BEGIN_CENTER")?;
|
||||
write_blank_lines(&mut w, block.pre_blank)?;
|
||||
}
|
||||
Element::VerseBlock(block) => {
|
||||
writeln!(&mut w, "#+BEGIN_VERSE")?;
|
||||
write_blank_lines(&mut w, block.pre_blank)?;
|
||||
}
|
||||
Element::Bold => write!(w, "*")?,
|
||||
Element::Document { pre_blank } => {
|
||||
write_blank_lines(w, *pre_blank)?;
|
||||
}
|
||||
Element::DynBlock(dyn_block) => {
|
||||
write!(&mut w, "#+BEGIN: {}", dyn_block.block_name)?;
|
||||
if let Some(parameters) = &dyn_block.arguments {
|
||||
write!(&mut w, " {}", parameters)?;
|
||||
}
|
||||
write_blank_lines(&mut w, dyn_block.pre_blank + 1)?;
|
||||
}
|
||||
Element::Headline { .. } => (),
|
||||
Element::List(_list) => (),
|
||||
Element::Italic => write!(w, "/")?,
|
||||
Element::ListItem(list_item) => {
|
||||
for _ in 0..list_item.indent {
|
||||
write!(&mut w, " ")?;
|
||||
}
|
||||
write!(&mut w, "{}", list_item.bullet)?;
|
||||
}
|
||||
Element::Paragraph { .. } => (),
|
||||
Element::Section => (),
|
||||
Element::Strike => write!(w, "+")?,
|
||||
Element::Underline => write!(w, "_")?,
|
||||
Element::Drawer(drawer) => {
|
||||
writeln!(&mut w, ":{}:", drawer.name)?;
|
||||
write_blank_lines(&mut w, drawer.pre_blank)?;
|
||||
}
|
||||
// non-container elements
|
||||
Element::CommentBlock(block) => {
|
||||
writeln!(&mut w, "#+BEGIN_COMMENT")?;
|
||||
write!(&mut w, "{}", block.contents)?;
|
||||
writeln!(&mut w, "#+END_COMMENT")?;
|
||||
write_blank_lines(&mut w, block.post_blank)?;
|
||||
}
|
||||
Element::ExampleBlock(block) => {
|
||||
writeln!(&mut w, "#+BEGIN_EXAMPLE")?;
|
||||
write!(&mut w, "{}", block.contents)?;
|
||||
writeln!(&mut w, "#+END_EXAMPLE")?;
|
||||
write_blank_lines(&mut w, block.post_blank)?;
|
||||
}
|
||||
Element::ExportBlock(block) => {
|
||||
writeln!(&mut w, "#+BEGIN_EXPORT {}", block.data)?;
|
||||
write!(&mut w, "{}", block.contents)?;
|
||||
writeln!(&mut w, "#+END_EXPORT")?;
|
||||
write_blank_lines(&mut w, block.post_blank)?;
|
||||
}
|
||||
Element::SourceBlock(block) => {
|
||||
writeln!(&mut w, "#+BEGIN_SRC {}", block.language)?;
|
||||
write!(&mut w, "{}", block.contents)?;
|
||||
writeln!(&mut w, "#+END_SRC")?;
|
||||
write_blank_lines(&mut w, block.post_blank)?;
|
||||
}
|
||||
Element::BabelCall(call) => {
|
||||
writeln!(&mut w, "#+CALL: {}", call.value)?;
|
||||
write_blank_lines(w, call.post_blank)?;
|
||||
}
|
||||
Element::InlineSrc(inline_src) => {
|
||||
write!(&mut w, "src_{}", inline_src.lang)?;
|
||||
if let Some(options) = &inline_src.options {
|
||||
write!(&mut w, "[{}]", options)?;
|
||||
}
|
||||
write!(&mut w, "{{{}}}", inline_src.body)?;
|
||||
}
|
||||
Element::Code { value } => write!(w, "~{}~", value)?,
|
||||
Element::FnRef(fn_ref) => {
|
||||
write!(&mut w, "[fn:{}", fn_ref.label)?;
|
||||
if let Some(definition) = &fn_ref.definition {
|
||||
write!(&mut w, ":{}", definition)?;
|
||||
}
|
||||
write!(&mut w, "]")?;
|
||||
}
|
||||
Element::InlineCall(inline_call) => {
|
||||
write!(&mut w, "call_{}", inline_call.name)?;
|
||||
if let Some(header) = &inline_call.inside_header {
|
||||
write!(&mut w, "[{}]", header)?;
|
||||
}
|
||||
write!(&mut w, "({})", inline_call.arguments)?;
|
||||
if let Some(header) = &inline_call.end_header {
|
||||
write!(&mut w, "[{}]", header)?;
|
||||
}
|
||||
}
|
||||
Element::Link(link) => {
|
||||
write!(&mut w, "[[{}]", link.path)?;
|
||||
if let Some(desc) = &link.desc {
|
||||
write!(&mut w, "[{}]", desc)?;
|
||||
}
|
||||
write!(&mut w, "]")?;
|
||||
}
|
||||
Element::Macros(_macros) => (),
|
||||
Element::RadioTarget => (),
|
||||
Element::Snippet(snippet) => write!(w, "@@{}:{}@@", snippet.name, snippet.value)?,
|
||||
Element::Target(_target) => (),
|
||||
Element::Text { value } => write!(w, "{}", value)?,
|
||||
Element::Timestamp(timestamp) => {
|
||||
write_timestamp(&mut w, ×tamp)?;
|
||||
}
|
||||
Element::Verbatim { value } => write!(w, "={}=", value)?,
|
||||
Element::FnDef(fn_def) => {
|
||||
write_blank_lines(w, fn_def.post_blank)?;
|
||||
}
|
||||
Element::Clock(clock) => {
|
||||
write!(w, "CLOCK: ")?;
|
||||
|
||||
match clock {
|
||||
Clock::Closed {
|
||||
start,
|
||||
end,
|
||||
duration,
|
||||
post_blank,
|
||||
..
|
||||
} => {
|
||||
write_datetime(&mut w, "[", &start, "]--")?;
|
||||
write_datetime(&mut w, "[", &end, "]")?;
|
||||
writeln!(&mut w, " => {}", duration)?;
|
||||
write_blank_lines(&mut w, *post_blank)?;
|
||||
}
|
||||
Clock::Running {
|
||||
start, post_blank, ..
|
||||
} => {
|
||||
write_datetime(&mut w, "[", &start, "]\n")?;
|
||||
write_blank_lines(&mut w, *post_blank)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Element::Comment(comment) => {
|
||||
write!(w, "{}", comment.value)?;
|
||||
write_blank_lines(&mut w, comment.post_blank)?;
|
||||
}
|
||||
Element::FixedWidth(fixed_width) => {
|
||||
write!(&mut w, "{}", fixed_width.value)?;
|
||||
write_blank_lines(&mut w, fixed_width.post_blank)?;
|
||||
}
|
||||
Element::Keyword(keyword) => {
|
||||
write!(&mut w, "#+{}", keyword.key)?;
|
||||
if let Some(optional) = &keyword.optional {
|
||||
write!(&mut w, "[{}]", optional)?;
|
||||
}
|
||||
writeln!(&mut w, ": {}", keyword.value)?;
|
||||
write_blank_lines(&mut w, keyword.post_blank)?;
|
||||
}
|
||||
Element::Rule(rule) => {
|
||||
writeln!(w, "-----")?;
|
||||
write_blank_lines(&mut w, rule.post_blank)?;
|
||||
}
|
||||
Element::Cookie(_cookie) => (),
|
||||
Element::Title(title) => {
|
||||
for _ in 0..title.level {
|
||||
write!(&mut w, "*")?;
|
||||
}
|
||||
if let Some(keyword) = &title.keyword {
|
||||
write!(&mut w, " {}", keyword)?;
|
||||
}
|
||||
if let Some(priority) = title.priority {
|
||||
write!(&mut w, " [#{}]", priority)?;
|
||||
}
|
||||
write!(&mut w, " ")?;
|
||||
}
|
||||
Element::Table(_) => (),
|
||||
Element::TableRow(_) => (),
|
||||
Element::TableCell(_) => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> IOResult<()> {
|
||||
match element {
|
||||
// container elements
|
||||
Element::SpecialBlock(block) => {
|
||||
writeln!(&mut w, "#+END_{}", block.name)?;
|
||||
write_blank_lines(&mut w, block.post_blank)?;
|
||||
}
|
||||
Element::QuoteBlock(block) => {
|
||||
writeln!(&mut w, "#+END_QUOTE")?;
|
||||
write_blank_lines(&mut w, block.post_blank)?;
|
||||
}
|
||||
Element::CenterBlock(block) => {
|
||||
writeln!(&mut w, "#+END_CENTER")?;
|
||||
write_blank_lines(&mut w, block.post_blank)?;
|
||||
}
|
||||
Element::VerseBlock(block) => {
|
||||
writeln!(&mut w, "#+END_VERSE")?;
|
||||
write_blank_lines(&mut w, block.post_blank)?;
|
||||
}
|
||||
Element::Bold => write!(w, "*")?,
|
||||
Element::Document { .. } => (),
|
||||
Element::DynBlock(dyn_block) => {
|
||||
writeln!(w, "#+END:")?;
|
||||
write_blank_lines(w, dyn_block.post_blank)?;
|
||||
}
|
||||
Element::Headline { .. } => (),
|
||||
Element::List(list) => {
|
||||
write_blank_lines(w, list.post_blank)?;
|
||||
}
|
||||
Element::Italic => write!(w, "/")?,
|
||||
Element::ListItem(_) => (),
|
||||
Element::Paragraph { post_blank } => {
|
||||
write_blank_lines(w, post_blank + 1)?;
|
||||
}
|
||||
Element::Section => (),
|
||||
Element::Strike => write!(w, "+")?,
|
||||
Element::Underline => write!(w, "_")?,
|
||||
Element::Drawer(drawer) => {
|
||||
writeln!(&mut w, ":END:")?;
|
||||
write_blank_lines(&mut w, drawer.post_blank)?;
|
||||
}
|
||||
Element::Title(title) => {
|
||||
if !title.tags.is_empty() {
|
||||
write!(&mut w, " :")?;
|
||||
for tag in &title.tags {
|
||||
write!(&mut w, "{}:", tag)?;
|
||||
}
|
||||
}
|
||||
writeln!(&mut w)?;
|
||||
if let Some(planning) = &title.planning {
|
||||
if let Some(scheduled) = &planning.scheduled {
|
||||
write!(&mut w, "SCHEDULED: ")?;
|
||||
write_timestamp(&mut w, &scheduled)?;
|
||||
}
|
||||
if let Some(deadline) = &planning.deadline {
|
||||
if planning.scheduled.is_some() {
|
||||
write!(&mut w, " ")?;
|
||||
}
|
||||
write!(&mut w, "DEADLINE: ")?;
|
||||
write_timestamp(&mut w, &deadline)?;
|
||||
}
|
||||
if let Some(closed) = &planning.closed {
|
||||
if planning.deadline.is_some() {
|
||||
write!(&mut w, " ")?;
|
||||
}
|
||||
write!(&mut w, "CLOSED: ")?;
|
||||
write_timestamp(&mut w, &closed)?;
|
||||
}
|
||||
writeln!(&mut w)?;
|
||||
}
|
||||
if !title.properties.is_empty() {
|
||||
writeln!(&mut w, ":PROPERTIES:")?;
|
||||
for (key, value) in title.properties.iter() {
|
||||
writeln!(&mut w, ":{}: {}", key, value)?;
|
||||
}
|
||||
writeln!(&mut w, ":END:")?;
|
||||
}
|
||||
write_blank_lines(&mut w, title.post_blank)?;
|
||||
}
|
||||
Element::Table(Table::Org { post_blank, .. }) => {
|
||||
write_blank_lines(w, *post_blank)?;
|
||||
}
|
||||
Element::Table(Table::TableEl { post_blank, .. }) => {
|
||||
write_blank_lines(w, *post_blank)?;
|
||||
}
|
||||
Element::TableRow(_) => (),
|
||||
Element::TableCell(_) => (),
|
||||
// non-container elements
|
||||
_ => debug_assert!(!element.is_container()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_blank_lines<W: Write>(mut w: W, count: usize) -> Result<(), Error> {
|
||||
for _ in 0..count {
|
||||
writeln!(w)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_timestamp<W: Write>(mut w: W, timestamp: &Timestamp) -> Result<(), Error> {
|
||||
match timestamp {
|
||||
Timestamp::Active { start, .. } => {
|
||||
write_datetime(w, "<", start, ">")?;
|
||||
}
|
||||
Timestamp::Inactive { start, .. } => {
|
||||
write_datetime(w, "[", start, "]")?;
|
||||
}
|
||||
Timestamp::ActiveRange { start, end, .. } => {
|
||||
write_datetime(&mut w, "<", start, ">--")?;
|
||||
write_datetime(&mut w, "<", end, ">")?;
|
||||
}
|
||||
Timestamp::InactiveRange { start, end, .. } => {
|
||||
write_datetime(&mut w, "[", start, "]--")?;
|
||||
write_datetime(&mut w, "[", end, "]")?;
|
||||
}
|
||||
Timestamp::Diary { value } => write!(w, "<%%({})>", value)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
249
src/export/traverse.rs
Normal file
249
src/export/traverse.rs
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
use crate::ast::*;
|
||||
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
|
||||
use rowan::{ast::AstNode, WalkEvent};
|
||||
use SyntaxKind::*;
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum TraversalControl {
|
||||
Up,
|
||||
Stop,
|
||||
Skip,
|
||||
#[default]
|
||||
Continue,
|
||||
}
|
||||
|
||||
macro_rules! take_control {
|
||||
($ctrl:expr) => {
|
||||
match $ctrl.control {
|
||||
TraversalControl::Stop => {
|
||||
$ctrl.control = TraversalControl::Stop;
|
||||
return;
|
||||
}
|
||||
TraversalControl::Up => {
|
||||
$ctrl.control = TraversalControl::Skip;
|
||||
return;
|
||||
}
|
||||
TraversalControl::Skip => {
|
||||
$ctrl.control = TraversalControl::Continue;
|
||||
return;
|
||||
}
|
||||
TraversalControl::Continue => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TraversalContext {
|
||||
control: TraversalControl,
|
||||
}
|
||||
|
||||
impl TraversalContext {
|
||||
/// Stops traversal completely
|
||||
pub fn stop(&mut self) {
|
||||
self.control = TraversalControl::Stop;
|
||||
}
|
||||
/// Skips traversal of the current node's siblings
|
||||
pub fn up(&mut self) {
|
||||
self.control = TraversalControl::Up;
|
||||
}
|
||||
/// Skips traversal of the current node's descendants
|
||||
pub fn skip(&mut self) {
|
||||
self.control = TraversalControl::Skip;
|
||||
}
|
||||
/// Continues traversal
|
||||
pub fn r#continue(&mut self) {
|
||||
self.control = TraversalControl::Continue;
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerates org syntax tree
|
||||
///
|
||||
/// Traverser enumerates org syntax tree and calls handle method on each
|
||||
/// enumerated node and token.
|
||||
///
|
||||
/// Each handle method can returns a `TraversalControl` to control the traversal.
|
||||
pub trait Traverser {
|
||||
/// Called when visiting any node
|
||||
fn node(&mut self, node: SyntaxNode, ctx: &mut TraversalContext) {
|
||||
macro_rules! traverse_children {
|
||||
($node:expr) => {{
|
||||
for child in $node.children_with_tokens() {
|
||||
match child {
|
||||
SyntaxElement::Node(node) => self.node(node, ctx),
|
||||
SyntaxElement::Token(token) => self.token(token, ctx),
|
||||
};
|
||||
take_control!(ctx);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! traverse {
|
||||
($node:ident, $method:ident) => {{
|
||||
debug_assert!($node::can_cast(node.kind()));
|
||||
let node = $node { syntax: node };
|
||||
self.$method(WalkEvent::Enter(&node), ctx);
|
||||
take_control!(ctx);
|
||||
traverse_children!(&node.syntax);
|
||||
self.$method(WalkEvent::Leave(&node), ctx);
|
||||
take_control!(ctx);
|
||||
}};
|
||||
}
|
||||
|
||||
match node.kind() {
|
||||
DOCUMENT => traverse!(Document, document),
|
||||
HEADLINE => traverse!(Headline, headline),
|
||||
SECTION => traverse!(Section, section),
|
||||
PARAGRAPH => traverse!(Paragraph, paragraph),
|
||||
BOLD => traverse!(Bold, bold),
|
||||
ITALIC => traverse!(Italic, italic),
|
||||
STRIKE => traverse!(Strike, strike),
|
||||
UNDERLINE => traverse!(Underline, underline),
|
||||
LIST => traverse!(List, list),
|
||||
LIST_ITEM => traverse!(ListItem, list_item),
|
||||
LIST_ITEM_CONTENT => traverse!(ListItemContent, list_item_content),
|
||||
LIST_ITEM_TAG => traverse!(ListItemTag, list_item_tag),
|
||||
CODE => traverse!(Code, code),
|
||||
INLINE_CALL => traverse!(InlineCall, inline_call),
|
||||
INLINE_SRC => traverse!(InlineSrc, inline_src),
|
||||
RULE => traverse!(Rule, rule),
|
||||
VERBATIM => traverse!(Verbatim, verbatim),
|
||||
SPECIAL_BLOCK => traverse!(SpecialBlock, special_block),
|
||||
QUOTE_BLOCK => traverse!(QuoteBlock, quote_block),
|
||||
CENTER_BLOCK => traverse!(CenterBlock, center_block),
|
||||
VERSE_BLOCK => traverse!(VerseBlock, verse_block),
|
||||
COMMENT_BLOCK => traverse!(CommentBlock, comment_block),
|
||||
EXAMPLE_BLOCK => traverse!(ExampleBlock, example_block),
|
||||
EXPORT_BLOCK => traverse!(ExportBlock, export_block),
|
||||
SOURCE_BLOCK => traverse!(SourceBlock, source_block),
|
||||
BABEL_CALL => traverse!(BabelCall, babel_call),
|
||||
CLOCK => traverse!(Clock, clock),
|
||||
COOKIE => traverse!(Cookie, cookie),
|
||||
RADIO_TARGET => traverse!(RadioTarget, radio_target),
|
||||
DRAWER => traverse!(Drawer, drawer),
|
||||
DYN_BLOCK => traverse!(DynBlock, dyn_block),
|
||||
FN_DEF => traverse!(FnDef, fn_def),
|
||||
FN_REF => traverse!(FnRef, fn_ref),
|
||||
MACROS => traverse!(Macros, macros),
|
||||
SNIPPET => traverse!(Snippet, snippet),
|
||||
TIMESTAMP_ACTIVE | TIMESTAMP_INACTIVE | TIMESTAMP_DIARY => {
|
||||
traverse!(Timestamp, timestamp)
|
||||
}
|
||||
TARGET => traverse!(Target, target),
|
||||
COMMENT => traverse!(Comment, comment),
|
||||
FIXED_WIDTH => traverse!(FixedWidth, fixed_width),
|
||||
HEADLINE_TITLE => traverse!(HeadlineTitle, headline_title),
|
||||
ORG_TABLE => traverse!(OrgTable, org_table),
|
||||
ORG_TABLE_RULE_ROW | ORG_TABLE_STANDARD_ROW => traverse!(OrgTableRow, org_table_row),
|
||||
ORG_TABLE_CELL => traverse!(OrgTableCell, org_table_cell),
|
||||
LINK => traverse!(Link, link),
|
||||
|
||||
BLOCK_CONTENT => traverse_children!(node),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when visiting any token
|
||||
fn token(&mut self, token: SyntaxToken, ctx: &mut TraversalContext) {
|
||||
match token.kind() {
|
||||
TEXT => self.text(token, ctx),
|
||||
_ => {}
|
||||
}
|
||||
take_control!(ctx);
|
||||
}
|
||||
|
||||
/// Called when visiting `Text` token
|
||||
fn text(&mut self, _token: SyntaxToken, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Document` node
|
||||
fn document(&mut self, _event: WalkEvent<&Document>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Headline` node
|
||||
fn headline(&mut self, _event: WalkEvent<&Headline>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Paragraph` node
|
||||
fn paragraph(&mut self, _event: WalkEvent<&Paragraph>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Section` node
|
||||
fn section(&mut self, _event: WalkEvent<&Section>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Rule` node
|
||||
fn rule(&mut self, _event: WalkEvent<&Rule>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Comment` node
|
||||
fn comment(&mut self, _event: WalkEvent<&Comment>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `InlineSrc` node
|
||||
fn inline_src(&mut self, _event: WalkEvent<&InlineSrc>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `InlineCall` node
|
||||
fn inline_call(&mut self, _event: WalkEvent<&InlineCall>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Code` node
|
||||
fn code(&mut self, _event: WalkEvent<&Code>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Bold` node
|
||||
fn bold(&mut self, _event: WalkEvent<&Bold>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Verbatim` node
|
||||
fn verbatim(&mut self, _event: WalkEvent<&Verbatim>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Italic` node
|
||||
fn italic(&mut self, _event: WalkEvent<&Italic>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Strike` node
|
||||
fn strike(&mut self, _event: WalkEvent<&Strike>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Underline` node
|
||||
fn underline(&mut self, _event: WalkEvent<&Underline>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `List` node
|
||||
fn list(&mut self, _event: WalkEvent<&List>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `ListItem` node
|
||||
fn list_item(&mut self, _event: WalkEvent<&ListItem>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `ListItemTag` node
|
||||
fn list_item_tag(&mut self, _event: WalkEvent<&ListItemTag>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `ListItemContent` node
|
||||
fn list_item_content(
|
||||
&mut self,
|
||||
_event: WalkEvent<&ListItemContent>,
|
||||
_ctx: &mut TraversalContext,
|
||||
);
|
||||
/// Called when entering or leaving `SpecialBlock` node
|
||||
fn special_block(&mut self, _event: WalkEvent<&SpecialBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `QuoteBlock` node
|
||||
fn quote_block(&mut self, _event: WalkEvent<&QuoteBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `CenterBlock` node
|
||||
fn center_block(&mut self, _event: WalkEvent<&CenterBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `VerseBlock` node
|
||||
fn verse_block(&mut self, _event: WalkEvent<&VerseBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `CommentBlock` node
|
||||
fn comment_block(&mut self, _event: WalkEvent<&CommentBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `ExampleBlock` node
|
||||
fn example_block(&mut self, _event: WalkEvent<&ExampleBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `ExportBlock` node
|
||||
fn export_block(&mut self, _event: WalkEvent<&ExportBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `SourceBlock` node
|
||||
fn source_block(&mut self, _event: WalkEvent<&SourceBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `BabelCall` node
|
||||
fn babel_call(&mut self, _event: WalkEvent<&BabelCall>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Clock` node
|
||||
fn clock(&mut self, _event: WalkEvent<&Clock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Cookie` node
|
||||
fn cookie(&mut self, _event: WalkEvent<&Cookie>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `RadioTarget` node
|
||||
fn radio_target(&mut self, _event: WalkEvent<&RadioTarget>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Drawer` node
|
||||
fn drawer(&mut self, _event: WalkEvent<&Drawer>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `DynBlock` node
|
||||
fn dyn_block(&mut self, _event: WalkEvent<&DynBlock>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `FnDef` node
|
||||
fn fn_def(&mut self, _event: WalkEvent<&FnDef>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `FnRef` node
|
||||
fn fn_ref(&mut self, _event: WalkEvent<&FnRef>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Macros` node
|
||||
fn macros(&mut self, _event: WalkEvent<&Macros>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Snippet` node
|
||||
fn snippet(&mut self, _event: WalkEvent<&Snippet>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Timestamp` node
|
||||
fn timestamp(&mut self, _event: WalkEvent<&Timestamp>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Target` node
|
||||
fn target(&mut self, _event: WalkEvent<&Target>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `FixedWidth` node
|
||||
fn fixed_width(&mut self, _event: WalkEvent<&FixedWidth>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `HeadlineTitle` node
|
||||
fn headline_title(&mut self, _event: WalkEvent<&HeadlineTitle>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `OrgTable` node
|
||||
fn org_table(&mut self, _event: WalkEvent<&OrgTable>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `OrgTableRow` node
|
||||
fn org_table_row(&mut self, _event: WalkEvent<&OrgTableRow>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `OrgTableCell` node
|
||||
fn org_table_cell(&mut self, _event: WalkEvent<&OrgTableCell>, _ctx: &mut TraversalContext);
|
||||
/// Called when entering or leaving `Link` node
|
||||
fn link(&mut self, _event: WalkEvent<&Link>, _ctx: &mut TraversalContext);
|
||||
}
|
||||
1219
src/headline.rs
1219
src/headline.rs
File diff suppressed because it is too large
Load diff
249
src/lib.rs
249
src/lib.rs
|
|
@ -1,245 +1,18 @@
|
|||
//! A Rust library for parsing orgmode files.
|
||||
//!
|
||||
//! [Live demo](https://orgize.herokuapp.com/)
|
||||
//!
|
||||
//! # Parse
|
||||
//!
|
||||
//! To parse a orgmode string, simply invoking the [`Org::parse`] function:
|
||||
//!
|
||||
//! [`Org::parse`]: struct.Org.html#method.parse
|
||||
//!
|
||||
//! ```rust
|
||||
//! use orgize::Org;
|
||||
//!
|
||||
//! Org::parse("* DONE Title :tag:");
|
||||
//! ```
|
||||
//!
|
||||
//! or [`Org::parse_custom`]:
|
||||
//!
|
||||
//! [`Org::parse_custom`]: struct.Org.html#method.parse_custom
|
||||
//!
|
||||
//! ```rust
|
||||
//! use orgize::{Org, ParseConfig};
|
||||
//!
|
||||
//! Org::parse_custom(
|
||||
//! "* TASK Title 1",
|
||||
//! &ParseConfig {
|
||||
//! // custom todo keywords
|
||||
//! todo_keywords: (vec!["TASK".to_string()], vec![]),
|
||||
//! ..Default::default()
|
||||
//! },
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! # Iter
|
||||
//!
|
||||
//! [`Org::iter`] function will returns an iterator of [`Event`]s, which is
|
||||
//! a simple wrapper of [`Element`].
|
||||
//!
|
||||
//! [`Org::iter`]: struct.Org.html#method.iter
|
||||
//! [`Event`]: enum.Event.html
|
||||
//! [`Element`]: elements/enum.Element.html
|
||||
//!
|
||||
//! ```rust
|
||||
//! use orgize::Org;
|
||||
//!
|
||||
//! for event in Org::parse("* DONE Title :tag:").iter() {
|
||||
//! // handling the event
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **Note**: whether an element is container or not, it will appears twice in one loop.
|
||||
//! One as [`Event::Start(element)`], one as [`Event::End(element)`].
|
||||
//!
|
||||
//! [`Event::Start(element)`]: enum.Event.html#variant.Start
|
||||
//! [`Event::End(element)`]: enum.Event.html#variant.End
|
||||
//!
|
||||
//! # Render html
|
||||
//!
|
||||
//! You can call the [`Org::write_html`] function to generate html directly, which
|
||||
//! uses the [`DefaultHtmlHandler`] internally:
|
||||
//!
|
||||
//! [`Org::write_html`]: struct.Org.html#method.write_html
|
||||
//! [`DefaultHtmlHandler`]: export/struct.DefaultHtmlHandler.html
|
||||
//!
|
||||
//! ```rust
|
||||
//! use orgize::Org;
|
||||
//!
|
||||
//! let mut writer = Vec::new();
|
||||
//! Org::parse("* title\n*section*").write_html(&mut writer).unwrap();
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! String::from_utf8(writer).unwrap(),
|
||||
//! "<main><h1>title</h1><section><p><b>section</b></p></section></main>"
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! # Render html with custom `HtmlHandler`
|
||||
//!
|
||||
//! To customize html rendering, simply implementing [`HtmlHandler`] trait and passing
|
||||
//! it to the [`Org::write_html_custom`] function.
|
||||
//!
|
||||
//! [`HtmlHandler`]: export/trait.HtmlHandler.html
|
||||
//! [`Org::write_html_custom`]: struct.Org.html#method.write_html_custom
|
||||
//!
|
||||
//! The following code demonstrates how to add a id for every headline and return
|
||||
//! own error type while rendering.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::convert::From;
|
||||
//! use std::io::{Error as IOError, Write};
|
||||
//! use std::string::FromUtf8Error;
|
||||
//!
|
||||
//! use orgize::export::{DefaultHtmlHandler, HtmlHandler};
|
||||
//! use orgize::{Element, Org};
|
||||
//! use slugify::slugify;
|
||||
//!
|
||||
//! #[derive(Debug)]
|
||||
//! enum MyError {
|
||||
//! IO(IOError),
|
||||
//! Heading,
|
||||
//! Utf8(FromUtf8Error),
|
||||
//! }
|
||||
//!
|
||||
//! // From<std::io::Error> trait is required for custom error type
|
||||
//! impl From<IOError> for MyError {
|
||||
//! fn from(err: IOError) -> Self {
|
||||
//! MyError::IO(err)
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl From<FromUtf8Error> for MyError {
|
||||
//! fn from(err: FromUtf8Error) -> Self {
|
||||
//! MyError::Utf8(err)
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Default)]
|
||||
//! struct MyHtmlHandler(DefaultHtmlHandler);
|
||||
//!
|
||||
//! impl HtmlHandler<MyError> for MyHtmlHandler {
|
||||
//! fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
|
||||
//! if let Element::Title(title) = element {
|
||||
//! if title.level > 6 {
|
||||
//! return Err(MyError::Heading);
|
||||
//! } else {
|
||||
//! write!(
|
||||
//! w,
|
||||
//! "<h{0}><a id=\"{1}\" href=\"#{1}\">",
|
||||
//! title.level,
|
||||
//! slugify!(&title.raw),
|
||||
//! )?;
|
||||
//! }
|
||||
//! } else {
|
||||
//! // fallthrough to default handler
|
||||
//! self.0.start(w, element)?;
|
||||
//! }
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
|
||||
//! if let Element::Title(title) = element {
|
||||
//! write!(w, "</a></h{}>", title.level)?;
|
||||
//! } else {
|
||||
//! self.0.end(w, element)?;
|
||||
//! }
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn main() -> Result<(), MyError> {
|
||||
//! let mut writer = Vec::new();
|
||||
//! let mut handler = MyHtmlHandler::default();
|
||||
//! Org::parse("* title\n*section*").write_html_custom(&mut writer, &mut handler)?;
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! String::from_utf8(writer)?,
|
||||
//! "<main><h1><a id=\"title\" href=\"#title\">title</a></h1>\
|
||||
//! <section><p><b>section</b></p></section></main>"
|
||||
//! );
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! **Note**: as I mentioned above, each element will appears two times while iterating.
|
||||
//! And handler will silently ignores all end events from non-container elements.
|
||||
//!
|
||||
//! So if you want to change how a non-container element renders, just redefine the `start`
|
||||
//! function and leave the `end` function unchanged.
|
||||
//!
|
||||
//! # Serde
|
||||
//!
|
||||
//! `Org` struct have already implemented serde's `Serialize` trait. It means you can
|
||||
//! serialize it into any format supported by serde, such as json:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use orgize::Org;
|
||||
//! use serde_json::{json, to_string};
|
||||
//!
|
||||
//! let org = Org::parse("I 'm *bold*.");
|
||||
//! #[cfg(feature = "ser")]
|
||||
//! println!("{}", to_string(&org).unwrap());
|
||||
//!
|
||||
//! // {
|
||||
//! // "type": "document",
|
||||
//! // "children": [{
|
||||
//! // "type": "section",
|
||||
//! // "children": [{
|
||||
//! // "type": "paragraph",
|
||||
//! // "children":[{
|
||||
//! // "type": "text",
|
||||
//! // "value":"I 'm "
|
||||
//! // }, {
|
||||
//! // "type": "bold",
|
||||
//! // "children":[{
|
||||
//! // "type": "text",
|
||||
//! // "value": "bold"
|
||||
//! // }]
|
||||
//! // }, {
|
||||
//! // "type":"text",
|
||||
//! // "value":"."
|
||||
//! // }]
|
||||
//! // }]
|
||||
//! // }]
|
||||
//! // }
|
||||
//! ```
|
||||
//!
|
||||
//! # Features
|
||||
//!
|
||||
//! By now, orgize provides three features:
|
||||
//!
|
||||
//! + `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default.
|
||||
//!
|
||||
//! + `chrono`: adds the ability to convert `Datetime` into `chrono` structs, disabled by default.
|
||||
//!
|
||||
//! + `syntect`: provides [`SyntectHtmlHandler`] for highlighting code block, disabled by default.
|
||||
//!
|
||||
//! [`SyntectHtmlHandler`]: export/struct.SyntectHtmlHandler.html
|
||||
//!
|
||||
//! # License
|
||||
//!
|
||||
//! MIT
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod ast;
|
||||
mod config;
|
||||
pub mod elements;
|
||||
pub mod export;
|
||||
mod headline;
|
||||
mod org;
|
||||
mod parse;
|
||||
mod parsers;
|
||||
mod validate;
|
||||
mod syntax;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
// Re-export of the indextree crate.
|
||||
pub use indextree;
|
||||
#[cfg(feature = "syntect")]
|
||||
pub use syntect;
|
||||
// Re-export of the rowan crate.
|
||||
pub use rowan;
|
||||
|
||||
pub use config::ParseConfig;
|
||||
pub use elements::Element;
|
||||
pub use headline::{Document, Headline};
|
||||
pub use org::{Event, Org};
|
||||
pub use validate::ValidationError;
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm;
|
||||
pub use org::Org;
|
||||
pub use syntax::{
|
||||
SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxNodeChildren, SyntaxToken,
|
||||
};
|
||||
|
|
|
|||
212
src/org.rs
212
src/org.rs
|
|
@ -1,193 +1,65 @@
|
|||
use indextree::{Arena, NodeEdge, NodeId};
|
||||
use std::io::{Error, Write};
|
||||
use std::ops::{Index, IndexMut};
|
||||
use rowan::ast::AstNode;
|
||||
use rowan::GreenNode;
|
||||
|
||||
use crate::{
|
||||
config::{ParseConfig, DEFAULT_CONFIG},
|
||||
elements::{Element, Keyword},
|
||||
export::{DefaultHtmlHandler, DefaultOrgHandler, HtmlHandler, OrgHandler},
|
||||
parsers::{blank_lines_count, parse_container, Container, OwnedArena},
|
||||
};
|
||||
|
||||
pub struct Org<'a> {
|
||||
pub(crate) arena: Arena<Element<'a>>,
|
||||
pub(crate) root: NodeId,
|
||||
}
|
||||
use crate::ast::Document;
|
||||
use crate::config::ParseConfig;
|
||||
use crate::export::{HtmlExport, TraversalContext, Traverser};
|
||||
use crate::syntax::{OrgLanguage, SyntaxNode};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Event<'a, 'b> {
|
||||
Start(&'b Element<'a>),
|
||||
End(&'b Element<'a>),
|
||||
pub struct Org {
|
||||
pub(crate) green: GreenNode,
|
||||
pub(crate) config: ParseConfig,
|
||||
}
|
||||
|
||||
impl<'a> Org<'a> {
|
||||
/// Creates a new empty `Org` struct.
|
||||
pub fn new() -> Org<'static> {
|
||||
let mut arena = Arena::new();
|
||||
let root = arena.new_node(Element::Document { pre_blank: 0 });
|
||||
Org { arena, root }
|
||||
impl Org {
|
||||
/// Parse input string to Org element tree using default parse config
|
||||
pub fn parse(input: impl AsRef<str>) -> Org {
|
||||
ParseConfig::default().parse(input)
|
||||
}
|
||||
|
||||
/// Parses string `text` into `Org` struct.
|
||||
pub fn parse(text: &'a str) -> Org<'a> {
|
||||
Org::parse_custom(text, &DEFAULT_CONFIG)
|
||||
pub fn green(&self) -> &GreenNode {
|
||||
&self.green
|
||||
}
|
||||
|
||||
/// Likes `parse`, but accepts `String`.
|
||||
pub fn parse_string(text: String) -> Org<'static> {
|
||||
Org::parse_string_custom(text, &DEFAULT_CONFIG)
|
||||
pub fn config(&self) -> &ParseConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Parses string `text` into `Org` struct with custom `ParseConfig`.
|
||||
pub fn parse_custom(text: &'a str, config: &ParseConfig) -> Org<'a> {
|
||||
let mut arena = Arena::new();
|
||||
let (text, pre_blank) = blank_lines_count(text);
|
||||
let root = arena.new_node(Element::Document { pre_blank });
|
||||
let mut org = Org { arena, root };
|
||||
|
||||
parse_container(
|
||||
&mut org.arena,
|
||||
Container::Document {
|
||||
content: text,
|
||||
node: org.root,
|
||||
},
|
||||
config,
|
||||
);
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
org
|
||||
/// Returns the document
|
||||
pub fn document(&self) -> Document {
|
||||
Document {
|
||||
syntax: SyntaxNode::new_root(self.green.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Likes `parse_custom`, but accepts `String`.
|
||||
pub fn parse_string_custom(text: String, config: &ParseConfig) -> Org<'static> {
|
||||
let mut arena = Arena::new();
|
||||
let (text, pre_blank) = blank_lines_count(&text);
|
||||
let root = arena.new_node(Element::Document { pre_blank });
|
||||
let mut org = Org { arena, root };
|
||||
|
||||
parse_container(
|
||||
&mut OwnedArena::new(&mut org.arena),
|
||||
Container::Document {
|
||||
content: text,
|
||||
node: org.root,
|
||||
},
|
||||
config,
|
||||
);
|
||||
|
||||
org.debug_validate();
|
||||
|
||||
org
|
||||
/// Returns org-mode string
|
||||
pub fn to_org(&self) -> String {
|
||||
self.green.to_string()
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlay arena.
|
||||
pub fn arena(&self) -> &Arena<Element<'a>> {
|
||||
&self.arena
|
||||
/// Convert org element tree to html-format using default html handler
|
||||
pub fn to_html(&self) -> String {
|
||||
let mut handler = HtmlExport::default();
|
||||
self.traverse(&mut handler);
|
||||
handler.finish()
|
||||
}
|
||||
|
||||
/// Returns a mutual reference to the underlay arena.
|
||||
pub fn arena_mut(&mut self) -> &mut Arena<Element<'a>> {
|
||||
&mut self.arena
|
||||
/// Walk through org element tree using given traverser
|
||||
pub fn traverse<T: Traverser>(&self, h: &mut T) {
|
||||
let mut ctx = TraversalContext::default();
|
||||
h.node(SyntaxNode::new_root(self.green.clone()), &mut ctx);
|
||||
}
|
||||
|
||||
/// Returns an iterator of `Event`s.
|
||||
pub fn iter<'b>(&'b self) -> impl Iterator<Item = Event<'a, 'b>> + 'b {
|
||||
self.root.traverse(&self.arena).map(move |edge| match edge {
|
||||
NodeEdge::Start(node) => Event::Start(&self[node]),
|
||||
NodeEdge::End(node) => Event::End(&self[node]),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator of `Keyword`s.
|
||||
pub fn keywords(&self) -> impl Iterator<Item = &Keyword<'_>> {
|
||||
self.root
|
||||
.descendants(&self.arena)
|
||||
.skip(1)
|
||||
.filter_map(move |node| match &self[node] {
|
||||
Element::Keyword(kw) => Some(kw),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes an `Org` struct as html format.
|
||||
pub fn write_html<W>(&self, writer: W) -> Result<(), Error>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
self.write_html_custom(writer, &mut DefaultHtmlHandler)
|
||||
}
|
||||
|
||||
/// Writes an `Org` struct as html format with custom `HtmlHandler`.
|
||||
pub fn write_html_custom<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E>
|
||||
where
|
||||
W: Write,
|
||||
E: From<Error>,
|
||||
H: HtmlHandler<E>,
|
||||
{
|
||||
for event in self.iter() {
|
||||
match event {
|
||||
Event::Start(element) => handler.start(&mut writer, element)?,
|
||||
Event::End(element) => handler.end(&mut writer, element)?,
|
||||
/// Returns the first node in org element tree in depth first order
|
||||
pub fn first_node<N: AstNode<Language = OrgLanguage>>(&self) -> Option<N> {
|
||||
fn find<N: AstNode<Language = OrgLanguage>>(node: SyntaxNode) -> Option<N> {
|
||||
if N::can_cast(node.kind()) {
|
||||
N::cast(node)
|
||||
} else {
|
||||
node.children().find_map(find)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes an `Org` struct as org format.
|
||||
pub fn write_org<W>(&self, writer: W) -> Result<(), Error>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
self.write_org_custom(writer, &mut DefaultOrgHandler)
|
||||
}
|
||||
|
||||
/// Writes an `Org` struct as org format with custom `OrgHandler`.
|
||||
pub fn write_org_custom<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E>
|
||||
where
|
||||
W: Write,
|
||||
E: From<Error>,
|
||||
H: OrgHandler<E>,
|
||||
{
|
||||
for event in self.iter() {
|
||||
match event {
|
||||
Event::Start(element) => handler.start(&mut writer, element)?,
|
||||
Event::End(element) => handler.end(&mut writer, element)?,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Org<'static> {
|
||||
fn default() -> Self {
|
||||
Org::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Index<NodeId> for Org<'a> {
|
||||
type Output = Element<'a>;
|
||||
|
||||
fn index(&self, node_id: NodeId) -> &Self::Output {
|
||||
self.arena[node_id].get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IndexMut<NodeId> for Org<'a> {
|
||||
fn index_mut(&mut self, node_id: NodeId) -> &mut Self::Output {
|
||||
self.arena[node_id].get_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ser")]
|
||||
use serde::{ser::Serializer, Serialize};
|
||||
|
||||
#[cfg(feature = "ser")]
|
||||
impl Serialize for Org<'_> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
use serde_indextree::Node;
|
||||
|
||||
serializer.serialize_newtype_struct("Org", &Node::new(self.root, &self.arena))
|
||||
find(SyntaxNode::new_root(self.green.clone()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
//! Parsers combinators
|
||||
|
||||
use memchr::memchr;
|
||||
use nom::{
|
||||
bytes::complete::take_while1,
|
||||
combinator::verify,
|
||||
error::{make_error, ErrorKind},
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
// read until the first line_ending, if line_ending is not present, return the input directly
|
||||
pub fn line(input: &str) -> IResult<&str, &str, ()> {
|
||||
if let Some(i) = memchr(b'\n', input.as_bytes()) {
|
||||
if i > 0 && input.as_bytes()[i - 1] == b'\r' {
|
||||
Ok((&input[i + 1..], &input[0..i - 1]))
|
||||
} else {
|
||||
Ok((&input[i + 1..], &input[0..i]))
|
||||
}
|
||||
} else {
|
||||
Ok(("", input))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lines_till<F>(predicate: F) -> impl Fn(&str) -> IResult<&str, &str, ()>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
move |i| {
|
||||
let mut input = i;
|
||||
|
||||
loop {
|
||||
// TODO: better error kind
|
||||
if input.is_empty() {
|
||||
return Err(Err::Error(make_error(input, ErrorKind::Many0)));
|
||||
}
|
||||
|
||||
let (input_, line_) = line(input)?;
|
||||
|
||||
debug_assert_ne!(input, input_);
|
||||
|
||||
if predicate(line_) {
|
||||
let offset = i.len() - input.len();
|
||||
return Ok((input_, &i[0..offset]));
|
||||
}
|
||||
|
||||
input = input_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lines_while<F>(predicate: F) -> impl Fn(&str) -> IResult<&str, &str, ()>
|
||||
where
|
||||
F: Fn(&str) -> bool,
|
||||
{
|
||||
move |i| {
|
||||
let mut input = i;
|
||||
|
||||
loop {
|
||||
// unlike lines_till, line_while won't return error
|
||||
if input.is_empty() {
|
||||
return Ok(("", i));
|
||||
}
|
||||
|
||||
let (input_, line_) = line(input)?;
|
||||
|
||||
debug_assert_ne!(input, input_);
|
||||
|
||||
if !predicate(line_) {
|
||||
let offset = i.len() - input.len();
|
||||
return Ok((input, &i[0..offset]));
|
||||
}
|
||||
|
||||
input = input_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lines_while() {
|
||||
assert_eq!(lines_while(|line| line == "foo")("foo"), Ok(("", "foo")));
|
||||
assert_eq!(lines_while(|line| line == "foo")("bar"), Ok(("bar", "")));
|
||||
assert_eq!(
|
||||
lines_while(|line| line == "foo")("foo\n\n"),
|
||||
Ok(("\n", "foo\n"))
|
||||
);
|
||||
assert_eq!(
|
||||
lines_while(|line| line.trim().is_empty())("\n\n\n"),
|
||||
Ok(("", "\n\n\n"))
|
||||
);
|
||||
}
|
||||
|
||||
pub fn eol(input: &str) -> IResult<&str, &str, ()> {
|
||||
verify(line, |s: &str| {
|
||||
s.as_bytes().iter().all(u8::is_ascii_whitespace)
|
||||
})(input)
|
||||
}
|
||||
|
||||
pub fn one_word(input: &str) -> IResult<&str, &str, ()> {
|
||||
take_while1(|c: char| !c.is_ascii_whitespace())(input)
|
||||
}
|
||||
|
||||
pub fn blank_lines_count(input: &str) -> IResult<&str, usize, ()> {
|
||||
let mut count = 0;
|
||||
let mut input = input;
|
||||
|
||||
loop {
|
||||
if input.is_empty() {
|
||||
return Ok(("", count));
|
||||
}
|
||||
|
||||
let (input_, line_) = line(input)?;
|
||||
|
||||
debug_assert_ne!(input, input_);
|
||||
|
||||
if !line_.chars().all(char::is_whitespace) {
|
||||
return Ok((input, count));
|
||||
}
|
||||
|
||||
count += 1;
|
||||
|
||||
input = input_;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blank_lines_count() {
|
||||
assert_eq!(blank_lines_count("foo"), Ok(("foo", 0)));
|
||||
assert_eq!(blank_lines_count(" foo"), Ok((" foo", 0)));
|
||||
assert_eq!(blank_lines_count(" \t\nfoo\n"), Ok(("foo\n", 1)));
|
||||
assert_eq!(blank_lines_count("\n \r\n\nfoo\n"), Ok(("foo\n", 3)));
|
||||
assert_eq!(
|
||||
blank_lines_count("\r\n \n \r\n foo\n"),
|
||||
Ok((" foo\n", 3))
|
||||
);
|
||||
assert_eq!(blank_lines_count("\r\n \n \r\n \n"), Ok(("", 4)));
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
pub mod combinators;
|
||||
657
src/parsers.rs
657
src/parsers.rs
|
|
@ -1,657 +0,0 @@
|
|||
use std::iter::once;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use indextree::{Arena, NodeId};
|
||||
use jetscii::{bytes, BytesConst};
|
||||
use memchr::{memchr, memchr_iter};
|
||||
use nom::bytes::complete::take_while1;
|
||||
|
||||
use crate::config::ParseConfig;
|
||||
use crate::elements::{
|
||||
block::RawBlock, emphasis::Emphasis, keyword::RawKeyword, radio_target::parse_radio_target,
|
||||
Clock, Comment, Cookie, Drawer, DynBlock, Element, FixedWidth, FnDef, FnRef, InlineCall,
|
||||
InlineSrc, Link, List, ListItem, Macros, Rule, Snippet, Table, TableCell, TableRow, Target,
|
||||
Timestamp, Title,
|
||||
};
|
||||
use crate::parse::combinators::lines_while;
|
||||
|
||||
pub trait ElementArena<'a> {
|
||||
fn append<T>(&mut self, element: T, parent: NodeId) -> NodeId
|
||||
where
|
||||
T: Into<Element<'a>>;
|
||||
fn insert_before_last_child<T>(&mut self, element: T, parent: NodeId) -> NodeId
|
||||
where
|
||||
T: Into<Element<'a>>;
|
||||
fn set<T>(&mut self, node: NodeId, element: T)
|
||||
where
|
||||
T: Into<Element<'a>>;
|
||||
}
|
||||
|
||||
pub type BorrowedArena<'a> = Arena<Element<'a>>;
|
||||
|
||||
impl<'a> ElementArena<'a> for BorrowedArena<'a> {
|
||||
fn append<T>(&mut self, element: T, parent: NodeId) -> NodeId
|
||||
where
|
||||
T: Into<Element<'a>>,
|
||||
{
|
||||
let node = self.new_node(element.into());
|
||||
parent.append(node, self);
|
||||
node
|
||||
}
|
||||
|
||||
fn insert_before_last_child<T>(&mut self, element: T, parent: NodeId) -> NodeId
|
||||
where
|
||||
T: Into<Element<'a>>,
|
||||
{
|
||||
if let Some(child) = self[parent].last_child() {
|
||||
let node = self.new_node(element.into());
|
||||
child.insert_before(node, self);
|
||||
node
|
||||
} else {
|
||||
self.append(element, parent)
|
||||
}
|
||||
}
|
||||
|
||||
fn set<T>(&mut self, node: NodeId, element: T)
|
||||
where
|
||||
T: Into<Element<'a>>,
|
||||
{
|
||||
*self[node].get_mut() = element.into();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OwnedArena<'a, 'b, 'c> {
|
||||
arena: &'b mut Arena<Element<'c>>,
|
||||
phantom: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl<'a, 'b, 'c> OwnedArena<'a, 'b, 'c> {
|
||||
pub fn new(arena: &'b mut Arena<Element<'c>>) -> OwnedArena<'a, 'b, 'c> {
|
||||
OwnedArena {
|
||||
arena,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ElementArena<'a> for OwnedArena<'a, '_, '_> {
|
||||
fn append<T>(&mut self, element: T, parent: NodeId) -> NodeId
|
||||
where
|
||||
T: Into<Element<'a>>,
|
||||
{
|
||||
self.arena.append(element.into().into_owned(), parent)
|
||||
}
|
||||
|
||||
fn insert_before_last_child<T>(&mut self, element: T, parent: NodeId) -> NodeId
|
||||
where
|
||||
T: Into<Element<'a>>,
|
||||
{
|
||||
self.arena
|
||||
.insert_before_last_child(element.into().into_owned(), parent)
|
||||
}
|
||||
|
||||
fn set<T>(&mut self, node: NodeId, element: T)
|
||||
where
|
||||
T: Into<Element<'a>>,
|
||||
{
|
||||
self.arena.set(node, element.into().into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Container<'a> {
|
||||
// Block, List Item
|
||||
Block { content: &'a str, node: NodeId },
|
||||
// Paragraph, Inline Markup
|
||||
Inline { content: &'a str, node: NodeId },
|
||||
// Headline
|
||||
Headline { content: &'a str, node: NodeId },
|
||||
// Document
|
||||
Document { content: &'a str, node: NodeId },
|
||||
}
|
||||
|
||||
pub fn parse_container<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
container: Container<'a>,
|
||||
config: &ParseConfig,
|
||||
) {
|
||||
let containers = &mut vec![container];
|
||||
|
||||
while let Some(container) = containers.pop() {
|
||||
match container {
|
||||
Container::Document { content, node } => {
|
||||
parse_section_and_headlines(arena, content, node, containers);
|
||||
}
|
||||
Container::Headline { content, node } => {
|
||||
parse_headline_content(arena, content, node, containers, config);
|
||||
}
|
||||
Container::Block { content, node } => {
|
||||
parse_blocks(arena, content, node, containers);
|
||||
}
|
||||
Container::Inline { content, node } => {
|
||||
parse_inlines(arena, content, node, containers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_headline_content<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
content: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
config: &ParseConfig,
|
||||
) {
|
||||
let (tail, (title, content)) = Title::parse(content, config).unwrap();
|
||||
let node = arena.append(title, parent);
|
||||
containers.push(Container::Inline { content, node });
|
||||
parse_section_and_headlines(arena, tail, parent, containers);
|
||||
}
|
||||
|
||||
pub fn parse_section_and_headlines<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
content: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) {
|
||||
let content = blank_lines_count(content).0;
|
||||
|
||||
if content.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut last_end = 0;
|
||||
for i in memchr_iter(b'\n', content.as_bytes()).chain(once(content.len())) {
|
||||
if let Some((mut tail, (headline_content, level))) = parse_headline(&content[last_end..]) {
|
||||
if last_end != 0 {
|
||||
let node = arena.append(Element::Section, parent);
|
||||
let content = &content[0..last_end];
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
|
||||
let node = arena.append(Element::Headline { level }, parent);
|
||||
containers.push(Container::Headline {
|
||||
content: headline_content,
|
||||
node,
|
||||
});
|
||||
|
||||
while let Some((new_tail, (content, level))) = parse_headline(tail) {
|
||||
debug_assert_ne!(tail, new_tail);
|
||||
let node = arena.append(Element::Headline { level }, parent);
|
||||
containers.push(Container::Headline { content, node });
|
||||
tail = new_tail;
|
||||
}
|
||||
return;
|
||||
}
|
||||
last_end = i + 1;
|
||||
}
|
||||
|
||||
let node = arena.append(Element::Section, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
|
||||
pub fn parse_blocks<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
content: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) {
|
||||
let mut tail = blank_lines_count(content).0;
|
||||
|
||||
if let Some(new_tail) = parse_block(content, arena, parent, containers) {
|
||||
tail = blank_lines_count(new_tail).0;
|
||||
}
|
||||
|
||||
let mut text = tail;
|
||||
let mut pos = 0;
|
||||
|
||||
while !tail.is_empty() {
|
||||
let i = memchr(b'\n', tail.as_bytes())
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or_else(|| tail.len());
|
||||
if tail.as_bytes()[0..i].iter().all(u8::is_ascii_whitespace) {
|
||||
let (tail_, blank) = blank_lines_count(&tail[i..]);
|
||||
debug_assert_ne!(tail, tail_);
|
||||
tail = tail_;
|
||||
|
||||
let node = arena.append(
|
||||
Element::Paragraph {
|
||||
// including the current line (&tail[0..i])
|
||||
post_blank: blank + 1,
|
||||
},
|
||||
parent,
|
||||
);
|
||||
|
||||
containers.push(Container::Inline {
|
||||
content: &text[0..pos].trim_end(),
|
||||
node,
|
||||
});
|
||||
|
||||
pos = 0;
|
||||
text = tail;
|
||||
} else if let Some(new_tail) = parse_block(tail, arena, parent, containers) {
|
||||
if pos != 0 {
|
||||
let node =
|
||||
arena.insert_before_last_child(Element::Paragraph { post_blank: 0 }, parent);
|
||||
|
||||
containers.push(Container::Inline {
|
||||
content: &text[0..pos].trim_end(),
|
||||
node,
|
||||
});
|
||||
|
||||
pos = 0;
|
||||
}
|
||||
debug_assert_ne!(tail, blank_lines_count(new_tail).0);
|
||||
tail = blank_lines_count(new_tail).0;
|
||||
text = tail;
|
||||
} else {
|
||||
debug_assert_ne!(tail, &tail[i..]);
|
||||
tail = &tail[i..];
|
||||
pos += i;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
let node = arena.append(Element::Paragraph { post_blank: 0 }, parent);
|
||||
|
||||
containers.push(Container::Inline {
|
||||
content: &text[0..pos].trim_end(),
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_block<'a, T: ElementArena<'a>>(
|
||||
contents: &'a str,
|
||||
arena: &mut T,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) -> Option<&'a str> {
|
||||
match contents
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.find(|c| !c.is_ascii_whitespace())?
|
||||
{
|
||||
b'[' => {
|
||||
let (tail, (fn_def, content)) = FnDef::parse(contents)?;
|
||||
let node = arena.append(fn_def, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
Some(tail)
|
||||
}
|
||||
b'0'..=b'9' | b'*' => {
|
||||
let tail = parse_list(arena, contents, parent, containers)?;
|
||||
Some(tail)
|
||||
}
|
||||
b'C' => {
|
||||
let (tail, clock) = Clock::parse(contents)?;
|
||||
arena.append(clock, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'\'' => {
|
||||
// TODO: LaTeX environment
|
||||
None
|
||||
}
|
||||
b'-' => {
|
||||
if let Some((tail, rule)) = Rule::parse(contents) {
|
||||
arena.append(rule, parent);
|
||||
Some(tail)
|
||||
} else {
|
||||
let tail = parse_list(arena, contents, parent, containers)?;
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
b':' => {
|
||||
if let Some((tail, (drawer, content))) = Drawer::parse(contents) {
|
||||
let node = arena.append(drawer, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
Some(tail)
|
||||
} else {
|
||||
let (tail, fixed_width) = FixedWidth::parse(contents)?;
|
||||
arena.append(fixed_width, parent);
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
b'|' => {
|
||||
let tail = parse_org_table(arena, contents, containers, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'+' => {
|
||||
if let Some((tail, table)) = Table::parse_table_el(contents) {
|
||||
arena.append(table, parent);
|
||||
Some(tail)
|
||||
} else {
|
||||
let tail = parse_list(arena, contents, parent, containers)?;
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
b'#' => {
|
||||
if let Some((tail, block)) = RawBlock::parse(contents) {
|
||||
let (element, content) = block.into_element();
|
||||
// avoid use after free
|
||||
let is_block_container = match element {
|
||||
Element::CenterBlock(_)
|
||||
| Element::QuoteBlock(_)
|
||||
| Element::VerseBlock(_)
|
||||
| Element::SpecialBlock(_) => true,
|
||||
_ => false,
|
||||
};
|
||||
let node = arena.append(element, parent);
|
||||
if is_block_container {
|
||||
containers.push(Container::Block { content, node });
|
||||
}
|
||||
Some(tail)
|
||||
} else if let Some((tail, (dyn_block, content))) = DynBlock::parse(contents) {
|
||||
let node = arena.append(dyn_block, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
Some(tail)
|
||||
} else if let Some((tail, keyword)) = RawKeyword::parse(contents) {
|
||||
arena.append(keyword.into_element(), parent);
|
||||
Some(tail)
|
||||
} else {
|
||||
let (tail, comment) = Comment::parse(contents)?;
|
||||
arena.append(comment, parent);
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
struct InlinePositions<'a> {
|
||||
bytes: &'a [u8],
|
||||
pos: usize,
|
||||
next: Option<usize>,
|
||||
}
|
||||
|
||||
impl InlinePositions<'_> {
|
||||
fn new(bytes: &[u8]) -> InlinePositions {
|
||||
InlinePositions {
|
||||
bytes,
|
||||
pos: 0,
|
||||
next: Some(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for InlinePositions<'_> {
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
lazy_static::lazy_static! {
|
||||
static ref PRE_BYTES: BytesConst =
|
||||
bytes!(b'@', b'<', b'[', b' ', b'(', b'{', b'\'', b'"', b'\n');
|
||||
}
|
||||
|
||||
self.next.take().or_else(|| {
|
||||
PRE_BYTES.find(&self.bytes[self.pos..]).map(|i| {
|
||||
self.pos += i + 1;
|
||||
|
||||
match self.bytes[self.pos - 1] {
|
||||
b'{' => {
|
||||
self.next = Some(self.pos);
|
||||
self.pos - 1
|
||||
}
|
||||
b' ' | b'(' | b'\'' | b'"' | b'\n' => self.pos,
|
||||
_ => self.pos - 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_inlines<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
content: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) {
|
||||
let mut tail = content;
|
||||
|
||||
if let Some(tail_) = parse_inline(tail, arena, containers, parent) {
|
||||
tail = tail_;
|
||||
}
|
||||
|
||||
while let Some((tail_, i)) = InlinePositions::new(tail.as_bytes())
|
||||
.filter_map(|i| parse_inline(&tail[i..], arena, containers, parent).map(|tail| (tail, i)))
|
||||
.next()
|
||||
{
|
||||
if i != 0 {
|
||||
arena.insert_before_last_child(
|
||||
Element::Text {
|
||||
value: tail[0..i].into(),
|
||||
},
|
||||
parent,
|
||||
);
|
||||
}
|
||||
tail = tail_;
|
||||
}
|
||||
|
||||
if !tail.is_empty() {
|
||||
arena.append(Element::Text { value: tail.into() }, parent);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_inline<'a, T: ElementArena<'a>>(
|
||||
contents: &'a str,
|
||||
arena: &mut T,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
parent: NodeId,
|
||||
) -> Option<&'a str> {
|
||||
if contents.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let byte = contents.as_bytes()[0];
|
||||
|
||||
match byte {
|
||||
b'@' => {
|
||||
let (tail, snippet) = Snippet::parse(contents)?;
|
||||
arena.append(snippet, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'{' => {
|
||||
let (tail, macros) = Macros::parse(contents)?;
|
||||
arena.append(macros, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'<' => {
|
||||
if let Some((tail, _content)) = parse_radio_target(contents) {
|
||||
arena.append(Element::RadioTarget, parent);
|
||||
Some(tail)
|
||||
} else if let Some((tail, target)) = Target::parse(contents) {
|
||||
arena.append(target, parent);
|
||||
Some(tail)
|
||||
} else if let Some((tail, timestamp)) = Timestamp::parse_active(contents) {
|
||||
arena.append(timestamp, parent);
|
||||
Some(tail)
|
||||
} else {
|
||||
let (tail, timestamp) = Timestamp::parse_diary(contents)?;
|
||||
arena.append(timestamp, parent);
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
b'[' => {
|
||||
if let Some((tail, fn_ref)) = FnRef::parse(contents) {
|
||||
arena.append(fn_ref, parent);
|
||||
Some(tail)
|
||||
} else if let Some((tail, link)) = Link::parse(contents) {
|
||||
arena.append(link, parent);
|
||||
Some(tail)
|
||||
} else if let Some((tail, cookie)) = Cookie::parse(contents) {
|
||||
arena.append(cookie, parent);
|
||||
Some(tail)
|
||||
} else {
|
||||
let (tail, timestamp) = Timestamp::parse_inactive(contents)?;
|
||||
arena.append(timestamp, parent);
|
||||
Some(tail)
|
||||
}
|
||||
}
|
||||
b'*' | b'+' | b'/' | b'_' | b'=' | b'~' => {
|
||||
let (tail, emphasis) = Emphasis::parse(contents, byte)?;
|
||||
let (element, content) = emphasis.into_element();
|
||||
let is_inline_container = match element {
|
||||
Element::Bold | Element::Strike | Element::Italic | Element::Underline => true,
|
||||
_ => false,
|
||||
};
|
||||
let node = arena.append(element, parent);
|
||||
if is_inline_container {
|
||||
containers.push(Container::Inline { content, node });
|
||||
}
|
||||
Some(tail)
|
||||
}
|
||||
b's' => {
|
||||
let (tail, inline_src) = InlineSrc::parse(contents)?;
|
||||
arena.append(inline_src, parent);
|
||||
Some(tail)
|
||||
}
|
||||
b'c' => {
|
||||
let (tail, inline_call) = InlineCall::parse(contents)?;
|
||||
arena.append(inline_call, parent);
|
||||
Some(tail)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_list<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
contents: &'a str,
|
||||
parent: NodeId,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
) -> Option<&'a str> {
|
||||
let (mut tail, (first_item, content)) = ListItem::parse(contents)?;
|
||||
let first_item_indent = first_item.indent;
|
||||
let first_item_ordered = first_item.ordered;
|
||||
|
||||
let parent = arena.append(Element::Document { pre_blank: 0 }, parent); // placeholder
|
||||
|
||||
let node = arena.append(first_item, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
|
||||
while let Some((tail_, (item, content))) = ListItem::parse(tail) {
|
||||
if item.indent == first_item_indent {
|
||||
let node = arena.append(item, parent);
|
||||
containers.push(Container::Block { content, node });
|
||||
debug_assert_ne!(tail, tail_);
|
||||
tail = tail_;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (tail, post_blank) = blank_lines_count(tail);
|
||||
|
||||
arena.set(
|
||||
parent,
|
||||
List {
|
||||
indent: first_item_indent,
|
||||
ordered: first_item_ordered,
|
||||
post_blank,
|
||||
},
|
||||
);
|
||||
|
||||
Some(tail)
|
||||
}
|
||||
|
||||
pub fn parse_org_table<'a, T: ElementArena<'a>>(
|
||||
arena: &mut T,
|
||||
contents: &'a str,
|
||||
containers: &mut Vec<Container<'a>>,
|
||||
parent: NodeId,
|
||||
) -> &'a str {
|
||||
let (tail, contents) =
|
||||
lines_while(|line| line.trim_start().starts_with('|'))(contents).unwrap_or((contents, ""));
|
||||
let (tail, post_blank) = blank_lines_count(tail);
|
||||
|
||||
let mut iter = contents.trim_end().lines().peekable();
|
||||
|
||||
let mut lines = vec![];
|
||||
|
||||
let mut has_header = false;
|
||||
|
||||
// TODO: merge contiguous rules
|
||||
|
||||
if let Some(line) = iter.next() {
|
||||
let line = line.trim_start();
|
||||
if !line.starts_with("|-") {
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = iter.next() {
|
||||
let line = line.trim_start();
|
||||
if iter.peek().is_none() && line.starts_with("|-") {
|
||||
break;
|
||||
} else if line.starts_with("|-") {
|
||||
has_header = true;
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
|
||||
let parent = arena.append(
|
||||
Table::Org {
|
||||
tblfm: None,
|
||||
post_blank,
|
||||
has_header,
|
||||
},
|
||||
parent,
|
||||
);
|
||||
|
||||
for line in lines {
|
||||
if line.starts_with("|-") {
|
||||
if has_header {
|
||||
arena.append(Element::TableRow(TableRow::HeaderRule), parent);
|
||||
has_header = false;
|
||||
} else {
|
||||
arena.append(Element::TableRow(TableRow::BodyRule), parent);
|
||||
}
|
||||
} else {
|
||||
if has_header {
|
||||
let parent = arena.append(Element::TableRow(TableRow::Header), parent);
|
||||
for content in line.split_terminator('|').skip(1) {
|
||||
let node = arena.append(Element::TableCell(TableCell::Header), parent);
|
||||
containers.push(Container::Inline {
|
||||
content: content.trim(),
|
||||
node,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let parent = arena.append(Element::TableRow(TableRow::Body), parent);
|
||||
for content in line.split_terminator('|').skip(1) {
|
||||
let node = arena.append(Element::TableCell(TableCell::Body), parent);
|
||||
containers.push(Container::Inline {
|
||||
content: content.trim(),
|
||||
node,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tail
|
||||
}
|
||||
|
||||
pub fn blank_lines_count(input: &str) -> (&str, usize) {
|
||||
crate::parse::combinators::blank_lines_count(input).unwrap_or((input, 0))
|
||||
}
|
||||
|
||||
pub fn parse_headline(input: &str) -> Option<(&str, (&str, usize))> {
|
||||
let (input_, level) = parse_headline_level(input)?;
|
||||
let (input_, content) = lines_while(move |line| {
|
||||
parse_headline_level(line)
|
||||
.map(|(_, l)| l > level)
|
||||
.unwrap_or(true)
|
||||
})(input_)
|
||||
.unwrap_or((input_, ""));
|
||||
Some((input_, (&input[0..level + content.len()], level)))
|
||||
}
|
||||
|
||||
pub fn parse_headline_level(input: &str) -> Option<(&str, usize)> {
|
||||
let (input, stars) = take_while1::<_, _, ()>(|c: char| c == '*')(input).ok()?;
|
||||
|
||||
if input.starts_with(' ') || input.starts_with('\n') || input.is_empty() {
|
||||
Some((input, stars.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
233
src/syntax/block.rs
Normal file
233
src/syntax/block.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::{tag, tag_no_case},
|
||||
character::complete::{alpha1, line_ending, space0},
|
||||
combinator::eof,
|
||||
sequence::tuple,
|
||||
IResult, InputTake,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
blank_lines, debug_assert_lossless, line_starts_iter, node, token, trim_line_end,
|
||||
GreenElement, NodeBuilder,
|
||||
},
|
||||
element::element_nodes,
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
fn block_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, (block_begin, name)) = block_begin_node(input)?;
|
||||
let (input, pre_blank) = blank_lines(input)?;
|
||||
|
||||
let (kind, is_greater_block) = match name {
|
||||
s if s.eq_ignore_ascii_case("COMMENT") => (COMMENT_BLOCK, false),
|
||||
s if s.eq_ignore_ascii_case("EXAMPLE") => (EXAMPLE_BLOCK, false),
|
||||
s if s.eq_ignore_ascii_case("EXPORT") => (EXPORT_BLOCK, false),
|
||||
s if s.eq_ignore_ascii_case("SRC") => (SOURCE_BLOCK, false),
|
||||
|
||||
s if s.eq_ignore_ascii_case("CENTER") => (CENTER_BLOCK, true),
|
||||
s if s.eq_ignore_ascii_case("QUOTE") => (QUOTE_BLOCK, true),
|
||||
s if s.eq_ignore_ascii_case("VERSE") => (VERSE_BLOCK, true),
|
||||
_ => (SPECIAL_BLOCK, true),
|
||||
};
|
||||
|
||||
for (input, contents) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
|
||||
if let Ok((input, block_end)) = block_end_node(input, name) {
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
|
||||
let mut children = vec![block_begin];
|
||||
children.extend(pre_blank);
|
||||
if is_greater_block {
|
||||
children.push(node(BLOCK_CONTENT, element_nodes(contents)?));
|
||||
} else {
|
||||
children.push(node(BLOCK_CONTENT, comma_quoted_text_nodes(contents)));
|
||||
}
|
||||
children.push(block_end);
|
||||
children.extend(post_blank);
|
||||
return Ok((input, node(kind, children)));
|
||||
}
|
||||
}
|
||||
|
||||
Err(nom::Err::Error(()))
|
||||
}
|
||||
|
||||
fn block_begin_node(input: Input) -> IResult<Input, (GreenElement, &str), ()> {
|
||||
let (input, (ws, start, name, (argument, ws_, nl))) =
|
||||
tuple((space0, tag_no_case("#+BEGIN_"), alpha1, trim_line_end))(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
b.ws(ws);
|
||||
b.text(start);
|
||||
b.text(name);
|
||||
b.text(argument);
|
||||
b.ws(ws_);
|
||||
b.nl(nl);
|
||||
|
||||
Ok((input, (b.finish(BLOCK_BEGIN), name.as_str())))
|
||||
}
|
||||
|
||||
fn block_end_node<'a>(input: Input<'a>, name: &str) -> IResult<Input<'a>, GreenElement, ()> {
|
||||
let (input, (ws, end, name, ws_, nl)) = tuple((
|
||||
space0,
|
||||
tag_no_case("#+END_"),
|
||||
tag(name),
|
||||
space0,
|
||||
alt((line_ending, eof)),
|
||||
))(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
b.ws(ws);
|
||||
b.text(end);
|
||||
b.text(name);
|
||||
b.ws(ws_);
|
||||
b.nl(nl);
|
||||
|
||||
Ok((input, b.finish(BLOCK_END)))
|
||||
}
|
||||
|
||||
fn comma_quoted_text_nodes(input: Input) -> Vec<GreenElement> {
|
||||
let mut nodes = vec![];
|
||||
|
||||
let s = input.as_str();
|
||||
|
||||
let mut start = 0;
|
||||
for i in line_starts_iter(s) {
|
||||
// line must start with either ",*" or ",#+"
|
||||
if s.get(i..i + 2) != Some(",*") && s.get(i..i + 3) != Some(",#+") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let text = &s[start..i];
|
||||
if !text.is_empty() {
|
||||
nodes.push(token(TEXT, text));
|
||||
}
|
||||
|
||||
nodes.push(token(COMMA, ","));
|
||||
start = i + 1;
|
||||
}
|
||||
|
||||
if !s[start..].is_empty() {
|
||||
nodes.push(token(TEXT, &s[start..]));
|
||||
}
|
||||
|
||||
nodes
|
||||
}
|
||||
|
||||
pub fn block_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(block_node_base)(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse() {
|
||||
use crate::ast::{ExampleBlock, SourceBlock};
|
||||
use crate::tests::to_ast;
|
||||
|
||||
let to_src_block = to_ast::<SourceBlock>(block_node);
|
||||
let to_example_block = to_ast::<ExampleBlock>(block_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_example_block(
|
||||
r#"#+BEGIN_EXAMPLE
|
||||
,* headline
|
||||
,#+block
|
||||
text
|
||||
#+END_EXAMPLE"#
|
||||
).syntax,
|
||||
@r###"
|
||||
EXAMPLE_BLOCK@0..59
|
||||
BLOCK_BEGIN@0..16
|
||||
TEXT@0..8 "#+BEGIN_"
|
||||
TEXT@8..15 "EXAMPLE"
|
||||
TEXT@15..15 ""
|
||||
NEW_LINE@15..16 "\n"
|
||||
BLOCK_CONTENT@16..42
|
||||
COMMA@16..17 ","
|
||||
TEXT@17..28 "* headline\n"
|
||||
COMMA@28..29 ","
|
||||
TEXT@29..42 "#+block\ntext\n"
|
||||
BLOCK_END@42..59
|
||||
WHITESPACE@42..46 " "
|
||||
TEXT@46..52 "#+END_"
|
||||
TEXT@52..59 "EXAMPLE"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_src_block(
|
||||
r#"#+BEGIN_SRC
|
||||
|
||||
|
||||
#+END_SRC"#
|
||||
).syntax,
|
||||
@r###"
|
||||
SOURCE_BLOCK@0..27
|
||||
BLOCK_BEGIN@0..12
|
||||
TEXT@0..8 "#+BEGIN_"
|
||||
TEXT@8..11 "SRC"
|
||||
TEXT@11..11 ""
|
||||
NEW_LINE@11..12 "\n"
|
||||
BLANK_LINE@12..13
|
||||
NEW_LINE@12..13 "\n"
|
||||
BLANK_LINE@13..14
|
||||
NEW_LINE@13..14 "\n"
|
||||
BLOCK_CONTENT@14..14
|
||||
BLOCK_END@14..27
|
||||
WHITESPACE@14..18 " "
|
||||
TEXT@18..24 "#+END_"
|
||||
TEXT@24..27 "SRC"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_src_block(
|
||||
r#"#+begin_src
|
||||
#+end_src"#
|
||||
).syntax,
|
||||
@r###"
|
||||
SOURCE_BLOCK@0..25
|
||||
BLOCK_BEGIN@0..12
|
||||
TEXT@0..8 "#+begin_"
|
||||
TEXT@8..11 "src"
|
||||
TEXT@11..11 ""
|
||||
NEW_LINE@11..12 "\n"
|
||||
BLOCK_CONTENT@12..12
|
||||
BLOCK_END@12..25
|
||||
WHITESPACE@12..16 " "
|
||||
TEXT@16..22 "#+end_"
|
||||
TEXT@22..25 "src"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_src_block(
|
||||
r#"#+BEGIN_SRC javascript
|
||||
alert('Hello World!');
|
||||
#+END_SRC
|
||||
|
||||
"#).syntax,
|
||||
@r###"
|
||||
SOURCE_BLOCK@0..69
|
||||
BLOCK_BEGIN@0..27
|
||||
TEXT@0..8 "#+BEGIN_"
|
||||
TEXT@8..11 "SRC"
|
||||
TEXT@11..22 " javascript"
|
||||
WHITESPACE@22..26 " "
|
||||
NEW_LINE@26..27 "\n"
|
||||
BLOCK_CONTENT@27..50
|
||||
TEXT@27..50 "alert('Hello World!');\n"
|
||||
BLOCK_END@50..64
|
||||
WHITESPACE@50..54 " "
|
||||
TEXT@54..60 "#+END_"
|
||||
TEXT@60..63 "SRC"
|
||||
NEW_LINE@63..64 "\n"
|
||||
BLANK_LINE@64..65
|
||||
NEW_LINE@64..65 "\n"
|
||||
BLANK_LINE@65..69
|
||||
WHITESPACE@65..69 " "
|
||||
"###
|
||||
);
|
||||
|
||||
// TODO: more testing
|
||||
}
|
||||
137
src/syntax/clock.rs
Normal file
137
src/syntax/clock.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag,
|
||||
character::complete::{digit1, line_ending, space0},
|
||||
combinator::{eof, map, opt},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
blank_lines, colon_token, debug_assert_lossless, double_arrow_token, GreenElement,
|
||||
NodeBuilder,
|
||||
},
|
||||
input::Input,
|
||||
timestamp::{timestamp_active_node, timestamp_inactive_node},
|
||||
SyntaxKind,
|
||||
};
|
||||
|
||||
pub fn clock_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
space0,
|
||||
tag("CLOCK:"),
|
||||
space0,
|
||||
alt((timestamp_inactive_node, timestamp_active_node)),
|
||||
opt(tuple((
|
||||
space0,
|
||||
double_arrow_token,
|
||||
space0,
|
||||
digit1,
|
||||
colon_token,
|
||||
digit1,
|
||||
))),
|
||||
space0,
|
||||
alt((line_ending, eof)),
|
||||
blank_lines,
|
||||
)),
|
||||
|(ws, clock, ws_, timestamp, duration, ws__, nl, post_blank)| {
|
||||
let mut b = NodeBuilder::new();
|
||||
|
||||
b.ws(ws);
|
||||
b.text(clock);
|
||||
b.ws(ws_);
|
||||
b.push(timestamp);
|
||||
if let Some((ws, double_arrow, ws_, hour, colon, minute)) = duration {
|
||||
b.ws(ws);
|
||||
b.push(double_arrow);
|
||||
b.ws(ws_);
|
||||
b.text(hour);
|
||||
b.push(colon);
|
||||
b.text(minute);
|
||||
}
|
||||
b.ws(ws__);
|
||||
b.nl(nl);
|
||||
b.children.extend(post_blank);
|
||||
b.finish(SyntaxKind::CLOCK)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::ast::Clock;
|
||||
use crate::tests::to_ast;
|
||||
|
||||
let to_clock = to_ast::<Clock>(clock_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_clock("CLOCK: [2003-09-16 Tue 09:39]").syntax,
|
||||
@r###"
|
||||
CLOCK@0..29
|
||||
TEXT@0..6 "CLOCK:"
|
||||
WHITESPACE@6..7 " "
|
||||
TIMESTAMP_INACTIVE@7..29
|
||||
L_BRACKET@7..8 "["
|
||||
TIMESTAMP_YEAR@8..12 "2003"
|
||||
MINUS@12..13 "-"
|
||||
TIMESTAMP_MONTH@13..15 "09"
|
||||
MINUS@15..16 "-"
|
||||
TIMESTAMP_DAY@16..18 "16"
|
||||
WHITESPACE@18..19 " "
|
||||
TIMESTAMP_DAYNAME@19..22 "Tue"
|
||||
WHITESPACE@22..23 " "
|
||||
TIMESTAMP_HOUR@23..25 "09"
|
||||
COLON@25..26 ":"
|
||||
TIMESTAMP_MINUTE@26..28 "39"
|
||||
R_BRACKET@28..29 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_clock("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00\n\n").syntax,
|
||||
@r###"
|
||||
CLOCK@0..64
|
||||
TEXT@0..6 "CLOCK:"
|
||||
WHITESPACE@6..7 " "
|
||||
TIMESTAMP_INACTIVE@7..53
|
||||
L_BRACKET@7..8 "["
|
||||
TIMESTAMP_YEAR@8..12 "2003"
|
||||
MINUS@12..13 "-"
|
||||
TIMESTAMP_MONTH@13..15 "09"
|
||||
MINUS@15..16 "-"
|
||||
TIMESTAMP_DAY@16..18 "16"
|
||||
WHITESPACE@18..19 " "
|
||||
TIMESTAMP_DAYNAME@19..22 "Tue"
|
||||
WHITESPACE@22..23 " "
|
||||
TIMESTAMP_HOUR@23..25 "09"
|
||||
COLON@25..26 ":"
|
||||
TIMESTAMP_MINUTE@26..28 "39"
|
||||
R_BRACKET@28..29 "]"
|
||||
MINUS2@29..31 "--"
|
||||
L_BRACKET@31..32 "["
|
||||
TIMESTAMP_YEAR@32..36 "2003"
|
||||
MINUS@36..37 "-"
|
||||
TIMESTAMP_MONTH@37..39 "09"
|
||||
MINUS@39..40 "-"
|
||||
TIMESTAMP_DAY@40..42 "16"
|
||||
WHITESPACE@42..43 " "
|
||||
TIMESTAMP_DAYNAME@43..46 "Tue"
|
||||
WHITESPACE@46..47 " "
|
||||
TIMESTAMP_HOUR@47..49 "10"
|
||||
COLON@49..50 ":"
|
||||
TIMESTAMP_MINUTE@50..52 "39"
|
||||
R_BRACKET@52..53 "]"
|
||||
WHITESPACE@53..54 " "
|
||||
DOUBLE_ARROW@54..56 "=>"
|
||||
WHITESPACE@56..58 " "
|
||||
TEXT@58..59 "1"
|
||||
COLON@59..60 ":"
|
||||
TEXT@60..62 "00"
|
||||
NEW_LINE@62..63 "\n"
|
||||
BLANK_LINE@63..64
|
||||
NEW_LINE@63..64 "\n"
|
||||
"###
|
||||
);
|
||||
}
|
||||
259
src/syntax/combinator.rs
Normal file
259
src/syntax/combinator.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
use std::iter::once;
|
||||
|
||||
use memchr::{memchr, memchr_iter};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag,
|
||||
character::complete::{line_ending, space0},
|
||||
combinator::eof,
|
||||
sequence::tuple,
|
||||
AsBytes, IResult, InputLength, InputTake, Parser,
|
||||
};
|
||||
use rowan::{GreenNode, GreenToken, Language, NodeOrToken};
|
||||
|
||||
use super::{input::Input, OrgLanguage, SyntaxKind, SyntaxKind::*};
|
||||
|
||||
pub type GreenElement = NodeOrToken<GreenNode, GreenToken>;
|
||||
|
||||
#[inline]
|
||||
pub fn token(kind: SyntaxKind, input: &str) -> GreenElement {
|
||||
GreenElement::Token(GreenToken::new(OrgLanguage::kind_to_raw(kind), input))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn node<I>(kind: SyntaxKind, children: I) -> GreenElement
|
||||
where
|
||||
I: IntoIterator<Item = GreenElement>,
|
||||
I::IntoIter: ExactSizeIterator,
|
||||
{
|
||||
GreenElement::Node(GreenNode::new(OrgLanguage::kind_to_raw(kind), children))
|
||||
}
|
||||
|
||||
macro_rules! token_parser {
|
||||
($name:ident, $token:literal, $kind:ident) => {
|
||||
#[doc = "Recognizes `"]
|
||||
#[doc = $token]
|
||||
#[doc = "` and returns GreenToken"]
|
||||
pub fn $name(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (i, o) = tag($token)(input)?;
|
||||
Ok((i, token($kind, o.as_str())))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
token_parser!(l_bracket_token, "[", L_BRACKET);
|
||||
token_parser!(r_bracket_token, "]", R_BRACKET);
|
||||
token_parser!(l_bracket2_token, "[[", L_BRACKET2);
|
||||
token_parser!(r_bracket2_token, "]]", R_BRACKET2);
|
||||
token_parser!(l_parens_token, "(", L_PARENS);
|
||||
token_parser!(r_parens_token, ")", R_PARENS);
|
||||
token_parser!(l_angle_token, "<", L_ANGLE);
|
||||
token_parser!(r_angle_token, ">", R_ANGLE);
|
||||
token_parser!(l_curly_token, "{", L_CURLY);
|
||||
token_parser!(r_curly_token, "}", R_CURLY);
|
||||
token_parser!(l_curly3_token, "{{{", L_CURLY3);
|
||||
token_parser!(r_curly3_token, "}}}", R_CURLY3);
|
||||
token_parser!(l_angle2_token, "<<", L_ANGLE2);
|
||||
token_parser!(r_angle2_token, ">>", R_ANGLE2);
|
||||
token_parser!(l_angle3_token, "<<<", L_ANGLE3);
|
||||
token_parser!(r_angle3_token, ">>>", R_ANGLE3);
|
||||
token_parser!(at_token, "@", AT);
|
||||
token_parser!(at2_token, "@@", AT2);
|
||||
token_parser!(minus2_token, "--", MINUS2);
|
||||
// token_parser!(percent_token, "%", PERCENT);
|
||||
token_parser!(percent2_token, "%%", PERCENT2);
|
||||
// token_parser!(slash_token, "/", SLASH);
|
||||
// token_parser!(underscore_token, "_", UNDERSCORE);
|
||||
// token_parser!(star_token, "*", STAR);
|
||||
token_parser!(plus_token, "+", PLUS);
|
||||
token_parser!(minus_token, "-", MINUS);
|
||||
token_parser!(colon_token, ":", COLON);
|
||||
token_parser!(colon2_token, "::", COLON2);
|
||||
token_parser!(pipe_token, "|", PIPE);
|
||||
// token_parser!(equal_token, "=", EQUAL);
|
||||
// token_parser!(tilde_token, "~", TILDE);
|
||||
token_parser!(hash_plus_token, "#+", HASH_PLUS);
|
||||
token_parser!(hash_token, "#", HASH);
|
||||
token_parser!(double_arrow_token, "=>", DOUBLE_ARROW);
|
||||
|
||||
pub fn debug_assert_lossless<'a, F>(
|
||||
mut f: F,
|
||||
) -> impl FnMut(Input<'a>) -> IResult<Input<'a>, GreenElement, ()>
|
||||
where
|
||||
F: Parser<Input<'a>, GreenElement, ()>,
|
||||
{
|
||||
move |input: Input| {
|
||||
let (i, o) = f.parse(input)?;
|
||||
|
||||
debug_assert_eq!(
|
||||
&input.as_str()[0..(input.input_len() - i.input_len())],
|
||||
&o.to_string(),
|
||||
"parser must be lossless"
|
||||
);
|
||||
|
||||
Ok((i, o))
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes all blank lines
|
||||
pub fn blank_lines(input: Input) -> IResult<Input, Vec<GreenElement>, ()> {
|
||||
let mut lines = vec![];
|
||||
let mut i = input;
|
||||
|
||||
while !i.is_empty() {
|
||||
match tuple::<_, _, (), _>((space0, alt((line_ending, eof))))(i) {
|
||||
Ok((input, (ws, nl))) => {
|
||||
let mut b = NodeBuilder::new();
|
||||
b.ws(ws);
|
||||
b.nl(nl);
|
||||
lines.push(b.finish(BLANK_LINE));
|
||||
i = input;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok((i, lines))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_blank_lines() {
|
||||
use crate::config::ParseConfig;
|
||||
let config = &ParseConfig::default();
|
||||
let (input, output) = blank_lines(("", config).into()).unwrap();
|
||||
assert_eq!(input.as_str(), "");
|
||||
assert_eq!(output, vec![]);
|
||||
|
||||
let (input, output) = blank_lines((" t", config).into()).unwrap();
|
||||
assert_eq!(input.as_str(), " t");
|
||||
assert_eq!(output, vec![]);
|
||||
|
||||
let (input, output) = blank_lines((" \r\n\n\t\t\r\n \n ", config).into()).unwrap();
|
||||
assert_eq!(input.as_str(), "");
|
||||
assert_eq!(output.len(), 5);
|
||||
assert_eq!(output[0].to_string(), " \r\n");
|
||||
assert_eq!(output[1].to_string(), "\n");
|
||||
assert_eq!(output[2].to_string(), "\t\t\r\n");
|
||||
assert_eq!(output[3].to_string(), " \n");
|
||||
assert_eq!(output[4].to_string(), " ");
|
||||
|
||||
let (input, output) =
|
||||
blank_lines((" \r\n\n\t\t\r\n \n t\n \r\n\n\t\t\r\n \n", config).into()).unwrap();
|
||||
assert_eq!(input.as_str(), " t\n \r\n\n\t\t\r\n \n");
|
||||
assert_eq!(output.len(), 4);
|
||||
assert_eq!(output[0].to_string(), " \r\n");
|
||||
assert_eq!(output[1].to_string(), "\n");
|
||||
assert_eq!(output[2].to_string(), "\t\t\r\n");
|
||||
assert_eq!(output[3].to_string(), " \n");
|
||||
}
|
||||
|
||||
/// Returns 1. anything before trailing whitespace, 2. whitespace itself, 3. line feeding
|
||||
pub fn trim_line_end(input: Input) -> IResult<Input, (Input, Input, Input), ()> {
|
||||
let (input, line) = input.take_split(
|
||||
memchr(b'\n', input.as_bytes())
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(input.input_len()),
|
||||
);
|
||||
|
||||
let (ws_and_nl, contents) = line.take_split(
|
||||
line.as_bytes()
|
||||
.iter()
|
||||
.rposition(|u| !u.is_ascii_whitespace())
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0),
|
||||
);
|
||||
|
||||
let (nl, ws) = space0(ws_and_nl)?;
|
||||
|
||||
Ok((input, (contents, ws, nl)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_line_end() {
|
||||
use crate::config::ParseConfig;
|
||||
let config = &ParseConfig::default();
|
||||
let (input, output) = trim_line_end(("", config).into()).unwrap();
|
||||
assert_eq!(input.as_str(), "");
|
||||
assert_eq!(output.0.as_str(), "");
|
||||
assert_eq!(output.1.as_str(), "");
|
||||
assert_eq!(output.2.as_str(), "");
|
||||
|
||||
let (input, output) = trim_line_end(("* hello, world :abc:", config).into()).unwrap();
|
||||
assert_eq!(input.as_str(), "");
|
||||
assert_eq!(output.0.as_str(), "* hello, world :abc:");
|
||||
assert_eq!(output.1.as_str(), "");
|
||||
assert_eq!(output.2.as_str(), "");
|
||||
|
||||
let (input, output) =
|
||||
trim_line_end(("* hello, world :abc: \r\nrest\n", config).into()).unwrap();
|
||||
assert_eq!(input.as_str(), "rest\n");
|
||||
assert_eq!(output.0.as_str(), "* hello, world :abc:");
|
||||
assert_eq!(output.1.as_str(), " ");
|
||||
assert_eq!(output.2.as_str(), "\r\n");
|
||||
}
|
||||
|
||||
/// Returns an iterator of positions of line start, including zero
|
||||
pub fn line_starts_iter(s: &str) -> impl Iterator<Item = usize> + '_ {
|
||||
once(0).chain(memchr_iter(b'\n', s.as_bytes()).map(|i| i + 1))
|
||||
}
|
||||
|
||||
/// Returns an iterator of positions of line end, including eof
|
||||
pub fn line_ends_iter(s: &str) -> impl Iterator<Item = usize> + '_ {
|
||||
memchr_iter(b'\n', s.as_bytes())
|
||||
.map(|i| i + 1)
|
||||
.chain(once(s.len()))
|
||||
}
|
||||
|
||||
pub struct NodeBuilder {
|
||||
pub children: Vec<GreenElement>,
|
||||
}
|
||||
|
||||
impl NodeBuilder {
|
||||
pub fn new() -> NodeBuilder {
|
||||
NodeBuilder { children: vec![] }
|
||||
}
|
||||
|
||||
pub fn ws(&mut self, i: Input) {
|
||||
if !i.is_empty() {
|
||||
debug_assert!(i.bytes().all(|c| c.is_ascii_whitespace()));
|
||||
self.children.push(i.ws_token())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nl(&mut self, i: Input) {
|
||||
if !i.is_empty() {
|
||||
debug_assert!(
|
||||
i.s == "\n" || i.s == "\r\n",
|
||||
"{:?} should be a new line",
|
||||
i.s
|
||||
);
|
||||
self.children.push(i.nl_token())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&mut self, i: Input) {
|
||||
self.children.push(i.text_token())
|
||||
}
|
||||
|
||||
pub fn token(&mut self, kind: SyntaxKind, i: Input) {
|
||||
self.children.push(i.token(kind))
|
||||
}
|
||||
|
||||
pub fn push(&mut self, elem: GreenElement) {
|
||||
self.children.push(elem)
|
||||
}
|
||||
|
||||
pub fn push_opt(&mut self, elem: Option<GreenElement>) {
|
||||
if let Some(elem) = elem {
|
||||
self.children.push(elem)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.children.len()
|
||||
}
|
||||
|
||||
pub fn finish(self, kind: SyntaxKind) -> GreenElement {
|
||||
GreenElement::Node(GreenNode::new(kind.into(), self.children))
|
||||
}
|
||||
}
|
||||
85
src/syntax/comment.rs
Normal file
85
src/syntax/comment.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use nom::{IResult, InputTake};
|
||||
|
||||
use super::{
|
||||
combinator::{blank_lines, debug_assert_lossless, line_ends_iter, node, GreenElement},
|
||||
input::Input,
|
||||
SyntaxKind,
|
||||
};
|
||||
|
||||
fn comment_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let mut start = 0;
|
||||
for i in line_ends_iter(input.as_str()) {
|
||||
let line = &input.as_str()[start..i];
|
||||
let trimmed = line.trim_start();
|
||||
|
||||
if trimmed == "#" || trimmed == "#\n" || trimmed == "#\r\n" || trimmed.starts_with("# ") {
|
||||
start = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if start == 0 {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
let (input, contents) = input.take_split(start);
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
|
||||
let mut children = vec![];
|
||||
children.push(contents.text_token());
|
||||
children.extend(post_blank);
|
||||
|
||||
Ok((input, node(SyntaxKind::COMMENT, children)))
|
||||
}
|
||||
|
||||
pub fn comment_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(comment_node_base)(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{
|
||||
syntax::{comment::comment_node, input::Input, SyntaxNode},
|
||||
ParseConfig,
|
||||
};
|
||||
|
||||
let t = |input: &str| {
|
||||
SyntaxNode::new_root(
|
||||
comment_node(Input {
|
||||
s: input,
|
||||
c: &ParseConfig::default(),
|
||||
})
|
||||
.unwrap()
|
||||
.1
|
||||
.into_node()
|
||||
.unwrap(),
|
||||
)
|
||||
};
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
t("#"),
|
||||
@r###"
|
||||
COMMENT@0..1
|
||||
TEXT@0..1 "#"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
t("#\n # a\n #\n\n"),
|
||||
@r###"
|
||||
COMMENT@0..12
|
||||
TEXT@0..11 "#\n # a\n #\n"
|
||||
BLANK_LINE@11..12
|
||||
NEW_LINE@11..12 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
t("#\na\n #\n\n"),
|
||||
@r###"
|
||||
COMMENT@0..2
|
||||
TEXT@0..2 "#\n"
|
||||
"###
|
||||
);
|
||||
}
|
||||
144
src/syntax/cookie.rs
Normal file
144
src/syntax/cookie.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag,
|
||||
character::complete::digit0,
|
||||
combinator::map,
|
||||
sequence::{pair, separated_pair, tuple},
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
debug_assert_lossless, l_bracket_token, node, r_bracket_token, token, GreenElement,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn cookie_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
l_bracket_token,
|
||||
alt((
|
||||
separated_pair(digit0, tag("/"), digit0),
|
||||
pair(digit0, tag("%")),
|
||||
)),
|
||||
r_bracket_token,
|
||||
)),
|
||||
|(l_bracket, value, r_bracket)| {
|
||||
let mut children = vec![l_bracket];
|
||||
|
||||
children.push(token(TEXT, value.0.as_str()));
|
||||
match value.1.as_str() {
|
||||
"%" => {
|
||||
children.push(token(PERCENT, value.1.as_str()));
|
||||
}
|
||||
_ => {
|
||||
children.push(token(SLASH, "/"));
|
||||
children.push(token(TEXT, value.1.as_str()));
|
||||
}
|
||||
}
|
||||
children.push(r_bracket);
|
||||
|
||||
node(COOKIE, children)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::ast::Cookie;
|
||||
use crate::tests::to_ast;
|
||||
use crate::ParseConfig;
|
||||
|
||||
let to_cookie = to_ast::<Cookie>(cookie_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_cookie("[1/10]").syntax,
|
||||
@r###"
|
||||
COOKIE@0..6
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..2 "1"
|
||||
SLASH@2..3 "/"
|
||||
TEXT@3..5 "10"
|
||||
R_BRACKET@5..6 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_cookie("[1/1000]").syntax,
|
||||
@r###"
|
||||
COOKIE@0..8
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..2 "1"
|
||||
SLASH@2..3 "/"
|
||||
TEXT@3..7 "1000"
|
||||
R_BRACKET@7..8 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_cookie("[10%]").syntax,
|
||||
@r###"
|
||||
COOKIE@0..5
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "10"
|
||||
PERCENT@3..4 "%"
|
||||
R_BRACKET@4..5 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_cookie("[%]").syntax,
|
||||
@r###"
|
||||
COOKIE@0..3
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..1 ""
|
||||
PERCENT@1..2 "%"
|
||||
R_BRACKET@2..3 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_cookie("[/]").syntax,
|
||||
@r###"
|
||||
COOKIE@0..3
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..1 ""
|
||||
SLASH@1..2 "/"
|
||||
TEXT@2..2 ""
|
||||
R_BRACKET@2..3 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_cookie("[100/]").syntax,
|
||||
@r###"
|
||||
COOKIE@0..6
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..4 "100"
|
||||
SLASH@4..5 "/"
|
||||
TEXT@5..5 ""
|
||||
R_BRACKET@5..6 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_cookie("[/100]").syntax,
|
||||
@r###"
|
||||
COOKIE@0..6
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..1 ""
|
||||
SLASH@1..2 "/"
|
||||
TEXT@2..5 "100"
|
||||
R_BRACKET@5..6 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(cookie_node(("[10% ]", config).into()).is_err());
|
||||
assert!(cookie_node(("[1//100]", config).into()).is_err());
|
||||
assert!(cookie_node(("[1\\100]", config).into()).is_err());
|
||||
assert!(cookie_node(("[10%%]", config).into()).is_err());
|
||||
}
|
||||
128
src/syntax/document.rs
Normal file
128
src/syntax/document.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use nom::{
|
||||
combinator::{iterator, opt},
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{blank_lines, debug_assert_lossless, node, GreenElement},
|
||||
headline::{headline_node, section_node},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn document_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(document_node_base)(input)
|
||||
}
|
||||
|
||||
fn document_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let mut children = vec![];
|
||||
|
||||
let (input, pre_blank) = blank_lines(input)?;
|
||||
|
||||
children.extend(pre_blank);
|
||||
|
||||
let (input, section) = opt(section_node)(input)?;
|
||||
if let Some(section) = section {
|
||||
children.push(section);
|
||||
}
|
||||
|
||||
let mut it = iterator(input, headline_node);
|
||||
children.extend(&mut it);
|
||||
let (input, _) = it.finish()?;
|
||||
|
||||
debug_assert!(input.is_empty());
|
||||
|
||||
Ok((input, node(DOCUMENT, children)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::ast::Document;
|
||||
use crate::tests::to_ast;
|
||||
|
||||
let to_document = to_ast::<Document>(document_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_document("").syntax,
|
||||
@r###"
|
||||
DOCUMENT@0..0
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_document("\n \n\n").syntax,
|
||||
@r###"
|
||||
DOCUMENT@0..5
|
||||
BLANK_LINE@0..1
|
||||
NEW_LINE@0..1 "\n"
|
||||
BLANK_LINE@1..4
|
||||
WHITESPACE@1..3 " "
|
||||
NEW_LINE@3..4 "\n"
|
||||
BLANK_LINE@4..5
|
||||
NEW_LINE@4..5 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_document("section").syntax,
|
||||
@r###"
|
||||
DOCUMENT@0..7
|
||||
SECTION@0..7
|
||||
PARAGRAPH@0..7
|
||||
TEXT@0..7 "section"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_document("\n* section").syntax,
|
||||
@r###"
|
||||
DOCUMENT@0..10
|
||||
BLANK_LINE@0..1
|
||||
NEW_LINE@0..1 "\n"
|
||||
HEADLINE@1..10
|
||||
HEADLINE_STARS@1..2 "*"
|
||||
WHITESPACE@2..3 " "
|
||||
HEADLINE_TITLE@3..10
|
||||
TEXT@3..10 "section"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_document("\n** heading 2\n* heading 1").syntax,
|
||||
@r###"
|
||||
DOCUMENT@0..25
|
||||
BLANK_LINE@0..1
|
||||
NEW_LINE@0..1 "\n"
|
||||
HEADLINE@1..14
|
||||
HEADLINE_STARS@1..3 "**"
|
||||
WHITESPACE@3..4 " "
|
||||
HEADLINE_TITLE@4..13
|
||||
TEXT@4..13 "heading 2"
|
||||
NEW_LINE@13..14 "\n"
|
||||
HEADLINE@14..25
|
||||
HEADLINE_STARS@14..15 "*"
|
||||
WHITESPACE@15..16 " "
|
||||
HEADLINE_TITLE@16..25
|
||||
TEXT@16..25 "heading 1"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_document("section\n** heading 2\n*heading 1").syntax,
|
||||
@r###"
|
||||
DOCUMENT@0..31
|
||||
SECTION@0..8
|
||||
PARAGRAPH@0..8
|
||||
TEXT@0..8 "section\n"
|
||||
HEADLINE@8..31
|
||||
HEADLINE_STARS@8..10 "**"
|
||||
WHITESPACE@10..11 " "
|
||||
HEADLINE_TITLE@11..20
|
||||
TEXT@11..20 "heading 2"
|
||||
NEW_LINE@20..21 "\n"
|
||||
SECTION@21..31
|
||||
PARAGRAPH@21..31
|
||||
TEXT@21..31 "*heading 1"
|
||||
"###
|
||||
);
|
||||
}
|
||||
200
src/syntax/drawer.rs
Normal file
200
src/syntax/drawer.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::{tag_no_case, take_while1},
|
||||
character::complete::{line_ending, space0, space1},
|
||||
combinator::{eof, iterator, map, opt},
|
||||
sequence::tuple,
|
||||
IResult, InputTake,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
blank_lines, colon_token, debug_assert_lossless, line_starts_iter, node, plus_token,
|
||||
trim_line_end, GreenElement, NodeBuilder,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
fn drawer_begin_node(input: Input) -> IResult<Input, (GreenElement, &str), ()> {
|
||||
let mut b = NodeBuilder::new();
|
||||
|
||||
let (input, (ws, colon, name, colon_, ws_, nl)) = tuple((
|
||||
space0,
|
||||
colon_token,
|
||||
take_while1(|c: char| c.is_ascii_alphabetic() || c == '-' || c == '_'),
|
||||
colon_token,
|
||||
space0,
|
||||
alt((line_ending, eof)),
|
||||
))(input)?;
|
||||
|
||||
b.ws(ws);
|
||||
b.push(colon);
|
||||
b.text(name);
|
||||
b.push(colon_);
|
||||
b.ws(ws_);
|
||||
b.nl(nl);
|
||||
|
||||
Ok((input, (b.finish(DRAWER_BEGIN), name.as_str())))
|
||||
}
|
||||
|
||||
fn drawer_end_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, (ws, colon, end, colon_, ws_, nl)) = tuple((
|
||||
space0,
|
||||
colon_token,
|
||||
tag_no_case("END"),
|
||||
colon_token,
|
||||
space0,
|
||||
alt((line_ending, eof)),
|
||||
))(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
b.ws(ws);
|
||||
b.push(colon);
|
||||
b.text(end);
|
||||
b.push(colon_);
|
||||
b.ws(ws_);
|
||||
b.nl(nl);
|
||||
|
||||
Ok((input, b.finish(DRAWER_END)))
|
||||
}
|
||||
|
||||
fn drawer_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, (begin, _)) = drawer_begin_node(input)?;
|
||||
|
||||
let (input, pre_blank) = blank_lines(input)?;
|
||||
|
||||
for (input, contents) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
|
||||
if let Ok((input, end)) = drawer_end_node(input) {
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
let mut children = vec![begin];
|
||||
children.extend(pre_blank);
|
||||
children.push(contents.text_token());
|
||||
children.push(end);
|
||||
children.extend(post_blank);
|
||||
|
||||
return Ok((input, node(DRAWER, children)));
|
||||
}
|
||||
}
|
||||
|
||||
Err(nom::Err::Error(()))
|
||||
}
|
||||
|
||||
fn property_drawer_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, (begin, name)) = drawer_begin_node(input)?;
|
||||
|
||||
if name != "PROPERTIES" {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
let mut children = vec![begin];
|
||||
|
||||
let mut it = iterator(input, node_property_node);
|
||||
children.extend(&mut it);
|
||||
let (input, _) = it.finish()?;
|
||||
let (input, end) = drawer_end_node(input)?;
|
||||
|
||||
children.push(end);
|
||||
|
||||
Ok((input, node(PROPERTY_DRAWER, children)))
|
||||
}
|
||||
|
||||
fn node_property_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
map(
|
||||
tuple((
|
||||
space0,
|
||||
colon_token,
|
||||
take_while1(|c| c != ':' && c != '+'),
|
||||
opt(plus_token),
|
||||
colon_token,
|
||||
space1,
|
||||
trim_line_end,
|
||||
)),
|
||||
|(ws, colon, name, plus, colon_, ws_, (value, ws__, nl))| {
|
||||
let mut b = NodeBuilder::new();
|
||||
b.ws(ws);
|
||||
b.push(colon);
|
||||
b.text(name);
|
||||
b.push_opt(plus);
|
||||
b.push(colon_);
|
||||
b.ws(ws_);
|
||||
b.text(value);
|
||||
b.ws(ws__);
|
||||
b.nl(nl);
|
||||
b.finish(NODE_PROPERTY)
|
||||
},
|
||||
)(input)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(input), fields(input = input.s))]
|
||||
pub fn property_drawer_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(property_drawer_node_base)(input)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(input), fields(input = input.s))]
|
||||
pub fn drawer_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(drawer_node_base)(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Drawer, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_drawer = to_ast::<Drawer>(drawer_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_drawer(
|
||||
r#":DRAWER:
|
||||
:CUSTOM_ID: id
|
||||
:END:"#
|
||||
).syntax,
|
||||
@r###"
|
||||
DRAWER@0..33
|
||||
DRAWER_BEGIN@0..9
|
||||
COLON@0..1 ":"
|
||||
TEXT@1..7 "DRAWER"
|
||||
COLON@7..8 ":"
|
||||
NEW_LINE@8..9 "\n"
|
||||
TEXT@9..26 " :CUSTOM_ID: id\n"
|
||||
DRAWER_END@26..33
|
||||
WHITESPACE@26..28 " "
|
||||
COLON@28..29 ":"
|
||||
TEXT@29..32 "END"
|
||||
COLON@32..33 ":"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_drawer(
|
||||
r#":DRAWER:
|
||||
|
||||
:END:
|
||||
|
||||
"#
|
||||
).syntax,
|
||||
@r###"
|
||||
DRAWER@0..19
|
||||
DRAWER_BEGIN@0..9
|
||||
COLON@0..1 ":"
|
||||
TEXT@1..7 "DRAWER"
|
||||
COLON@7..8 ":"
|
||||
NEW_LINE@8..9 "\n"
|
||||
BLANK_LINE@9..10
|
||||
NEW_LINE@9..10 "\n"
|
||||
TEXT@10..10 ""
|
||||
DRAWER_END@10..18
|
||||
WHITESPACE@10..12 " "
|
||||
COLON@12..13 ":"
|
||||
TEXT@13..16 "END"
|
||||
COLON@16..17 ":"
|
||||
NEW_LINE@17..18 "\n"
|
||||
BLANK_LINE@18..19
|
||||
NEW_LINE@18..19 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
// https://github.com/PoiScript/orgize/issues/9
|
||||
assert!(drawer_node((":SPAGHETTI:\n", config).into()).is_err());
|
||||
}
|
||||
112
src/syntax/dyn_block.rs
Normal file
112
src/syntax/dyn_block.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag_no_case,
|
||||
character::complete::{alpha1, line_ending, space0, space1},
|
||||
combinator::eof,
|
||||
sequence::tuple,
|
||||
IResult, InputTake,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
blank_lines, debug_assert_lossless, line_starts_iter, node, trim_line_end, GreenElement,
|
||||
NodeBuilder,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
fn dyn_block_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, begin) = dyn_block_begin_node(input)?;
|
||||
let (input, pre_blank) = blank_lines(input)?;
|
||||
|
||||
for (input, contents) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
|
||||
if let Ok((input, end)) = dyn_block_end_node(input) {
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
let mut children = vec![begin];
|
||||
children.extend(pre_blank);
|
||||
children.push(contents.text_token());
|
||||
children.push(end);
|
||||
children.extend(post_blank);
|
||||
|
||||
return Ok((input, node(DYN_BLOCK, children)));
|
||||
}
|
||||
}
|
||||
|
||||
Err(nom::Err::Error(()))
|
||||
}
|
||||
|
||||
fn dyn_block_begin_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, (ws, begin, ws_, name, (args, ws__, nl))) = tuple((
|
||||
space0,
|
||||
tag_no_case("#+BEGIN:"),
|
||||
space1,
|
||||
alpha1,
|
||||
trim_line_end,
|
||||
))(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
b.ws(ws);
|
||||
b.text(begin);
|
||||
b.ws(ws_);
|
||||
b.text(name);
|
||||
b.text(args);
|
||||
b.ws(ws__);
|
||||
b.nl(nl);
|
||||
|
||||
Ok((input, b.finish(DYN_BLOCK_BEGIN)))
|
||||
}
|
||||
|
||||
fn dyn_block_end_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, (ws, end, ws_, nl)) = tuple((
|
||||
space0,
|
||||
tag_no_case("#+END:"),
|
||||
space0,
|
||||
alt((line_ending, eof)),
|
||||
))(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
b.ws(ws);
|
||||
b.text(end);
|
||||
b.ws(ws_);
|
||||
b.nl(nl);
|
||||
|
||||
Ok((input, b.finish(DYN_BLOCK_END)))
|
||||
}
|
||||
|
||||
pub fn dyn_block_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(dyn_block_node_base)(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::DynBlock, tests::to_ast};
|
||||
|
||||
let to_dyn_block = to_ast::<DynBlock>(dyn_block_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_dyn_block(
|
||||
r#"#+BEGIN: clocktable :scope file
|
||||
|
||||
CONTENTS
|
||||
#+END:
|
||||
"#).syntax,
|
||||
@r###"
|
||||
DYN_BLOCK@0..53
|
||||
DYN_BLOCK_BEGIN@0..32
|
||||
TEXT@0..8 "#+BEGIN:"
|
||||
WHITESPACE@8..9 " "
|
||||
TEXT@9..19 "clocktable"
|
||||
TEXT@19..31 " :scope file"
|
||||
NEW_LINE@31..32 "\n"
|
||||
BLANK_LINE@32..33
|
||||
NEW_LINE@32..33 "\n"
|
||||
TEXT@33..42 "CONTENTS\n"
|
||||
DYN_BLOCK_END@42..49
|
||||
TEXT@42..48 "#+END:"
|
||||
NEW_LINE@48..49 "\n"
|
||||
BLANK_LINE@49..53
|
||||
WHITESPACE@49..53 " "
|
||||
"###
|
||||
);
|
||||
}
|
||||
235
src/syntax/element.rs
Normal file
235
src/syntax/element.rs
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
use nom::{AsBytes, IResult, InputTake};
|
||||
|
||||
use super::{
|
||||
block::block_node,
|
||||
clock::clock_node,
|
||||
combinator::{line_starts_iter, GreenElement},
|
||||
comment::comment_node,
|
||||
drawer::drawer_node,
|
||||
dyn_block::dyn_block_node,
|
||||
fixed_width::fixed_width_node,
|
||||
fn_def::fn_def_node,
|
||||
input::Input,
|
||||
keyword::keyword_node,
|
||||
list::list_node,
|
||||
paragraph::paragraph_nodes,
|
||||
rule::rule_node,
|
||||
table::{org_table_node, table_el_node},
|
||||
};
|
||||
|
||||
/// Parses input into multiple element
|
||||
#[tracing::instrument(skip(input), fields(input = input.s))]
|
||||
pub fn element_nodes(input: Input) -> Result<Vec<GreenElement>, nom::Err<()>> {
|
||||
// TODO:
|
||||
// debug_assert!(!input.is_empty());
|
||||
let nodes = element_nodes_base(input)?;
|
||||
debug_assert_eq!(
|
||||
input.as_str(),
|
||||
nodes.iter().fold(String::new(), |s, n| s + &n.to_string()),
|
||||
"parser must be lossless"
|
||||
);
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
/// Parses input into multiple elements
|
||||
///
|
||||
/// input must not contains blank line in the beginning
|
||||
fn element_nodes_base(input: Input) -> Result<Vec<GreenElement>, nom::Err<()>> {
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum PreviousLine {
|
||||
None,
|
||||
BlankLine,
|
||||
AffiliatedKeyword,
|
||||
Other,
|
||||
}
|
||||
|
||||
let mut children = vec![];
|
||||
|
||||
let mut i = input;
|
||||
|
||||
let mut previous_line = PreviousLine::None;
|
||||
|
||||
'l: loop {
|
||||
for (input, head) in line_starts_iter(i.as_str()).map(|idx| i.take_split(idx)) {
|
||||
// find the first byte that's not a whitespace
|
||||
let trimmed = input.as_str().trim_start_matches(|c| c == ' ' || c == '\t');
|
||||
|
||||
// if this line is an affiliated keyword, that skip it
|
||||
if is_affiliated_keyword(trimmed) {
|
||||
if previous_line == PreviousLine::BlankLine {
|
||||
children.extend(paragraph_nodes(head)?);
|
||||
}
|
||||
previous_line = PreviousLine::AffiliatedKeyword;
|
||||
continue;
|
||||
}
|
||||
|
||||
// if this line is a blank line
|
||||
if is_blank_line(trimmed) {
|
||||
if previous_line == PreviousLine::AffiliatedKeyword {
|
||||
previous_line = PreviousLine::BlankLine;
|
||||
if let Ok((input, node)) = keyword_node(input) {
|
||||
if !head.is_empty() {
|
||||
children.extend(paragraph_nodes(head)?);
|
||||
}
|
||||
children.push(node);
|
||||
i = input;
|
||||
continue 'l;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok((input, node)) = match trimmed.bytes().next() {
|
||||
Some(b'[') => fn_def_node(input),
|
||||
Some(b'0'..=b'9') | Some(b'*') => list_node(input),
|
||||
Some(b'C') => clock_node(input),
|
||||
Some(b'-') => rule_node(input).or_else(|_| list_node(input)),
|
||||
Some(b':') => drawer_node(input).or_else(|_| fixed_width_node(input)),
|
||||
Some(b'|') => org_table_node(input),
|
||||
Some(b'+') => table_el_node(input).or_else(|_| list_node(input)),
|
||||
Some(b'#') => block_node(input)
|
||||
.or_else(|_| keyword_node(input))
|
||||
.or_else(|_| dyn_block_node(input))
|
||||
.or_else(|_| comment_node(input)),
|
||||
_ => Err(nom::Err::Error(())),
|
||||
} {
|
||||
if !head.is_empty() {
|
||||
children.extend(paragraph_nodes(head)?);
|
||||
}
|
||||
children.push(node);
|
||||
i = input;
|
||||
continue 'l;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if !i.is_empty() {
|
||||
children.extend(paragraph_nodes(i)?);
|
||||
}
|
||||
|
||||
Ok(children)
|
||||
}
|
||||
|
||||
pub fn is_affiliated_keyword(line: &str) -> bool {
|
||||
line.starts_with("#+CAPTION:")
|
||||
|| line.starts_with("#+DATA:")
|
||||
|| line.starts_with("#+HEADER:")
|
||||
|| line.starts_with("#+HEADERS:")
|
||||
|| line.starts_with("#+LABEL:")
|
||||
|| line.starts_with("#+NAME:")
|
||||
|| line.starts_with("#+PLOT:")
|
||||
|| line.starts_with("#+RESNAME:")
|
||||
|| line.starts_with("#+RESULT:")
|
||||
|| line.starts_with("#+RESULTS:")
|
||||
|| line.starts_with("#+SOURCE:")
|
||||
|| line.starts_with("#+SRCNAME:")
|
||||
|| line.starts_with("#+TBLNAME:")
|
||||
|| line.starts_with("#+ATTR_")
|
||||
}
|
||||
|
||||
pub fn is_blank_line(line: &str) -> bool {
|
||||
matches!(line.bytes().next(), None | Some(b'\n') | Some(b'\r'))
|
||||
}
|
||||
|
||||
pub fn element_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let mut has_affiliated_keyword = false;
|
||||
|
||||
for offset in line_starts_iter(input.as_str()) {
|
||||
// find the first byte that's not a whitespace
|
||||
let Some(idx) = input.as_bytes()[offset..]
|
||||
.iter()
|
||||
.position(|b| *b != b' ' && *b != b'\t')
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
let line = &input.as_str()[(idx + offset)..];
|
||||
|
||||
// if this line is an affiliated keyword, that we skip it
|
||||
if line.starts_with("#+CAPTION:")
|
||||
|| line.starts_with("#+DATA:")
|
||||
|| line.starts_with("#+HEADER:")
|
||||
|| line.starts_with("#+HEADERS:")
|
||||
|| line.starts_with("#+LABEL:")
|
||||
|| line.starts_with("#+NAME:")
|
||||
|| line.starts_with("#+PLOT:")
|
||||
|| line.starts_with("#+RESNAME:")
|
||||
|| line.starts_with("#+RESULT:")
|
||||
|| line.starts_with("#+RESULTS:")
|
||||
|| line.starts_with("#+SOURCE:")
|
||||
|| line.starts_with("#+SRCNAME:")
|
||||
|| line.starts_with("#+TBLNAME:")
|
||||
|| line.starts_with("#+ATTR_")
|
||||
{
|
||||
has_affiliated_keyword = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
return match input.as_bytes()[idx + offset] {
|
||||
b'[' => fn_def_node(input),
|
||||
b'0'..=b'9' | b'*' => list_node(input),
|
||||
b'C' => clock_node(input),
|
||||
b'-' => rule_node(input).or_else(|_| list_node(input)),
|
||||
b':' => drawer_node(input).or_else(|_| fixed_width_node(input)),
|
||||
b'|' => org_table_node(input),
|
||||
b'+' => table_el_node(input).or_else(|_| list_node(input)),
|
||||
b'#' => block_node(input)
|
||||
.or_else(|_| keyword_node(input))
|
||||
.or_else(|_| dyn_block_node(input))
|
||||
.or_else(|_| comment_node(input)),
|
||||
_ => Err(nom::Err::Error(())),
|
||||
};
|
||||
}
|
||||
|
||||
// we find an affiliated keyword, but it's not followed by any element
|
||||
// in this case, we treat it as a simple keyword
|
||||
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::syntax::{SyntaxKind, SyntaxNode};
|
||||
use crate::{syntax::combinator::node, ParseConfig};
|
||||
|
||||
let t = |input: &str| {
|
||||
let config = &ParseConfig::default();
|
||||
let children = element_nodes((input, config).into()).unwrap();
|
||||
SyntaxNode::new_root(node(SyntaxKind::SECTION, children).into_node().unwrap())
|
||||
};
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
t(r#"a
|
||||
|
||||
b"#),
|
||||
@r###"
|
||||
SECTION@0..4
|
||||
PARAGRAPH@0..3
|
||||
TEXT@0..2 "a\n"
|
||||
BLANK_LINE@2..3
|
||||
NEW_LINE@2..3 "\n"
|
||||
PARAGRAPH@3..4
|
||||
TEXT@3..4 "b"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
t("#+ATTR_HTML: :width 300px\n[[./img/a.jpg]]"),
|
||||
@r###"
|
||||
SECTION@0..41
|
||||
KEYWORD@0..26
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..11 "ATTR_HTML"
|
||||
COLON@11..12 ":"
|
||||
TEXT@12..25 " :width 300px"
|
||||
NEW_LINE@25..26 "\n"
|
||||
PARAGRAPH@26..41
|
||||
LINK@26..41
|
||||
L_BRACKET2@26..28 "[["
|
||||
LINK_PATH@28..39 "./img/a.jpg"
|
||||
R_BRACKET2@39..41 "]]"
|
||||
"###
|
||||
)
|
||||
}
|
||||
146
src/syntax/emphasis.rs
Normal file
146
src/syntax/emphasis.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
use bytecount::count;
|
||||
use memchr::memchr_iter;
|
||||
use nom::{combinator::map, AsBytes, IResult, Slice};
|
||||
|
||||
use super::{
|
||||
combinator::{debug_assert_lossless, node, token, GreenElement},
|
||||
input::Input,
|
||||
object::object_nodes,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn bold_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(emphasis(b'*'), |contents| {
|
||||
let mut children = vec![token(STAR, "*")];
|
||||
children.extend(object_nodes(contents));
|
||||
children.push(token(STAR, "*"));
|
||||
node(BOLD, children)
|
||||
}))(input)
|
||||
}
|
||||
|
||||
pub fn code_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(emphasis(b'~'), |contents| {
|
||||
node(
|
||||
CODE,
|
||||
[token(TILDE, "~"), contents.text_token(), token(TILDE, "~")],
|
||||
)
|
||||
}))(input)
|
||||
}
|
||||
|
||||
pub fn strike_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(emphasis(b'+'), |contents| {
|
||||
let mut children = vec![token(PLUS, "+")];
|
||||
children.extend(object_nodes(contents));
|
||||
children.push(token(PLUS, "+"));
|
||||
node(STRIKE, children)
|
||||
}))(input)
|
||||
}
|
||||
|
||||
pub fn verbatim_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(emphasis(b'='), |contents| {
|
||||
node(
|
||||
VERBATIM,
|
||||
[token(EQUAL, "="), contents.text_token(), token(EQUAL, "=")],
|
||||
)
|
||||
}))(input)
|
||||
}
|
||||
|
||||
pub fn underline_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(emphasis(b'_'), |contents| {
|
||||
let mut children = vec![token(UNDERSCORE, "_")];
|
||||
children.extend(object_nodes(contents));
|
||||
children.push(token(UNDERSCORE, "_"));
|
||||
node(UNDERLINE, children)
|
||||
}))(input)
|
||||
}
|
||||
|
||||
pub fn italic_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(emphasis(b'/'), |contents| {
|
||||
let mut children = vec![token(SLASH, "/")];
|
||||
children.extend(object_nodes(contents));
|
||||
children.push(token(SLASH, "/"));
|
||||
node(ITALIC, children)
|
||||
}))(input)
|
||||
}
|
||||
|
||||
fn emphasis(marker: u8) -> impl Fn(Input) -> IResult<Input, Input, ()> {
|
||||
move |input: Input| {
|
||||
let bytes = input.as_bytes();
|
||||
|
||||
if bytes.len() < 3 || bytes[0] != marker || bytes[1].is_ascii_whitespace() {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
for idx in memchr_iter(marker, bytes).skip(1) {
|
||||
// contains at least one character
|
||||
if idx == 1 {
|
||||
continue;
|
||||
} else if count(&bytes[1..idx], b'\n') >= 2 {
|
||||
break;
|
||||
} else if validate_marker(idx, input) {
|
||||
return Ok((input.slice(idx + 1..), input.slice(1..idx)));
|
||||
}
|
||||
}
|
||||
|
||||
Err(nom::Err::Error(()))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_marker(pos: usize, text: Input) -> bool {
|
||||
if text.as_bytes()[pos - 1].is_ascii_whitespace() {
|
||||
false
|
||||
} else if let Some(post) = text.as_bytes().get(pos + 1) {
|
||||
[
|
||||
b' ', b'\t', b'\r', b'\n', b'-', b'.', b',', b';', b':', b'!', b'?', b'\'', b')', b'}',
|
||||
b'[',
|
||||
]
|
||||
.contains(post)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Bold, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_bold = to_ast::<Bold>(bold_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_bold("*bold*").syntax,
|
||||
@r###"
|
||||
BOLD@0..6
|
||||
STAR@0..1 "*"
|
||||
TEXT@1..5 "bold"
|
||||
STAR@5..6 "*"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_bold("*bo*ld*").syntax,
|
||||
@r###"
|
||||
BOLD@0..7
|
||||
STAR@0..1 "*"
|
||||
TEXT@1..6 "bo*ld"
|
||||
STAR@6..7 "*"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_bold("*bo\nld*").syntax,
|
||||
@r###"
|
||||
BOLD@0..7
|
||||
STAR@0..1 "*"
|
||||
TEXT@1..6 "bo\nld"
|
||||
STAR@6..7 "*"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(bold_node(("*bold*a", config).into()).is_err());
|
||||
assert!(bold_node(("*bold *", config).into()).is_err());
|
||||
assert!(bold_node(("* bold*", config).into()).is_err());
|
||||
assert!(bold_node(("*b\nol\nd*", config).into()).is_err());
|
||||
assert!(italic_node(("*bold*", config).into()).is_err());
|
||||
}
|
||||
64
src/syntax/fixed_width.rs
Normal file
64
src/syntax/fixed_width.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use nom::{IResult, InputTake};
|
||||
|
||||
use super::{
|
||||
combinator::{blank_lines, debug_assert_lossless, line_ends_iter, node, GreenElement},
|
||||
input::Input,
|
||||
SyntaxKind,
|
||||
};
|
||||
|
||||
fn fixed_width_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let mut start = 0;
|
||||
for i in line_ends_iter(input.as_str()) {
|
||||
let line = &input.s[start..i];
|
||||
let trimmed = line.trim_start();
|
||||
|
||||
if trimmed == ":" || trimmed == ":\n" || trimmed == ":\r\n" || trimmed.starts_with(": ") {
|
||||
start = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if start == 0 {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
let (input, contents) = input.take_split(start);
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
|
||||
let mut children = vec![];
|
||||
children.push(contents.text_token());
|
||||
children.extend(post_blank);
|
||||
|
||||
Ok((input, node(SyntaxKind::FIXED_WIDTH, children)))
|
||||
}
|
||||
|
||||
pub fn fixed_width_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(fixed_width_node_base)(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::FixedWidth, tests::to_ast};
|
||||
|
||||
let to_fixed_width = to_ast::<FixedWidth>(fixed_width_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fixed_width(
|
||||
r#": A
|
||||
:
|
||||
: B
|
||||
: C
|
||||
|
||||
"#
|
||||
).syntax,
|
||||
@r###"
|
||||
FIXED_WIDTH@0..19
|
||||
TEXT@0..14 ": A\n:\n: B\n: C\n"
|
||||
BLANK_LINE@14..15
|
||||
NEW_LINE@14..15 "\n"
|
||||
BLANK_LINE@15..19
|
||||
WHITESPACE@15..19 " "
|
||||
"###
|
||||
);
|
||||
}
|
||||
154
src/syntax/fn_def.rs
Normal file
154
src/syntax/fn_def.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use nom::{
|
||||
bytes::complete::{tag, take_while1},
|
||||
combinator::map,
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
blank_lines, colon_token, debug_assert_lossless, l_bracket_token, r_bracket_token,
|
||||
trim_line_end, GreenElement, NodeBuilder,
|
||||
},
|
||||
input::Input,
|
||||
keyword::affiliated_keyword_nodes,
|
||||
SyntaxKind,
|
||||
};
|
||||
|
||||
#[tracing::instrument(skip(input), fields(input = input.s))]
|
||||
pub fn fn_def_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
affiliated_keyword_nodes,
|
||||
l_bracket_token,
|
||||
tag("fn"),
|
||||
colon_token,
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||
r_bracket_token,
|
||||
trim_line_end,
|
||||
blank_lines,
|
||||
)),
|
||||
|(
|
||||
affiliated_keywords,
|
||||
l_bracket,
|
||||
fn_,
|
||||
colon,
|
||||
label,
|
||||
r_bracket,
|
||||
(content, ws_, nl),
|
||||
post_blank,
|
||||
)| {
|
||||
let mut b = NodeBuilder::new();
|
||||
b.children.extend(affiliated_keywords);
|
||||
b.push(l_bracket);
|
||||
b.text(fn_);
|
||||
b.push(colon);
|
||||
b.text(label);
|
||||
b.push(r_bracket);
|
||||
b.text(content);
|
||||
b.ws(ws_);
|
||||
b.nl(nl);
|
||||
b.children.extend(post_blank);
|
||||
b.finish(SyntaxKind::FN_DEF)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::ParseConfig;
|
||||
use crate::{ast::FnDef, tests::to_ast};
|
||||
|
||||
let to_fn_def = to_ast::<FnDef>(fn_def_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_def("[fn:1] https://orgmode.org").syntax,
|
||||
@r###"
|
||||
FN_DEF@0..26
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..5 "1"
|
||||
R_BRACKET@5..6 "]"
|
||||
TEXT@6..26 " https://orgmode.org"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_def("[fn:word_1] https://orgmode.org").syntax,
|
||||
@r###"
|
||||
FN_DEF@0..31
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..10 "word_1"
|
||||
R_BRACKET@10..11 "]"
|
||||
TEXT@11..31 " https://orgmode.org"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_def("[fn:WORD-1] https://orgmode.org").syntax,
|
||||
@r###"
|
||||
FN_DEF@0..31
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..10 "WORD-1"
|
||||
R_BRACKET@10..11 "]"
|
||||
TEXT@11..31 " https://orgmode.org"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_def("[fn:WORD]").syntax,
|
||||
@r###"
|
||||
FN_DEF@0..9
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..8 "WORD"
|
||||
R_BRACKET@8..9 "]"
|
||||
TEXT@9..9 ""
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_def("[fn:1] In particular, the parser requires stars at column 0 to be\n").syntax,
|
||||
@r###"
|
||||
FN_DEF@0..66
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..5 "1"
|
||||
R_BRACKET@5..6 "]"
|
||||
TEXT@6..65 " In particular, the p ..."
|
||||
NEW_LINE@65..66 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(fn_def_node(("[fn:] https://orgmode.org", config).into()).is_err());
|
||||
assert!(fn_def_node(("[fn:wor d] https://orgmode.org", config).into()).is_err());
|
||||
assert!(fn_def_node(("[fn:WORD https://orgmode.org", config).into()).is_err());
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_def("#+ATTR_poi: 1\n[fn:WORD-1] https://orgmode.org").syntax,
|
||||
@r###"
|
||||
FN_DEF@0..45
|
||||
KEYWORD@0..14
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..10 "ATTR_poi"
|
||||
COLON@10..11 ":"
|
||||
TEXT@11..13 " 1"
|
||||
NEW_LINE@13..14 "\n"
|
||||
L_BRACKET@14..15 "["
|
||||
TEXT@15..17 "fn"
|
||||
COLON@17..18 ":"
|
||||
TEXT@18..24 "WORD-1"
|
||||
R_BRACKET@24..25 "]"
|
||||
TEXT@25..45 " https://orgmode.org"
|
||||
"###
|
||||
);
|
||||
}
|
||||
120
src/syntax/fn_ref.rs
Normal file
120
src/syntax/fn_ref.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
use memchr::memchr2_iter;
|
||||
use nom::{
|
||||
bytes::complete::{tag, take_while},
|
||||
combinator::opt,
|
||||
sequence::tuple,
|
||||
AsBytes, Err, IResult, InputTake,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
colon_token, debug_assert_lossless, l_bracket_token, node, r_bracket_token, GreenElement,
|
||||
},
|
||||
input::Input,
|
||||
object::object_nodes,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn fn_ref_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(fn_ref_node_base)(input)
|
||||
}
|
||||
|
||||
fn fn_ref_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, (l_bracket, fn_, colon, label, definition, r_bracket)) = tuple((
|
||||
l_bracket_token,
|
||||
tag("fn"),
|
||||
colon_token,
|
||||
take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||
opt(tuple((colon_token, balanced_brackets))),
|
||||
r_bracket_token,
|
||||
))(input)?;
|
||||
|
||||
let mut children = vec![l_bracket, fn_.text_token(), colon, label.text_token()];
|
||||
if let Some((colon, definition)) = definition {
|
||||
children.push(colon);
|
||||
children.extend(object_nodes(definition));
|
||||
}
|
||||
children.push(r_bracket);
|
||||
|
||||
Ok((input, node(FN_REF, children)))
|
||||
}
|
||||
|
||||
fn balanced_brackets(input: Input) -> IResult<Input, Input, ()> {
|
||||
let mut pairs = 1;
|
||||
let bytes = input.as_bytes();
|
||||
for i in memchr2_iter(b'[', b']', bytes) {
|
||||
if bytes[i] == b'[' {
|
||||
pairs += 1;
|
||||
} else if pairs != 1 {
|
||||
pairs -= 1;
|
||||
} else {
|
||||
return Ok(input.take_split(i));
|
||||
}
|
||||
}
|
||||
Err(Err::Error(()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::FnRef, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_fn_ref = to_ast::<FnRef>(fn_ref_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_ref("[fn:1]").syntax,
|
||||
@r###"
|
||||
FN_REF@0..6
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..5 "1"
|
||||
R_BRACKET@5..6 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_ref("[fn:1:2]").syntax,
|
||||
@r###"
|
||||
FN_REF@0..8
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..5 "1"
|
||||
COLON@5..6 ":"
|
||||
TEXT@6..7 "2"
|
||||
R_BRACKET@7..8 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_ref("[fn::2]").syntax,
|
||||
@r###"
|
||||
FN_REF@0..7
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..4 ""
|
||||
COLON@4..5 ":"
|
||||
TEXT@5..6 "2"
|
||||
R_BRACKET@6..7 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_fn_ref("[fn::[]]").syntax,
|
||||
@r###"
|
||||
FN_REF@0..8
|
||||
L_BRACKET@0..1 "["
|
||||
TEXT@1..3 "fn"
|
||||
COLON@3..4 ":"
|
||||
TEXT@4..4 ""
|
||||
COLON@4..5 ":"
|
||||
TEXT@5..7 "[]"
|
||||
R_BRACKET@7..8 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(fn_ref_node(("[fn::[]", config).into()).is_err());
|
||||
}
|
||||
350
src/syntax/headline.rs
Normal file
350
src/syntax/headline.rs
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
use memchr::memrchr_iter;
|
||||
use nom::{
|
||||
bytes::complete::take_while1,
|
||||
character::complete::{anychar, space0},
|
||||
combinator::{map, opt, verify},
|
||||
sequence::tuple,
|
||||
AsBytes, IResult, InputLength, InputTake, Slice,
|
||||
};
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
debug_assert_lossless, hash_token, l_bracket_token, line_starts_iter, node,
|
||||
r_bracket_token, token, trim_line_end, GreenElement, NodeBuilder,
|
||||
},
|
||||
drawer::property_drawer_node,
|
||||
element::element_nodes,
|
||||
input::Input,
|
||||
object::object_nodes,
|
||||
planning::planning_node,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn headline_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(headline_node_base)(input)
|
||||
}
|
||||
|
||||
#[instrument(skip(input), fields(input = input.s))]
|
||||
fn headline_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, stars) = headline_stars(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
|
||||
b.token(HEADLINE_STARS, stars);
|
||||
|
||||
let (input, ws) = space0(input)?;
|
||||
b.ws(ws);
|
||||
|
||||
let (input, headline_keyword) = opt(headline_keyword_token)(input)?;
|
||||
|
||||
if let Some((headline_keyword, ws)) = headline_keyword {
|
||||
b.push(headline_keyword);
|
||||
b.ws(ws);
|
||||
}
|
||||
|
||||
let (input, headline_priority) = opt(headline_priority_node)(input)?;
|
||||
|
||||
if let Some((headline_priority, ws)) = headline_priority {
|
||||
b.push(headline_priority);
|
||||
b.ws(ws);
|
||||
}
|
||||
|
||||
let (input, (title_and_tags, ws_, nl)) = trim_line_end(input)?;
|
||||
let (title, tags) = opt(headline_tags_node)(title_and_tags)?;
|
||||
|
||||
if !title.is_empty() {
|
||||
b.push(node(HEADLINE_TITLE, object_nodes(title)));
|
||||
}
|
||||
b.push_opt(tags);
|
||||
b.ws(ws_);
|
||||
b.nl(nl);
|
||||
|
||||
if nl.is_empty() {
|
||||
return Ok((input, b.finish(HEADLINE)));
|
||||
}
|
||||
|
||||
let (input, planning) = opt(planning_node)(input)?;
|
||||
b.push_opt(planning);
|
||||
|
||||
let (input, property_drawer) = opt(property_drawer_node)(input)?;
|
||||
b.push_opt(property_drawer);
|
||||
|
||||
let (input, section) = opt(section_node)(input)?;
|
||||
b.push_opt(section);
|
||||
|
||||
let mut i = input;
|
||||
let current_level = stars.input_len();
|
||||
while !i.is_empty() {
|
||||
let next_level = i.bytes().take_while(|&c| c == b'*').count();
|
||||
|
||||
if next_level <= current_level {
|
||||
break;
|
||||
}
|
||||
|
||||
let (input, headline) = headline_node(i)?;
|
||||
b.push(headline);
|
||||
i = input;
|
||||
}
|
||||
|
||||
Ok((i, b.finish(HEADLINE)))
|
||||
}
|
||||
|
||||
#[instrument(skip(input), fields(input = input.s))]
|
||||
pub fn section_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, section) = section_text(input)?;
|
||||
Ok((input, node(SECTION, element_nodes(section)?)))
|
||||
}
|
||||
|
||||
pub fn section_text(input: Input) -> IResult<Input, Input, ()> {
|
||||
if input.is_empty() {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
for (input, section) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
|
||||
if headline_stars(input).is_ok() {
|
||||
if section.is_empty() {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
return Ok((input, section));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(input.take_split(input.input_len()))
|
||||
}
|
||||
|
||||
#[instrument(skip(input), fields(input = input.s))]
|
||||
fn headline_stars(input: Input) -> IResult<Input, Input, ()> {
|
||||
let bytes = input.as_bytes();
|
||||
let level = bytes.iter().take_while(|&&c| c == b'*').count();
|
||||
|
||||
if level == 0 {
|
||||
Err(nom::Err::Error(()))
|
||||
} else if input.input_len() == level {
|
||||
Ok(input.take_split(level))
|
||||
} else if bytes[level] == b'\n' || bytes[level] == b'\r' || bytes[level] == b' ' {
|
||||
Ok(input.take_split(level))
|
||||
} else {
|
||||
Err(nom::Err::Error(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(input), fields(input = input.s))]
|
||||
fn headline_tags_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
if !input.s.ends_with(':') {
|
||||
return Err(nom::Err::Error(()));
|
||||
};
|
||||
|
||||
let bytes = input.as_bytes();
|
||||
|
||||
// we're going to skip to first colon, so we start from the
|
||||
// second last character
|
||||
let mut i = input.input_len() - 1;
|
||||
let mut can_not_be_ws = true;
|
||||
let mut children = vec![token(COLON, ":")];
|
||||
|
||||
for ii in memrchr_iter(b':', bytes).skip(1) {
|
||||
let item = &bytes[ii + 1..i];
|
||||
|
||||
if item.is_empty() {
|
||||
children.push(token(COLON, ":"));
|
||||
can_not_be_ws = false;
|
||||
i = ii;
|
||||
} else if item
|
||||
.iter()
|
||||
.all(|&c| c.is_ascii_alphanumeric() || c == b'_' || c == b'@' || c == b'#' || c == b'%')
|
||||
{
|
||||
children.push(input.slice(ii + 1..i).text_token());
|
||||
children.push(token(COLON, ":"));
|
||||
can_not_be_ws = false;
|
||||
i = ii;
|
||||
} else if item.iter().all(|&c| c == b' ' || c == b'\t') && !can_not_be_ws {
|
||||
children.push(input.slice(ii + 1..i).ws_token());
|
||||
children.push(token(COLON, ":"));
|
||||
can_not_be_ws = true;
|
||||
i = ii;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if children.len() == 1 {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
if i != 0 && bytes[i - 1] != b' ' && bytes[i - 1] != b'\t' {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
// we parse headline tag from right to left,
|
||||
// so we need to reverse the result after it finishes
|
||||
children.reverse();
|
||||
|
||||
Ok((input.slice(0..i), node(HEADLINE_TAGS, children)))
|
||||
}
|
||||
|
||||
fn headline_keyword_token(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
|
||||
let (input, word) = verify(
|
||||
take_while1(|c: char| !c.is_ascii_whitespace()),
|
||||
|input: &Input| {
|
||||
let Input { c, s } = input;
|
||||
c.todo_keywords.0.iter().any(|k| k == s) || c.todo_keywords.1.iter().any(|k| k == s)
|
||||
},
|
||||
)(input)?;
|
||||
|
||||
let (input, ws) = space0(input)?;
|
||||
|
||||
Ok((input, (word.token(HEADLINE_KEYWORD), ws)))
|
||||
}
|
||||
|
||||
fn headline_priority_node(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
|
||||
let (input, node) = map(
|
||||
tuple((l_bracket_token, hash_token, anychar, r_bracket_token)),
|
||||
|(l_bracket, hash, char, r_bracket)| {
|
||||
node(
|
||||
HEADLINE_PRIORITY,
|
||||
[l_bracket, hash, token(TEXT, &char.to_string()), r_bracket],
|
||||
)
|
||||
},
|
||||
)(input)?;
|
||||
|
||||
let (input, ws) = space0(input)?;
|
||||
|
||||
Ok((input, (node, ws)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Headline, tests::to_ast};
|
||||
|
||||
let to_headline = to_ast::<Headline>(headline_node);
|
||||
|
||||
let hdl = to_headline("* foo");
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
hdl.syntax,
|
||||
@r###"
|
||||
HEADLINE@0..5
|
||||
HEADLINE_STARS@0..1 "*"
|
||||
WHITESPACE@1..2 " "
|
||||
HEADLINE_TITLE@2..5
|
||||
TEXT@2..5 "foo"
|
||||
"###
|
||||
);
|
||||
|
||||
let hdl = to_headline("* foo\n\n** bar");
|
||||
insta::assert_debug_snapshot!(
|
||||
hdl.syntax,
|
||||
@r###"
|
||||
HEADLINE@0..13
|
||||
HEADLINE_STARS@0..1 "*"
|
||||
WHITESPACE@1..2 " "
|
||||
HEADLINE_TITLE@2..5
|
||||
TEXT@2..5 "foo"
|
||||
NEW_LINE@5..6 "\n"
|
||||
SECTION@6..7
|
||||
PARAGRAPH@6..7
|
||||
BLANK_LINE@6..7
|
||||
NEW_LINE@6..7 "\n"
|
||||
HEADLINE@7..13
|
||||
HEADLINE_STARS@7..9 "**"
|
||||
WHITESPACE@9..10 " "
|
||||
HEADLINE_TITLE@10..13
|
||||
TEXT@10..13 "bar"
|
||||
"###
|
||||
);
|
||||
|
||||
let hdl = to_headline("* TODO foo\nbar\n** baz\n");
|
||||
assert_eq!(hdl.level(), Some(1));
|
||||
assert_eq!(hdl.keyword().as_ref().map(|x| x.text()), Some("TODO"));
|
||||
insta::assert_debug_snapshot!(
|
||||
hdl.syntax,
|
||||
@r###"
|
||||
HEADLINE@0..22
|
||||
HEADLINE_STARS@0..1 "*"
|
||||
WHITESPACE@1..2 " "
|
||||
HEADLINE_KEYWORD@2..6 "TODO"
|
||||
WHITESPACE@6..7 " "
|
||||
HEADLINE_TITLE@7..10
|
||||
TEXT@7..10 "foo"
|
||||
NEW_LINE@10..11 "\n"
|
||||
SECTION@11..15
|
||||
PARAGRAPH@11..15
|
||||
TEXT@11..15 "bar\n"
|
||||
HEADLINE@15..22
|
||||
HEADLINE_STARS@15..17 "**"
|
||||
WHITESPACE@17..18 " "
|
||||
HEADLINE_TITLE@18..21
|
||||
TEXT@18..21 "baz"
|
||||
NEW_LINE@21..22 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
let hdl = to_headline("** [#A] foo\n* baz");
|
||||
assert_eq!(hdl.level(), Some(2));
|
||||
assert_eq!(
|
||||
hdl.priority().unwrap().text_string().unwrap(),
|
||||
"A".to_string()
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
hdl.syntax,
|
||||
@r###"
|
||||
HEADLINE@0..12
|
||||
HEADLINE_STARS@0..2 "**"
|
||||
WHITESPACE@2..3 " "
|
||||
HEADLINE_PRIORITY@3..7
|
||||
L_BRACKET@3..4 "["
|
||||
HASH@4..5 "#"
|
||||
TEXT@5..6 "A"
|
||||
R_BRACKET@6..7 "]"
|
||||
WHITESPACE@7..8 " "
|
||||
HEADLINE_TITLE@8..11
|
||||
TEXT@8..11 "foo"
|
||||
NEW_LINE@11..12 "\n"
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_15_16() {
|
||||
use crate::{ast::Headline, tests::to_ast};
|
||||
|
||||
let to_headline = to_ast::<Headline>(headline_node);
|
||||
|
||||
let tags = to_headline("* a ::").tags().unwrap();
|
||||
assert_eq!(tags.iter().count(), 0);
|
||||
|
||||
// let tags = to_headline("* a :(:").tags().unwrap();
|
||||
// assert_eq!(tags.iter().count(), 0);
|
||||
|
||||
let tags = to_headline("* a \t:_:").tags().unwrap();
|
||||
assert_eq!(
|
||||
vec!["_".to_string()],
|
||||
tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let tags = to_headline("* a \t :@:").tags().unwrap();
|
||||
assert_eq!(
|
||||
vec!["@".to_string()],
|
||||
tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let tags = to_headline("* a :#:").tags().unwrap();
|
||||
assert_eq!(
|
||||
vec!["#".to_string()],
|
||||
tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let tags = to_headline("* a\t :%:").tags().unwrap();
|
||||
assert_eq!(
|
||||
vec!["%".to_string()],
|
||||
tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
// let tags = to_headline("* a :余:").tags().unwrap();
|
||||
// assert_eq!(
|
||||
// vec!["余".to_string()],
|
||||
// tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
|
||||
// );
|
||||
}
|
||||
126
src/syntax/inline_call.rs
Normal file
126
src/syntax/inline_call.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
use nom::{
|
||||
bytes::complete::{tag, take_till},
|
||||
combinator::{map, opt},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
debug_assert_lossless, l_bracket_token, l_parens_token, node, r_bracket_token,
|
||||
r_parens_token, GreenElement,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind,
|
||||
};
|
||||
|
||||
pub fn inline_call_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
tag("call_"),
|
||||
take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')'),
|
||||
opt(tuple((
|
||||
l_bracket_token,
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
r_bracket_token,
|
||||
))),
|
||||
l_parens_token,
|
||||
take_till(|c| c == ')' || c == '\n'),
|
||||
r_parens_token,
|
||||
opt(tuple((
|
||||
l_bracket_token,
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
r_bracket_token,
|
||||
))),
|
||||
)),
|
||||
|(call, name, inside_header, l_paren, arguments, r_paren, end_header)| {
|
||||
let mut children = vec![call.text_token()];
|
||||
children.push(name.text_token());
|
||||
if let Some((l_bracket, header, r_bracket)) = inside_header {
|
||||
children.push(l_bracket);
|
||||
children.push(header.text_token());
|
||||
children.push(r_bracket);
|
||||
}
|
||||
children.push(l_paren);
|
||||
children.push(arguments.text_token());
|
||||
children.push(r_paren);
|
||||
if let Some((l_bracket, header, r_bracket)) = end_header {
|
||||
children.push(l_bracket);
|
||||
children.push(header.text_token());
|
||||
children.push(r_bracket);
|
||||
}
|
||||
node(SyntaxKind::INLINE_CALL, children)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::InlineCall, tests::to_ast};
|
||||
|
||||
let to_inline_call = to_ast::<InlineCall>(inline_call_node);
|
||||
|
||||
let call = to_inline_call("call_square(4)");
|
||||
insta::assert_debug_snapshot!(
|
||||
call.syntax,
|
||||
@r###"
|
||||
INLINE_CALL@0..14
|
||||
TEXT@0..5 "call_"
|
||||
TEXT@5..11 "square"
|
||||
L_PARENS@11..12 "("
|
||||
TEXT@12..13 "4"
|
||||
R_PARENS@13..14 ")"
|
||||
"###
|
||||
);
|
||||
|
||||
let call = to_inline_call("call_square[:results output](4)");
|
||||
insta::assert_debug_snapshot!(
|
||||
call.syntax,
|
||||
@r###"
|
||||
INLINE_CALL@0..31
|
||||
TEXT@0..5 "call_"
|
||||
TEXT@5..11 "square"
|
||||
L_BRACKET@11..12 "["
|
||||
TEXT@12..27 ":results output"
|
||||
R_BRACKET@27..28 "]"
|
||||
L_PARENS@28..29 "("
|
||||
TEXT@29..30 "4"
|
||||
R_PARENS@30..31 ")"
|
||||
"###
|
||||
);
|
||||
|
||||
let call = to_inline_call("call_square(4)[:results html]");
|
||||
insta::assert_debug_snapshot!(
|
||||
call.syntax,
|
||||
@r###"
|
||||
INLINE_CALL@0..29
|
||||
TEXT@0..5 "call_"
|
||||
TEXT@5..11 "square"
|
||||
L_PARENS@11..12 "("
|
||||
TEXT@12..13 "4"
|
||||
R_PARENS@13..14 ")"
|
||||
L_BRACKET@14..15 "["
|
||||
TEXT@15..28 ":results html"
|
||||
R_BRACKET@28..29 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
let call = to_inline_call("call_square[:results output](4)[:results html]");
|
||||
insta::assert_debug_snapshot!(
|
||||
call.syntax,
|
||||
@r###"
|
||||
INLINE_CALL@0..46
|
||||
TEXT@0..5 "call_"
|
||||
TEXT@5..11 "square"
|
||||
L_BRACKET@11..12 "["
|
||||
TEXT@12..27 ":results output"
|
||||
R_BRACKET@27..28 "]"
|
||||
L_PARENS@28..29 "("
|
||||
TEXT@29..30 "4"
|
||||
R_PARENS@30..31 ")"
|
||||
L_BRACKET@31..32 "["
|
||||
TEXT@32..45 ":results html"
|
||||
R_BRACKET@45..46 "]"
|
||||
"###
|
||||
);
|
||||
}
|
||||
84
src/syntax/inline_src.rs
Normal file
84
src/syntax/inline_src.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use nom::{
|
||||
bytes::complete::{tag, take_till, take_while1},
|
||||
combinator::{map, opt},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
debug_assert_lossless, l_bracket_token, l_curly_token, node, r_bracket_token,
|
||||
r_curly_token, GreenElement,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind,
|
||||
};
|
||||
|
||||
pub fn inline_src_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
tag("src_"),
|
||||
take_while1(|c: char| !c.is_ascii_whitespace() && c != '[' && c != '{'),
|
||||
opt(tuple((
|
||||
l_bracket_token,
|
||||
take_till(|c| c == '\n' || c == ']'),
|
||||
r_bracket_token,
|
||||
))),
|
||||
l_curly_token,
|
||||
take_till(|c| c == '\n' || c == '}'),
|
||||
r_curly_token,
|
||||
)),
|
||||
|(src, lang, options, l_curly, body, r_curly)| {
|
||||
let mut children = vec![src.text_token(), lang.text_token()];
|
||||
if let Some((l_bracket, options, r_bracket)) = options {
|
||||
children.push(l_bracket);
|
||||
children.push(options.text_token());
|
||||
children.push(r_bracket);
|
||||
}
|
||||
children.push(l_curly);
|
||||
children.push(body.text_token());
|
||||
children.push(r_curly);
|
||||
node(SyntaxKind::INLINE_SRC, children)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::InlineSrc, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_inline_src = to_ast::<InlineSrc>(inline_src_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_inline_src("src_C{int a = 0;}").syntax,
|
||||
@r###"
|
||||
INLINE_SRC@0..17
|
||||
TEXT@0..4 "src_"
|
||||
TEXT@4..5 "C"
|
||||
L_CURLY@5..6 "{"
|
||||
TEXT@6..16 "int a = 0;"
|
||||
R_CURLY@16..17 "}"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_inline_src("src_xml[:exports code]{<tag>text</tag>}").syntax,
|
||||
@r###"
|
||||
INLINE_SRC@0..39
|
||||
TEXT@0..4 "src_"
|
||||
TEXT@4..7 "xml"
|
||||
L_BRACKET@7..8 "["
|
||||
TEXT@8..21 ":exports code"
|
||||
R_BRACKET@21..22 "]"
|
||||
L_CURLY@22..23 "{"
|
||||
TEXT@23..38 "<tag>text</tag>"
|
||||
R_CURLY@38..39 "}"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(inline_src_node(("src_xml[:exports code]{<tag>text</tag>", config).into()).is_err());
|
||||
assert!(inline_src_node(("src_[:exports code]{<tag>text</tag>}", config).into()).is_err());
|
||||
assert!(inline_src_node(("src_xml[:exports code]", config).into()).is_err());
|
||||
}
|
||||
250
src/syntax/input.rs
Normal file
250
src/syntax/input.rs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
use nom::{
|
||||
error::{ErrorKind, ParseError},
|
||||
AsBytes, Compare, CompareResult, Err, FindSubstring, IResult, InputIter, InputLength,
|
||||
InputTake, InputTakeAtPosition, Needed, Offset, Slice,
|
||||
};
|
||||
use std::{
|
||||
ops::{Range, RangeFrom, RangeFull, RangeTo},
|
||||
str::{Bytes, CharIndices, Chars},
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{token, GreenElement},
|
||||
SyntaxKind,
|
||||
};
|
||||
use crate::config::ParseConfig;
|
||||
|
||||
/// A custom Input struct
|
||||
///
|
||||
/// It helps us to pass the `ParseConfig` all the way down to each parsers
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Input<'a> {
|
||||
pub(crate) s: &'a str,
|
||||
pub(crate) c: &'a ParseConfig,
|
||||
}
|
||||
|
||||
impl<'a> Input<'a> {
|
||||
#[inline]
|
||||
pub(crate) fn of(&self, i: &'a str) -> Input<'a> {
|
||||
Input { s: i, c: self.c }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &'a str {
|
||||
self.s
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.s.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn token(&self, kind: SyntaxKind) -> GreenElement {
|
||||
token(kind, self.s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn text_token(&self) -> GreenElement {
|
||||
token(SyntaxKind::TEXT, self.s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn ws_token(&self) -> GreenElement {
|
||||
token(SyntaxKind::WHITESPACE, self.s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn nl_token(&self) -> GreenElement {
|
||||
token(SyntaxKind::NEW_LINE, self.s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bytes(&self) -> Bytes {
|
||||
self.s.bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<(&'a str, &'a ParseConfig)> for Input<'a> {
|
||||
fn from(value: (&'a str, &'a ParseConfig)) -> Self {
|
||||
Input {
|
||||
s: value.0,
|
||||
c: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsBytes for Input<'a> {
|
||||
#[inline]
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
self.s.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Slice<Range<usize>> for Input<'a> {
|
||||
fn slice(&self, range: Range<usize>) -> Self {
|
||||
self.of(self.s.slice(range))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Slice<RangeTo<usize>> for Input<'a> {
|
||||
fn slice(&self, range: RangeTo<usize>) -> Self {
|
||||
self.of(self.s.slice(range))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Slice<RangeFrom<usize>> for Input<'a> {
|
||||
fn slice(&self, range: RangeFrom<usize>) -> Self {
|
||||
self.of(self.s.slice(range))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Slice<RangeFull> for Input<'a> {
|
||||
fn slice(&self, range: RangeFull) -> Self {
|
||||
self.of(self.s.slice(range))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> FindSubstring<&'b str> for Input<'a> {
|
||||
fn find_substring(&self, substr: &str) -> Option<usize> {
|
||||
self.s.find(substr)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Compare<&'b str> for Input<'a> {
|
||||
#[inline]
|
||||
fn compare(&self, t: &'b str) -> CompareResult {
|
||||
self.s.compare(t)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn compare_no_case(&self, t: &'b str) -> CompareResult {
|
||||
self.s.compare_no_case(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InputLength for Input<'a> {
|
||||
#[inline]
|
||||
fn input_len(&self) -> usize {
|
||||
self.s.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InputIter for Input<'a> {
|
||||
type Item = char;
|
||||
type Iter = CharIndices<'a>;
|
||||
type IterElem = Chars<'a>;
|
||||
#[inline]
|
||||
fn iter_indices(&self) -> Self::Iter {
|
||||
self.s.char_indices()
|
||||
}
|
||||
#[inline]
|
||||
fn iter_elements(&self) -> Self::IterElem {
|
||||
self.s.chars()
|
||||
}
|
||||
fn position<P>(&self, predicate: P) -> Option<usize>
|
||||
where
|
||||
P: Fn(Self::Item) -> bool,
|
||||
{
|
||||
self.s.position(predicate)
|
||||
}
|
||||
#[inline]
|
||||
fn slice_index(&self, count: usize) -> Result<usize, Needed> {
|
||||
self.s.slice_index(count)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InputTake for Input<'a> {
|
||||
#[inline]
|
||||
fn take(&self, count: usize) -> Self {
|
||||
let s = self.s.take(count);
|
||||
self.of(s)
|
||||
}
|
||||
#[inline]
|
||||
fn take_split(&self, count: usize) -> (Self, Self) {
|
||||
let (l, r) = self.s.take_split(count);
|
||||
(self.of(l), self.of(r))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InputTakeAtPosition for Input<'a> {
|
||||
type Item = char;
|
||||
|
||||
#[inline]
|
||||
fn split_at_position<P, E: ParseError<Self>>(&self, predicate: P) -> IResult<Self, Self, E>
|
||||
where
|
||||
P: Fn(Self::Item) -> bool,
|
||||
{
|
||||
match self.s.split_at_position::<_, (&str, ErrorKind)>(predicate) {
|
||||
Ok((l, r)) => Ok((self.of(l), self.of(r))),
|
||||
Err(Err::Error((i, kind))) => Err(Err::Error(E::from_error_kind(self.of(i), kind))),
|
||||
Err(Err::Failure((i, kind))) => Err(Err::Failure(E::from_error_kind(self.of(i), kind))),
|
||||
Err(Err::Incomplete(x)) => Err(Err::Incomplete(x)),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn split_at_position1<P, E: ParseError<Self>>(
|
||||
&self,
|
||||
predicate: P,
|
||||
e: ErrorKind,
|
||||
) -> IResult<Self, Self, E>
|
||||
where
|
||||
P: Fn(Self::Item) -> bool,
|
||||
{
|
||||
match self
|
||||
.s
|
||||
.split_at_position1::<_, (&str, ErrorKind)>(predicate, e)
|
||||
{
|
||||
Ok((l, r)) => Ok((self.of(l), self.of(r))),
|
||||
Err(Err::Error((i, kind))) => Err(Err::Error(E::from_error_kind(self.of(i), kind))),
|
||||
Err(Err::Failure((i, kind))) => Err(Err::Failure(E::from_error_kind(self.of(i), kind))),
|
||||
Err(Err::Incomplete(x)) => Err(Err::Incomplete(x)),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn split_at_position_complete<P, E: ParseError<Self>>(
|
||||
&self,
|
||||
predicate: P,
|
||||
) -> IResult<Self, Self, E>
|
||||
where
|
||||
P: Fn(Self::Item) -> bool,
|
||||
{
|
||||
match self
|
||||
.s
|
||||
.split_at_position_complete::<_, (&str, ErrorKind)>(predicate)
|
||||
{
|
||||
Ok((l, r)) => Ok((self.of(l), self.of(r))),
|
||||
Err(Err::Error((i, kind))) => Err(Err::Error(E::from_error_kind(self.of(i), kind))),
|
||||
Err(Err::Failure((i, kind))) => Err(Err::Failure(E::from_error_kind(self.of(i), kind))),
|
||||
Err(Err::Incomplete(x)) => Err(Err::Incomplete(x)),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn split_at_position1_complete<P, E: ParseError<Self>>(
|
||||
&self,
|
||||
predicate: P,
|
||||
e: ErrorKind,
|
||||
) -> IResult<Self, Self, E>
|
||||
where
|
||||
P: Fn(Self::Item) -> bool,
|
||||
{
|
||||
match self
|
||||
.s
|
||||
.split_at_position1_complete::<_, (&str, ErrorKind)>(predicate, e)
|
||||
{
|
||||
Ok((l, r)) => Ok((self.of(l), self.of(r))),
|
||||
Err(Err::Error((i, kind))) => Err(Err::Error(E::from_error_kind(self.of(i), kind))),
|
||||
Err(Err::Failure((i, kind))) => Err(Err::Failure(E::from_error_kind(self.of(i), kind))),
|
||||
Err(Err::Incomplete(x)) => Err(Err::Incomplete(x)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Offset for Input<'a> {
|
||||
fn offset(&self, second: &Self) -> usize {
|
||||
self.s.offset(second.s)
|
||||
}
|
||||
}
|
||||
215
src/syntax/keyword.rs
Normal file
215
src/syntax/keyword.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
use nom::{
|
||||
bytes::complete::take_till,
|
||||
character::complete::space0,
|
||||
combinator::{cond, opt},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
blank_lines, colon_token, debug_assert_lossless, hash_plus_token, l_bracket_token,
|
||||
r_bracket_token, trim_line_end, GreenElement, NodeBuilder,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind,
|
||||
};
|
||||
|
||||
pub fn keyword_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(keyword_node_base)(input)
|
||||
}
|
||||
|
||||
fn keyword_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, (ws, hash_plus, key)) = tuple((
|
||||
space0,
|
||||
hash_plus_token,
|
||||
take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '['),
|
||||
))(input)?;
|
||||
|
||||
let is_babel_call = key.s.eq_ignore_ascii_case("CALL");
|
||||
|
||||
let (input, optional) = cond(
|
||||
!is_babel_call,
|
||||
opt(tuple((
|
||||
l_bracket_token,
|
||||
take_till(|c| c == ']' || c == '\n'),
|
||||
r_bracket_token,
|
||||
))),
|
||||
)(input)?;
|
||||
|
||||
let (input, (colon, (value, ws_, nl), post_blank)) =
|
||||
tuple((colon_token, trim_line_end, blank_lines))(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
|
||||
b.ws(ws);
|
||||
b.push(hash_plus);
|
||||
b.text(key);
|
||||
if let Some(Some((l_bracket, optional, r_bracket))) = optional {
|
||||
b.children
|
||||
.extend([l_bracket, optional.text_token(), r_bracket]);
|
||||
}
|
||||
b.push(colon);
|
||||
b.ws(ws_);
|
||||
b.text(value);
|
||||
b.nl(nl);
|
||||
b.children.extend(post_blank);
|
||||
|
||||
Ok((
|
||||
input,
|
||||
b.finish(if is_babel_call {
|
||||
SyntaxKind::BABEL_CALL
|
||||
} else {
|
||||
SyntaxKind::KEYWORD
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn affiliated_keyword_nodes(input: Input) -> IResult<Input, Vec<GreenElement>, ()> {
|
||||
use rowan::NodeOrToken;
|
||||
|
||||
let mut children = vec![];
|
||||
let mut i = input;
|
||||
|
||||
while !i.is_empty() {
|
||||
let Ok((input, keyword)) = keyword_node(i) else {
|
||||
break;
|
||||
};
|
||||
i = input;
|
||||
|
||||
let Some(node) = keyword.as_node() else {
|
||||
return Err(nom::Err::Error(()));
|
||||
};
|
||||
|
||||
// find the first text token in children
|
||||
let Some(NodeOrToken::Token(token)) = node
|
||||
.children()
|
||||
.into_iter()
|
||||
.find(|t| t.kind() == SyntaxKind::TEXT.into())
|
||||
else {
|
||||
return Err(nom::Err::Error(()));
|
||||
};
|
||||
|
||||
let text = token.text();
|
||||
|
||||
if input.c.affiliated_keywords.iter().all(|w| w != text) && !text.starts_with("ATTR_") {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
children.push(keyword);
|
||||
}
|
||||
|
||||
Ok((i, children))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{
|
||||
ast::{BabelCall, Keyword},
|
||||
tests::to_ast,
|
||||
ParseConfig,
|
||||
};
|
||||
|
||||
let to_keyword = to_ast::<Keyword>(keyword_node);
|
||||
|
||||
let to_babel_call = to_ast::<BabelCall>(keyword_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_keyword("#+KEY:").syntax,
|
||||
@r###"
|
||||
KEYWORD@0..6
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..5 "KEY"
|
||||
COLON@5..6 ":"
|
||||
TEXT@6..6 ""
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_keyword("#+KEY: VALUE").syntax,
|
||||
@r###"
|
||||
KEYWORD@0..12
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..5 "KEY"
|
||||
COLON@5..6 ":"
|
||||
TEXT@6..12 " VALUE"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_keyword("#+K_E_Y: VALUE").syntax,
|
||||
@r###"
|
||||
KEYWORD@0..14
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..7 "K_E_Y"
|
||||
COLON@7..8 ":"
|
||||
TEXT@8..14 " VALUE"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_keyword("#+KEY:VALUE\n").syntax,
|
||||
@r###"
|
||||
KEYWORD@0..12
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..5 "KEY"
|
||||
COLON@5..6 ":"
|
||||
TEXT@6..11 "VALUE"
|
||||
NEW_LINE@11..12 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_keyword("#+RESULTS:").syntax,
|
||||
@r###"
|
||||
KEYWORD@0..10
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..9 "RESULTS"
|
||||
COLON@9..10 ":"
|
||||
TEXT@10..10 ""
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_keyword("#+ATTR_LATEX: :width 5cm\n").syntax,
|
||||
@r###"
|
||||
KEYWORD@0..25
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..12 "ATTR_LATEX"
|
||||
COLON@12..13 ":"
|
||||
TEXT@13..24 " :width 5cm"
|
||||
NEW_LINE@24..25 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_babel_call("#+CALL: double(n=4)").syntax,
|
||||
@r###"
|
||||
BABEL_CALL@0..19
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..6 "CALL"
|
||||
COLON@6..7 ":"
|
||||
TEXT@7..19 " double(n=4)"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_keyword("#+CAPTION[Short caption]: Longer caption.").syntax,
|
||||
@r###"
|
||||
KEYWORD@0..41
|
||||
HASH_PLUS@0..2 "#+"
|
||||
TEXT@2..9 "CAPTION"
|
||||
L_BRACKET@9..10 "["
|
||||
TEXT@10..23 "Short caption"
|
||||
R_BRACKET@23..24 "]"
|
||||
COLON@24..25 ":"
|
||||
TEXT@25..41 " Longer caption."
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(keyword_node(("#+KE Y: VALUE", config).into()).is_err());
|
||||
assert!(keyword_node(("#+CALL[option]: VALUE", config).into()).is_err());
|
||||
assert!(keyword_node(("#+ KEY: VALUE", config).into()).is_err());
|
||||
}
|
||||
89
src/syntax/link.rs
Normal file
89
src/syntax/link.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
use nom::{
|
||||
bytes::complete::take_while,
|
||||
combinator::{map, opt},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
debug_assert_lossless, l_bracket2_token, l_bracket_token, node, r_bracket2_token,
|
||||
r_bracket_token, GreenElement,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn link_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
l_bracket2_token,
|
||||
take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'),
|
||||
opt(tuple((
|
||||
r_bracket_token,
|
||||
l_bracket_token,
|
||||
take_while(|c: char| c != '[' && c != ']'),
|
||||
))),
|
||||
r_bracket2_token,
|
||||
)),
|
||||
|(l_bracket2, path, desc, r_bracket2)| {
|
||||
let mut children = vec![l_bracket2, path.token(LINK_PATH)];
|
||||
|
||||
if let Some((r_bracket, l_bracket, desc)) = desc {
|
||||
children.extend([r_bracket, l_bracket, desc.text_token()]);
|
||||
}
|
||||
|
||||
children.push(r_bracket2);
|
||||
|
||||
node(LINK, children)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Link, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_link = to_ast::<Link>(link_node);
|
||||
|
||||
let link = to_link("[[#id]]");
|
||||
assert_eq!(link.path().as_ref().map(|x| x.text()), Some("#id"));
|
||||
insta::assert_debug_snapshot!(
|
||||
link.syntax,
|
||||
@r###"
|
||||
LINK@0..7
|
||||
L_BRACKET2@0..2 "[["
|
||||
LINK_PATH@2..5 "#id"
|
||||
R_BRACKET2@5..7 "]]"
|
||||
"###
|
||||
);
|
||||
|
||||
let link = to_link("[[#id][desc]]");
|
||||
insta::assert_debug_snapshot!(
|
||||
link.syntax,
|
||||
@r###"
|
||||
LINK@0..13
|
||||
L_BRACKET2@0..2 "[["
|
||||
LINK_PATH@2..5 "#id"
|
||||
R_BRACKET@5..6 "]"
|
||||
L_BRACKET@6..7 "["
|
||||
TEXT@7..11 "desc"
|
||||
R_BRACKET2@11..13 "]]"
|
||||
"###
|
||||
);
|
||||
|
||||
let link = to_link("[[file:/home/dominik/images/jupiter.jpg]]");
|
||||
insta::assert_debug_snapshot!(
|
||||
link.syntax,
|
||||
@r###"
|
||||
LINK@0..41
|
||||
L_BRACKET2@0..2 "[["
|
||||
LINK_PATH@2..39 "file:/home/dominik/im ..."
|
||||
R_BRACKET2@39..41 "]]"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(link_node(("[[#id][desc]", config).into()).is_err());
|
||||
}
|
||||
583
src/syntax/list.rs
Normal file
583
src/syntax/list.rs
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
use memchr::{memchr, memchr2};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take},
|
||||
character::complete::{alphanumeric1, digit1, space0},
|
||||
combinator::{cond, map, opt, recognize, verify},
|
||||
sequence::{preceded, tuple},
|
||||
AsBytes, IResult, InputLength, InputTake,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
at_token, blank_lines, colon2_token, debug_assert_lossless, l_bracket_token,
|
||||
line_starts_iter, node, r_bracket_token, GreenElement,
|
||||
},
|
||||
element::element_node,
|
||||
input::Input,
|
||||
object::object_nodes,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn list_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(list_node_base)(input)
|
||||
}
|
||||
|
||||
fn list_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, first_indent) = space0(input)?;
|
||||
let (input, first_item) = list_item_node(first_indent, input)?;
|
||||
|
||||
let mut children = vec![first_item];
|
||||
|
||||
let mut input = input;
|
||||
while !input.is_empty() {
|
||||
let (input_, indent) = space0(input)?;
|
||||
|
||||
if indent.input_len() != first_indent.input_len() {
|
||||
break;
|
||||
}
|
||||
|
||||
if let Ok((input_, list_item)) = list_item_node(indent, input_) {
|
||||
children.push(list_item);
|
||||
input = input_;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
|
||||
children.extend(post_blank);
|
||||
|
||||
Ok((input, node(LIST, children)))
|
||||
}
|
||||
|
||||
fn list_item_node<'a>(indent: Input<'a>, input: Input<'a>) -> IResult<Input<'a>, GreenElement, ()> {
|
||||
let (input, bullet) = recognize(tuple((
|
||||
alt((
|
||||
tag("+"),
|
||||
tag("*"),
|
||||
tag("-"),
|
||||
preceded(digit1, tag(".")),
|
||||
preceded(digit1, tag(")")),
|
||||
)),
|
||||
space0,
|
||||
)))(input)?;
|
||||
|
||||
// bullet must ends with whitespace,
|
||||
if !(bullet
|
||||
.s
|
||||
.bytes()
|
||||
.last()
|
||||
.map(|b| b == b' ' || b == b'\t')
|
||||
.unwrap_or(true)
|
||||
// or input should be a line end
|
||||
|| input
|
||||
.s
|
||||
.bytes()
|
||||
.next()
|
||||
.map(|b| b == b'\r' || b == b'\n')
|
||||
.unwrap_or(true))
|
||||
{
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
let is_ordered = bullet.s.starts_with(|c: char| c.is_ascii_digit());
|
||||
let (input, counter) = opt(list_item_counter)(input)?;
|
||||
let (input, checkbox) = opt(list_item_checkbox)(input)?;
|
||||
let (input, tag) = cond(!is_ordered, opt(list_item_tag))(input)?;
|
||||
let (input, content) = list_item_content_node(input, indent.input_len())?;
|
||||
|
||||
let mut children = vec![
|
||||
indent.token(LIST_ITEM_INDENT),
|
||||
bullet.token(LIST_ITEM_BULLET),
|
||||
];
|
||||
|
||||
if let Some((counter, ws)) = counter {
|
||||
children.extend([counter, ws.ws_token()]);
|
||||
}
|
||||
if let Some((checkbox, ws)) = checkbox {
|
||||
children.extend([checkbox, ws.ws_token()]);
|
||||
}
|
||||
if let Some(Some((tag, ws))) = tag {
|
||||
children.extend([tag, ws.ws_token()]);
|
||||
}
|
||||
|
||||
children.push(content);
|
||||
|
||||
Ok((input, node(LIST_ITEM, children)))
|
||||
}
|
||||
|
||||
fn list_item_counter(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
|
||||
let (input, node) = map(
|
||||
tuple((l_bracket_token, at_token, alphanumeric1, r_bracket_token)),
|
||||
|(l_bracket, at, char, r_bracket)| {
|
||||
node(
|
||||
LIST_ITEM_COUNTER,
|
||||
[l_bracket, at, char.text_token(), r_bracket],
|
||||
)
|
||||
},
|
||||
)(input)?;
|
||||
|
||||
let (input, ws) = space0(input)?;
|
||||
|
||||
Ok((input, (node, ws)))
|
||||
}
|
||||
|
||||
fn list_item_checkbox(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
|
||||
let (input, node) = map(
|
||||
tuple((
|
||||
l_bracket_token,
|
||||
verify(take(1usize), |input: &Input| {
|
||||
input.s == " " || input.s == "X" || input.s == "-"
|
||||
}),
|
||||
r_bracket_token,
|
||||
)),
|
||||
|(l_bracket, char, r_bracket)| {
|
||||
node(
|
||||
LIST_ITEM_CHECK_BOX,
|
||||
[l_bracket, char.text_token(), r_bracket],
|
||||
)
|
||||
},
|
||||
)(input)?;
|
||||
|
||||
let (input, ws) = space0(input)?;
|
||||
|
||||
Ok((input, (node, ws)))
|
||||
}
|
||||
|
||||
fn list_item_tag(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
|
||||
let bytes = input.as_bytes();
|
||||
|
||||
let (input, tag) = match memchr2(b'\n', b':', bytes) {
|
||||
Some(idx) if idx > 0 && bytes[idx] == b':' => input.take_split(idx),
|
||||
_ => return Err(nom::Err::Error(())),
|
||||
};
|
||||
let (input, ws) = space0(input)?;
|
||||
let (input, colon2) = colon2_token(input)?;
|
||||
|
||||
let mut children = object_nodes(tag);
|
||||
children.push(colon2);
|
||||
|
||||
Ok((input, (node(LIST_ITEM_TAG, children), ws)))
|
||||
}
|
||||
|
||||
fn list_item_content_node(input: Input, indent: usize) -> IResult<Input, GreenElement, ()> {
|
||||
if memchr(b'\n', input.as_bytes()).is_none() {
|
||||
return Ok((
|
||||
input.of(""),
|
||||
node(LIST_ITEM_CONTENT, [node(PARAGRAPH, object_nodes(input))]),
|
||||
));
|
||||
};
|
||||
|
||||
let mut skip_one = true;
|
||||
let mut i = input;
|
||||
let mut children = vec![];
|
||||
let mut previous_line_is_blank = false;
|
||||
'l: loop {
|
||||
for (input, head) in line_starts_iter(i.as_str())
|
||||
// the first line in list item content will always be a paragraph
|
||||
// so we need to skip it in the first iteration
|
||||
.skip(if skip_one { 1 } else { 0 })
|
||||
.map(|idx| i.take_split(idx))
|
||||
{
|
||||
match get_line_indent(input.as_str()) {
|
||||
Some(next_indent) => {
|
||||
previous_line_is_blank = false;
|
||||
|
||||
if next_indent <= indent {
|
||||
if !head.is_empty() {
|
||||
children.push(node(PARAGRAPH, object_nodes(head)));
|
||||
}
|
||||
return Ok((input, node(LIST_ITEM_CONTENT, children)));
|
||||
}
|
||||
|
||||
if let Ok((input, element)) = element_node(input) {
|
||||
if !head.is_empty() {
|
||||
children.push(node(PARAGRAPH, object_nodes(head)));
|
||||
}
|
||||
children.push(element);
|
||||
i = input;
|
||||
skip_one = false;
|
||||
continue 'l;
|
||||
}
|
||||
}
|
||||
_ if previous_line_is_blank => {
|
||||
// list item ends at two consecutive empty lines
|
||||
if !head.is_empty() {
|
||||
children.push(node(PARAGRAPH, object_nodes(head)));
|
||||
}
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
|
||||
children.extend(post_blank);
|
||||
|
||||
return Ok((input, node(LIST_ITEM_CONTENT, children)));
|
||||
}
|
||||
_ => {
|
||||
previous_line_is_blank = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if !i.is_empty() {
|
||||
children.push(node(PARAGRAPH, object_nodes(i)));
|
||||
}
|
||||
|
||||
Ok((input.of(""), node(LIST_ITEM_CONTENT, children)))
|
||||
}
|
||||
|
||||
fn get_line_indent(input: &str) -> Option<usize> {
|
||||
input
|
||||
.bytes()
|
||||
.take_while(|b| *b != b'\n')
|
||||
.position(|b| !b.is_ascii_whitespace())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::List, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_list = to_ast::<List>(list_node);
|
||||
|
||||
let list = to_list("1)");
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..2
|
||||
LIST_ITEM@0..2
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "1)"
|
||||
LIST_ITEM_CONTENT@2..2
|
||||
PARAGRAPH@2..2
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list("+ ");
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..2
|
||||
LIST_ITEM@0..2
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "+ "
|
||||
LIST_ITEM_CONTENT@2..2
|
||||
PARAGRAPH@2..2
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list("-\n");
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..2
|
||||
LIST_ITEM@0..2
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..1 "-"
|
||||
LIST_ITEM_CONTENT@1..2
|
||||
PARAGRAPH@1..2
|
||||
TEXT@1..2 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list("+ 1");
|
||||
assert!(!list.is_ordered());
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..3
|
||||
LIST_ITEM@0..3
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "+ "
|
||||
LIST_ITEM_CONTENT@2..3
|
||||
PARAGRAPH@2..3
|
||||
TEXT@2..3 "1"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list("+ 1\n");
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..4
|
||||
LIST_ITEM@0..4
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "+ "
|
||||
LIST_ITEM_CONTENT@2..4
|
||||
PARAGRAPH@2..4
|
||||
TEXT@2..4 "1\n"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list("+ [@A] 1\n\n\n+ 2");
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..14
|
||||
LIST_ITEM@0..11
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "+ "
|
||||
LIST_ITEM_COUNTER@2..6
|
||||
L_BRACKET@2..3 "["
|
||||
AT@3..4 "@"
|
||||
TEXT@4..5 "A"
|
||||
R_BRACKET@5..6 "]"
|
||||
WHITESPACE@6..7 " "
|
||||
LIST_ITEM_CONTENT@7..11
|
||||
PARAGRAPH@7..10
|
||||
TEXT@7..10 "1\n\n"
|
||||
BLANK_LINE@10..11
|
||||
NEW_LINE@10..11 "\n"
|
||||
LIST_ITEM@11..14
|
||||
LIST_ITEM_INDENT@11..11 ""
|
||||
LIST_ITEM_BULLET@11..13 "+ "
|
||||
LIST_ITEM_CONTENT@13..14
|
||||
PARAGRAPH@13..14
|
||||
TEXT@13..14 "2"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list("+ *TAG* :: item1\n+ [X] item2");
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..28
|
||||
LIST_ITEM@0..17
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "+ "
|
||||
LIST_ITEM_TAG@2..10
|
||||
BOLD@2..7
|
||||
STAR@2..3 "*"
|
||||
TEXT@3..6 "TAG"
|
||||
STAR@6..7 "*"
|
||||
TEXT@7..8 " "
|
||||
COLON2@8..10 "::"
|
||||
WHITESPACE@10..10 ""
|
||||
LIST_ITEM_CONTENT@10..17
|
||||
PARAGRAPH@10..17
|
||||
TEXT@10..17 " item1\n"
|
||||
LIST_ITEM@17..28
|
||||
LIST_ITEM_INDENT@17..17 ""
|
||||
LIST_ITEM_BULLET@17..19 "+ "
|
||||
LIST_ITEM_CHECK_BOX@19..22
|
||||
L_BRACKET@19..20 "["
|
||||
TEXT@20..21 "X"
|
||||
R_BRACKET@21..22 "]"
|
||||
WHITESPACE@22..23 " "
|
||||
LIST_ITEM_CONTENT@23..28
|
||||
PARAGRAPH@23..28
|
||||
TEXT@23..28 "item2"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list(
|
||||
r#"+ item1
|
||||
+ item2"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..17
|
||||
LIST_ITEM@0..17
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "+ "
|
||||
LIST_ITEM_CONTENT@2..17
|
||||
PARAGRAPH@2..8
|
||||
TEXT@2..8 "item1\n"
|
||||
LIST@8..17
|
||||
LIST_ITEM@8..17
|
||||
LIST_ITEM_INDENT@8..10 " "
|
||||
LIST_ITEM_BULLET@10..12 "+ "
|
||||
LIST_ITEM_CONTENT@12..17
|
||||
PARAGRAPH@12..17
|
||||
TEXT@12..17 "item2"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list("* item1\nitem2");
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..8
|
||||
LIST_ITEM@0..8
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "* "
|
||||
LIST_ITEM_CONTENT@2..8
|
||||
PARAGRAPH@2..8
|
||||
TEXT@2..8 "item1\n"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list(
|
||||
r#"* item1
|
||||
|
||||
still item 1"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..23
|
||||
LIST_ITEM@0..23
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "* "
|
||||
LIST_ITEM_CONTENT@2..23
|
||||
PARAGRAPH@2..23
|
||||
TEXT@2..23 "item1\n\n still item 1"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list(
|
||||
r#"+ item1
|
||||
+ item2
|
||||
"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..26
|
||||
LIST_ITEM@0..26
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..2 "+ "
|
||||
LIST_ITEM_CONTENT@2..26
|
||||
PARAGRAPH@2..8
|
||||
TEXT@2..8 "item1\n"
|
||||
LIST@8..26
|
||||
LIST_ITEM@8..26
|
||||
LIST_ITEM_INDENT@8..14 " "
|
||||
LIST_ITEM_BULLET@14..16 "+ "
|
||||
LIST_ITEM_CONTENT@16..26
|
||||
PARAGRAPH@16..26
|
||||
TEXT@16..26 "item2\n "
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list(
|
||||
r#"1. item1
|
||||
|
||||
- item2
|
||||
|
||||
3. item 3"#,
|
||||
);
|
||||
assert!(list.is_ordered());
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..32
|
||||
LIST_ITEM@0..23
|
||||
LIST_ITEM_INDENT@0..0 ""
|
||||
LIST_ITEM_BULLET@0..3 "1. "
|
||||
LIST_ITEM_CONTENT@3..23
|
||||
PARAGRAPH@3..10
|
||||
TEXT@3..10 "item1\n\n"
|
||||
LIST@10..23
|
||||
LIST_ITEM@10..23
|
||||
LIST_ITEM_INDENT@10..14 " "
|
||||
LIST_ITEM_BULLET@14..16 "- "
|
||||
LIST_ITEM_CONTENT@16..23
|
||||
PARAGRAPH@16..23
|
||||
TEXT@16..23 "item2\n\n"
|
||||
LIST_ITEM@23..32
|
||||
LIST_ITEM_INDENT@23..23 ""
|
||||
LIST_ITEM_BULLET@23..26 "3. "
|
||||
LIST_ITEM_CONTENT@26..32
|
||||
PARAGRAPH@26..32
|
||||
TEXT@26..32 "item 3"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list(
|
||||
r#" + item1
|
||||
|
||||
+ item2"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..20
|
||||
LIST_ITEM@0..11
|
||||
LIST_ITEM_INDENT@0..2 " "
|
||||
LIST_ITEM_BULLET@2..4 "+ "
|
||||
LIST_ITEM_CONTENT@4..11
|
||||
PARAGRAPH@4..11
|
||||
TEXT@4..11 "item1\n\n"
|
||||
LIST_ITEM@11..20
|
||||
LIST_ITEM_INDENT@11..13 " "
|
||||
LIST_ITEM_BULLET@13..15 "+ "
|
||||
LIST_ITEM_CONTENT@15..20
|
||||
PARAGRAPH@15..20
|
||||
TEXT@15..20 "item2"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list(
|
||||
r#" 1. item1
|
||||
2. item2
|
||||
3. item3"#,
|
||||
);
|
||||
assert!(list.is_ordered());
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..42
|
||||
LIST_ITEM@0..42
|
||||
LIST_ITEM_INDENT@0..2 " "
|
||||
LIST_ITEM_BULLET@2..5 "1. "
|
||||
LIST_ITEM_CONTENT@5..42
|
||||
PARAGRAPH@5..11
|
||||
TEXT@5..11 "item1\n"
|
||||
LIST@11..28
|
||||
LIST_ITEM@11..28
|
||||
LIST_ITEM_INDENT@11..19 " "
|
||||
LIST_ITEM_BULLET@19..22 "2. "
|
||||
LIST_ITEM_CONTENT@22..28
|
||||
PARAGRAPH@22..28
|
||||
TEXT@22..28 "item2\n"
|
||||
LIST@28..42
|
||||
LIST_ITEM@28..42
|
||||
LIST_ITEM_INDENT@28..34 " "
|
||||
LIST_ITEM_BULLET@34..37 "3. "
|
||||
LIST_ITEM_CONTENT@37..42
|
||||
PARAGRAPH@37..42
|
||||
TEXT@37..42 "item3"
|
||||
"###
|
||||
);
|
||||
|
||||
let list = to_list(
|
||||
r#" 1. item1
|
||||
#+begin_example
|
||||
hello
|
||||
#+end_example
|
||||
"#,
|
||||
);
|
||||
insta::assert_debug_snapshot!(
|
||||
list.syntax,
|
||||
@r###"
|
||||
LIST@0..51
|
||||
LIST_ITEM@0..51
|
||||
LIST_ITEM_INDENT@0..2 " "
|
||||
LIST_ITEM_BULLET@2..5 "1. "
|
||||
LIST_ITEM_CONTENT@5..51
|
||||
PARAGRAPH@5..11
|
||||
TEXT@5..11 "item1\n"
|
||||
EXAMPLE_BLOCK@11..51
|
||||
BLOCK_BEGIN@11..31
|
||||
WHITESPACE@11..15 " "
|
||||
TEXT@15..23 "#+begin_"
|
||||
TEXT@23..30 "example"
|
||||
TEXT@30..30 ""
|
||||
NEW_LINE@30..31 "\n"
|
||||
BLOCK_CONTENT@31..37
|
||||
TEXT@31..37 "hello\n"
|
||||
BLOCK_END@37..51
|
||||
TEXT@37..43 "#+end_"
|
||||
TEXT@43..50 "example"
|
||||
NEW_LINE@50..51 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(list_node(("-a", config).into()).is_err());
|
||||
}
|
||||
108
src/syntax/macros.rs
Normal file
108
src/syntax/macros.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
use nom::{
|
||||
bytes::complete::{take_until, take_while1},
|
||||
combinator::{map, opt, verify},
|
||||
sequence::tuple,
|
||||
AsBytes, IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
debug_assert_lossless, l_curly3_token, l_parens_token, node, r_curly3_token,
|
||||
r_parens_token, GreenElement,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn macros_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
l_curly3_token,
|
||||
verify(
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|
||||
|s: &Input| s.as_bytes()[0].is_ascii_alphabetic(),
|
||||
),
|
||||
opt(tuple((l_parens_token, take_until(")}}}"), r_parens_token))),
|
||||
r_curly3_token,
|
||||
)),
|
||||
|(l_curly3, name, argument, r_curly3)| {
|
||||
let mut children = vec![];
|
||||
children.push(l_curly3);
|
||||
children.push(name.text_token());
|
||||
if let Some((l_parens, argument, r_parens)) = argument {
|
||||
children.push(node(
|
||||
MACROS_ARGUMENT,
|
||||
[l_parens, argument.text_token(), r_parens],
|
||||
));
|
||||
}
|
||||
children.push(r_curly3);
|
||||
node(MACROS, children)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::{ast::Macros, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_macros = to_ast::<Macros>(macros_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_macros("{{{title}}}").syntax,
|
||||
@r###"
|
||||
MACROS@0..11
|
||||
L_CURLY3@0..3 "{{{"
|
||||
TEXT@3..8 "title"
|
||||
R_CURLY3@8..11 "}}}"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_macros("{{{one_arg_macro(1)}}}").syntax,
|
||||
@r###"
|
||||
MACROS@0..22
|
||||
L_CURLY3@0..3 "{{{"
|
||||
TEXT@3..16 "one_arg_macro"
|
||||
MACROS_ARGUMENT@16..19
|
||||
L_PARENS@16..17 "("
|
||||
TEXT@17..18 "1"
|
||||
R_PARENS@18..19 ")"
|
||||
R_CURLY3@19..22 "}}}"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_macros("{{{two_arg_macro(1, 2)}}}").syntax,
|
||||
@r###"
|
||||
MACROS@0..25
|
||||
L_CURLY3@0..3 "{{{"
|
||||
TEXT@3..16 "two_arg_macro"
|
||||
MACROS_ARGUMENT@16..22
|
||||
L_PARENS@16..17 "("
|
||||
TEXT@17..21 "1, 2"
|
||||
R_PARENS@21..22 ")"
|
||||
R_CURLY3@22..25 "}}}"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_macros("{{{two_arg_macro(1\\,a, 2)}}}").syntax,
|
||||
@r###"
|
||||
MACROS@0..28
|
||||
L_CURLY3@0..3 "{{{"
|
||||
TEXT@3..16 "two_arg_macro"
|
||||
MACROS_ARGUMENT@16..25
|
||||
L_PARENS@16..17 "("
|
||||
TEXT@17..24 "1\\,a, 2"
|
||||
R_PARENS@24..25 ")"
|
||||
R_CURLY3@25..28 "}}}"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(macros_node(("{{{0uthor}}}", config).into()).is_err());
|
||||
assert!(macros_node(("{{{author}}", config).into()).is_err());
|
||||
assert!(macros_node(("{{{poem(}}}", config).into()).is_err());
|
||||
assert!(macros_node(("{{{poem)}}}", config).into()).is_err());
|
||||
}
|
||||
209
src/syntax/mod.rs
Normal file
209
src/syntax/mod.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
//! Org-mode elements
|
||||
|
||||
pub mod block;
|
||||
pub mod clock;
|
||||
pub mod combinator;
|
||||
pub mod comment;
|
||||
pub mod cookie;
|
||||
pub mod document;
|
||||
pub mod drawer;
|
||||
pub mod dyn_block;
|
||||
pub mod element;
|
||||
pub mod emphasis;
|
||||
pub mod fixed_width;
|
||||
pub mod fn_def;
|
||||
pub mod fn_ref;
|
||||
pub mod headline;
|
||||
pub mod inline_call;
|
||||
pub mod inline_src;
|
||||
pub mod input;
|
||||
pub mod keyword;
|
||||
pub mod link;
|
||||
pub mod list;
|
||||
pub mod macros;
|
||||
pub mod object;
|
||||
pub mod paragraph;
|
||||
pub mod planning;
|
||||
pub mod radio_target;
|
||||
pub mod rule;
|
||||
pub mod snippet;
|
||||
pub mod table;
|
||||
pub mod target;
|
||||
pub mod timestamp;
|
||||
|
||||
use rowan::Language;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct OrgLanguage;
|
||||
|
||||
impl Language for OrgLanguage {
|
||||
type Kind = SyntaxKind;
|
||||
|
||||
fn kind_from_raw(raw: rowan::SyntaxKind) -> SyntaxKind {
|
||||
// SAFETY: SyntaxKind is `repr(u16)`
|
||||
unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
|
||||
}
|
||||
|
||||
fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind {
|
||||
rowan::SyntaxKind(kind as u16)
|
||||
}
|
||||
}
|
||||
|
||||
pub type SyntaxNode = rowan::SyntaxNode<OrgLanguage>;
|
||||
pub type SyntaxToken = rowan::SyntaxToken<OrgLanguage>;
|
||||
pub type SyntaxElement = rowan::SyntaxElement<OrgLanguage>;
|
||||
pub type SyntaxNodeChildren = rowan::SyntaxNodeChildren<OrgLanguage>;
|
||||
pub type SyntaxElementChildren = rowan::SyntaxElementChildren<OrgLanguage>;
|
||||
|
||||
#[allow(bad_style)]
|
||||
#[allow(clippy::all)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
#[repr(u16)]
|
||||
pub enum SyntaxKind {
|
||||
//
|
||||
// token
|
||||
//
|
||||
L_BRACKET, // '['
|
||||
R_BRACKET, // ']'
|
||||
L_BRACKET2, // '[['
|
||||
R_BRACKET2, // ']]'
|
||||
L_PARENS, // '('
|
||||
R_PARENS, // ')'
|
||||
L_ANGLE, // '<'
|
||||
R_ANGLE, // '>'
|
||||
L_CURLY, // '{'
|
||||
R_CURLY, // '}'
|
||||
L_CURLY3, // '{{{'
|
||||
R_CURLY3, // '}}}'
|
||||
L_ANGLE2, // '<<'
|
||||
R_ANGLE2, // '>>'
|
||||
L_ANGLE3, // '<<<'
|
||||
R_ANGLE3, // '>>>'
|
||||
AT, // '@'
|
||||
AT2, // '@@'
|
||||
PERCENT, // '%'
|
||||
PERCENT2, // '%%'
|
||||
SLASH, // '/'
|
||||
UNDERSCORE, // '_'
|
||||
STAR, // '*'
|
||||
PLUS, // '+'
|
||||
MINUS, // '-'
|
||||
MINUS2, // '--'
|
||||
COLON, // ':'
|
||||
COLON2, // '::'
|
||||
EQUAL, // '='
|
||||
TILDE, // '~'
|
||||
HASH, // '#'
|
||||
HASH_PLUS, // '#+'
|
||||
DOUBLE_ARROW, // '=>'
|
||||
PIPE, // '|'
|
||||
COMMA, // ','
|
||||
TEXT,
|
||||
BLANK_LINE,
|
||||
WHITESPACE,
|
||||
NEW_LINE,
|
||||
|
||||
DOCUMENT,
|
||||
SECTION,
|
||||
PARAGRAPH,
|
||||
|
||||
HEADLINE,
|
||||
HEADLINE_STARS,
|
||||
HEADLINE_TITLE,
|
||||
HEADLINE_KEYWORD,
|
||||
HEADLINE_PRIORITY,
|
||||
HEADLINE_TAGS,
|
||||
PROPERTY_DRAWER,
|
||||
NODE_PROPERTY,
|
||||
PLANNING,
|
||||
PLANNING_DEADLINE,
|
||||
PLANNING_SCHEDULED,
|
||||
PLANNING_CLOSED,
|
||||
|
||||
//
|
||||
// elements
|
||||
//
|
||||
/* table */
|
||||
ORG_TABLE,
|
||||
ORG_TABLE_RULE_ROW,
|
||||
ORG_TABLE_STANDARD_ROW,
|
||||
ORG_TABLE_CELL,
|
||||
/* list */
|
||||
LIST,
|
||||
LIST_ITEM,
|
||||
LIST_ITEM_INDENT,
|
||||
LIST_ITEM_BULLET,
|
||||
LIST_ITEM_COUNTER,
|
||||
LIST_ITEM_CHECK_BOX,
|
||||
LIST_ITEM_TAG,
|
||||
LIST_ITEM_CONTENT,
|
||||
/* drawer */
|
||||
DRAWER,
|
||||
DRAWER_BEGIN,
|
||||
DRAWER_END,
|
||||
KEYWORD,
|
||||
BABEL_CALL,
|
||||
TABLE_EL,
|
||||
CLOCK,
|
||||
FN_DEF,
|
||||
COMMENT,
|
||||
RULE,
|
||||
FIXED_WIDTH,
|
||||
/* dyn block */
|
||||
DYN_BLOCK,
|
||||
DYN_BLOCK_BEGIN,
|
||||
DYN_BLOCK_END,
|
||||
/* block */
|
||||
SPECIAL_BLOCK,
|
||||
QUOTE_BLOCK,
|
||||
CENTER_BLOCK,
|
||||
VERSE_BLOCK,
|
||||
COMMENT_BLOCK,
|
||||
EXAMPLE_BLOCK,
|
||||
EXPORT_BLOCK,
|
||||
SOURCE_BLOCK,
|
||||
SOURCE_BLOCK_LANG,
|
||||
BLOCK_BEGIN,
|
||||
BLOCK_END,
|
||||
BLOCK_CONTENT,
|
||||
|
||||
//
|
||||
// objects
|
||||
//
|
||||
INLINE_CALL,
|
||||
INLINE_SRC,
|
||||
LINK,
|
||||
LINK_PATH,
|
||||
COOKIE,
|
||||
RADIO_TARGET,
|
||||
FN_REF,
|
||||
LATEX_ENVIRONMENT,
|
||||
MACROS,
|
||||
MACROS_ARGUMENT,
|
||||
SNIPPET,
|
||||
TARGET,
|
||||
BOLD,
|
||||
STRIKE,
|
||||
ITALIC,
|
||||
UNDERLINE,
|
||||
VERBATIM,
|
||||
CODE,
|
||||
|
||||
/* timestamp */
|
||||
TIMESTAMP_ACTIVE,
|
||||
TIMESTAMP_INACTIVE,
|
||||
TIMESTAMP_DIARY,
|
||||
TIMESTAMP_YEAR,
|
||||
TIMESTAMP_MONTH,
|
||||
TIMESTAMP_DAY,
|
||||
TIMESTAMP_HOUR,
|
||||
TIMESTAMP_MINUTE,
|
||||
TIMESTAMP_DAYNAME,
|
||||
}
|
||||
|
||||
impl From<SyntaxKind> for rowan::SyntaxKind {
|
||||
fn from(value: SyntaxKind) -> Self {
|
||||
OrgLanguage::kind_to_raw(value)
|
||||
}
|
||||
}
|
||||
194
src/syntax/object.rs
Normal file
194
src/syntax/object.rs
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
use nom::{AsBytes, IResult, InputLength, InputTake};
|
||||
|
||||
use super::{
|
||||
combinator::GreenElement,
|
||||
cookie::cookie_node,
|
||||
emphasis::{bold_node, code_node, italic_node, strike_node, underline_node, verbatim_node},
|
||||
fn_ref::fn_ref_node,
|
||||
inline_call::inline_call_node,
|
||||
inline_src::inline_src_node,
|
||||
input::Input,
|
||||
link::link_node,
|
||||
macros::macros_node,
|
||||
radio_target::radio_target_node,
|
||||
snippet::snippet_node,
|
||||
target::target_node,
|
||||
timestamp::{timestamp_active_node, timestamp_diary_node, timestamp_inactive_node},
|
||||
};
|
||||
|
||||
pub struct InlinePositions<'a> {
|
||||
bytes: &'a [u8],
|
||||
pos: usize,
|
||||
next: Option<usize>,
|
||||
}
|
||||
|
||||
impl InlinePositions<'_> {
|
||||
pub fn new(bytes: &[u8]) -> InlinePositions {
|
||||
InlinePositions {
|
||||
bytes,
|
||||
pos: 0,
|
||||
next: Some(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for InlinePositions<'_> {
|
||||
type Item = usize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.next.take().or_else(|| {
|
||||
jetscii::bytes!(b'@', b'<', b'[', b' ', b'(', b'{', b'\'', b'"', b'\n')
|
||||
.find(&self.bytes[self.pos..])
|
||||
.map(|i| {
|
||||
self.pos += i + 1;
|
||||
|
||||
match self.bytes[self.pos - 1] {
|
||||
b'{' => {
|
||||
self.next = Some(self.pos);
|
||||
self.pos - 1
|
||||
}
|
||||
b' ' | b'(' | b'\'' | b'"' | b'\n' => self.pos,
|
||||
_ => self.pos - 1,
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_nodes(input: Input) -> Vec<GreenElement> {
|
||||
// debug_assert!(!input.is_empty());
|
||||
let nodes = object_nodes_base(input);
|
||||
debug_assert_eq!(
|
||||
input.as_str(),
|
||||
nodes.iter().fold(String::new(), |s, i| s + &i.to_string()),
|
||||
"parser must be lossless"
|
||||
);
|
||||
nodes
|
||||
}
|
||||
|
||||
fn object_nodes_base(input: Input) -> Vec<GreenElement> {
|
||||
let mut children = vec![];
|
||||
|
||||
let mut i = input;
|
||||
'l: loop {
|
||||
for (input, head) in InlinePositions::new(i.as_bytes()).map(|idx| i.take_split(idx)) {
|
||||
if let Ok((input, node)) = object_node(input) {
|
||||
if !head.is_empty() {
|
||||
children.push(head.text_token())
|
||||
}
|
||||
children.push(node);
|
||||
i = input;
|
||||
continue 'l;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if !i.is_empty() {
|
||||
children.push(i.text_token());
|
||||
}
|
||||
|
||||
children
|
||||
}
|
||||
|
||||
fn object_node(i: Input) -> IResult<Input, GreenElement, ()> {
|
||||
if i.input_len() < 3 {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
match &i.as_bytes()[0] {
|
||||
b'*' => bold_node(i),
|
||||
b'+' => strike_node(i),
|
||||
b'/' => italic_node(i),
|
||||
b'_' => underline_node(i),
|
||||
b'=' => verbatim_node(i),
|
||||
b'~' => code_node(i),
|
||||
b'@' => snippet_node(i),
|
||||
b'{' => macros_node(i),
|
||||
b'<' => radio_target_node(i)
|
||||
.or_else(|_| target_node(i))
|
||||
.or_else(|_| timestamp_diary_node(i))
|
||||
.or_else(|_| timestamp_active_node(i)),
|
||||
b'[' => cookie_node(i)
|
||||
.or_else(|_| link_node(i))
|
||||
.or_else(|_| fn_ref_node(i))
|
||||
.or_else(|_| timestamp_inactive_node(i)),
|
||||
b'c' => inline_call_node(i),
|
||||
b's' => inline_src_node(i),
|
||||
_ => Err(nom::Err::Error(())),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{
|
||||
syntax::{combinator::node, SyntaxKind, SyntaxNode},
|
||||
ParseConfig,
|
||||
};
|
||||
|
||||
let t = |input: &str| {
|
||||
let config = &ParseConfig::default();
|
||||
let children = object_nodes((input, config).into());
|
||||
SyntaxNode::new_root(node(SyntaxKind::PARAGRAPH, children).into_node().unwrap())
|
||||
};
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
t("~org-inlinetask-min-level~[fn:oiml:The default value of \n~org-inlinetask-min-level~ is =15=.]"),
|
||||
@r###"
|
||||
PARAGRAPH@0..93
|
||||
CODE@0..26
|
||||
TILDE@0..1 "~"
|
||||
TEXT@1..25 "org-inlinetask-min-level"
|
||||
TILDE@25..26 "~"
|
||||
FN_REF@26..93
|
||||
L_BRACKET@26..27 "["
|
||||
TEXT@27..29 "fn"
|
||||
COLON@29..30 ":"
|
||||
TEXT@30..34 "oiml"
|
||||
COLON@34..35 ":"
|
||||
TEXT@35..57 "The default value of \n"
|
||||
CODE@57..83
|
||||
TILDE@57..58 "~"
|
||||
TEXT@58..82 "org-inlinetask-min-level"
|
||||
TILDE@82..83 "~"
|
||||
TEXT@83..87 " is "
|
||||
VERBATIM@87..91
|
||||
EQUAL@87..88 "="
|
||||
TEXT@88..90 "15"
|
||||
EQUAL@90..91 "="
|
||||
TEXT@91..92 "."
|
||||
R_BRACKET@92..93 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
t(r#"Org is a /plaintext markup syntax/ developed with *Emacs* in 2003.
|
||||
The canonical parser is =org-element.el=, which provides a number of
|
||||
functions starting with ~org-element-~."#),
|
||||
@r###"
|
||||
PARAGRAPH@0..175
|
||||
TEXT@0..9 "Org is a "
|
||||
ITALIC@9..34
|
||||
SLASH@9..10 "/"
|
||||
TEXT@10..33 "plaintext markup syntax"
|
||||
SLASH@33..34 "/"
|
||||
TEXT@34..50 " developed with "
|
||||
BOLD@50..57
|
||||
STAR@50..51 "*"
|
||||
TEXT@51..56 "Emacs"
|
||||
STAR@56..57 "*"
|
||||
TEXT@57..91 " in 2003.\nThe canonic ..."
|
||||
VERBATIM@91..107
|
||||
EQUAL@91..92 "="
|
||||
TEXT@92..106 "org-element.el"
|
||||
EQUAL@106..107 "="
|
||||
TEXT@107..160 ", which provides a nu ..."
|
||||
CODE@160..174
|
||||
TILDE@160..161 "~"
|
||||
TEXT@161..173 "org-element-"
|
||||
TILDE@173..174 "~"
|
||||
TEXT@174..175 "."
|
||||
"###
|
||||
);
|
||||
}
|
||||
96
src/syntax/paragraph.rs
Normal file
96
src/syntax/paragraph.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
use nom::{IResult, InputTake};
|
||||
|
||||
use super::{
|
||||
combinator::{blank_lines, debug_assert_lossless, line_ends_iter, node, GreenElement},
|
||||
input::Input,
|
||||
object::object_nodes,
|
||||
SyntaxKind,
|
||||
};
|
||||
|
||||
fn paragraph_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert!(!input.is_empty());
|
||||
|
||||
let mut start = 0;
|
||||
for idx in line_ends_iter(input.as_str()) {
|
||||
// stops at blank line
|
||||
if input.s[start..idx].bytes().all(|c| c.is_ascii_whitespace()) {
|
||||
break;
|
||||
}
|
||||
|
||||
start = idx;
|
||||
}
|
||||
|
||||
let (input, contents) = input.take_split(start);
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
|
||||
let mut children = vec![];
|
||||
children.extend(object_nodes(contents));
|
||||
children.extend(post_blank);
|
||||
|
||||
Ok((input, node(SyntaxKind::PARAGRAPH, children)))
|
||||
}
|
||||
|
||||
pub fn paragraph_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(paragraph_node_base)(input)
|
||||
}
|
||||
|
||||
pub fn paragraph_nodes(input: Input) -> Result<Vec<GreenElement>, nom::Err<()>> {
|
||||
let mut i = input;
|
||||
let mut children = vec![];
|
||||
while !i.is_empty() {
|
||||
let (input, node) = paragraph_node(i)?;
|
||||
children.push(node);
|
||||
i = input;
|
||||
}
|
||||
Ok(children)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Paragraph, tests::to_ast};
|
||||
|
||||
let to_paragraph = to_ast::<Paragraph>(paragraph_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_paragraph(r#"a"#).syntax,
|
||||
@r###"
|
||||
PARAGRAPH@0..1
|
||||
TEXT@0..1 "a"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_paragraph(r#"a
|
||||
"#).syntax,
|
||||
@r###"
|
||||
PARAGRAPH@0..6
|
||||
TEXT@0..2 "a\n"
|
||||
BLANK_LINE@2..6
|
||||
WHITESPACE@2..6 " "
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_paragraph(r#"a
|
||||
b
|
||||
c
|
||||
"#).syntax,
|
||||
@r###"
|
||||
PARAGRAPH@0..6
|
||||
TEXT@0..6 "a\nb\nc\n"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_paragraph(r#"a
|
||||
|
||||
c
|
||||
"#).syntax,
|
||||
@r###"
|
||||
PARAGRAPH@0..3
|
||||
TEXT@0..2 "a\n"
|
||||
BLANK_LINE@2..3
|
||||
NEW_LINE@2..3 "\n"
|
||||
"###
|
||||
);
|
||||
}
|
||||
94
src/syntax/planning.rs
Normal file
94
src/syntax/planning.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::tag,
|
||||
character::complete::{line_ending, space0},
|
||||
combinator::{eof, iterator},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{debug_assert_lossless, GreenElement, NodeBuilder},
|
||||
input::Input,
|
||||
timestamp::{timestamp_active_node, timestamp_inactive_node},
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn planning_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(planning_node_base)(input)
|
||||
}
|
||||
|
||||
fn planning_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let mut b = NodeBuilder::new();
|
||||
|
||||
let mut it = iterator(
|
||||
input,
|
||||
tuple((
|
||||
space0,
|
||||
alt((tag("DEADLINE:"), tag("SCHEDULED:"), tag("CLOSED:"))),
|
||||
space0,
|
||||
alt((timestamp_active_node, timestamp_inactive_node)),
|
||||
)),
|
||||
);
|
||||
|
||||
let start_len = b.len();
|
||||
|
||||
it.for_each(|(ws, text, ws_, timestamp)| {
|
||||
let mut b_ = NodeBuilder::new();
|
||||
b_.ws(ws);
|
||||
b_.text(text);
|
||||
b_.ws(ws_);
|
||||
b_.push(timestamp);
|
||||
b.push(b_.finish(match text.as_str() {
|
||||
"DEADLINE:" => PLANNING_DEADLINE,
|
||||
"SCHEDULED:" => PLANNING_SCHEDULED,
|
||||
"CLOSED:" => PLANNING_CLOSED,
|
||||
_ => unreachable!(),
|
||||
}));
|
||||
});
|
||||
|
||||
if b.len() == start_len {
|
||||
return Err(nom::Err::Error(()));
|
||||
}
|
||||
|
||||
let (input, _) = it.finish()?;
|
||||
let (input, ws) = space0(input)?;
|
||||
let (input, nl) = alt((line_ending, eof))(input)?;
|
||||
|
||||
b.ws(ws);
|
||||
b.nl(nl);
|
||||
|
||||
Ok((input, b.finish(PLANNING)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prase() {
|
||||
use crate::{ast::Planning, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_planning = to_ast::<Planning>(planning_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_planning("SCHEDULED: <2019-04-08 Mon>").syntax,
|
||||
@r###"
|
||||
PLANNING@0..27
|
||||
PLANNING_SCHEDULED@0..27
|
||||
TEXT@0..10 "SCHEDULED:"
|
||||
WHITESPACE@10..11 " "
|
||||
TIMESTAMP_ACTIVE@11..27
|
||||
L_ANGLE@11..12 "<"
|
||||
TIMESTAMP_YEAR@12..16 "2019"
|
||||
MINUS@16..17 "-"
|
||||
TIMESTAMP_MONTH@17..19 "04"
|
||||
MINUS@19..20 "-"
|
||||
TIMESTAMP_DAY@20..22 "08"
|
||||
WHITESPACE@22..23 " "
|
||||
TIMESTAMP_DAYNAME@23..26 "Mon"
|
||||
R_ANGLE@26..27 ">"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(planning_node((" ", config).into()).is_err());
|
||||
assert!(planning_node((" SCHEDULED: ", config).into()).is_err());
|
||||
}
|
||||
68
src/syntax/radio_target.rs
Normal file
68
src/syntax/radio_target.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
use nom::{
|
||||
bytes::complete::take_while,
|
||||
combinator::{map, verify},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{debug_assert_lossless, l_angle3_token, node, r_angle3_token, GreenElement},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
// TODO: text-markup, entities, latex-fragments, subscript and superscript
|
||||
|
||||
pub fn radio_target_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
l_angle3_token,
|
||||
verify(
|
||||
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|
||||
|s: &Input| {
|
||||
s.as_str().starts_with(|c| c != ' ') && s.as_str().ends_with(|c| c != ' ')
|
||||
},
|
||||
),
|
||||
r_angle3_token,
|
||||
)),
|
||||
|(l_angle3, contents, r_angle3)| {
|
||||
node(RADIO_TARGET, [l_angle3, contents.text_token(), r_angle3])
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::RadioTarget, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_radio_target = to_ast::<RadioTarget>(radio_target_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_radio_target("<<<target>>>").syntax,
|
||||
@r###"
|
||||
RADIO_TARGET@0..12
|
||||
L_ANGLE3@0..3 "<<<"
|
||||
TEXT@3..9 "target"
|
||||
R_ANGLE3@9..12 ">>>"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_radio_target("<<<tar get>>>").syntax,
|
||||
@r###"
|
||||
RADIO_TARGET@0..13
|
||||
L_ANGLE3@0..3 "<<<"
|
||||
TEXT@3..10 "tar get"
|
||||
R_ANGLE3@10..13 ">>>"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(radio_target_node(("<<<target >>>", config).into()).is_err());
|
||||
assert!(radio_target_node(("<<< target>>>", config).into()).is_err());
|
||||
assert!(radio_target_node(("<<<ta<get>>>", config).into()).is_err());
|
||||
assert!(radio_target_node(("<<<ta>get>>>", config).into()).is_err());
|
||||
assert!(radio_target_node(("<<<ta\nget>>>", config).into()).is_err());
|
||||
assert!(radio_target_node(("<<<target>>", config).into()).is_err());
|
||||
}
|
||||
93
src/syntax/rule.rs
Normal file
93
src/syntax/rule.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::take_while_m_n,
|
||||
character::complete::{line_ending, space0},
|
||||
combinator::{eof, map},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{blank_lines, debug_assert_lossless, GreenElement, NodeBuilder},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn rule_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
space0,
|
||||
take_while_m_n(5, usize::max_value(), |c| c == '-'),
|
||||
space0,
|
||||
alt((line_ending, eof)),
|
||||
blank_lines,
|
||||
)),
|
||||
|(ws, dashes, ws_, nl, post_blank)| {
|
||||
let mut b = NodeBuilder::new();
|
||||
b.ws(ws);
|
||||
b.text(dashes);
|
||||
b.ws(ws_);
|
||||
b.nl(nl);
|
||||
b.children.extend(post_blank);
|
||||
b.finish(RULE)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Rule, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_rule = to_ast::<Rule>(rule_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_rule("-----").syntax,
|
||||
@r###"
|
||||
RULE@0..5
|
||||
TEXT@0..5 "-----"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_rule("--------").syntax,
|
||||
@r###"
|
||||
RULE@0..8
|
||||
TEXT@0..8 "--------"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_rule("-----\n\n\n").syntax,
|
||||
@r###"
|
||||
RULE@0..8
|
||||
TEXT@0..5 "-----"
|
||||
NEW_LINE@5..6 "\n"
|
||||
BLANK_LINE@6..7
|
||||
NEW_LINE@6..7 "\n"
|
||||
BLANK_LINE@7..8
|
||||
NEW_LINE@7..8 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_rule("----- \n").syntax,
|
||||
@r###"
|
||||
RULE@0..8
|
||||
TEXT@0..5 "-----"
|
||||
WHITESPACE@5..7 " "
|
||||
NEW_LINE@7..8 "\n"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(rule_node(("", config).into()).is_err());
|
||||
assert!(rule_node(("----", config).into()).is_err());
|
||||
assert!(rule_node(("None----", config).into()).is_err());
|
||||
assert!(rule_node(("None ----", config).into()).is_err());
|
||||
assert!(rule_node(("None------", config).into()).is_err());
|
||||
assert!(rule_node(("----None----", config).into()).is_err());
|
||||
assert!(rule_node(("\t\t----", config).into()).is_err());
|
||||
assert!(rule_node(("------None", config).into()).is_err());
|
||||
assert!(rule_node(("----- None", config).into()).is_err());
|
||||
}
|
||||
91
src/syntax/snippet.rs
Normal file
91
src/syntax/snippet.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
use nom::{
|
||||
bytes::complete::{take_until, take_while1},
|
||||
combinator::map,
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{at2_token, colon_token, debug_assert_lossless, node, GreenElement},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn snippet_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
at2_token,
|
||||
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-'),
|
||||
colon_token,
|
||||
take_until("@@"),
|
||||
at2_token,
|
||||
)),
|
||||
|(at2, name, colon, value, at2_)| {
|
||||
node(
|
||||
SNIPPET,
|
||||
[at2, name.text_token(), colon, value.text_token(), at2_],
|
||||
)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Snippet, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_snippet = to_ast::<Snippet>(snippet_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_snippet("@@html:<b>@@").syntax,
|
||||
@r###"
|
||||
SNIPPET@0..12
|
||||
AT2@0..2 "@@"
|
||||
TEXT@2..6 "html"
|
||||
COLON@6..7 ":"
|
||||
TEXT@7..10 "<b>"
|
||||
AT2@10..12 "@@"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_snippet("@@latex:any arbitrary LaTeX code@@").syntax,
|
||||
@r###"
|
||||
SNIPPET@0..34
|
||||
AT2@0..2 "@@"
|
||||
TEXT@2..7 "latex"
|
||||
COLON@7..8 ":"
|
||||
TEXT@8..32 "any arbitrary LaTeX code"
|
||||
AT2@32..34 "@@"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_snippet("@@html:@@").syntax,
|
||||
@r###"
|
||||
SNIPPET@0..9
|
||||
AT2@0..2 "@@"
|
||||
TEXT@2..6 "html"
|
||||
COLON@6..7 ":"
|
||||
TEXT@7..7 ""
|
||||
AT2@7..9 "@@"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_snippet("@@html:<p>@</p>@@").syntax,
|
||||
@r###"
|
||||
SNIPPET@0..17
|
||||
AT2@0..2 "@@"
|
||||
TEXT@2..6 "html"
|
||||
COLON@6..7 ":"
|
||||
TEXT@7..15 "<p>@</p>"
|
||||
AT2@15..17 "@@"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(snippet_node(("@@html:<b>@", config).into()).is_err());
|
||||
assert!(snippet_node(("@@html<b>@@", config).into()).is_err());
|
||||
assert!(snippet_node(("@@:<b>@@", config).into()).is_err());
|
||||
}
|
||||
209
src/syntax/table.rs
Normal file
209
src/syntax/table.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
use nom::{
|
||||
bytes::complete::take_while,
|
||||
character::complete::{multispace0, space0},
|
||||
combinator::iterator,
|
||||
sequence::tuple,
|
||||
AsBytes, Err, IResult, InputTake, Slice,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
blank_lines, debug_assert_lossless, line_ends_iter, node, pipe_token, GreenElement,
|
||||
NodeBuilder,
|
||||
},
|
||||
input::Input,
|
||||
object::object_nodes,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
fn org_table_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let mut children = vec![];
|
||||
|
||||
let mut start = 0;
|
||||
for i in line_ends_iter(input.as_str()) {
|
||||
let line = input.slice(start..i);
|
||||
let trimmed = line.as_str().trim_start();
|
||||
|
||||
// Org tables end at the first line not starting with a vertical bar.
|
||||
if !trimmed.starts_with('|') {
|
||||
if start == 0 {
|
||||
return Err(nom::Err::Error(()));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if trimmed.starts_with("|-") {
|
||||
children.push(node(ORG_TABLE_RULE_ROW, [line.text_token()]));
|
||||
} else {
|
||||
children.push(table_standard_row_node(line)?);
|
||||
}
|
||||
|
||||
start = i;
|
||||
}
|
||||
|
||||
let (input, post_blank) = blank_lines(input.slice(start..))?;
|
||||
|
||||
children.extend(post_blank);
|
||||
|
||||
Ok((input, node(ORG_TABLE, children)))
|
||||
}
|
||||
|
||||
fn table_standard_row_node(input: Input) -> Result<GreenElement, nom::Err<()>> {
|
||||
let mut b = NodeBuilder::new();
|
||||
|
||||
let (input, ws) = space0(input)?;
|
||||
|
||||
b.ws(ws);
|
||||
|
||||
let mut it = iterator(
|
||||
input,
|
||||
tuple((pipe_token, multispace0, take_while(|c: char| c != '|'))),
|
||||
);
|
||||
|
||||
it.for_each(|(pipe, ws, input)| {
|
||||
b.push(pipe);
|
||||
b.ws(ws);
|
||||
|
||||
if input.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match input
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.rposition(|b| !b.is_ascii_whitespace())
|
||||
{
|
||||
Some(idx) => {
|
||||
let (ws, cell) = input.take_split(idx + 1);
|
||||
b.push(node(ORG_TABLE_CELL, object_nodes(cell)));
|
||||
b.ws(ws);
|
||||
}
|
||||
_ => {
|
||||
b.push(node(ORG_TABLE_CELL, object_nodes(input)));
|
||||
}
|
||||
}
|
||||
});
|
||||
it.finish()?;
|
||||
|
||||
Ok(b.finish(ORG_TABLE_STANDARD_ROW))
|
||||
}
|
||||
|
||||
fn table_el_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let mut start = 0;
|
||||
for i in line_ends_iter(input.as_str()) {
|
||||
let line = &input.s[start..i];
|
||||
let trimmed = line.trim();
|
||||
|
||||
if start == 0 {
|
||||
// Table.el tables start at lines beginning with "+-" string and followed by plus or minus signs
|
||||
if !trimmed.starts_with("+-") || trimmed.bytes().any(|c| c != b'+' && c != b'-') {
|
||||
return Err(Err::Error(()));
|
||||
}
|
||||
}
|
||||
|
||||
// Table.el tables end at the first line not starting with either a vertical line or a plus sign.
|
||||
if !trimmed.starts_with('|') && !trimmed.starts_with('+') {
|
||||
break;
|
||||
}
|
||||
|
||||
start = i;
|
||||
}
|
||||
|
||||
let (input, contents) = input.take_split(start);
|
||||
let (input, post_blank) = blank_lines(input)?;
|
||||
|
||||
let mut children = vec![];
|
||||
children.push(contents.text_token());
|
||||
children.extend(post_blank);
|
||||
|
||||
Ok((input, node(TABLE_EL, children)))
|
||||
}
|
||||
|
||||
pub fn org_table_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(org_table_node_base)(input)
|
||||
}
|
||||
|
||||
pub fn table_el_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(table_el_node_base)(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_org_table() {
|
||||
use crate::{ast::OrgTable, tests::to_ast};
|
||||
|
||||
let to_org_table = to_ast::<OrgTable>(org_table_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_org_table("|").syntax,
|
||||
@r###"
|
||||
ORG_TABLE@0..1
|
||||
ORG_TABLE_STANDARD_ROW@0..1
|
||||
PIPE@0..1 "|"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_org_table(
|
||||
r#"|
|
||||
|-
|
||||
|a
|
||||
|-
|
||||
| a |
|
||||
"#
|
||||
).syntax,
|
||||
@r###"
|
||||
ORG_TABLE@0..20
|
||||
ORG_TABLE_STANDARD_ROW@0..2
|
||||
PIPE@0..1 "|"
|
||||
WHITESPACE@1..2 "\n"
|
||||
ORG_TABLE_RULE_ROW@2..5
|
||||
TEXT@2..5 "|-\n"
|
||||
ORG_TABLE_STANDARD_ROW@5..8
|
||||
PIPE@5..6 "|"
|
||||
ORG_TABLE_CELL@6..7
|
||||
TEXT@6..7 "a"
|
||||
WHITESPACE@7..8 "\n"
|
||||
ORG_TABLE_RULE_ROW@8..11
|
||||
TEXT@8..11 "|-\n"
|
||||
ORG_TABLE_STANDARD_ROW@11..20
|
||||
PIPE@11..12 "|"
|
||||
WHITESPACE@12..15 " "
|
||||
ORG_TABLE_CELL@15..16
|
||||
TEXT@15..16 "a"
|
||||
WHITESPACE@16..18 " "
|
||||
PIPE@18..19 "|"
|
||||
WHITESPACE@19..20 "\n"
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_table_el() {
|
||||
use crate::{ast::TableEl, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_table_el = to_ast::<TableEl>(table_el_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_table_el(
|
||||
r#" +---+
|
||||
| |
|
||||
+---+
|
||||
|
||||
"#
|
||||
).syntax,
|
||||
@r###"
|
||||
TABLE_EL@0..37
|
||||
TEXT@0..32 " +---+\n | |\n ..."
|
||||
BLANK_LINE@32..33
|
||||
NEW_LINE@32..33 "\n"
|
||||
BLANK_LINE@33..37
|
||||
WHITESPACE@33..37 " "
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(table_el_node(("", config).into()).is_err());
|
||||
assert!(table_el_node(("+----|---", config).into()).is_err());
|
||||
}
|
||||
64
src/syntax/target.rs
Normal file
64
src/syntax/target.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use nom::{
|
||||
bytes::complete::take_while,
|
||||
combinator::{map, verify},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{debug_assert_lossless, l_angle2_token, node, r_angle2_token, GreenElement},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn target_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
l_angle2_token,
|
||||
verify(
|
||||
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|
||||
|s: &Input| {
|
||||
s.as_str().starts_with(|c| c != ' ') && s.as_str().ends_with(|c| c != ' ')
|
||||
},
|
||||
),
|
||||
r_angle2_token,
|
||||
)),
|
||||
|(l_angle2, target, r_angle2)| node(TARGET, [l_angle2, target.text_token(), r_angle2]),
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Target, tests::to_ast, ParseConfig};
|
||||
|
||||
let to_target = to_ast::<Target>(target_node);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_target("<<target>>").syntax,
|
||||
@r###"
|
||||
TARGET@0..10
|
||||
L_ANGLE2@0..2 "<<"
|
||||
TEXT@2..8 "target"
|
||||
R_ANGLE2@8..10 ">>"
|
||||
"###
|
||||
);
|
||||
|
||||
insta::assert_debug_snapshot!(
|
||||
to_target("<<tar get>>").syntax,
|
||||
@r###"
|
||||
TARGET@0..11
|
||||
L_ANGLE2@0..2 "<<"
|
||||
TEXT@2..9 "tar get"
|
||||
R_ANGLE2@9..11 ">>"
|
||||
"###
|
||||
);
|
||||
|
||||
let config = &ParseConfig::default();
|
||||
|
||||
assert!(target_node(("<<target >>", config).into()).is_err());
|
||||
assert!(target_node(("<< target>>", config).into()).is_err());
|
||||
assert!(target_node(("<<ta<get>>", config).into()).is_err());
|
||||
assert!(target_node(("<<ta>get>>", config).into()).is_err());
|
||||
assert!(target_node(("<<ta\nget>>", config).into()).is_err());
|
||||
assert!(target_node(("<<target>", config).into()).is_err());
|
||||
}
|
||||
326
src/syntax/timestamp.rs
Normal file
326
src/syntax/timestamp.rs
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
use nom::{
|
||||
bytes::complete::{take, take_till, take_while},
|
||||
character::complete::{space0, space1},
|
||||
combinator::{map, opt, verify},
|
||||
sequence::tuple,
|
||||
IResult,
|
||||
};
|
||||
|
||||
use super::{
|
||||
combinator::{
|
||||
colon_token, debug_assert_lossless, l_angle_token, l_bracket_token, l_parens_token,
|
||||
minus2_token, minus_token, node, percent2_token, r_angle_token, r_bracket_token,
|
||||
r_parens_token, GreenElement, NodeBuilder,
|
||||
},
|
||||
input::Input,
|
||||
SyntaxKind::*,
|
||||
};
|
||||
|
||||
pub fn timestamp_diary_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(map(
|
||||
tuple((
|
||||
l_angle_token,
|
||||
percent2_token,
|
||||
l_parens_token,
|
||||
take_till(|c| c == ')' || c == '>' || c == '\n'),
|
||||
r_parens_token,
|
||||
r_angle_token,
|
||||
)),
|
||||
|(l_angle, percent2, l_paren, value, r_paren, r_angle)| {
|
||||
node(
|
||||
TIMESTAMP_DIARY,
|
||||
[
|
||||
l_angle,
|
||||
percent2,
|
||||
l_paren,
|
||||
value.text_token(),
|
||||
r_paren,
|
||||
r_angle,
|
||||
],
|
||||
)
|
||||
},
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn is_digit_str(s: &Input) -> bool {
|
||||
s.as_str().bytes().all(|u| u.is_ascii_digit())
|
||||
}
|
||||
|
||||
fn date(i: Input) -> IResult<Input, [GreenElement; 7], ()> {
|
||||
map(
|
||||
tuple((
|
||||
verify(take(4usize), is_digit_str),
|
||||
minus_token,
|
||||
verify(take(2usize), is_digit_str),
|
||||
minus_token,
|
||||
verify(take(2usize), is_digit_str),
|
||||
space1,
|
||||
take_while(|c: char| {
|
||||
!c.is_ascii_whitespace()
|
||||
&& !c.is_ascii_digit()
|
||||
&& c != '+'
|
||||
&& c != '-'
|
||||
&& c != ']'
|
||||
&& c != '>'
|
||||
}),
|
||||
)),
|
||||
|(year, minus, month, minus_, day, ws, dayname)| {
|
||||
[
|
||||
year.token(TIMESTAMP_YEAR),
|
||||
minus,
|
||||
month.token(TIMESTAMP_MONTH),
|
||||
minus_,
|
||||
day.token(TIMESTAMP_DAY),
|
||||
ws.ws_token(),
|
||||
dayname.token(TIMESTAMP_DAYNAME),
|
||||
]
|
||||
},
|
||||
)(i)
|
||||
}
|
||||
|
||||
fn time(i: Input) -> IResult<Input, [GreenElement; 3], ()> {
|
||||
map(
|
||||
tuple((
|
||||
verify(take(2usize), is_digit_str),
|
||||
colon_token,
|
||||
verify(take(2usize), is_digit_str),
|
||||
)),
|
||||
|(hour, colon, minute)| {
|
||||
[
|
||||
hour.token(TIMESTAMP_HOUR),
|
||||
colon,
|
||||
minute.token(TIMESTAMP_MINUTE),
|
||||
]
|
||||
},
|
||||
)(i)
|
||||
}
|
||||
|
||||
fn timestamp_active_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, l_angle) = l_angle_token(input)?;
|
||||
let (input, start_date) = date(input)?;
|
||||
let (input, start_time) = opt(tuple((space1, time)))(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
b.push(l_angle);
|
||||
b.children.extend(start_date);
|
||||
|
||||
if input.as_str().starts_with('-') {
|
||||
let (ws, start_time) = match start_time {
|
||||
Some(start_time) => start_time,
|
||||
None => return Err(nom::Err::Error(())),
|
||||
};
|
||||
|
||||
let (input, minus) = minus_token(input)?;
|
||||
let (input, end_time) = time(input)?;
|
||||
let (input, space) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, r_angle) = r_angle_token(input)?;
|
||||
|
||||
b.ws(ws);
|
||||
b.children.extend(start_time);
|
||||
b.push(minus);
|
||||
b.children.extend(end_time);
|
||||
b.ws(space);
|
||||
b.push(r_angle);
|
||||
|
||||
return Ok((input, b.finish(TIMESTAMP_ACTIVE)));
|
||||
}
|
||||
|
||||
let (input, space) = space0(input)?;
|
||||
let (input, r_angle) = r_angle_token(input)?;
|
||||
|
||||
if let Some((ws, start_time)) = start_time {
|
||||
b.ws(ws);
|
||||
b.children.extend(start_time);
|
||||
}
|
||||
|
||||
b.ws(space);
|
||||
b.push(r_angle);
|
||||
|
||||
if input.as_str().starts_with("--<") {
|
||||
let (input, minus2) = minus2_token(input)?;
|
||||
let (input, l_angle) = l_angle_token(input)?;
|
||||
let (input, end_date) = date(input)?;
|
||||
let (input, end_time) = opt(tuple((space1, time)))(input)?;
|
||||
let (input, space_) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, r_angle) = r_angle_token(input)?;
|
||||
|
||||
b.children.extend([minus2, l_angle]);
|
||||
b.children.extend(end_date);
|
||||
|
||||
if let Some((ws, end_time)) = end_time {
|
||||
b.ws(ws);
|
||||
b.children.extend(end_time);
|
||||
}
|
||||
|
||||
b.ws(space_);
|
||||
b.push(r_angle);
|
||||
|
||||
Ok((input, b.finish(TIMESTAMP_ACTIVE)))
|
||||
} else {
|
||||
Ok((input, b.finish(TIMESTAMP_ACTIVE)))
|
||||
}
|
||||
}
|
||||
|
||||
fn timestamp_inactive_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
let (input, l_bracket) = l_bracket_token(input)?;
|
||||
let (input, start_date) = date(input)?;
|
||||
let (input, start_time) = opt(tuple((space1, time)))(input)?;
|
||||
|
||||
let mut b = NodeBuilder::new();
|
||||
b.push(l_bracket);
|
||||
b.children.extend(start_date);
|
||||
|
||||
if input.s.starts_with('-') {
|
||||
let (ws, start_time) = match start_time {
|
||||
Some(start_time) => start_time,
|
||||
None => return Err(nom::Err::Error(())),
|
||||
};
|
||||
|
||||
let (input, minus) = minus_token(input)?;
|
||||
let (input, end_time) = time(input)?;
|
||||
let (input, space) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, r_bracket) = r_bracket_token(input)?;
|
||||
|
||||
b.ws(ws);
|
||||
b.children.extend(start_time);
|
||||
b.push(minus);
|
||||
b.children.extend(end_time);
|
||||
b.ws(space);
|
||||
b.push(r_bracket);
|
||||
|
||||
return Ok((input, b.finish(TIMESTAMP_INACTIVE)));
|
||||
}
|
||||
|
||||
let (input, space) = space0(input)?;
|
||||
let (input, r_bracket) = r_bracket_token(input)?;
|
||||
|
||||
if let Some((ws, start_time)) = start_time {
|
||||
b.ws(ws);
|
||||
b.children.extend(start_time);
|
||||
}
|
||||
|
||||
b.ws(space);
|
||||
b.push(r_bracket);
|
||||
|
||||
if input.s.starts_with("--[") {
|
||||
let (input, minus2) = minus2_token(input)?;
|
||||
let (input, l_bracket) = l_bracket_token(input)?;
|
||||
let (input, end_date) = date(input)?;
|
||||
let (input, end_time) = opt(tuple((space1, time)))(input)?;
|
||||
let (input, space_) = space0(input)?;
|
||||
// TODO: delay-or-repeater
|
||||
let (input, r_bracket) = r_bracket_token(input)?;
|
||||
|
||||
b.children.extend([minus2, l_bracket]);
|
||||
b.children.extend(end_date);
|
||||
|
||||
if let Some((ws, end_time)) = end_time {
|
||||
b.ws(ws);
|
||||
b.children.extend(end_time);
|
||||
}
|
||||
|
||||
b.ws(space_);
|
||||
b.push(r_bracket);
|
||||
|
||||
Ok((input, b.finish(TIMESTAMP_INACTIVE)))
|
||||
} else {
|
||||
Ok((input, b.finish(TIMESTAMP_INACTIVE)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp_active_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(timestamp_active_node_base)(input)
|
||||
}
|
||||
pub fn timestamp_inactive_node(input: Input) -> IResult<Input, GreenElement, ()> {
|
||||
debug_assert_lossless(timestamp_inactive_node_base)(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
use crate::{ast::Timestamp, tests::to_ast};
|
||||
|
||||
let to_timestamp = to_ast::<Timestamp>(timestamp_inactive_node);
|
||||
|
||||
let ts = to_timestamp("[2003-09-16 Tue]");
|
||||
assert!(!ts.is_range());
|
||||
insta::assert_debug_snapshot!(
|
||||
ts.syntax,
|
||||
@r###"
|
||||
TIMESTAMP_INACTIVE@0..16
|
||||
L_BRACKET@0..1 "["
|
||||
TIMESTAMP_YEAR@1..5 "2003"
|
||||
MINUS@5..6 "-"
|
||||
TIMESTAMP_MONTH@6..8 "09"
|
||||
MINUS@8..9 "-"
|
||||
TIMESTAMP_DAY@9..11 "16"
|
||||
WHITESPACE@11..12 " "
|
||||
TIMESTAMP_DAYNAME@12..15 "Tue"
|
||||
R_BRACKET@15..16 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
let ts = to_timestamp("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]");
|
||||
assert!(ts.is_range());
|
||||
insta::assert_debug_snapshot!(
|
||||
ts.syntax,
|
||||
@r###"
|
||||
TIMESTAMP_INACTIVE@0..46
|
||||
L_BRACKET@0..1 "["
|
||||
TIMESTAMP_YEAR@1..5 "2003"
|
||||
MINUS@5..6 "-"
|
||||
TIMESTAMP_MONTH@6..8 "09"
|
||||
MINUS@8..9 "-"
|
||||
TIMESTAMP_DAY@9..11 "16"
|
||||
WHITESPACE@11..12 " "
|
||||
TIMESTAMP_DAYNAME@12..15 "Tue"
|
||||
WHITESPACE@15..16 " "
|
||||
TIMESTAMP_HOUR@16..18 "09"
|
||||
COLON@18..19 ":"
|
||||
TIMESTAMP_MINUTE@19..21 "39"
|
||||
R_BRACKET@21..22 "]"
|
||||
MINUS2@22..24 "--"
|
||||
L_BRACKET@24..25 "["
|
||||
TIMESTAMP_YEAR@25..29 "2003"
|
||||
MINUS@29..30 "-"
|
||||
TIMESTAMP_MONTH@30..32 "09"
|
||||
MINUS@32..33 "-"
|
||||
TIMESTAMP_DAY@33..35 "16"
|
||||
WHITESPACE@35..36 " "
|
||||
TIMESTAMP_DAYNAME@36..39 "Tue"
|
||||
WHITESPACE@39..40 " "
|
||||
TIMESTAMP_HOUR@40..42 "10"
|
||||
COLON@42..43 ":"
|
||||
TIMESTAMP_MINUTE@43..45 "39"
|
||||
R_BRACKET@45..46 "]"
|
||||
"###
|
||||
);
|
||||
|
||||
let ts = to_timestamp("[2003-09-16 Tue 09:39-10:39]");
|
||||
assert!(ts.is_range());
|
||||
insta::assert_debug_snapshot!(
|
||||
ts.syntax,
|
||||
@r###"
|
||||
TIMESTAMP_INACTIVE@0..28
|
||||
L_BRACKET@0..1 "["
|
||||
TIMESTAMP_YEAR@1..5 "2003"
|
||||
MINUS@5..6 "-"
|
||||
TIMESTAMP_MONTH@6..8 "09"
|
||||
MINUS@8..9 "-"
|
||||
TIMESTAMP_DAY@9..11 "16"
|
||||
WHITESPACE@11..12 " "
|
||||
TIMESTAMP_DAYNAME@12..15 "Tue"
|
||||
WHITESPACE@15..16 " "
|
||||
TIMESTAMP_HOUR@16..18 "09"
|
||||
COLON@18..19 ":"
|
||||
TIMESTAMP_MINUTE@19..21 "39"
|
||||
MINUS@21..22 "-"
|
||||
TIMESTAMP_HOUR@22..24 "10"
|
||||
COLON@24..25 ":"
|
||||
TIMESTAMP_MINUTE@25..27 "39"
|
||||
R_BRACKET@27..28 "]"
|
||||
"###
|
||||
);
|
||||
}
|
||||
24
src/tests.rs
Normal file
24
src/tests.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//! test utils
|
||||
|
||||
use nom::IResult;
|
||||
use rowan::{ast::AstNode, SyntaxNode};
|
||||
|
||||
use crate::{
|
||||
syntax::{combinator::GreenElement, input::Input},
|
||||
ParseConfig,
|
||||
};
|
||||
|
||||
pub fn to_ast<N: AstNode>(
|
||||
parser: impl Fn(Input) -> IResult<Input, GreenElement, ()>,
|
||||
) -> impl Fn(&str) -> N {
|
||||
move |s: &str| {
|
||||
let input = Input {
|
||||
s,
|
||||
c: &ParseConfig::default(),
|
||||
};
|
||||
let element = parser(input).unwrap().1;
|
||||
let node = element.into_node().unwrap();
|
||||
let node = SyntaxNode::<N::Language>::new_root(node);
|
||||
AstNode::cast(node).unwrap()
|
||||
}
|
||||
}
|
||||
209
src/validate.rs
209
src/validate.rs
|
|
@ -1,209 +0,0 @@
|
|||
use indextree::NodeId;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use crate::elements::{Element, Table, TableCell, TableRow};
|
||||
use crate::Org;
|
||||
|
||||
/// Validation Error
|
||||
#[derive(Debug)]
|
||||
pub enum ValidationError {
|
||||
/// Expected at least one child
|
||||
ExpectedChildren {
|
||||
at: NodeId,
|
||||
},
|
||||
/// Expected no children
|
||||
UnexpectedChildren {
|
||||
at: NodeId,
|
||||
},
|
||||
UnexpectedElement {
|
||||
expected: &'static str,
|
||||
at: NodeId,
|
||||
},
|
||||
/// Expected a detached element
|
||||
ExpectedDetached {
|
||||
at: NodeId,
|
||||
},
|
||||
/// Expected headline level in specify range
|
||||
HeadlineLevelMismatch {
|
||||
range: RangeInclusive<usize>,
|
||||
at: NodeId,
|
||||
},
|
||||
}
|
||||
|
||||
impl ValidationError {
|
||||
pub fn element<'a, 'b>(&self, org: &'a Org<'b>) -> &'a Element<'b> {
|
||||
match self {
|
||||
ValidationError::ExpectedChildren { at }
|
||||
| ValidationError::UnexpectedChildren { at }
|
||||
| ValidationError::UnexpectedElement { at, .. }
|
||||
| ValidationError::ExpectedDetached { at }
|
||||
| ValidationError::HeadlineLevelMismatch { at, .. } => &org[*at],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ValidationResult<T> = Result<T, ValidationError>;
|
||||
|
||||
impl Org<'_> {
|
||||
/// Validates an `Org` struct.
|
||||
pub fn validate(&self) -> Vec<ValidationError> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
macro_rules! expect_element {
|
||||
($node:ident, $expect:expr, $($pattern:pat)|+) => {
|
||||
match self[$node] {
|
||||
$($pattern)|+ => (),
|
||||
_ => errors.push(ValidationError::UnexpectedElement {
|
||||
expected: $expect,
|
||||
at: $node
|
||||
}),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! expect_children {
|
||||
($node:ident) => {
|
||||
if self.arena[$node].first_child().is_none() {
|
||||
errors.push(ValidationError::ExpectedChildren { at: $node });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for node_id in self.root.descendants(&self.arena) {
|
||||
let node = &self.arena[node_id];
|
||||
match node.get() {
|
||||
Element::Document { .. } => {
|
||||
let mut children = node_id.children(&self.arena);
|
||||
if let Some(child) = children.next() {
|
||||
expect_element!(
|
||||
child,
|
||||
"Headline|Section",
|
||||
Element::Headline { .. } | Element::Section
|
||||
);
|
||||
}
|
||||
|
||||
for child in children {
|
||||
expect_element!(child, "Headline", Element::Headline { .. });
|
||||
}
|
||||
}
|
||||
Element::Headline { .. } => {
|
||||
expect_children!(node_id);
|
||||
|
||||
let mut children = node_id.children(&self.arena);
|
||||
if let Some(child) = children.next() {
|
||||
expect_element!(child, "Title", Element::Title(_));
|
||||
}
|
||||
|
||||
if let Some(child) = children.next() {
|
||||
expect_element!(
|
||||
child,
|
||||
"Headline|Section",
|
||||
Element::Headline { .. } | Element::Section
|
||||
);
|
||||
}
|
||||
|
||||
for child in children {
|
||||
expect_element!(child, "Headline", Element::Headline { .. });
|
||||
}
|
||||
}
|
||||
Element::Title(title) => {
|
||||
if !title.raw.is_empty() && node.first_child().is_none() {
|
||||
errors.push(ValidationError::ExpectedChildren { at: node_id });
|
||||
}
|
||||
}
|
||||
Element::List(_) => {
|
||||
expect_children!(node_id);
|
||||
for child in node_id.children(&self.arena) {
|
||||
expect_element!(child, "ListItem", Element::ListItem(_));
|
||||
}
|
||||
}
|
||||
Element::Table(Table::Org { .. }) => {
|
||||
for child in node_id.children(&self.arena) {
|
||||
expect_element!(child, "TableRow", Element::TableRow(_));
|
||||
}
|
||||
}
|
||||
Element::TableRow(TableRow::Header) => {
|
||||
for child in node_id.children(&self.arena) {
|
||||
expect_element!(
|
||||
child,
|
||||
"TableCell::Header",
|
||||
Element::TableCell(TableCell::Header)
|
||||
);
|
||||
}
|
||||
}
|
||||
Element::TableRow(TableRow::Body) => {
|
||||
for child in node_id.children(&self.arena) {
|
||||
expect_element!(
|
||||
child,
|
||||
"TableCell::Body",
|
||||
Element::TableCell(TableCell::Body)
|
||||
);
|
||||
}
|
||||
}
|
||||
Element::CommentBlock(_)
|
||||
| Element::ExampleBlock(_)
|
||||
| Element::ExportBlock(_)
|
||||
| Element::SourceBlock(_)
|
||||
| Element::BabelCall(_)
|
||||
| Element::InlineSrc(_)
|
||||
| Element::Code { .. }
|
||||
| Element::FnRef(_)
|
||||
| Element::InlineCall(_)
|
||||
| Element::Link(_)
|
||||
| Element::Macros(_)
|
||||
| Element::RadioTarget
|
||||
| Element::Snippet(_)
|
||||
| Element::Target(_)
|
||||
| Element::Text { .. }
|
||||
| Element::Timestamp(_)
|
||||
| Element::Verbatim { .. }
|
||||
| Element::FnDef(_)
|
||||
| Element::Clock(_)
|
||||
| Element::Comment { .. }
|
||||
| Element::FixedWidth { .. }
|
||||
| Element::Keyword(_)
|
||||
| Element::Rule(_)
|
||||
| Element::Cookie(_)
|
||||
| Element::TableRow(TableRow::BodyRule)
|
||||
| Element::TableRow(TableRow::HeaderRule) => {
|
||||
if node.first_child().is_some() {
|
||||
errors.push(ValidationError::UnexpectedChildren { at: node_id });
|
||||
}
|
||||
}
|
||||
Element::SpecialBlock(_)
|
||||
| Element::QuoteBlock(_)
|
||||
| Element::CenterBlock(_)
|
||||
| Element::VerseBlock(_)
|
||||
| Element::Paragraph { .. }
|
||||
| Element::Section
|
||||
| Element::Bold
|
||||
| Element::Italic
|
||||
| Element::Underline
|
||||
| Element::Strike
|
||||
| Element::DynBlock(_) => {
|
||||
expect_children!(node_id);
|
||||
}
|
||||
Element::ListItem(_)
|
||||
| Element::Drawer(_)
|
||||
| Element::TableCell(_)
|
||||
| Element::Table(_) => (),
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
pub(crate) fn debug_validate(&self) {
|
||||
if cfg!(debug_assertions) {
|
||||
let errors = self.validate();
|
||||
if !errors.is_empty() {
|
||||
eprintln!("Org validation failed. {} error(s) found:", errors.len());
|
||||
for err in errors {
|
||||
eprintln!("{:?} at {:?}", err, err.element(self));
|
||||
}
|
||||
panic!(
|
||||
"Looks like there's a bug in orgize! Please report it with your org-mode content at https://github.com/PoiScript/orgize/issues."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/wasm/mod.rs
189
src/wasm/mod.rs
|
|
@ -1,189 +0,0 @@
|
|||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde_wasm_bindgen::Serializer;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{Element, Event};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Org(crate::Org<'static>);
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Org {
|
||||
#[wasm_bindgen]
|
||||
pub fn parse(input: String) -> Self {
|
||||
Org(crate::Org::parse_string(input))
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toJson)]
|
||||
pub fn to_json(&self) -> JsValue {
|
||||
to_value(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(skip_typescript)]
|
||||
pub type Handler;
|
||||
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn text(this: &Handler, text: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn code(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn cookie(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn rule(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = exampleBlock)]
|
||||
pub fn example_block(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = exportBlock)]
|
||||
pub fn export_block(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = sourceBlock)]
|
||||
pub fn source_block(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = inlineSrc)]
|
||||
pub fn inline_src(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn link(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn snippet(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn timestamp(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn verbatim(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn fixedWidth(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn keyword(this: &Handler, item: JsValue);
|
||||
|
||||
#[wasm_bindgen(method, js_name = listStart)]
|
||||
pub fn list_start(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = listEnd)]
|
||||
pub fn list_end(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = tableStart)]
|
||||
pub fn table_start(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = tableEnd)]
|
||||
pub fn table_end(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = tableRowStart)]
|
||||
pub fn table_row_start(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = tableRowEnd)]
|
||||
pub fn table_row_end(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = tableCellStart)]
|
||||
pub fn table_cell_start(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = tableCellEnd)]
|
||||
pub fn table_cell_end(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = titleStart)]
|
||||
pub fn title_start(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = titleEnd)]
|
||||
pub fn title_end(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = boldStart)]
|
||||
pub fn bold_start(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = boldEnd)]
|
||||
pub fn bold_end(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = centerBlockStart)]
|
||||
pub fn center_block_start(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = centerBlockEnd)]
|
||||
pub fn center_block_end(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = documentStart)]
|
||||
pub fn document_start(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = documentEnd)]
|
||||
pub fn document_end(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = italicStart)]
|
||||
pub fn italic_start(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = italicEnd)]
|
||||
pub fn italic_end(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = listItemStart)]
|
||||
pub fn list_item_start(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = listItemEnd)]
|
||||
pub fn list_item_end(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = paragraphStart)]
|
||||
pub fn paragraph_start(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = paragraphEnd)]
|
||||
pub fn paragraph_end(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = quoteBlockStart)]
|
||||
pub fn quote_block_start(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = quoteBlockEnd)]
|
||||
pub fn quote_block_end(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = sectionStart)]
|
||||
pub fn section_start(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = sectionEnd)]
|
||||
pub fn section_end(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = strikeStart)]
|
||||
pub fn strike_start(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = strikeEnd)]
|
||||
pub fn strike_end(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = underlineStart)]
|
||||
pub fn underline_start(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = underlineEnd)]
|
||||
pub fn underline_end(this: &Handler);
|
||||
#[wasm_bindgen(method, js_name = verseBlockStart)]
|
||||
pub fn verse_block_start(this: &Handler, item: JsValue);
|
||||
#[wasm_bindgen(method, js_name = verseBlockEnd)]
|
||||
pub fn verse_block_end(this: &Handler, item: JsValue);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn handle(org: &Org, handler: Handler) {
|
||||
for event in org.0.iter() {
|
||||
use Element::*;
|
||||
|
||||
match event {
|
||||
Event::Start(Text { value }) => handler.text(JsValue::from_str(value)),
|
||||
Event::Start(ExampleBlock(block)) => handler.example_block(to_value(block)),
|
||||
Event::Start(ExportBlock(block)) => handler.export_block(to_value(block)),
|
||||
Event::Start(SourceBlock(block)) => handler.source_block(to_value(block)),
|
||||
Event::Start(InlineSrc(src)) => handler.inline_src(to_value(src)),
|
||||
Event::Start(Code { value }) => handler.code(JsValue::from_str(value)),
|
||||
Event::Start(Link(link)) => handler.link(to_value(link)),
|
||||
Event::Start(Snippet(snippet)) => handler.snippet(to_value(snippet)),
|
||||
Event::Start(Timestamp(timestamp)) => handler.timestamp(to_value(timestamp)),
|
||||
Event::Start(Verbatim { value }) => handler.verbatim(JsValue::from_str(value)),
|
||||
Event::Start(FixedWidth(fixed_width)) => handler.fixedWidth(to_value(fixed_width)),
|
||||
Event::Start(Rule(_)) => handler.rule(),
|
||||
Event::Start(Cookie(cookie)) => handler.cookie(to_value(cookie)),
|
||||
Event::Start(Keyword(keyword)) => handler.keyword(to_value(keyword)),
|
||||
|
||||
Event::Start(Table(table)) => handler.table_start(to_value(table)),
|
||||
Event::End(Table(table)) => handler.table_start(to_value(table)),
|
||||
Event::Start(TableRow(row)) => handler.table_row_start(to_value(row)),
|
||||
Event::End(TableRow(row)) => handler.table_row_start(to_value(row)),
|
||||
Event::Start(TableCell(cell)) => handler.table_cell_start(to_value(cell)),
|
||||
Event::End(TableCell(cell)) => handler.table_cell_start(to_value(cell)),
|
||||
Event::Start(Title(title)) => handler.title_start(to_value(title)),
|
||||
Event::End(Title(title)) => handler.title_end(to_value(title)),
|
||||
Event::Start(QuoteBlock(block)) => handler.quote_block_start(to_value(block)),
|
||||
Event::End(QuoteBlock(block)) => handler.quote_block_end(to_value(block)),
|
||||
Event::Start(CenterBlock(block)) => handler.center_block_start(to_value(block)),
|
||||
Event::End(CenterBlock(block)) => handler.center_block_end(to_value(block)),
|
||||
Event::Start(VerseBlock(block)) => handler.verse_block_start(to_value(block)),
|
||||
Event::End(VerseBlock(block)) => handler.verse_block_end(to_value(block)),
|
||||
Event::Start(Bold) => handler.bold_start(),
|
||||
Event::End(Bold) => handler.bold_end(),
|
||||
Event::Start(Document { .. }) => handler.document_start(),
|
||||
Event::End(Document { .. }) => handler.document_end(),
|
||||
Event::Start(List(list)) => handler.list_start(to_value(list)),
|
||||
Event::End(List(list)) => handler.list_end(to_value(list)),
|
||||
Event::Start(Italic) => handler.italic_start(),
|
||||
Event::End(Italic) => handler.italic_end(),
|
||||
Event::Start(ListItem(_)) => handler.list_item_start(),
|
||||
Event::End(ListItem(_)) => handler.list_item_end(),
|
||||
Event::Start(Paragraph { .. }) => handler.paragraph_start(),
|
||||
Event::End(Paragraph { .. }) => handler.paragraph_end(),
|
||||
Event::Start(Section) => handler.section_start(),
|
||||
Event::End(Section) => handler.section_end(),
|
||||
Event::Start(Strike) => handler.strike_start(),
|
||||
Event::End(Strike) => handler.strike_end(),
|
||||
Event::Start(Underline) => handler.underline_start(),
|
||||
Event::End(Underline) => handler.underline_end(),
|
||||
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_value<T: Serialize + ?Sized>(value: &T) -> JsValue {
|
||||
value
|
||||
.serialize(&Serializer::new().serialize_maps_as_objects(true))
|
||||
.unwrap()
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
use orgize::Org;
|
||||
|
||||
const ORG_STR: &str = r#"
|
||||
|
||||
#+TITLE: org
|
||||
|
||||
#+BEGIN_QUOTE
|
||||
|
||||
CONTENTS
|
||||
|
||||
#+END_QUOTE
|
||||
|
||||
* Headline 1
|
||||
SCHEDULED: <2019-10-28 Mon>
|
||||
:PROPERTIES:
|
||||
:ID: headline-1
|
||||
:END:
|
||||
|
||||
:LOGBOOK:
|
||||
|
||||
CLOCK: [2019-10-28 Mon 08:53]
|
||||
|
||||
CLOCK: [2019-10-28 Mon 08:53]--[2019-10-28 Mon 08:53] => 0:00
|
||||
|
||||
:END:
|
||||
|
||||
-----
|
||||
|
||||
#+CALL: VALUE
|
||||
|
||||
#
|
||||
# Comment
|
||||
#
|
||||
|
||||
#+BEGIN: NAME PARAMETERS
|
||||
|
||||
CONTENTS
|
||||
|
||||
#+END:
|
||||
|
||||
:
|
||||
: Fixed width
|
||||
:
|
||||
|
||||
#+BEGIN_COMMENT
|
||||
|
||||
COMMENT
|
||||
|
||||
#+END_COMMENT
|
||||
|
||||
#+BEGIN_EXAMPLE
|
||||
#+END_EXAMPLE
|
||||
|
||||
1. 1
|
||||
|
||||
2. 2
|
||||
|
||||
3. 3
|
||||
|
||||
+ 1
|
||||
|
||||
+ 2
|
||||
|
||||
- 3
|
||||
|
||||
- 4
|
||||
|
||||
+ 5
|
||||
|
||||
|
||||
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn blank() {
|
||||
let org = Org::parse(ORG_STR);
|
||||
|
||||
let mut writer = Vec::new();
|
||||
org.write_org(&mut writer).unwrap();
|
||||
|
||||
// eprintln!("{}", serde_json::to_string_pretty(&org).unwrap());
|
||||
|
||||
assert_eq!(String::from_utf8(writer).unwrap(), ORG_STR);
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
use orgize::Org;
|
||||
|
||||
#[test]
|
||||
fn bad_headline_tags() {
|
||||
contains_no_tag(Org::parse("* a ::"));
|
||||
|
||||
contains_no_tag(Org::parse("* a :(:"));
|
||||
|
||||
contains_one_tag(Org::parse("* a \t:_:"), "_");
|
||||
|
||||
contains_one_tag(Org::parse("* a \t :@:"), "@");
|
||||
|
||||
contains_one_tag(Org::parse("* a :#:"), "#");
|
||||
|
||||
contains_one_tag(Org::parse("* a\t :%:"), "%");
|
||||
|
||||
contains_one_tag(Org::parse("* a :余:"), "余");
|
||||
}
|
||||
|
||||
fn contains_no_tag(org: Org) {
|
||||
assert!(org.headlines().next().unwrap().title(&org).tags.is_empty());
|
||||
}
|
||||
|
||||
fn contains_one_tag(org: Org, tag: &str) {
|
||||
assert_eq!(vec![tag], org.headlines().next().unwrap().title(&org).tags);
|
||||
}
|
||||
|
|
@ -5,11 +5,7 @@ macro_rules! test_suite {
|
|||
($name:ident, $content:expr, $expected:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut writer = Vec::new();
|
||||
let org = Org::parse($content);
|
||||
org.write_html(&mut writer).unwrap();
|
||||
let string = String::from_utf8(writer).unwrap();
|
||||
assert_eq!(string, $expected);
|
||||
assert_eq!(Org::parse($content).to_html(), $expected);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
12
wasm/Cargo.toml
Normal file
12
wasm/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "orgize-wasm"
|
||||
version = "0.1.0"
|
||||
publish = false
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
orgize = { path = ".." }
|
||||
wasm-bindgen = "0.2"
|
||||
|
|
@ -2,40 +2,61 @@
|
|||
|
||||

|
||||
|
||||
## Quick start
|
||||
|
||||
Install the package:
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install orgize
|
||||
yarn add orgize
|
||||
```
|
||||
|
||||
Load the wasm module and init:
|
||||
|
||||
### Browser
|
||||
## Browser
|
||||
|
||||
```js
|
||||
import { init, renderHtml } from "orgize";
|
||||
import init, { Org } from "orgize";
|
||||
|
||||
init().then(() => {
|
||||
console.log(renderHtml("* Hello, /world/!"));
|
||||
const org = new Org("* Hello, /world/!");
|
||||
const html = org.html();
|
||||
console.log(html);
|
||||
org.free();
|
||||
});
|
||||
```
|
||||
|
||||
### Node.js
|
||||
## Node.js
|
||||
|
||||
```js
|
||||
const { init, renderHtml } = require("orgize");
|
||||
const { readFile } = require("fs/promises");
|
||||
import { Org, initSync } from "orgize";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
readFile(require.resolve("orgize/lib/orgize_bg.wasm"))
|
||||
.then((bytes) => init(new WebAssembly.Module(bytes)))
|
||||
.then(() => {
|
||||
console.log(renderHtml("* Hello, /world/!"));
|
||||
});
|
||||
// you can also use import.meta.resolve, but it's currently behind
|
||||
// an experimental flag --experimental-import-meta-resolve
|
||||
import { createRequire } from "node:module";
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
readFile(require.resolve("orgize/wasm")).then((bytes) => {
|
||||
initSync(bytes);
|
||||
|
||||
const org = new Org("* Hello, /world/!");
|
||||
const html = org.html();
|
||||
console.log(html);
|
||||
org.free();
|
||||
});
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
1. You must **initialize** the WebAssembly module (using either `init` or
|
||||
`initSync` function) before using the `Org` class;
|
||||
|
||||
2. Don't forgot to call `org.free()` to **release the memory** that
|
||||
allocated by Rust;
|
||||
|
||||
3. This npm package is primarily aim to demonstrate and power the online
|
||||
demo, so it doesn't provide any customization or settings.
|
||||
|
||||
If you need to, please build your own npm package by `wasm-pack`.
|
||||
(or `napi` if you're only targeting node.js users)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
|
|||
22
wasm/build.rs
Normal file
22
wasm/build.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
{
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
println!("cargo:rustc-env=CARGO_GIT_HASH={}", git_hash);
|
||||
}
|
||||
|
||||
{
|
||||
let output = Command::new("date").args(["-R"]).output().unwrap();
|
||||
|
||||
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||
|
||||
println!("cargo:rustc-env=CARGO_BUILD_TIME={}", git_hash);
|
||||
}
|
||||
}
|
||||
309
wasm/index.html
309
wasm/index.html
|
|
@ -1,71 +1,274 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Orgize wasm demo</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script defer src="https://unpkg.com/alpinejs@3/dist/cdn.min.js"></script>
|
||||
<title>Orgize</title>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-light.min.css"
|
||||
/>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.0/beautify-html.js"></script>
|
||||
|
||||
<script src="./orgize.umd.js"></script>
|
||||
<style>
|
||||
h3 {
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body > * {
|
||||
margin-bottom: 16px;
|
||||
.bordered {
|
||||
overflow: auto;
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#took {
|
||||
color: #808080;
|
||||
font-size: 14px;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
#tabsList {
|
||||
align-items: center;
|
||||
border-bottom-color: #dbdbdb;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
justify-content: flex-start;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#tabsList span {
|
||||
text-align: center;
|
||||
color: #4a4a4a;
|
||||
padding: 0.5em 1em;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#tabsList span.is-active {
|
||||
color: #485fc7;
|
||||
}
|
||||
|
||||
.markdown-body a[data-range] {
|
||||
color: #485fc7;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
x-data="{ loaded: false, org: '* Hello, /world/!', type: 'json' }"
|
||||
x-init="orgize.init('./orgize_bg.wasm').then(() => loaded = true)"
|
||||
>
|
||||
<h2>Orgize wasm demo</h2>
|
||||
|
||||
<div>
|
||||
<a href="https://github.com/PoiScript/orgize">GitHub</a>
|
||||
<a href="https://www.npmjs.com/package/orgize">NPM</a>
|
||||
<a href="https://crates.io/crates/orgize">crates.io</a>
|
||||
</div>
|
||||
|
||||
<div>Input:</div>
|
||||
|
||||
<textarea
|
||||
x-model="org"
|
||||
style="width: 100%; height: 100px; margin-bottom: 16px"
|
||||
></textarea>
|
||||
|
||||
<div>Type:</div>
|
||||
|
||||
<div>
|
||||
<button @click="type = 'json'">JSON</button>
|
||||
<button @click="type = 'html'">HTML</button>
|
||||
<button @click="type = 'html-rendered'">HTML (Rendered)</button>
|
||||
</div>
|
||||
|
||||
<div>Output:</div>
|
||||
|
||||
<noscript>
|
||||
<p style="color: red">JavaScript is required.</p>
|
||||
</noscript>
|
||||
|
||||
<pre
|
||||
x-show="type === 'json'"
|
||||
x-transition
|
||||
x-text="loaded ? JSON.stringify(orgize.Org.parse(org).toJson(), null, 2) : 'Loading...'"
|
||||
></pre>
|
||||
<pre
|
||||
x-show="type === 'html'"
|
||||
x-transition
|
||||
x-text="loaded ? html_beautify(orgize.renderHtml(org), { indent_size: 2 }) : 'Loading...'"
|
||||
></pre>
|
||||
<body style="height: 100%; display: flex">
|
||||
<div
|
||||
x-show="type === 'html-rendered'"
|
||||
x-transition
|
||||
x-html="loaded ? orgize.renderHtml(org) : 'Loading...'"
|
||||
></div>
|
||||
style="
|
||||
margin: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 16px;
|
||||
"
|
||||
>
|
||||
<div class="bordered">
|
||||
<div style="height: 100%" id="editor"></div>
|
||||
</div>
|
||||
|
||||
<div class="bordered markdown-body">
|
||||
<div id="tabsList">
|
||||
<span class="is-active" data-value="html-rendered">
|
||||
HTML (rendered)
|
||||
</span>
|
||||
<span data-value="html">HTML</span>
|
||||
<span data-value="syntax">Syntax</span>
|
||||
<div id="spacer"></div>
|
||||
<div id="took"></div>
|
||||
</div>
|
||||
|
||||
<div style="flex: 1; overflow: auto">
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script type="module">
|
||||
import init, { Org } from "./dist/orgize.js";
|
||||
|
||||
let org;
|
||||
const result = document.getElementById("result");
|
||||
const took = document.getElementById("took");
|
||||
|
||||
let type = "html-rendered";
|
||||
|
||||
const editor = ace.edit("editor");
|
||||
editor.setTheme("ace/theme/tomorrow");
|
||||
|
||||
// render by type
|
||||
|
||||
const render = () => {
|
||||
const startTime = performance.now();
|
||||
|
||||
org.update(editor.session.getValue());
|
||||
|
||||
switch (type) {
|
||||
case "html-rendered": {
|
||||
const html = injectHeadingClass(org.html());
|
||||
result.innerHTML =
|
||||
"<div style='padding:1.25rem 1.5rem'>" + html + "</div>";
|
||||
break;
|
||||
}
|
||||
|
||||
case "html": {
|
||||
const html = html_beautify(org.html(), { indent_size: 2 });
|
||||
result.innerHTML = "<pre>" + escapeHtml(html) + "</pre>";
|
||||
break;
|
||||
}
|
||||
|
||||
case "syntax": {
|
||||
const syntax = escapeHtml(org.syntax())
|
||||
.split("\n")
|
||||
.map((line) =>
|
||||
line.replace(
|
||||
/([A-Z0-9_-]*)\@(\d+)\.\.(\d+)/,
|
||||
(text, name, start, end) =>
|
||||
`<b>${name}</b><a data-range="${start}-${end}">@${start}..${end}</a>`
|
||||
)
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
result.innerHTML = "<pre>" + syntax + "</pre>";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const finishTime = performance.now();
|
||||
|
||||
took.innerText = "took " + (finishTime - startTime).toFixed(2) + "ms";
|
||||
};
|
||||
|
||||
// select token or node range by click
|
||||
|
||||
result.addEventListener("click", (ev) => {
|
||||
if (!ev.target.dataset.range) return;
|
||||
|
||||
const [start, end] = ev.target.dataset.range.split("-");
|
||||
|
||||
const startPos = editor.session.doc.indexToPosition(parseInt(start));
|
||||
const endPos = editor.session.doc.indexToPosition(parseInt(end));
|
||||
const range = new ace.Range(
|
||||
startPos.row,
|
||||
startPos.column,
|
||||
endPos.row,
|
||||
endPos.column
|
||||
);
|
||||
editor.selection.setRange(range);
|
||||
});
|
||||
|
||||
// switch render type
|
||||
|
||||
document.getElementById("tabsList").addEventListener("click", (ev) => {
|
||||
const clicked = event.target.closest("span");
|
||||
|
||||
if (!clicked) return;
|
||||
|
||||
type = clicked.dataset.value;
|
||||
|
||||
document.querySelectorAll("#tabsList span").forEach((el) => {
|
||||
if (el === clicked) {
|
||||
el.classList.add("is-active");
|
||||
} else {
|
||||
el.classList.remove("is-active");
|
||||
}
|
||||
});
|
||||
|
||||
render();
|
||||
});
|
||||
|
||||
// utils
|
||||
|
||||
const escapeHtml = (html) =>
|
||||
html
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
const injectHeadingClass = (html) =>
|
||||
html.replace(/<h(\d)>/g, (_, l) => `<h${l} class="title is-${l}">`);
|
||||
|
||||
// init
|
||||
|
||||
init().then(() => {
|
||||
org = new Org("");
|
||||
|
||||
editor.session.setValue(`# git hash: ${Org.gitHash}
|
||||
# build time: ${Org.buildTime}
|
||||
|
||||
*Orgize*, a /pure/ =Rust= library for
|
||||
parsing +Emacs+ _org-mode_ files.
|
||||
|
||||
See also:
|
||||
[[https://github.com/PoiScript/orgize][GitHub]] |
|
||||
[[https://crates.io/crates/orgize][crates.io]] |
|
||||
[[https://www.npmjs.com/package/orgize][NPM]]
|
||||
|
||||
-----
|
||||
|
||||
* Heading 1 *bold*
|
||||
** Heading 2 =verbatim=
|
||||
*** Heading 3 ~code~
|
||||
**** Heading 4 /italic/
|
||||
***** Heading 5 +strike+
|
||||
****** Heading 6 _underline_
|
||||
|
||||
This's section
|
||||
|
||||
#+begin_quote
|
||||
This is a quote
|
||||
#+end_quote
|
||||
|
||||
#+begin_example
|
||||
This is an example block
|
||||
#+end_example
|
||||
|
||||
1. First item
|
||||
2. Second item
|
||||
3. Third item
|
||||
* Indented item
|
||||
* Indented item
|
||||
4. Fourth item
|
||||
|
||||
Table:
|
||||
|Syntax |Description|
|
||||
|-----------|-----------|
|
||||
|Header |Title |
|
||||
|Paragraph |Text |
|
||||
|
||||
Image:
|
||||
|
||||
[[https://www.rust-lang.org/static/images/rust-logo-blk.svg]]
|
||||
`);
|
||||
|
||||
editor.session.on("change", () => render());
|
||||
|
||||
render();
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue