From af7c305c9efa52f532ff37cfbe68624170373a6c Mon Sep 17 00:00:00 2001 From: PoiScript Date: Tue, 9 Nov 2021 17:01:57 +0800 Subject: [PATCH] chore: prepare for v0.10.0-alpha.1 --- .cargo/config.toml | 2 + Cargo.toml | 55 +- README.md | 240 ++---- benches/parse.rs | 44 +- examples/custom.rs | 81 -- examples/html-slugify.rs | 64 ++ examples/iter.rs | 19 - examples/json.rs | 17 - fuzz/.gitignore | 1 + fuzz/Cargo.toml | 15 +- fuzz/fuzz_targets/fuzz_target_1.rs | 15 +- src/ast/drawer.rs | 67 ++ src/ast/generate.js | 362 ++++++++ src/ast/generated.rs | 1266 ++++++++++++++++++++++++++++ src/ast/headline.rs | 358 ++++++++ src/ast/inline_call.rs | 22 + src/ast/link.rs | 41 + src/ast/list.rs | 47 ++ src/ast/mod.rs | 49 ++ src/ast/snippet.rs | 18 + src/ast/table.rs | 30 + src/ast/timestamp.rs | 112 +++ src/config.rs | 48 +- src/elements/block.rs | 408 --------- src/elements/clock.rs | 242 ------ src/elements/comment.rs | 53 -- src/elements/cookie.rs | 122 --- src/elements/drawer.rs | 121 --- src/elements/dyn_block.rs | 99 --- src/elements/emphasis.rs | 113 --- src/elements/fixed_width.rs | 80 -- src/elements/fn_def.rs | 117 --- src/elements/fn_ref.rs | 111 --- src/elements/inline_call.rs | 122 --- src/elements/inline_src.rs | 88 -- src/elements/keyword.rs | 230 ----- src/elements/link.rs | 80 -- src/elements/list.rs | 316 ------- src/elements/macros.rs | 91 -- src/elements/mod.rs | 245 ------ src/elements/planning.rs | 98 --- src/elements/radio_target.rs | 40 - src/elements/rule.rs | 48 -- src/elements/snippet.rs | 100 --- src/elements/table.rs | 169 ---- src/elements/target.rs | 78 -- src/elements/timestamp.rs | 482 ----------- src/elements/title.rs | 551 ------------ src/export/forward.rs | 210 +++++ src/export/html.rs | 682 +++++++-------- src/export/mod.rs | 31 +- src/export/org.rs | 321 ------- src/export/traverse.rs | 249 ++++++ src/headline.rs | 1219 -------------------------- src/lib.rs | 249 +----- src/org.rs | 212 +---- src/parse/combinators.rs | 136 --- src/parse/mod.rs | 1 - src/parsers.rs | 657 --------------- src/syntax/block.rs | 233 +++++ src/syntax/clock.rs | 137 +++ src/syntax/combinator.rs | 259 ++++++ src/syntax/comment.rs | 85 ++ src/syntax/cookie.rs | 144 ++++ src/syntax/document.rs | 128 +++ src/syntax/drawer.rs | 200 +++++ src/syntax/dyn_block.rs | 112 +++ src/syntax/element.rs | 235 ++++++ src/syntax/emphasis.rs | 146 ++++ src/syntax/fixed_width.rs | 64 ++ src/syntax/fn_def.rs | 154 ++++ src/syntax/fn_ref.rs | 120 +++ src/syntax/headline.rs | 350 ++++++++ src/syntax/inline_call.rs | 126 +++ src/syntax/inline_src.rs | 84 ++ src/syntax/input.rs | 250 ++++++ src/syntax/keyword.rs | 215 +++++ src/syntax/link.rs | 89 ++ src/syntax/list.rs | 583 +++++++++++++ src/syntax/macros.rs | 108 +++ src/syntax/mod.rs | 209 +++++ src/syntax/object.rs | 194 +++++ src/syntax/paragraph.rs | 96 +++ src/syntax/planning.rs | 94 +++ src/syntax/radio_target.rs | 68 ++ src/syntax/rule.rs | 93 ++ src/syntax/snippet.rs | 91 ++ src/syntax/table.rs | 209 +++++ src/syntax/target.rs | 64 ++ src/syntax/timestamp.rs | 326 +++++++ src/tests.rs | 24 + src/validate.rs | 209 ----- src/wasm/mod.rs | 189 ----- tests/blank.rs | 84 -- tests/issue_15_16.rs | 26 - tests/parse.rs | 6 +- wasm/Cargo.toml | 12 + wasm/README.md | 53 +- wasm/build.rs | 22 + wasm/index.html | 309 +++++-- wasm/package.json | 28 +- wasm/rollup.js | 31 - wasm/src/handler.ts | 102 --- wasm/src/html.ts | 160 ---- wasm/src/index.ts | 34 - wasm/src/keyword.ts | 10 - wasm/src/lib.rs | 44 + wasm/tests/html.js | 82 -- wasm/tests/keyword.js | 17 - wasm/tsconfig.json | 12 - wasm/yarn.lock | 416 --------- 111 files changed, 9132 insertions(+), 9148 deletions(-) create mode 100644 .cargo/config.toml delete mode 100644 examples/custom.rs create mode 100644 examples/html-slugify.rs delete mode 100644 examples/iter.rs delete mode 100644 examples/json.rs create mode 100644 src/ast/drawer.rs create mode 100644 src/ast/generate.js create mode 100644 src/ast/generated.rs create mode 100644 src/ast/headline.rs create mode 100644 src/ast/inline_call.rs create mode 100644 src/ast/link.rs create mode 100644 src/ast/list.rs create mode 100644 src/ast/mod.rs create mode 100644 src/ast/snippet.rs create mode 100644 src/ast/table.rs create mode 100644 src/ast/timestamp.rs delete mode 100644 src/elements/block.rs delete mode 100644 src/elements/clock.rs delete mode 100644 src/elements/comment.rs delete mode 100644 src/elements/cookie.rs delete mode 100644 src/elements/drawer.rs delete mode 100644 src/elements/dyn_block.rs delete mode 100644 src/elements/emphasis.rs delete mode 100644 src/elements/fixed_width.rs delete mode 100644 src/elements/fn_def.rs delete mode 100644 src/elements/fn_ref.rs delete mode 100644 src/elements/inline_call.rs delete mode 100644 src/elements/inline_src.rs delete mode 100644 src/elements/keyword.rs delete mode 100644 src/elements/link.rs delete mode 100644 src/elements/list.rs delete mode 100644 src/elements/macros.rs delete mode 100644 src/elements/mod.rs delete mode 100644 src/elements/planning.rs delete mode 100644 src/elements/radio_target.rs delete mode 100644 src/elements/rule.rs delete mode 100644 src/elements/snippet.rs delete mode 100644 src/elements/table.rs delete mode 100644 src/elements/target.rs delete mode 100644 src/elements/timestamp.rs delete mode 100644 src/elements/title.rs create mode 100644 src/export/forward.rs delete mode 100644 src/export/org.rs create mode 100644 src/export/traverse.rs delete mode 100644 src/headline.rs delete mode 100644 src/parse/combinators.rs delete mode 100644 src/parse/mod.rs delete mode 100644 src/parsers.rs create mode 100644 src/syntax/block.rs create mode 100644 src/syntax/clock.rs create mode 100644 src/syntax/combinator.rs create mode 100644 src/syntax/comment.rs create mode 100644 src/syntax/cookie.rs create mode 100644 src/syntax/document.rs create mode 100644 src/syntax/drawer.rs create mode 100644 src/syntax/dyn_block.rs create mode 100644 src/syntax/element.rs create mode 100644 src/syntax/emphasis.rs create mode 100644 src/syntax/fixed_width.rs create mode 100644 src/syntax/fn_def.rs create mode 100644 src/syntax/fn_ref.rs create mode 100644 src/syntax/headline.rs create mode 100644 src/syntax/inline_call.rs create mode 100644 src/syntax/inline_src.rs create mode 100644 src/syntax/input.rs create mode 100644 src/syntax/keyword.rs create mode 100644 src/syntax/link.rs create mode 100644 src/syntax/list.rs create mode 100644 src/syntax/macros.rs create mode 100644 src/syntax/mod.rs create mode 100644 src/syntax/object.rs create mode 100644 src/syntax/paragraph.rs create mode 100644 src/syntax/planning.rs create mode 100644 src/syntax/radio_target.rs create mode 100644 src/syntax/rule.rs create mode 100644 src/syntax/snippet.rs create mode 100644 src/syntax/table.rs create mode 100644 src/syntax/target.rs create mode 100644 src/syntax/timestamp.rs create mode 100644 src/tests.rs delete mode 100644 src/validate.rs delete mode 100644 src/wasm/mod.rs delete mode 100644 tests/blank.rs delete mode 100644 tests/issue_15_16.rs create mode 100644 wasm/Cargo.toml create mode 100644 wasm/build.rs delete mode 100644 wasm/rollup.js delete mode 100644 wasm/src/handler.ts delete mode 100644 wasm/src/html.ts delete mode 100644 wasm/src/index.ts delete mode 100644 wasm/src/keyword.ts create mode 100644 wasm/src/lib.rs delete mode 100644 wasm/tests/html.js delete mode 100644 wasm/tests/keyword.js delete mode 100644 wasm/tsconfig.json delete mode 100644 wasm/yarn.lock diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..70f9eae --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[registries.crates-io] +protocol = "sparse" diff --git a/Cargo.toml b/Cargo.toml index 4f8da57..e41374c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "orgize" -version = "0.9.0" +version = "0.10.0-alpha.1" authors = ["PoiScript "] -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 diff --git a/README.md b/README.md index 8074a44..87df33d 100644 --- a/README.md +++ b/README.md @@ -1,210 +1,66 @@ -# Orgize +A Rust library for parsing org-mode files. -[![Build Status](https://travis-ci.org/PoiScript/orgize.svg?branch=master)](https://travis-ci.org/PoiScript/orgize) [![Crates.io](https://img.shields.io/crates/v/orgize.svg)](https://crates.io/crates/orgize) -[![Document](https://docs.rs/orgize/badge.svg)](https://docs.rs/orgize) +[![Documentation](https://docs.rs/orgize/badge.svg)](https://docs.rs/orgize) +![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg) -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::().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(), "

title

section

" ); ``` -## 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 trait is required for custom error type -impl From for MyError { - fn from(err: IOError) -> Self { - MyError::IO(err) - } -} - -impl From for MyError { - fn from(err: FromUtf8Error) -> Self { - MyError::Utf8(err) - } -} - -#[derive(Default)] -struct MyHtmlHandler(DefaultHtmlHandler); - -impl HtmlHandler for MyHtmlHandler { - fn start(&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, - "", - title.level, - slugify!(&title.raw), - )?; - } - } else { - // fallthrough to default handler - self.0.start(w, element)?; - } - Ok(()) - } - - fn end(&mut self, mut w: W, element: &Element) -> Result<(), MyError> { - if let Element::Title(title) = element { - write!(w, "", 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)?, - "

title

\ -

section

" - ); - - 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. diff --git a/benches/parse.rs b/benches/parse.rs index 0666be2..ff9f089 100644 --- a/benches/parse.rs +++ b/benches/parse.rs @@ -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); diff --git a/examples/custom.rs b/examples/custom.rs deleted file mode 100644 index 3f650ff..0000000 --- a/examples/custom.rs +++ /dev/null @@ -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 trait is required for custom error type -impl From for MyError { - fn from(err: IOError) -> Self { - MyError::IO(err) - } -} - -impl From for MyError { - fn from(err: FromUtf8Error) -> Self { - MyError::Utf8(err) - } -} - -#[derive(Default)] -struct MyHtmlHandler(DefaultHtmlHandler); - -impl HtmlHandler for MyHtmlHandler { - fn start(&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, - "", - title.level, - slugify!(&title.raw), - )?; - } - } else { - // fallthrough to default handler - self.0.start(w, element)?; - } - Ok(()) - } - - fn end(&mut self, mut w: W, element: &Element) -> Result<(), MyError> { - if let Element::Title(title) = element { - write!(w, "", title.level)?; - } else { - self.0.end(w, element)?; - } - Ok(()) - } -} - -fn main() -> Result<(), MyError> { - let args: Vec<_> = args().collect(); - - if args.len() < 2 { - eprintln!("Usage: {} ", 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(()) -} diff --git a/examples/html-slugify.rs b/examples/html-slugify.rs new file mode 100644 index 0000000..cbad5c5 --- /dev/null +++ b/examples/html-slugify.rs @@ -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 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!("", 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!(""); + } + } + } + + 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: {} ", args[0]); + } else { + let mut handler = MyHtmlHandler::default(); + Org::parse(&args[1]).traverse(&mut handler); + + println!("{}", handler.0.finish()); + } +} diff --git a/examples/iter.rs b/examples/iter.rs deleted file mode 100644 index 1f95f67..0000000 --- a/examples/iter.rs +++ /dev/null @@ -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: {} ", args[0]); - } else { - let contents = String::from_utf8(fs::read(&args[1])?).unwrap(); - - for event in Org::parse(&contents).iter() { - println!("{:?}", event); - } - } - Ok(()) -} diff --git a/examples/json.rs b/examples/json.rs deleted file mode 100644 index e2ac5cf..0000000 --- a/examples/json.rs +++ /dev/null @@ -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: {} ", args[0]); - } else { - let contents = String::from_utf8(fs::read(&args[1])?).unwrap(); - println!("{}", to_string(&Org::parse(&contents)).unwrap()); - } - Ok(()) -} diff --git a/fuzz/.gitignore b/fuzz/.gitignore index a092511..1a45eee 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,3 +1,4 @@ target corpus artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 9162691..eeb3de4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -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 diff --git a/fuzz/fuzz_targets/fuzz_target_1.rs b/fuzz/fuzz_targets/fuzz_target_1.rs index bee8bcb..5e13431 100644 --- a/fuzz/fuzz_targets/fuzz_target_1.rs +++ b/fuzz/fuzz_targets/fuzz_target_1.rs @@ -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); } }); diff --git a/src/ast/drawer.rs b/src/ast/drawer.rs new file mode 100644 index 0000000..3baa2a8 --- /dev/null +++ b/src/ast/drawer.rs @@ -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::().unwrap(); + /// assert_eq!(drawer.iter().count(), 2); + /// ``` + pub fn iter(&self) -> impl Iterator { + 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::().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 { + 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::().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 { + 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::().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 { + self.iter() + .map(|(k, v)| (k.text().into(), v.text().into())) + .collect() + } +} diff --git a/src/ast/generate.js b/src/ast/generate.js new file mode 100644 index 0000000..bc98eaa --- /dev/null +++ b/src/ast/generate.js @@ -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 { support::token(&self.syntax, ${kind}) }\n`; + } + for (const [method, kind] of node.last_token || []) { + content += ` pub fn ${method}(&self) -> Option { 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); diff --git a/src/ast/generated.rs b/src/ast/generated.rs new file mode 100644 index 0000000..97031e0 --- /dev/null +++ b/src/ast/generated.rs @@ -0,0 +1,1266 @@ +//! generated file, do not modify it directly +#![allow(clippy::all)] +#![allow(unused)] + +use crate::syntax::{OrgLanguage, SyntaxKind, SyntaxKind::*, SyntaxNode, SyntaxToken}; +use rowan::ast::{support::{self, token}, AstChildren, AstNode}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Document { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Document { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == DOCUMENT + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Document { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Document { + pub fn section(&self) -> Option
{ + support::child(&self.syntax) + } + pub fn first_headline(&self) -> Option { + support::child(&self.syntax) + } + pub fn last_headline(&self) -> Option { + super::last_child(&self.syntax) + } + pub fn headlines(&self) -> AstChildren { + support::children(&self.syntax) + } + pub fn pre_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Section { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Section { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == SECTION + } + fn cast(node: SyntaxNode) -> Option
{ + Self::can_cast(node.kind()).then(|| Section { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Section { + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Paragraph { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Paragraph { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == PARAGRAPH + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Paragraph { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Paragraph { + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Headline { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Headline { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == HEADLINE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Headline { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Headline { + pub fn stars(&self) -> Option { + support::token(&self.syntax, HEADLINE_STARS) + } + pub fn keyword(&self) -> Option { + support::token(&self.syntax, HEADLINE_KEYWORD) + } + pub fn title(&self) -> Option { + support::child(&self.syntax) + } + pub fn section(&self) -> Option
{ + support::child(&self.syntax) + } + pub fn tags(&self) -> Option { + support::child(&self.syntax) + } + pub fn planning(&self) -> Option { + support::child(&self.syntax) + } + pub fn priority(&self) -> Option { + support::child(&self.syntax) + } + pub fn headlines(&self) -> AstChildren { + support::children(&self.syntax) + } + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HeadlineStars { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for HeadlineStars { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == HEADLINE_STARS + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| HeadlineStars { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl HeadlineStars { + pub fn headline(&self) -> Option { + self.syntax.parent().and_then(Headline::cast) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HeadlineTitle { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for HeadlineTitle { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == HEADLINE_TITLE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| HeadlineTitle { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl HeadlineTitle { + pub fn headline(&self) -> Option { + self.syntax.parent().and_then(Headline::cast) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HeadlineKeyword { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for HeadlineKeyword { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == HEADLINE_KEYWORD + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| HeadlineKeyword { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl HeadlineKeyword { + pub fn headline(&self) -> Option { + self.syntax.parent().and_then(Headline::cast) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HeadlinePriority { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for HeadlinePriority { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == HEADLINE_PRIORITY + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| HeadlinePriority { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl HeadlinePriority { + pub fn text(&self) -> Option { + support::token(&self.syntax, TEXT) + } + pub fn headline(&self) -> Option { + self.syntax.parent().and_then(Headline::cast) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct HeadlineTags { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for HeadlineTags { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == HEADLINE_TAGS + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| HeadlineTags { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl HeadlineTags { + pub fn headline(&self) -> Option { + self.syntax.parent().and_then(Headline::cast) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PropertyDrawer { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for PropertyDrawer { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == PROPERTY_DRAWER + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| PropertyDrawer { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl PropertyDrawer { + pub fn node_properties(&self) -> AstChildren { + support::children(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct NodeProperty { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for NodeProperty { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == NODE_PROPERTY + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| NodeProperty { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl NodeProperty {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Planning { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Planning { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == PLANNING + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Planning { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Planning { + pub fn deadline(&self) -> Option { + super::last_child(&self.syntax) + } + pub fn scheduled(&self) -> Option { + super::last_child(&self.syntax) + } + pub fn closed(&self) -> Option { + super::last_child(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlanningDeadline { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for PlanningDeadline { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == PLANNING_DEADLINE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| PlanningDeadline { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl PlanningDeadline {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlanningScheduled { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for PlanningScheduled { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == PLANNING_SCHEDULED + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| PlanningScheduled { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl PlanningScheduled {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PlanningClosed { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for PlanningClosed { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == PLANNING_CLOSED + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| PlanningClosed { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl PlanningClosed {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OrgTable { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for OrgTable { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == ORG_TABLE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| OrgTable { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl OrgTable { + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OrgTableRow { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for OrgTableRow { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == ORG_TABLE_RULE_ROW || kind == ORG_TABLE_STANDARD_ROW + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| OrgTableRow { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl OrgTableRow {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct OrgTableCell { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for OrgTableCell { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == ORG_TABLE_CELL + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| OrgTableCell { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl OrgTableCell {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct List { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for List { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == LIST + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| List { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl List { + pub fn items(&self) -> AstChildren { + support::children(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ListItem { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for ListItem { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == LIST_ITEM + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| ListItem { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl ListItem { + pub fn indent(&self) -> Option { + support::token(&self.syntax, LIST_ITEM_INDENT) + } + pub fn bullet(&self) -> Option { + support::token(&self.syntax, LIST_ITEM_BULLET) + } + pub fn content(&self) -> Option { + support::child(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ListItemIndent { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for ListItemIndent { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == LIST_ITEM_INDENT + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| ListItemIndent { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl ListItemIndent {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ListItemTag { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for ListItemTag { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == LIST_ITEM_TAG + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| ListItemTag { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl ListItemTag {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ListItemBullet { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for ListItemBullet { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == LIST_ITEM_BULLET + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| ListItemBullet { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl ListItemBullet {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ListItemContent { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for ListItemContent { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == LIST_ITEM_CONTENT + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| ListItemContent { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl ListItemContent {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Drawer { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Drawer { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == DRAWER + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Drawer { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Drawer {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DynBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for DynBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == DYN_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| DynBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl DynBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Keyword { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Keyword { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == KEYWORD + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Keyword { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Keyword {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct BabelCall { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for BabelCall { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == BABEL_CALL + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| BabelCall { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl BabelCall {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TableEl { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for TableEl { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == TABLE_EL + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| TableEl { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl TableEl { + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Clock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Clock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == CLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Clock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Clock { + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FnDef { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for FnDef { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == FN_DEF + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| FnDef { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl FnDef { + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Comment { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Comment { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == COMMENT + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Comment { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Comment { + pub fn text(&self) -> Option { + support::token(&self.syntax, TEXT) + } + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Rule { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Rule { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == RULE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Rule { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Rule { + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FixedWidth { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for FixedWidth { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == FIXED_WIDTH + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| FixedWidth { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl FixedWidth { + pub fn text(&self) -> Option { + support::token(&self.syntax, TEXT) + } + pub fn post_blank(&self) -> usize { + super::blank_lines(&self.syntax) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SpecialBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for SpecialBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == SPECIAL_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| SpecialBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl SpecialBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct QuoteBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for QuoteBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == QUOTE_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| QuoteBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl QuoteBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CenterBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for CenterBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == CENTER_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| CenterBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl CenterBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct VerseBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for VerseBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == VERSE_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| VerseBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl VerseBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CommentBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for CommentBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == COMMENT_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| CommentBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl CommentBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExampleBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for ExampleBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == EXAMPLE_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| ExampleBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl ExampleBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ExportBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for ExportBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == EXPORT_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| ExportBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl ExportBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SourceBlock { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for SourceBlock { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == SOURCE_BLOCK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| SourceBlock { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl SourceBlock {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct InlineCall { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for InlineCall { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == INLINE_CALL + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| InlineCall { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl InlineCall {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct InlineSrc { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for InlineSrc { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == INLINE_SRC + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| InlineSrc { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl InlineSrc {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Link { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Link { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == LINK + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Link { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Link { + pub fn path(&self) -> Option { + support::token(&self.syntax, LINK_PATH) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Cookie { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Cookie { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == COOKIE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Cookie { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Cookie {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RadioTarget { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for RadioTarget { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == RADIO_TARGET + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| RadioTarget { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl RadioTarget {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FnRef { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for FnRef { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == FN_REF + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| FnRef { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl FnRef {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct LatexEnvironment { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for LatexEnvironment { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == LATEX_ENVIRONMENT + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| LatexEnvironment { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl LatexEnvironment {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Macros { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Macros { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == MACROS + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Macros { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Macros {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MacrosArgument { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for MacrosArgument { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == MACROS_ARGUMENT + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| MacrosArgument { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl MacrosArgument {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Snippet { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Snippet { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == SNIPPET + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Snippet { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Snippet { + pub fn name(&self) -> Option { + support::token(&self.syntax, TEXT) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Target { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Target { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == TARGET + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Target { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Target {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Bold { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Bold { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == BOLD + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Bold { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Bold {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Strike { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Strike { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == STRIKE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Strike { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Strike {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Italic { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Italic { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == ITALIC + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Italic { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Italic {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Underline { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Underline { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == UNDERLINE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Underline { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Underline {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Verbatim { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Verbatim { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == VERBATIM + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Verbatim { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Verbatim {} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Code { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Code { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == CODE + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Code { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Code { + pub fn text(&self) -> Option { + support::token(&self.syntax, TEXT) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Timestamp { + pub(crate) syntax: SyntaxNode, +} +impl AstNode for Timestamp { + type Language = OrgLanguage; + fn can_cast(kind: SyntaxKind) -> bool { + kind == TIMESTAMP_ACTIVE || kind == TIMESTAMP_INACTIVE || kind == TIMESTAMP_DIARY + } + fn cast(node: SyntaxNode) -> Option { + Self::can_cast(node.kind()).then(|| Timestamp { syntax: node }) + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } +} +impl Timestamp { + pub fn year_start(&self) -> Option { + support::token(&self.syntax, TIMESTAMP_YEAR) + } + pub fn month_start(&self) -> Option { + support::token(&self.syntax, TIMESTAMP_MONTH) + } + pub fn day_start(&self) -> Option { + support::token(&self.syntax, TIMESTAMP_DAY) + } + pub fn hour_start(&self) -> Option { + support::token(&self.syntax, TIMESTAMP_HOUR) + } + pub fn minute_start(&self) -> Option { + support::token(&self.syntax, TIMESTAMP_MINUTE) + } + pub fn year_end(&self) -> Option { + super::last_token(&self.syntax, TIMESTAMP_YEAR) + } + pub fn month_end(&self) -> Option { + super::last_token(&self.syntax, TIMESTAMP_MONTH) + } + pub fn day_end(&self) -> Option { + super::last_token(&self.syntax, TIMESTAMP_DAY) + } + pub fn hour_end(&self) -> Option { + super::last_token(&self.syntax, TIMESTAMP_HOUR) + } + pub fn minute_end(&self) -> Option { + super::last_token(&self.syntax, TIMESTAMP_MINUTE) + } +} diff --git a/src/ast/headline.rs b/src/ast/headline.rs new file mode 100644 index 0000000..f4d4dfe --- /dev/null +++ b/src/ast/headline.rs @@ -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::().unwrap(); + /// assert_eq!(hdl.level(), Some(1)); + /// let hdl = Org::parse("****** hello").first_node::().unwrap(); + /// assert_eq!(hdl.level(), Some(6)); + /// ``` + pub fn level(&self) -> Option { + 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::().unwrap(); + /// assert!(hdl.is_commented()); + /// let hdl = Org::parse("* COMMENT hello").first_node::().unwrap(); + /// assert!(hdl.is_commented()); + /// let hdl = Org::parse("* hello").first_node::().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::().unwrap(); + /// assert!(hdl.is_archived()); + /// let hdl = Org::parse("* hello :ARCHIVED:").first_node::().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 { + self.planning() + .and_then(|planning| planning.closed()) + .and_then(|node| support::child::(&node.syntax)) + } + + /// Returns this headline's scheduled timestamp, or `None` if not set. + pub fn scheduled(&self) -> Option { + self.planning() + .and_then(|planning| planning.scheduled()) + .and_then(|node| support::child::(&node.syntax)) + } + + /// Returns this headline's deadline timestamp, or `None` if not set. + pub fn deadline(&self) -> Option { + self.planning() + .and_then(|planning| planning.deadline()) + .and_then(|node| support::child::(&node.syntax)) + } +} + +// pub enum DocumentOrHeadline { +// Document(Document), +// Headline(Headline), +// } + +// impl From for DocumentOrHeadline { +// fn from(value: Document) -> Self { +// DocumentOrHeadline::Document(value) +// } +// } + +// impl From for DocumentOrHeadline { +// fn from(value: Headline) -> Self { +// DocumentOrHeadline::Headline(value) +// } +// } + +// impl DocumentOrHeadline { +// pub fn section(&self) -> Option
{ +// 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 { +// 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, +// section: &str, +// ) -> Option
{ +// 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::().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 { + 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::().unwrap(); + /// assert_eq!(priority.text_string().unwrap(), "A".to_string()); + /// let priority = Org::parse("* [#破]").first_node::().unwrap(); + /// assert_eq!(priority.text_string().unwrap(), "破".to_string()); + /// ``` + pub fn text_string(&self) -> Option { + self.text().map(|tk| tk.to_string()) + } +} diff --git a/src/ast/inline_call.rs b/src/ast/inline_call.rs new file mode 100644 index 0000000..d259af0 --- /dev/null +++ b/src/ast/inline_call.rs @@ -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::().unwrap(); + /// assert_eq!(call.call().unwrap().text(), "square"); + /// ``` + pub fn call(&self) -> Option { + self.syntax + .children_with_tokens() + .filter_map(|it| match it { + SyntaxElement::Token(t) if t.kind() == SyntaxKind::TEXT => Some(t), + _ => None, + }) + .nth(1) + } +} diff --git a/src/ast/link.rs b/src/ast/link.rs new file mode 100644 index 0000000..2e85101 --- /dev/null +++ b/src/ast/link.rs @@ -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::().unwrap(); + /// assert!(!link.has_description()); + /// let link = Org::parse("[[https://google.com][Google]]").first_node::().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::().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() + } +} diff --git a/src/ast/list.rs b/src/ast/list.rs new file mode 100644 index 0000000..1ca114e --- /dev/null +++ b/src/ast/list.rs @@ -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::().unwrap(); + /// assert!(!list.is_ordered()); + /// + /// let list = Org::parse("1. 1").first_node::().unwrap(); + /// assert!(list.is_ordered()); + /// + /// let list = Org::parse("1) 1\n- 2\n3. 3").first_node::().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::().unwrap(); + /// assert!(list.is_descriptive()); + /// let list = Org::parse("2. [X] item 2").first_node::().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() + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs new file mode 100644 index 0000000..4587a66 --- /dev/null +++ b/src/ast/mod.rs @@ -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(parent: &rowan::SyntaxNode) -> Option { + parent.children().filter_map(N::cast).last() +} + +pub fn last_token( + parent: &rowan::SyntaxNode, + kind: L::Kind, +) -> Option> { + parent + .children_with_tokens() + .filter_map(filter_token(kind)) + .last() +} + +pub fn filter_token( + kind: L::Kind, +) -> impl Fn(NodeOrToken, rowan::SyntaxToken>) -> Option> +{ + move |elem| match elem { + NodeOrToken::Token(tk) if tk.kind() == kind => Some(tk), + _ => None, + } +} diff --git a/src/ast/snippet.rs b/src/ast/snippet.rs new file mode 100644 index 0000000..398ec40 --- /dev/null +++ b/src/ast/snippet.rs @@ -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::().unwrap(); + /// assert_eq!(snippet.value().unwrap().text(), "VALUE"); + /// ``` + pub fn value(&self) -> Option { + self.syntax + .children_with_tokens() + .filter_map(filter_token(SyntaxKind::TEXT)) + .nth(1) + } +} diff --git a/src/ast/table.rs b/src/ast/table.rs new file mode 100644 index 0000000..2d1ebc4 --- /dev/null +++ b/src/ast/table.rs @@ -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::().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::().unwrap(); + /// assert!(row.is_standard()); + /// ``` + pub fn is_standard(&self) -> bool { + self.syntax.kind() == SyntaxKind::ORG_TABLE_STANDARD_ROW + } +} diff --git a/src/ast/timestamp.rs b/src/ast/timestamp.rs new file mode 100644 index 0000000..68d926a --- /dev/null +++ b/src/ast/timestamp.rs @@ -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::().unwrap(); + /// assert!(ts.is_active()); + /// let ts = Org::parse("<2003-09-16 Tue 09:39>--<2003-09-16 Tue 10:39>").first_node::().unwrap(); + /// assert!(ts.is_active()); + /// let ts = Org::parse("<2003-09-16 Tue 09:39>").first_node::().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::().unwrap(); + /// assert!(ts.is_inactive()); + /// let ts = Org::parse("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]").first_node::().unwrap(); + /// assert!(ts.is_inactive()); + /// let ts = Org::parse("[2003-09-16 Tue 09:39]").first_node::().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::().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::().unwrap(); + /// assert!(ts.is_range()); + /// let ts = Org::parse("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]").first_node::().unwrap(); + /// assert!(ts.is_range()); + /// let ts = Org::parse("[2003-09-16 Tue 09:39]").first_node::().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::().unwrap(); + /// assert_eq!(ts.start_to_chrono().unwrap(), "2003-09-16T09:39:00".parse::().unwrap()); + /// ``` + #[cfg(feature = "chrono")] + pub fn start_to_chrono(&self) -> Option { + 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::().unwrap(); + /// assert_eq!(ts.end_to_chrono().unwrap(), "2003-09-16T10:39:00".parse::().unwrap()); + /// ``` + #[cfg(feature = "chrono")] + pub fn end_to_chrono(&self) -> Option { + 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, + )?, + )) + } +} diff --git a/src/config.rs b/src/config.rs index 955252d..8776c8e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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, Vec), + + pub dual_keywords: Vec, + + pub parsed_keywords: Vec, + + /// 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, +} + +impl ParseConfig { + /// Parses input with current config + pub fn parse(self, input: impl AsRef) -> 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(); -} diff --git a/src/elements/block.rs b/src/elements/block.rs deleted file mode 100644 index f138950..0000000 --- a/src/elements/block.rs +++ /dev/null @@ -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>, - /// 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>, - /// 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>, - /// 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>, - /// 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>, - /// 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>, - /// 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 { } - // 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> = 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 -} diff --git a/src/elements/clock.rs b/src/elements/clock.rs deleted file mode 100644 index c489a88..0000000 --- a/src/elements/clock.rs +++ /dev/null @@ -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>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - delay: Option>, - /// 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>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - delay: Option>, - /// 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, - } - )) - ); -} diff --git a/src/elements/comment.rs b/src/elements/comment.rs deleted file mode 100644 index d6d414d..0000000 --- a/src/elements/comment.rs +++ /dev/null @@ -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, - }, - )) -} diff --git a/src/elements/cookie.rs b/src/elements/cookie.rs deleted file mode 100644 index 59dd012..0000000 --- a/src/elements/cookie.rs +++ /dev/null @@ -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()); -} diff --git a/src/elements/drawer.rs b/src/elements/drawer.rs deleted file mode 100644 index 20bb956..0000000 --- a/src/elements/drawer.rs +++ /dev/null @@ -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()); -} diff --git a/src/elements/dyn_block.rs b/src/elements/dyn_block.rs deleted file mode 100644 index c74e7c1..0000000 --- a/src/elements/dyn_block.rs +++ /dev/null @@ -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>, - /// 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" - ) - )) - ); -} diff --git a/src/elements/emphasis.rs b/src/elements/emphasis.rs deleted file mode 100644 index 6517c8b..0000000 --- a/src/elements/emphasis.rs +++ /dev/null @@ -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); -} diff --git a/src/elements/fixed_width.rs b/src/elements/fixed_width.rs deleted file mode 100644 index ae06677..0000000 --- a/src/elements/fixed_width.rs +++ /dev/null @@ -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 - } - )) - ); -} diff --git a/src/elements/fn_def.rs b/src/elements/fn_def.rs deleted file mode 100644 index 2c91f16..0000000 --- a/src/elements/fn_def.rs +++ /dev/null @@ -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()); -} diff --git a/src/elements/fn_ref.rs b/src/elements/fn_ref.rs deleted file mode 100644 index c03253e..0000000 --- a/src/elements/fn_ref.rs +++ /dev/null @@ -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>, -} - -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()); -} diff --git a/src/elements/inline_call.rs b/src/elements/inline_call.rs deleted file mode 100644 index 8878beb..0000000 --- a/src/elements/inline_call.rs +++ /dev/null @@ -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>, - /// 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>, -} - -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()), - }, - )) - ); -} diff --git a/src/elements/inline_src.rs b/src/elements/inline_src.rs deleted file mode 100644 index f04d31a..0000000 --- a/src/elements/inline_src.rs +++ /dev/null @@ -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>, - /// 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]{text}"), - Some(( - "", - InlineSrc { - lang: "xml".into(), - options: Some(":exports code".into()), - body: "text".into(), - }, - )) - ); - - assert!(InlineSrc::parse("src_xml[:exports code]{text").is_none()); - assert!(InlineSrc::parse("src_[:exports code]{text}").is_none()); - assert!(InlineSrc::parse("src_xml[:exports code]").is_none()); -} diff --git a/src/elements/keyword.rs b/src/elements/keyword.rs deleted file mode 100644 index af8f8d9..0000000 --- a/src/elements/keyword.rs +++ /dev/null @@ -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>, - /// 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 - } - )) - ); -} diff --git a/src/elements/link.rs b/src/elements/link.rs deleted file mode 100644 index b0bb08d..0000000 --- a/src/elements/link.rs +++ /dev/null @@ -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>, -} - -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()); -} diff --git a/src/elements/list.rs b/src/elements/list.rs deleted file mode 100644 index 3b49852..0000000 --- a/src/elements/list.rs +++ /dev/null @@ -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 - -"# - ) - )) - ); -} diff --git a/src/elements/macros.rs b/src/elements/macros.rs deleted file mode 100644 index 8568d5b..0000000 --- a/src/elements/macros.rs +++ /dev/null @@ -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>, -} - -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()); -} diff --git a/src/elements/mod.rs b/src/elements/mod.rs deleted file mode 100644 index 7beec74..0000000 --- a/src/elements/mod.rs +++ /dev/null @@ -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 -); diff --git a/src/elements/planning.rs b/src/elements/planning.rs deleted file mode 100644 index 1659924..0000000 --- a/src/elements/planning.rs +++ /dev/null @@ -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 associated to scheduled keyword - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - pub scheduled: Option>, - /// Timestamp associated to closed keyword - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - pub closed: Option>, -} - -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, - } - )) - ) -} diff --git a/src/elements/radio_target.rs b/src/elements/radio_target.rs deleted file mode 100644 index fd529c7..0000000 --- a/src/elements/radio_target.rs +++ /dev/null @@ -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("<<>>"), Some(("", "target"))); - assert_eq!(parse_radio_target("<<>>"), Some(("", "tar get"))); - - assert!(parse_radio_target("<<>>").is_none()); - assert!(parse_radio_target("<<< target>>>").is_none()); - assert!(parse_radio_target("<<>>").is_none()); - assert!(parse_radio_target("<<get>>>").is_none()); - assert!(parse_radio_target("<<>>").is_none()); - assert!(parse_radio_target("<<>").is_none()); -} diff --git a/src/elements/rule.rs b/src/elements/rule.rs deleted file mode 100644 index b331746..0000000 --- a/src/elements/rule.rs +++ /dev/null @@ -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()); -} diff --git a/src/elements/snippet.rs b/src/elements/snippet.rs deleted file mode 100644 index 31e2117..0000000 --- a/src/elements/snippet.rs +++ /dev/null @@ -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:@@"), - Some(( - "", - Snippet { - name: "html".into(), - value: "".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:

@

@@"), - Some(( - "", - Snippet { - name: "html".into(), - value: "

@

".into(), - } - )) - ); - - assert!(Snippet::parse("@@html:@").is_none()); - assert!(Snippet::parse("@@html@@").is_none()); - assert!(Snippet::parse("@@:@@").is_none()); -} diff --git a/src/elements/table.rs b/src/elements/table.rs deleted file mode 100644 index 752083a..0000000 --- a/src/elements/table.rs +++ /dev/null @@ -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>, - /// 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()); -} diff --git a/src/elements/target.rs b/src/elements/target.rs deleted file mode 100644 index b847b59..0000000 --- a/src/elements/target.rs +++ /dev/null @@ -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("<>"), - Some(( - "", - Target { - target: "target".into() - } - )) - ); - assert_eq!( - Target::parse("<>"), - Some(( - "", - Target { - target: "tar get".into() - } - )) - ); - - assert!(Target::parse("<>").is_none()); - assert!(Target::parse("<< target>>").is_none()); - assert!(Target::parse("<>").is_none()); - assert!(Target::parse("<get>>").is_none()); - assert!(Target::parse("<>").is_none()); - assert!(Target::parse("<").is_none()); -} diff --git a/src/elements/timestamp.rs b/src/elements/timestamp.rs deleted file mode 100644 index 15f1255..0000000 --- a/src/elements/timestamp.rs +++ /dev/null @@ -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, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - pub minute: Option, -} - -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 for Datetime<'_> { - fn into(self) -> NaiveDate { - (&self).into() - } - } - - impl Into for Datetime<'_> { - fn into(self) -> NaiveTime { - (&self).into() - } - } - - impl Into for Datetime<'_> { - fn into(self) -> NaiveDateTime { - (&self).into() - } - } - - impl Into> for Datetime<'_> { - fn into(self) -> DateTime { - (&self).into() - } - } - - impl Into for &Datetime<'_> { - fn into(self) -> NaiveDate { - NaiveDate::from_ymd(self.year.into(), self.month.into(), self.day.into()) - } - } - - impl Into for &Datetime<'_> { - fn into(self) -> NaiveTime { - NaiveTime::from_hms( - self.hour.unwrap_or_default().into(), - self.minute.unwrap_or_default().into(), - 0, - ) - } - } - - impl Into for &Datetime<'_> { - fn into(self) -> NaiveDateTime { - NaiveDateTime::new(self.into(), self.into()) - } - } - - impl Into> for &Datetime<'_> { - fn into(self) -> DateTime { - 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>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - delay: Option>, - }, - Inactive { - start: Datetime<'a>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - repeater: Option>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - delay: Option>, - }, - ActiveRange { - start: Datetime<'a>, - end: Datetime<'a>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - repeater: Option>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - delay: Option>, - }, - InactiveRange { - start: Datetime<'a>, - end: Datetime<'a>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - repeater: Option>, - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - delay: Option>, - }, - 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 - }, - )) - ); -} diff --git a/src/elements/title.rs b/src/elements/title.rs deleted file mode 100644 index 0edc3d6..0000000 --- a/src/elements/title.rs +++ /dev/null @@ -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, - /// Headline title tags - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Vec::is_empty"))] - pub tags: Vec>, - /// Headline todo keyword - #[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))] - pub keyword: Option>, - /// 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>>, - /// 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, Cow<'a, str>)> { - self.pairs.iter() - } - - pub fn iter_mut(&mut self) -> impl Iterator, Cow<'a, str>)> { - self.pairs.iter_mut() - } - - pub fn into_iter(self) -> impl Iterator, Cow<'a, str>)> { - self.pairs.into_iter() - } - - pub fn into_hash_map(self) -> HashMap, Cow<'a, str>> { - self.pairs.into_iter().collect() - } - - #[cfg(feature = "indexmap")] - pub fn into_index_map(self) -> indexmap::IndexMap, 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, 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::() - )) - ) -} - -#[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); - } -} diff --git a/src/export/forward.rs b/src/export/forward.rs new file mode 100644 index 0000000..e98e30e --- /dev/null +++ b/src/export/forward.rs @@ -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 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!("", 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!(""); +/// } +/// } +/// } +/// +/// 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##"

hello world!

"##); +/// ``` +#[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) { + >::as_mut(self).$name(item, ctx) + } + }; +} diff --git a/src/export/html.rs b/src/export/html.rs index e0b0bd1..0c5786b 100644 --- a/src/export/html.rs +++ b/src/export/html.rs @@ -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> fmt::Display for HtmlEscape { 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> fmt::Display for HtmlEscape { } } -pub trait HtmlHandler>: Default { - fn start(&mut self, w: W, element: &Element) -> Result<(), E>; - fn end(&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, +} -impl HtmlHandler for DefaultHtmlHandler { - fn start(&mut self, mut w: W, element: &Element) -> IOResult<()> { - match element { - // container elements - Element::SpecialBlock(_) => (), - Element::QuoteBlock(_) => write!(w, "
")?, - Element::CenterBlock(_) => write!(w, "
")?, - Element::VerseBlock(_) => write!(w, "

")?, - Element::Bold => write!(w, "")?, - Element::Document { .. } => write!(w, "

")?, - Element::DynBlock(_dyn_block) => (), - Element::Headline { .. } => (), - Element::List(list) => { - if list.ordered { - write!(w, "
    ")?; - } else { - write!(w, "
      ")?; - } - } - Element::Italic => write!(w, "")?, - Element::ListItem(_) => write!(w, "
    • ")?, - Element::Paragraph { .. } => write!(w, "

      ")?, - Element::Section => write!(w, "

      ")?, - Element::Strike => write!(w, "")?, - Element::Underline => write!(w, "")?, - // non-container elements - Element::CommentBlock(_) => (), - Element::ExampleBlock(block) => write!( - w, - "
      {}
      ", - 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, - "
      {}
      ", - HtmlEscape(&block.contents) - )?; - } else { - write!( - w, - "
      {}
      ", - block.language, - HtmlEscape(&block.contents) - )?; - } - } - Element::BabelCall(_) => (), - Element::InlineSrc(inline_src) => write!( - w, - "{}", - inline_src.lang, - HtmlEscape(&inline_src.body) - )?, - Element::Code { value } => write!(w, "{}", HtmlEscape(value))?, - Element::FnRef(_fn_ref) => (), - Element::InlineCall(_) => (), - Element::Link(link) => write!( - w, - "{}", - 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, - "" - )?; - - 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, "")?; - } - Element::Verbatim { value } => write!(&mut w, "{}", HtmlEscape(value))?, - Element::FnDef(_fn_def) => (), - Element::Clock(_clock) => (), - Element::Comment(_) => (), - Element::FixedWidth(fixed_width) => write!( - w, - "
      {}
      ", - HtmlEscape(&fixed_width.value) - )?, - Element::Keyword(_keyword) => (), - Element::Drawer(_drawer) => (), - Element::Rule(_) => write!(w, "
      ")?, - Element::Cookie(cookie) => write!(w, "{}", cookie.value)?, - Element::Title(title) => { - write!(w, "", if title.level <= 6 { title.level } else { 6 })?; - } - Element::Table(Table::TableEl { .. }) => (), - Element::Table(Table::Org { has_header, .. }) => { - write!(w, "")?; - if *has_header { - write!(w, "")?; - } else { - write!(w, "")?; - } - } - Element::TableRow(row) => match row { - TableRow::Body => write!(w, "")?, - TableRow::BodyRule => write!(w, "")?, - TableRow::Header => write!(w, "")?, - TableRow::HeaderRule => write!(w, "")?, - }, - Element::TableCell(cell) => match cell { - TableCell::Body => write!(w, "
      ")?, - TableCell::Header => write!(w, "")?, - }, - } - - Ok(()) - } - - fn end(&mut self, mut w: W, element: &Element) -> IOResult<()> { - match element { - // container elements - Element::SpecialBlock(_) => (), - Element::QuoteBlock(_) => write!(w, "")?, - Element::CenterBlock(_) => write!(w, "")?, - Element::VerseBlock(_) => write!(w, "

      ")?, - Element::Bold => write!(w, "")?, - Element::Document { .. } => write!(w, "")?, - Element::DynBlock(_dyn_block) => (), - Element::Headline { .. } => (), - Element::List(list) => { - if list.ordered { - write!(w, "")?; - } else { - write!(w, "")?; - } - } - Element::Italic => write!(w, "")?, - Element::ListItem(_) => write!(w, "")?, - Element::Paragraph { .. } => write!(w, "

      ")?, - Element::Section => write!(w, "")?, - Element::Strike => write!(w, "")?, - Element::Underline => write!(w, "")?, - Element::Title(title) => { - write!(w, "", if title.level <= 6 { title.level } else { 6 })? - } - Element::Table(Table::TableEl { .. }) => (), - Element::Table(Table::Org { .. }) => { - write!(w, "
      ")?; - } - Element::TableRow(TableRow::Body) | Element::TableRow(TableRow::Header) => { - write!(w, "")?; - } - Element::TableCell(cell) => match cell { - TableCell::Body => write!(w, "")?, - TableCell::Header => write!(w, "")?, - }, - // 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, H: HtmlHandler> { - /// 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, +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, H: HtmlHandler> SyntectHtmlHandler { - 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(_) => "
      ", + WalkEvent::Leave(_) => "
      ", + }; + } + + #[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); + "
        " + } else if list.is_descriptive() { + self.in_descriptive_list.push(true); + "
        " + } else { + self.in_descriptive_list.push(false); + "
          " + }; } - } - - 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, H: HtmlHandler> Default for SyntectHtmlHandler { - 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() { + "
      " + } else if let Some(true) = self.in_descriptive_list.last() { + "" + } else { + "
    " + }; + 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(_) => "
  1. ", + WalkEvent::Leave(_) => "
  2. ", + }; } } - impl, H: HtmlHandler> HtmlHandler for SyntectHtmlHandler { - fn start(&mut self, mut w: W, element: &Element) -> Result<(), E> { - match element { - Element::InlineSrc(inline_src) => write!( - w, - "{}", - self.highlight(Some(&inline_src.lang), &inline_src.body) - )?, - Element::SourceBlock(block) => { - if block.language.is_empty() { - write!(w, "
    {}
    ", block.contents)?; - } else { - write!( - w, - "
    {}
    ", - 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(_) => "
    ", + WalkEvent::Leave(_) => "
    ", + }; + } + } + + #[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(_) => "
    ", + WalkEvent::Leave(_) => "
    ", + }; + } + } + + #[tracing::instrument(skip(self, _ctx))] + fn paragraph(&mut self, event: WalkEvent<&Paragraph>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "

    ", + WalkEvent::Leave(_) => "

    ", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn section(&mut self, event: WalkEvent<&Section>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "
    ", + WalkEvent::Leave(_) => "
    ", + }; + } + + #[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, - "
    {}
    ", - self.highlight(None, &fixed_width.value) - )?, - Element::ExampleBlock(block) => write!( - w, - "
    {}
    ", - self.highlight(None, &block.contents) - )?, - _ => self.inner.start(w, element)?, } - Ok(()) - } + return ctx.skip(); + }; + } - fn end(&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!("") + } + WalkEvent::Leave(title) => { + let level = title + .headline() + .and_then(|hdl| hdl.level()) + .map(|lvl| std::cmp::min(lvl, 6)) + .unwrap_or(1); + format!("") + } + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn italic(&mut self, event: WalkEvent<&Italic>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + WalkEvent::Leave(_) => "", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn bold(&mut self, event: WalkEvent<&Bold>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + WalkEvent::Leave(_) => "", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn strike(&mut self, event: WalkEvent<&Strike>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + WalkEvent::Leave(_) => "", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn underline(&mut self, event: WalkEvent<&Underline>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + WalkEvent::Leave(_) => "", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn verbatim(&mut self, event: WalkEvent<&Verbatim>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + WalkEvent::Leave(_) => "", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn code(&mut self, event: WalkEvent<&Code>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + WalkEvent::Leave(_) => "", + }; + } + + #[tracing::instrument(skip(self, ctx))] + fn rule(&mut self, event: WalkEvent<&Rule>, ctx: &mut TraversalContext) { + if let WalkEvent::Enter(_) = event { + self.output += "
    " + }; + 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#""#, HtmlEscape(path)); + return ctx.skip(); + } + + self.output += &format!(r#""#, HtmlEscape(path)); + + if !link.has_description() { + self.output += &HtmlEscape(path).to_string(); + self.output += ""; + return ctx.skip(); + } + } + WalkEvent::Leave(_) => { + self.output += ""; + } } } -} -#[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(_) => "
    ", + WalkEvent::Leave(_) => "
    ", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn verse_block(&mut self, event: WalkEvent<&VerseBlock>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "

    ", + WalkEvent::Leave(_) => "

    ", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn example_block(&mut self, event: WalkEvent<&ExampleBlock>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "
    ",
    +            WalkEvent::Leave(_) => "
    ", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn center_block(&mut self, event: WalkEvent<&CenterBlock>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "
    ", + WalkEvent::Leave(_) => "
    ", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn org_table(&mut self, event: WalkEvent<&OrgTable>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + WalkEvent::Leave(_) => "
    ", + }; + } + + #[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(_) => "", + WalkEvent::Leave(_) => "", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn org_table_cell(&mut self, event: WalkEvent<&OrgTableCell>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + WalkEvent::Leave(_) => "", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn comment(&mut self, event: WalkEvent<&Comment>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + }; + } + + #[tracing::instrument(skip(self, _ctx))] + fn comment_block(&mut self, event: WalkEvent<&CommentBlock>, _ctx: &mut TraversalContext) { + self.output += match event { + WalkEvent::Enter(_) => "", + }; + } + + #[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) {} +} diff --git a/src/export/mod.rs b/src/export/mod.rs index 4eddfba..3d3f0bd 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -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( - 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}; diff --git a/src/export/org.rs b/src/export/org.rs deleted file mode 100644 index 44c0a5c..0000000 --- a/src/export/org.rs +++ /dev/null @@ -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>: Default { - fn start(&mut self, w: W, element: &Element) -> Result<(), E>; - fn end(&mut self, w: W, element: &Element) -> Result<(), E>; -} - -#[derive(Default)] -pub struct DefaultOrgHandler; - -impl OrgHandler for DefaultOrgHandler { - fn start(&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(&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(mut w: W, count: usize) -> Result<(), Error> { - for _ in 0..count { - writeln!(w)?; - } - Ok(()) -} - -fn write_timestamp(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(()) -} diff --git a/src/export/traverse.rs b/src/export/traverse.rs new file mode 100644 index 0000000..8c3b446 --- /dev/null +++ b/src/export/traverse.rs @@ -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); +} diff --git a/src/headline.rs b/src/headline.rs deleted file mode 100644 index 49c3617..0000000 --- a/src/headline.rs +++ /dev/null @@ -1,1219 +0,0 @@ -use indextree::NodeId; -use std::borrow::Cow; -use std::ops::RangeInclusive; -use std::usize; - -use crate::{ - config::ParseConfig, - elements::{Element, Title}, - parsers::{parse_container, Container, OwnedArena}, - validate::{ValidationError, ValidationResult}, - Org, -}; - -/// Represents the document in `Org` struct. -/// -/// Each `Org` struct only has one `Document`. -#[derive(Copy, Clone, Debug)] -pub struct Document { - doc_n: NodeId, - sec_n: Option, -} - -impl Document { - pub(crate) fn from_org(org: &Org) -> Document { - let sec_n = org.arena[org.root] - .first_child() - .and_then(|n| match org[n] { - Element::Section => Some(n), - Element::Headline { .. } => None, - _ => unreachable!("Document should only contains section and headline."), - }); - - Document { - doc_n: org.root, - sec_n, - } - } - - /// Returns the ID of the section element of this document, - /// or `None` if it has no section. - pub fn section_node(self) -> Option { - self.sec_n - } - - /// Returns an iterator of this document's children. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// ** h1 - /// ** h2 - /// *** h2_1 - /// *** h2_2 - /// ** h3 - /// "#, - /// ); - /// - /// let d = org.document(); - /// - /// let mut iter = d.children(&org); - /// - /// assert_eq!(iter.next().unwrap().title(&org).raw, "h1"); - /// assert_eq!(iter.next().unwrap().title(&org).raw, "h2"); - /// assert_eq!(iter.next().unwrap().title(&org).raw, "h3"); - /// assert!(iter.next().is_none()); - /// ``` - pub fn children<'a>(self, org: &'a Org) -> impl Iterator + 'a { - self.doc_n - .children(&org.arena) - // skip section if exists - .skip(if self.sec_n.is_some() { 1 } else { 0 }) - .map(move |n| match org[n] { - Element::Headline { level } => Headline::from_node(n, level, org), - _ => unreachable!(), - }) - } - - /// Returns the first child of this document, or `None` if it has no child. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// ** h1 - /// ** h2 - /// *** h2_1 - /// *** h2_2 - /// ** h3 - /// "#, - /// ); - /// - /// let d = org.document(); - /// - /// assert_eq!(d.first_child(&org).unwrap().title(&org).raw, "h1"); - /// ``` - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let org = Org::new(); - /// - /// assert!(org.document().first_child(&org).is_none()); - /// ``` - pub fn first_child(self, org: &Org) -> Option { - self.doc_n - .children(&org.arena) - // skip section if exists - .nth(if self.sec_n.is_some() { 1 } else { 0 }) - .map(move |n| match org[n] { - Element::Headline { level } => Headline::from_node(n, level, org), - _ => unreachable!(), - }) - } - - /// Returns the last child of this document, or `None` if it has no child. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// ** h1_1 - /// ** h1_2 - /// *** h1_2_1 - /// *** h1_2_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let d = org.document(); - /// - /// assert_eq!(d.last_child(&org).unwrap().title(&org).raw, "h1_3"); - /// ``` - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let org = Org::new(); - /// - /// assert!(org.document().last_child(&org).is_none()); - /// ``` - pub fn last_child(self, org: &Org) -> Option { - org.arena[self.doc_n] - .last_child() - .and_then(|n| match org[n] { - Element::Headline { level } => Some(Headline::from_node(n, level, org)), - Element::Section => None, - _ => unreachable!("Document should only contains section and headline."), - }) - } - - /// Changes the section content of this document. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// ** h1_1 - /// ** h1_2 - /// "#, - /// ); - /// - /// let mut d = org.document(); - /// - /// d.set_section_content("s", &mut org); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// s - /// ** h1_1 - /// ** h1_2 - /// "#, - /// ); - /// ``` - pub fn set_section_content<'a, S>(&mut self, content: S, org: &mut Org<'a>) - where - S: Into>, - { - if let Some(sec_n) = self.sec_n { - let children: Vec<_> = sec_n.children(&org.arena).collect(); - for child in children { - child.detach(&mut org.arena); - } - } else { - let sec_n = org.arena.new_node(Element::Section); - self.sec_n = Some(sec_n); - self.doc_n.prepend(sec_n, &mut org.arena); - } - - match content.into() { - Cow::Borrowed(content) => parse_container( - &mut org.arena, - Container::Block { - node: self.sec_n.unwrap(), - content, - }, - &ParseConfig::default(), - ), - Cow::Owned(ref content) => parse_container( - &mut OwnedArena::new(&mut org.arena), - Container::Block { - node: self.sec_n.unwrap(), - content, - }, - &ParseConfig::default(), - ), - } - - org.debug_validate(); - } - - /// Appends a new child to this document. - /// - /// Returns an error if the given new child was already attached, - /// or the given new child didn't meet the requirements. - /// - /// ```rust - /// # use orgize::{elements::Title, Headline, Org}; - /// # - /// let mut org = Org::parse( - /// r#" - /// ***** h1 - /// **** h2 - /// *** h3 - /// "#, - /// ); - /// - /// let d = org.document(); - /// - /// let mut h4 = Headline::new( - /// Title { - /// raw: "h4".into(), - /// ..Default::default() - /// }, - /// &mut org, - /// ); - /// - /// // level must be smaller than or equal to 3 - /// h4.set_level(4, &mut org).unwrap(); - /// assert!(d.append(h4, &mut org).is_err()); - /// - /// h4.set_level(2, &mut org).unwrap(); - /// assert!(d.append(h4, &mut org).is_ok()); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// ***** h1 - /// **** h2 - /// *** h3 - /// ** h4 - /// "#, - /// ); - /// - /// // cannot append an attached headline - /// assert!(d.append(h4, &mut org).is_err()); - /// ``` - pub fn append(self, hdl: Headline, org: &mut Org) -> ValidationResult<()> { - hdl.check_detached(org)?; - - if let Some(last) = self.last_child(org) { - hdl.check_level(1..=last.lvl)?; - } else { - hdl.check_level(1..=usize::max_value())?; - } - - self.doc_n.append(hdl.hdl_n, &mut org.arena); - - org.debug_validate(); - - Ok(()) - } - - /// Prepends a new child to this document. - /// - /// Returns an error if the given new child was already attached, - /// or the given new child didn't meet the requirements. - /// - /// ```rust - /// # use orgize::{elements::Title, Headline, Org}; - /// # - /// let mut org = Org::parse( - /// r#" - /// ** h2 - /// ** h3 - /// "#, - /// ); - /// - /// let d = org.document(); - /// - /// let mut h1 = Headline::new( - /// Title { - /// raw: "h1".into(), - /// ..Default::default() - /// }, - /// &mut org, - /// ); - /// - /// // level must be greater than 2 - /// h1.set_level(1, &mut org).unwrap(); - /// assert!(d.prepend(h1, &mut org).is_err()); - /// - /// h1.set_level(4, &mut org).unwrap(); - /// assert!(d.prepend(h1, &mut org).is_ok()); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// **** h1 - /// ** h2 - /// ** h3 - /// "#, - /// ); - /// - /// // cannot prepend an attached headline - /// assert!(d.prepend(h1, &mut org).is_err()); - /// ``` - pub fn prepend(self, hdl: Headline, org: &mut Org) -> ValidationResult<()> { - hdl.check_detached(org)?; - - if let Some(first) = self.first_child(org) { - hdl.check_level(first.lvl..=usize::MAX)?; - } else { - hdl.check_level(1..=usize::MAX)?; - } - - if let Some(sec_n) = self.sec_n { - sec_n.insert_after(hdl.hdl_n, &mut org.arena); - } else { - self.doc_n.prepend(hdl.hdl_n, &mut org.arena); - } - - org.debug_validate(); - - Ok(()) - } -} - -/// Represents a headline in `Org` struct. -/// -/// Each `Org` has zero or more `Headline`s. -#[derive(Copy, Clone, Debug)] -pub struct Headline { - lvl: usize, - hdl_n: NodeId, - ttl_n: NodeId, - sec_n: Option, -} - -impl Headline { - /// Creates a new detached Headline. - pub fn new<'a>(ttl: Title<'a>, org: &mut Org<'a>) -> Headline { - let lvl = ttl.level; - let hdl_n = org.arena.new_node(Element::Headline { level: ttl.level }); - let ttl_n = org.arena.new_node(Element::Document { pre_blank: 0 }); // placeholder - hdl_n.append(ttl_n, &mut org.arena); - - match ttl.raw { - Cow::Borrowed(content) => parse_container( - &mut org.arena, - Container::Inline { - node: ttl_n, - content, - }, - &ParseConfig::default(), - ), - Cow::Owned(ref content) => parse_container( - &mut OwnedArena::new(&mut org.arena), - Container::Inline { - node: ttl_n, - content, - }, - &ParseConfig::default(), - ), - } - - org[ttl_n] = Element::Title(ttl); - - Headline { - lvl, - hdl_n, - ttl_n, - sec_n: None, - } - } - - pub(crate) fn from_node(hdl_n: NodeId, lvl: usize, org: &Org) -> Headline { - let ttl_n = org.arena[hdl_n].first_child().unwrap(); - let sec_n = org.arena[ttl_n].next_sibling().and_then(|n| match org[n] { - Element::Section => Some(n), - _ => None, - }); - - Headline { - lvl, - hdl_n, - ttl_n, - sec_n, - } - } - - /// Returns the level of this headline. - pub fn level(self) -> usize { - self.lvl - } - - /// Returns the ID of the headline element of this headline. - pub fn headline_node(self) -> NodeId { - self.hdl_n - } - - /// Returns the ID of the title element of this headline. - pub fn title_node(self) -> NodeId { - self.ttl_n - } - - /// Returns the ID of the section element of this headline, or `None` if it has no section. - pub fn section_node(self) -> Option { - self.sec_n - } - - /// Returns a reference to the title element of this headline. - pub fn title<'a: 'b, 'b>(self, org: &'b Org<'a>) -> &'b Title<'a> { - match &org[self.ttl_n] { - Element::Title(title) => title, - _ => unreachable!(), - } - } - - /// Returns a mutual reference to the title element of this headline. - /// - /// Don't change the level and content of the `&mut Titile` directly. - /// Alternatively, uses [`Headline::set_level`] and [`Headline::set_title_content`]. - /// - /// [`Headline::set_level`]: #method.set_level - /// [`Headline::set_title_content`]: #method.set_title_content - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse("* h1"); - /// - /// let h1 = org.headlines().nth(0).unwrap(); - /// - /// h1.title_mut(&mut org).priority = Some('A'); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// "* [#A] h1\n", - /// ); - /// ``` - pub fn title_mut<'a: 'b, 'b>(self, org: &'b mut Org<'a>) -> &'b mut Title<'a> { - match &mut org[self.ttl_n] { - Element::Title(title) => title, - _ => unreachable!(), - } - } - - /// Changes the level of this headline. - /// - /// Returns an error if this headline is attached and the given new level - /// doesn't meet the requirements. - /// - /// ```rust - /// # use orgize::{elements::Title, Headline, Org}; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ****** h1_1 - /// *** h1_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let mut h1_2 = org.headlines().nth(2).unwrap(); - /// - /// // level must be greater than or equal to 2, and smaller than or equal to 6 - /// assert!(h1_2.set_level(42, &mut org).is_err()); - /// - /// assert!(h1_2.set_level(5, &mut org).is_ok()); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// * h1 - /// ****** h1_1 - /// ***** h1_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// // detached headline's levels can be changed freely - /// let mut new_headline = Headline::new( - /// Title { - /// raw: "new".into(), - /// ..Default::default() - /// }, - /// &mut org, - /// ); - /// new_headline.set_level(42, &mut org).unwrap(); - /// ``` - pub fn set_level(&mut self, lvl: usize, org: &mut Org) -> ValidationResult<()> { - if !self.is_detached(org) { - let min = self - .next(&org) - .or_else(|| self.parent(&org)) - .map(|hdl| hdl.lvl) - .unwrap_or(1); - let max = self - .previous(&org) - .map(|hdl| hdl.lvl) - .unwrap_or(usize::max_value()); - if !(min..=max).contains(&lvl) { - return Err(ValidationError::HeadlineLevelMismatch { - range: min..=max, - at: self.hdl_n, - }); - } - } - self.lvl = lvl; - self.title_mut(org).level = lvl; - if let Element::Headline { level } = &mut org[self.hdl_n] { - *level = lvl; - } - Ok(()) - } - - /// Changes the title content of this headline. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// "#, - /// ); - /// - /// let h1 = org.headlines().nth(0).unwrap(); - /// let h1_1 = org.headlines().nth(1).unwrap(); - /// - /// h1.set_title_content("H1", &mut org); - /// h1_1.set_title_content(String::from("*H1_1*"), &mut org); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// * H1 - /// ** *H1_1* - /// "#, - /// ); - /// ``` - pub fn set_title_content<'a, S>(self, content: S, org: &mut Org<'a>) - where - S: Into>, - { - let content = content.into(); - - let children: Vec<_> = self.ttl_n.children(&org.arena).collect(); - for child in children { - child.detach(&mut org.arena); - } - - match &content { - Cow::Borrowed(content) => parse_container( - &mut org.arena, - Container::Inline { - node: self.ttl_n, - content, - }, - &ParseConfig::default(), - ), - Cow::Owned(ref content) => parse_container( - &mut OwnedArena::new(&mut org.arena), - Container::Inline { - node: self.ttl_n, - content, - }, - &ParseConfig::default(), - ), - } - - self.title_mut(org).raw = content; - - org.debug_validate(); - } - - /// Changes the section content of this headline. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// s1_1 - /// "#, - /// ); - /// - /// let mut h1 = org.headlines().nth(0).unwrap(); - /// let mut h1_1 = org.headlines().nth(1).unwrap(); - /// - /// h1.set_section_content("s1", &mut org); - /// h1_1.set_section_content(String::from("*s1_1*"), &mut org); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// * h1 - /// s1 - /// ** h1_1 - /// *s1_1* - /// "#, - /// ); - /// ``` - pub fn set_section_content<'a, S>(&mut self, content: S, org: &mut Org<'a>) - where - S: Into>, - { - if let Some(sec_n) = self.sec_n { - let children: Vec<_> = sec_n.children(&org.arena).collect(); - for child in children { - child.detach(&mut org.arena); - } - } else { - let sec_n = org.arena.new_node(Element::Section); - self.sec_n = Some(sec_n); - self.ttl_n.insert_after(sec_n, &mut org.arena); - } - - match content.into() { - Cow::Borrowed(content) => parse_container( - &mut org.arena, - Container::Block { - node: self.sec_n.unwrap(), - content, - }, - &ParseConfig::default(), - ), - Cow::Owned(ref content) => parse_container( - &mut OwnedArena::new(&mut org.arena), - Container::Block { - node: self.sec_n.unwrap(), - content, - }, - &ParseConfig::default(), - ), - } - - org.debug_validate(); - } - - /// Returns the parent of this headline, or `None` if it is detached or attached to the document. - /// - /// ```rust - /// # use orgize::{elements::Title, Headline, Org}; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ** h1_2 - /// *** h1_2_1 - /// *** h1_2_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let h1 = org.headlines().nth(0).unwrap(); - /// let h1_1 = org.headlines().nth(1).unwrap(); - /// let h1_2_1 = org.headlines().nth(3).unwrap(); - /// - /// assert_eq!(h1_1.parent(&org).unwrap().title(&org).raw, "h1"); - /// assert_eq!(h1_2_1.parent(&org).unwrap().title(&org).raw, "h1_2"); - /// - /// assert!(h1.parent(&org).is_none()); - /// - /// // detached headline have no parent - /// assert!(Headline::new(Title::default(), &mut org).parent(&org).is_none()); - /// ``` - pub fn parent(self, org: &Org) -> Option { - org.arena[self.hdl_n].parent().and_then(|n| match org[n] { - Element::Headline { level } => Some(Headline::from_node(n, level, org)), - Element::Document { .. } => None, - _ => unreachable!(), - }) - } - - /// Returns an iterator of this headline's children. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ** h1_2 - /// *** h1_2_1 - /// *** h1_2_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let h1 = org.headlines().nth(0).unwrap(); - /// - /// let mut iter = h1.children(&org); - /// - /// assert_eq!(iter.next().unwrap().title(&org).raw, "h1_1"); - /// assert_eq!(iter.next().unwrap().title(&org).raw, "h1_2"); - /// assert_eq!(iter.next().unwrap().title(&org).raw, "h1_3"); - /// assert!(iter.next().is_none()); - /// ``` - pub fn children<'a>(self, org: &'a Org) -> impl Iterator + 'a { - self.hdl_n - .children(&org.arena) - // skip title and section - .skip(if self.sec_n.is_some() { 2 } else { 1 }) - .filter_map(move |n| match org[n] { - Element::Headline { level } => Some(Headline::from_node(n, level, org)), - _ => unreachable!(), - }) - } - - /// Returns the first child of this headline, or `None` if it has no child. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ** h1_2 - /// *** h1_2_1 - /// *** h1_2_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let h1_1 = org.headlines().nth(1).unwrap(); - /// let h1_2 = org.headlines().nth(2).unwrap(); - /// let h1_3 = org.headlines().nth(5).unwrap(); - /// - /// assert_eq!(h1_2.first_child(&org).unwrap().title(&org).raw, "h1_2_1"); - /// - /// assert!(h1_1.first_child(&org).is_none()); - /// assert!(h1_3.first_child(&org).is_none()); - /// ``` - pub fn first_child(self, org: &Org) -> Option { - self.hdl_n - .children(&org.arena) - // skip title and section - .nth(if self.sec_n.is_some() { 2 } else { 1 }) - .map(|n| match org[n] { - Element::Headline { level } => Headline::from_node(n, level, org), - _ => unreachable!(), - }) - } - - /// Returns the last child of this headline, or `None` if it has no child. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ** h1_2 - /// *** h1_2_1 - /// *** h1_2_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let h1_1 = org.headlines().nth(1).unwrap(); - /// let h1_2 = org.headlines().nth(2).unwrap(); - /// let h1_3 = org.headlines().nth(5).unwrap(); - /// - /// assert_eq!(h1_2.last_child(&org).unwrap().title(&org).raw, "h1_2_2"); - /// - /// assert!(h1_1.last_child(&org).is_none()); - /// assert!(h1_3.last_child(&org).is_none()); - /// ``` - pub fn last_child(self, org: &Org) -> Option { - org.arena[self.hdl_n] - .last_child() - .and_then(|n| match org[n] { - Element::Headline { level } => Some(Headline::from_node(n, level, org)), - Element::Section | Element::Title(_) => None, - _ => unreachable!("Headline should only contains section and headline."), - }) - } - - /// Returns the previous sibling of this headline, or `None` if it is a first child. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ** h1_2 - /// *** h1_2_1 - /// *** h1_2_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let h1_1 = org.headlines().nth(1).unwrap(); - /// let h1_2 = org.headlines().nth(2).unwrap(); - /// let h1_2_1 = org.headlines().nth(3).unwrap(); - /// - /// assert_eq!(h1_2.previous(&org).unwrap().title(&org).raw, "h1_1"); - /// - /// assert!(h1_1.previous(&org).is_none()); - /// assert!(h1_2_1.previous(&org).is_none()); - /// ``` - pub fn previous(self, org: &Org) -> Option { - org.arena[self.hdl_n] - .previous_sibling() - .and_then(|n| match org[n] { - Element::Headline { level } => Some(Headline::from_node(n, level, org)), - Element::Title(_) | Element::Section => None, - _ => unreachable!(), - }) - } - - /// Returns the next sibling of this headline, or `None` if it is a last child. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ** h1_2 - /// *** h1_2_1 - /// *** h1_2_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let h1_2 = org.headlines().nth(2).unwrap(); - /// let h1_2_2 = org.headlines().nth(4).unwrap(); - /// let h1_3 = org.headlines().nth(5).unwrap(); - /// - /// assert_eq!(h1_2.next(&org).unwrap().title(&org).raw, "h1_3"); - /// - /// assert!(h1_3.next(&org).is_none()); - /// assert!(h1_2_2.next(&org).is_none()); - /// ``` - pub fn next(self, org: &Org) -> Option { - org.arena[self.hdl_n].next_sibling().map(|n| match org[n] { - Element::Headline { level } => Headline::from_node(n, level, org), - _ => unreachable!(), - }) - } - - /// Detaches this headline from arena. - /// - /// ```rust - /// # use orgize::Org; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ** h1_2 - /// *** h1_2_1 - /// *** h1_2_2 - /// ** h1_3 - /// "#, - /// ); - /// - /// let h1_2 = org.headlines().nth(2).unwrap(); - /// - /// h1_2.detach(&mut org); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// * h1 - /// ** h1_1 - /// ** h1_3 - /// "#, - /// ); - /// ``` - pub fn detach(self, org: &mut Org) { - self.hdl_n.detach(&mut org.arena); - } - - /// Returns `true` if this headline is detached. - pub fn is_detached(self, org: &Org) -> bool { - org.arena[self.hdl_n].parent().is_none() - } - - /// Appends a new child to this headline. - /// - /// Returns an error if the given new child was already attached, or - /// the given new child didn't meet the requirements. - /// - /// ```rust - /// # use orgize::{elements::Title, Headline, Org}; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ***** h1_1_1 - /// "#, - /// ); - /// - /// let h1_1 = org.headlines().nth(1).unwrap(); - /// - /// let mut h1_1_2 = Headline::new( - /// Title { - /// raw: "h1_1_2".into(), - /// ..Default::default() - /// }, - /// &mut org, - /// ); - /// - /// // level must be greater than 2, and smaller than or equal to 5 - /// h1_1_2.set_level(2, &mut org).unwrap(); - /// assert!(h1_1.append(h1_1_2, &mut org).is_err()); - /// h1_1_2.set_level(6, &mut org).unwrap(); - /// assert!(h1_1.append(h1_1_2, &mut org).is_err()); - /// - /// h1_1_2.set_level(4, &mut org).unwrap(); - /// assert!(h1_1.append(h1_1_2, &mut org).is_ok()); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// * h1 - /// ** h1_1 - /// ***** h1_1_1 - /// **** h1_1_2 - /// "#, - /// ); - /// - /// // cannot append an attached headline - /// assert!(h1_1.append(h1_1_2, &mut org).is_err()); - /// ``` - pub fn append(self, hdl: Headline, org: &mut Org) -> ValidationResult<()> { - hdl.check_detached(org)?; - - if let Some(last) = self.last_child(org) { - hdl.check_level(self.lvl + 1..=last.lvl)?; - } else { - hdl.check_level(self.lvl + 1..=usize::MAX)?; - } - - self.hdl_n.append(hdl.hdl_n, &mut org.arena); - - org.debug_validate(); - - Ok(()) - } - - /// Prepends a new child to this headline. - /// - /// Returns an error if the given new child was already attached, or - /// the given new child didn't meet the requirements. - /// - /// ```rust - /// # use orgize::{elements::Title, Headline, Org}; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// ***** h1_1_1 - /// "#, - /// ); - /// - /// let h1_1 = org.headlines().nth(1).unwrap(); - /// - /// let mut h1_1_2 = Headline::new( - /// Title { - /// raw: "h1_1_2".into(), - /// ..Default::default() - /// }, - /// &mut org, - /// ); - /// - /// // level must be greater than or equal to 5 - /// h1_1_2.set_level(2, &mut org).unwrap(); - /// assert!(h1_1.prepend(h1_1_2, &mut org).is_err()); - /// - /// h1_1_2.set_level(5, &mut org).unwrap(); - /// assert!(h1_1.prepend(h1_1_2, &mut org).is_ok()); - /// - /// // cannot prepend an attached headline - /// assert!(h1_1.prepend(h1_1_2, &mut org).is_err()); - /// ``` - pub fn prepend(self, hdl: Headline, org: &mut Org) -> ValidationResult<()> { - hdl.check_detached(org)?; - - if let Some(first) = self.first_child(org) { - hdl.check_level(first.lvl..=usize::MAX)?; - } else { - hdl.check_level(self.lvl + 1..=usize::MAX)?; - } - - self.sec_n - .unwrap_or(self.ttl_n) - .insert_after(hdl.hdl_n, &mut org.arena); - - org.debug_validate(); - - Ok(()) - } - - /// Inserts a new sibling before this headline. - /// - /// Returns an error if the given new child was already attached, or - /// the given new child didn't meet the requirements. - /// - /// ```rust - /// # use orgize::{elements::Title, Headline, Org}; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// **** h1_1_1 - /// *** h1_1_3 - /// "#, - /// ); - /// - /// let h1_1_3 = org.headlines().nth(3).unwrap(); - /// - /// let mut h1_1_2 = Headline::new( - /// Title { - /// raw: "h1_1_2".into(), - /// ..Default::default() - /// }, - /// &mut org, - /// ); - /// - /// // level must be greater than or equal to 3, but smaller than or equal to 4 - /// h1_1_2.set_level(2, &mut org).unwrap(); - /// assert!(h1_1_3.insert_before(h1_1_2, &mut org).is_err()); - /// h1_1_2.set_level(5, &mut org).unwrap(); - /// assert!(h1_1_3.insert_before(h1_1_2, &mut org).is_err()); - /// - /// h1_1_2.set_level(4, &mut org).unwrap(); - /// assert!(h1_1_3.insert_before(h1_1_2, &mut org).is_ok()); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// * h1 - /// ** h1_1 - /// **** h1_1_1 - /// **** h1_1_2 - /// *** h1_1_3 - /// "#, - /// ); - /// - /// // cannot insert an attached headline - /// assert!(h1_1_3.insert_before(h1_1_2, &mut org).is_err()); - /// ``` - pub fn insert_before(self, hdl: Headline, org: &mut Org) -> ValidationResult<()> { - hdl.check_detached(org)?; - - if let Some(previous) = self.previous(org) { - hdl.check_level(self.lvl..=previous.lvl)?; - } else { - hdl.check_level(self.lvl..=usize::MAX)?; - } - - self.hdl_n.insert_before(hdl.hdl_n, &mut org.arena); - - org.debug_validate(); - - Ok(()) - } - - /// Inserts a new sibling after this headline. - /// - /// Returns an error if the given new child was already attached, or - /// the given new child didn't meet the requirements. - /// - /// ```rust - /// # use orgize::{elements::Title, Headline, Org}; - /// # - /// let mut org = Org::parse( - /// r#" - /// * h1 - /// ** h1_1 - /// **** h1_1_1 - /// *** h1_1_3 - /// "#, - /// ); - /// - /// let h1_1_1 = org.headlines().nth(2).unwrap(); - /// - /// let mut h1_1_2 = Headline::new( - /// Title { - /// raw: "h1_1_2".into(), - /// ..Default::default() - /// }, - /// &mut org, - /// ); - /// - /// // level must be greater than or equal to 3, but smaller than or equal to 4 - /// h1_1_2.set_level(2, &mut org).unwrap(); - /// assert!(h1_1_1.insert_after(h1_1_2, &mut org).is_err()); - /// h1_1_2.set_level(5, &mut org).unwrap(); - /// assert!(h1_1_1.insert_after(h1_1_2, &mut org).is_err()); - /// - /// h1_1_2.set_level(4, &mut org).unwrap(); - /// assert!(h1_1_1.insert_after(h1_1_2, &mut org).is_ok()); - /// - /// let mut writer = Vec::new(); - /// org.write_org(&mut writer).unwrap(); - /// assert_eq!( - /// String::from_utf8(writer).unwrap(), - /// r#" - /// * h1 - /// ** h1_1 - /// **** h1_1_1 - /// **** h1_1_2 - /// *** h1_1_3 - /// "#, - /// ); - /// - /// // cannot insert an attached headline - /// assert!(h1_1_1.insert_after(h1_1_2, &mut org).is_err()); - /// ``` - pub fn insert_after(self, hdl: Headline, org: &mut Org) -> ValidationResult<()> { - hdl.check_detached(org)?; - - if let Some(next) = self.next(org) { - hdl.check_level(next.lvl..=self.lvl)?; - } else if let Some(parent) = self.parent(org) { - hdl.check_level(parent.lvl + 1..=self.lvl)?; - } else { - hdl.check_level(1..=self.lvl)?; - } - - self.hdl_n.insert_after(hdl.hdl_n, &mut org.arena); - - org.debug_validate(); - - Ok(()) - } - - fn check_detached(self, org: &Org) -> ValidationResult<()> { - if !self.is_detached(org) { - Err(ValidationError::ExpectedDetached { at: self.hdl_n }) - } else { - Ok(()) - } - } - - fn check_level(self, range: RangeInclusive) -> ValidationResult<()> { - if !range.contains(&self.lvl) { - Err(ValidationError::HeadlineLevelMismatch { - range, - at: self.hdl_n, - }) - } else { - Ok(()) - } - } -} - -impl Org<'_> { - /// Returns the `Document`. - pub fn document(&self) -> Document { - Document::from_org(self) - } - - /// Returns an iterator of `Headline`s. - pub fn headlines(&self) -> impl Iterator + '_ { - self.root - .descendants(&self.arena) - .skip(1) - .filter_map(move |node| match self[node] { - Element::Headline { level } => Some(Headline::from_node(node, level, self)), - _ => None, - }) - } -} diff --git a/src/lib.rs b/src/lib.rs index 0033ea8..fe7956e 100644 --- a/src/lib.rs +++ b/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(), -//! "

    title

    section

    " -//! ); -//! ``` -//! -//! # 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 trait is required for custom error type -//! impl From for MyError { -//! fn from(err: IOError) -> Self { -//! MyError::IO(err) -//! } -//! } -//! -//! impl From for MyError { -//! fn from(err: FromUtf8Error) -> Self { -//! MyError::Utf8(err) -//! } -//! } -//! -//! #[derive(Default)] -//! struct MyHtmlHandler(DefaultHtmlHandler); -//! -//! impl HtmlHandler for MyHtmlHandler { -//! fn start(&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, -//! "", -//! title.level, -//! slugify!(&title.raw), -//! )?; -//! } -//! } else { -//! // fallthrough to default handler -//! self.0.start(w, element)?; -//! } -//! Ok(()) -//! } -//! -//! fn end(&mut self, mut w: W, element: &Element) -> Result<(), MyError> { -//! if let Element::Title(title) = element { -//! write!(w, "", 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)?, -//! "

    title

    \ -//!

    section

    " -//! ); -//! -//! 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, +}; diff --git a/src/org.rs b/src/org.rs index 37c06fc..dfd088e 100644 --- a/src/org.rs +++ b/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>, - 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) -> 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> { - &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> { - &mut self.arena + /// Walk through org element tree using given traverser + pub fn traverse(&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> + '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> { - 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(&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(&self, mut writer: W, handler: &mut H) -> Result<(), E> - where - W: Write, - E: From, - H: HtmlHandler, - { - 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>(&self) -> Option { + fn find>(node: SyntaxNode) -> Option { + 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(&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(&self, mut writer: W, handler: &mut H) -> Result<(), E> - where - W: Write, - E: From, - H: OrgHandler, - { - 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 for Org<'a> { - type Output = Element<'a>; - - fn index(&self, node_id: NodeId) -> &Self::Output { - self.arena[node_id].get() - } -} - -impl<'a> IndexMut 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(&self, serializer: S) -> Result { - use serde_indextree::Node; - - serializer.serialize_newtype_struct("Org", &Node::new(self.root, &self.arena)) + find(SyntaxNode::new_root(self.green.clone())) } } diff --git a/src/parse/combinators.rs b/src/parse/combinators.rs deleted file mode 100644 index f5d518e..0000000 --- a/src/parse/combinators.rs +++ /dev/null @@ -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(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(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))); -} diff --git a/src/parse/mod.rs b/src/parse/mod.rs deleted file mode 100644 index 0c49327..0000000 --- a/src/parse/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod combinators; diff --git a/src/parsers.rs b/src/parsers.rs deleted file mode 100644 index d277057..0000000 --- a/src/parsers.rs +++ /dev/null @@ -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(&mut self, element: T, parent: NodeId) -> NodeId - where - T: Into>; - fn insert_before_last_child(&mut self, element: T, parent: NodeId) -> NodeId - where - T: Into>; - fn set(&mut self, node: NodeId, element: T) - where - T: Into>; -} - -pub type BorrowedArena<'a> = Arena>; - -impl<'a> ElementArena<'a> for BorrowedArena<'a> { - fn append(&mut self, element: T, parent: NodeId) -> NodeId - where - T: Into>, - { - let node = self.new_node(element.into()); - parent.append(node, self); - node - } - - fn insert_before_last_child(&mut self, element: T, parent: NodeId) -> NodeId - where - T: Into>, - { - 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(&mut self, node: NodeId, element: T) - where - T: Into>, - { - *self[node].get_mut() = element.into(); - } -} - -pub struct OwnedArena<'a, 'b, 'c> { - arena: &'b mut Arena>, - phantom: PhantomData<&'a ()>, -} - -impl<'a, 'b, 'c> OwnedArena<'a, 'b, 'c> { - pub fn new(arena: &'b mut Arena>) -> OwnedArena<'a, 'b, 'c> { - OwnedArena { - arena, - phantom: PhantomData, - } - } -} - -impl<'a> ElementArena<'a> for OwnedArena<'a, '_, '_> { - fn append(&mut self, element: T, parent: NodeId) -> NodeId - where - T: Into>, - { - self.arena.append(element.into().into_owned(), parent) - } - - fn insert_before_last_child(&mut self, element: T, parent: NodeId) -> NodeId - where - T: Into>, - { - self.arena - .insert_before_last_child(element.into().into_owned(), parent) - } - - fn set(&mut self, node: NodeId, element: T) - where - T: Into>, - { - 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>, - 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>, -) { - 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>, -) { - 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>, -) -> 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, -} - -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 { - 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>, -) { - 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>, - 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>, -) -> 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>, - 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 - } -} diff --git a/src/syntax/block.rs b/src/syntax/block.rs new file mode 100644 index 0000000..b6d764f --- /dev/null +++ b/src/syntax/block.rs @@ -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 { + 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 { + 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, 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 { + 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 { + 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::(block_node); + let to_example_block = to_ast::(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 +} diff --git a/src/syntax/clock.rs b/src/syntax/clock.rs new file mode 100644 index 0000000..7505a35 --- /dev/null +++ b/src/syntax/clock.rs @@ -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 { + 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_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" + "### + ); +} diff --git a/src/syntax/combinator.rs b/src/syntax/combinator.rs new file mode 100644 index 0000000..282052c --- /dev/null +++ b/src/syntax/combinator.rs @@ -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; + +#[inline] +pub fn token(kind: SyntaxKind, input: &str) -> GreenElement { + GreenElement::Token(GreenToken::new(OrgLanguage::kind_to_raw(kind), input)) +} + +#[inline] +pub fn node(kind: SyntaxKind, children: I) -> GreenElement +where + I: IntoIterator, + 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 { + 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, GreenElement, ()> +where + F: Parser, 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, ()> { + 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 { + 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 + '_ { + 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 + '_ { + memchr_iter(b'\n', s.as_bytes()) + .map(|i| i + 1) + .chain(once(s.len())) +} + +pub struct NodeBuilder { + pub children: Vec, +} + +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) { + 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)) + } +} diff --git a/src/syntax/comment.rs b/src/syntax/comment.rs new file mode 100644 index 0000000..45d5303 --- /dev/null +++ b/src/syntax/comment.rs @@ -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 { + 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 { + 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" + "### + ); +} diff --git a/src/syntax/cookie.rs b/src/syntax/cookie.rs new file mode 100644 index 0000000..e62e66d --- /dev/null +++ b/src/syntax/cookie.rs @@ -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 { + 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_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()); +} diff --git a/src/syntax/document.rs b/src/syntax/document.rs new file mode 100644 index 0000000..d3e8ff3 --- /dev/null +++ b/src/syntax/document.rs @@ -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 { + debug_assert_lossless(document_node_base)(input) +} + +fn document_node_base(input: Input) -> IResult { + 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_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" + "### + ); +} diff --git a/src/syntax/drawer.rs b/src/syntax/drawer.rs new file mode 100644 index 0000000..1c91d64 --- /dev/null +++ b/src/syntax/drawer.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + debug_assert_lossless(property_drawer_node_base)(input) +} + +#[tracing::instrument(skip(input), fields(input = input.s))] +pub fn drawer_node(input: Input) -> IResult { + debug_assert_lossless(drawer_node_base)(input) +} + +#[test] +fn parse() { + use crate::{ast::Drawer, tests::to_ast, ParseConfig}; + + let to_drawer = to_ast::(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()); +} diff --git a/src/syntax/dyn_block.rs b/src/syntax/dyn_block.rs new file mode 100644 index 0000000..81ed103 --- /dev/null +++ b/src/syntax/dyn_block.rs @@ -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 { + 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 { + 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 { + 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 { + debug_assert_lossless(dyn_block_node_base)(input) +} + +#[test] +fn parse() { + use crate::{ast::DynBlock, tests::to_ast}; + + let to_dyn_block = to_ast::(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 " " + "### + ); +} diff --git a/src/syntax/element.rs b/src/syntax/element.rs new file mode 100644 index 0000000..244c5e4 --- /dev/null +++ b/src/syntax/element.rs @@ -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, 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, 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 { + 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 "]]" + "### + ) +} diff --git a/src/syntax/emphasis.rs b/src/syntax/emphasis.rs new file mode 100644 index 0000000..06c45a3 --- /dev/null +++ b/src/syntax/emphasis.rs @@ -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 { + 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 { + debug_assert_lossless(map(emphasis(b'~'), |contents| { + node( + CODE, + [token(TILDE, "~"), contents.text_token(), token(TILDE, "~")], + ) + }))(input) +} + +pub fn strike_node(input: Input) -> IResult { + 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 { + debug_assert_lossless(map(emphasis(b'='), |contents| { + node( + VERBATIM, + [token(EQUAL, "="), contents.text_token(), token(EQUAL, "=")], + ) + }))(input) +} + +pub fn underline_node(input: Input) -> IResult { + 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 { + 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 { + 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_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()); +} diff --git a/src/syntax/fixed_width.rs b/src/syntax/fixed_width.rs new file mode 100644 index 0000000..5fb8690 --- /dev/null +++ b/src/syntax/fixed_width.rs @@ -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 { + 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 { + debug_assert_lossless(fixed_width_node_base)(input) +} + +#[test] +fn parse() { + use crate::{ast::FixedWidth, tests::to_ast}; + + let to_fixed_width = to_ast::(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 " " + "### + ); +} diff --git a/src/syntax/fn_def.rs b/src/syntax/fn_def.rs new file mode 100644 index 0000000..6014dc5 --- /dev/null +++ b/src/syntax/fn_def.rs @@ -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 { + 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::(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" + "### + ); +} diff --git a/src/syntax/fn_ref.rs b/src/syntax/fn_ref.rs new file mode 100644 index 0000000..6902101 --- /dev/null +++ b/src/syntax/fn_ref.rs @@ -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 { + debug_assert_lossless(fn_ref_node_base)(input) +} + +fn fn_ref_node_base(input: Input) -> IResult { + 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 { + 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::(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()); +} diff --git a/src/syntax/headline.rs b/src/syntax/headline.rs new file mode 100644 index 0000000..afd26aa --- /dev/null +++ b/src/syntax/headline.rs @@ -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 { + debug_assert_lossless(headline_node_base)(input) +} + +#[instrument(skip(input), fields(input = input.s))] +fn headline_node_base(input: Input) -> IResult { + 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 { + let (input, section) = section_text(input)?; + Ok((input, node(SECTION, element_nodes(section)?))) +} + +pub fn section_text(input: Input) -> IResult { + 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 { + 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 { + 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 { + 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 { + 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_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_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::>(), + ); + + let tags = to_headline("* a \t :@:").tags().unwrap(); + assert_eq!( + vec!["@".to_string()], + tags.iter().map(|x| x.to_string()).collect::>(), + ); + + let tags = to_headline("* a :#:").tags().unwrap(); + assert_eq!( + vec!["#".to_string()], + tags.iter().map(|x| x.to_string()).collect::>(), + ); + + let tags = to_headline("* a\t :%:").tags().unwrap(); + assert_eq!( + vec!["%".to_string()], + tags.iter().map(|x| x.to_string()).collect::>(), + ); + + // let tags = to_headline("* a :余:").tags().unwrap(); + // assert_eq!( + // vec!["余".to_string()], + // tags.iter().map(|x| x.to_string()).collect::>(), + // ); +} diff --git a/src/syntax/inline_call.rs b/src/syntax/inline_call.rs new file mode 100644 index 0000000..15c07a2 --- /dev/null +++ b/src/syntax/inline_call.rs @@ -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 { + 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::(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 "]" + "### + ); +} diff --git a/src/syntax/inline_src.rs b/src/syntax/inline_src.rs new file mode 100644 index 0000000..6d6c98a --- /dev/null +++ b/src/syntax/inline_src.rs @@ -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 { + 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::(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]{text}").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 "text" + R_CURLY@38..39 "}" + "### + ); + + let config = &ParseConfig::default(); + + assert!(inline_src_node(("src_xml[:exports code]{text", config).into()).is_err()); + assert!(inline_src_node(("src_[:exports code]{text}", config).into()).is_err()); + assert!(inline_src_node(("src_xml[:exports code]", config).into()).is_err()); +} diff --git a/src/syntax/input.rs b/src/syntax/input.rs new file mode 100644 index 0000000..fe2054f --- /dev/null +++ b/src/syntax/input.rs @@ -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> for Input<'a> { + fn slice(&self, range: Range) -> Self { + self.of(self.s.slice(range)) + } +} + +impl<'a> Slice> for Input<'a> { + fn slice(&self, range: RangeTo) -> Self { + self.of(self.s.slice(range)) + } +} + +impl<'a> Slice> for Input<'a> { + fn slice(&self, range: RangeFrom) -> Self { + self.of(self.s.slice(range)) + } +} + +impl<'a> Slice 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 { + 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

    (&self, predicate: P) -> Option + where + P: Fn(Self::Item) -> bool, + { + self.s.position(predicate) + } + #[inline] + fn slice_index(&self, count: usize) -> Result { + 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>(&self, predicate: P) -> IResult + 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>( + &self, + predicate: P, + e: ErrorKind, + ) -> IResult + 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>( + &self, + predicate: P, + ) -> IResult + 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>( + &self, + predicate: P, + e: ErrorKind, + ) -> IResult + 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) + } +} diff --git a/src/syntax/keyword.rs b/src/syntax/keyword.rs new file mode 100644 index 0000000..9da9aed --- /dev/null +++ b/src/syntax/keyword.rs @@ -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 { + debug_assert_lossless(keyword_node_base)(input) +} + +fn keyword_node_base(input: Input) -> IResult { + 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, ()> { + 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_node); + + let to_babel_call = to_ast::(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()); +} diff --git a/src/syntax/link.rs b/src/syntax/link.rs new file mode 100644 index 0000000..85b1824 --- /dev/null +++ b/src/syntax/link.rs @@ -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 { + 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_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()); +} diff --git a/src/syntax/list.rs b/src/syntax/list.rs new file mode 100644 index 0000000..28b521d --- /dev/null +++ b/src/syntax/list.rs @@ -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 { + debug_assert_lossless(list_node_base)(input) +} + +fn list_node_base(input: Input) -> IResult { + 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, 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 { + 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 { + 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 { + 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 { + 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 { + 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_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()); +} diff --git a/src/syntax/macros.rs b/src/syntax/macros.rs new file mode 100644 index 0000000..1c187f3 --- /dev/null +++ b/src/syntax/macros.rs @@ -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 { + 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_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()); +} diff --git a/src/syntax/mod.rs b/src/syntax/mod.rs new file mode 100644 index 0000000..b3f9d8b --- /dev/null +++ b/src/syntax/mod.rs @@ -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::(raw.0) } + } + + fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind { + rowan::SyntaxKind(kind as u16) + } +} + +pub type SyntaxNode = rowan::SyntaxNode; +pub type SyntaxToken = rowan::SyntaxToken; +pub type SyntaxElement = rowan::SyntaxElement; +pub type SyntaxNodeChildren = rowan::SyntaxNodeChildren; +pub type SyntaxElementChildren = rowan::SyntaxElementChildren; + +#[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 for rowan::SyntaxKind { + fn from(value: SyntaxKind) -> Self { + OrgLanguage::kind_to_raw(value) + } +} diff --git a/src/syntax/object.rs b/src/syntax/object.rs new file mode 100644 index 0000000..c1b0888 --- /dev/null +++ b/src/syntax/object.rs @@ -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, +} + +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.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 { + // 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 { + 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 { + 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 "." + "### + ); +} diff --git a/src/syntax/paragraph.rs b/src/syntax/paragraph.rs new file mode 100644 index 0000000..1faf528 --- /dev/null +++ b/src/syntax/paragraph.rs @@ -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 { + 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 { + debug_assert_lossless(paragraph_node_base)(input) +} + +pub fn paragraph_nodes(input: Input) -> Result, 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_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" + "### + ); +} diff --git a/src/syntax/planning.rs b/src/syntax/planning.rs new file mode 100644 index 0000000..a93cf91 --- /dev/null +++ b/src/syntax/planning.rs @@ -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 { + debug_assert_lossless(planning_node_base)(input) +} + +fn planning_node_base(input: Input) -> IResult { + 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_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()); +} diff --git a/src/syntax/radio_target.rs b/src/syntax/radio_target.rs new file mode 100644 index 0000000..6787ad7 --- /dev/null +++ b/src/syntax/radio_target.rs @@ -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 { + 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::(radio_target_node); + + insta::assert_debug_snapshot!( + to_radio_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("<<>>").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(("<<>>", config).into()).is_err()); + assert!(radio_target_node(("<<< target>>>", config).into()).is_err()); + assert!(radio_target_node(("<<>>", config).into()).is_err()); + assert!(radio_target_node(("<<get>>>", config).into()).is_err()); + assert!(radio_target_node(("<<>>", config).into()).is_err()); + assert!(radio_target_node(("<<>", config).into()).is_err()); +} diff --git a/src/syntax/rule.rs b/src/syntax/rule.rs new file mode 100644 index 0000000..df28294 --- /dev/null +++ b/src/syntax/rule.rs @@ -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 { + 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_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()); +} diff --git a/src/syntax/snippet.rs b/src/syntax/snippet.rs new file mode 100644 index 0000000..e09e914 --- /dev/null +++ b/src/syntax/snippet.rs @@ -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 { + 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_node); + + insta::assert_debug_snapshot!( + to_snippet("@@html:@@").syntax, + @r###" + SNIPPET@0..12 + AT2@0..2 "@@" + TEXT@2..6 "html" + COLON@6..7 ":" + TEXT@7..10 "" + 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:

    @

    @@").syntax, + @r###" + SNIPPET@0..17 + AT2@0..2 "@@" + TEXT@2..6 "html" + COLON@6..7 ":" + TEXT@7..15 "

    @

    " + AT2@15..17 "@@" + "### + ); + + let config = &ParseConfig::default(); + + assert!(snippet_node(("@@html:@", config).into()).is_err()); + assert!(snippet_node(("@@html@@", config).into()).is_err()); + assert!(snippet_node(("@@:@@", config).into()).is_err()); +} diff --git a/src/syntax/table.rs b/src/syntax/table.rs new file mode 100644 index 0000000..4f53bfe --- /dev/null +++ b/src/syntax/table.rs @@ -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 { + 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> { + 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 { + 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 { + debug_assert_lossless(org_table_node_base)(input) +} + +pub fn table_el_node(input: Input) -> IResult { + 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::(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::(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()); +} diff --git a/src/syntax/target.rs b/src/syntax/target.rs new file mode 100644 index 0000000..2368d9b --- /dev/null +++ b/src/syntax/target.rs @@ -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 { + 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_node); + + insta::assert_debug_snapshot!( + to_target("<>").syntax, + @r###" + TARGET@0..10 + L_ANGLE2@0..2 "<<" + TEXT@2..8 "target" + R_ANGLE2@8..10 ">>" + "### + ); + + insta::assert_debug_snapshot!( + to_target("<>").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(("<>", config).into()).is_err()); + assert!(target_node(("<< target>>", config).into()).is_err()); + assert!(target_node(("<>", config).into()).is_err()); + assert!(target_node(("<get>>", config).into()).is_err()); + assert!(target_node(("<>", config).into()).is_err()); + assert!(target_node(("<", config).into()).is_err()); +} diff --git a/src/syntax/timestamp.rs b/src/syntax/timestamp.rs new file mode 100644 index 0000000..4188069 --- /dev/null +++ b/src/syntax/timestamp.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + debug_assert_lossless(timestamp_active_node_base)(input) +} +pub fn timestamp_inactive_node(input: Input) -> IResult { + debug_assert_lossless(timestamp_inactive_node_base)(input) +} + +#[test] +fn parse() { + use crate::{ast::Timestamp, tests::to_ast}; + + let to_timestamp = to_ast::(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 "]" + "### + ); +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..5ed57c3 --- /dev/null +++ b/src/tests.rs @@ -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( + parser: impl Fn(Input) -> IResult, +) -> 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::::new_root(node); + AstNode::cast(node).unwrap() + } +} diff --git a/src/validate.rs b/src/validate.rs deleted file mode 100644 index 8f09a42..0000000 --- a/src/validate.rs +++ /dev/null @@ -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, - 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 = Result; - -impl Org<'_> { - /// Validates an `Org` struct. - pub fn validate(&self) -> Vec { - 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." - ); - } - } - } -} diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs deleted file mode 100644 index 1f8aeb1..0000000 --- a/src/wasm/mod.rs +++ /dev/null @@ -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(value: &T) -> JsValue { - value - .serialize(&Serializer::new().serialize_maps_as_objects(true)) - .unwrap() -} diff --git a/tests/blank.rs b/tests/blank.rs deleted file mode 100644 index 288e26a..0000000 --- a/tests/blank.rs +++ /dev/null @@ -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); -} diff --git a/tests/issue_15_16.rs b/tests/issue_15_16.rs deleted file mode 100644 index 3d8d9fc..0000000 --- a/tests/issue_15_16.rs +++ /dev/null @@ -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); -} diff --git a/tests/parse.rs b/tests/parse.rs index 69fb98c..e7eda2a 100644 --- a/tests/parse.rs +++ b/tests/parse.rs @@ -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); } }; } diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 0000000..97d0a01 --- /dev/null +++ b/wasm/Cargo.toml @@ -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" diff --git a/wasm/README.md b/wasm/README.md index b914486..673208b 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -2,40 +2,61 @@ ![npm](https://img.shields.io/npm/v/orgize) -## 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 diff --git a/wasm/build.rs b/wasm/build.rs new file mode 100644 index 0000000..db4d2ca --- /dev/null +++ b/wasm/build.rs @@ -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); + } +} diff --git a/wasm/index.html b/wasm/index.html index 48bb9b4..423ecc9 100644 --- a/wasm/index.html +++ b/wasm/index.html @@ -1,71 +1,274 @@ - Orgize wasm demo - + Orgize + + + + + - - -

    Orgize wasm demo

    - -
    - GitHub - NPM - crates.io -
    - -
    Input:
    - - - -
    Type:
    - -
    - - - -
    - -
    Output:
    - - - -
    
    -    
    
    +  
         
    + style=" + margin: 16px; + display: flex; + flex-direction: row; + flex: 1; + min-height: 0; + gap: 16px; + " + > +
    +
    +
    + +
    +
    + + HTML (rendered) + + HTML + Syntax +
    +
    +
    + +
    +
    +
    +
    +
+ + diff --git a/wasm/package.json b/wasm/package.json index 5cd5b93..17f8c82 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -3,25 +3,27 @@ "version": "0.0.3", "license": "MIT", "author": "PoiScript ", - "main": "lib/orgize.umd.js", - "module": "lib/orgize.es.js", - "typings": "lib/orgize.d.ts", + "scripts": { + "build": "rm -rf dist && wasm-pack build -t web -d dist --out-name orgize" + }, "repository": { "type": "git", "url": "https://github.com/PoiScript/orgize" }, - "scripts": { - "prebuild": "rm -rf out-tsc/ lib/", - "build": "tsc && rollup -c rollup.js && cp pkg/orgize_bg.wasm lib/" - }, - "devDependencies": { - "rollup": "^2.56.3", - "rollup-plugin-copy": "^3.4.0", - "rollup-plugin-dts": "^4.0.0", - "typescript": "^4.4.2" + "module": "orgize.js", + "typings": "orgize.d.ts", + "exports": { + ".": { + "types": "./dist/orgize.d.ts", + "import": "./dist/orgize.js" + }, + "./wasm": "./dist/orgize_bg.wasm" }, "files": [ - "lib", + "dist/orgize_bg.wasm", + "dist/orgize.js", + "dist/orgize.d.ts", + "index.html", "README.md" ] } diff --git a/wasm/rollup.js b/wasm/rollup.js deleted file mode 100644 index 1da0f98..0000000 --- a/wasm/rollup.js +++ /dev/null @@ -1,31 +0,0 @@ -import dts from "rollup-plugin-dts"; -import copy from "rollup-plugin-copy"; - -export default [ - { - input: "./out-tsc/index.d.ts", - output: { - file: "./lib/orgize.d.ts", - }, - plugins: [dts()], - }, - { - input: "./out-tsc/index.js", - output: [ - { - file: "./lib/orgize.es.js", - format: "es", - }, - { - name: "orgize", - file: "./lib/orgize.umd.js", - format: "umd", - }, - ], - plugins: [ - copy({ - targets: [{ src: "index.html", dest: "lib" }], - }), - ], - }, -]; diff --git a/wasm/src/handler.ts b/wasm/src/handler.ts deleted file mode 100644 index 497ab93..0000000 --- a/wasm/src/handler.ts +++ /dev/null @@ -1,102 +0,0 @@ -export class Handler { - text(_text: string) {} - code(_item: string) {} - cookie(_item: Cookie) {} - rule() {} - exampleBlock(_item: Block) {} - exportBlock(_item: Block) {} - sourceBlock(_item: SourceBlock) {} - inlineSrc(_item: InlineSrc) {} - link(_item: Link) {} - snippet(_item: Snippet) {} - timestamp(_item: any) {} - verbatim(_item: string) {} - fixedWidth(_item: FixedWidth) {} - listStart(_item: List) {} - listEnd(_item: List) {} - tableStart(_item: any) {} - tableEnd(_item: any) {} - tableRowStart(_item: any) {} - tableRowEnd(_item: any) {} - tableCellStart(_item: any) {} - tableCellEnd(_item: any) {} - titleStart(_item: Title) {} - titleEnd(_item: Title) {} - boldStart() {} - boldEnd() {} - centerBlockStart(_item: any) {} - centerBlockEnd(_item: any) {} - documentStart() {} - documentEnd() {} - italicStart() {} - italicEnd() {} - listItemStart() {} - listItemEnd() {} - paragraphStart() {} - paragraphEnd() {} - quoteBlockStart(_item: any) {} - quoteBlockEnd(_item: any) {} - sectionStart() {} - sectionEnd() {} - strikeStart() {} - strikeEnd() {} - underlineStart() {} - underlineEnd() {} - verseBlockStart(_item: any) {} - verseBlockEnd(_item: any) {} - keyword(_item: Keyword) {} -} - -export type Title = { - level: number; - priority?: string; - tags?: string[]; - keyword?: string; - raw: string; - properties?: { [key: string]: string }; - post_blank: number; -}; - -export type List = { - ordered: boolean; -}; - -export type Block = { - contents: string; -}; - -export type InlineSrc = { - lang: string; - body: string; -}; - -export type Link = { - path: string; - desc?: string; -}; - -export type FixedWidth = { - value: string; -}; - -export type Cookie = { - value: string; -}; - -export type SourceBlock = { - contents: string; - language: string; - arguments: string; - post_blank: number; -}; - -export type Keyword = { - key: string; - optional?: string; - value: string; -}; - -export type Snippet = { - name: string; - value: string; -}; diff --git a/wasm/src/html.ts b/wasm/src/html.ts deleted file mode 100644 index 9053f38..0000000 --- a/wasm/src/html.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { - Block, - Cookie, - FixedWidth, - Handler, - InlineSrc, - Link, - List, - Snippet, - Title, -} from "./handler"; - -const tags: { [tag: string]: string } = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", -}; - -const replaceTags = (tag: string): string => tags[tag]; - -export const escapeHtml = (str: string): string => - str.replace(/[&<>"']/g, replaceTags); - -export class HtmlHandler extends Handler { - result: string; - - constructor(result: string = "") { - super(); - this.result = result; - } - - static escape(): string { - return ""; - } - - quoteBlockStart() { - this.result += "
"; - } - quoteBlockEnd() { - this.result += "
"; - } - centerBlockStart() { - this.result += '
'; - } - centerBlockEnd() { - this.result += "
"; - } - verseBlockStart() { - this.result += '

'; - } - verseBlockEnd() { - this.result += "

"; - } - boldStart() { - this.result += ""; - } - boldEnd() { - this.result += ""; - } - documentStart() { - this.result += "
"; - } - documentEnd() { - this.result += "
"; - } - - listStart(list: List) { - this.result += `<${list.ordered ? "o" : "u"}l>`; - } - listEnd(list: List) { - this.result += ``; - } - - italicStart() { - this.result += ""; - } - italicEnd() { - this.result += ""; - } - listItemStart() { - this.result += "
  • "; - } - listItemEnd() { - this.result += "
  • "; - } - paragraphStart() { - this.result += "

    "; - } - paragraphEnd() { - this.result += "

    "; - } - sectionStart() { - this.result += "
    "; - } - sectionEnd() { - this.result += "
    "; - } - strikeStart() { - this.result += ""; - } - strikeEnd() { - this.result += ""; - } - underlineStart() { - this.result += ""; - } - underlineEnd() { - this.result += ""; - } - - exampleBlock(block: Block) { - this.result += `
    ${escapeHtml(block.contents)}
    `; - } - - sourceBlock(block: Block) { - this.result += `
    ${escapeHtml(block.contents)}
    `; - } - inlineSrc(src: InlineSrc) { - this.result += `${escapeHtml( - src.body - )}`; - } - code(value: string) { - this.result += `${escapeHtml(value)}`; - } - link(link: Link) { - this.result += `${escapeHtml( - link.desc || link.path - )}`; - } - snippet(snippet: Snippet) { - if (snippet.name.toLowerCase() === "html") { - this.result += snippet.value; - } - } - text(value: string) { - this.result += escapeHtml(value); - } - verbatim(value: string) { - this.result += `${escapeHtml(value)}`; - } - fixedWidth(item: FixedWidth) { - this.result += `
    ${escapeHtml(item.value)}
    `; - } - rule() { - this.result += "
    "; - } - cookie(cookie: Cookie) { - this.result += `${escapeHtml(cookie.value)}`; - } - - titleStart(title: Title) { - this.result += ``; - } - titleEnd(title: Title) { - this.result += ``; - } -} diff --git a/wasm/src/index.ts b/wasm/src/index.ts deleted file mode 100644 index ed302d9..0000000 --- a/wasm/src/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import init, { - handle as internalHandle, - InitInput, - InitOutput, - Org, -} from "../pkg/orgize"; -import { Handler } from "./handler"; -import { HtmlHandler } from "./html"; -import { CollectKeywords } from "./keyword"; - -export const handle = (org: Org | string, handler: Handler) => { - if (typeof org === "string") { - org = Org.parse(org); - } - internalHandle(org, handler); -}; - -export const renderHtml = ( - org: Org | string, - handler: HtmlHandler = new HtmlHandler() -): string => { - handle(org, handler); - return handler.result; -}; - -export const keywords = (org: Org | string): { [key: string]: string[] } => { - const handler = new CollectKeywords(); - handle(org, handler); - return handler.keywords; -}; - -export * from "./handler"; -export * from "./html"; -export { Org, init, InitInput, InitOutput }; diff --git a/wasm/src/keyword.ts b/wasm/src/keyword.ts deleted file mode 100644 index 85f2968..0000000 --- a/wasm/src/keyword.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Handler, Keyword } from "./handler"; - -export class CollectKeywords extends Handler { - keywords: { [key: string]: string[] } = {}; - - keyword(keyword: Keyword) { - this.keywords[keyword.key] = this.keywords[keyword.key] || []; - this.keywords[keyword.key].push(keyword.value); - } -} diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 0000000..f7c536d --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,44 @@ +use orgize::{rowan::ast::AstNode, Org as Inner}; + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Org { + inner: Inner, +} + +#[wasm_bindgen] +impl Org { + #[wasm_bindgen(constructor)] + pub fn parse(input: &str) -> Self { + Org { + inner: Inner::parse(input), + } + } + + pub fn html(&self) -> String { + self.inner.to_html() + } + + pub fn org(&self) -> String { + self.inner.to_org() + } + + pub fn syntax(&self) -> String { + format!("{:#?}", self.inner.document().syntax()) + } + + pub fn update(&mut self, s: &str) { + self.inner = Inner::parse(s); + } + + #[wasm_bindgen(getter, js_name = "buildTime")] + pub fn build_time() -> String { + env!("CARGO_BUILD_TIME").into() + } + + #[wasm_bindgen(getter, js_name = "gitHash")] + pub fn git_hash() -> String { + env!("CARGO_GIT_HASH").into() + } +} diff --git a/wasm/tests/html.js b/wasm/tests/html.js deleted file mode 100644 index 107e22d..0000000 --- a/wasm/tests/html.js +++ /dev/null @@ -1,82 +0,0 @@ -const { readFile } = require("fs/promises"); -const { resolve } = require("path"); -const { strictEqual } = require("assert"); - -const { init, renderHtml } = require("../lib/orgize.umd"); - -const assert = (org, html) => strictEqual(renderHtml(org), html); - -readFile(resolve(__dirname, "../lib/orgize_bg.wasm")) - .then((bytes) => new WebAssembly.Module(bytes)) - .then((module) => init(module)) - .then(() => { - assert( - "*bold*, /italic/,\n_underlined_, =verbatim= and ~code~", - "

    bold, italic,\nunderlined, " + - "verbatim and code

    " - ); - - assert( - "Visit[[http://example.com][link1]]or[[http://example.com][link1]].", - `

    Visitlink1orlink1.

    ` - ); - - assert( - ` -* title 1 -section 1 -** title 2 -section 2 -* title 3 -section 3 -* title 4 -section 4 -`, - "

    title 1

    section 1

    " + - "

    title 2

    section 2

    " + - "

    title 3

    section 3

    " + - "

    title 4

    section 4

    " - ); - - assert( - ` -+ 1 - -+ 2 - - - 3 - - - 4 - -+ 5 -`, - "
      " + - "
    • 1

    • " + - "
    • 2

      • 3

      • 4

    • " + - "
    • 5

    • " + - "
    " - ); - - assert( - "@@html:@@delete this@@html:@@", - "

    delete this

    " - ); - - assert( - ` -* title - -paragraph 1 - -paragraph 2 - -paragraph 3 - -paragraph 4 -`, - "

    title

    " + - "

    paragraph 1

    paragraph 2

    " + - "

    paragraph 3

    paragraph 4

    " + - "
    " - ); - }); diff --git a/wasm/tests/keyword.js b/wasm/tests/keyword.js deleted file mode 100644 index df2a255..0000000 --- a/wasm/tests/keyword.js +++ /dev/null @@ -1,17 +0,0 @@ -const { readFile } = require("fs/promises"); -const { resolve } = require("path"); -const { deepStrictEqual } = require("assert"); - -const { init, keywords } = require("../lib/orgize.umd"); - -const assert = (org, kw) => deepStrictEqual(keywords(org), kw); - -readFile(resolve(__dirname, "../lib/orgize_bg.wasm")) - .then((bytes) => new WebAssembly.Module(bytes)) - .then((module) => init(module)) - .then(() => { - assert("#+TITLE: orgize test cases\n#+FOO: bar", { - TITLE: ["orgize test cases"], - FOO: ["bar"], - }); - }); diff --git a/wasm/tsconfig.json b/wasm/tsconfig.json deleted file mode 100644 index dc4e3af..0000000 --- a/wasm/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "lib": ["ESNext", "WebWorker"], - "strict": true, - "outDir": "out-tsc", - "rootDir": "./src", - "declaration": true - }, - "include": ["./src"] -} diff --git a/wasm/yarn.lock b/wasm/yarn.lock deleted file mode 100644 index b704acd..0000000 --- a/wasm/yarn.lock +++ /dev/null @@ -1,416 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb" - integrity sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw== - dependencies: - "@babel/highlight" "^7.14.5" - -"@babel/helper-validator-identifier@^7.14.5": - version "7.14.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz#6654d171b2024f6d8ee151bf2509699919131d48" - integrity sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g== - -"@babel/highlight@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" - integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== - dependencies: - "@babel/helper-validator-identifier" "^7.14.5" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@types/fs-extra@^8.0.1": - version "8.1.2" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.2.tgz#7125cc2e4bdd9bd2fc83005ffdb1d0ba00cca61f" - integrity sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg== - dependencies: - "@types/node" "*" - -"@types/glob@^7.1.1": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" - integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - -"@types/minimatch@*": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" - integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== - -"@types/node@*": - version "16.11.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" - integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -colorette@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" - integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -fast-glob@^3.0.3: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== - dependencies: - reusify "^1.0.4" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -glob-parent@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globby@10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22" - integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A== - dependencies: - "@types/glob" "^7.1.1" - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.0.3" - glob "^7.1.3" - ignore "^5.1.1" - merge2 "^1.2.3" - slash "^3.0.0" - -graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -ignore@^5.1.1: - version "5.1.9" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb" - integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-glob@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-object@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b" - integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g== - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= - optionalDependencies: - graceful-fs "^4.1.6" - -magic-string@^0.25.7: - version "0.25.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== - dependencies: - sourcemap-codec "^1.4.4" - -merge2@^1.2.3, merge2@^1.3.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== - dependencies: - braces "^3.0.1" - picomatch "^2.2.3" - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rollup-plugin-copy@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-copy/-/rollup-plugin-copy-3.4.0.tgz#f1228a3ffb66ffad8606e2f3fb7ff23141ed3286" - integrity sha512-rGUmYYsYsceRJRqLVlE9FivJMxJ7X6jDlP79fmFkL8sJs7VVMSVyA2yfyL+PGyO/vJs4A87hwhgVfz61njI+uQ== - dependencies: - "@types/fs-extra" "^8.0.1" - colorette "^1.1.0" - fs-extra "^8.1.0" - globby "10.0.1" - is-plain-object "^3.0.0" - -rollup-plugin-dts@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-4.0.0.tgz#7645280183b7624e77375a548a11297f9916f6d8" - integrity sha512-tgUC8CxVgtlLDVloUEA9uACVaxjJHuYxlDSTp1LdCexA0bJx+RuMi45RjdLG9RTCgZlV5YBh3O7P2u6dS1KlnA== - dependencies: - magic-string "^0.25.7" - optionalDependencies: - "@babel/code-frame" "^7.14.5" - -rollup@^2.56.3: - version "2.56.3" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.56.3.tgz#b63edadd9851b0d618a6d0e6af8201955a77aeff" - integrity sha512-Au92NuznFklgQCUcV96iXlxUbHuB1vQMaH76DHl5M11TotjOHwqk9CwcrT78+Tnv4FN9uTBxq6p4EJoYkpyekg== - optionalDependencies: - fsevents "~2.3.2" - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -sourcemap-codec@^1.4.4: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -typescript@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86" - integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ== - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=