chore: prepare for v0.10.0-alpha.1

This commit is contained in:
PoiScript 2021-11-09 17:01:57 +08:00
parent 9d7852c4f9
commit af7c305c9e
No known key found for this signature in database
GPG key ID: 22C2B1249D99985E
111 changed files with 9132 additions and 9148 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[registries.crates-io]
protocol = "sparse"

View file

@ -1,13 +1,13 @@
[package]
name = "orgize"
version = "0.9.0"
version = "0.10.0-alpha.1"
authors = ["PoiScript <poiscript@gmail.com>"]
description = "A Rust library for parsing orgmode files."
description = "A Rust library for parsing org-mode files."
repository = "https://github.com/PoiScript/orgize"
readme = "README.md"
edition = "2018"
license = "MIT"
keywords = ["orgmode", "emacs", "parser"]
keywords = ["orgmode", "org-mode", "emacs", "parser"]
exclude = ["/wasm", "/.github"]
[package.metadata.docs.rs]
@ -16,36 +16,39 @@ all-features = true
[badges]
travis-ci = { repository = "PoiScript/orgize" }
[lib]
crate-type = ["cdylib", "rlib"]
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"
[features]
default = ["ser"]
wasm = ["serde-wasm-bindgen", "wasm-bindgen", "wee_alloc"]
ser = ["serde", "serde_indextree", "indexmap/serde-1"]
default = []
indexmap = ["dep:indexmap"]
chrono = ["dep:chrono"]
[workspace]
members = [".", "./wasm"]
[dependencies]
bytecount = "0.6"
chrono = { version = "0.4", optional = true }
indextree = "4.3"
indexmap = { version = "1.9", optional = true }
jetscii = "0.5"
lazy_static = "1.4"
memchr = "2.4"
nom = { version = "7.0", default-features = false, features = ["std"] }
serde = { version = "1.0", optional = true, features = ["derive"] }
serde_indextree = { version = "0.2", optional = true }
syntect = { version = "4.6", optional = true }
indexmap = { version = "1.7", features = ["serde-1"], optional = true }
# wasm stuff
serde-wasm-bindgen = { version = "0.3", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
wee_alloc = { version = "0.4", optional = true }
memchr = "2.5"
nom = { version = "7.1", default-features = false, features = ["std"] }
rowan = "0.15"
tracing = "0.1"
[dev-dependencies]
pretty_assertions = "1.0"
criterion = "0.4"
pretty_assertions = "1.3"
insta = "1.29"
serde_json = "1.0"
slugify = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] }
[profile.dev.package]
insta.opt-level = 3
similar.opt-level = 3
[profile.bench]
debug = true
[[bench]]
name = "parse"
harness = false

240
README.md
View file

@ -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::<Headline>().unwrap();
assert_eq!(hdl.keyword().unwrap().text(), "TASK");
```
# Render to html
Call the `Org::to_html` function to export org element tree to html:
```rust
use orgize::Org;
Org::parse("* DONE Title :tag:");
```
or `Org::parse_custom`:
``` rust
use orgize::{Org, ParseConfig};
Org::parse_custom(
"* TASK Title 1",
&ParseConfig {
// custom todo keywords
todo_keywords: (vec!["TASK".to_string()], vec![]),
..Default::default()
},
);
```
## Iter
`Org::iter` function will returns an iterator of `Event`s, which is
a simple wrapper of `Element`.
```rust
use orgize::Org;
for event in Org::parse("* DONE Title :tag:").iter() {
// handling the event
}
```
**Note**: whether an element is container or not, it will appears twice in one loop.
One as `Event::Start(element)`, one as `Event::End(element)`.
## Render html
You can call the `Org::write_html` function to generate html directly, which
uses the `DefaultHtmlHandler` internally:
```rust
use orgize::Org;
let mut writer = Vec::new();
Org::parse("* title\n*section*").write_html(&mut writer).unwrap();
assert_eq!(
String::from_utf8(writer).unwrap(),
Org::parse("* title\n*section*").to_html(),
"<main><h1>title</h1><section><p><b>section</b></p></section></main>"
);
```
## Render html with custom `HtmlHandler`
Checkout `examples/html-slugify.rs` on how to customizing html export process.
To customize html rendering, simply implementing `HtmlHandler` trait and passing
it to the `Org::wirte_html_custom` function.
# Features
The following code demonstrates how to add a id for every headline and return
own error type while rendering.
- `chrono`: adds the ability to convert `Timestamp` into `chrono::NaiveDateTime`, disabled by default.
```rust
use std::convert::From;
use std::io::{Error as IOError, Write};
use std::string::FromUtf8Error;
use orgize::export::{DefaultHtmlHandler, HtmlHandler};
use orgize::{Element, Org};
use slugify::slugify;
#[derive(Debug)]
enum MyError {
IO(IOError),
Heading,
Utf8(FromUtf8Error),
}
// From<std::io::Error> trait is required for custom error type
impl From<IOError> for MyError {
fn from(err: IOError) -> Self {
MyError::IO(err)
}
}
impl From<FromUtf8Error> for MyError {
fn from(err: FromUtf8Error) -> Self {
MyError::Utf8(err)
}
}
#[derive(Default)]
struct MyHtmlHandler(DefaultHtmlHandler);
impl HtmlHandler<MyError> for MyHtmlHandler {
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
if let Element::Title(title) = element {
if title.level > 6 {
return Err(MyError::Heading);
} else {
write!(
w,
"<h{0}><a id=\"{1}\" href=\"#{1}\">",
title.level,
slugify!(&title.raw),
)?;
}
} else {
// fallthrough to default handler
self.0.start(w, element)?;
}
Ok(())
}
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
if let Element::Title(title) = element {
write!(w, "</a></h{}>", title.level)?;
} else {
self.0.end(w, element)?;
}
Ok(())
}
}
fn main() -> Result<(), MyError> {
let mut writer = Vec::new();
let mut handler = MyHtmlHandler::default();
Org::parse("* title\n*section*").wirte_html_custom(&mut writer, &mut handler)?;
assert_eq!(
String::from_utf8(writer)?,
"<main><h1><a id=\"title\" href=\"#title\">title</a></h1>\
<section><p><b>section</b></p></section></main>"
);
Ok(())
}
```
**Note**: as I mentioned above, each element will appears two times while iterating.
And handler will silently ignores all end events from non-container elements.
So if you want to change how a non-container element renders, just redefine the `start`
function and leave the `end` function unchanged.
## Serde
`Org` struct have already implemented serde's `Serialize` trait. It means you can
serialize it into any format supported by serde, such as json:
```rust
use orgize::Org;
use serde_json::{json, to_string};
let org = Org::parse("I 'm *bold*.");
println!("{}", to_string(&org).unwrap());
// {
// "type": "document",
// "children": [{
// "type": "section",
// "children": [{
// "type": "paragraph",
// "children":[{
// "type": "text",
// "value":"I 'm "
// }, {
// "type": "bold",
// "children":[{
// "type": "text",
// "value": "bold"
// }]
// }, {
// "type":"text",
// "value":"."
// }]
// }]
// }]
// }
```
## Features
By now, orgize provides four features:
+ `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default.
+ `chrono`: adds the ability to convert `Datetime` into `chrono` structs, disabled by default.
+ `syntect`: provides `SyntectHtmlHandler` for highlighting code block, disabled by default.
+ `indexmap`: Uses `IndexMap` instead of `HashMap` for properties to preserve their order, disabled by default.
## License
MIT
- `indexmap`: adds the ability to convert `PropertyDrawer` properties into `IndexMap`, disabled by default.

View file

@ -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);

View file

@ -1,81 +0,0 @@
use std::convert::From;
use std::env::args;
use std::fs;
use std::io::{Error as IOError, Write};
use std::result::Result;
use std::string::FromUtf8Error;
use orgize::export::{DefaultHtmlHandler, HtmlHandler};
use orgize::{Element, Org};
use slugify::slugify;
#[derive(Debug)]
enum MyError {
IO(IOError),
Heading,
Utf8(FromUtf8Error),
}
// From<std::io::Error> trait is required for custom error type
impl From<IOError> for MyError {
fn from(err: IOError) -> Self {
MyError::IO(err)
}
}
impl From<FromUtf8Error> for MyError {
fn from(err: FromUtf8Error) -> Self {
MyError::Utf8(err)
}
}
#[derive(Default)]
struct MyHtmlHandler(DefaultHtmlHandler);
impl HtmlHandler<MyError> for MyHtmlHandler {
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
if let Element::Title(title) = element {
if title.level > 6 {
return Err(MyError::Heading);
} else {
write!(
w,
"<h{0}><a id=\"{1}\" href=\"#{1}\">",
title.level,
slugify!(&title.raw),
)?;
}
} else {
// fallthrough to default handler
self.0.start(w, element)?;
}
Ok(())
}
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
if let Element::Title(title) = element {
write!(w, "</a></h{}>", title.level)?;
} else {
self.0.end(w, element)?;
}
Ok(())
}
}
fn main() -> Result<(), MyError> {
let args: Vec<_> = args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <org-file>", args[0]);
} else {
let contents = String::from_utf8(fs::read(&args[1])?)?;
let mut writer = Vec::new();
let mut handler = MyHtmlHandler::default();
Org::parse(&contents).write_html_custom(&mut writer, &mut handler)?;
println!("{}", String::from_utf8(writer)?);
}
Ok(())
}

64
examples/html-slugify.rs Normal file
View file

@ -0,0 +1,64 @@
//! ```bash
//! cargo run --example html-slugify '* hello world!'
//! ```
use orgize::{
ast::HeadlineTitle,
export::{HtmlExport, TraversalContext, Traverser},
forward_handler,
rowan::{ast::AstNode, WalkEvent},
Org,
};
use slugify::slugify;
use std::cmp::min;
use std::env::args;
#[derive(Default)]
struct MyHtmlHandler(pub HtmlExport);
// AsMut trait is required for using forward_handler macros
impl AsMut<HtmlExport> for MyHtmlHandler {
fn as_mut(&mut self) -> &mut HtmlExport {
&mut self.0
}
}
impl Traverser for MyHtmlHandler {
fn headline_title(&mut self, event: WalkEvent<&HeadlineTitle>, _ctx: &mut TraversalContext) {
match event {
WalkEvent::Enter(title) => {
let level = title.headline().and_then(|h| h.level()).unwrap_or(1);
let level = min(level, 6);
let raw = title.syntax().to_string();
self.0.output += &format!("<h{level}><a id=\"{0}\" href=\"#{0}\">", slugify!(&raw));
}
WalkEvent::Leave(title) => {
let level = title.headline().and_then(|h| h.level()).unwrap_or(1);
let level = min(level, 6);
self.0.output += &format!("</a></h{level}>");
}
}
}
forward_handler! {
HtmlExport,
link text document headline paragraph section rule comment
inline_src inline_call code bold verbatim italic strike underline list list_item list_item_tag
special_block quote_block center_block verse_block comment_block example_block export_block
source_block babel_call clock cookie radio_target drawer dyn_block fn_def fn_ref macros
snippet timestamp target fixed_width org_table org_table_row org_table_cell list_item_content
}
}
fn main() {
let args: Vec<_> = args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <org-mode-string>", args[0]);
} else {
let mut handler = MyHtmlHandler::default();
Org::parse(&args[1]).traverse(&mut handler);
println!("{}", handler.0.finish());
}
}

View file

@ -1,19 +0,0 @@
use orgize::Org;
use std::env::args;
use std::fs;
use std::io::Result;
fn main() -> Result<()> {
let args: Vec<_> = args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <org-file>", args[0]);
} else {
let contents = String::from_utf8(fs::read(&args[1])?).unwrap();
for event in Org::parse(&contents).iter() {
println!("{:?}", event);
}
}
Ok(())
}

View file

@ -1,17 +0,0 @@
use orgize::Org;
use serde_json::to_string;
use std::env::args;
use std::fs;
use std::io::Result;
fn main() -> Result<()> {
let args: Vec<_> = args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <org-file>", args[0]);
} else {
let contents = String::from_utf8(fs::read(&args[1])?).unwrap();
println!("{}", to_string(&Org::parse(&contents)).unwrap());
}
Ok(())
}

1
fuzz/.gitignore vendored
View file

@ -1,3 +1,4 @@
target
corpus
artifacts
coverage

View file

@ -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

View file

@ -1,14 +1,11 @@
#![no_main]
#[macro_use]
extern crate libfuzzer_sys;
extern crate orgize;
use libfuzzer_sys::fuzz_target;
use orgize::syntax::{HtmlHandler, Org};
use std::str;
use orgize::Org;
#[cfg_attr(rustfmt, rustfmt_skip)]
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
let _ = Org::parse(s);
fuzz_target!(|data: &[u8]| {
if let Ok(utf8) = str::from_utf8(data) {
let _ = Org::parse(utf8);
}
});

67
src/ast/drawer.rs Normal file
View file

@ -0,0 +1,67 @@
use std::collections::HashMap;
use super::{filter_token, SyntaxKind::*};
use crate::{ast::PropertyDrawer, syntax::SyntaxToken};
impl PropertyDrawer {
/// ```rust
/// use orgize::{Org, ast::PropertyDrawer};
///
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:ID: id\n:END:");
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
/// assert_eq!(drawer.iter().count(), 2);
/// ```
pub fn iter(&self) -> impl Iterator<Item = (SyntaxToken, SyntaxToken)> {
self.node_properties().filter_map(|property| {
let mut texts = property
.syntax
.children_with_tokens()
.filter_map(filter_token(TEXT));
Some((texts.next()?, texts.next()?))
})
}
/// ```rust
/// use orgize::{Org, ast::PropertyDrawer};
///
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:ID: id\n:END:");
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
/// assert_eq!(drawer.get("CUSTOM_ID").unwrap().text(), "someid");
/// assert_eq!(drawer.get("ID").unwrap().text(), "id");
/// ```
pub fn get(&self, key: &str) -> Option<SyntaxToken> {
self.iter()
.find_map(|(k, v)| (k.text() == key).then_some(v))
}
/// ```rust
/// use orgize::{Org, ast::PropertyDrawer};
///
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:CUSTOM_ID: id\n:END:");
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
/// let map = drawer.to_hash_map();
/// assert_eq!(map.len(), 1);
/// assert_eq!(map.get("CUSTOM_ID").unwrap(), "id");
/// ```
pub fn to_hash_map(&self) -> HashMap<String, String> {
self.iter()
.map(|(k, v)| (k.text().into(), v.text().into()))
.collect()
}
#[cfg(feature = "indexmap")]
/// ```rust
/// use orgize::{Org, ast::PropertyDrawer};
///
/// let org = Org::parse("* Heading\n:PROPERTIES:\n:CUSTOM_ID: someid\n:ID: id\n:END:");
/// let drawer = org.first_node::<PropertyDrawer>().unwrap();
/// let map = drawer.to_index_map();
/// assert_eq!(map.get_index(1).unwrap(), (&"ID".to_string(), &"id".to_string()));
/// ```
pub fn to_index_map(&self) -> indexmap::IndexMap<String, String> {
self.iter()
.map(|(k, v)| (k.text().into(), v.text().into()))
.collect()
}
}

362
src/ast/generate.js Normal file
View file

@ -0,0 +1,362 @@
const nodes = [
{
struct: "Document",
kind: ["DOCUMENT"],
pre_blank: true,
first_child: [
["section", "Section"],
["first_headline", "Headline"],
],
last_child: [["last_headline", "Headline"]],
children: [["headlines", "Headline"]],
},
{
struct: "Section",
kind: ["SECTION"],
post_blank: true,
},
{
struct: "Paragraph",
kind: ["PARAGRAPH"],
post_blank: true,
},
{
struct: "Headline",
kind: ["HEADLINE"],
first_child: [
["title", "HeadlineTitle"],
["section", "Section"],
["tags", "HeadlineTags"],
["planning", "Planning"],
["priority", "HeadlinePriority"],
],
children: [["headlines", "Headline"]],
token: [
["stars", "HEADLINE_STARS"],
["keyword", "HEADLINE_KEYWORD"],
],
post_blank: true,
},
{
struct: "HeadlineStars",
kind: ["HEADLINE_STARS"],
parent: [["headline", "Headline"]],
},
{
struct: "HeadlineTitle",
kind: ["HEADLINE_TITLE"],
parent: [["headline", "Headline"]],
},
{
struct: "HeadlineKeyword",
kind: ["HEADLINE_KEYWORD"],
parent: [["headline", "Headline"]],
},
{
struct: "HeadlinePriority",
kind: ["HEADLINE_PRIORITY"],
parent: [["headline", "Headline"]],
token: [["text", "TEXT"]],
},
{
struct: "HeadlineTags",
kind: ["HEADLINE_TAGS"],
parent: [["headline", "Headline"]],
},
{
struct: "PropertyDrawer",
kind: ["PROPERTY_DRAWER"],
children: [["node_properties", "NodeProperty"]],
},
{
struct: "NodeProperty",
kind: ["NODE_PROPERTY"],
},
{
struct: "Planning",
kind: ["PLANNING"],
last_child: [
["deadline", "PlanningDeadline"],
["scheduled", "PlanningScheduled"],
["closed", "PlanningClosed"],
],
},
{
struct: "PlanningDeadline",
kind: ["PLANNING_DEADLINE"],
},
{
struct: "PlanningScheduled",
kind: ["PLANNING_SCHEDULED"],
},
{
struct: "PlanningClosed",
kind: ["PLANNING_CLOSED"],
},
{
struct: "OrgTable",
kind: ["ORG_TABLE"],
post_blank: true,
},
{
struct: "OrgTableRow",
kind: ["ORG_TABLE_RULE_ROW", "ORG_TABLE_STANDARD_ROW"],
},
{
struct: "OrgTableCell",
kind: ["ORG_TABLE_CELL"],
},
{
struct: "List",
kind: ["LIST"],
children: [["items", "ListItem"]],
},
{
struct: "ListItem",
kind: ["LIST_ITEM"],
first_child: [["content", "ListItemContent"]],
token: [
["indent", "LIST_ITEM_INDENT"],
["bullet", "LIST_ITEM_BULLET"],
],
},
{
struct: "ListItemIndent",
kind: ["LIST_ITEM_INDENT"],
},
{
struct: "ListItemTag",
kind: ["LIST_ITEM_TAG"],
},
{
struct: "ListItemBullet",
kind: ["LIST_ITEM_BULLET"],
},
{
struct: "ListItemContent",
kind: ["LIST_ITEM_CONTENT"],
},
{
struct: "Drawer",
kind: ["DRAWER"],
},
{
struct: "DynBlock",
kind: ["DYN_BLOCK"],
},
{
struct: "Keyword",
kind: ["KEYWORD"],
},
{
struct: "BabelCall",
kind: ["BABEL_CALL"],
},
{
struct: "TableEl",
kind: ["TABLE_EL"],
post_blank: true,
},
{
struct: "Clock",
kind: ["CLOCK"],
post_blank: true,
},
{
struct: "FnDef",
kind: ["FN_DEF"],
post_blank: true,
},
{
struct: "Comment",
kind: ["COMMENT"],
post_blank: true,
token: [["text", "TEXT"]],
},
{
struct: "Rule",
kind: ["RULE"],
post_blank: true,
},
{
struct: "FixedWidth",
kind: ["FIXED_WIDTH"],
post_blank: true,
token: [["text", "TEXT"]],
},
{
struct: "SpecialBlock",
kind: ["SPECIAL_BLOCK"],
},
{
struct: "QuoteBlock",
kind: ["QUOTE_BLOCK"],
},
{
struct: "CenterBlock",
kind: ["CENTER_BLOCK"],
},
{
struct: "VerseBlock",
kind: ["VERSE_BLOCK"],
},
{
struct: "CommentBlock",
kind: ["COMMENT_BLOCK"],
},
{
struct: "ExampleBlock",
kind: ["EXAMPLE_BLOCK"],
},
{
struct: "ExportBlock",
kind: ["EXPORT_BLOCK"],
},
{
struct: "SourceBlock",
kind: ["SOURCE_BLOCK"],
},
{
struct: "InlineCall",
kind: ["INLINE_CALL"],
},
{
struct: "InlineSrc",
kind: ["INLINE_SRC"],
},
{
struct: "Link",
kind: ["LINK"],
token: [["path", "LINK_PATH"]],
},
{
struct: "Cookie",
kind: ["COOKIE"],
},
{
struct: "RadioTarget",
kind: ["RADIO_TARGET"],
},
{
struct: "FnRef",
kind: ["FN_REF"],
},
{
struct: "LatexEnvironment",
kind: ["LATEX_ENVIRONMENT"],
},
{
struct: "Macros",
kind: ["MACROS"],
},
{
struct: "MacrosArgument",
kind: ["MACROS_ARGUMENT"],
},
{
struct: "Snippet",
kind: ["SNIPPET"],
token: [["name", "TEXT"]],
},
{
struct: "Target",
kind: ["TARGET"],
},
{
struct: "Bold",
kind: ["BOLD"],
},
{
struct: "Strike",
kind: ["STRIKE"],
},
{
struct: "Italic",
kind: ["ITALIC"],
},
{
struct: "Underline",
kind: ["UNDERLINE"],
},
{
struct: "Verbatim",
kind: ["VERBATIM"],
},
{
struct: "Code",
kind: ["CODE"],
token: [["text", "TEXT"]],
},
{
struct: "Timestamp",
kind: ["TIMESTAMP_ACTIVE", "TIMESTAMP_INACTIVE", "TIMESTAMP_DIARY"],
token: [
["year_start", "TIMESTAMP_YEAR"],
["month_start", "TIMESTAMP_MONTH"],
["day_start", "TIMESTAMP_DAY"],
["hour_start", "TIMESTAMP_HOUR"],
["minute_start", "TIMESTAMP_MINUTE"],
],
last_token: [
["year_end", "TIMESTAMP_YEAR"],
["month_end", "TIMESTAMP_MONTH"],
["day_end", "TIMESTAMP_DAY"],
["hour_end", "TIMESTAMP_HOUR"],
["minute_end", "TIMESTAMP_MINUTE"],
],
},
];
let content = `//! generated file, do not modify it directly
#![allow(clippy::all)]
#![allow(unused)]
use rowan::ast::{support, AstChildren, AstNode};
use crate::syntax::{OrgLanguage, SyntaxKind, SyntaxKind::*, SyntaxNode, SyntaxToken};
`;
for (const node of nodes) {
content += `
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ${node.struct} {
pub(crate) syntax: SyntaxNode,
}
impl AstNode for ${node.struct} {
type Language = OrgLanguage;
fn can_cast(kind: SyntaxKind) -> bool { ${node.kind
.map((k) => `kind == ${k}`)
.join(" || ")} }
fn cast(node: SyntaxNode) -> Option<${
node.struct
}> { Self::can_cast(node.kind()).then(|| ${node.struct} { syntax: node }) }
fn syntax(&self) -> &SyntaxNode { &self.syntax }
}
impl ${node.struct} {\n`;
for (const [method, kind] of node.token || []) {
content += ` pub fn ${method}(&self) -> Option<SyntaxToken> { support::token(&self.syntax, ${kind}) }\n`;
}
for (const [method, kind] of node.last_token || []) {
content += ` pub fn ${method}(&self) -> Option<SyntaxToken> { super::last_token(&self.syntax, ${kind}) }\n`;
}
for (const [method, kind] of node.parent || []) {
content += ` pub fn ${method}(&self) -> Option<${kind}> { self.syntax.parent().and_then(${kind}::cast) }\n`;
}
for (const [method, kind] of node.first_child || []) {
content += ` pub fn ${method}(&self) -> Option<${kind}> { support::child(&self.syntax) }\n`;
}
for (const [method, kind] of node.last_child || []) {
content += ` pub fn ${method}(&self) -> Option<${kind}> { super::last_child(&self.syntax) }\n`;
}
for (const [method, kind] of node.children || []) {
content += ` pub fn ${method}(&self) -> AstChildren<${kind}> { support::children(&self.syntax) }\n`;
}
if (node.post_blank) {
content += ` pub fn post_blank(&self) -> usize { super::blank_lines(&self.syntax) }\n`;
}
if (node.pre_blank) {
content += ` pub fn pre_blank(&self) -> usize { super::blank_lines(&self.syntax) }\n`;
}
content += `}\n`;
}
require("fs").writeFileSync(__dirname + "/generated.rs", content);

1266
src/ast/generated.rs Normal file

File diff suppressed because it is too large Load diff

358
src/ast/headline.rs Normal file
View file

@ -0,0 +1,358 @@
use rowan::ast::support;
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxToken};
use super::{filter_token, Headline, HeadlinePriority, HeadlineTags, Timestamp};
impl Headline {
/// Return level of this headline
///
/// ```rust
/// use orgize::{Org, ast::Headline};
///
/// let hdl = Org::parse("* ").first_node::<Headline>().unwrap();
/// assert_eq!(hdl.level(), Some(1));
/// let hdl = Org::parse("****** hello").first_node::<Headline>().unwrap();
/// assert_eq!(hdl.level(), Some(6));
/// ```
pub fn level(&self) -> Option<usize> {
self.stars().map(|stars| stars.text().len())
}
/// Return `true` if this headline contains a COMMENT keyword
///
/// ```rust
/// use orgize::{Org, ast::Headline};
///
/// let hdl = Org::parse("* COMMENT").first_node::<Headline>().unwrap();
/// assert!(hdl.is_commented());
/// let hdl = Org::parse("* COMMENT hello").first_node::<Headline>().unwrap();
/// assert!(hdl.is_commented());
/// let hdl = Org::parse("* hello").first_node::<Headline>().unwrap();
/// assert!(!hdl.is_commented());
/// ```
pub fn is_commented(&self) -> bool {
self.title()
.and_then(|title| title.syntax.first_token())
.map(|title| {
let text = title.text();
title.kind() == SyntaxKind::TEXT
&& text.starts_with("COMMENT")
&& (text.len() == 7 || text[7..].starts_with(char::is_whitespace))
})
.unwrap_or_default()
}
/// Return `true` if this headline contains an archive tag
///
/// ```rust
/// use orgize::{Org, ast::Headline};
///
/// let hdl = Org::parse("* hello :ARCHIVE:").first_node::<Headline>().unwrap();
/// assert!(hdl.is_archived());
/// let hdl = Org::parse("* hello :ARCHIVED:").first_node::<Headline>().unwrap();
/// assert!(!hdl.is_archived());
/// ```
pub fn is_archived(&self) -> bool {
self.tags()
.map(|tags| {
tags.syntax
.children_with_tokens()
.any(|elem| matches!(elem, SyntaxElement::Token(t) if t.text() == "ARCHIVE"))
})
.unwrap_or_default()
}
/// Returns this headline's closed timestamp, or `None` if not set.
pub fn closed(&self) -> Option<Timestamp> {
self.planning()
.and_then(|planning| planning.closed())
.and_then(|node| support::child::<Timestamp>(&node.syntax))
}
/// Returns this headline's scheduled timestamp, or `None` if not set.
pub fn scheduled(&self) -> Option<Timestamp> {
self.planning()
.and_then(|planning| planning.scheduled())
.and_then(|node| support::child::<Timestamp>(&node.syntax))
}
/// Returns this headline's deadline timestamp, or `None` if not set.
pub fn deadline(&self) -> Option<Timestamp> {
self.planning()
.and_then(|planning| planning.deadline())
.and_then(|node| support::child::<Timestamp>(&node.syntax))
}
}
// pub enum DocumentOrHeadline {
// Document(Document),
// Headline(Headline),
// }
// impl From<Document> for DocumentOrHeadline {
// fn from(value: Document) -> Self {
// DocumentOrHeadline::Document(value)
// }
// }
// impl From<Headline> for DocumentOrHeadline {
// fn from(value: Headline) -> Self {
// DocumentOrHeadline::Headline(value)
// }
// }
// impl DocumentOrHeadline {
// pub fn section(&self) -> Option<Section> {
// match self {
// DocumentOrHeadline::Document(v) => v.section(),
// DocumentOrHeadline::Headline(v) => v.section(),
// }
// }
// }
// impl Org {
// /// set the title of this headline
// ///
// /// ```rust
// /// use orgize::Org;
// ///
// /// let mut org = Org::parse("* [#A]");
// /// let hdl = org.document().first_headline().unwrap();
// /// org.set_title(hdl, "world");
// /// assert_eq!(org.to_org(), "* [#A] world");
// /// let hdl = org.document().first_headline().unwrap();
// /// org.set_title(hdl, "world!");
// /// assert_eq!(org.to_org(), "* [#A] world!");
// /// ```
// pub fn set_title(&mut self, headline: Headline, title: &str) -> Option<HeadlineTitle> {
// let bytes = title.as_bytes();
// let title = match memchr(b'\n', bytes) {
// Some(i) if i > 0 && bytes[i] == b'\r' => &title[0..i - 1],
// Some(i) => &title[0..i],
// _ => title,
// };
// let new_title = node(HEADLINE_TITLE, object_nodes(self.create_input(title)));
// if let Some(title) = headline.title() {
// self.green = title.syntax.replace_with(new_title.into_node().unwrap());
// return Some(title);
// }
// let mut child: Vec<_> = headline
// .syntax
// .green()
// .children()
// .map(|ch| ch.to_owned())
// .collect();
// let index = support::child
// .iter()
// .enumerate()
// .filter_map(|(idx, it)| {
// if it.kind() == HEADLINE_STARS.into()
// || it.kind() == HEADLINE_KEYWORD.into()
// || it.kind() == HEADLINE_PRIORITY.into()
// {
// Some(idx + 1)
// } else {
// None
// }
// })
// .last()
// .unwrap_or_default();
// if index == child.len() {
// child.push(token(WHITESPACE, " "));
// child.push(new_title);
// } else if child[index].kind() != WHITESPACE.into() {
// child.insert(index, token(WHITESPACE, " "));
// child.insert(index + 1, new_title);
// } else {
// child.insert(index, new_title);
// }
// self.green = headline
// .syntax
// .replace_with(node(HEADLINE, child).into_node().unwrap());
// None
// }
// /// set the section of this document or headline
// ///
// /// ```rust
// /// use orgize::Org;
// ///
// /// let mut org = Org::parse("* hello");
// ///
// /// let hdl = org.document().first_headline().unwrap();
// /// org.set_section(hdl, "world");
// /// assert_eq!(org.to_org(), "* hello\nworld\n");
// ///
// /// let hdl = org.document().first_headline().unwrap();
// /// org.set_section(hdl, "world!");
// /// assert_eq!(org.to_org(), "* hello\nworld!\n");
// ///
// /// let doc = org.document();
// /// org.set_section(doc, "doc");
// /// assert_eq!(org.to_org(), "doc\n* hello\nworld!\n");
// /// ```
// pub fn set_section(
// &mut self,
// document_or_headline: impl Into<DocumentOrHeadline>,
// section: &str,
// ) -> Option<Section> {
// let document_or_headline = document_or_headline.into();
// let section = section_text(self.create_input(section)).ok()?.1.as_str();
// let section = if section.ends_with('\n') {
// section_node(self.create_input(section)).map(|(_, s)| s)
// } else {
// section_node(self.create_input(&format!("{section}\n"))).map(|(_, s)| s)
// }
// .ok()?;
// if let Some(old) = document_or_headline.section() {
// self.green = old.syntax.replace_with(section.into_node().unwrap());
// return Some(old);
// }
// match document_or_headline {
// DocumentOrHeadline::Document(document) => {
// let mut child: Vec<_> = document
// .syntax
// .green()
// .children()
// .map(|ch| ch.to_owned())
// .collect();
// let headline_idx = child.iter().position(|it| it.kind() == HEADLINE.into());
// if let Some(idx) = headline_idx {
// child.insert(idx, section);
// } else {
// child.push(section);
// }
// self.green = document
// .syntax
// .replace_with(GreenNode::new(DOCUMENT.into(), child));
// None
// }
// DocumentOrHeadline::Headline(headline) => {
// let mut child: Vec<_> = headline
// .syntax
// .green()
// .children()
// .map(|ch| ch.to_owned())
// .collect();
// let new_line_idx = support::child
// .iter()
// .position(|it| it.kind() == NEW_LINE.into());
// if let Some(idx) = new_line_idx {
// // add section *after* newline
// if idx < support::child.len() {
// support::child.insert(idx, section);
// } else {
// support::child.push(section);
// }
// } else {
// support::child.push(token(NEW_LINE, "\n"));
// support::child.push(section);
// }
// self.green = headline
// .syntax
// .replace_with(GreenNode::new(HEADLINE.into(), support::child));
// None
// }
// }
// }
// /// set the level of this headline
// ///
// /// ```rust
// /// use orgize::Org;
// ///
// /// let mut org = Org::parse("** 1\n** 2");
// ///
// /// let hdl = org.document().last_headline().unwrap();
// /// org.set_level(hdl, 1);
// /// assert_eq!(org.to_org(), "** 1\n* 2");
// ///
// /// let hdl = org.document().last_headline().unwrap();
// /// org.set_level(hdl, 3);
// /// assert_eq!(org.to_org(), "** 1\n* 2");
// /// ```
// pub fn set_level(&mut self, headline: Headline, level: usize) {
// if level == 0 {
// return;
// }
// let min_level_in_siblings = headline
// .syntax
// .siblings(rowan::Direction::Next)
// .chain(headline.syntax.siblings(rowan::Direction::Prev))
// .filter_map(Headline::cast)
// .filter_map(|headline| headline.level())
// .min()
// .unwrap_or(1);
// if level <= min_level_in_siblings {
// if let Some(stars) = headline.stars() {
// self.green = stars.replace_with(GreenToken::new(
// SyntaxKind::HEADLINE_STARS.into(),
// "*".repeat(level).as_str(),
// ));
// }
// }
// }
// }
impl HeadlineTags {
/// Returns an iterator of text token in this tags
///
/// ```rust
/// use orgize::{Org, ast::HeadlineTags};
///
/// let tags_vec = |input: &str| {
/// let tags = Org::parse(input).first_node::<HeadlineTags>().unwrap();
/// let tags: Vec<_> = tags.iter().map(|t| t.to_string()).collect();
/// tags
/// };
///
/// assert_eq!(tags_vec("* :tag:"), vec!["tag".to_string()]);
/// assert_eq!(tags_vec("* [#A] :::::a2%:"), vec!["a2%".to_string()]);
/// assert_eq!(tags_vec("* TODO :tag: :a2%:"), vec!["tag".to_string(), "a2%".to_string()]);
/// assert_eq!(tags_vec("* title :tag:a2%:"), vec!["tag".to_string(), "a2%".to_string()]);
/// ```
pub fn iter(&self) -> impl Iterator<Item = SyntaxToken> {
self.syntax
.children_with_tokens()
.filter_map(filter_token(SyntaxKind::TEXT))
}
}
impl HeadlinePriority {
/// Returns priority text
///
/// ```rust
/// use orgize::{Org, ast::HeadlinePriority};
///
/// let priority = Org::parse("* [#A]").first_node::<HeadlinePriority>().unwrap();
/// assert_eq!(priority.text_string().unwrap(), "A".to_string());
/// let priority = Org::parse("* [#破]").first_node::<HeadlinePriority>().unwrap();
/// assert_eq!(priority.text_string().unwrap(), "破".to_string());
/// ```
pub fn text_string(&self) -> Option<String> {
self.text().map(|tk| tk.to_string())
}
}

22
src/ast/inline_call.rs Normal file
View file

@ -0,0 +1,22 @@
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxToken};
use super::InlineCall;
impl InlineCall {
///
/// ```rust
/// use orgize::{Org, ast::InlineCall};
///
/// let call = Org::parse("call_square(4)").first_node::<InlineCall>().unwrap();
/// assert_eq!(call.call().unwrap().text(), "square");
/// ```
pub fn call(&self) -> Option<SyntaxToken> {
self.syntax
.children_with_tokens()
.filter_map(|it| match it {
SyntaxElement::Token(t) if t.kind() == SyntaxKind::TEXT => Some(t),
_ => None,
})
.nth(1)
}
}

41
src/ast/link.rs Normal file
View file

@ -0,0 +1,41 @@
use rowan::ast::{support, AstNode};
use super::Link;
use crate::syntax::SyntaxKind;
impl Link {
/// Returns `true` if link contains description
///
/// ```rust
/// use orgize::{Org, ast::Link};
///
/// let link = Org::parse("[[https://google.com]]").first_node::<Link>().unwrap();
/// assert!(!link.has_description());
/// let link = Org::parse("[[https://google.com][Google]]").first_node::<Link>().unwrap();
/// assert!(link.has_description());
/// ```
pub fn has_description(&self) -> bool {
support::token(self.syntax(), SyntaxKind::TEXT).is_some()
}
/// Returns `true` if link is an image link
///
/// ```rust
/// use orgize::{Org, ast::Link};
///
/// let link = Org::parse("[[file:/home/dominik/images/jupiter.jpg]]").first_node::<Link>().unwrap();
/// assert!(link.is_image());
/// ```
pub fn is_image(&self) -> bool {
const IMAGE_SUFFIX: &[&str] = &[
// https://github.com/bzg/org-mode/blob/7de1e818d5fbe6a05c6b1a007eed07dc27e7246b/lisp/ox.el#L253
".png", ".jpeg", ".jpg", ".gif", ".tiff", ".tif", ".xbm", ".xpm", ".pbm", ".pgm",
".ppm", ".webp", ".avif", ".svg",
];
self.path()
.map(|path| IMAGE_SUFFIX.iter().any(|e| path.text().ends_with(e)))
.unwrap_or_default()
&& !self.has_description()
}
}

47
src/ast/list.rs Normal file
View file

@ -0,0 +1,47 @@
use super::List;
use crate::syntax::SyntaxKind;
impl List {
/// Returns `true` if this list is an ordered link
///
/// ```rust
/// use orgize::{Org, ast::List};
///
/// let list = Org::parse("+ 1").first_node::<List>().unwrap();
/// assert!(!list.is_ordered());
///
/// let list = Org::parse("1. 1").first_node::<List>().unwrap();
/// assert!(list.is_ordered());
///
/// let list = Org::parse("1) 1\n- 2\n3. 3").first_node::<List>().unwrap();
/// assert!(list.is_ordered());
/// ```
pub fn is_ordered(&self) -> bool {
self.items()
.next()
.and_then(|item| item.bullet())
.map(|bullet| bullet.text().starts_with(|c: char| c.is_ascii_digit()))
.unwrap_or_default()
}
/// Returns `true` if this list contains a TAG
///
/// ```rust
/// use orgize::{Org, ast::List};
///
/// let list = Org::parse("- some tag :: item 2.1").first_node::<List>().unwrap();
/// assert!(list.is_descriptive());
/// let list = Org::parse("2. [X] item 2").first_node::<List>().unwrap();
/// assert!(!list.is_descriptive());
/// ```
pub fn is_descriptive(&self) -> bool {
self.items()
.next()
.map(|item| {
item.syntax
.children()
.any(|it| it.kind() == SyntaxKind::LIST_ITEM_TAG)
})
.unwrap_or_default()
}
}

49
src/ast/mod.rs Normal file
View file

@ -0,0 +1,49 @@
#[rustfmt::skip]
mod generated;
mod drawer;
mod headline;
mod inline_call;
mod link;
mod list;
mod snippet;
mod table;
mod timestamp;
pub use generated::*;
pub use rowan::ast::support::*;
use crate::syntax::{SyntaxKind, SyntaxNode};
use rowan::{ast::AstNode, Language, NodeOrToken};
pub fn blank_lines(parent: &SyntaxNode) -> usize {
parent
.children()
.filter(|n| n.kind() == SyntaxKind::BLANK_LINE)
.count()
}
pub fn last_child<N: AstNode>(parent: &rowan::SyntaxNode<N::Language>) -> Option<N> {
parent.children().filter_map(N::cast).last()
}
pub fn last_token<L: Language>(
parent: &rowan::SyntaxNode<L>,
kind: L::Kind,
) -> Option<rowan::SyntaxToken<L>> {
parent
.children_with_tokens()
.filter_map(filter_token(kind))
.last()
}
pub fn filter_token<L: Language>(
kind: L::Kind,
) -> impl Fn(NodeOrToken<rowan::SyntaxNode<L>, rowan::SyntaxToken<L>>) -> Option<rowan::SyntaxToken<L>>
{
move |elem| match elem {
NodeOrToken::Token(tk) if tk.kind() == kind => Some(tk),
_ => None,
}
}

18
src/ast/snippet.rs Normal file
View file

@ -0,0 +1,18 @@
use crate::syntax::{SyntaxKind, SyntaxToken};
use super::{filter_token, Snippet};
impl Snippet {
/// ```rust
/// use orgize::{Org, ast::Snippet};
///
/// let snippet = Org::parse("@@BACKEND:VALUE@@").first_node::<Snippet>().unwrap();
/// assert_eq!(snippet.value().unwrap().text(), "VALUE");
/// ```
pub fn value(&self) -> Option<SyntaxToken> {
self.syntax
.children_with_tokens()
.filter_map(filter_token(SyntaxKind::TEXT))
.nth(1)
}
}

30
src/ast/table.rs Normal file
View file

@ -0,0 +1,30 @@
use super::OrgTableRow;
use crate::syntax::SyntaxKind;
impl OrgTableRow {
/// Returns `true` if this row is a rule
///
/// ```rust
/// use orgize::{Org, ast::OrgTableRow};
///
/// let org = Org::parse("|----|----|\n|Foo |Bar |");
/// let row = org.first_node::<OrgTableRow>().unwrap();
/// assert!(row.is_rule());
/// ```
pub fn is_rule(&self) -> bool {
self.syntax.kind() == SyntaxKind::ORG_TABLE_RULE_ROW
}
/// Returns `true` if this row is a standard row
///
/// ```rust
/// use orgize::{Org, ast::OrgTableRow};
///
/// let org = Org::parse("|Foo |Bar |\n|----|----|");
/// let row = org.first_node::<OrgTableRow>().unwrap();
/// assert!(row.is_standard());
/// ```
pub fn is_standard(&self) -> bool {
self.syntax.kind() == SyntaxKind::ORG_TABLE_STANDARD_ROW
}
}

112
src/ast/timestamp.rs Normal file
View file

@ -0,0 +1,112 @@
use super::{filter_token, Timestamp};
use crate::syntax::SyntaxKind;
impl Timestamp {
/// ```rust
/// use orgize::{Org, ast::Timestamp};
///
/// let ts = Org::parse("<2003-09-16 Tue 09:39-10:39>").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_active());
/// let ts = Org::parse("<2003-09-16 Tue 09:39>--<2003-09-16 Tue 10:39>").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_active());
/// let ts = Org::parse("<2003-09-16 Tue 09:39>").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_active());
/// ```
pub fn is_active(&self) -> bool {
self.syntax.kind() == SyntaxKind::TIMESTAMP_ACTIVE
}
/// ```rust
/// use orgize::{Org, ast::Timestamp};
///
/// let ts = Org::parse("[2003-09-16 Tue 09:39-10:39]").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_inactive());
/// let ts = Org::parse("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_inactive());
/// let ts = Org::parse("[2003-09-16 Tue 09:39]").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_inactive());
/// ```
pub fn is_inactive(&self) -> bool {
self.syntax.kind() == SyntaxKind::TIMESTAMP_INACTIVE
}
/// ```rust
/// use orgize::{Org, ast::Timestamp};
///
/// let ts = Org::parse("<%%(org-calendar-holiday)>").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_diary());
/// ```
pub fn is_diary(&self) -> bool {
self.syntax.kind() == SyntaxKind::TIMESTAMP_DIARY
}
/// Returns `true` if this timestamp has a range
///
/// ```rust
/// use orgize::{Org, ast::Timestamp};
///
/// let ts = Org::parse("[2003-09-16 Tue 09:39-10:39]").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_range());
/// let ts = Org::parse("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]").first_node::<Timestamp>().unwrap();
/// assert!(ts.is_range());
/// let ts = Org::parse("[2003-09-16 Tue 09:39]").first_node::<Timestamp>().unwrap();
/// assert!(!ts.is_range());
/// ```
pub fn is_range(&self) -> bool {
self.syntax
.children_with_tokens()
.filter_map(filter_token(SyntaxKind::MINUS))
.count()
> 2
}
/// Converts timestamp start to chrono NaiveDateTime
///
/// ```rust
/// use orgize::{Org, ast::Timestamp};
/// use chrono::NaiveDateTime;
///
/// let ts = Org::parse("[2003-09-16 Tue 09:39-10:39]").first_node::<Timestamp>().unwrap();
/// assert_eq!(ts.start_to_chrono().unwrap(), "2003-09-16T09:39:00".parse::<NaiveDateTime>().unwrap());
/// ```
#[cfg(feature = "chrono")]
pub fn start_to_chrono(&self) -> Option<chrono::NaiveDateTime> {
Some(chrono::NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(
self.year_start()?.text().parse().ok()?,
self.month_start()?.text().parse().ok()?,
self.day_start()?.text().parse().ok()?,
)?,
chrono::NaiveTime::from_hms_opt(
self.hour_start()?.text().parse().ok()?,
self.minute_start()?.text().parse().ok()?,
0,
)?,
))
}
/// Converts timestamp end to chrono NaiveDateTime
///
/// ```rust
/// use orgize::{Org, ast::Timestamp};
/// use chrono::NaiveDateTime;
///
/// let ts = Org::parse("[2003-09-16 Tue 09:39-10:39]").first_node::<Timestamp>().unwrap();
/// assert_eq!(ts.end_to_chrono().unwrap(), "2003-09-16T10:39:00".parse::<NaiveDateTime>().unwrap());
/// ```
#[cfg(feature = "chrono")]
pub fn end_to_chrono(&self) -> Option<chrono::NaiveDateTime> {
Some(chrono::NaiveDateTime::new(
chrono::NaiveDate::from_ymd_opt(
self.year_end()?.text().parse().ok()?,
self.month_end()?.text().parse().ok()?,
self.day_end()?.text().parse().ok()?,
)?,
chrono::NaiveTime::from_hms_opt(
self.hour_end()?.text().parse().ok()?,
self.minute_end()?.text().parse().ok()?,
0,
)?,
))
}
}

View file

@ -1,18 +1,56 @@
use crate::syntax::document::document_node;
use crate::Org;
/// Parse configuration
#[derive(Clone, Debug)]
pub struct ParseConfig {
/// Headline's todo keywords
pub todo_keywords: (Vec<String>, Vec<String>),
pub dual_keywords: Vec<String>,
pub parsed_keywords: Vec<String>,
/// Affiliated keywords
///
/// Equivalent to [`org-element-affiliated-keywords`](https://git.sr.ht/~bzg/org-mode/tree/6f960f3c6a4dfe137fbd33fef9f7dadfd229600c/item/lisp/org-element.el#L331)
pub affiliated_keywords: Vec<String>,
}
impl ParseConfig {
/// Parses input with current config
pub fn parse(self, input: impl AsRef<str>) -> Org {
let input = (input.as_ref(), &self).into();
let node = document_node(input).unwrap().1;
Org {
config: self,
green: node.into_node().unwrap(),
}
}
}
impl Default for ParseConfig {
fn default() -> Self {
ParseConfig {
todo_keywords: (vec![String::from("TODO")], vec![String::from("DONE")]),
todo_keywords: (vec!["TODO".into()], vec!["DONE".into()]),
dual_keywords: vec!["CAPTION".into(), "RESULTS".into()],
parsed_keywords: vec!["CAPTION".into()],
affiliated_keywords: vec![
"CAPTION".into(),
"DATA".into(),
"HEADER".into(),
"HEADERS".into(),
"LABEL".into(),
"NAME".into(),
"PLOT".into(),
"RESNAME".into(),
"RESULT".into(),
"RESULTS".into(),
"SOURCE".into(),
"SRCNAME".into(),
"TBLNAME".into(),
],
}
}
}
lazy_static::lazy_static! {
pub static ref DEFAULT_CONFIG: ParseConfig = ParseConfig::default();
}

View file

@ -1,408 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::tag_no_case,
character::complete::{alpha1, space0},
sequence::preceded,
IResult,
};
use crate::elements::Element;
use crate::parse::combinators::{blank_lines_count, line, lines_till};
/// Special Block Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct SpecialBlock<'a> {
/// Block parameters
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub parameters: Option<Cow<'a, str>>,
/// Block name
pub name: Cow<'a, str>,
/// Numbers of blank lines between first block's line and next non-blank
/// line
pub pre_blank: usize,
/// Numbers of blank lines between last block's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl SpecialBlock<'_> {
pub fn into_owned(self) -> SpecialBlock<'static> {
SpecialBlock {
name: self.name.into_owned().into(),
parameters: self.parameters.map(Into::into).map(Cow::Owned),
pre_blank: self.pre_blank,
post_blank: self.post_blank,
}
}
}
/// Quote Block Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct QuoteBlock<'a> {
/// Optional block parameters
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub parameters: Option<Cow<'a, str>>,
/// Numbers of blank lines between first block's line and next non-blank
/// line
pub pre_blank: usize,
/// Numbers of blank lines between last block's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl QuoteBlock<'_> {
pub fn into_owned(self) -> QuoteBlock<'static> {
QuoteBlock {
parameters: self.parameters.map(Into::into).map(Cow::Owned),
pre_blank: self.pre_blank,
post_blank: self.post_blank,
}
}
}
/// Center Block Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct CenterBlock<'a> {
/// Optional block parameters
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub parameters: Option<Cow<'a, str>>,
/// Numbers of blank lines between first block's line and next non-blank
/// line
pub pre_blank: usize,
/// Numbers of blank lines between last block's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl CenterBlock<'_> {
pub fn into_owned(self) -> CenterBlock<'static> {
CenterBlock {
parameters: self.parameters.map(Into::into).map(Cow::Owned),
pre_blank: self.pre_blank,
post_blank: self.post_blank,
}
}
}
/// Verse Block Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct VerseBlock<'a> {
/// Optional block parameters
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub parameters: Option<Cow<'a, str>>,
/// Numbers of blank lines between first block's line and next non-blank
/// line
pub pre_blank: usize,
/// Numbers of blank lines between last block's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl VerseBlock<'_> {
pub fn into_owned(self) -> VerseBlock<'static> {
VerseBlock {
parameters: self.parameters.map(Into::into).map(Cow::Owned),
pre_blank: self.pre_blank,
post_blank: self.post_blank,
}
}
}
/// Comment Block Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct CommentBlock<'a> {
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub data: Option<Cow<'a, str>>,
/// Comment block contents
pub contents: Cow<'a, str>,
/// Numbers of blank lines between last block's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl CommentBlock<'_> {
pub fn into_owned(self) -> CommentBlock<'static> {
CommentBlock {
data: self.data.map(Into::into).map(Cow::Owned),
contents: self.contents.into_owned().into(),
post_blank: self.post_blank,
}
}
}
/// Example Block Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct ExampleBlock<'a> {
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub data: Option<Cow<'a, str>>,
/// Block contents
pub contents: Cow<'a, str>,
/// Numbers of blank lines between last block's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl ExampleBlock<'_> {
pub fn into_owned(self) -> ExampleBlock<'static> {
ExampleBlock {
data: self.data.map(Into::into).map(Cow::Owned),
contents: self.contents.into_owned().into(),
post_blank: self.post_blank,
}
}
}
/// Export Block Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct ExportBlock<'a> {
pub data: Cow<'a, str>,
/// Block contents
pub contents: Cow<'a, str>,
/// Numbers of blank lines between last block's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl ExportBlock<'_> {
pub fn into_owned(self) -> ExportBlock<'static> {
ExportBlock {
data: self.data.into_owned().into(),
contents: self.contents.into_owned().into(),
post_blank: self.post_blank,
}
}
}
/// Src Block Element
#[derive(Debug, Default, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct SourceBlock<'a> {
/// Block contents
pub contents: Cow<'a, str>,
/// Language of the code in the block
pub language: Cow<'a, str>,
pub arguments: Cow<'a, str>,
/// Numbers of blank lines between last block's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl SourceBlock<'_> {
pub fn into_owned(self) -> SourceBlock<'static> {
SourceBlock {
language: self.language.into_owned().into(),
arguments: self.arguments.into_owned().into(),
contents: self.contents.into_owned().into(),
post_blank: self.post_blank,
}
}
// TODO: fn number_lines() -> Some(New) | Some(Continued) | None { }
// TODO: fn preserve_indent() -> bool { }
// TODO: fn use_labels() -> bool { }
// TODO: fn label_fmt() -> Option<String> { }
// TODO: fn retain_labels() -> bool { }
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct RawBlock<'a> {
pub name: &'a str,
pub arguments: &'a str,
pub pre_blank: usize,
pub contents: &'a str,
pub contents_without_blank_lines: &'a str,
pub post_blank: usize,
}
impl<'a> RawBlock<'a> {
pub fn parse(input: &str) -> Option<(&str, RawBlock)> {
parse_internal(input).ok()
}
pub fn into_element(self) -> (Element<'a>, &'a str) {
let RawBlock {
name,
contents,
arguments,
pre_blank,
contents_without_blank_lines,
post_blank,
} = self;
let arguments: Option<Cow<'a, str>> = if arguments.is_empty() {
None
} else {
Some(arguments.into())
};
let element = match &*name.to_uppercase() {
"CENTER" => CenterBlock {
parameters: arguments,
pre_blank,
post_blank,
}
.into(),
"QUOTE" => QuoteBlock {
parameters: arguments,
pre_blank,
post_blank,
}
.into(),
"VERSE" => VerseBlock {
parameters: arguments,
pre_blank,
post_blank,
}
.into(),
"COMMENT" => CommentBlock {
data: arguments,
contents: contents.into(),
post_blank,
}
.into(),
"EXAMPLE" => ExampleBlock {
data: arguments,
contents: contents.into(),
post_blank,
}
.into(),
"EXPORT" => ExportBlock {
data: arguments.unwrap_or_default(),
contents: contents.into(),
post_blank,
}
.into(),
"SRC" => {
let (language, arguments) = match &arguments {
Some(Cow::Borrowed(args)) => {
let (language, arguments) =
args.split_at(args.find(' ').unwrap_or_else(|| args.len()));
(language.into(), arguments.into())
}
None => (Cow::Borrowed(""), Cow::Borrowed("")),
_ => unreachable!(
"`parse_block_element` returns `Some(Cow::Borrowed)` or `None`"
),
};
SourceBlock {
arguments,
language,
contents: contents.into(),
post_blank,
}
.into()
}
_ => SpecialBlock {
parameters: arguments,
name: name.into(),
pre_blank,
post_blank,
}
.into(),
};
(element, contents_without_blank_lines)
}
}
fn parse_internal(input: &str) -> IResult<&str, RawBlock, ()> {
let (input, _) = space0(input)?;
let (input, name) = preceded(tag_no_case("#+BEGIN_"), alpha1)(input)?;
let (input, arguments) = line(input)?;
let end_line = format!("#+END_{}", name);
let (input, contents) = lines_till(|line| line.trim().eq_ignore_ascii_case(&end_line))(input)?;
let (contents_without_blank_lines, pre_blank) = blank_lines_count(contents)?;
let (input, post_blank) = blank_lines_count(input)?;
Ok((
input,
RawBlock {
name,
contents,
arguments: arguments.trim(),
pre_blank,
contents_without_blank_lines,
post_blank,
},
))
}
#[test]
fn parse() {
assert_eq!(
RawBlock::parse(
r#"#+BEGIN_SRC
#+END_SRC"#
),
Some((
"",
RawBlock {
contents: "",
contents_without_blank_lines: "",
pre_blank: 0,
post_blank: 0,
name: "SRC".into(),
arguments: ""
}
))
);
assert_eq!(
RawBlock::parse(
r#"#+begin_src
#+end_src"#
),
Some((
"",
RawBlock {
contents: "",
contents_without_blank_lines: "",
pre_blank: 0,
post_blank: 0,
name: "src".into(),
arguments: ""
}
))
);
assert_eq!(
RawBlock::parse(
r#"#+BEGIN_SRC javascript
console.log('Hello World!');
#+END_SRC
"#
),
Some((
"",
RawBlock {
contents: "console.log('Hello World!');\n",
contents_without_blank_lines: "console.log('Hello World!');\n",
pre_blank: 0,
post_blank: 1,
name: "SRC".into(),
arguments: "javascript"
}
))
);
// TODO: more testing
}

View file

@ -1,242 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::tag,
character::complete::{char, digit1, space0},
combinator::recognize,
sequence::separated_pair,
IResult,
};
use crate::elements::timestamp::{parse_inactive, Datetime, Timestamp};
use crate::parse::combinators::{blank_lines_count, eol};
/// Clock Element
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(untagged))]
#[derive(Debug, Clone)]
pub enum Clock<'a> {
/// Closed Clock
Closed {
/// Time start
start: Datetime<'a>,
/// Time end
end: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
/// Clock duration
duration: Cow<'a, str>,
/// Numbers of blank lines between the clock line and next non-blank
/// line or buffer's end
post_blank: usize,
},
/// Running Clock
Running {
/// Time start
start: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
/// Numbers of blank lines between the clock line and next non-blank
/// line or buffer's end
post_blank: usize,
},
}
impl Clock<'_> {
pub(crate) fn parse(input: &str) -> Option<(&str, Clock)> {
parse_internal(input).ok()
}
pub fn into_onwed(self) -> Clock<'static> {
match self {
Clock::Closed {
start,
end,
repeater,
delay,
duration,
post_blank,
} => Clock::Closed {
start: start.into_owned(),
end: end.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
duration: duration.into_owned().into(),
post_blank,
},
Clock::Running {
start,
repeater,
delay,
post_blank,
} => Clock::Running {
start: start.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
post_blank,
},
}
}
/// Returns `true` if the clock is running.
pub fn is_running(&self) -> bool {
match self {
Clock::Closed { .. } => false,
Clock::Running { .. } => true,
}
}
/// Returns `true` if the clock is closed.
pub fn is_closed(&self) -> bool {
match self {
Clock::Closed { .. } => true,
Clock::Running { .. } => false,
}
}
/// Returns clock duration, or `None` if it's running.
pub fn duration(&self) -> Option<&str> {
match self {
Clock::Closed { duration, .. } => Some(duration),
Clock::Running { .. } => None,
}
}
/// Constructs a timestamp from the clock.
pub fn value(&self) -> Timestamp {
match &*self {
Clock::Closed {
start,
end,
repeater,
delay,
..
} => Timestamp::InactiveRange {
start: start.clone(),
end: end.clone(),
repeater: repeater.clone(),
delay: delay.clone(),
},
Clock::Running {
start,
repeater,
delay,
..
} => Timestamp::Inactive {
start: start.clone(),
repeater: repeater.clone(),
delay: delay.clone(),
},
}
}
}
fn parse_internal(input: &str) -> IResult<&str, Clock, ()> {
let (input, _) = space0(input)?;
let (input, _) = tag("CLOCK:")(input)?;
let (input, _) = space0(input)?;
let (input, timestamp) = parse_inactive(input)?;
match timestamp {
Timestamp::InactiveRange {
start,
end,
repeater,
delay,
} => {
let (input, _) = space0(input)?;
let (input, _) = tag("=>")(input)?;
let (input, _) = space0(input)?;
let (input, duration) = recognize(separated_pair(digit1, char(':'), digit1))(input)?;
let (input, _) = eol(input)?;
let (input, blank) = blank_lines_count(input)?;
Ok((
input,
Clock::Closed {
start,
end,
repeater,
delay,
duration: duration.into(),
post_blank: blank,
},
))
}
Timestamp::Inactive {
start,
repeater,
delay,
} => {
let (input, _) = eol(input)?;
let (input, blank) = blank_lines_count(input)?;
Ok((
input,
Clock::Running {
start,
repeater,
delay,
post_blank: blank,
},
))
}
_ => unreachable!(
"`parse_inactive` only returns `Timestamp::InactiveRange` or `Timestamp::Inactive`."
),
}
}
#[test]
fn parse() {
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]"),
Some((
"",
Clock::Running {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(9),
minute: Some(39)
},
repeater: None,
delay: None,
post_blank: 0,
}
))
);
assert_eq!(
Clock::parse("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00\n\n"),
Some((
"",
Clock::Closed {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(9),
minute: Some(39)
},
end: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(10),
minute: Some(39)
},
repeater: None,
delay: None,
duration: "1:00".into(),
post_blank: 1,
}
))
);
}

View file

@ -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,
},
))
}

View file

@ -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());
}

View file

@ -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());
}

View file

@ -1,99 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::tag_no_case,
character::complete::{alpha1, space0, space1},
IResult,
};
use crate::parse::combinators::{blank_lines_count, line, lines_till};
/// Dynamic Block Element
#[derive(Debug, Default, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct DynBlock<'a> {
/// Block name
pub block_name: Cow<'a, str>,
/// Block argument
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub arguments: Option<Cow<'a, str>>,
/// Numbers of blank lines between first block's line and next non-blank
/// line
pub pre_blank: usize,
/// Numbers of blank lines between last drawer's line and next non-blank
/// line or buffer's end
pub post_blank: usize,
}
impl DynBlock<'_> {
pub(crate) fn parse(input: &str) -> Option<(&str, (DynBlock, &str))> {
parse_internal(input).ok()
}
pub fn into_owned(self) -> DynBlock<'static> {
DynBlock {
block_name: self.block_name.into_owned().into(),
arguments: self.arguments.map(Into::into).map(Cow::Owned),
pre_blank: self.pre_blank,
post_blank: self.post_blank,
}
}
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, (DynBlock, &str), ()> {
let (input, _) = space0(input)?;
let (input, _) = tag_no_case("#+BEGIN:")(input)?;
let (input, _) = space1(input)?;
let (input, name) = alpha1(input)?;
let (input, args) = line(input)?;
let (input, contents) = lines_till(|line| line.trim().eq_ignore_ascii_case("#+END:"))(input)?;
let (contents, pre_blank) = blank_lines_count(contents)?;
let (input, post_blank) = blank_lines_count(input)?;
Ok((
input,
(
DynBlock {
block_name: name.into(),
arguments: if args.trim().is_empty() {
None
} else {
Some(args.trim().into())
},
pre_blank,
post_blank,
},
contents,
),
))
}
#[test]
fn parse() {
// TODO: testing
assert_eq!(
DynBlock::parse(
r#"#+BEGIN: clocktable :scope file
CONTENTS
#+END:
"#
),
Some((
"",
(
DynBlock {
block_name: "clocktable".into(),
arguments: Some(":scope file".into()),
pre_blank: 2,
post_blank: 1,
},
"CONTENTS\n"
)
))
);
}

View file

@ -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);
}

View file

@ -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
}
))
);
}

View file

@ -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());
}

View file

@ -1,111 +0,0 @@
use std::borrow::Cow;
use memchr::memchr2_iter;
use nom::{
bytes::complete::{tag, take_while},
combinator::opt,
error::{make_error, ErrorKind},
sequence::preceded,
Err, IResult,
};
/// Footnote Reference Element
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct FnRef<'a> {
/// Footnote label
pub label: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub definition: Option<Cow<'a, str>>,
}
impl FnRef<'_> {
pub(crate) fn parse(input: &str) -> Option<(&str, FnRef)> {
parse_internal(input).ok()
}
pub fn into_owned(self) -> FnRef<'static> {
FnRef {
label: self.label.into_owned().into(),
definition: self.definition.map(Into::into).map(Cow::Owned),
}
}
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, FnRef, ()> {
let (input, _) = tag("[fn:")(input)?;
let (input, label) =
take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_')(input)?;
let (input, definition) = opt(preceded(tag(":"), balanced_brackets))(input)?;
let (input, _) = tag("]")(input)?;
Ok((
input,
FnRef {
label: label.into(),
definition: definition.map(Into::into),
},
))
}
fn balanced_brackets(input: &str) -> IResult<&str, &str, ()> {
let mut pairs = 1;
for i in memchr2_iter(b'[', b']', input.as_bytes()) {
if input.as_bytes()[i] == b'[' {
pairs += 1;
} else if pairs != 1 {
pairs -= 1;
} else {
return Ok((&input[i..], &input[0..i]));
}
}
Err(Err::Error(make_error(input, ErrorKind::Tag)))
}
#[test]
fn parse() {
assert_eq!(
FnRef::parse("[fn:1]"),
Some((
"",
FnRef {
label: "1".into(),
definition: None
},
))
);
assert_eq!(
FnRef::parse("[fn:1:2]"),
Some((
"",
FnRef {
label: "1".into(),
definition: Some("2".into())
},
))
);
assert_eq!(
FnRef::parse("[fn::2]"),
Some((
"",
FnRef {
label: "".into(),
definition: Some("2".into())
},
))
);
assert_eq!(
FnRef::parse("[fn::[]]"),
Some((
"",
FnRef {
label: "".into(),
definition: Some("[]".into())
},
))
);
assert!(FnRef::parse("[fn::[]").is_none());
}

View file

@ -1,122 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_till},
combinator::opt,
sequence::{delimited, preceded},
IResult,
};
/// Inline Babel Call Object
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Default, Clone)]
pub struct InlineCall<'a> {
/// Called code block name
pub name: Cow<'a, str>,
/// Header arguments applied to the code block
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub inside_header: Option<Cow<'a, str>>,
/// Argument passed to the code block
pub arguments: Cow<'a, str>,
/// Header arguments applied to the calling instance
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub end_header: Option<Cow<'a, str>>,
}
impl InlineCall<'_> {
pub(crate) fn parse(input: &str) -> Option<(&str, InlineCall)> {
parse_internal(input).ok()
}
pub fn into_owned(self) -> InlineCall<'static> {
InlineCall {
name: self.name.into_owned().into(),
arguments: self.arguments.into_owned().into(),
inside_header: self.inside_header.map(Into::into).map(Cow::Owned),
end_header: self.end_header.map(Into::into).map(Cow::Owned),
}
}
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, InlineCall, ()> {
let (input, name) = preceded(
tag("call_"),
take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')'),
)(input)?;
let (input, inside_header) = opt(delimited(
tag("["),
take_till(|c| c == ']' || c == '\n'),
tag("]"),
))(input)?;
let (input, arguments) =
delimited(tag("("), take_till(|c| c == ')' || c == '\n'), tag(")"))(input)?;
let (input, end_header) = opt(delimited(
tag("["),
take_till(|c| c == ']' || c == '\n'),
tag("]"),
))(input)?;
Ok((
input,
InlineCall {
name: name.into(),
arguments: arguments.into(),
inside_header: inside_header.map(Into::into),
end_header: end_header.map(Into::into),
},
))
}
#[test]
fn parse() {
assert_eq!(
InlineCall::parse("call_square(4)"),
Some((
"",
InlineCall {
name: "square".into(),
arguments: "4".into(),
inside_header: None,
end_header: None,
}
))
);
assert_eq!(
InlineCall::parse("call_square[:results output](4)"),
Some((
"",
InlineCall {
name: "square".into(),
arguments: "4".into(),
inside_header: Some(":results output".into()),
end_header: None,
},
))
);
assert_eq!(
InlineCall::parse("call_square(4)[:results html]"),
Some((
"",
InlineCall {
name: "square".into(),
arguments: "4".into(),
inside_header: None,
end_header: Some(":results html".into()),
},
))
);
assert_eq!(
InlineCall::parse("call_square[:results output](4)[:results html]"),
Some((
"",
InlineCall {
name: "square".into(),
arguments: "4".into(),
inside_header: Some(":results output".into()),
end_header: Some(":results html".into()),
},
))
);
}

View file

@ -1,88 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_till, take_while1},
combinator::opt,
sequence::delimited,
IResult,
};
/// Inline Src Block Object
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct InlineSrc<'a> {
/// Language of the code
pub lang: Cow<'a, str>,
/// Optional header arguments
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub options: Option<Cow<'a, str>>,
/// Source code
pub body: Cow<'a, str>,
}
impl InlineSrc<'_> {
pub(crate) fn parse(input: &str) -> Option<(&str, InlineSrc)> {
parse_internal(input).ok()
}
pub fn into_owned(self) -> InlineSrc<'static> {
InlineSrc {
lang: self.lang.into_owned().into(),
options: self.options.map(Into::into).map(Cow::Owned),
body: self.body.into_owned().into(),
}
}
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, InlineSrc, ()> {
let (input, _) = tag("src_")(input)?;
let (input, lang) =
take_while1(|c: char| !c.is_ascii_whitespace() && c != '[' && c != '{')(input)?;
let (input, options) = opt(delimited(
tag("["),
take_till(|c| c == '\n' || c == ']'),
tag("]"),
))(input)?;
let (input, body) = delimited(tag("{"), take_till(|c| c == '\n' || c == '}'), tag("}"))(input)?;
Ok((
input,
InlineSrc {
lang: lang.into(),
options: options.map(Into::into),
body: body.into(),
},
))
}
#[test]
fn parse() {
assert_eq!(
InlineSrc::parse("src_C{int a = 0;}"),
Some((
"",
InlineSrc {
lang: "C".into(),
options: None,
body: "int a = 0;".into()
},
))
);
assert_eq!(
InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>}"),
Some((
"",
InlineSrc {
lang: "xml".into(),
options: Some(":exports code".into()),
body: "<tag>text</tag>".into(),
},
))
);
assert!(InlineSrc::parse("src_xml[:exports code]{<tag>text</tag>").is_none());
assert!(InlineSrc::parse("src_[:exports code]{<tag>text</tag>}").is_none());
assert!(InlineSrc::parse("src_xml[:exports code]").is_none());
}

View file

@ -1,230 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_till},
character::complete::space0,
combinator::opt,
sequence::delimited,
IResult,
};
use crate::elements::Element;
use crate::parse::combinators::{blank_lines_count, line};
/// Keyword Element
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Keyword<'a> {
/// Keyword name
pub key: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub optional: Option<Cow<'a, str>>,
/// Keyword value
pub value: Cow<'a, str>,
/// Numbers of blank lines between keyword line and next non-blank line or
/// buffer's end
pub post_blank: usize,
}
impl Keyword<'_> {
pub fn into_owned(self) -> Keyword<'static> {
Keyword {
key: self.key.into_owned().into(),
optional: self.optional.map(Into::into).map(Cow::Owned),
value: self.value.into_owned().into(),
post_blank: self.post_blank,
}
}
}
/// Babel Call Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct BabelCall<'a> {
/// Babel call value
pub value: Cow<'a, str>,
/// Numbers of blank lines between babel call line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl BabelCall<'_> {
pub fn into_owned(self) -> BabelCall<'static> {
BabelCall {
value: self.value.into_owned().into(),
post_blank: self.post_blank,
}
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub(crate) struct RawKeyword<'a> {
pub key: &'a str,
pub value: &'a str,
pub optional: Option<&'a str>,
pub post_blank: usize,
}
impl<'a> RawKeyword<'a> {
pub fn parse(input: &str) -> Option<(&str, RawKeyword)> {
parse_internal(input).ok()
}
pub fn into_element(self) -> Element<'a> {
let RawKeyword {
key,
value,
optional,
post_blank,
} = self;
if (&*key).eq_ignore_ascii_case("CALL") {
BabelCall {
value: value.into(),
post_blank,
}
.into()
} else {
Keyword {
key: key.into(),
optional: optional.map(Into::into),
value: value.into(),
post_blank,
}
.into()
}
}
}
fn parse_internal(input: &str) -> IResult<&str, RawKeyword, ()> {
let (input, _) = space0(input)?;
let (input, _) = tag("#+")(input)?;
let (input, key) = take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '[')(input)?;
let (input, optional) = opt(delimited(
tag("["),
take_till(|c| c == ']' || c == '\n'),
tag("]"),
))(input)?;
let (input, _) = tag(":")(input)?;
let (input, value) = line(input)?;
let (input, post_blank) = blank_lines_count(input)?;
Ok((
input,
RawKeyword {
key,
optional,
value: value.trim(),
post_blank,
},
))
}
#[test]
fn parse() {
assert_eq!(
RawKeyword::parse("#+KEY:"),
Some((
"",
RawKeyword {
key: "KEY",
optional: None,
value: "",
post_blank: 0
}
))
);
assert_eq!(
RawKeyword::parse("#+KEY: VALUE"),
Some((
"",
RawKeyword {
key: "KEY",
optional: None,
value: "VALUE",
post_blank: 0
}
))
);
assert_eq!(
RawKeyword::parse("#+K_E_Y: VALUE"),
Some((
"",
RawKeyword {
key: "K_E_Y",
optional: None,
value: "VALUE",
post_blank: 0
}
))
);
assert_eq!(
RawKeyword::parse("#+KEY:VALUE\n"),
Some((
"",
RawKeyword {
key: "KEY",
optional: None,
value: "VALUE",
post_blank: 0
}
))
);
assert!(RawKeyword::parse("#+KE Y: VALUE").is_none());
assert!(RawKeyword::parse("#+ KEY: VALUE").is_none());
assert_eq!(
RawKeyword::parse("#+RESULTS:"),
Some((
"",
RawKeyword {
key: "RESULTS",
optional: None,
value: "",
post_blank: 0
}
))
);
assert_eq!(
RawKeyword::parse("#+ATTR_LATEX: :width 5cm\n"),
Some((
"",
RawKeyword {
key: "ATTR_LATEX",
optional: None,
value: ":width 5cm",
post_blank: 0
}
))
);
assert_eq!(
RawKeyword::parse("#+CALL: double(n=4)"),
Some((
"",
RawKeyword {
key: "CALL",
optional: None,
value: "double(n=4)",
post_blank: 0
}
))
);
assert_eq!(
RawKeyword::parse("#+CAPTION[Short caption]: Longer caption."),
Some((
"",
RawKeyword {
key: "CAPTION",
optional: Some("Short caption"),
value: "Longer caption.",
post_blank: 0
}
))
);
}

View file

@ -1,80 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_while},
combinator::opt,
sequence::delimited,
IResult,
};
/// Link Object
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Link<'a> {
/// Link destination
pub path: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub desc: Option<Cow<'a, str>>,
}
impl Link<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> Option<(&str, Link)> {
parse_internal(input).ok()
}
pub fn into_owned(self) -> Link<'static> {
Link {
path: self.path.into_owned().into(),
desc: self.desc.map(Into::into).map(Cow::Owned),
}
}
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, Link, ()> {
let (input, path) = delimited(
tag("[["),
take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'),
tag("]"),
)(input)?;
let (input, desc) = opt(delimited(
tag("["),
take_while(|c: char| c != '[' && c != ']'),
tag("]"),
))(input)?;
let (input, _) = tag("]")(input)?;
Ok((
input,
Link {
path: path.into(),
desc: desc.map(Into::into),
},
))
}
#[test]
fn parse() {
assert_eq!(
Link::parse("[[#id]]"),
Some((
"",
Link {
path: "#id".into(),
desc: None
}
))
);
assert_eq!(
Link::parse("[[#id][desc]]"),
Some((
"",
Link {
path: "#id".into(),
desc: Some("desc".into())
}
))
);
assert!(Link::parse("[[#id][desc]").is_none());
}

View file

@ -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
"#
)
))
);
}

View file

@ -1,91 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take, take_until, take_while1},
combinator::{opt, verify},
sequence::delimited,
IResult,
};
/// Macro Object
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Macros<'a> {
/// Macro name
pub name: Cow<'a, str>,
/// Arguments passed to the macro
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub arguments: Option<Cow<'a, str>>,
}
impl Macros<'_> {
pub(crate) fn parse(input: &str) -> Option<(&str, Macros)> {
parse_internal(input).ok()
}
pub fn into_owned(self) -> Macros<'static> {
Macros {
name: self.name.into_owned().into(),
arguments: self.arguments.map(Into::into).map(Cow::Owned),
}
}
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, Macros, ()> {
let (input, _) = tag("{{{")(input)?;
let (input, name) = verify(
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|s: &str| s.starts_with(|c: char| c.is_ascii_alphabetic()),
)(input)?;
let (input, arguments) = opt(delimited(tag("("), take_until(")}}}"), take(1usize)))(input)?;
let (input, _) = tag("}}}")(input)?;
Ok((
input,
Macros {
name: name.into(),
arguments: arguments.map(Into::into),
},
))
}
#[test]
fn test() {
assert_eq!(
Macros::parse("{{{poem(red,blue)}}}"),
Some((
"",
Macros {
name: "poem".into(),
arguments: Some("red,blue".into())
}
))
);
assert_eq!(
Macros::parse("{{{poem())}}}"),
Some((
"",
Macros {
name: "poem".into(),
arguments: Some(")".into())
}
))
);
assert_eq!(
Macros::parse("{{{author}}}"),
Some((
"",
Macros {
name: "author".into(),
arguments: None
}
))
);
assert!(Macros::parse("{{{0uthor}}}").is_none());
assert!(Macros::parse("{{{author}}").is_none());
assert!(Macros::parse("{{{poem(}}}").is_none());
assert!(Macros::parse("{{{poem)}}}").is_none());
}

View file

@ -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
);

View file

@ -1,98 +0,0 @@
use memchr::memchr;
use crate::elements::Timestamp;
/// Planning element
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Planning<'a> {
/// Timestamp associated to deadline keyword
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub deadline: Option<Timestamp<'a>>,
/// Timestamp associated to scheduled keyword
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub scheduled: Option<Timestamp<'a>>,
/// Timestamp associated to closed keyword
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub closed: Option<Timestamp<'a>>,
}
impl Planning<'_> {
#[inline]
pub(crate) fn parse(text: &str) -> Option<(&str, Planning)> {
let (mut deadline, mut scheduled, mut closed) = (None, None, None);
let (mut tail, off) = memchr(b'\n', text.as_bytes())
.map(|i| (text[..i].trim(), i + 1))
.unwrap_or_else(|| (text.trim(), text.len()));
while let Some(i) = memchr(b' ', tail.as_bytes()) {
let next = &tail[i + 1..].trim_start();
macro_rules! set_timestamp {
($timestamp:expr) => {{
let (new_tail, timestamp) =
Timestamp::parse_active(next).or(Timestamp::parse_inactive(next))?;
$timestamp = Some(timestamp);
tail = new_tail.trim_start();
}};
}
match &tail[..i] {
"DEADLINE:" if deadline.is_none() => set_timestamp!(deadline),
"SCHEDULED:" if scheduled.is_none() => set_timestamp!(scheduled),
"CLOSED:" if closed.is_none() => set_timestamp!(closed),
_ => return None,
}
}
if deadline.is_none() && scheduled.is_none() && closed.is_none() {
None
} else {
Some((
&text[off..],
Planning {
deadline,
scheduled,
closed,
},
))
}
}
pub fn into_owned(self) -> Planning<'static> {
Planning {
deadline: self.deadline.map(|x| x.into_owned()),
scheduled: self.scheduled.map(|x| x.into_owned()),
closed: self.closed.map(|x| x.into_owned()),
}
}
}
#[test]
fn prase() {
use crate::elements::Datetime;
assert_eq!(
Planning::parse("SCHEDULED: <2019-04-08 Mon>\n"),
Some((
"",
Planning {
scheduled: Some(Timestamp::Active {
start: Datetime {
year: 2019,
month: 4,
day: 8,
dayname: "Mon".into(),
hour: None,
minute: None
},
repeater: None,
delay: None
}),
deadline: None,
closed: None,
}
))
)
}

View file

@ -1,40 +0,0 @@
use nom::{
bytes::complete::{tag, take_while},
combinator::verify,
sequence::delimited,
IResult,
};
// TODO: text-markup, entities, latex-fragments, subscript and superscript
#[inline]
pub fn parse_radio_target(input: &str) -> Option<(&str, &str)> {
parse_internal(input).ok()
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, &str, ()> {
let (input, contents) = delimited(
tag("<<<"),
verify(
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
),
tag(">>>"),
)(input)?;
Ok((input, contents))
}
#[test]
fn parse() {
assert_eq!(parse_radio_target("<<<target>>>"), Some(("", "target")));
assert_eq!(parse_radio_target("<<<tar get>>>"), Some(("", "tar get")));
assert!(parse_radio_target("<<<target >>>").is_none());
assert!(parse_radio_target("<<< target>>>").is_none());
assert!(parse_radio_target("<<<ta<get>>>").is_none());
assert!(parse_radio_target("<<<ta>get>>>").is_none());
assert!(parse_radio_target("<<<ta\nget>>>").is_none());
assert!(parse_radio_target("<<<target>>").is_none());
}

View file

@ -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());
}

View file

@ -1,100 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take, take_until, take_while1},
sequence::{delimited, separated_pair},
IResult,
};
/// Export Snippet Object
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Snippet<'a> {
/// Back-end name
pub name: Cow<'a, str>,
/// Export code
pub value: Cow<'a, str>,
}
impl Snippet<'_> {
pub(crate) fn parse(input: &str) -> Option<(&str, Snippet)> {
parse_internal(input).ok()
}
pub fn into_owned(self) -> Snippet<'static> {
Snippet {
name: self.name.into_owned().into(),
value: self.value.into_owned().into(),
}
}
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, Snippet, ()> {
let (input, (name, value)) = delimited(
tag("@@"),
separated_pair(
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-'),
tag(":"),
take_until("@@"),
),
take(2usize),
)(input)?;
Ok((
input,
Snippet {
name: name.into(),
value: value.into(),
},
))
}
#[test]
fn parse() {
assert_eq!(
Snippet::parse("@@html:<b>@@"),
Some((
"",
Snippet {
name: "html".into(),
value: "<b>".into()
}
))
);
assert_eq!(
Snippet::parse("@@latex:any arbitrary LaTeX code@@"),
Some((
"",
Snippet {
name: "latex".into(),
value: "any arbitrary LaTeX code".into(),
}
))
);
assert_eq!(
Snippet::parse("@@html:@@"),
Some((
"",
Snippet {
name: "html".into(),
value: "".into(),
}
))
);
assert_eq!(
Snippet::parse("@@html:<p>@</p>@@"),
Some((
"",
Snippet {
name: "html".into(),
value: "<p>@</p>".into(),
}
))
);
assert!(Snippet::parse("@@html:<b>@").is_none());
assert!(Snippet::parse("@@html<b>@@").is_none());
assert!(Snippet::parse("@@:<b>@@").is_none());
}

View file

@ -1,169 +0,0 @@
use std::borrow::Cow;
use nom::{
error::{make_error, ErrorKind},
Err, IResult,
};
use crate::parse::combinators::{blank_lines_count, line, lines_while};
/// Table Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(tag = "table_type"))]
pub enum Table<'a> {
/// "org" type table
#[cfg_attr(feature = "ser", serde(rename = "org"))]
Org {
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
tblfm: Option<Cow<'a, str>>,
/// Numbers of blank lines between last table's line and next non-blank
/// line or buffer's end
post_blank: usize,
has_header: bool,
},
/// "table.el" type table
#[cfg_attr(feature = "ser", serde(rename = "table.el"))]
TableEl {
value: Cow<'a, str>,
/// Numbers of blank lines between last table's line and next non-blank
/// line or buffer's end
post_blank: usize,
},
}
impl Table<'_> {
pub fn parse_table_el(input: &str) -> Option<(&str, Table)> {
Self::parse_table_el_internal(input).ok()
}
fn parse_table_el_internal(input: &str) -> IResult<&str, Table, ()> {
let (_, first_line) = line(input)?;
let first_line = first_line.trim();
// Table.el tables start at lines beginning with "+-" string and followed by plus or minus signs
if !first_line.starts_with("+-")
|| first_line
.as_bytes()
.iter()
.any(|&c| c != b'+' && c != b'-')
{
// TODO: better error kind
return Err(Err::Error(make_error(input, ErrorKind::Many0)));
}
// Table.el tables end at the first line not starting with either a vertical line or a plus sign.
let (input, content) = lines_while(|line| {
let line = line.trim_start();
line.starts_with('|') || line.starts_with('+')
})(input)?;
let (input, post_blank) = blank_lines_count(input)?;
Ok((
input,
Table::TableEl {
value: content.into(),
post_blank,
},
))
}
pub fn into_owned(self) -> Table<'static> {
match self {
Table::Org {
tblfm,
post_blank,
has_header,
} => Table::Org {
tblfm: tblfm.map(Into::into).map(Cow::Owned),
post_blank,
has_header,
},
Table::TableEl { value, post_blank } => Table::TableEl {
value: value.into_owned().into(),
post_blank,
},
}
}
}
/// Table Row Element
///
/// # Syntax
///
/// ```text
/// | 0 | 1 | 2 | <- TableRow::Body
/// | 0 | 1 | 2 | <- TableRow::Body
/// ```
///
/// ```text
/// |-----+-----+-----| <- ignores
/// | 0 | 1 | 2 | <- TableRow::Header
/// | 0 | 1 | 2 | <- TableRow::Header
/// |-----+-----+-----| <- TableRow::HeaderRule
/// | 0 | 1 | 2 | <- TableRow::Body
/// |-----+-----+-----| <- TableRow::BodyRule
/// | 0 | 1 | 2 | <- TableRow::Body
/// |-----+-----+-----| <- TableRow::BodyRule
/// |-----+-----+-----| <- TableRow::BodyRule
/// | 0 | 1 | 2 | <- TableRow::Body
/// |-----+-----+-----| <- ignores
/// ```
///
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(tag = "table_row_type"))]
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
pub enum TableRow {
/// This row is part of table header
Header,
/// This row is part of table body
Body,
/// This row is between table header and body
HeaderRule,
/// This row is between table body and next body
BodyRule,
}
/// Table Cell Element
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(tag = "table_cell_type"))]
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
pub enum TableCell {
/// Header cell
Header,
/// Body cell, or standard cell
Body,
}
#[test]
fn parse_table_el_() {
assert_eq!(
Table::parse_table_el(
r#" +---+
| |
+---+
"#
),
Some((
"",
Table::TableEl {
value: r#" +---+
| |
+---+
"#
.into(),
post_blank: 1
}
))
);
assert!(Table::parse_table_el("").is_none());
assert!(Table::parse_table_el("+----|---").is_none());
}

View file

@ -1,78 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take_while},
combinator::verify,
sequence::delimited,
IResult,
};
/// Target Object
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Target<'a> {
/// Target ID
pub target: Cow<'a, str>,
}
impl Target<'_> {
#[inline]
pub(crate) fn parse(input: &str) -> Option<(&str, Target)> {
parse_internal(input).ok()
}
pub fn into_owned(self) -> Target<'static> {
Target {
target: self.target.into_owned().into(),
}
}
}
#[inline]
fn parse_internal(input: &str) -> IResult<&str, Target, ()> {
let (input, target) = delimited(
tag("<<"),
verify(
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|s: &str| s.starts_with(|c| c != ' ') && s.ends_with(|c| c != ' '),
),
tag(">>"),
)(input)?;
Ok((
input,
Target {
target: target.into(),
},
))
}
#[test]
fn parse() {
assert_eq!(
Target::parse("<<target>>"),
Some((
"",
Target {
target: "target".into()
}
))
);
assert_eq!(
Target::parse("<<tar get>>"),
Some((
"",
Target {
target: "tar get".into()
}
))
);
assert!(Target::parse("<<target >>").is_none());
assert!(Target::parse("<< target>>").is_none());
assert!(Target::parse("<<ta<get>>").is_none());
assert!(Target::parse("<<ta>get>>").is_none());
assert!(Target::parse("<<ta\nget>>").is_none());
assert!(Target::parse("<<target>").is_none());
}

View file

@ -1,482 +0,0 @@
use std::borrow::Cow;
use nom::{
bytes::complete::{tag, take, take_till, take_while, take_while_m_n},
character::complete::{space0, space1},
combinator::{map, map_res, opt},
sequence::preceded,
IResult,
};
/// Datetime Struct
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Datetime<'a> {
pub year: u16,
pub month: u8,
pub day: u8,
pub dayname: Cow<'a, str>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub hour: Option<u8>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub minute: Option<u8>,
}
impl Datetime<'_> {
pub fn into_owned(self) -> Datetime<'static> {
Datetime {
year: self.year,
month: self.month,
day: self.day,
dayname: self.dayname.into_owned().into(),
hour: self.hour,
minute: self.minute,
}
}
}
#[cfg(feature = "chrono")]
mod chrono {
use super::Datetime;
use chrono::*;
impl Into<NaiveDate> for Datetime<'_> {
fn into(self) -> NaiveDate {
(&self).into()
}
}
impl Into<NaiveTime> for Datetime<'_> {
fn into(self) -> NaiveTime {
(&self).into()
}
}
impl Into<NaiveDateTime> for Datetime<'_> {
fn into(self) -> NaiveDateTime {
(&self).into()
}
}
impl Into<DateTime<Utc>> for Datetime<'_> {
fn into(self) -> DateTime<Utc> {
(&self).into()
}
}
impl Into<NaiveDate> for &Datetime<'_> {
fn into(self) -> NaiveDate {
NaiveDate::from_ymd(self.year.into(), self.month.into(), self.day.into())
}
}
impl Into<NaiveTime> for &Datetime<'_> {
fn into(self) -> NaiveTime {
NaiveTime::from_hms(
self.hour.unwrap_or_default().into(),
self.minute.unwrap_or_default().into(),
0,
)
}
}
impl Into<NaiveDateTime> for &Datetime<'_> {
fn into(self) -> NaiveDateTime {
NaiveDateTime::new(self.into(), self.into())
}
}
impl Into<DateTime<Utc>> for &Datetime<'_> {
fn into(self) -> DateTime<Utc> {
DateTime::from_utc(self.into(), Utc)
}
}
}
/// Timestamp Object
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[cfg_attr(feature = "ser", serde(rename_all = "kebab-case"))]
#[cfg_attr(feature = "ser", serde(tag = "timestamp_type"))]
#[derive(Debug, Clone)]
pub enum Timestamp<'a> {
Active {
start: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
Inactive {
start: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
ActiveRange {
start: Datetime<'a>,
end: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
InactiveRange {
start: Datetime<'a>,
end: Datetime<'a>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
repeater: Option<Cow<'a, str>>,
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
delay: Option<Cow<'a, str>>,
},
Diary {
value: Cow<'a, str>,
},
}
impl Timestamp<'_> {
pub(crate) fn parse_active(input: &str) -> Option<(&str, Timestamp)> {
parse_active(input).ok()
}
pub(crate) fn parse_inactive(input: &str) -> Option<(&str, Timestamp)> {
parse_inactive(input).ok()
}
pub(crate) fn parse_diary(input: &str) -> Option<(&str, Timestamp)> {
parse_diary(input).ok()
}
pub fn into_owned(self) -> Timestamp<'static> {
match self {
Timestamp::Active {
start,
repeater,
delay,
} => Timestamp::Active {
start: start.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
Timestamp::Inactive {
start,
repeater,
delay,
} => Timestamp::Inactive {
start: start.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
Timestamp::ActiveRange {
start,
end,
repeater,
delay,
} => Timestamp::ActiveRange {
start: start.into_owned(),
end: end.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
Timestamp::InactiveRange {
start,
end,
repeater,
delay,
} => Timestamp::InactiveRange {
start: start.into_owned(),
end: end.into_owned(),
repeater: repeater.map(Into::into).map(Cow::Owned),
delay: delay.map(Into::into).map(Cow::Owned),
},
Timestamp::Diary { value } => Timestamp::Diary {
value: value.into_owned().into(),
},
}
}
}
pub fn parse_active(input: &str) -> IResult<&str, Timestamp, ()> {
let (input, _) = tag("<")(input)?;
let (input, start) = parse_datetime(input)?;
if input.starts_with('-') {
let (input, (hour, minute)) = parse_time(&input[1..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
let mut end = start.clone();
end.hour = Some(hour);
end.minute = Some(minute);
return Ok((
input,
Timestamp::ActiveRange {
start,
end,
repeater: None,
delay: None,
},
));
}
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
if input.starts_with("--<") {
let (input, end) = parse_datetime(&input["--<".len()..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag(">")(input)?;
Ok((
input,
Timestamp::ActiveRange {
start,
end,
repeater: None,
delay: None,
},
))
} else {
Ok((
input,
Timestamp::Active {
start,
repeater: None,
delay: None,
},
))
}
}
pub fn parse_inactive(input: &str) -> IResult<&str, Timestamp, ()> {
let (input, _) = tag("[")(input)?;
let (input, start) = parse_datetime(input)?;
if input.starts_with('-') {
let (input, (hour, minute)) = parse_time(&input[1..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
let mut end = start.clone();
end.hour = Some(hour);
end.minute = Some(minute);
return Ok((
input,
Timestamp::InactiveRange {
start,
end,
repeater: None,
delay: None,
},
));
}
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
if input.starts_with("--[") {
let (input, end) = parse_datetime(&input["--[".len()..])?;
let (input, _) = space0(input)?;
// TODO: delay-or-repeater
let (input, _) = tag("]")(input)?;
Ok((
input,
Timestamp::InactiveRange {
start,
end,
repeater: None,
delay: None,
},
))
} else {
Ok((
input,
Timestamp::Inactive {
start,
repeater: None,
delay: None,
},
))
}
}
pub fn parse_diary(input: &str) -> IResult<&str, Timestamp, ()> {
let (input, _) = tag("<%%(")(input)?;
let (input, value) = take_till(|c| c == ')' || c == '>' || c == '\n')(input)?;
let (input, _) = tag(")>")(input)?;
Ok((
input,
Timestamp::Diary {
value: value.into(),
},
))
}
fn parse_time(input: &str) -> IResult<&str, (u8, u8), ()> {
let (input, hour) = map_res(take_while_m_n(1, 2, |c: char| c.is_ascii_digit()), |num| {
u8::from_str_radix(num, 10)
})(input)?;
let (input, _) = tag(":")(input)?;
let (input, minute) = map_res(take(2usize), |num| u8::from_str_radix(num, 10))(input)?;
Ok((input, (hour, minute)))
}
fn parse_datetime(input: &str) -> IResult<&str, Datetime, ()> {
let parse_u8 = |num| u8::from_str_radix(num, 10);
let (input, year) = map_res(take(4usize), |num| u16::from_str_radix(num, 10))(input)?;
let (input, _) = tag("-")(input)?;
let (input, month) = map_res(take(2usize), parse_u8)(input)?;
let (input, _) = tag("-")(input)?;
let (input, day) = map_res(take(2usize), parse_u8)(input)?;
let (input, _) = space1(input)?;
let (input, dayname) = take_while(|c: char| {
!c.is_ascii_whitespace()
&& !c.is_ascii_digit()
&& c != '+'
&& c != '-'
&& c != ']'
&& c != '>'
})(input)?;
let (input, (hour, minute)) = map(opt(preceded(space1, parse_time)), |time| {
(time.map(|t| t.0), time.map(|t| t.1))
})(input)?;
Ok((
input,
Datetime {
year,
month,
day,
dayname: dayname.into(),
hour,
minute,
},
))
}
// TODO
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum RepeaterType {
// Cumulate,
// CatchUp,
// Restart,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum DelayType {
// All,
// First,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub enum TimeUnit {
// Hour,
// Day,
// Week,
// Month,
// Year,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub struct Repeater {
// pub ty: RepeaterType,
// pub value: usize,
// pub unit: TimeUnit,
// }
// #[cfg_attr(test, derive(PartialEq))]
// #[cfg_attr(feature = "ser", derive(serde::Serialize))]
// #[derive(Debug, Copy, Clone)]
// pub struct Delay {
// pub ty: DelayType,
// pub value: usize,
// pub unit: TimeUnit,
// }
#[test]
fn parse() {
assert_eq!(
parse_inactive("[2003-09-16 Tue]"),
Ok((
"",
Timestamp::Inactive {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: None,
minute: None
},
repeater: None,
delay: None,
},
))
);
assert_eq!(
parse_inactive("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]"),
Ok((
"",
Timestamp::InactiveRange {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(9),
minute: Some(39)
},
end: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(10),
minute: Some(39),
},
repeater: None,
delay: None
},
))
);
assert_eq!(
parse_active("<2003-09-16 Tue 09:39-10:39>"),
Ok((
"",
Timestamp::ActiveRange {
start: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(9),
minute: Some(39),
},
end: Datetime {
year: 2003,
month: 9,
day: 16,
dayname: "Tue".into(),
hour: Some(10),
minute: Some(39),
},
repeater: None,
delay: None
},
))
);
}

View file

@ -1,551 +0,0 @@
//! Headline Title
use std::collections::HashMap;
use std::{borrow::Cow, iter::FromIterator};
use memchr::memrchr2;
use nom::{
branch::alt,
bytes::complete::{tag, take_until, take_while},
character::complete::{anychar, line_ending, space1},
combinator::{map, opt, verify},
error::{make_error, ErrorKind},
multi::fold_many0,
sequence::{delimited, preceded},
Err, IResult,
};
use crate::{
config::ParseConfig,
elements::{drawer::parse_drawer_without_blank, Planning, Timestamp},
parse::combinators::{blank_lines_count, line, one_word},
};
/// Title Element
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
#[derive(Debug, Clone)]
pub struct Title<'a> {
/// Headline level, number of stars
pub level: usize,
/// Headline priority cookie
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub priority: Option<char>,
/// Headline title tags
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Vec::is_empty"))]
pub tags: Vec<Cow<'a, str>>,
/// Headline todo keyword
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub keyword: Option<Cow<'a, str>>,
/// Raw headline's text, without the stars and the tags
pub raw: Cow<'a, str>,
/// Planning element associated to this headline
#[cfg_attr(feature = "ser", serde(skip_serializing_if = "Option::is_none"))]
pub planning: Option<Box<Planning<'a>>>,
/// Property drawer associated to this headline
#[cfg_attr(
feature = "ser",
serde(skip_serializing_if = "PropertiesMap::is_empty")
)]
pub properties: PropertiesMap<'a>,
/// Numbers of blank lines between last title's line and next non-blank line
/// or buffer's end
pub post_blank: usize,
}
impl Title<'_> {
pub(crate) fn parse<'a>(
input: &'a str,
config: &ParseConfig,
) -> Option<(&'a str, (Title<'a>, &'a str))> {
parse_title(input, config).ok()
}
// TODO: fn is_quoted(&self) -> bool { }
// TODO: fn is_footnote_section(&self) -> bool { }
/// Returns this headline's closed timestamp, or `None` if not set.
pub fn closed(&self) -> Option<&Timestamp> {
self.planning.as_ref().and_then(|p| p.closed.as_ref())
}
/// Returns this headline's scheduled timestamp, or `None` if not set.
pub fn scheduled(&self) -> Option<&Timestamp> {
self.planning.as_ref().and_then(|p| p.scheduled.as_ref())
}
/// Returns this headline's deadline timestamp, or `None` if not set.
pub fn deadline(&self) -> Option<&Timestamp> {
self.planning.as_ref().and_then(|p| p.deadline.as_ref())
}
/// Returns `true` if this headline is archived
pub fn is_archived(&self) -> bool {
self.tags.iter().any(|tag| tag == "ARCHIVE")
}
/// Returns `true` if this headline is commented
pub fn is_commented(&self) -> bool {
self.raw.starts_with("COMMENT")
&& (self.raw.len() == 7 || self.raw[7..].starts_with(char::is_whitespace))
}
pub fn into_owned(self) -> Title<'static> {
Title {
level: self.level,
priority: self.priority,
tags: self
.tags
.into_iter()
.map(|s| s.into_owned().into())
.collect(),
keyword: self.keyword.map(Into::into).map(Cow::Owned),
raw: self.raw.into_owned().into(),
planning: self.planning.map(|p| Box::new(p.into_owned())),
properties: self.properties.into_owned(),
post_blank: self.post_blank,
}
}
}
impl Default for Title<'_> {
fn default() -> Title<'static> {
Title {
level: 1,
priority: None,
tags: Vec::new(),
keyword: None,
raw: Cow::Borrowed(""),
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
}
}
}
/// Properties
#[derive(Default, Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "ser", derive(serde::Serialize))]
pub struct PropertiesMap<'a> {
pub pairs: Vec<(Cow<'a, str>, Cow<'a, str>)>,
}
impl<'a> PropertiesMap<'a> {
pub fn new() -> Self {
PropertiesMap { pairs: Vec::new() }
}
pub fn is_empty(&self) -> bool {
self.pairs.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &(Cow<'a, str>, Cow<'a, str>)> {
self.pairs.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut (Cow<'a, str>, Cow<'a, str>)> {
self.pairs.iter_mut()
}
pub fn into_iter(self) -> impl Iterator<Item = (Cow<'a, str>, Cow<'a, str>)> {
self.pairs.into_iter()
}
pub fn into_hash_map(self) -> HashMap<Cow<'a, str>, Cow<'a, str>> {
self.pairs.into_iter().collect()
}
#[cfg(feature = "indexmap")]
pub fn into_index_map(self) -> indexmap::IndexMap<Cow<'a, str>, Cow<'a, str>> {
self.pairs.into_iter().collect()
}
pub fn into_owned(self) -> PropertiesMap<'static> {
self.pairs
.into_iter()
.map(|(k, v)| (k.into_owned().into(), v.into_owned().into()))
.collect()
}
}
impl<'a> FromIterator<(Cow<'a, str>, Cow<'a, str>)> for PropertiesMap<'a> {
fn from_iter<T: IntoIterator<Item = (Cow<'a, str>, Cow<'a, str>)>>(iter: T) -> Self {
let mut map = PropertiesMap::new();
map.pairs.extend(iter);
map
}
}
fn white_spaces_or_eol(input: &str) -> IResult<&str, &str, ()> {
alt((space1, line_ending))(input)
}
#[inline]
fn parse_title<'a>(
input: &'a str,
config: &ParseConfig,
) -> IResult<&'a str, (Title<'a>, &'a str), ()> {
let (input, level) = map(take_while(|c: char| c == '*'), |s: &str| s.len())(input)?;
debug_assert!(level > 0);
let (input, keyword) = opt(preceded(
space1,
verify(one_word, |s: &str| {
config.todo_keywords.0.iter().any(|x| x == s)
|| config.todo_keywords.1.iter().any(|x| x == s)
}),
))(input)?;
let (input, priority) = opt(delimited(
space1,
delimited(
tag("[#"),
verify(anychar, |c: &char| c.is_ascii_uppercase()),
tag("]"),
),
white_spaces_or_eol,
))(input)?;
let (input, tail) = line(input)?;
let tail = tail.trim();
// tags can be separated by space or \t
let (raw, tags) = memrchr2(b' ', b'\t', tail.as_bytes())
.map(|i| (tail[0..i].trim(), &tail[i + 1..]))
.filter(|(_, x)| is_tag_line(x))
.unwrap_or((tail, ""));
let tags = tags
.split(':')
.filter(|s| !s.is_empty())
.map(Into::into)
.collect();
let (input, planning) = Planning::parse(input)
.map(|(input, planning)| (input, Some(Box::new(planning))))
.unwrap_or((input, None));
let (input, properties) = opt(parse_properties_drawer)(input)?;
let (input, post_blank) = blank_lines_count(input)?;
Ok((
input,
(
Title {
properties: properties.unwrap_or_default(),
level,
keyword: keyword.map(Into::into),
priority,
tags,
raw: raw.into(),
planning,
post_blank,
},
raw,
),
))
}
fn is_tag_line(input: &str) -> bool {
input.len() > 2
&& input.starts_with(':')
&& input.ends_with(':')
&& input.chars().all(|ch| {
ch.is_alphanumeric() || ch == '_' || ch == '@' || ch == '#' || ch == '%' || ch == ':'
})
}
#[inline]
fn parse_properties_drawer(input: &str) -> IResult<&str, PropertiesMap<'_>, ()> {
let (input, (drawer, content)) = parse_drawer_without_blank(input.trim_start())?;
if drawer.name != "PROPERTIES" {
return Err(Err::Error(make_error(input, ErrorKind::Tag)));
}
let (_, map) = fold_many0(
parse_node_property,
PropertiesMap::new,
|mut acc: PropertiesMap, (name, value)| {
acc.pairs.push((name.into(), value.into()));
acc
},
)(content)?;
Ok((input, map))
}
#[inline]
fn parse_node_property(input: &str) -> IResult<&str, (&str, &str), ()> {
let (input, _) = blank_lines_count(input)?;
let input = input.trim_start();
let (input, name) = map(delimited(tag(":"), take_until(":"), tag(":")), |s: &str| {
s.trim_end_matches('+')
})(input)?;
let (input, value) = line(input)?;
Ok((input, (name, value.trim())))
}
#[test]
fn parse_title_() {
use crate::config::DEFAULT_CONFIG;
assert_eq!(
parse_title("**** DONE [#A] COMMENT Title :tag:a2%:", &DEFAULT_CONFIG),
Ok((
"",
(
Title {
level: 4,
keyword: Some("DONE".into()),
priority: Some('A'),
raw: "COMMENT Title".into(),
tags: vec!["tag".into(), "a2%".into()],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"COMMENT Title"
)
))
);
assert_eq!(
parse_title("**** ToDO [#A] COMMENT Title", &DEFAULT_CONFIG),
Ok((
"",
(
Title {
level: 4,
keyword: None,
priority: None,
raw: "ToDO [#A] COMMENT Title".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"ToDO [#A] COMMENT Title"
)
))
);
assert_eq!(
parse_title("**** T0DO [#A] COMMENT Title", &DEFAULT_CONFIG),
Ok((
"",
(
Title {
level: 4,
keyword: None,
priority: None,
raw: "T0DO [#A] COMMENT Title".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"T0DO [#A] COMMENT Title"
)
))
);
assert_eq!(
parse_title("**** DONE [#1] COMMENT Title", &DEFAULT_CONFIG),
Ok((
"",
(
Title {
level: 4,
keyword: Some("DONE".into()),
priority: None,
raw: "[#1] COMMENT Title".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"[#1] COMMENT Title"
)
))
);
assert_eq!(
parse_title("**** DONE [#a] COMMENT Title", &DEFAULT_CONFIG),
Ok((
"",
(
Title {
level: 4,
keyword: Some("DONE".into()),
priority: None,
raw: "[#a] COMMENT Title".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"[#a] COMMENT Title"
)
))
);
// https://github.com/PoiScript/orgize/issues/20
assert_eq!(
parse_title("** DONE [#B]::", &DEFAULT_CONFIG),
Ok((
"",
(
Title {
level: 2,
keyword: Some("DONE".into()),
priority: None,
raw: "[#B]::".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"[#B]::"
)
))
);
assert_eq!(
parse_title("**** Title :tag:a2%", &DEFAULT_CONFIG),
Ok((
"",
(
Title {
level: 4,
keyword: None,
priority: None,
raw: "Title :tag:a2%".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"Title :tag:a2%"
)
))
);
assert_eq!(
parse_title("**** Title tag:a2%:", &DEFAULT_CONFIG),
Ok((
"",
(
Title {
level: 4,
keyword: None,
priority: None,
raw: "Title tag:a2%:".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"Title tag:a2%:"
)
))
);
assert_eq!(
parse_title(
"**** DONE Title",
&ParseConfig {
todo_keywords: (vec![], vec![]),
..Default::default()
}
),
Ok((
"",
(
Title {
level: 4,
keyword: None,
priority: None,
raw: "DONE Title".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"DONE Title"
)
))
);
assert_eq!(
parse_title(
"**** TASK [#A] Title",
&ParseConfig {
todo_keywords: (vec!["TASK".to_string()], vec![]),
..Default::default()
}
),
Ok((
"",
(
Title {
level: 4,
keyword: Some("TASK".into()),
priority: Some('A'),
raw: "Title".into(),
tags: vec![],
planning: None,
properties: PropertiesMap::new(),
post_blank: 0,
},
"Title"
)
))
);
}
#[test]
fn parse_properties_drawer_() {
assert_eq!(
parse_properties_drawer(" :PROPERTIES:\n :CUSTOM_ID: id\n :END:"),
Ok((
"",
vec![("CUSTOM_ID".into(), "id".into())]
.into_iter()
.collect::<PropertiesMap>()
))
)
}
#[test]
#[cfg(feature = "indexmap")]
fn preserve_properties_drawer_order() {
let mut vec = Vec::default();
// Use a large number of properties to reduce false pass rate, since HashMap
// is non-deterministic. There are roughly 10^18 possible derangements of this sequence.
for i in 0..20 {
// Avoid alphabetic or numeric order.
let j = (i + 7) % 20;
vec.push((
Cow::Owned(format!(
"{}{}",
if i % 3 == 0 {
"FOO"
} else if i % 3 == 1 {
"QUX"
} else {
"BAR"
},
j
)),
Cow::Owned(i.to_string()),
));
}
let mut s = String::default();
for (k, v) in &vec {
s += &format!(" :{}: {}\n", k, v);
}
let drawer = format!(" :PROPERTIES:\n{}:END:\n", &s);
let map = parse_properties_drawer(&drawer).unwrap().1.into_index_map();
// indexmap should be in the same order as vector
for (left, right) in vec.iter().zip(map) {
assert_eq!(left, &right);
}
}

210
src/export/forward.rs Normal file
View file

@ -0,0 +1,210 @@
/// Forward handler method implement to other handler
///
/// This macros is commonly used if you want to extend
/// some builtin handlers like HtmlExport.
///
/// ```rust
/// use orgize::{
/// ast::HeadlineTitle,
/// export::{HtmlExport, TraversalContext, Traverser},
/// forward_handler,
/// rowan::{ast::AstNode, WalkEvent},
/// Org,
/// };
/// use slugify::slugify;
/// use std::cmp::min;
///
/// #[derive(Default)]
/// struct SlugifyTitleHandler(pub HtmlExport);
///
/// // AsMut trait is required
/// impl AsMut<HtmlExport> for SlugifyTitleHandler {
/// fn as_mut(&mut self) -> &mut HtmlExport {
/// &mut self.0
/// }
/// }
///
/// impl Traverser for SlugifyTitleHandler {
/// fn headline_title(&mut self, event: WalkEvent<&HeadlineTitle>, _ctx: &mut TraversalContext) {
/// match event {
/// WalkEvent::Enter(title) => {
/// let level = title.headline().and_then(|h| h.level()).unwrap_or(1);
/// let level = min(level, 6);
/// let raw = title.syntax().to_string();
/// self.0.output += &format!("<h{level}><a id=\"{0}\" href=\"#{0}\">", slugify!(&raw));
/// }
/// WalkEvent::Leave(title) => {
/// let level = title.headline().and_then(|h| h.level()).unwrap_or(1);
/// let level = min(level, 6);
/// self.0.output += &format!("</a></h{level}>");
/// }
/// }
/// }
///
/// forward_handler! {
/// HtmlExport,
/// link text document headline paragraph section rule comment
/// inline_src inline_call code bold verbatim italic strike underline list list_item list_item_tag
/// special_block quote_block center_block verse_block comment_block example_block export_block
/// source_block babel_call clock cookie radio_target drawer dyn_block fn_def fn_ref macros
/// snippet timestamp target fixed_width org_table org_table_row org_table_cell list_item_content
/// }
/// }
///
/// let mut handler = SlugifyTitleHandler::default();
/// Org::parse("* hello world!").traverse(&mut handler);
/// assert_eq!(handler.0.finish(), r##"<main><h1><a id="hello-world" href="#hello-world">hello world!</a></h1></main>"##);
/// ```
#[macro_export(local_inner_macros)]
macro_rules! forward_handler {
($handler:ty, $($func:ident)*) => {
$(
forward_handler!(@method $handler, $func);
)*
};
(@method $handler:ty, text) => {
forward_handler!(@method $handler, text, ::orgize::SyntaxToken);
};
(@method $handler:ty, document) => {
forward_handler!(@method $handler, document, WalkEvent<&::orgize::ast::Document>);
};
(@method $handler:ty, headline) => {
forward_handler!(@method $handler, headline, WalkEvent<&::orgize::ast::Headline>);
};
(@method $handler:ty, paragraph) => {
forward_handler!(@method $handler, paragraph, WalkEvent<&::orgize::ast::Paragraph>);
};
(@method $handler:ty, section) => {
forward_handler!(@method $handler, section, WalkEvent<&::orgize::ast::Section>);
};
(@method $handler:ty, rule) => {
forward_handler!(@method $handler, rule, WalkEvent<&::orgize::ast::Rule>);
};
(@method $handler:ty, comment) => {
forward_handler!(@method $handler, comment, WalkEvent<&::orgize::ast::Comment>);
};
(@method $handler:ty, inline_src) => {
forward_handler!(@method $handler, inline_src, WalkEvent<&::orgize::ast::InlineSrc>);
};
(@method $handler:ty, inline_call) => {
forward_handler!(@method $handler, inline_call, WalkEvent<&::orgize::ast::InlineCall>);
};
(@method $handler:ty, code) => {
forward_handler!(@method $handler, code, WalkEvent<&::orgize::ast::Code>);
};
(@method $handler:ty, bold) => {
forward_handler!(@method $handler, bold, WalkEvent<&::orgize::ast::Bold>);
};
(@method $handler:ty, verbatim) => {
forward_handler!(@method $handler, verbatim, WalkEvent<&::orgize::ast::Verbatim>);
};
(@method $handler:ty, italic) => {
forward_handler!(@method $handler, italic, WalkEvent<&::orgize::ast::Italic>);
};
(@method $handler:ty, strike) => {
forward_handler!(@method $handler, strike, WalkEvent<&::orgize::ast::Strike>);
};
(@method $handler:ty, underline) => {
forward_handler!(@method $handler, underline, WalkEvent<&::orgize::ast::Underline>);
};
(@method $handler:ty, list) => {
forward_handler!(@method $handler, list, WalkEvent<&::orgize::ast::List>);
};
(@method $handler:ty, list_item) => {
forward_handler!(@method $handler, list_item, WalkEvent<&::orgize::ast::ListItem>);
};
(@method $handler:ty, list_item_tag) => {
forward_handler!(@method $handler, list_item_tag, WalkEvent<&::orgize::ast::ListItemTag>);
};
(@method $handler:ty, list_item_content) => {
forward_handler!(@method $handler, list_item_content, WalkEvent<&::orgize::ast::ListItemContent>);
};
(@method $handler:ty, special_block) => {
forward_handler!(@method $handler, special_block, WalkEvent<&::orgize::ast::SpecialBlock>);
};
(@method $handler:ty, quote_block) => {
forward_handler!(@method $handler, quote_block, WalkEvent<&::orgize::ast::QuoteBlock>);
};
(@method $handler:ty, center_block) => {
forward_handler!(@method $handler, center_block, WalkEvent<&::orgize::ast::CenterBlock>);
};
(@method $handler:ty, verse_block) => {
forward_handler!(@method $handler, verse_block, WalkEvent<&::orgize::ast::VerseBlock>);
};
(@method $handler:ty, comment_block) => {
forward_handler!(@method $handler, comment_block, WalkEvent<&::orgize::ast::CommentBlock>);
};
(@method $handler:ty, example_block) => {
forward_handler!(@method $handler, example_block, WalkEvent<&::orgize::ast::ExampleBlock>);
};
(@method $handler:ty, export_block) => {
forward_handler!(@method $handler, export_block, WalkEvent<&::orgize::ast::ExportBlock>);
};
(@method $handler:ty, source_block) => {
forward_handler!(@method $handler, source_block, WalkEvent<&::orgize::ast::SourceBlock>);
};
(@method $handler:ty, babel_call) => {
forward_handler!(@method $handler, babel_call, WalkEvent<&::orgize::ast::BabelCall>);
};
(@method $handler:ty, clock) => {
forward_handler!(@method $handler, clock, WalkEvent<&::orgize::ast::Clock>);
};
(@method $handler:ty, cookie) => {
forward_handler!(@method $handler, cookie, WalkEvent<&::orgize::ast::Cookie>);
};
(@method $handler:ty, radio_target) => {
forward_handler!(@method $handler, radio_target, WalkEvent<&::orgize::ast::RadioTarget>);
};
(@method $handler:ty, drawer) => {
forward_handler!(@method $handler, drawer, WalkEvent<&::orgize::ast::Drawer>);
};
(@method $handler:ty, dyn_block) => {
forward_handler!(@method $handler, dyn_block, WalkEvent<&::orgize::ast::DynBlock>);
};
(@method $handler:ty, fn_def) => {
forward_handler!(@method $handler, fn_def, WalkEvent<&::orgize::ast::FnDef>);
};
(@method $handler:ty, fn_ref) => {
forward_handler!(@method $handler, fn_ref, WalkEvent<&::orgize::ast::FnRef>);
};
(@method $handler:ty, macros) => {
forward_handler!(@method $handler, macros, WalkEvent<&::orgize::ast::Macros>);
};
(@method $handler:ty, snippet) => {
forward_handler!(@method $handler, snippet, WalkEvent<&::orgize::ast::Snippet>);
};
(@method $handler:ty, timestamp) => {
forward_handler!(@method $handler, timestamp, WalkEvent<&::orgize::ast::Timestamp>);
};
(@method $handler:ty, target) => {
forward_handler!(@method $handler, target, WalkEvent<&::orgize::ast::Target>);
};
(@method $handler:ty, fixed_width) => {
forward_handler!(@method $handler, fixed_width, WalkEvent<&::orgize::ast::FixedWidth>);
};
(@method $handler:ty, headline_title) => {
forward_handler!(@method $handler, headline_title, WalkEvent<&::orgize::ast::HeadlineTitle>);
};
(@method $handler:ty, org_table) => {
forward_handler!(@method $handler, org_table, WalkEvent<&::orgize::ast::OrgTable>);
};
(@method $handler:ty, org_table_row) => {
forward_handler!(@method $handler, org_table_row, WalkEvent<&::orgize::ast::OrgTableRow>);
};
(@method $handler:ty, org_table_cell) => {
forward_handler!(@method $handler, org_table_cell, WalkEvent<&::orgize::ast::OrgTableCell>);
};
(@method $handler:ty, link) => {
forward_handler!(@method $handler, link, WalkEvent<&::orgize::ast::Link>);
};
(@method $handler:ty, $x:ident) => {
std::compile_error!(std::concat!(std::stringify!($x), " is not a method"));
};
(@method $handler:ty, $name:ident, $type:ty) => {
fn $name(&mut self, item: $type, ctx: &mut ::orgize::export::TraversalContext) {
<Self as AsMut<$handler>>::as_mut(self).$name(item, ctx)
}
};
}

View file

@ -1,10 +1,10 @@
use rowan::WalkEvent;
use std::fmt;
use std::io::{Error, Result as IOResult, Write};
use jetscii::{bytes, BytesConst};
use crate::elements::{Element, Table, TableCell, TableRow, Timestamp};
use crate::export::write_datetime;
use super::TraversalContext;
use super::Traverser;
use crate::ast::*;
use crate::syntax::SyntaxToken;
/// A wrapper for escaping sensitive characters in html.
///
@ -26,11 +26,7 @@ impl<S: AsRef<str>> fmt::Display for HtmlEscape<S> {
let content = self.0.as_ref();
let bytes = content.as_bytes();
lazy_static::lazy_static! {
static ref ESCAPE_BYTES: BytesConst = bytes!(b'<', b'>', b'&', b'\'', b'"');
}
while let Some(off) = ESCAPE_BYTES.find(&bytes[pos..]) {
while let Some(off) = jetscii::bytes!(b'<', b'>', b'&', b'\'', b'"').find(&bytes[pos..]) {
write!(f, "{}", &content[pos..pos + off])?;
pos += off + 1;
@ -49,349 +45,359 @@ impl<S: AsRef<str>> fmt::Display for HtmlEscape<S> {
}
}
pub trait HtmlHandler<E: From<Error>>: Default {
fn start<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E>;
fn end<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E>;
}
/// Default Html Handler
#[derive(Default)]
pub struct DefaultHtmlHandler;
pub struct HtmlExport {
pub output: String,
in_descriptive_list: Vec<bool>,
}
impl HtmlHandler<Error> for DefaultHtmlHandler {
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> IOResult<()> {
match element {
// container elements
Element::SpecialBlock(_) => (),
Element::QuoteBlock(_) => write!(w, "<blockquote>")?,
Element::CenterBlock(_) => write!(w, "<div class=\"center\">")?,
Element::VerseBlock(_) => write!(w, "<p class=\"verse\">")?,
Element::Bold => write!(w, "<b>")?,
Element::Document { .. } => write!(w, "<main>")?,
Element::DynBlock(_dyn_block) => (),
Element::Headline { .. } => (),
Element::List(list) => {
if list.ordered {
write!(w, "<ol>")?;
} else {
write!(w, "<ul>")?;
}
}
Element::Italic => write!(w, "<i>")?,
Element::ListItem(_) => write!(w, "<li>")?,
Element::Paragraph { .. } => write!(w, "<p>")?,
Element::Section => write!(w, "<section>")?,
Element::Strike => write!(w, "<s>")?,
Element::Underline => write!(w, "<u>")?,
// non-container elements
Element::CommentBlock(_) => (),
Element::ExampleBlock(block) => write!(
w,
"<pre class=\"example\">{}</pre>",
HtmlEscape(&block.contents)
)?,
Element::ExportBlock(block) => {
if block.data.eq_ignore_ascii_case("HTML") {
write!(w, "{}", block.contents)?
}
}
Element::SourceBlock(block) => {
if block.language.is_empty() {
write!(
w,
"<pre class=\"example\">{}</pre>",
HtmlEscape(&block.contents)
)?;
} else {
write!(
w,
"<div class=\"org-src-container\"><pre class=\"src src-{}\">{}</pre></div>",
block.language,
HtmlEscape(&block.contents)
)?;
}
}
Element::BabelCall(_) => (),
Element::InlineSrc(inline_src) => write!(
w,
"<code class=\"src src-{}\">{}</code>",
inline_src.lang,
HtmlEscape(&inline_src.body)
)?,
Element::Code { value } => write!(w, "<code>{}</code>", HtmlEscape(value))?,
Element::FnRef(_fn_ref) => (),
Element::InlineCall(_) => (),
Element::Link(link) => write!(
w,
"<a href=\"{}\">{}</a>",
HtmlEscape(&link.path),
HtmlEscape(link.desc.as_ref().unwrap_or(&link.path)),
)?,
Element::Macros(_macros) => (),
Element::RadioTarget => (),
Element::Snippet(snippet) => {
if snippet.name.eq_ignore_ascii_case("HTML") {
write!(w, "{}", snippet.value)?;
}
}
Element::Target(_target) => (),
Element::Text { value } => write!(w, "{}", HtmlEscape(value))?,
Element::Timestamp(timestamp) => {
write!(
&mut w,
"<span class=\"timestamp-wrapper\"><span class=\"timestamp\">"
)?;
match timestamp {
Timestamp::Active { start, .. } => {
write_datetime(&mut w, "&lt;", start, "&gt;")?;
}
Timestamp::Inactive { start, .. } => {
write_datetime(&mut w, "[", start, "]")?;
}
Timestamp::ActiveRange { start, end, .. } => {
write_datetime(&mut w, "&lt;", start, "&gt;&#x2013;")?;
write_datetime(&mut w, "&lt;", end, "&gt;")?;
}
Timestamp::InactiveRange { start, end, .. } => {
write_datetime(&mut w, "[", start, "]&#x2013;")?;
write_datetime(&mut w, "[", end, "]")?;
}
Timestamp::Diary { value } => {
write!(&mut w, "&lt;%%({})&gt;", HtmlEscape(value))?
}
}
write!(&mut w, "</span></span>")?;
}
Element::Verbatim { value } => write!(&mut w, "<code>{}</code>", HtmlEscape(value))?,
Element::FnDef(_fn_def) => (),
Element::Clock(_clock) => (),
Element::Comment(_) => (),
Element::FixedWidth(fixed_width) => write!(
w,
"<pre class=\"example\">{}</pre>",
HtmlEscape(&fixed_width.value)
)?,
Element::Keyword(_keyword) => (),
Element::Drawer(_drawer) => (),
Element::Rule(_) => write!(w, "<hr>")?,
Element::Cookie(cookie) => write!(w, "<code>{}</code>", cookie.value)?,
Element::Title(title) => {
write!(w, "<h{}>", if title.level <= 6 { title.level } else { 6 })?;
}
Element::Table(Table::TableEl { .. }) => (),
Element::Table(Table::Org { has_header, .. }) => {
write!(w, "<table>")?;
if *has_header {
write!(w, "<thead>")?;
} else {
write!(w, "<tbody>")?;
}
}
Element::TableRow(row) => match row {
TableRow::Body => write!(w, "<tr>")?,
TableRow::BodyRule => write!(w, "</tbody><tbody>")?,
TableRow::Header => write!(w, "<tr>")?,
TableRow::HeaderRule => write!(w, "</thead><tbody>")?,
},
Element::TableCell(cell) => match cell {
TableCell::Body => write!(w, "<td>")?,
TableCell::Header => write!(w, "<th>")?,
},
}
Ok(())
}
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> IOResult<()> {
match element {
// container elements
Element::SpecialBlock(_) => (),
Element::QuoteBlock(_) => write!(w, "</blockquote>")?,
Element::CenterBlock(_) => write!(w, "</div>")?,
Element::VerseBlock(_) => write!(w, "</p>")?,
Element::Bold => write!(w, "</b>")?,
Element::Document { .. } => write!(w, "</main>")?,
Element::DynBlock(_dyn_block) => (),
Element::Headline { .. } => (),
Element::List(list) => {
if list.ordered {
write!(w, "</ol>")?;
} else {
write!(w, "</ul>")?;
}
}
Element::Italic => write!(w, "</i>")?,
Element::ListItem(_) => write!(w, "</li>")?,
Element::Paragraph { .. } => write!(w, "</p>")?,
Element::Section => write!(w, "</section>")?,
Element::Strike => write!(w, "</s>")?,
Element::Underline => write!(w, "</u>")?,
Element::Title(title) => {
write!(w, "</h{}>", if title.level <= 6 { title.level } else { 6 })?
}
Element::Table(Table::TableEl { .. }) => (),
Element::Table(Table::Org { .. }) => {
write!(w, "</tbody></table>")?;
}
Element::TableRow(TableRow::Body) | Element::TableRow(TableRow::Header) => {
write!(w, "</tr>")?;
}
Element::TableCell(cell) => match cell {
TableCell::Body => write!(w, "</td>")?,
TableCell::Header => write!(w, "</th>")?,
},
// non-container elements
_ => debug_assert!(!element.is_container()),
}
Ok(())
impl HtmlExport {
pub fn finish(self) -> String {
self.output
}
}
#[cfg(feature = "syntect")]
mod syntect_handler {
use super::*;
use std::marker::PhantomData;
use syntect::{
easy::HighlightLines,
highlighting::ThemeSet,
html::{styled_line_to_highlighted_html, IncludeBackground},
parsing::SyntaxSet,
};
/// Syntect Html Handler
///
/// Simple Usage:
///
/// ```rust
/// use orgize::Org;
/// use orgize::export::{DefaultHtmlHandler, SyntectHtmlHandler};
///
/// let mut handler = SyntectHtmlHandler::new(DefaultHtmlHandler);
/// let org = Org::parse("src_rust{println!(\"Hello\")}");
///
/// let mut vec = vec![];
///
/// org.write_html_custom(&mut vec, &mut handler).unwrap();
/// ```
///
/// Customize:
///
/// ```rust,no_run
/// // orgize has re-exported the whole syntect crate
/// use orgize::syntect::parsing::SyntaxSet;
/// use orgize::export::{DefaultHtmlHandler, SyntectHtmlHandler};
///
/// let mut handler = SyntectHtmlHandler {
/// syntax_set: {
/// let set = SyntaxSet::load_defaults_newlines();
/// let mut builder = set.into_builder();
/// // add extra language syntax
/// builder.add_from_folder("path/to/syntax/dir", true).unwrap();
/// builder.build()
/// },
/// // specify theme
/// theme: String::from("Solarized (dark)"),
/// inner: DefaultHtmlHandler,
/// ..Default::default()
/// };
///
/// // Make sure to check if theme presents or it will panic at runtime
/// if handler.theme_set.themes.contains_key("dont-exists") {
///
/// }
/// ```
pub struct SyntectHtmlHandler<E: From<Error>, H: HtmlHandler<E>> {
/// syntax set, default is `SyntaxSet::load_defaults_newlines()`
pub syntax_set: SyntaxSet,
/// theme set, default is `ThemeSet::load_defaults()`
pub theme_set: ThemeSet,
/// theme used for highlighting, default is `"InspiredGitHub"`
pub theme: String,
/// inner html handler
pub inner: H,
/// background color, default is `IncludeBackground::No`
pub background: IncludeBackground,
/// handler error type
pub error_type: PhantomData<E>,
impl Traverser for HtmlExport {
#[tracing::instrument(skip(self, _ctx))]
fn text(&mut self, token: SyntaxToken, _ctx: &mut TraversalContext) {
self.output += &HtmlEscape(token.text()).to_string();
}
impl<E: From<Error>, H: HtmlHandler<E>> SyntectHtmlHandler<E, H> {
pub fn new(inner: H) -> Self {
SyntectHtmlHandler {
inner,
..Default::default()
#[tracing::instrument(skip(self, _ctx))]
fn document(&mut self, event: WalkEvent<&Document>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<main>",
WalkEvent::Leave(_) => "</main>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn list(&mut self, event: WalkEvent<&List>, _ctx: &mut TraversalContext) {
match event {
WalkEvent::Enter(list) => {
self.output += if list.is_ordered() {
self.in_descriptive_list.push(false);
"<ol>"
} else if list.is_descriptive() {
self.in_descriptive_list.push(true);
"<dl>"
} else {
self.in_descriptive_list.push(false);
"<ul>"
};
}
}
fn highlight(&self, language: Option<&str>, content: &str) -> String {
let mut highlighter = HighlightLines::new(
language
.and_then(|lang| self.syntax_set.find_syntax_by_token(lang))
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()),
&self.theme_set.themes[&self.theme],
);
let regions = highlighter.highlight(content, &self.syntax_set);
styled_line_to_highlighted_html(&regions[..], self.background)
}
}
impl<E: From<Error>, H: HtmlHandler<E>> Default for SyntectHtmlHandler<E, H> {
fn default() -> Self {
SyntectHtmlHandler {
syntax_set: SyntaxSet::load_defaults_newlines(),
theme_set: ThemeSet::load_defaults(),
theme: String::from("InspiredGitHub"),
inner: H::default(),
background: IncludeBackground::No,
error_type: PhantomData,
WalkEvent::Leave(list) => {
self.output += if list.is_ordered() {
"</ol>"
} else if let Some(true) = self.in_descriptive_list.last() {
"</dl>"
} else {
"</ul>"
};
self.in_descriptive_list.pop();
}
};
}
#[tracing::instrument(skip(self, _ctx))]
fn list_item(&mut self, event: WalkEvent<&ListItem>, _ctx: &mut TraversalContext) {
if !self.in_descriptive_list.last().copied().unwrap_or_default() {
self.output += match event {
WalkEvent::Enter(_) => "<li>",
WalkEvent::Leave(_) => "</li>",
};
}
}
impl<E: From<Error>, H: HtmlHandler<E>> HtmlHandler<E> for SyntectHtmlHandler<E, H> {
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), E> {
match element {
Element::InlineSrc(inline_src) => write!(
w,
"<code>{}</code>",
self.highlight(Some(&inline_src.lang), &inline_src.body)
)?,
Element::SourceBlock(block) => {
if block.language.is_empty() {
write!(w, "<pre class=\"example\">{}</pre>", block.contents)?;
} else {
write!(
w,
"<div class=\"org-src-container\"><pre class=\"src src-{}\">{}</pre></div>",
block.language,
self.highlight(Some(&block.language), &block.contents)
)?;
}
#[tracing::instrument(skip(self, _ctx))]
fn list_item_content(
&mut self,
event: WalkEvent<&ListItemContent>,
_ctx: &mut TraversalContext,
) {
if self.in_descriptive_list.last().copied().unwrap_or_default() {
self.output += match event {
WalkEvent::Enter(_) => "<dd>",
WalkEvent::Leave(_) => "</dd>",
};
}
}
#[tracing::instrument(skip(self, _ctx))]
fn list_item_tag(&mut self, event: WalkEvent<&ListItemTag>, _ctx: &mut TraversalContext) {
if self.in_descriptive_list.last().copied().unwrap_or_default() {
self.output += match event {
WalkEvent::Enter(_) => "<dt>",
WalkEvent::Leave(_) => "</dt>",
};
}
}
#[tracing::instrument(skip(self, _ctx))]
fn paragraph(&mut self, event: WalkEvent<&Paragraph>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<p>",
WalkEvent::Leave(_) => "</p>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn section(&mut self, event: WalkEvent<&Section>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<section>",
WalkEvent::Leave(_) => "</section>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn fixed_width(&mut self, event: WalkEvent<&FixedWidth>, _ctx: &mut TraversalContext) {
if let WalkEvent::Enter(_f) = event {
// self.output += f.text();
};
}
#[tracing::instrument(skip(self, ctx))]
fn snippet(&mut self, event: WalkEvent<&Snippet>, ctx: &mut TraversalContext) {
if let WalkEvent::Enter(snippet) = event {
if matches!(snippet.name(), Some(name) if name.text().eq_ignore_ascii_case("html")) {
if let Some(value) = snippet.value() {
self.output += value.text()
}
Element::FixedWidth(fixed_width) => write!(
w,
"<pre class=\"example\">{}</pre>",
self.highlight(None, &fixed_width.value)
)?,
Element::ExampleBlock(block) => write!(
w,
"<pre class=\"example\">{}</pre>",
self.highlight(None, &block.contents)
)?,
_ => self.inner.start(w, element)?,
}
Ok(())
}
return ctx.skip();
};
}
fn end<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E> {
self.inner.end(w, element)
#[tracing::instrument(skip(self, _ctx))]
fn headline_title(&mut self, event: WalkEvent<&HeadlineTitle>, _ctx: &mut TraversalContext) {
self.output += &match event {
WalkEvent::Enter(title) => {
let level = title
.headline()
.and_then(|hdl| hdl.level())
.map(|lvl| std::cmp::min(lvl, 6))
.unwrap_or(1);
format!("<h{level}>")
}
WalkEvent::Leave(title) => {
let level = title
.headline()
.and_then(|hdl| hdl.level())
.map(|lvl| std::cmp::min(lvl, 6))
.unwrap_or(1);
format!("</h{level}>")
}
};
}
#[tracing::instrument(skip(self, _ctx))]
fn italic(&mut self, event: WalkEvent<&Italic>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<i>",
WalkEvent::Leave(_) => "</i>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn bold(&mut self, event: WalkEvent<&Bold>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<b>",
WalkEvent::Leave(_) => "</b>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn strike(&mut self, event: WalkEvent<&Strike>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<s>",
WalkEvent::Leave(_) => "</s>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn underline(&mut self, event: WalkEvent<&Underline>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<u>",
WalkEvent::Leave(_) => "</u>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn verbatim(&mut self, event: WalkEvent<&Verbatim>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<code>",
WalkEvent::Leave(_) => "</code>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn code(&mut self, event: WalkEvent<&Code>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<code>",
WalkEvent::Leave(_) => "</code>",
};
}
#[tracing::instrument(skip(self, ctx))]
fn rule(&mut self, event: WalkEvent<&Rule>, ctx: &mut TraversalContext) {
if let WalkEvent::Enter(_) = event {
self.output += "<hr/>"
};
ctx.skip()
}
#[tracing::instrument(skip(self, ctx))]
fn link(&mut self, event: WalkEvent<&Link>, ctx: &mut TraversalContext) {
match event {
WalkEvent::Enter(link) => {
let path = link.path();
let path = path.as_ref().map(|path| path.text()).unwrap_or_default();
if link.is_image() {
self.output += &format!(r#"<img src="{}">"#, HtmlEscape(path));
return ctx.skip();
}
self.output += &format!(r#"<a href="{}">"#, HtmlEscape(path));
if !link.has_description() {
self.output += &HtmlEscape(path).to_string();
self.output += "</a>";
return ctx.skip();
}
}
WalkEvent::Leave(_) => {
self.output += "</a>";
}
}
}
}
#[cfg(feature = "syntect")]
pub use syntect_handler::SyntectHtmlHandler;
#[tracing::instrument(skip(self, _ctx))]
fn quote_block(&mut self, event: WalkEvent<&QuoteBlock>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<blockquote>",
WalkEvent::Leave(_) => "</blockquote>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn verse_block(&mut self, event: WalkEvent<&VerseBlock>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<p class=\"verse\">",
WalkEvent::Leave(_) => "</p>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn example_block(&mut self, event: WalkEvent<&ExampleBlock>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<pre class=\"example\">",
WalkEvent::Leave(_) => "</pre>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn center_block(&mut self, event: WalkEvent<&CenterBlock>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<div class=\"center\">",
WalkEvent::Leave(_) => "</div>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn org_table(&mut self, event: WalkEvent<&OrgTable>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<table><tbody>",
WalkEvent::Leave(_) => "</tbody></table>",
};
}
#[tracing::instrument(skip(self, ctx))]
fn org_table_row(&mut self, event: WalkEvent<&OrgTableRow>, ctx: &mut TraversalContext) {
if match event {
WalkEvent::Enter(n) | WalkEvent::Leave(n) => n.is_rule(),
} {
return ctx.skip();
}
self.output += match event {
WalkEvent::Enter(_) => "<tr>",
WalkEvent::Leave(_) => "</tr>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn org_table_cell(&mut self, event: WalkEvent<&OrgTableCell>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<td>",
WalkEvent::Leave(_) => "</td>",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn comment(&mut self, event: WalkEvent<&Comment>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<!--",
WalkEvent::Leave(_) => "-->",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn comment_block(&mut self, event: WalkEvent<&CommentBlock>, _ctx: &mut TraversalContext) {
self.output += match event {
WalkEvent::Enter(_) => "<!--",
WalkEvent::Leave(_) => "-->",
};
}
#[tracing::instrument(skip(self, _ctx))]
fn headline(&mut self, _event: WalkEvent<&Headline>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn inline_src(&mut self, _event: WalkEvent<&InlineSrc>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn inline_call(&mut self, _event: WalkEvent<&InlineCall>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn special_block(&mut self, _event: WalkEvent<&SpecialBlock>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn export_block(&mut self, _event: WalkEvent<&ExportBlock>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn source_block(&mut self, _event: WalkEvent<&SourceBlock>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn babel_call(&mut self, _event: WalkEvent<&BabelCall>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn clock(&mut self, _event: WalkEvent<&Clock>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn cookie(&mut self, _event: WalkEvent<&Cookie>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn radio_target(&mut self, _event: WalkEvent<&RadioTarget>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn drawer(&mut self, _event: WalkEvent<&Drawer>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn dyn_block(&mut self, _event: WalkEvent<&DynBlock>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn fn_def(&mut self, _event: WalkEvent<&FnDef>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn fn_ref(&mut self, _event: WalkEvent<&FnRef>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn macros(&mut self, _event: WalkEvent<&Macros>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn timestamp(&mut self, _event: WalkEvent<&Timestamp>, _ctx: &mut TraversalContext) {}
#[tracing::instrument(skip(self, _ctx))]
fn target(&mut self, _event: WalkEvent<&Target>, _ctx: &mut TraversalContext) {}
}

View file

@ -1,31 +1,8 @@
//! Export `Org` struct to various formats.
mod forward;
mod html;
mod org;
mod traverse;
#[cfg(feature = "syntect")]
pub use html::SyntectHtmlHandler;
pub use html::{DefaultHtmlHandler, HtmlEscape, HtmlHandler};
pub use org::{DefaultOrgHandler, OrgHandler};
use std::io::{Error, Write};
use crate::elements::Datetime;
pub(crate) fn write_datetime<W: Write>(
mut w: W,
start: &str,
datetime: &Datetime,
end: &str,
) -> Result<(), Error> {
write!(w, "{}", start)?;
write!(
w,
"{}-{:02}-{:02} {}",
datetime.year, datetime.month, datetime.day, datetime.dayname
)?;
if let (Some(hour), Some(minute)) = (datetime.hour, datetime.minute) {
write!(w, " {:02}:{:02}", hour, minute)?;
}
write!(w, "{}", end)
}
pub use html::{HtmlEscape, HtmlExport};
pub use traverse::{TraversalContext, Traverser};

View file

@ -1,321 +0,0 @@
use std::io::{Error, Result as IOResult, Write};
use crate::elements::{Clock, Element, Table, Timestamp};
use crate::export::write_datetime;
pub trait OrgHandler<E: From<Error>>: Default {
fn start<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E>;
fn end<W: Write>(&mut self, w: W, element: &Element) -> Result<(), E>;
}
#[derive(Default)]
pub struct DefaultOrgHandler;
impl OrgHandler<Error> for DefaultOrgHandler {
fn start<W: Write>(&mut self, mut w: W, element: &Element) -> IOResult<()> {
match element {
// container elements
Element::SpecialBlock(block) => {
writeln!(w, "#+BEGIN_{}", block.name)?;
write_blank_lines(&mut w, block.pre_blank)?;
}
Element::QuoteBlock(block) => {
writeln!(&mut w, "#+BEGIN_QUOTE")?;
write_blank_lines(&mut w, block.pre_blank)?;
}
Element::CenterBlock(block) => {
writeln!(&mut w, "#+BEGIN_CENTER")?;
write_blank_lines(&mut w, block.pre_blank)?;
}
Element::VerseBlock(block) => {
writeln!(&mut w, "#+BEGIN_VERSE")?;
write_blank_lines(&mut w, block.pre_blank)?;
}
Element::Bold => write!(w, "*")?,
Element::Document { pre_blank } => {
write_blank_lines(w, *pre_blank)?;
}
Element::DynBlock(dyn_block) => {
write!(&mut w, "#+BEGIN: {}", dyn_block.block_name)?;
if let Some(parameters) = &dyn_block.arguments {
write!(&mut w, " {}", parameters)?;
}
write_blank_lines(&mut w, dyn_block.pre_blank + 1)?;
}
Element::Headline { .. } => (),
Element::List(_list) => (),
Element::Italic => write!(w, "/")?,
Element::ListItem(list_item) => {
for _ in 0..list_item.indent {
write!(&mut w, " ")?;
}
write!(&mut w, "{}", list_item.bullet)?;
}
Element::Paragraph { .. } => (),
Element::Section => (),
Element::Strike => write!(w, "+")?,
Element::Underline => write!(w, "_")?,
Element::Drawer(drawer) => {
writeln!(&mut w, ":{}:", drawer.name)?;
write_blank_lines(&mut w, drawer.pre_blank)?;
}
// non-container elements
Element::CommentBlock(block) => {
writeln!(&mut w, "#+BEGIN_COMMENT")?;
write!(&mut w, "{}", block.contents)?;
writeln!(&mut w, "#+END_COMMENT")?;
write_blank_lines(&mut w, block.post_blank)?;
}
Element::ExampleBlock(block) => {
writeln!(&mut w, "#+BEGIN_EXAMPLE")?;
write!(&mut w, "{}", block.contents)?;
writeln!(&mut w, "#+END_EXAMPLE")?;
write_blank_lines(&mut w, block.post_blank)?;
}
Element::ExportBlock(block) => {
writeln!(&mut w, "#+BEGIN_EXPORT {}", block.data)?;
write!(&mut w, "{}", block.contents)?;
writeln!(&mut w, "#+END_EXPORT")?;
write_blank_lines(&mut w, block.post_blank)?;
}
Element::SourceBlock(block) => {
writeln!(&mut w, "#+BEGIN_SRC {}", block.language)?;
write!(&mut w, "{}", block.contents)?;
writeln!(&mut w, "#+END_SRC")?;
write_blank_lines(&mut w, block.post_blank)?;
}
Element::BabelCall(call) => {
writeln!(&mut w, "#+CALL: {}", call.value)?;
write_blank_lines(w, call.post_blank)?;
}
Element::InlineSrc(inline_src) => {
write!(&mut w, "src_{}", inline_src.lang)?;
if let Some(options) = &inline_src.options {
write!(&mut w, "[{}]", options)?;
}
write!(&mut w, "{{{}}}", inline_src.body)?;
}
Element::Code { value } => write!(w, "~{}~", value)?,
Element::FnRef(fn_ref) => {
write!(&mut w, "[fn:{}", fn_ref.label)?;
if let Some(definition) = &fn_ref.definition {
write!(&mut w, ":{}", definition)?;
}
write!(&mut w, "]")?;
}
Element::InlineCall(inline_call) => {
write!(&mut w, "call_{}", inline_call.name)?;
if let Some(header) = &inline_call.inside_header {
write!(&mut w, "[{}]", header)?;
}
write!(&mut w, "({})", inline_call.arguments)?;
if let Some(header) = &inline_call.end_header {
write!(&mut w, "[{}]", header)?;
}
}
Element::Link(link) => {
write!(&mut w, "[[{}]", link.path)?;
if let Some(desc) = &link.desc {
write!(&mut w, "[{}]", desc)?;
}
write!(&mut w, "]")?;
}
Element::Macros(_macros) => (),
Element::RadioTarget => (),
Element::Snippet(snippet) => write!(w, "@@{}:{}@@", snippet.name, snippet.value)?,
Element::Target(_target) => (),
Element::Text { value } => write!(w, "{}", value)?,
Element::Timestamp(timestamp) => {
write_timestamp(&mut w, &timestamp)?;
}
Element::Verbatim { value } => write!(w, "={}=", value)?,
Element::FnDef(fn_def) => {
write_blank_lines(w, fn_def.post_blank)?;
}
Element::Clock(clock) => {
write!(w, "CLOCK: ")?;
match clock {
Clock::Closed {
start,
end,
duration,
post_blank,
..
} => {
write_datetime(&mut w, "[", &start, "]--")?;
write_datetime(&mut w, "[", &end, "]")?;
writeln!(&mut w, " => {}", duration)?;
write_blank_lines(&mut w, *post_blank)?;
}
Clock::Running {
start, post_blank, ..
} => {
write_datetime(&mut w, "[", &start, "]\n")?;
write_blank_lines(&mut w, *post_blank)?;
}
}
}
Element::Comment(comment) => {
write!(w, "{}", comment.value)?;
write_blank_lines(&mut w, comment.post_blank)?;
}
Element::FixedWidth(fixed_width) => {
write!(&mut w, "{}", fixed_width.value)?;
write_blank_lines(&mut w, fixed_width.post_blank)?;
}
Element::Keyword(keyword) => {
write!(&mut w, "#+{}", keyword.key)?;
if let Some(optional) = &keyword.optional {
write!(&mut w, "[{}]", optional)?;
}
writeln!(&mut w, ": {}", keyword.value)?;
write_blank_lines(&mut w, keyword.post_blank)?;
}
Element::Rule(rule) => {
writeln!(w, "-----")?;
write_blank_lines(&mut w, rule.post_blank)?;
}
Element::Cookie(_cookie) => (),
Element::Title(title) => {
for _ in 0..title.level {
write!(&mut w, "*")?;
}
if let Some(keyword) = &title.keyword {
write!(&mut w, " {}", keyword)?;
}
if let Some(priority) = title.priority {
write!(&mut w, " [#{}]", priority)?;
}
write!(&mut w, " ")?;
}
Element::Table(_) => (),
Element::TableRow(_) => (),
Element::TableCell(_) => (),
}
Ok(())
}
fn end<W: Write>(&mut self, mut w: W, element: &Element) -> IOResult<()> {
match element {
// container elements
Element::SpecialBlock(block) => {
writeln!(&mut w, "#+END_{}", block.name)?;
write_blank_lines(&mut w, block.post_blank)?;
}
Element::QuoteBlock(block) => {
writeln!(&mut w, "#+END_QUOTE")?;
write_blank_lines(&mut w, block.post_blank)?;
}
Element::CenterBlock(block) => {
writeln!(&mut w, "#+END_CENTER")?;
write_blank_lines(&mut w, block.post_blank)?;
}
Element::VerseBlock(block) => {
writeln!(&mut w, "#+END_VERSE")?;
write_blank_lines(&mut w, block.post_blank)?;
}
Element::Bold => write!(w, "*")?,
Element::Document { .. } => (),
Element::DynBlock(dyn_block) => {
writeln!(w, "#+END:")?;
write_blank_lines(w, dyn_block.post_blank)?;
}
Element::Headline { .. } => (),
Element::List(list) => {
write_blank_lines(w, list.post_blank)?;
}
Element::Italic => write!(w, "/")?,
Element::ListItem(_) => (),
Element::Paragraph { post_blank } => {
write_blank_lines(w, post_blank + 1)?;
}
Element::Section => (),
Element::Strike => write!(w, "+")?,
Element::Underline => write!(w, "_")?,
Element::Drawer(drawer) => {
writeln!(&mut w, ":END:")?;
write_blank_lines(&mut w, drawer.post_blank)?;
}
Element::Title(title) => {
if !title.tags.is_empty() {
write!(&mut w, " :")?;
for tag in &title.tags {
write!(&mut w, "{}:", tag)?;
}
}
writeln!(&mut w)?;
if let Some(planning) = &title.planning {
if let Some(scheduled) = &planning.scheduled {
write!(&mut w, "SCHEDULED: ")?;
write_timestamp(&mut w, &scheduled)?;
}
if let Some(deadline) = &planning.deadline {
if planning.scheduled.is_some() {
write!(&mut w, " ")?;
}
write!(&mut w, "DEADLINE: ")?;
write_timestamp(&mut w, &deadline)?;
}
if let Some(closed) = &planning.closed {
if planning.deadline.is_some() {
write!(&mut w, " ")?;
}
write!(&mut w, "CLOSED: ")?;
write_timestamp(&mut w, &closed)?;
}
writeln!(&mut w)?;
}
if !title.properties.is_empty() {
writeln!(&mut w, ":PROPERTIES:")?;
for (key, value) in title.properties.iter() {
writeln!(&mut w, ":{}: {}", key, value)?;
}
writeln!(&mut w, ":END:")?;
}
write_blank_lines(&mut w, title.post_blank)?;
}
Element::Table(Table::Org { post_blank, .. }) => {
write_blank_lines(w, *post_blank)?;
}
Element::Table(Table::TableEl { post_blank, .. }) => {
write_blank_lines(w, *post_blank)?;
}
Element::TableRow(_) => (),
Element::TableCell(_) => (),
// non-container elements
_ => debug_assert!(!element.is_container()),
}
Ok(())
}
}
fn write_blank_lines<W: Write>(mut w: W, count: usize) -> Result<(), Error> {
for _ in 0..count {
writeln!(w)?;
}
Ok(())
}
fn write_timestamp<W: Write>(mut w: W, timestamp: &Timestamp) -> Result<(), Error> {
match timestamp {
Timestamp::Active { start, .. } => {
write_datetime(w, "<", start, ">")?;
}
Timestamp::Inactive { start, .. } => {
write_datetime(w, "[", start, "]")?;
}
Timestamp::ActiveRange { start, end, .. } => {
write_datetime(&mut w, "<", start, ">--")?;
write_datetime(&mut w, "<", end, ">")?;
}
Timestamp::InactiveRange { start, end, .. } => {
write_datetime(&mut w, "[", start, "]--")?;
write_datetime(&mut w, "[", end, "]")?;
}
Timestamp::Diary { value } => write!(w, "<%%({})>", value)?,
}
Ok(())
}

249
src/export/traverse.rs Normal file
View file

@ -0,0 +1,249 @@
use crate::ast::*;
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
use rowan::{ast::AstNode, WalkEvent};
use SyntaxKind::*;
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
enum TraversalControl {
Up,
Stop,
Skip,
#[default]
Continue,
}
macro_rules! take_control {
($ctrl:expr) => {
match $ctrl.control {
TraversalControl::Stop => {
$ctrl.control = TraversalControl::Stop;
return;
}
TraversalControl::Up => {
$ctrl.control = TraversalControl::Skip;
return;
}
TraversalControl::Skip => {
$ctrl.control = TraversalControl::Continue;
return;
}
TraversalControl::Continue => {}
}
};
}
#[derive(Default)]
pub struct TraversalContext {
control: TraversalControl,
}
impl TraversalContext {
/// Stops traversal completely
pub fn stop(&mut self) {
self.control = TraversalControl::Stop;
}
/// Skips traversal of the current node's siblings
pub fn up(&mut self) {
self.control = TraversalControl::Up;
}
/// Skips traversal of the current node's descendants
pub fn skip(&mut self) {
self.control = TraversalControl::Skip;
}
/// Continues traversal
pub fn r#continue(&mut self) {
self.control = TraversalControl::Continue;
}
}
/// Enumerates org syntax tree
///
/// Traverser enumerates org syntax tree and calls handle method on each
/// enumerated node and token.
///
/// Each handle method can returns a `TraversalControl` to control the traversal.
pub trait Traverser {
/// Called when visiting any node
fn node(&mut self, node: SyntaxNode, ctx: &mut TraversalContext) {
macro_rules! traverse_children {
($node:expr) => {{
for child in $node.children_with_tokens() {
match child {
SyntaxElement::Node(node) => self.node(node, ctx),
SyntaxElement::Token(token) => self.token(token, ctx),
};
take_control!(ctx);
}
}};
}
macro_rules! traverse {
($node:ident, $method:ident) => {{
debug_assert!($node::can_cast(node.kind()));
let node = $node { syntax: node };
self.$method(WalkEvent::Enter(&node), ctx);
take_control!(ctx);
traverse_children!(&node.syntax);
self.$method(WalkEvent::Leave(&node), ctx);
take_control!(ctx);
}};
}
match node.kind() {
DOCUMENT => traverse!(Document, document),
HEADLINE => traverse!(Headline, headline),
SECTION => traverse!(Section, section),
PARAGRAPH => traverse!(Paragraph, paragraph),
BOLD => traverse!(Bold, bold),
ITALIC => traverse!(Italic, italic),
STRIKE => traverse!(Strike, strike),
UNDERLINE => traverse!(Underline, underline),
LIST => traverse!(List, list),
LIST_ITEM => traverse!(ListItem, list_item),
LIST_ITEM_CONTENT => traverse!(ListItemContent, list_item_content),
LIST_ITEM_TAG => traverse!(ListItemTag, list_item_tag),
CODE => traverse!(Code, code),
INLINE_CALL => traverse!(InlineCall, inline_call),
INLINE_SRC => traverse!(InlineSrc, inline_src),
RULE => traverse!(Rule, rule),
VERBATIM => traverse!(Verbatim, verbatim),
SPECIAL_BLOCK => traverse!(SpecialBlock, special_block),
QUOTE_BLOCK => traverse!(QuoteBlock, quote_block),
CENTER_BLOCK => traverse!(CenterBlock, center_block),
VERSE_BLOCK => traverse!(VerseBlock, verse_block),
COMMENT_BLOCK => traverse!(CommentBlock, comment_block),
EXAMPLE_BLOCK => traverse!(ExampleBlock, example_block),
EXPORT_BLOCK => traverse!(ExportBlock, export_block),
SOURCE_BLOCK => traverse!(SourceBlock, source_block),
BABEL_CALL => traverse!(BabelCall, babel_call),
CLOCK => traverse!(Clock, clock),
COOKIE => traverse!(Cookie, cookie),
RADIO_TARGET => traverse!(RadioTarget, radio_target),
DRAWER => traverse!(Drawer, drawer),
DYN_BLOCK => traverse!(DynBlock, dyn_block),
FN_DEF => traverse!(FnDef, fn_def),
FN_REF => traverse!(FnRef, fn_ref),
MACROS => traverse!(Macros, macros),
SNIPPET => traverse!(Snippet, snippet),
TIMESTAMP_ACTIVE | TIMESTAMP_INACTIVE | TIMESTAMP_DIARY => {
traverse!(Timestamp, timestamp)
}
TARGET => traverse!(Target, target),
COMMENT => traverse!(Comment, comment),
FIXED_WIDTH => traverse!(FixedWidth, fixed_width),
HEADLINE_TITLE => traverse!(HeadlineTitle, headline_title),
ORG_TABLE => traverse!(OrgTable, org_table),
ORG_TABLE_RULE_ROW | ORG_TABLE_STANDARD_ROW => traverse!(OrgTableRow, org_table_row),
ORG_TABLE_CELL => traverse!(OrgTableCell, org_table_cell),
LINK => traverse!(Link, link),
BLOCK_CONTENT => traverse_children!(node),
_ => {}
}
}
/// Called when visiting any token
fn token(&mut self, token: SyntaxToken, ctx: &mut TraversalContext) {
match token.kind() {
TEXT => self.text(token, ctx),
_ => {}
}
take_control!(ctx);
}
/// Called when visiting `Text` token
fn text(&mut self, _token: SyntaxToken, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Document` node
fn document(&mut self, _event: WalkEvent<&Document>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Headline` node
fn headline(&mut self, _event: WalkEvent<&Headline>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Paragraph` node
fn paragraph(&mut self, _event: WalkEvent<&Paragraph>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Section` node
fn section(&mut self, _event: WalkEvent<&Section>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Rule` node
fn rule(&mut self, _event: WalkEvent<&Rule>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Comment` node
fn comment(&mut self, _event: WalkEvent<&Comment>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `InlineSrc` node
fn inline_src(&mut self, _event: WalkEvent<&InlineSrc>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `InlineCall` node
fn inline_call(&mut self, _event: WalkEvent<&InlineCall>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Code` node
fn code(&mut self, _event: WalkEvent<&Code>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Bold` node
fn bold(&mut self, _event: WalkEvent<&Bold>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Verbatim` node
fn verbatim(&mut self, _event: WalkEvent<&Verbatim>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Italic` node
fn italic(&mut self, _event: WalkEvent<&Italic>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Strike` node
fn strike(&mut self, _event: WalkEvent<&Strike>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Underline` node
fn underline(&mut self, _event: WalkEvent<&Underline>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `List` node
fn list(&mut self, _event: WalkEvent<&List>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `ListItem` node
fn list_item(&mut self, _event: WalkEvent<&ListItem>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `ListItemTag` node
fn list_item_tag(&mut self, _event: WalkEvent<&ListItemTag>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `ListItemContent` node
fn list_item_content(
&mut self,
_event: WalkEvent<&ListItemContent>,
_ctx: &mut TraversalContext,
);
/// Called when entering or leaving `SpecialBlock` node
fn special_block(&mut self, _event: WalkEvent<&SpecialBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `QuoteBlock` node
fn quote_block(&mut self, _event: WalkEvent<&QuoteBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `CenterBlock` node
fn center_block(&mut self, _event: WalkEvent<&CenterBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `VerseBlock` node
fn verse_block(&mut self, _event: WalkEvent<&VerseBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `CommentBlock` node
fn comment_block(&mut self, _event: WalkEvent<&CommentBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `ExampleBlock` node
fn example_block(&mut self, _event: WalkEvent<&ExampleBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `ExportBlock` node
fn export_block(&mut self, _event: WalkEvent<&ExportBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `SourceBlock` node
fn source_block(&mut self, _event: WalkEvent<&SourceBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `BabelCall` node
fn babel_call(&mut self, _event: WalkEvent<&BabelCall>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Clock` node
fn clock(&mut self, _event: WalkEvent<&Clock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Cookie` node
fn cookie(&mut self, _event: WalkEvent<&Cookie>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `RadioTarget` node
fn radio_target(&mut self, _event: WalkEvent<&RadioTarget>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Drawer` node
fn drawer(&mut self, _event: WalkEvent<&Drawer>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `DynBlock` node
fn dyn_block(&mut self, _event: WalkEvent<&DynBlock>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `FnDef` node
fn fn_def(&mut self, _event: WalkEvent<&FnDef>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `FnRef` node
fn fn_ref(&mut self, _event: WalkEvent<&FnRef>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Macros` node
fn macros(&mut self, _event: WalkEvent<&Macros>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Snippet` node
fn snippet(&mut self, _event: WalkEvent<&Snippet>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Timestamp` node
fn timestamp(&mut self, _event: WalkEvent<&Timestamp>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Target` node
fn target(&mut self, _event: WalkEvent<&Target>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `FixedWidth` node
fn fixed_width(&mut self, _event: WalkEvent<&FixedWidth>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `HeadlineTitle` node
fn headline_title(&mut self, _event: WalkEvent<&HeadlineTitle>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `OrgTable` node
fn org_table(&mut self, _event: WalkEvent<&OrgTable>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `OrgTableRow` node
fn org_table_row(&mut self, _event: WalkEvent<&OrgTableRow>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `OrgTableCell` node
fn org_table_cell(&mut self, _event: WalkEvent<&OrgTableCell>, _ctx: &mut TraversalContext);
/// Called when entering or leaving `Link` node
fn link(&mut self, _event: WalkEvent<&Link>, _ctx: &mut TraversalContext);
}

File diff suppressed because it is too large Load diff

View file

@ -1,245 +1,18 @@
//! A Rust library for parsing orgmode files.
//!
//! [Live demo](https://orgize.herokuapp.com/)
//!
//! # Parse
//!
//! To parse a orgmode string, simply invoking the [`Org::parse`] function:
//!
//! [`Org::parse`]: struct.Org.html#method.parse
//!
//! ```rust
//! use orgize::Org;
//!
//! Org::parse("* DONE Title :tag:");
//! ```
//!
//! or [`Org::parse_custom`]:
//!
//! [`Org::parse_custom`]: struct.Org.html#method.parse_custom
//!
//! ```rust
//! use orgize::{Org, ParseConfig};
//!
//! Org::parse_custom(
//! "* TASK Title 1",
//! &ParseConfig {
//! // custom todo keywords
//! todo_keywords: (vec!["TASK".to_string()], vec![]),
//! ..Default::default()
//! },
//! );
//! ```
//!
//! # Iter
//!
//! [`Org::iter`] function will returns an iterator of [`Event`]s, which is
//! a simple wrapper of [`Element`].
//!
//! [`Org::iter`]: struct.Org.html#method.iter
//! [`Event`]: enum.Event.html
//! [`Element`]: elements/enum.Element.html
//!
//! ```rust
//! use orgize::Org;
//!
//! for event in Org::parse("* DONE Title :tag:").iter() {
//! // handling the event
//! }
//! ```
//!
//! **Note**: whether an element is container or not, it will appears twice in one loop.
//! One as [`Event::Start(element)`], one as [`Event::End(element)`].
//!
//! [`Event::Start(element)`]: enum.Event.html#variant.Start
//! [`Event::End(element)`]: enum.Event.html#variant.End
//!
//! # Render html
//!
//! You can call the [`Org::write_html`] function to generate html directly, which
//! uses the [`DefaultHtmlHandler`] internally:
//!
//! [`Org::write_html`]: struct.Org.html#method.write_html
//! [`DefaultHtmlHandler`]: export/struct.DefaultHtmlHandler.html
//!
//! ```rust
//! use orgize::Org;
//!
//! let mut writer = Vec::new();
//! Org::parse("* title\n*section*").write_html(&mut writer).unwrap();
//!
//! assert_eq!(
//! String::from_utf8(writer).unwrap(),
//! "<main><h1>title</h1><section><p><b>section</b></p></section></main>"
//! );
//! ```
//!
//! # Render html with custom `HtmlHandler`
//!
//! To customize html rendering, simply implementing [`HtmlHandler`] trait and passing
//! it to the [`Org::write_html_custom`] function.
//!
//! [`HtmlHandler`]: export/trait.HtmlHandler.html
//! [`Org::write_html_custom`]: struct.Org.html#method.write_html_custom
//!
//! The following code demonstrates how to add a id for every headline and return
//! own error type while rendering.
//!
//! ```rust
//! use std::convert::From;
//! use std::io::{Error as IOError, Write};
//! use std::string::FromUtf8Error;
//!
//! use orgize::export::{DefaultHtmlHandler, HtmlHandler};
//! use orgize::{Element, Org};
//! use slugify::slugify;
//!
//! #[derive(Debug)]
//! enum MyError {
//! IO(IOError),
//! Heading,
//! Utf8(FromUtf8Error),
//! }
//!
//! // From<std::io::Error> trait is required for custom error type
//! impl From<IOError> for MyError {
//! fn from(err: IOError) -> Self {
//! MyError::IO(err)
//! }
//! }
//!
//! impl From<FromUtf8Error> for MyError {
//! fn from(err: FromUtf8Error) -> Self {
//! MyError::Utf8(err)
//! }
//! }
//!
//! #[derive(Default)]
//! struct MyHtmlHandler(DefaultHtmlHandler);
//!
//! impl HtmlHandler<MyError> for MyHtmlHandler {
//! fn start<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
//! if let Element::Title(title) = element {
//! if title.level > 6 {
//! return Err(MyError::Heading);
//! } else {
//! write!(
//! w,
//! "<h{0}><a id=\"{1}\" href=\"#{1}\">",
//! title.level,
//! slugify!(&title.raw),
//! )?;
//! }
//! } else {
//! // fallthrough to default handler
//! self.0.start(w, element)?;
//! }
//! Ok(())
//! }
//!
//! fn end<W: Write>(&mut self, mut w: W, element: &Element) -> Result<(), MyError> {
//! if let Element::Title(title) = element {
//! write!(w, "</a></h{}>", title.level)?;
//! } else {
//! self.0.end(w, element)?;
//! }
//! Ok(())
//! }
//! }
//!
//! fn main() -> Result<(), MyError> {
//! let mut writer = Vec::new();
//! let mut handler = MyHtmlHandler::default();
//! Org::parse("* title\n*section*").write_html_custom(&mut writer, &mut handler)?;
//!
//! assert_eq!(
//! String::from_utf8(writer)?,
//! "<main><h1><a id=\"title\" href=\"#title\">title</a></h1>\
//! <section><p><b>section</b></p></section></main>"
//! );
//!
//! Ok(())
//! }
//! ```
//!
//! **Note**: as I mentioned above, each element will appears two times while iterating.
//! And handler will silently ignores all end events from non-container elements.
//!
//! So if you want to change how a non-container element renders, just redefine the `start`
//! function and leave the `end` function unchanged.
//!
//! # Serde
//!
//! `Org` struct have already implemented serde's `Serialize` trait. It means you can
//! serialize it into any format supported by serde, such as json:
//!
//! ```rust
//! use orgize::Org;
//! use serde_json::{json, to_string};
//!
//! let org = Org::parse("I 'm *bold*.");
//! #[cfg(feature = "ser")]
//! println!("{}", to_string(&org).unwrap());
//!
//! // {
//! // "type": "document",
//! // "children": [{
//! // "type": "section",
//! // "children": [{
//! // "type": "paragraph",
//! // "children":[{
//! // "type": "text",
//! // "value":"I 'm "
//! // }, {
//! // "type": "bold",
//! // "children":[{
//! // "type": "text",
//! // "value": "bold"
//! // }]
//! // }, {
//! // "type":"text",
//! // "value":"."
//! // }]
//! // }]
//! // }]
//! // }
//! ```
//!
//! # Features
//!
//! By now, orgize provides three features:
//!
//! + `ser`: adds the ability to serialize `Org` and other elements using `serde`, enabled by default.
//!
//! + `chrono`: adds the ability to convert `Datetime` into `chrono` structs, disabled by default.
//!
//! + `syntect`: provides [`SyntectHtmlHandler`] for highlighting code block, disabled by default.
//!
//! [`SyntectHtmlHandler`]: export/struct.SyntectHtmlHandler.html
//!
//! # License
//!
//! MIT
#![doc = include_str!("../README.md")]
pub mod ast;
mod config;
pub mod elements;
pub mod export;
mod headline;
mod org;
mod parse;
mod parsers;
mod validate;
mod syntax;
#[cfg(test)]
mod tests;
// Re-export of the indextree crate.
pub use indextree;
#[cfg(feature = "syntect")]
pub use syntect;
// Re-export of the rowan crate.
pub use rowan;
pub use config::ParseConfig;
pub use elements::Element;
pub use headline::{Document, Headline};
pub use org::{Event, Org};
pub use validate::ValidationError;
#[cfg(feature = "wasm")]
mod wasm;
pub use org::Org;
pub use syntax::{
SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxNodeChildren, SyntaxToken,
};

View file

@ -1,193 +1,65 @@
use indextree::{Arena, NodeEdge, NodeId};
use std::io::{Error, Write};
use std::ops::{Index, IndexMut};
use rowan::ast::AstNode;
use rowan::GreenNode;
use crate::{
config::{ParseConfig, DEFAULT_CONFIG},
elements::{Element, Keyword},
export::{DefaultHtmlHandler, DefaultOrgHandler, HtmlHandler, OrgHandler},
parsers::{blank_lines_count, parse_container, Container, OwnedArena},
};
pub struct Org<'a> {
pub(crate) arena: Arena<Element<'a>>,
pub(crate) root: NodeId,
}
use crate::ast::Document;
use crate::config::ParseConfig;
use crate::export::{HtmlExport, TraversalContext, Traverser};
use crate::syntax::{OrgLanguage, SyntaxNode};
#[derive(Debug)]
pub enum Event<'a, 'b> {
Start(&'b Element<'a>),
End(&'b Element<'a>),
pub struct Org {
pub(crate) green: GreenNode,
pub(crate) config: ParseConfig,
}
impl<'a> Org<'a> {
/// Creates a new empty `Org` struct.
pub fn new() -> Org<'static> {
let mut arena = Arena::new();
let root = arena.new_node(Element::Document { pre_blank: 0 });
Org { arena, root }
impl Org {
/// Parse input string to Org element tree using default parse config
pub fn parse(input: impl AsRef<str>) -> Org {
ParseConfig::default().parse(input)
}
/// Parses string `text` into `Org` struct.
pub fn parse(text: &'a str) -> Org<'a> {
Org::parse_custom(text, &DEFAULT_CONFIG)
pub fn green(&self) -> &GreenNode {
&self.green
}
/// Likes `parse`, but accepts `String`.
pub fn parse_string(text: String) -> Org<'static> {
Org::parse_string_custom(text, &DEFAULT_CONFIG)
pub fn config(&self) -> &ParseConfig {
&self.config
}
/// Parses string `text` into `Org` struct with custom `ParseConfig`.
pub fn parse_custom(text: &'a str, config: &ParseConfig) -> Org<'a> {
let mut arena = Arena::new();
let (text, pre_blank) = blank_lines_count(text);
let root = arena.new_node(Element::Document { pre_blank });
let mut org = Org { arena, root };
parse_container(
&mut org.arena,
Container::Document {
content: text,
node: org.root,
},
config,
);
org.debug_validate();
org
/// Returns the document
pub fn document(&self) -> Document {
Document {
syntax: SyntaxNode::new_root(self.green.clone()),
}
}
/// Likes `parse_custom`, but accepts `String`.
pub fn parse_string_custom(text: String, config: &ParseConfig) -> Org<'static> {
let mut arena = Arena::new();
let (text, pre_blank) = blank_lines_count(&text);
let root = arena.new_node(Element::Document { pre_blank });
let mut org = Org { arena, root };
parse_container(
&mut OwnedArena::new(&mut org.arena),
Container::Document {
content: text,
node: org.root,
},
config,
);
org.debug_validate();
org
/// Returns org-mode string
pub fn to_org(&self) -> String {
self.green.to_string()
}
/// Returns a reference to the underlay arena.
pub fn arena(&self) -> &Arena<Element<'a>> {
&self.arena
/// Convert org element tree to html-format using default html handler
pub fn to_html(&self) -> String {
let mut handler = HtmlExport::default();
self.traverse(&mut handler);
handler.finish()
}
/// Returns a mutual reference to the underlay arena.
pub fn arena_mut(&mut self) -> &mut Arena<Element<'a>> {
&mut self.arena
/// Walk through org element tree using given traverser
pub fn traverse<T: Traverser>(&self, h: &mut T) {
let mut ctx = TraversalContext::default();
h.node(SyntaxNode::new_root(self.green.clone()), &mut ctx);
}
/// Returns an iterator of `Event`s.
pub fn iter<'b>(&'b self) -> impl Iterator<Item = Event<'a, 'b>> + 'b {
self.root.traverse(&self.arena).map(move |edge| match edge {
NodeEdge::Start(node) => Event::Start(&self[node]),
NodeEdge::End(node) => Event::End(&self[node]),
})
}
/// Returns an iterator of `Keyword`s.
pub fn keywords(&self) -> impl Iterator<Item = &Keyword<'_>> {
self.root
.descendants(&self.arena)
.skip(1)
.filter_map(move |node| match &self[node] {
Element::Keyword(kw) => Some(kw),
_ => None,
})
}
/// Writes an `Org` struct as html format.
pub fn write_html<W>(&self, writer: W) -> Result<(), Error>
where
W: Write,
{
self.write_html_custom(writer, &mut DefaultHtmlHandler)
}
/// Writes an `Org` struct as html format with custom `HtmlHandler`.
pub fn write_html_custom<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E>
where
W: Write,
E: From<Error>,
H: HtmlHandler<E>,
{
for event in self.iter() {
match event {
Event::Start(element) => handler.start(&mut writer, element)?,
Event::End(element) => handler.end(&mut writer, element)?,
/// Returns the first node in org element tree in depth first order
pub fn first_node<N: AstNode<Language = OrgLanguage>>(&self) -> Option<N> {
fn find<N: AstNode<Language = OrgLanguage>>(node: SyntaxNode) -> Option<N> {
if N::can_cast(node.kind()) {
N::cast(node)
} else {
node.children().find_map(find)
}
}
Ok(())
}
/// Writes an `Org` struct as org format.
pub fn write_org<W>(&self, writer: W) -> Result<(), Error>
where
W: Write,
{
self.write_org_custom(writer, &mut DefaultOrgHandler)
}
/// Writes an `Org` struct as org format with custom `OrgHandler`.
pub fn write_org_custom<W, H, E>(&self, mut writer: W, handler: &mut H) -> Result<(), E>
where
W: Write,
E: From<Error>,
H: OrgHandler<E>,
{
for event in self.iter() {
match event {
Event::Start(element) => handler.start(&mut writer, element)?,
Event::End(element) => handler.end(&mut writer, element)?,
}
}
Ok(())
}
}
impl Default for Org<'static> {
fn default() -> Self {
Org::new()
}
}
impl<'a> Index<NodeId> for Org<'a> {
type Output = Element<'a>;
fn index(&self, node_id: NodeId) -> &Self::Output {
self.arena[node_id].get()
}
}
impl<'a> IndexMut<NodeId> for Org<'a> {
fn index_mut(&mut self, node_id: NodeId) -> &mut Self::Output {
self.arena[node_id].get_mut()
}
}
#[cfg(feature = "ser")]
use serde::{ser::Serializer, Serialize};
#[cfg(feature = "ser")]
impl Serialize for Org<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde_indextree::Node;
serializer.serialize_newtype_struct("Org", &Node::new(self.root, &self.arena))
find(SyntaxNode::new_root(self.green.clone()))
}
}

View file

@ -1,136 +0,0 @@
//! Parsers combinators
use memchr::memchr;
use nom::{
bytes::complete::take_while1,
combinator::verify,
error::{make_error, ErrorKind},
Err, IResult,
};
// read until the first line_ending, if line_ending is not present, return the input directly
pub fn line(input: &str) -> IResult<&str, &str, ()> {
if let Some(i) = memchr(b'\n', input.as_bytes()) {
if i > 0 && input.as_bytes()[i - 1] == b'\r' {
Ok((&input[i + 1..], &input[0..i - 1]))
} else {
Ok((&input[i + 1..], &input[0..i]))
}
} else {
Ok(("", input))
}
}
pub fn lines_till<F>(predicate: F) -> impl Fn(&str) -> IResult<&str, &str, ()>
where
F: Fn(&str) -> bool,
{
move |i| {
let mut input = i;
loop {
// TODO: better error kind
if input.is_empty() {
return Err(Err::Error(make_error(input, ErrorKind::Many0)));
}
let (input_, line_) = line(input)?;
debug_assert_ne!(input, input_);
if predicate(line_) {
let offset = i.len() - input.len();
return Ok((input_, &i[0..offset]));
}
input = input_;
}
}
}
pub fn lines_while<F>(predicate: F) -> impl Fn(&str) -> IResult<&str, &str, ()>
where
F: Fn(&str) -> bool,
{
move |i| {
let mut input = i;
loop {
// unlike lines_till, line_while won't return error
if input.is_empty() {
return Ok(("", i));
}
let (input_, line_) = line(input)?;
debug_assert_ne!(input, input_);
if !predicate(line_) {
let offset = i.len() - input.len();
return Ok((input, &i[0..offset]));
}
input = input_;
}
}
}
#[test]
fn test_lines_while() {
assert_eq!(lines_while(|line| line == "foo")("foo"), Ok(("", "foo")));
assert_eq!(lines_while(|line| line == "foo")("bar"), Ok(("bar", "")));
assert_eq!(
lines_while(|line| line == "foo")("foo\n\n"),
Ok(("\n", "foo\n"))
);
assert_eq!(
lines_while(|line| line.trim().is_empty())("\n\n\n"),
Ok(("", "\n\n\n"))
);
}
pub fn eol(input: &str) -> IResult<&str, &str, ()> {
verify(line, |s: &str| {
s.as_bytes().iter().all(u8::is_ascii_whitespace)
})(input)
}
pub fn one_word(input: &str) -> IResult<&str, &str, ()> {
take_while1(|c: char| !c.is_ascii_whitespace())(input)
}
pub fn blank_lines_count(input: &str) -> IResult<&str, usize, ()> {
let mut count = 0;
let mut input = input;
loop {
if input.is_empty() {
return Ok(("", count));
}
let (input_, line_) = line(input)?;
debug_assert_ne!(input, input_);
if !line_.chars().all(char::is_whitespace) {
return Ok((input, count));
}
count += 1;
input = input_;
}
}
#[test]
fn test_blank_lines_count() {
assert_eq!(blank_lines_count("foo"), Ok(("foo", 0)));
assert_eq!(blank_lines_count(" foo"), Ok((" foo", 0)));
assert_eq!(blank_lines_count(" \t\nfoo\n"), Ok(("foo\n", 1)));
assert_eq!(blank_lines_count("\n \r\n\nfoo\n"), Ok(("foo\n", 3)));
assert_eq!(
blank_lines_count("\r\n \n \r\n foo\n"),
Ok((" foo\n", 3))
);
assert_eq!(blank_lines_count("\r\n \n \r\n \n"), Ok(("", 4)));
}

View file

@ -1 +0,0 @@
pub mod combinators;

View file

@ -1,657 +0,0 @@
use std::iter::once;
use std::marker::PhantomData;
use indextree::{Arena, NodeId};
use jetscii::{bytes, BytesConst};
use memchr::{memchr, memchr_iter};
use nom::bytes::complete::take_while1;
use crate::config::ParseConfig;
use crate::elements::{
block::RawBlock, emphasis::Emphasis, keyword::RawKeyword, radio_target::parse_radio_target,
Clock, Comment, Cookie, Drawer, DynBlock, Element, FixedWidth, FnDef, FnRef, InlineCall,
InlineSrc, Link, List, ListItem, Macros, Rule, Snippet, Table, TableCell, TableRow, Target,
Timestamp, Title,
};
use crate::parse::combinators::lines_while;
pub trait ElementArena<'a> {
fn append<T>(&mut self, element: T, parent: NodeId) -> NodeId
where
T: Into<Element<'a>>;
fn insert_before_last_child<T>(&mut self, element: T, parent: NodeId) -> NodeId
where
T: Into<Element<'a>>;
fn set<T>(&mut self, node: NodeId, element: T)
where
T: Into<Element<'a>>;
}
pub type BorrowedArena<'a> = Arena<Element<'a>>;
impl<'a> ElementArena<'a> for BorrowedArena<'a> {
fn append<T>(&mut self, element: T, parent: NodeId) -> NodeId
where
T: Into<Element<'a>>,
{
let node = self.new_node(element.into());
parent.append(node, self);
node
}
fn insert_before_last_child<T>(&mut self, element: T, parent: NodeId) -> NodeId
where
T: Into<Element<'a>>,
{
if let Some(child) = self[parent].last_child() {
let node = self.new_node(element.into());
child.insert_before(node, self);
node
} else {
self.append(element, parent)
}
}
fn set<T>(&mut self, node: NodeId, element: T)
where
T: Into<Element<'a>>,
{
*self[node].get_mut() = element.into();
}
}
pub struct OwnedArena<'a, 'b, 'c> {
arena: &'b mut Arena<Element<'c>>,
phantom: PhantomData<&'a ()>,
}
impl<'a, 'b, 'c> OwnedArena<'a, 'b, 'c> {
pub fn new(arena: &'b mut Arena<Element<'c>>) -> OwnedArena<'a, 'b, 'c> {
OwnedArena {
arena,
phantom: PhantomData,
}
}
}
impl<'a> ElementArena<'a> for OwnedArena<'a, '_, '_> {
fn append<T>(&mut self, element: T, parent: NodeId) -> NodeId
where
T: Into<Element<'a>>,
{
self.arena.append(element.into().into_owned(), parent)
}
fn insert_before_last_child<T>(&mut self, element: T, parent: NodeId) -> NodeId
where
T: Into<Element<'a>>,
{
self.arena
.insert_before_last_child(element.into().into_owned(), parent)
}
fn set<T>(&mut self, node: NodeId, element: T)
where
T: Into<Element<'a>>,
{
self.arena.set(node, element.into().into_owned());
}
}
#[derive(Debug)]
pub enum Container<'a> {
// Block, List Item
Block { content: &'a str, node: NodeId },
// Paragraph, Inline Markup
Inline { content: &'a str, node: NodeId },
// Headline
Headline { content: &'a str, node: NodeId },
// Document
Document { content: &'a str, node: NodeId },
}
pub fn parse_container<'a, T: ElementArena<'a>>(
arena: &mut T,
container: Container<'a>,
config: &ParseConfig,
) {
let containers = &mut vec![container];
while let Some(container) = containers.pop() {
match container {
Container::Document { content, node } => {
parse_section_and_headlines(arena, content, node, containers);
}
Container::Headline { content, node } => {
parse_headline_content(arena, content, node, containers, config);
}
Container::Block { content, node } => {
parse_blocks(arena, content, node, containers);
}
Container::Inline { content, node } => {
parse_inlines(arena, content, node, containers);
}
}
}
}
pub fn parse_headline_content<'a, T: ElementArena<'a>>(
arena: &mut T,
content: &'a str,
parent: NodeId,
containers: &mut Vec<Container<'a>>,
config: &ParseConfig,
) {
let (tail, (title, content)) = Title::parse(content, config).unwrap();
let node = arena.append(title, parent);
containers.push(Container::Inline { content, node });
parse_section_and_headlines(arena, tail, parent, containers);
}
pub fn parse_section_and_headlines<'a, T: ElementArena<'a>>(
arena: &mut T,
content: &'a str,
parent: NodeId,
containers: &mut Vec<Container<'a>>,
) {
let content = blank_lines_count(content).0;
if content.is_empty() {
return;
}
let mut last_end = 0;
for i in memchr_iter(b'\n', content.as_bytes()).chain(once(content.len())) {
if let Some((mut tail, (headline_content, level))) = parse_headline(&content[last_end..]) {
if last_end != 0 {
let node = arena.append(Element::Section, parent);
let content = &content[0..last_end];
containers.push(Container::Block { content, node });
}
let node = arena.append(Element::Headline { level }, parent);
containers.push(Container::Headline {
content: headline_content,
node,
});
while let Some((new_tail, (content, level))) = parse_headline(tail) {
debug_assert_ne!(tail, new_tail);
let node = arena.append(Element::Headline { level }, parent);
containers.push(Container::Headline { content, node });
tail = new_tail;
}
return;
}
last_end = i + 1;
}
let node = arena.append(Element::Section, parent);
containers.push(Container::Block { content, node });
}
pub fn parse_blocks<'a, T: ElementArena<'a>>(
arena: &mut T,
content: &'a str,
parent: NodeId,
containers: &mut Vec<Container<'a>>,
) {
let mut tail = blank_lines_count(content).0;
if let Some(new_tail) = parse_block(content, arena, parent, containers) {
tail = blank_lines_count(new_tail).0;
}
let mut text = tail;
let mut pos = 0;
while !tail.is_empty() {
let i = memchr(b'\n', tail.as_bytes())
.map(|i| i + 1)
.unwrap_or_else(|| tail.len());
if tail.as_bytes()[0..i].iter().all(u8::is_ascii_whitespace) {
let (tail_, blank) = blank_lines_count(&tail[i..]);
debug_assert_ne!(tail, tail_);
tail = tail_;
let node = arena.append(
Element::Paragraph {
// including the current line (&tail[0..i])
post_blank: blank + 1,
},
parent,
);
containers.push(Container::Inline {
content: &text[0..pos].trim_end(),
node,
});
pos = 0;
text = tail;
} else if let Some(new_tail) = parse_block(tail, arena, parent, containers) {
if pos != 0 {
let node =
arena.insert_before_last_child(Element::Paragraph { post_blank: 0 }, parent);
containers.push(Container::Inline {
content: &text[0..pos].trim_end(),
node,
});
pos = 0;
}
debug_assert_ne!(tail, blank_lines_count(new_tail).0);
tail = blank_lines_count(new_tail).0;
text = tail;
} else {
debug_assert_ne!(tail, &tail[i..]);
tail = &tail[i..];
pos += i;
}
}
if !text.is_empty() {
let node = arena.append(Element::Paragraph { post_blank: 0 }, parent);
containers.push(Container::Inline {
content: &text[0..pos].trim_end(),
node,
});
}
}
pub fn parse_block<'a, T: ElementArena<'a>>(
contents: &'a str,
arena: &mut T,
parent: NodeId,
containers: &mut Vec<Container<'a>>,
) -> Option<&'a str> {
match contents
.as_bytes()
.iter()
.find(|c| !c.is_ascii_whitespace())?
{
b'[' => {
let (tail, (fn_def, content)) = FnDef::parse(contents)?;
let node = arena.append(fn_def, parent);
containers.push(Container::Block { content, node });
Some(tail)
}
b'0'..=b'9' | b'*' => {
let tail = parse_list(arena, contents, parent, containers)?;
Some(tail)
}
b'C' => {
let (tail, clock) = Clock::parse(contents)?;
arena.append(clock, parent);
Some(tail)
}
b'\'' => {
// TODO: LaTeX environment
None
}
b'-' => {
if let Some((tail, rule)) = Rule::parse(contents) {
arena.append(rule, parent);
Some(tail)
} else {
let tail = parse_list(arena, contents, parent, containers)?;
Some(tail)
}
}
b':' => {
if let Some((tail, (drawer, content))) = Drawer::parse(contents) {
let node = arena.append(drawer, parent);
containers.push(Container::Block { content, node });
Some(tail)
} else {
let (tail, fixed_width) = FixedWidth::parse(contents)?;
arena.append(fixed_width, parent);
Some(tail)
}
}
b'|' => {
let tail = parse_org_table(arena, contents, containers, parent);
Some(tail)
}
b'+' => {
if let Some((tail, table)) = Table::parse_table_el(contents) {
arena.append(table, parent);
Some(tail)
} else {
let tail = parse_list(arena, contents, parent, containers)?;
Some(tail)
}
}
b'#' => {
if let Some((tail, block)) = RawBlock::parse(contents) {
let (element, content) = block.into_element();
// avoid use after free
let is_block_container = match element {
Element::CenterBlock(_)
| Element::QuoteBlock(_)
| Element::VerseBlock(_)
| Element::SpecialBlock(_) => true,
_ => false,
};
let node = arena.append(element, parent);
if is_block_container {
containers.push(Container::Block { content, node });
}
Some(tail)
} else if let Some((tail, (dyn_block, content))) = DynBlock::parse(contents) {
let node = arena.append(dyn_block, parent);
containers.push(Container::Block { content, node });
Some(tail)
} else if let Some((tail, keyword)) = RawKeyword::parse(contents) {
arena.append(keyword.into_element(), parent);
Some(tail)
} else {
let (tail, comment) = Comment::parse(contents)?;
arena.append(comment, parent);
Some(tail)
}
}
_ => None,
}
}
struct InlinePositions<'a> {
bytes: &'a [u8],
pos: usize,
next: Option<usize>,
}
impl InlinePositions<'_> {
fn new(bytes: &[u8]) -> InlinePositions {
InlinePositions {
bytes,
pos: 0,
next: Some(0),
}
}
}
impl Iterator for InlinePositions<'_> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
lazy_static::lazy_static! {
static ref PRE_BYTES: BytesConst =
bytes!(b'@', b'<', b'[', b' ', b'(', b'{', b'\'', b'"', b'\n');
}
self.next.take().or_else(|| {
PRE_BYTES.find(&self.bytes[self.pos..]).map(|i| {
self.pos += i + 1;
match self.bytes[self.pos - 1] {
b'{' => {
self.next = Some(self.pos);
self.pos - 1
}
b' ' | b'(' | b'\'' | b'"' | b'\n' => self.pos,
_ => self.pos - 1,
}
})
})
}
}
pub fn parse_inlines<'a, T: ElementArena<'a>>(
arena: &mut T,
content: &'a str,
parent: NodeId,
containers: &mut Vec<Container<'a>>,
) {
let mut tail = content;
if let Some(tail_) = parse_inline(tail, arena, containers, parent) {
tail = tail_;
}
while let Some((tail_, i)) = InlinePositions::new(tail.as_bytes())
.filter_map(|i| parse_inline(&tail[i..], arena, containers, parent).map(|tail| (tail, i)))
.next()
{
if i != 0 {
arena.insert_before_last_child(
Element::Text {
value: tail[0..i].into(),
},
parent,
);
}
tail = tail_;
}
if !tail.is_empty() {
arena.append(Element::Text { value: tail.into() }, parent);
}
}
pub fn parse_inline<'a, T: ElementArena<'a>>(
contents: &'a str,
arena: &mut T,
containers: &mut Vec<Container<'a>>,
parent: NodeId,
) -> Option<&'a str> {
if contents.len() < 3 {
return None;
}
let byte = contents.as_bytes()[0];
match byte {
b'@' => {
let (tail, snippet) = Snippet::parse(contents)?;
arena.append(snippet, parent);
Some(tail)
}
b'{' => {
let (tail, macros) = Macros::parse(contents)?;
arena.append(macros, parent);
Some(tail)
}
b'<' => {
if let Some((tail, _content)) = parse_radio_target(contents) {
arena.append(Element::RadioTarget, parent);
Some(tail)
} else if let Some((tail, target)) = Target::parse(contents) {
arena.append(target, parent);
Some(tail)
} else if let Some((tail, timestamp)) = Timestamp::parse_active(contents) {
arena.append(timestamp, parent);
Some(tail)
} else {
let (tail, timestamp) = Timestamp::parse_diary(contents)?;
arena.append(timestamp, parent);
Some(tail)
}
}
b'[' => {
if let Some((tail, fn_ref)) = FnRef::parse(contents) {
arena.append(fn_ref, parent);
Some(tail)
} else if let Some((tail, link)) = Link::parse(contents) {
arena.append(link, parent);
Some(tail)
} else if let Some((tail, cookie)) = Cookie::parse(contents) {
arena.append(cookie, parent);
Some(tail)
} else {
let (tail, timestamp) = Timestamp::parse_inactive(contents)?;
arena.append(timestamp, parent);
Some(tail)
}
}
b'*' | b'+' | b'/' | b'_' | b'=' | b'~' => {
let (tail, emphasis) = Emphasis::parse(contents, byte)?;
let (element, content) = emphasis.into_element();
let is_inline_container = match element {
Element::Bold | Element::Strike | Element::Italic | Element::Underline => true,
_ => false,
};
let node = arena.append(element, parent);
if is_inline_container {
containers.push(Container::Inline { content, node });
}
Some(tail)
}
b's' => {
let (tail, inline_src) = InlineSrc::parse(contents)?;
arena.append(inline_src, parent);
Some(tail)
}
b'c' => {
let (tail, inline_call) = InlineCall::parse(contents)?;
arena.append(inline_call, parent);
Some(tail)
}
_ => None,
}
}
pub fn parse_list<'a, T: ElementArena<'a>>(
arena: &mut T,
contents: &'a str,
parent: NodeId,
containers: &mut Vec<Container<'a>>,
) -> Option<&'a str> {
let (mut tail, (first_item, content)) = ListItem::parse(contents)?;
let first_item_indent = first_item.indent;
let first_item_ordered = first_item.ordered;
let parent = arena.append(Element::Document { pre_blank: 0 }, parent); // placeholder
let node = arena.append(first_item, parent);
containers.push(Container::Block { content, node });
while let Some((tail_, (item, content))) = ListItem::parse(tail) {
if item.indent == first_item_indent {
let node = arena.append(item, parent);
containers.push(Container::Block { content, node });
debug_assert_ne!(tail, tail_);
tail = tail_;
} else {
break;
}
}
let (tail, post_blank) = blank_lines_count(tail);
arena.set(
parent,
List {
indent: first_item_indent,
ordered: first_item_ordered,
post_blank,
},
);
Some(tail)
}
pub fn parse_org_table<'a, T: ElementArena<'a>>(
arena: &mut T,
contents: &'a str,
containers: &mut Vec<Container<'a>>,
parent: NodeId,
) -> &'a str {
let (tail, contents) =
lines_while(|line| line.trim_start().starts_with('|'))(contents).unwrap_or((contents, ""));
let (tail, post_blank) = blank_lines_count(tail);
let mut iter = contents.trim_end().lines().peekable();
let mut lines = vec![];
let mut has_header = false;
// TODO: merge contiguous rules
if let Some(line) = iter.next() {
let line = line.trim_start();
if !line.starts_with("|-") {
lines.push(line);
}
}
while let Some(line) = iter.next() {
let line = line.trim_start();
if iter.peek().is_none() && line.starts_with("|-") {
break;
} else if line.starts_with("|-") {
has_header = true;
}
lines.push(line);
}
let parent = arena.append(
Table::Org {
tblfm: None,
post_blank,
has_header,
},
parent,
);
for line in lines {
if line.starts_with("|-") {
if has_header {
arena.append(Element::TableRow(TableRow::HeaderRule), parent);
has_header = false;
} else {
arena.append(Element::TableRow(TableRow::BodyRule), parent);
}
} else {
if has_header {
let parent = arena.append(Element::TableRow(TableRow::Header), parent);
for content in line.split_terminator('|').skip(1) {
let node = arena.append(Element::TableCell(TableCell::Header), parent);
containers.push(Container::Inline {
content: content.trim(),
node,
});
}
} else {
let parent = arena.append(Element::TableRow(TableRow::Body), parent);
for content in line.split_terminator('|').skip(1) {
let node = arena.append(Element::TableCell(TableCell::Body), parent);
containers.push(Container::Inline {
content: content.trim(),
node,
});
}
}
}
}
tail
}
pub fn blank_lines_count(input: &str) -> (&str, usize) {
crate::parse::combinators::blank_lines_count(input).unwrap_or((input, 0))
}
pub fn parse_headline(input: &str) -> Option<(&str, (&str, usize))> {
let (input_, level) = parse_headline_level(input)?;
let (input_, content) = lines_while(move |line| {
parse_headline_level(line)
.map(|(_, l)| l > level)
.unwrap_or(true)
})(input_)
.unwrap_or((input_, ""));
Some((input_, (&input[0..level + content.len()], level)))
}
pub fn parse_headline_level(input: &str) -> Option<(&str, usize)> {
let (input, stars) = take_while1::<_, _, ()>(|c: char| c == '*')(input).ok()?;
if input.starts_with(' ') || input.starts_with('\n') || input.is_empty() {
Some((input, stars.len()))
} else {
None
}
}

233
src/syntax/block.rs Normal file
View file

@ -0,0 +1,233 @@
use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case},
character::complete::{alpha1, line_ending, space0},
combinator::eof,
sequence::tuple,
IResult, InputTake,
};
use super::{
combinator::{
blank_lines, debug_assert_lossless, line_starts_iter, node, token, trim_line_end,
GreenElement, NodeBuilder,
},
element::element_nodes,
input::Input,
SyntaxKind::*,
};
fn block_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (block_begin, name)) = block_begin_node(input)?;
let (input, pre_blank) = blank_lines(input)?;
let (kind, is_greater_block) = match name {
s if s.eq_ignore_ascii_case("COMMENT") => (COMMENT_BLOCK, false),
s if s.eq_ignore_ascii_case("EXAMPLE") => (EXAMPLE_BLOCK, false),
s if s.eq_ignore_ascii_case("EXPORT") => (EXPORT_BLOCK, false),
s if s.eq_ignore_ascii_case("SRC") => (SOURCE_BLOCK, false),
s if s.eq_ignore_ascii_case("CENTER") => (CENTER_BLOCK, true),
s if s.eq_ignore_ascii_case("QUOTE") => (QUOTE_BLOCK, true),
s if s.eq_ignore_ascii_case("VERSE") => (VERSE_BLOCK, true),
_ => (SPECIAL_BLOCK, true),
};
for (input, contents) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
if let Ok((input, block_end)) = block_end_node(input, name) {
let (input, post_blank) = blank_lines(input)?;
let mut children = vec![block_begin];
children.extend(pre_blank);
if is_greater_block {
children.push(node(BLOCK_CONTENT, element_nodes(contents)?));
} else {
children.push(node(BLOCK_CONTENT, comma_quoted_text_nodes(contents)));
}
children.push(block_end);
children.extend(post_blank);
return Ok((input, node(kind, children)));
}
}
Err(nom::Err::Error(()))
}
fn block_begin_node(input: Input) -> IResult<Input, (GreenElement, &str), ()> {
let (input, (ws, start, name, (argument, ws_, nl))) =
tuple((space0, tag_no_case("#+BEGIN_"), alpha1, trim_line_end))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.text(start);
b.text(name);
b.text(argument);
b.ws(ws_);
b.nl(nl);
Ok((input, (b.finish(BLOCK_BEGIN), name.as_str())))
}
fn block_end_node<'a>(input: Input<'a>, name: &str) -> IResult<Input<'a>, GreenElement, ()> {
let (input, (ws, end, name, ws_, nl)) = tuple((
space0,
tag_no_case("#+END_"),
tag(name),
space0,
alt((line_ending, eof)),
))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.text(end);
b.text(name);
b.ws(ws_);
b.nl(nl);
Ok((input, b.finish(BLOCK_END)))
}
fn comma_quoted_text_nodes(input: Input) -> Vec<GreenElement> {
let mut nodes = vec![];
let s = input.as_str();
let mut start = 0;
for i in line_starts_iter(s) {
// line must start with either ",*" or ",#+"
if s.get(i..i + 2) != Some(",*") && s.get(i..i + 3) != Some(",#+") {
continue;
}
let text = &s[start..i];
if !text.is_empty() {
nodes.push(token(TEXT, text));
}
nodes.push(token(COMMA, ","));
start = i + 1;
}
if !s[start..].is_empty() {
nodes.push(token(TEXT, &s[start..]));
}
nodes
}
pub fn block_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(block_node_base)(input)
}
#[test]
fn test_parse() {
use crate::ast::{ExampleBlock, SourceBlock};
use crate::tests::to_ast;
let to_src_block = to_ast::<SourceBlock>(block_node);
let to_example_block = to_ast::<ExampleBlock>(block_node);
insta::assert_debug_snapshot!(
to_example_block(
r#"#+BEGIN_EXAMPLE
,* headline
,#+block
text
#+END_EXAMPLE"#
).syntax,
@r###"
EXAMPLE_BLOCK@0..59
BLOCK_BEGIN@0..16
TEXT@0..8 "#+BEGIN_"
TEXT@8..15 "EXAMPLE"
TEXT@15..15 ""
NEW_LINE@15..16 "\n"
BLOCK_CONTENT@16..42
COMMA@16..17 ","
TEXT@17..28 "* headline\n"
COMMA@28..29 ","
TEXT@29..42 "#+block\ntext\n"
BLOCK_END@42..59
WHITESPACE@42..46 " "
TEXT@46..52 "#+END_"
TEXT@52..59 "EXAMPLE"
"###
);
insta::assert_debug_snapshot!(
to_src_block(
r#"#+BEGIN_SRC
#+END_SRC"#
).syntax,
@r###"
SOURCE_BLOCK@0..27
BLOCK_BEGIN@0..12
TEXT@0..8 "#+BEGIN_"
TEXT@8..11 "SRC"
TEXT@11..11 ""
NEW_LINE@11..12 "\n"
BLANK_LINE@12..13
NEW_LINE@12..13 "\n"
BLANK_LINE@13..14
NEW_LINE@13..14 "\n"
BLOCK_CONTENT@14..14
BLOCK_END@14..27
WHITESPACE@14..18 " "
TEXT@18..24 "#+END_"
TEXT@24..27 "SRC"
"###
);
insta::assert_debug_snapshot!(
to_src_block(
r#"#+begin_src
#+end_src"#
).syntax,
@r###"
SOURCE_BLOCK@0..25
BLOCK_BEGIN@0..12
TEXT@0..8 "#+begin_"
TEXT@8..11 "src"
TEXT@11..11 ""
NEW_LINE@11..12 "\n"
BLOCK_CONTENT@12..12
BLOCK_END@12..25
WHITESPACE@12..16 " "
TEXT@16..22 "#+end_"
TEXT@22..25 "src"
"###
);
insta::assert_debug_snapshot!(
to_src_block(
r#"#+BEGIN_SRC javascript
alert('Hello World!');
#+END_SRC
"#).syntax,
@r###"
SOURCE_BLOCK@0..69
BLOCK_BEGIN@0..27
TEXT@0..8 "#+BEGIN_"
TEXT@8..11 "SRC"
TEXT@11..22 " javascript"
WHITESPACE@22..26 " "
NEW_LINE@26..27 "\n"
BLOCK_CONTENT@27..50
TEXT@27..50 "alert('Hello World!');\n"
BLOCK_END@50..64
WHITESPACE@50..54 " "
TEXT@54..60 "#+END_"
TEXT@60..63 "SRC"
NEW_LINE@63..64 "\n"
BLANK_LINE@64..65
NEW_LINE@64..65 "\n"
BLANK_LINE@65..69
WHITESPACE@65..69 " "
"###
);
// TODO: more testing
}

137
src/syntax/clock.rs Normal file
View file

@ -0,0 +1,137 @@
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{digit1, line_ending, space0},
combinator::{eof, map, opt},
sequence::tuple,
IResult,
};
use super::{
combinator::{
blank_lines, colon_token, debug_assert_lossless, double_arrow_token, GreenElement,
NodeBuilder,
},
input::Input,
timestamp::{timestamp_active_node, timestamp_inactive_node},
SyntaxKind,
};
pub fn clock_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
space0,
tag("CLOCK:"),
space0,
alt((timestamp_inactive_node, timestamp_active_node)),
opt(tuple((
space0,
double_arrow_token,
space0,
digit1,
colon_token,
digit1,
))),
space0,
alt((line_ending, eof)),
blank_lines,
)),
|(ws, clock, ws_, timestamp, duration, ws__, nl, post_blank)| {
let mut b = NodeBuilder::new();
b.ws(ws);
b.text(clock);
b.ws(ws_);
b.push(timestamp);
if let Some((ws, double_arrow, ws_, hour, colon, minute)) = duration {
b.ws(ws);
b.push(double_arrow);
b.ws(ws_);
b.text(hour);
b.push(colon);
b.text(minute);
}
b.ws(ws__);
b.nl(nl);
b.children.extend(post_blank);
b.finish(SyntaxKind::CLOCK)
},
))(input)
}
#[test]
fn parse() {
use crate::ast::Clock;
use crate::tests::to_ast;
let to_clock = to_ast::<Clock>(clock_node);
insta::assert_debug_snapshot!(
to_clock("CLOCK: [2003-09-16 Tue 09:39]").syntax,
@r###"
CLOCK@0..29
TEXT@0..6 "CLOCK:"
WHITESPACE@6..7 " "
TIMESTAMP_INACTIVE@7..29
L_BRACKET@7..8 "["
TIMESTAMP_YEAR@8..12 "2003"
MINUS@12..13 "-"
TIMESTAMP_MONTH@13..15 "09"
MINUS@15..16 "-"
TIMESTAMP_DAY@16..18 "16"
WHITESPACE@18..19 " "
TIMESTAMP_DAYNAME@19..22 "Tue"
WHITESPACE@22..23 " "
TIMESTAMP_HOUR@23..25 "09"
COLON@25..26 ":"
TIMESTAMP_MINUTE@26..28 "39"
R_BRACKET@28..29 "]"
"###
);
insta::assert_debug_snapshot!(
to_clock("CLOCK: [2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39] => 1:00\n\n").syntax,
@r###"
CLOCK@0..64
TEXT@0..6 "CLOCK:"
WHITESPACE@6..7 " "
TIMESTAMP_INACTIVE@7..53
L_BRACKET@7..8 "["
TIMESTAMP_YEAR@8..12 "2003"
MINUS@12..13 "-"
TIMESTAMP_MONTH@13..15 "09"
MINUS@15..16 "-"
TIMESTAMP_DAY@16..18 "16"
WHITESPACE@18..19 " "
TIMESTAMP_DAYNAME@19..22 "Tue"
WHITESPACE@22..23 " "
TIMESTAMP_HOUR@23..25 "09"
COLON@25..26 ":"
TIMESTAMP_MINUTE@26..28 "39"
R_BRACKET@28..29 "]"
MINUS2@29..31 "--"
L_BRACKET@31..32 "["
TIMESTAMP_YEAR@32..36 "2003"
MINUS@36..37 "-"
TIMESTAMP_MONTH@37..39 "09"
MINUS@39..40 "-"
TIMESTAMP_DAY@40..42 "16"
WHITESPACE@42..43 " "
TIMESTAMP_DAYNAME@43..46 "Tue"
WHITESPACE@46..47 " "
TIMESTAMP_HOUR@47..49 "10"
COLON@49..50 ":"
TIMESTAMP_MINUTE@50..52 "39"
R_BRACKET@52..53 "]"
WHITESPACE@53..54 " "
DOUBLE_ARROW@54..56 "=>"
WHITESPACE@56..58 " "
TEXT@58..59 "1"
COLON@59..60 ":"
TEXT@60..62 "00"
NEW_LINE@62..63 "\n"
BLANK_LINE@63..64
NEW_LINE@63..64 "\n"
"###
);
}

259
src/syntax/combinator.rs Normal file
View file

@ -0,0 +1,259 @@
use std::iter::once;
use memchr::{memchr, memchr_iter};
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{line_ending, space0},
combinator::eof,
sequence::tuple,
AsBytes, IResult, InputLength, InputTake, Parser,
};
use rowan::{GreenNode, GreenToken, Language, NodeOrToken};
use super::{input::Input, OrgLanguage, SyntaxKind, SyntaxKind::*};
pub type GreenElement = NodeOrToken<GreenNode, GreenToken>;
#[inline]
pub fn token(kind: SyntaxKind, input: &str) -> GreenElement {
GreenElement::Token(GreenToken::new(OrgLanguage::kind_to_raw(kind), input))
}
#[inline]
pub fn node<I>(kind: SyntaxKind, children: I) -> GreenElement
where
I: IntoIterator<Item = GreenElement>,
I::IntoIter: ExactSizeIterator,
{
GreenElement::Node(GreenNode::new(OrgLanguage::kind_to_raw(kind), children))
}
macro_rules! token_parser {
($name:ident, $token:literal, $kind:ident) => {
#[doc = "Recognizes `"]
#[doc = $token]
#[doc = "` and returns GreenToken"]
pub fn $name(input: Input) -> IResult<Input, GreenElement, ()> {
let (i, o) = tag($token)(input)?;
Ok((i, token($kind, o.as_str())))
}
};
}
token_parser!(l_bracket_token, "[", L_BRACKET);
token_parser!(r_bracket_token, "]", R_BRACKET);
token_parser!(l_bracket2_token, "[[", L_BRACKET2);
token_parser!(r_bracket2_token, "]]", R_BRACKET2);
token_parser!(l_parens_token, "(", L_PARENS);
token_parser!(r_parens_token, ")", R_PARENS);
token_parser!(l_angle_token, "<", L_ANGLE);
token_parser!(r_angle_token, ">", R_ANGLE);
token_parser!(l_curly_token, "{", L_CURLY);
token_parser!(r_curly_token, "}", R_CURLY);
token_parser!(l_curly3_token, "{{{", L_CURLY3);
token_parser!(r_curly3_token, "}}}", R_CURLY3);
token_parser!(l_angle2_token, "<<", L_ANGLE2);
token_parser!(r_angle2_token, ">>", R_ANGLE2);
token_parser!(l_angle3_token, "<<<", L_ANGLE3);
token_parser!(r_angle3_token, ">>>", R_ANGLE3);
token_parser!(at_token, "@", AT);
token_parser!(at2_token, "@@", AT2);
token_parser!(minus2_token, "--", MINUS2);
// token_parser!(percent_token, "%", PERCENT);
token_parser!(percent2_token, "%%", PERCENT2);
// token_parser!(slash_token, "/", SLASH);
// token_parser!(underscore_token, "_", UNDERSCORE);
// token_parser!(star_token, "*", STAR);
token_parser!(plus_token, "+", PLUS);
token_parser!(minus_token, "-", MINUS);
token_parser!(colon_token, ":", COLON);
token_parser!(colon2_token, "::", COLON2);
token_parser!(pipe_token, "|", PIPE);
// token_parser!(equal_token, "=", EQUAL);
// token_parser!(tilde_token, "~", TILDE);
token_parser!(hash_plus_token, "#+", HASH_PLUS);
token_parser!(hash_token, "#", HASH);
token_parser!(double_arrow_token, "=>", DOUBLE_ARROW);
pub fn debug_assert_lossless<'a, F>(
mut f: F,
) -> impl FnMut(Input<'a>) -> IResult<Input<'a>, GreenElement, ()>
where
F: Parser<Input<'a>, GreenElement, ()>,
{
move |input: Input| {
let (i, o) = f.parse(input)?;
debug_assert_eq!(
&input.as_str()[0..(input.input_len() - i.input_len())],
&o.to_string(),
"parser must be lossless"
);
Ok((i, o))
}
}
/// Takes all blank lines
pub fn blank_lines(input: Input) -> IResult<Input, Vec<GreenElement>, ()> {
let mut lines = vec![];
let mut i = input;
while !i.is_empty() {
match tuple::<_, _, (), _>((space0, alt((line_ending, eof))))(i) {
Ok((input, (ws, nl))) => {
let mut b = NodeBuilder::new();
b.ws(ws);
b.nl(nl);
lines.push(b.finish(BLANK_LINE));
i = input;
}
_ => break,
}
}
Ok((i, lines))
}
#[test]
fn test_blank_lines() {
use crate::config::ParseConfig;
let config = &ParseConfig::default();
let (input, output) = blank_lines(("", config).into()).unwrap();
assert_eq!(input.as_str(), "");
assert_eq!(output, vec![]);
let (input, output) = blank_lines((" t", config).into()).unwrap();
assert_eq!(input.as_str(), " t");
assert_eq!(output, vec![]);
let (input, output) = blank_lines((" \r\n\n\t\t\r\n \n ", config).into()).unwrap();
assert_eq!(input.as_str(), "");
assert_eq!(output.len(), 5);
assert_eq!(output[0].to_string(), " \r\n");
assert_eq!(output[1].to_string(), "\n");
assert_eq!(output[2].to_string(), "\t\t\r\n");
assert_eq!(output[3].to_string(), " \n");
assert_eq!(output[4].to_string(), " ");
let (input, output) =
blank_lines((" \r\n\n\t\t\r\n \n t\n \r\n\n\t\t\r\n \n", config).into()).unwrap();
assert_eq!(input.as_str(), " t\n \r\n\n\t\t\r\n \n");
assert_eq!(output.len(), 4);
assert_eq!(output[0].to_string(), " \r\n");
assert_eq!(output[1].to_string(), "\n");
assert_eq!(output[2].to_string(), "\t\t\r\n");
assert_eq!(output[3].to_string(), " \n");
}
/// Returns 1. anything before trailing whitespace, 2. whitespace itself, 3. line feeding
pub fn trim_line_end(input: Input) -> IResult<Input, (Input, Input, Input), ()> {
let (input, line) = input.take_split(
memchr(b'\n', input.as_bytes())
.map(|i| i + 1)
.unwrap_or(input.input_len()),
);
let (ws_and_nl, contents) = line.take_split(
line.as_bytes()
.iter()
.rposition(|u| !u.is_ascii_whitespace())
.map(|i| i + 1)
.unwrap_or(0),
);
let (nl, ws) = space0(ws_and_nl)?;
Ok((input, (contents, ws, nl)))
}
#[test]
fn test_trim_line_end() {
use crate::config::ParseConfig;
let config = &ParseConfig::default();
let (input, output) = trim_line_end(("", config).into()).unwrap();
assert_eq!(input.as_str(), "");
assert_eq!(output.0.as_str(), "");
assert_eq!(output.1.as_str(), "");
assert_eq!(output.2.as_str(), "");
let (input, output) = trim_line_end(("* hello, world :abc:", config).into()).unwrap();
assert_eq!(input.as_str(), "");
assert_eq!(output.0.as_str(), "* hello, world :abc:");
assert_eq!(output.1.as_str(), "");
assert_eq!(output.2.as_str(), "");
let (input, output) =
trim_line_end(("* hello, world :abc: \r\nrest\n", config).into()).unwrap();
assert_eq!(input.as_str(), "rest\n");
assert_eq!(output.0.as_str(), "* hello, world :abc:");
assert_eq!(output.1.as_str(), " ");
assert_eq!(output.2.as_str(), "\r\n");
}
/// Returns an iterator of positions of line start, including zero
pub fn line_starts_iter(s: &str) -> impl Iterator<Item = usize> + '_ {
once(0).chain(memchr_iter(b'\n', s.as_bytes()).map(|i| i + 1))
}
/// Returns an iterator of positions of line end, including eof
pub fn line_ends_iter(s: &str) -> impl Iterator<Item = usize> + '_ {
memchr_iter(b'\n', s.as_bytes())
.map(|i| i + 1)
.chain(once(s.len()))
}
pub struct NodeBuilder {
pub children: Vec<GreenElement>,
}
impl NodeBuilder {
pub fn new() -> NodeBuilder {
NodeBuilder { children: vec![] }
}
pub fn ws(&mut self, i: Input) {
if !i.is_empty() {
debug_assert!(i.bytes().all(|c| c.is_ascii_whitespace()));
self.children.push(i.ws_token())
}
}
pub fn nl(&mut self, i: Input) {
if !i.is_empty() {
debug_assert!(
i.s == "\n" || i.s == "\r\n",
"{:?} should be a new line",
i.s
);
self.children.push(i.nl_token())
}
}
pub fn text(&mut self, i: Input) {
self.children.push(i.text_token())
}
pub fn token(&mut self, kind: SyntaxKind, i: Input) {
self.children.push(i.token(kind))
}
pub fn push(&mut self, elem: GreenElement) {
self.children.push(elem)
}
pub fn push_opt(&mut self, elem: Option<GreenElement>) {
if let Some(elem) = elem {
self.children.push(elem)
}
}
pub fn len(&self) -> usize {
self.children.len()
}
pub fn finish(self, kind: SyntaxKind) -> GreenElement {
GreenElement::Node(GreenNode::new(kind.into(), self.children))
}
}

85
src/syntax/comment.rs Normal file
View file

@ -0,0 +1,85 @@
use nom::{IResult, InputTake};
use super::{
combinator::{blank_lines, debug_assert_lossless, line_ends_iter, node, GreenElement},
input::Input,
SyntaxKind,
};
fn comment_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let mut start = 0;
for i in line_ends_iter(input.as_str()) {
let line = &input.as_str()[start..i];
let trimmed = line.trim_start();
if trimmed == "#" || trimmed == "#\n" || trimmed == "#\r\n" || trimmed.starts_with("# ") {
start = i;
} else {
break;
}
}
if start == 0 {
return Err(nom::Err::Error(()));
}
let (input, contents) = input.take_split(start);
let (input, post_blank) = blank_lines(input)?;
let mut children = vec![];
children.push(contents.text_token());
children.extend(post_blank);
Ok((input, node(SyntaxKind::COMMENT, children)))
}
pub fn comment_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(comment_node_base)(input)
}
#[test]
fn parse() {
use crate::{
syntax::{comment::comment_node, input::Input, SyntaxNode},
ParseConfig,
};
let t = |input: &str| {
SyntaxNode::new_root(
comment_node(Input {
s: input,
c: &ParseConfig::default(),
})
.unwrap()
.1
.into_node()
.unwrap(),
)
};
insta::assert_debug_snapshot!(
t("#"),
@r###"
COMMENT@0..1
TEXT@0..1 "#"
"###
);
insta::assert_debug_snapshot!(
t("#\n # a\n #\n\n"),
@r###"
COMMENT@0..12
TEXT@0..11 "#\n # a\n #\n"
BLANK_LINE@11..12
NEW_LINE@11..12 "\n"
"###
);
insta::assert_debug_snapshot!(
t("#\na\n #\n\n"),
@r###"
COMMENT@0..2
TEXT@0..2 "#\n"
"###
);
}

144
src/syntax/cookie.rs Normal file
View file

@ -0,0 +1,144 @@
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::digit0,
combinator::map,
sequence::{pair, separated_pair, tuple},
IResult,
};
use super::{
combinator::{
debug_assert_lossless, l_bracket_token, node, r_bracket_token, token, GreenElement,
},
input::Input,
SyntaxKind::*,
};
pub fn cookie_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
l_bracket_token,
alt((
separated_pair(digit0, tag("/"), digit0),
pair(digit0, tag("%")),
)),
r_bracket_token,
)),
|(l_bracket, value, r_bracket)| {
let mut children = vec![l_bracket];
children.push(token(TEXT, value.0.as_str()));
match value.1.as_str() {
"%" => {
children.push(token(PERCENT, value.1.as_str()));
}
_ => {
children.push(token(SLASH, "/"));
children.push(token(TEXT, value.1.as_str()));
}
}
children.push(r_bracket);
node(COOKIE, children)
},
))(input)
}
#[test]
fn parse() {
use crate::ast::Cookie;
use crate::tests::to_ast;
use crate::ParseConfig;
let to_cookie = to_ast::<Cookie>(cookie_node);
insta::assert_debug_snapshot!(
to_cookie("[1/10]").syntax,
@r###"
COOKIE@0..6
L_BRACKET@0..1 "["
TEXT@1..2 "1"
SLASH@2..3 "/"
TEXT@3..5 "10"
R_BRACKET@5..6 "]"
"###
);
insta::assert_debug_snapshot!(
to_cookie("[1/1000]").syntax,
@r###"
COOKIE@0..8
L_BRACKET@0..1 "["
TEXT@1..2 "1"
SLASH@2..3 "/"
TEXT@3..7 "1000"
R_BRACKET@7..8 "]"
"###
);
insta::assert_debug_snapshot!(
to_cookie("[10%]").syntax,
@r###"
COOKIE@0..5
L_BRACKET@0..1 "["
TEXT@1..3 "10"
PERCENT@3..4 "%"
R_BRACKET@4..5 "]"
"###
);
insta::assert_debug_snapshot!(
to_cookie("[%]").syntax,
@r###"
COOKIE@0..3
L_BRACKET@0..1 "["
TEXT@1..1 ""
PERCENT@1..2 "%"
R_BRACKET@2..3 "]"
"###
);
insta::assert_debug_snapshot!(
to_cookie("[/]").syntax,
@r###"
COOKIE@0..3
L_BRACKET@0..1 "["
TEXT@1..1 ""
SLASH@1..2 "/"
TEXT@2..2 ""
R_BRACKET@2..3 "]"
"###
);
insta::assert_debug_snapshot!(
to_cookie("[100/]").syntax,
@r###"
COOKIE@0..6
L_BRACKET@0..1 "["
TEXT@1..4 "100"
SLASH@4..5 "/"
TEXT@5..5 ""
R_BRACKET@5..6 "]"
"###
);
insta::assert_debug_snapshot!(
to_cookie("[/100]").syntax,
@r###"
COOKIE@0..6
L_BRACKET@0..1 "["
TEXT@1..1 ""
SLASH@1..2 "/"
TEXT@2..5 "100"
R_BRACKET@5..6 "]"
"###
);
let config = &ParseConfig::default();
assert!(cookie_node(("[10% ]", config).into()).is_err());
assert!(cookie_node(("[1//100]", config).into()).is_err());
assert!(cookie_node(("[1\\100]", config).into()).is_err());
assert!(cookie_node(("[10%%]", config).into()).is_err());
}

128
src/syntax/document.rs Normal file
View file

@ -0,0 +1,128 @@
use nom::{
combinator::{iterator, opt},
IResult,
};
use super::{
combinator::{blank_lines, debug_assert_lossless, node, GreenElement},
headline::{headline_node, section_node},
input::Input,
SyntaxKind::*,
};
pub fn document_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(document_node_base)(input)
}
fn document_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let mut children = vec![];
let (input, pre_blank) = blank_lines(input)?;
children.extend(pre_blank);
let (input, section) = opt(section_node)(input)?;
if let Some(section) = section {
children.push(section);
}
let mut it = iterator(input, headline_node);
children.extend(&mut it);
let (input, _) = it.finish()?;
debug_assert!(input.is_empty());
Ok((input, node(DOCUMENT, children)))
}
#[test]
fn parse() {
use crate::ast::Document;
use crate::tests::to_ast;
let to_document = to_ast::<Document>(document_node);
insta::assert_debug_snapshot!(
to_document("").syntax,
@r###"
DOCUMENT@0..0
"###
);
insta::assert_debug_snapshot!(
to_document("\n \n\n").syntax,
@r###"
DOCUMENT@0..5
BLANK_LINE@0..1
NEW_LINE@0..1 "\n"
BLANK_LINE@1..4
WHITESPACE@1..3 " "
NEW_LINE@3..4 "\n"
BLANK_LINE@4..5
NEW_LINE@4..5 "\n"
"###
);
insta::assert_debug_snapshot!(
to_document("section").syntax,
@r###"
DOCUMENT@0..7
SECTION@0..7
PARAGRAPH@0..7
TEXT@0..7 "section"
"###
);
insta::assert_debug_snapshot!(
to_document("\n* section").syntax,
@r###"
DOCUMENT@0..10
BLANK_LINE@0..1
NEW_LINE@0..1 "\n"
HEADLINE@1..10
HEADLINE_STARS@1..2 "*"
WHITESPACE@2..3 " "
HEADLINE_TITLE@3..10
TEXT@3..10 "section"
"###
);
insta::assert_debug_snapshot!(
to_document("\n** heading 2\n* heading 1").syntax,
@r###"
DOCUMENT@0..25
BLANK_LINE@0..1
NEW_LINE@0..1 "\n"
HEADLINE@1..14
HEADLINE_STARS@1..3 "**"
WHITESPACE@3..4 " "
HEADLINE_TITLE@4..13
TEXT@4..13 "heading 2"
NEW_LINE@13..14 "\n"
HEADLINE@14..25
HEADLINE_STARS@14..15 "*"
WHITESPACE@15..16 " "
HEADLINE_TITLE@16..25
TEXT@16..25 "heading 1"
"###
);
insta::assert_debug_snapshot!(
to_document("section\n** heading 2\n*heading 1").syntax,
@r###"
DOCUMENT@0..31
SECTION@0..8
PARAGRAPH@0..8
TEXT@0..8 "section\n"
HEADLINE@8..31
HEADLINE_STARS@8..10 "**"
WHITESPACE@10..11 " "
HEADLINE_TITLE@11..20
TEXT@11..20 "heading 2"
NEW_LINE@20..21 "\n"
SECTION@21..31
PARAGRAPH@21..31
TEXT@21..31 "*heading 1"
"###
);
}

200
src/syntax/drawer.rs Normal file
View file

@ -0,0 +1,200 @@
use nom::{
branch::alt,
bytes::complete::{tag_no_case, take_while1},
character::complete::{line_ending, space0, space1},
combinator::{eof, iterator, map, opt},
sequence::tuple,
IResult, InputTake,
};
use super::{
combinator::{
blank_lines, colon_token, debug_assert_lossless, line_starts_iter, node, plus_token,
trim_line_end, GreenElement, NodeBuilder,
},
input::Input,
SyntaxKind::*,
};
fn drawer_begin_node(input: Input) -> IResult<Input, (GreenElement, &str), ()> {
let mut b = NodeBuilder::new();
let (input, (ws, colon, name, colon_, ws_, nl)) = tuple((
space0,
colon_token,
take_while1(|c: char| c.is_ascii_alphabetic() || c == '-' || c == '_'),
colon_token,
space0,
alt((line_ending, eof)),
))(input)?;
b.ws(ws);
b.push(colon);
b.text(name);
b.push(colon_);
b.ws(ws_);
b.nl(nl);
Ok((input, (b.finish(DRAWER_BEGIN), name.as_str())))
}
fn drawer_end_node(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (ws, colon, end, colon_, ws_, nl)) = tuple((
space0,
colon_token,
tag_no_case("END"),
colon_token,
space0,
alt((line_ending, eof)),
))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.push(colon);
b.text(end);
b.push(colon_);
b.ws(ws_);
b.nl(nl);
Ok((input, b.finish(DRAWER_END)))
}
fn drawer_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (begin, _)) = drawer_begin_node(input)?;
let (input, pre_blank) = blank_lines(input)?;
for (input, contents) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
if let Ok((input, end)) = drawer_end_node(input) {
let (input, post_blank) = blank_lines(input)?;
let mut children = vec![begin];
children.extend(pre_blank);
children.push(contents.text_token());
children.push(end);
children.extend(post_blank);
return Ok((input, node(DRAWER, children)));
}
}
Err(nom::Err::Error(()))
}
fn property_drawer_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (begin, name)) = drawer_begin_node(input)?;
if name != "PROPERTIES" {
return Err(nom::Err::Error(()));
}
let mut children = vec![begin];
let mut it = iterator(input, node_property_node);
children.extend(&mut it);
let (input, _) = it.finish()?;
let (input, end) = drawer_end_node(input)?;
children.push(end);
Ok((input, node(PROPERTY_DRAWER, children)))
}
fn node_property_node(input: Input) -> IResult<Input, GreenElement, ()> {
map(
tuple((
space0,
colon_token,
take_while1(|c| c != ':' && c != '+'),
opt(plus_token),
colon_token,
space1,
trim_line_end,
)),
|(ws, colon, name, plus, colon_, ws_, (value, ws__, nl))| {
let mut b = NodeBuilder::new();
b.ws(ws);
b.push(colon);
b.text(name);
b.push_opt(plus);
b.push(colon_);
b.ws(ws_);
b.text(value);
b.ws(ws__);
b.nl(nl);
b.finish(NODE_PROPERTY)
},
)(input)
}
#[tracing::instrument(skip(input), fields(input = input.s))]
pub fn property_drawer_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(property_drawer_node_base)(input)
}
#[tracing::instrument(skip(input), fields(input = input.s))]
pub fn drawer_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(drawer_node_base)(input)
}
#[test]
fn parse() {
use crate::{ast::Drawer, tests::to_ast, ParseConfig};
let to_drawer = to_ast::<Drawer>(drawer_node);
insta::assert_debug_snapshot!(
to_drawer(
r#":DRAWER:
:CUSTOM_ID: id
:END:"#
).syntax,
@r###"
DRAWER@0..33
DRAWER_BEGIN@0..9
COLON@0..1 ":"
TEXT@1..7 "DRAWER"
COLON@7..8 ":"
NEW_LINE@8..9 "\n"
TEXT@9..26 " :CUSTOM_ID: id\n"
DRAWER_END@26..33
WHITESPACE@26..28 " "
COLON@28..29 ":"
TEXT@29..32 "END"
COLON@32..33 ":"
"###
);
insta::assert_debug_snapshot!(
to_drawer(
r#":DRAWER:
:END:
"#
).syntax,
@r###"
DRAWER@0..19
DRAWER_BEGIN@0..9
COLON@0..1 ":"
TEXT@1..7 "DRAWER"
COLON@7..8 ":"
NEW_LINE@8..9 "\n"
BLANK_LINE@9..10
NEW_LINE@9..10 "\n"
TEXT@10..10 ""
DRAWER_END@10..18
WHITESPACE@10..12 " "
COLON@12..13 ":"
TEXT@13..16 "END"
COLON@16..17 ":"
NEW_LINE@17..18 "\n"
BLANK_LINE@18..19
NEW_LINE@18..19 "\n"
"###
);
let config = &ParseConfig::default();
// https://github.com/PoiScript/orgize/issues/9
assert!(drawer_node((":SPAGHETTI:\n", config).into()).is_err());
}

112
src/syntax/dyn_block.rs Normal file
View file

@ -0,0 +1,112 @@
use nom::{
branch::alt,
bytes::complete::tag_no_case,
character::complete::{alpha1, line_ending, space0, space1},
combinator::eof,
sequence::tuple,
IResult, InputTake,
};
use super::{
combinator::{
blank_lines, debug_assert_lossless, line_starts_iter, node, trim_line_end, GreenElement,
NodeBuilder,
},
input::Input,
SyntaxKind::*,
};
fn dyn_block_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, begin) = dyn_block_begin_node(input)?;
let (input, pre_blank) = blank_lines(input)?;
for (input, contents) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
if let Ok((input, end)) = dyn_block_end_node(input) {
let (input, post_blank) = blank_lines(input)?;
let mut children = vec![begin];
children.extend(pre_blank);
children.push(contents.text_token());
children.push(end);
children.extend(post_blank);
return Ok((input, node(DYN_BLOCK, children)));
}
}
Err(nom::Err::Error(()))
}
fn dyn_block_begin_node(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (ws, begin, ws_, name, (args, ws__, nl))) = tuple((
space0,
tag_no_case("#+BEGIN:"),
space1,
alpha1,
trim_line_end,
))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.text(begin);
b.ws(ws_);
b.text(name);
b.text(args);
b.ws(ws__);
b.nl(nl);
Ok((input, b.finish(DYN_BLOCK_BEGIN)))
}
fn dyn_block_end_node(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (ws, end, ws_, nl)) = tuple((
space0,
tag_no_case("#+END:"),
space0,
alt((line_ending, eof)),
))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.text(end);
b.ws(ws_);
b.nl(nl);
Ok((input, b.finish(DYN_BLOCK_END)))
}
pub fn dyn_block_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(dyn_block_node_base)(input)
}
#[test]
fn parse() {
use crate::{ast::DynBlock, tests::to_ast};
let to_dyn_block = to_ast::<DynBlock>(dyn_block_node);
insta::assert_debug_snapshot!(
to_dyn_block(
r#"#+BEGIN: clocktable :scope file
CONTENTS
#+END:
"#).syntax,
@r###"
DYN_BLOCK@0..53
DYN_BLOCK_BEGIN@0..32
TEXT@0..8 "#+BEGIN:"
WHITESPACE@8..9 " "
TEXT@9..19 "clocktable"
TEXT@19..31 " :scope file"
NEW_LINE@31..32 "\n"
BLANK_LINE@32..33
NEW_LINE@32..33 "\n"
TEXT@33..42 "CONTENTS\n"
DYN_BLOCK_END@42..49
TEXT@42..48 "#+END:"
NEW_LINE@48..49 "\n"
BLANK_LINE@49..53
WHITESPACE@49..53 " "
"###
);
}

235
src/syntax/element.rs Normal file
View file

@ -0,0 +1,235 @@
use nom::{AsBytes, IResult, InputTake};
use super::{
block::block_node,
clock::clock_node,
combinator::{line_starts_iter, GreenElement},
comment::comment_node,
drawer::drawer_node,
dyn_block::dyn_block_node,
fixed_width::fixed_width_node,
fn_def::fn_def_node,
input::Input,
keyword::keyword_node,
list::list_node,
paragraph::paragraph_nodes,
rule::rule_node,
table::{org_table_node, table_el_node},
};
/// Parses input into multiple element
#[tracing::instrument(skip(input), fields(input = input.s))]
pub fn element_nodes(input: Input) -> Result<Vec<GreenElement>, nom::Err<()>> {
// TODO:
// debug_assert!(!input.is_empty());
let nodes = element_nodes_base(input)?;
debug_assert_eq!(
input.as_str(),
nodes.iter().fold(String::new(), |s, n| s + &n.to_string()),
"parser must be lossless"
);
Ok(nodes)
}
/// Parses input into multiple elements
///
/// input must not contains blank line in the beginning
fn element_nodes_base(input: Input) -> Result<Vec<GreenElement>, nom::Err<()>> {
#[derive(PartialEq, Eq)]
enum PreviousLine {
None,
BlankLine,
AffiliatedKeyword,
Other,
}
let mut children = vec![];
let mut i = input;
let mut previous_line = PreviousLine::None;
'l: loop {
for (input, head) in line_starts_iter(i.as_str()).map(|idx| i.take_split(idx)) {
// find the first byte that's not a whitespace
let trimmed = input.as_str().trim_start_matches(|c| c == ' ' || c == '\t');
// if this line is an affiliated keyword, that skip it
if is_affiliated_keyword(trimmed) {
if previous_line == PreviousLine::BlankLine {
children.extend(paragraph_nodes(head)?);
}
previous_line = PreviousLine::AffiliatedKeyword;
continue;
}
// if this line is a blank line
if is_blank_line(trimmed) {
if previous_line == PreviousLine::AffiliatedKeyword {
previous_line = PreviousLine::BlankLine;
if let Ok((input, node)) = keyword_node(input) {
if !head.is_empty() {
children.extend(paragraph_nodes(head)?);
}
children.push(node);
i = input;
continue 'l;
}
}
continue;
}
if let Ok((input, node)) = match trimmed.bytes().next() {
Some(b'[') => fn_def_node(input),
Some(b'0'..=b'9') | Some(b'*') => list_node(input),
Some(b'C') => clock_node(input),
Some(b'-') => rule_node(input).or_else(|_| list_node(input)),
Some(b':') => drawer_node(input).or_else(|_| fixed_width_node(input)),
Some(b'|') => org_table_node(input),
Some(b'+') => table_el_node(input).or_else(|_| list_node(input)),
Some(b'#') => block_node(input)
.or_else(|_| keyword_node(input))
.or_else(|_| dyn_block_node(input))
.or_else(|_| comment_node(input)),
_ => Err(nom::Err::Error(())),
} {
if !head.is_empty() {
children.extend(paragraph_nodes(head)?);
}
children.push(node);
i = input;
continue 'l;
}
}
break;
}
if !i.is_empty() {
children.extend(paragraph_nodes(i)?);
}
Ok(children)
}
pub fn is_affiliated_keyword(line: &str) -> bool {
line.starts_with("#+CAPTION:")
|| line.starts_with("#+DATA:")
|| line.starts_with("#+HEADER:")
|| line.starts_with("#+HEADERS:")
|| line.starts_with("#+LABEL:")
|| line.starts_with("#+NAME:")
|| line.starts_with("#+PLOT:")
|| line.starts_with("#+RESNAME:")
|| line.starts_with("#+RESULT:")
|| line.starts_with("#+RESULTS:")
|| line.starts_with("#+SOURCE:")
|| line.starts_with("#+SRCNAME:")
|| line.starts_with("#+TBLNAME:")
|| line.starts_with("#+ATTR_")
}
pub fn is_blank_line(line: &str) -> bool {
matches!(line.bytes().next(), None | Some(b'\n') | Some(b'\r'))
}
pub fn element_node(input: Input) -> IResult<Input, GreenElement, ()> {
let mut has_affiliated_keyword = false;
for offset in line_starts_iter(input.as_str()) {
// find the first byte that's not a whitespace
let Some(idx) = input.as_bytes()[offset..]
.iter()
.position(|b| *b != b' ' && *b != b'\t')
else {
break;
};
let line = &input.as_str()[(idx + offset)..];
// if this line is an affiliated keyword, that we skip it
if line.starts_with("#+CAPTION:")
|| line.starts_with("#+DATA:")
|| line.starts_with("#+HEADER:")
|| line.starts_with("#+HEADERS:")
|| line.starts_with("#+LABEL:")
|| line.starts_with("#+NAME:")
|| line.starts_with("#+PLOT:")
|| line.starts_with("#+RESNAME:")
|| line.starts_with("#+RESULT:")
|| line.starts_with("#+RESULTS:")
|| line.starts_with("#+SOURCE:")
|| line.starts_with("#+SRCNAME:")
|| line.starts_with("#+TBLNAME:")
|| line.starts_with("#+ATTR_")
{
has_affiliated_keyword = true;
continue;
}
return match input.as_bytes()[idx + offset] {
b'[' => fn_def_node(input),
b'0'..=b'9' | b'*' => list_node(input),
b'C' => clock_node(input),
b'-' => rule_node(input).or_else(|_| list_node(input)),
b':' => drawer_node(input).or_else(|_| fixed_width_node(input)),
b'|' => org_table_node(input),
b'+' => table_el_node(input).or_else(|_| list_node(input)),
b'#' => block_node(input)
.or_else(|_| keyword_node(input))
.or_else(|_| dyn_block_node(input))
.or_else(|_| comment_node(input)),
_ => Err(nom::Err::Error(())),
};
}
// we find an affiliated keyword, but it's not followed by any element
// in this case, we treat it as a simple keyword
return Err(nom::Err::Error(()));
}
#[test]
fn parse() {
use crate::syntax::{SyntaxKind, SyntaxNode};
use crate::{syntax::combinator::node, ParseConfig};
let t = |input: &str| {
let config = &ParseConfig::default();
let children = element_nodes((input, config).into()).unwrap();
SyntaxNode::new_root(node(SyntaxKind::SECTION, children).into_node().unwrap())
};
insta::assert_debug_snapshot!(
t(r#"a
b"#),
@r###"
SECTION@0..4
PARAGRAPH@0..3
TEXT@0..2 "a\n"
BLANK_LINE@2..3
NEW_LINE@2..3 "\n"
PARAGRAPH@3..4
TEXT@3..4 "b"
"###
);
insta::assert_debug_snapshot!(
t("#+ATTR_HTML: :width 300px\n[[./img/a.jpg]]"),
@r###"
SECTION@0..41
KEYWORD@0..26
HASH_PLUS@0..2 "#+"
TEXT@2..11 "ATTR_HTML"
COLON@11..12 ":"
TEXT@12..25 " :width 300px"
NEW_LINE@25..26 "\n"
PARAGRAPH@26..41
LINK@26..41
L_BRACKET2@26..28 "[["
LINK_PATH@28..39 "./img/a.jpg"
R_BRACKET2@39..41 "]]"
"###
)
}

146
src/syntax/emphasis.rs Normal file
View file

@ -0,0 +1,146 @@
use bytecount::count;
use memchr::memchr_iter;
use nom::{combinator::map, AsBytes, IResult, Slice};
use super::{
combinator::{debug_assert_lossless, node, token, GreenElement},
input::Input,
object::object_nodes,
SyntaxKind::*,
};
pub fn bold_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(emphasis(b'*'), |contents| {
let mut children = vec![token(STAR, "*")];
children.extend(object_nodes(contents));
children.push(token(STAR, "*"));
node(BOLD, children)
}))(input)
}
pub fn code_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(emphasis(b'~'), |contents| {
node(
CODE,
[token(TILDE, "~"), contents.text_token(), token(TILDE, "~")],
)
}))(input)
}
pub fn strike_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(emphasis(b'+'), |contents| {
let mut children = vec![token(PLUS, "+")];
children.extend(object_nodes(contents));
children.push(token(PLUS, "+"));
node(STRIKE, children)
}))(input)
}
pub fn verbatim_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(emphasis(b'='), |contents| {
node(
VERBATIM,
[token(EQUAL, "="), contents.text_token(), token(EQUAL, "=")],
)
}))(input)
}
pub fn underline_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(emphasis(b'_'), |contents| {
let mut children = vec![token(UNDERSCORE, "_")];
children.extend(object_nodes(contents));
children.push(token(UNDERSCORE, "_"));
node(UNDERLINE, children)
}))(input)
}
pub fn italic_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(emphasis(b'/'), |contents| {
let mut children = vec![token(SLASH, "/")];
children.extend(object_nodes(contents));
children.push(token(SLASH, "/"));
node(ITALIC, children)
}))(input)
}
fn emphasis(marker: u8) -> impl Fn(Input) -> IResult<Input, Input, ()> {
move |input: Input| {
let bytes = input.as_bytes();
if bytes.len() < 3 || bytes[0] != marker || bytes[1].is_ascii_whitespace() {
return Err(nom::Err::Error(()));
}
for idx in memchr_iter(marker, bytes).skip(1) {
// contains at least one character
if idx == 1 {
continue;
} else if count(&bytes[1..idx], b'\n') >= 2 {
break;
} else if validate_marker(idx, input) {
return Ok((input.slice(idx + 1..), input.slice(1..idx)));
}
}
Err(nom::Err::Error(()))
}
}
fn validate_marker(pos: usize, text: Input) -> bool {
if text.as_bytes()[pos - 1].is_ascii_whitespace() {
false
} else if let Some(post) = text.as_bytes().get(pos + 1) {
[
b' ', b'\t', b'\r', b'\n', b'-', b'.', b',', b';', b':', b'!', b'?', b'\'', b')', b'}',
b'[',
]
.contains(post)
} else {
true
}
}
#[test]
fn parse() {
use crate::{ast::Bold, tests::to_ast, ParseConfig};
let to_bold = to_ast::<Bold>(bold_node);
insta::assert_debug_snapshot!(
to_bold("*bold*").syntax,
@r###"
BOLD@0..6
STAR@0..1 "*"
TEXT@1..5 "bold"
STAR@5..6 "*"
"###
);
insta::assert_debug_snapshot!(
to_bold("*bo*ld*").syntax,
@r###"
BOLD@0..7
STAR@0..1 "*"
TEXT@1..6 "bo*ld"
STAR@6..7 "*"
"###
);
insta::assert_debug_snapshot!(
to_bold("*bo\nld*").syntax,
@r###"
BOLD@0..7
STAR@0..1 "*"
TEXT@1..6 "bo\nld"
STAR@6..7 "*"
"###
);
let config = &ParseConfig::default();
assert!(bold_node(("*bold*a", config).into()).is_err());
assert!(bold_node(("*bold *", config).into()).is_err());
assert!(bold_node(("* bold*", config).into()).is_err());
assert!(bold_node(("*b\nol\nd*", config).into()).is_err());
assert!(italic_node(("*bold*", config).into()).is_err());
}

64
src/syntax/fixed_width.rs Normal file
View file

@ -0,0 +1,64 @@
use nom::{IResult, InputTake};
use super::{
combinator::{blank_lines, debug_assert_lossless, line_ends_iter, node, GreenElement},
input::Input,
SyntaxKind,
};
fn fixed_width_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let mut start = 0;
for i in line_ends_iter(input.as_str()) {
let line = &input.s[start..i];
let trimmed = line.trim_start();
if trimmed == ":" || trimmed == ":\n" || trimmed == ":\r\n" || trimmed.starts_with(": ") {
start = i;
} else {
break;
}
}
if start == 0 {
return Err(nom::Err::Error(()));
}
let (input, contents) = input.take_split(start);
let (input, post_blank) = blank_lines(input)?;
let mut children = vec![];
children.push(contents.text_token());
children.extend(post_blank);
Ok((input, node(SyntaxKind::FIXED_WIDTH, children)))
}
pub fn fixed_width_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(fixed_width_node_base)(input)
}
#[test]
fn parse() {
use crate::{ast::FixedWidth, tests::to_ast};
let to_fixed_width = to_ast::<FixedWidth>(fixed_width_node);
insta::assert_debug_snapshot!(
to_fixed_width(
r#": A
:
: B
: C
"#
).syntax,
@r###"
FIXED_WIDTH@0..19
TEXT@0..14 ": A\n:\n: B\n: C\n"
BLANK_LINE@14..15
NEW_LINE@14..15 "\n"
BLANK_LINE@15..19
WHITESPACE@15..19 " "
"###
);
}

154
src/syntax/fn_def.rs Normal file
View file

@ -0,0 +1,154 @@
use nom::{
bytes::complete::{tag, take_while1},
combinator::map,
sequence::tuple,
IResult,
};
use super::{
combinator::{
blank_lines, colon_token, debug_assert_lossless, l_bracket_token, r_bracket_token,
trim_line_end, GreenElement, NodeBuilder,
},
input::Input,
keyword::affiliated_keyword_nodes,
SyntaxKind,
};
#[tracing::instrument(skip(input), fields(input = input.s))]
pub fn fn_def_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
affiliated_keyword_nodes,
l_bracket_token,
tag("fn"),
colon_token,
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
r_bracket_token,
trim_line_end,
blank_lines,
)),
|(
affiliated_keywords,
l_bracket,
fn_,
colon,
label,
r_bracket,
(content, ws_, nl),
post_blank,
)| {
let mut b = NodeBuilder::new();
b.children.extend(affiliated_keywords);
b.push(l_bracket);
b.text(fn_);
b.push(colon);
b.text(label);
b.push(r_bracket);
b.text(content);
b.ws(ws_);
b.nl(nl);
b.children.extend(post_blank);
b.finish(SyntaxKind::FN_DEF)
},
))(input)
}
#[test]
fn parse() {
use crate::ParseConfig;
use crate::{ast::FnDef, tests::to_ast};
let to_fn_def = to_ast::<FnDef>(fn_def_node);
insta::assert_debug_snapshot!(
to_fn_def("[fn:1] https://orgmode.org").syntax,
@r###"
FN_DEF@0..26
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..5 "1"
R_BRACKET@5..6 "]"
TEXT@6..26 " https://orgmode.org"
"###
);
insta::assert_debug_snapshot!(
to_fn_def("[fn:word_1] https://orgmode.org").syntax,
@r###"
FN_DEF@0..31
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..10 "word_1"
R_BRACKET@10..11 "]"
TEXT@11..31 " https://orgmode.org"
"###
);
insta::assert_debug_snapshot!(
to_fn_def("[fn:WORD-1] https://orgmode.org").syntax,
@r###"
FN_DEF@0..31
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..10 "WORD-1"
R_BRACKET@10..11 "]"
TEXT@11..31 " https://orgmode.org"
"###
);
insta::assert_debug_snapshot!(
to_fn_def("[fn:WORD]").syntax,
@r###"
FN_DEF@0..9
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..8 "WORD"
R_BRACKET@8..9 "]"
TEXT@9..9 ""
"###
);
insta::assert_debug_snapshot!(
to_fn_def("[fn:1] In particular, the parser requires stars at column 0 to be\n").syntax,
@r###"
FN_DEF@0..66
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..5 "1"
R_BRACKET@5..6 "]"
TEXT@6..65 " In particular, the p ..."
NEW_LINE@65..66 "\n"
"###
);
let config = &ParseConfig::default();
assert!(fn_def_node(("[fn:] https://orgmode.org", config).into()).is_err());
assert!(fn_def_node(("[fn:wor d] https://orgmode.org", config).into()).is_err());
assert!(fn_def_node(("[fn:WORD https://orgmode.org", config).into()).is_err());
insta::assert_debug_snapshot!(
to_fn_def("#+ATTR_poi: 1\n[fn:WORD-1] https://orgmode.org").syntax,
@r###"
FN_DEF@0..45
KEYWORD@0..14
HASH_PLUS@0..2 "#+"
TEXT@2..10 "ATTR_poi"
COLON@10..11 ":"
TEXT@11..13 " 1"
NEW_LINE@13..14 "\n"
L_BRACKET@14..15 "["
TEXT@15..17 "fn"
COLON@17..18 ":"
TEXT@18..24 "WORD-1"
R_BRACKET@24..25 "]"
TEXT@25..45 " https://orgmode.org"
"###
);
}

120
src/syntax/fn_ref.rs Normal file
View file

@ -0,0 +1,120 @@
use memchr::memchr2_iter;
use nom::{
bytes::complete::{tag, take_while},
combinator::opt,
sequence::tuple,
AsBytes, Err, IResult, InputTake,
};
use super::{
combinator::{
colon_token, debug_assert_lossless, l_bracket_token, node, r_bracket_token, GreenElement,
},
input::Input,
object::object_nodes,
SyntaxKind::*,
};
pub fn fn_ref_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(fn_ref_node_base)(input)
}
fn fn_ref_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (l_bracket, fn_, colon, label, definition, r_bracket)) = tuple((
l_bracket_token,
tag("fn"),
colon_token,
take_while(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
opt(tuple((colon_token, balanced_brackets))),
r_bracket_token,
))(input)?;
let mut children = vec![l_bracket, fn_.text_token(), colon, label.text_token()];
if let Some((colon, definition)) = definition {
children.push(colon);
children.extend(object_nodes(definition));
}
children.push(r_bracket);
Ok((input, node(FN_REF, children)))
}
fn balanced_brackets(input: Input) -> IResult<Input, Input, ()> {
let mut pairs = 1;
let bytes = input.as_bytes();
for i in memchr2_iter(b'[', b']', bytes) {
if bytes[i] == b'[' {
pairs += 1;
} else if pairs != 1 {
pairs -= 1;
} else {
return Ok(input.take_split(i));
}
}
Err(Err::Error(()))
}
#[test]
fn parse() {
use crate::{ast::FnRef, tests::to_ast, ParseConfig};
let to_fn_ref = to_ast::<FnRef>(fn_ref_node);
insta::assert_debug_snapshot!(
to_fn_ref("[fn:1]").syntax,
@r###"
FN_REF@0..6
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..5 "1"
R_BRACKET@5..6 "]"
"###
);
insta::assert_debug_snapshot!(
to_fn_ref("[fn:1:2]").syntax,
@r###"
FN_REF@0..8
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..5 "1"
COLON@5..6 ":"
TEXT@6..7 "2"
R_BRACKET@7..8 "]"
"###
);
insta::assert_debug_snapshot!(
to_fn_ref("[fn::2]").syntax,
@r###"
FN_REF@0..7
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..4 ""
COLON@4..5 ":"
TEXT@5..6 "2"
R_BRACKET@6..7 "]"
"###
);
insta::assert_debug_snapshot!(
to_fn_ref("[fn::[]]").syntax,
@r###"
FN_REF@0..8
L_BRACKET@0..1 "["
TEXT@1..3 "fn"
COLON@3..4 ":"
TEXT@4..4 ""
COLON@4..5 ":"
TEXT@5..7 "[]"
R_BRACKET@7..8 "]"
"###
);
let config = &ParseConfig::default();
assert!(fn_ref_node(("[fn::[]", config).into()).is_err());
}

350
src/syntax/headline.rs Normal file
View file

@ -0,0 +1,350 @@
use memchr::memrchr_iter;
use nom::{
bytes::complete::take_while1,
character::complete::{anychar, space0},
combinator::{map, opt, verify},
sequence::tuple,
AsBytes, IResult, InputLength, InputTake, Slice,
};
use tracing::instrument;
use super::{
combinator::{
debug_assert_lossless, hash_token, l_bracket_token, line_starts_iter, node,
r_bracket_token, token, trim_line_end, GreenElement, NodeBuilder,
},
drawer::property_drawer_node,
element::element_nodes,
input::Input,
object::object_nodes,
planning::planning_node,
SyntaxKind::*,
};
pub fn headline_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(headline_node_base)(input)
}
#[instrument(skip(input), fields(input = input.s))]
fn headline_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, stars) = headline_stars(input)?;
let mut b = NodeBuilder::new();
b.token(HEADLINE_STARS, stars);
let (input, ws) = space0(input)?;
b.ws(ws);
let (input, headline_keyword) = opt(headline_keyword_token)(input)?;
if let Some((headline_keyword, ws)) = headline_keyword {
b.push(headline_keyword);
b.ws(ws);
}
let (input, headline_priority) = opt(headline_priority_node)(input)?;
if let Some((headline_priority, ws)) = headline_priority {
b.push(headline_priority);
b.ws(ws);
}
let (input, (title_and_tags, ws_, nl)) = trim_line_end(input)?;
let (title, tags) = opt(headline_tags_node)(title_and_tags)?;
if !title.is_empty() {
b.push(node(HEADLINE_TITLE, object_nodes(title)));
}
b.push_opt(tags);
b.ws(ws_);
b.nl(nl);
if nl.is_empty() {
return Ok((input, b.finish(HEADLINE)));
}
let (input, planning) = opt(planning_node)(input)?;
b.push_opt(planning);
let (input, property_drawer) = opt(property_drawer_node)(input)?;
b.push_opt(property_drawer);
let (input, section) = opt(section_node)(input)?;
b.push_opt(section);
let mut i = input;
let current_level = stars.input_len();
while !i.is_empty() {
let next_level = i.bytes().take_while(|&c| c == b'*').count();
if next_level <= current_level {
break;
}
let (input, headline) = headline_node(i)?;
b.push(headline);
i = input;
}
Ok((i, b.finish(HEADLINE)))
}
#[instrument(skip(input), fields(input = input.s))]
pub fn section_node(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, section) = section_text(input)?;
Ok((input, node(SECTION, element_nodes(section)?)))
}
pub fn section_text(input: Input) -> IResult<Input, Input, ()> {
if input.is_empty() {
return Err(nom::Err::Error(()));
}
for (input, section) in line_starts_iter(input.as_str()).map(|i| input.take_split(i)) {
if headline_stars(input).is_ok() {
if section.is_empty() {
return Err(nom::Err::Error(()));
}
return Ok((input, section));
}
}
Ok(input.take_split(input.input_len()))
}
#[instrument(skip(input), fields(input = input.s))]
fn headline_stars(input: Input) -> IResult<Input, Input, ()> {
let bytes = input.as_bytes();
let level = bytes.iter().take_while(|&&c| c == b'*').count();
if level == 0 {
Err(nom::Err::Error(()))
} else if input.input_len() == level {
Ok(input.take_split(level))
} else if bytes[level] == b'\n' || bytes[level] == b'\r' || bytes[level] == b' ' {
Ok(input.take_split(level))
} else {
Err(nom::Err::Error(()))
}
}
#[instrument(skip(input), fields(input = input.s))]
fn headline_tags_node(input: Input) -> IResult<Input, GreenElement, ()> {
if !input.s.ends_with(':') {
return Err(nom::Err::Error(()));
};
let bytes = input.as_bytes();
// we're going to skip to first colon, so we start from the
// second last character
let mut i = input.input_len() - 1;
let mut can_not_be_ws = true;
let mut children = vec![token(COLON, ":")];
for ii in memrchr_iter(b':', bytes).skip(1) {
let item = &bytes[ii + 1..i];
if item.is_empty() {
children.push(token(COLON, ":"));
can_not_be_ws = false;
i = ii;
} else if item
.iter()
.all(|&c| c.is_ascii_alphanumeric() || c == b'_' || c == b'@' || c == b'#' || c == b'%')
{
children.push(input.slice(ii + 1..i).text_token());
children.push(token(COLON, ":"));
can_not_be_ws = false;
i = ii;
} else if item.iter().all(|&c| c == b' ' || c == b'\t') && !can_not_be_ws {
children.push(input.slice(ii + 1..i).ws_token());
children.push(token(COLON, ":"));
can_not_be_ws = true;
i = ii;
} else {
break;
}
}
if children.len() == 1 {
return Err(nom::Err::Error(()));
}
if i != 0 && bytes[i - 1] != b' ' && bytes[i - 1] != b'\t' {
return Err(nom::Err::Error(()));
}
// we parse headline tag from right to left,
// so we need to reverse the result after it finishes
children.reverse();
Ok((input.slice(0..i), node(HEADLINE_TAGS, children)))
}
fn headline_keyword_token(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
let (input, word) = verify(
take_while1(|c: char| !c.is_ascii_whitespace()),
|input: &Input| {
let Input { c, s } = input;
c.todo_keywords.0.iter().any(|k| k == s) || c.todo_keywords.1.iter().any(|k| k == s)
},
)(input)?;
let (input, ws) = space0(input)?;
Ok((input, (word.token(HEADLINE_KEYWORD), ws)))
}
fn headline_priority_node(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
let (input, node) = map(
tuple((l_bracket_token, hash_token, anychar, r_bracket_token)),
|(l_bracket, hash, char, r_bracket)| {
node(
HEADLINE_PRIORITY,
[l_bracket, hash, token(TEXT, &char.to_string()), r_bracket],
)
},
)(input)?;
let (input, ws) = space0(input)?;
Ok((input, (node, ws)))
}
#[test]
fn parse() {
use crate::{ast::Headline, tests::to_ast};
let to_headline = to_ast::<Headline>(headline_node);
let hdl = to_headline("* foo");
insta::assert_debug_snapshot!(
hdl.syntax,
@r###"
HEADLINE@0..5
HEADLINE_STARS@0..1 "*"
WHITESPACE@1..2 " "
HEADLINE_TITLE@2..5
TEXT@2..5 "foo"
"###
);
let hdl = to_headline("* foo\n\n** bar");
insta::assert_debug_snapshot!(
hdl.syntax,
@r###"
HEADLINE@0..13
HEADLINE_STARS@0..1 "*"
WHITESPACE@1..2 " "
HEADLINE_TITLE@2..5
TEXT@2..5 "foo"
NEW_LINE@5..6 "\n"
SECTION@6..7
PARAGRAPH@6..7
BLANK_LINE@6..7
NEW_LINE@6..7 "\n"
HEADLINE@7..13
HEADLINE_STARS@7..9 "**"
WHITESPACE@9..10 " "
HEADLINE_TITLE@10..13
TEXT@10..13 "bar"
"###
);
let hdl = to_headline("* TODO foo\nbar\n** baz\n");
assert_eq!(hdl.level(), Some(1));
assert_eq!(hdl.keyword().as_ref().map(|x| x.text()), Some("TODO"));
insta::assert_debug_snapshot!(
hdl.syntax,
@r###"
HEADLINE@0..22
HEADLINE_STARS@0..1 "*"
WHITESPACE@1..2 " "
HEADLINE_KEYWORD@2..6 "TODO"
WHITESPACE@6..7 " "
HEADLINE_TITLE@7..10
TEXT@7..10 "foo"
NEW_LINE@10..11 "\n"
SECTION@11..15
PARAGRAPH@11..15
TEXT@11..15 "bar\n"
HEADLINE@15..22
HEADLINE_STARS@15..17 "**"
WHITESPACE@17..18 " "
HEADLINE_TITLE@18..21
TEXT@18..21 "baz"
NEW_LINE@21..22 "\n"
"###
);
let hdl = to_headline("** [#A] foo\n* baz");
assert_eq!(hdl.level(), Some(2));
assert_eq!(
hdl.priority().unwrap().text_string().unwrap(),
"A".to_string()
);
insta::assert_debug_snapshot!(
hdl.syntax,
@r###"
HEADLINE@0..12
HEADLINE_STARS@0..2 "**"
WHITESPACE@2..3 " "
HEADLINE_PRIORITY@3..7
L_BRACKET@3..4 "["
HASH@4..5 "#"
TEXT@5..6 "A"
R_BRACKET@6..7 "]"
WHITESPACE@7..8 " "
HEADLINE_TITLE@8..11
TEXT@8..11 "foo"
NEW_LINE@11..12 "\n"
"###
);
}
#[test]
fn issue_15_16() {
use crate::{ast::Headline, tests::to_ast};
let to_headline = to_ast::<Headline>(headline_node);
let tags = to_headline("* a ::").tags().unwrap();
assert_eq!(tags.iter().count(), 0);
// let tags = to_headline("* a :(:").tags().unwrap();
// assert_eq!(tags.iter().count(), 0);
let tags = to_headline("* a \t:_:").tags().unwrap();
assert_eq!(
vec!["_".to_string()],
tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
);
let tags = to_headline("* a \t :@:").tags().unwrap();
assert_eq!(
vec!["@".to_string()],
tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
);
let tags = to_headline("* a :#:").tags().unwrap();
assert_eq!(
vec!["#".to_string()],
tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
);
let tags = to_headline("* a\t :%:").tags().unwrap();
assert_eq!(
vec!["%".to_string()],
tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
);
// let tags = to_headline("* a :余:").tags().unwrap();
// assert_eq!(
// vec!["余".to_string()],
// tags.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
// );
}

126
src/syntax/inline_call.rs Normal file
View file

@ -0,0 +1,126 @@
use nom::{
bytes::complete::{tag, take_till},
combinator::{map, opt},
sequence::tuple,
IResult,
};
use super::{
combinator::{
debug_assert_lossless, l_bracket_token, l_parens_token, node, r_bracket_token,
r_parens_token, GreenElement,
},
input::Input,
SyntaxKind,
};
pub fn inline_call_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
tag("call_"),
take_till(|c| c == '[' || c == '\n' || c == '(' || c == ')'),
opt(tuple((
l_bracket_token,
take_till(|c| c == ']' || c == '\n'),
r_bracket_token,
))),
l_parens_token,
take_till(|c| c == ')' || c == '\n'),
r_parens_token,
opt(tuple((
l_bracket_token,
take_till(|c| c == ']' || c == '\n'),
r_bracket_token,
))),
)),
|(call, name, inside_header, l_paren, arguments, r_paren, end_header)| {
let mut children = vec![call.text_token()];
children.push(name.text_token());
if let Some((l_bracket, header, r_bracket)) = inside_header {
children.push(l_bracket);
children.push(header.text_token());
children.push(r_bracket);
}
children.push(l_paren);
children.push(arguments.text_token());
children.push(r_paren);
if let Some((l_bracket, header, r_bracket)) = end_header {
children.push(l_bracket);
children.push(header.text_token());
children.push(r_bracket);
}
node(SyntaxKind::INLINE_CALL, children)
},
))(input)
}
#[test]
fn parse() {
use crate::{ast::InlineCall, tests::to_ast};
let to_inline_call = to_ast::<InlineCall>(inline_call_node);
let call = to_inline_call("call_square(4)");
insta::assert_debug_snapshot!(
call.syntax,
@r###"
INLINE_CALL@0..14
TEXT@0..5 "call_"
TEXT@5..11 "square"
L_PARENS@11..12 "("
TEXT@12..13 "4"
R_PARENS@13..14 ")"
"###
);
let call = to_inline_call("call_square[:results output](4)");
insta::assert_debug_snapshot!(
call.syntax,
@r###"
INLINE_CALL@0..31
TEXT@0..5 "call_"
TEXT@5..11 "square"
L_BRACKET@11..12 "["
TEXT@12..27 ":results output"
R_BRACKET@27..28 "]"
L_PARENS@28..29 "("
TEXT@29..30 "4"
R_PARENS@30..31 ")"
"###
);
let call = to_inline_call("call_square(4)[:results html]");
insta::assert_debug_snapshot!(
call.syntax,
@r###"
INLINE_CALL@0..29
TEXT@0..5 "call_"
TEXT@5..11 "square"
L_PARENS@11..12 "("
TEXT@12..13 "4"
R_PARENS@13..14 ")"
L_BRACKET@14..15 "["
TEXT@15..28 ":results html"
R_BRACKET@28..29 "]"
"###
);
let call = to_inline_call("call_square[:results output](4)[:results html]");
insta::assert_debug_snapshot!(
call.syntax,
@r###"
INLINE_CALL@0..46
TEXT@0..5 "call_"
TEXT@5..11 "square"
L_BRACKET@11..12 "["
TEXT@12..27 ":results output"
R_BRACKET@27..28 "]"
L_PARENS@28..29 "("
TEXT@29..30 "4"
R_PARENS@30..31 ")"
L_BRACKET@31..32 "["
TEXT@32..45 ":results html"
R_BRACKET@45..46 "]"
"###
);
}

84
src/syntax/inline_src.rs Normal file
View file

@ -0,0 +1,84 @@
use nom::{
bytes::complete::{tag, take_till, take_while1},
combinator::{map, opt},
sequence::tuple,
IResult,
};
use super::{
combinator::{
debug_assert_lossless, l_bracket_token, l_curly_token, node, r_bracket_token,
r_curly_token, GreenElement,
},
input::Input,
SyntaxKind,
};
pub fn inline_src_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
tag("src_"),
take_while1(|c: char| !c.is_ascii_whitespace() && c != '[' && c != '{'),
opt(tuple((
l_bracket_token,
take_till(|c| c == '\n' || c == ']'),
r_bracket_token,
))),
l_curly_token,
take_till(|c| c == '\n' || c == '}'),
r_curly_token,
)),
|(src, lang, options, l_curly, body, r_curly)| {
let mut children = vec![src.text_token(), lang.text_token()];
if let Some((l_bracket, options, r_bracket)) = options {
children.push(l_bracket);
children.push(options.text_token());
children.push(r_bracket);
}
children.push(l_curly);
children.push(body.text_token());
children.push(r_curly);
node(SyntaxKind::INLINE_SRC, children)
},
))(input)
}
#[test]
fn parse() {
use crate::{ast::InlineSrc, tests::to_ast, ParseConfig};
let to_inline_src = to_ast::<InlineSrc>(inline_src_node);
insta::assert_debug_snapshot!(
to_inline_src("src_C{int a = 0;}").syntax,
@r###"
INLINE_SRC@0..17
TEXT@0..4 "src_"
TEXT@4..5 "C"
L_CURLY@5..6 "{"
TEXT@6..16 "int a = 0;"
R_CURLY@16..17 "}"
"###
);
insta::assert_debug_snapshot!(
to_inline_src("src_xml[:exports code]{<tag>text</tag>}").syntax,
@r###"
INLINE_SRC@0..39
TEXT@0..4 "src_"
TEXT@4..7 "xml"
L_BRACKET@7..8 "["
TEXT@8..21 ":exports code"
R_BRACKET@21..22 "]"
L_CURLY@22..23 "{"
TEXT@23..38 "<tag>text</tag>"
R_CURLY@38..39 "}"
"###
);
let config = &ParseConfig::default();
assert!(inline_src_node(("src_xml[:exports code]{<tag>text</tag>", config).into()).is_err());
assert!(inline_src_node(("src_[:exports code]{<tag>text</tag>}", config).into()).is_err());
assert!(inline_src_node(("src_xml[:exports code]", config).into()).is_err());
}

250
src/syntax/input.rs Normal file
View file

@ -0,0 +1,250 @@
use nom::{
error::{ErrorKind, ParseError},
AsBytes, Compare, CompareResult, Err, FindSubstring, IResult, InputIter, InputLength,
InputTake, InputTakeAtPosition, Needed, Offset, Slice,
};
use std::{
ops::{Range, RangeFrom, RangeFull, RangeTo},
str::{Bytes, CharIndices, Chars},
};
use super::{
combinator::{token, GreenElement},
SyntaxKind,
};
use crate::config::ParseConfig;
/// A custom Input struct
///
/// It helps us to pass the `ParseConfig` all the way down to each parsers
#[derive(Clone, Copy, Debug)]
pub struct Input<'a> {
pub(crate) s: &'a str,
pub(crate) c: &'a ParseConfig,
}
impl<'a> Input<'a> {
#[inline]
pub(crate) fn of(&self, i: &'a str) -> Input<'a> {
Input { s: i, c: self.c }
}
#[inline]
pub fn as_str(&self) -> &'a str {
self.s
}
#[inline]
pub fn is_empty(&self) -> bool {
self.s.is_empty()
}
#[inline]
pub fn token(&self, kind: SyntaxKind) -> GreenElement {
token(kind, self.s)
}
#[inline]
pub fn text_token(&self) -> GreenElement {
token(SyntaxKind::TEXT, self.s)
}
#[inline]
pub fn ws_token(&self) -> GreenElement {
token(SyntaxKind::WHITESPACE, self.s)
}
#[inline]
pub fn nl_token(&self) -> GreenElement {
token(SyntaxKind::NEW_LINE, self.s)
}
#[inline]
pub fn bytes(&self) -> Bytes {
self.s.bytes()
}
}
impl<'a> From<(&'a str, &'a ParseConfig)> for Input<'a> {
fn from(value: (&'a str, &'a ParseConfig)) -> Self {
Input {
s: value.0,
c: value.1,
}
}
}
impl<'a> AsBytes for Input<'a> {
#[inline]
fn as_bytes(&self) -> &[u8] {
self.s.as_bytes()
}
}
impl<'a> Slice<Range<usize>> for Input<'a> {
fn slice(&self, range: Range<usize>) -> Self {
self.of(self.s.slice(range))
}
}
impl<'a> Slice<RangeTo<usize>> for Input<'a> {
fn slice(&self, range: RangeTo<usize>) -> Self {
self.of(self.s.slice(range))
}
}
impl<'a> Slice<RangeFrom<usize>> for Input<'a> {
fn slice(&self, range: RangeFrom<usize>) -> Self {
self.of(self.s.slice(range))
}
}
impl<'a> Slice<RangeFull> for Input<'a> {
fn slice(&self, range: RangeFull) -> Self {
self.of(self.s.slice(range))
}
}
impl<'a, 'b> FindSubstring<&'b str> for Input<'a> {
fn find_substring(&self, substr: &str) -> Option<usize> {
self.s.find(substr)
}
}
impl<'a, 'b> Compare<&'b str> for Input<'a> {
#[inline]
fn compare(&self, t: &'b str) -> CompareResult {
self.s.compare(t)
}
#[inline]
fn compare_no_case(&self, t: &'b str) -> CompareResult {
self.s.compare_no_case(t)
}
}
impl<'a> InputLength for Input<'a> {
#[inline]
fn input_len(&self) -> usize {
self.s.len()
}
}
impl<'a> InputIter for Input<'a> {
type Item = char;
type Iter = CharIndices<'a>;
type IterElem = Chars<'a>;
#[inline]
fn iter_indices(&self) -> Self::Iter {
self.s.char_indices()
}
#[inline]
fn iter_elements(&self) -> Self::IterElem {
self.s.chars()
}
fn position<P>(&self, predicate: P) -> Option<usize>
where
P: Fn(Self::Item) -> bool,
{
self.s.position(predicate)
}
#[inline]
fn slice_index(&self, count: usize) -> Result<usize, Needed> {
self.s.slice_index(count)
}
}
impl<'a> InputTake for Input<'a> {
#[inline]
fn take(&self, count: usize) -> Self {
let s = self.s.take(count);
self.of(s)
}
#[inline]
fn take_split(&self, count: usize) -> (Self, Self) {
let (l, r) = self.s.take_split(count);
(self.of(l), self.of(r))
}
}
impl<'a> InputTakeAtPosition for Input<'a> {
type Item = char;
#[inline]
fn split_at_position<P, E: ParseError<Self>>(&self, predicate: P) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match self.s.split_at_position::<_, (&str, ErrorKind)>(predicate) {
Ok((l, r)) => Ok((self.of(l), self.of(r))),
Err(Err::Error((i, kind))) => Err(Err::Error(E::from_error_kind(self.of(i), kind))),
Err(Err::Failure((i, kind))) => Err(Err::Failure(E::from_error_kind(self.of(i), kind))),
Err(Err::Incomplete(x)) => Err(Err::Incomplete(x)),
}
}
#[inline]
fn split_at_position1<P, E: ParseError<Self>>(
&self,
predicate: P,
e: ErrorKind,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match self
.s
.split_at_position1::<_, (&str, ErrorKind)>(predicate, e)
{
Ok((l, r)) => Ok((self.of(l), self.of(r))),
Err(Err::Error((i, kind))) => Err(Err::Error(E::from_error_kind(self.of(i), kind))),
Err(Err::Failure((i, kind))) => Err(Err::Failure(E::from_error_kind(self.of(i), kind))),
Err(Err::Incomplete(x)) => Err(Err::Incomplete(x)),
}
}
#[inline]
fn split_at_position_complete<P, E: ParseError<Self>>(
&self,
predicate: P,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match self
.s
.split_at_position_complete::<_, (&str, ErrorKind)>(predicate)
{
Ok((l, r)) => Ok((self.of(l), self.of(r))),
Err(Err::Error((i, kind))) => Err(Err::Error(E::from_error_kind(self.of(i), kind))),
Err(Err::Failure((i, kind))) => Err(Err::Failure(E::from_error_kind(self.of(i), kind))),
Err(Err::Incomplete(x)) => Err(Err::Incomplete(x)),
}
}
#[inline]
fn split_at_position1_complete<P, E: ParseError<Self>>(
&self,
predicate: P,
e: ErrorKind,
) -> IResult<Self, Self, E>
where
P: Fn(Self::Item) -> bool,
{
match self
.s
.split_at_position1_complete::<_, (&str, ErrorKind)>(predicate, e)
{
Ok((l, r)) => Ok((self.of(l), self.of(r))),
Err(Err::Error((i, kind))) => Err(Err::Error(E::from_error_kind(self.of(i), kind))),
Err(Err::Failure((i, kind))) => Err(Err::Failure(E::from_error_kind(self.of(i), kind))),
Err(Err::Incomplete(x)) => Err(Err::Incomplete(x)),
}
}
}
impl<'a> Offset for Input<'a> {
fn offset(&self, second: &Self) -> usize {
self.s.offset(second.s)
}
}

215
src/syntax/keyword.rs Normal file
View file

@ -0,0 +1,215 @@
use nom::{
bytes::complete::take_till,
character::complete::space0,
combinator::{cond, opt},
sequence::tuple,
IResult,
};
use super::{
combinator::{
blank_lines, colon_token, debug_assert_lossless, hash_plus_token, l_bracket_token,
r_bracket_token, trim_line_end, GreenElement, NodeBuilder,
},
input::Input,
SyntaxKind,
};
pub fn keyword_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(keyword_node_base)(input)
}
fn keyword_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, (ws, hash_plus, key)) = tuple((
space0,
hash_plus_token,
take_till(|c: char| c.is_ascii_whitespace() || c == ':' || c == '['),
))(input)?;
let is_babel_call = key.s.eq_ignore_ascii_case("CALL");
let (input, optional) = cond(
!is_babel_call,
opt(tuple((
l_bracket_token,
take_till(|c| c == ']' || c == '\n'),
r_bracket_token,
))),
)(input)?;
let (input, (colon, (value, ws_, nl), post_blank)) =
tuple((colon_token, trim_line_end, blank_lines))(input)?;
let mut b = NodeBuilder::new();
b.ws(ws);
b.push(hash_plus);
b.text(key);
if let Some(Some((l_bracket, optional, r_bracket))) = optional {
b.children
.extend([l_bracket, optional.text_token(), r_bracket]);
}
b.push(colon);
b.ws(ws_);
b.text(value);
b.nl(nl);
b.children.extend(post_blank);
Ok((
input,
b.finish(if is_babel_call {
SyntaxKind::BABEL_CALL
} else {
SyntaxKind::KEYWORD
}),
))
}
pub fn affiliated_keyword_nodes(input: Input) -> IResult<Input, Vec<GreenElement>, ()> {
use rowan::NodeOrToken;
let mut children = vec![];
let mut i = input;
while !i.is_empty() {
let Ok((input, keyword)) = keyword_node(i) else {
break;
};
i = input;
let Some(node) = keyword.as_node() else {
return Err(nom::Err::Error(()));
};
// find the first text token in children
let Some(NodeOrToken::Token(token)) = node
.children()
.into_iter()
.find(|t| t.kind() == SyntaxKind::TEXT.into())
else {
return Err(nom::Err::Error(()));
};
let text = token.text();
if input.c.affiliated_keywords.iter().all(|w| w != text) && !text.starts_with("ATTR_") {
return Err(nom::Err::Error(()));
}
children.push(keyword);
}
Ok((i, children))
}
#[test]
fn parse() {
use crate::{
ast::{BabelCall, Keyword},
tests::to_ast,
ParseConfig,
};
let to_keyword = to_ast::<Keyword>(keyword_node);
let to_babel_call = to_ast::<BabelCall>(keyword_node);
insta::assert_debug_snapshot!(
to_keyword("#+KEY:").syntax,
@r###"
KEYWORD@0..6
HASH_PLUS@0..2 "#+"
TEXT@2..5 "KEY"
COLON@5..6 ":"
TEXT@6..6 ""
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+KEY: VALUE").syntax,
@r###"
KEYWORD@0..12
HASH_PLUS@0..2 "#+"
TEXT@2..5 "KEY"
COLON@5..6 ":"
TEXT@6..12 " VALUE"
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+K_E_Y: VALUE").syntax,
@r###"
KEYWORD@0..14
HASH_PLUS@0..2 "#+"
TEXT@2..7 "K_E_Y"
COLON@7..8 ":"
TEXT@8..14 " VALUE"
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+KEY:VALUE\n").syntax,
@r###"
KEYWORD@0..12
HASH_PLUS@0..2 "#+"
TEXT@2..5 "KEY"
COLON@5..6 ":"
TEXT@6..11 "VALUE"
NEW_LINE@11..12 "\n"
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+RESULTS:").syntax,
@r###"
KEYWORD@0..10
HASH_PLUS@0..2 "#+"
TEXT@2..9 "RESULTS"
COLON@9..10 ":"
TEXT@10..10 ""
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+ATTR_LATEX: :width 5cm\n").syntax,
@r###"
KEYWORD@0..25
HASH_PLUS@0..2 "#+"
TEXT@2..12 "ATTR_LATEX"
COLON@12..13 ":"
TEXT@13..24 " :width 5cm"
NEW_LINE@24..25 "\n"
"###
);
insta::assert_debug_snapshot!(
to_babel_call("#+CALL: double(n=4)").syntax,
@r###"
BABEL_CALL@0..19
HASH_PLUS@0..2 "#+"
TEXT@2..6 "CALL"
COLON@6..7 ":"
TEXT@7..19 " double(n=4)"
"###
);
insta::assert_debug_snapshot!(
to_keyword("#+CAPTION[Short caption]: Longer caption.").syntax,
@r###"
KEYWORD@0..41
HASH_PLUS@0..2 "#+"
TEXT@2..9 "CAPTION"
L_BRACKET@9..10 "["
TEXT@10..23 "Short caption"
R_BRACKET@23..24 "]"
COLON@24..25 ":"
TEXT@25..41 " Longer caption."
"###
);
let config = &ParseConfig::default();
assert!(keyword_node(("#+KE Y: VALUE", config).into()).is_err());
assert!(keyword_node(("#+CALL[option]: VALUE", config).into()).is_err());
assert!(keyword_node(("#+ KEY: VALUE", config).into()).is_err());
}

89
src/syntax/link.rs Normal file
View file

@ -0,0 +1,89 @@
use nom::{
bytes::complete::take_while,
combinator::{map, opt},
sequence::tuple,
IResult,
};
use super::{
combinator::{
debug_assert_lossless, l_bracket2_token, l_bracket_token, node, r_bracket2_token,
r_bracket_token, GreenElement,
},
input::Input,
SyntaxKind::*,
};
pub fn link_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
l_bracket2_token,
take_while(|c: char| c != '<' && c != '>' && c != '\n' && c != ']'),
opt(tuple((
r_bracket_token,
l_bracket_token,
take_while(|c: char| c != '[' && c != ']'),
))),
r_bracket2_token,
)),
|(l_bracket2, path, desc, r_bracket2)| {
let mut children = vec![l_bracket2, path.token(LINK_PATH)];
if let Some((r_bracket, l_bracket, desc)) = desc {
children.extend([r_bracket, l_bracket, desc.text_token()]);
}
children.push(r_bracket2);
node(LINK, children)
},
))(input)
}
#[test]
fn parse() {
use crate::{ast::Link, tests::to_ast, ParseConfig};
let to_link = to_ast::<Link>(link_node);
let link = to_link("[[#id]]");
assert_eq!(link.path().as_ref().map(|x| x.text()), Some("#id"));
insta::assert_debug_snapshot!(
link.syntax,
@r###"
LINK@0..7
L_BRACKET2@0..2 "[["
LINK_PATH@2..5 "#id"
R_BRACKET2@5..7 "]]"
"###
);
let link = to_link("[[#id][desc]]");
insta::assert_debug_snapshot!(
link.syntax,
@r###"
LINK@0..13
L_BRACKET2@0..2 "[["
LINK_PATH@2..5 "#id"
R_BRACKET@5..6 "]"
L_BRACKET@6..7 "["
TEXT@7..11 "desc"
R_BRACKET2@11..13 "]]"
"###
);
let link = to_link("[[file:/home/dominik/images/jupiter.jpg]]");
insta::assert_debug_snapshot!(
link.syntax,
@r###"
LINK@0..41
L_BRACKET2@0..2 "[["
LINK_PATH@2..39 "file:/home/dominik/im ..."
R_BRACKET2@39..41 "]]"
"###
);
let config = &ParseConfig::default();
assert!(link_node(("[[#id][desc]", config).into()).is_err());
}

583
src/syntax/list.rs Normal file
View file

@ -0,0 +1,583 @@
use memchr::{memchr, memchr2};
use nom::{
branch::alt,
bytes::complete::{tag, take},
character::complete::{alphanumeric1, digit1, space0},
combinator::{cond, map, opt, recognize, verify},
sequence::{preceded, tuple},
AsBytes, IResult, InputLength, InputTake,
};
use super::{
combinator::{
at_token, blank_lines, colon2_token, debug_assert_lossless, l_bracket_token,
line_starts_iter, node, r_bracket_token, GreenElement,
},
element::element_node,
input::Input,
object::object_nodes,
SyntaxKind::*,
};
pub fn list_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(list_node_base)(input)
}
fn list_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, first_indent) = space0(input)?;
let (input, first_item) = list_item_node(first_indent, input)?;
let mut children = vec![first_item];
let mut input = input;
while !input.is_empty() {
let (input_, indent) = space0(input)?;
if indent.input_len() != first_indent.input_len() {
break;
}
if let Ok((input_, list_item)) = list_item_node(indent, input_) {
children.push(list_item);
input = input_;
} else {
break;
}
}
let (input, post_blank) = blank_lines(input)?;
children.extend(post_blank);
Ok((input, node(LIST, children)))
}
fn list_item_node<'a>(indent: Input<'a>, input: Input<'a>) -> IResult<Input<'a>, GreenElement, ()> {
let (input, bullet) = recognize(tuple((
alt((
tag("+"),
tag("*"),
tag("-"),
preceded(digit1, tag(".")),
preceded(digit1, tag(")")),
)),
space0,
)))(input)?;
// bullet must ends with whitespace,
if !(bullet
.s
.bytes()
.last()
.map(|b| b == b' ' || b == b'\t')
.unwrap_or(true)
// or input should be a line end
|| input
.s
.bytes()
.next()
.map(|b| b == b'\r' || b == b'\n')
.unwrap_or(true))
{
return Err(nom::Err::Error(()));
}
let is_ordered = bullet.s.starts_with(|c: char| c.is_ascii_digit());
let (input, counter) = opt(list_item_counter)(input)?;
let (input, checkbox) = opt(list_item_checkbox)(input)?;
let (input, tag) = cond(!is_ordered, opt(list_item_tag))(input)?;
let (input, content) = list_item_content_node(input, indent.input_len())?;
let mut children = vec![
indent.token(LIST_ITEM_INDENT),
bullet.token(LIST_ITEM_BULLET),
];
if let Some((counter, ws)) = counter {
children.extend([counter, ws.ws_token()]);
}
if let Some((checkbox, ws)) = checkbox {
children.extend([checkbox, ws.ws_token()]);
}
if let Some(Some((tag, ws))) = tag {
children.extend([tag, ws.ws_token()]);
}
children.push(content);
Ok((input, node(LIST_ITEM, children)))
}
fn list_item_counter(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
let (input, node) = map(
tuple((l_bracket_token, at_token, alphanumeric1, r_bracket_token)),
|(l_bracket, at, char, r_bracket)| {
node(
LIST_ITEM_COUNTER,
[l_bracket, at, char.text_token(), r_bracket],
)
},
)(input)?;
let (input, ws) = space0(input)?;
Ok((input, (node, ws)))
}
fn list_item_checkbox(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
let (input, node) = map(
tuple((
l_bracket_token,
verify(take(1usize), |input: &Input| {
input.s == " " || input.s == "X" || input.s == "-"
}),
r_bracket_token,
)),
|(l_bracket, char, r_bracket)| {
node(
LIST_ITEM_CHECK_BOX,
[l_bracket, char.text_token(), r_bracket],
)
},
)(input)?;
let (input, ws) = space0(input)?;
Ok((input, (node, ws)))
}
fn list_item_tag(input: Input) -> IResult<Input, (GreenElement, Input), ()> {
let bytes = input.as_bytes();
let (input, tag) = match memchr2(b'\n', b':', bytes) {
Some(idx) if idx > 0 && bytes[idx] == b':' => input.take_split(idx),
_ => return Err(nom::Err::Error(())),
};
let (input, ws) = space0(input)?;
let (input, colon2) = colon2_token(input)?;
let mut children = object_nodes(tag);
children.push(colon2);
Ok((input, (node(LIST_ITEM_TAG, children), ws)))
}
fn list_item_content_node(input: Input, indent: usize) -> IResult<Input, GreenElement, ()> {
if memchr(b'\n', input.as_bytes()).is_none() {
return Ok((
input.of(""),
node(LIST_ITEM_CONTENT, [node(PARAGRAPH, object_nodes(input))]),
));
};
let mut skip_one = true;
let mut i = input;
let mut children = vec![];
let mut previous_line_is_blank = false;
'l: loop {
for (input, head) in line_starts_iter(i.as_str())
// the first line in list item content will always be a paragraph
// so we need to skip it in the first iteration
.skip(if skip_one { 1 } else { 0 })
.map(|idx| i.take_split(idx))
{
match get_line_indent(input.as_str()) {
Some(next_indent) => {
previous_line_is_blank = false;
if next_indent <= indent {
if !head.is_empty() {
children.push(node(PARAGRAPH, object_nodes(head)));
}
return Ok((input, node(LIST_ITEM_CONTENT, children)));
}
if let Ok((input, element)) = element_node(input) {
if !head.is_empty() {
children.push(node(PARAGRAPH, object_nodes(head)));
}
children.push(element);
i = input;
skip_one = false;
continue 'l;
}
}
_ if previous_line_is_blank => {
// list item ends at two consecutive empty lines
if !head.is_empty() {
children.push(node(PARAGRAPH, object_nodes(head)));
}
let (input, post_blank) = blank_lines(input)?;
children.extend(post_blank);
return Ok((input, node(LIST_ITEM_CONTENT, children)));
}
_ => {
previous_line_is_blank = true;
}
}
}
break;
}
if !i.is_empty() {
children.push(node(PARAGRAPH, object_nodes(i)));
}
Ok((input.of(""), node(LIST_ITEM_CONTENT, children)))
}
fn get_line_indent(input: &str) -> Option<usize> {
input
.bytes()
.take_while(|b| *b != b'\n')
.position(|b| !b.is_ascii_whitespace())
}
#[test]
fn parse() {
use crate::{ast::List, tests::to_ast, ParseConfig};
let to_list = to_ast::<List>(list_node);
let list = to_list("1)");
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..2
LIST_ITEM@0..2
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "1)"
LIST_ITEM_CONTENT@2..2
PARAGRAPH@2..2
"###
);
let list = to_list("+ ");
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..2
LIST_ITEM@0..2
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "+ "
LIST_ITEM_CONTENT@2..2
PARAGRAPH@2..2
"###
);
let list = to_list("-\n");
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..2
LIST_ITEM@0..2
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..1 "-"
LIST_ITEM_CONTENT@1..2
PARAGRAPH@1..2
TEXT@1..2 "\n"
"###
);
let list = to_list("+ 1");
assert!(!list.is_ordered());
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..3
LIST_ITEM@0..3
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "+ "
LIST_ITEM_CONTENT@2..3
PARAGRAPH@2..3
TEXT@2..3 "1"
"###
);
let list = to_list("+ 1\n");
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..4
LIST_ITEM@0..4
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "+ "
LIST_ITEM_CONTENT@2..4
PARAGRAPH@2..4
TEXT@2..4 "1\n"
"###
);
let list = to_list("+ [@A] 1\n\n\n+ 2");
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..14
LIST_ITEM@0..11
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "+ "
LIST_ITEM_COUNTER@2..6
L_BRACKET@2..3 "["
AT@3..4 "@"
TEXT@4..5 "A"
R_BRACKET@5..6 "]"
WHITESPACE@6..7 " "
LIST_ITEM_CONTENT@7..11
PARAGRAPH@7..10
TEXT@7..10 "1\n\n"
BLANK_LINE@10..11
NEW_LINE@10..11 "\n"
LIST_ITEM@11..14
LIST_ITEM_INDENT@11..11 ""
LIST_ITEM_BULLET@11..13 "+ "
LIST_ITEM_CONTENT@13..14
PARAGRAPH@13..14
TEXT@13..14 "2"
"###
);
let list = to_list("+ *TAG* :: item1\n+ [X] item2");
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..28
LIST_ITEM@0..17
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "+ "
LIST_ITEM_TAG@2..10
BOLD@2..7
STAR@2..3 "*"
TEXT@3..6 "TAG"
STAR@6..7 "*"
TEXT@7..8 " "
COLON2@8..10 "::"
WHITESPACE@10..10 ""
LIST_ITEM_CONTENT@10..17
PARAGRAPH@10..17
TEXT@10..17 " item1\n"
LIST_ITEM@17..28
LIST_ITEM_INDENT@17..17 ""
LIST_ITEM_BULLET@17..19 "+ "
LIST_ITEM_CHECK_BOX@19..22
L_BRACKET@19..20 "["
TEXT@20..21 "X"
R_BRACKET@21..22 "]"
WHITESPACE@22..23 " "
LIST_ITEM_CONTENT@23..28
PARAGRAPH@23..28
TEXT@23..28 "item2"
"###
);
let list = to_list(
r#"+ item1
+ item2"#,
);
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..17
LIST_ITEM@0..17
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "+ "
LIST_ITEM_CONTENT@2..17
PARAGRAPH@2..8
TEXT@2..8 "item1\n"
LIST@8..17
LIST_ITEM@8..17
LIST_ITEM_INDENT@8..10 " "
LIST_ITEM_BULLET@10..12 "+ "
LIST_ITEM_CONTENT@12..17
PARAGRAPH@12..17
TEXT@12..17 "item2"
"###
);
let list = to_list("* item1\nitem2");
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..8
LIST_ITEM@0..8
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "* "
LIST_ITEM_CONTENT@2..8
PARAGRAPH@2..8
TEXT@2..8 "item1\n"
"###
);
let list = to_list(
r#"* item1
still item 1"#,
);
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..23
LIST_ITEM@0..23
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "* "
LIST_ITEM_CONTENT@2..23
PARAGRAPH@2..23
TEXT@2..23 "item1\n\n still item 1"
"###
);
let list = to_list(
r#"+ item1
+ item2
"#,
);
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..26
LIST_ITEM@0..26
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..2 "+ "
LIST_ITEM_CONTENT@2..26
PARAGRAPH@2..8
TEXT@2..8 "item1\n"
LIST@8..26
LIST_ITEM@8..26
LIST_ITEM_INDENT@8..14 " "
LIST_ITEM_BULLET@14..16 "+ "
LIST_ITEM_CONTENT@16..26
PARAGRAPH@16..26
TEXT@16..26 "item2\n "
"###
);
let list = to_list(
r#"1. item1
- item2
3. item 3"#,
);
assert!(list.is_ordered());
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..32
LIST_ITEM@0..23
LIST_ITEM_INDENT@0..0 ""
LIST_ITEM_BULLET@0..3 "1. "
LIST_ITEM_CONTENT@3..23
PARAGRAPH@3..10
TEXT@3..10 "item1\n\n"
LIST@10..23
LIST_ITEM@10..23
LIST_ITEM_INDENT@10..14 " "
LIST_ITEM_BULLET@14..16 "- "
LIST_ITEM_CONTENT@16..23
PARAGRAPH@16..23
TEXT@16..23 "item2\n\n"
LIST_ITEM@23..32
LIST_ITEM_INDENT@23..23 ""
LIST_ITEM_BULLET@23..26 "3. "
LIST_ITEM_CONTENT@26..32
PARAGRAPH@26..32
TEXT@26..32 "item 3"
"###
);
let list = to_list(
r#" + item1
+ item2"#,
);
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..20
LIST_ITEM@0..11
LIST_ITEM_INDENT@0..2 " "
LIST_ITEM_BULLET@2..4 "+ "
LIST_ITEM_CONTENT@4..11
PARAGRAPH@4..11
TEXT@4..11 "item1\n\n"
LIST_ITEM@11..20
LIST_ITEM_INDENT@11..13 " "
LIST_ITEM_BULLET@13..15 "+ "
LIST_ITEM_CONTENT@15..20
PARAGRAPH@15..20
TEXT@15..20 "item2"
"###
);
let list = to_list(
r#" 1. item1
2. item2
3. item3"#,
);
assert!(list.is_ordered());
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..42
LIST_ITEM@0..42
LIST_ITEM_INDENT@0..2 " "
LIST_ITEM_BULLET@2..5 "1. "
LIST_ITEM_CONTENT@5..42
PARAGRAPH@5..11
TEXT@5..11 "item1\n"
LIST@11..28
LIST_ITEM@11..28
LIST_ITEM_INDENT@11..19 " "
LIST_ITEM_BULLET@19..22 "2. "
LIST_ITEM_CONTENT@22..28
PARAGRAPH@22..28
TEXT@22..28 "item2\n"
LIST@28..42
LIST_ITEM@28..42
LIST_ITEM_INDENT@28..34 " "
LIST_ITEM_BULLET@34..37 "3. "
LIST_ITEM_CONTENT@37..42
PARAGRAPH@37..42
TEXT@37..42 "item3"
"###
);
let list = to_list(
r#" 1. item1
#+begin_example
hello
#+end_example
"#,
);
insta::assert_debug_snapshot!(
list.syntax,
@r###"
LIST@0..51
LIST_ITEM@0..51
LIST_ITEM_INDENT@0..2 " "
LIST_ITEM_BULLET@2..5 "1. "
LIST_ITEM_CONTENT@5..51
PARAGRAPH@5..11
TEXT@5..11 "item1\n"
EXAMPLE_BLOCK@11..51
BLOCK_BEGIN@11..31
WHITESPACE@11..15 " "
TEXT@15..23 "#+begin_"
TEXT@23..30 "example"
TEXT@30..30 ""
NEW_LINE@30..31 "\n"
BLOCK_CONTENT@31..37
TEXT@31..37 "hello\n"
BLOCK_END@37..51
TEXT@37..43 "#+end_"
TEXT@43..50 "example"
NEW_LINE@50..51 "\n"
"###
);
let config = &ParseConfig::default();
assert!(list_node(("-a", config).into()).is_err());
}

108
src/syntax/macros.rs Normal file
View file

@ -0,0 +1,108 @@
use nom::{
bytes::complete::{take_until, take_while1},
combinator::{map, opt, verify},
sequence::tuple,
AsBytes, IResult,
};
use super::{
combinator::{
debug_assert_lossless, l_curly3_token, l_parens_token, node, r_curly3_token,
r_parens_token, GreenElement,
},
input::Input,
SyntaxKind::*,
};
pub fn macros_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
l_curly3_token,
verify(
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
|s: &Input| s.as_bytes()[0].is_ascii_alphabetic(),
),
opt(tuple((l_parens_token, take_until(")}}}"), r_parens_token))),
r_curly3_token,
)),
|(l_curly3, name, argument, r_curly3)| {
let mut children = vec![];
children.push(l_curly3);
children.push(name.text_token());
if let Some((l_parens, argument, r_parens)) = argument {
children.push(node(
MACROS_ARGUMENT,
[l_parens, argument.text_token(), r_parens],
));
}
children.push(r_curly3);
node(MACROS, children)
},
))(input)
}
#[test]
fn test() {
use crate::{ast::Macros, tests::to_ast, ParseConfig};
let to_macros = to_ast::<Macros>(macros_node);
insta::assert_debug_snapshot!(
to_macros("{{{title}}}").syntax,
@r###"
MACROS@0..11
L_CURLY3@0..3 "{{{"
TEXT@3..8 "title"
R_CURLY3@8..11 "}}}"
"###
);
insta::assert_debug_snapshot!(
to_macros("{{{one_arg_macro(1)}}}").syntax,
@r###"
MACROS@0..22
L_CURLY3@0..3 "{{{"
TEXT@3..16 "one_arg_macro"
MACROS_ARGUMENT@16..19
L_PARENS@16..17 "("
TEXT@17..18 "1"
R_PARENS@18..19 ")"
R_CURLY3@19..22 "}}}"
"###
);
insta::assert_debug_snapshot!(
to_macros("{{{two_arg_macro(1, 2)}}}").syntax,
@r###"
MACROS@0..25
L_CURLY3@0..3 "{{{"
TEXT@3..16 "two_arg_macro"
MACROS_ARGUMENT@16..22
L_PARENS@16..17 "("
TEXT@17..21 "1, 2"
R_PARENS@21..22 ")"
R_CURLY3@22..25 "}}}"
"###
);
insta::assert_debug_snapshot!(
to_macros("{{{two_arg_macro(1\\,a, 2)}}}").syntax,
@r###"
MACROS@0..28
L_CURLY3@0..3 "{{{"
TEXT@3..16 "two_arg_macro"
MACROS_ARGUMENT@16..25
L_PARENS@16..17 "("
TEXT@17..24 "1\\,a, 2"
R_PARENS@24..25 ")"
R_CURLY3@25..28 "}}}"
"###
);
let config = &ParseConfig::default();
assert!(macros_node(("{{{0uthor}}}", config).into()).is_err());
assert!(macros_node(("{{{author}}", config).into()).is_err());
assert!(macros_node(("{{{poem(}}}", config).into()).is_err());
assert!(macros_node(("{{{poem)}}}", config).into()).is_err());
}

209
src/syntax/mod.rs Normal file
View file

@ -0,0 +1,209 @@
//! Org-mode elements
pub mod block;
pub mod clock;
pub mod combinator;
pub mod comment;
pub mod cookie;
pub mod document;
pub mod drawer;
pub mod dyn_block;
pub mod element;
pub mod emphasis;
pub mod fixed_width;
pub mod fn_def;
pub mod fn_ref;
pub mod headline;
pub mod inline_call;
pub mod inline_src;
pub mod input;
pub mod keyword;
pub mod link;
pub mod list;
pub mod macros;
pub mod object;
pub mod paragraph;
pub mod planning;
pub mod radio_target;
pub mod rule;
pub mod snippet;
pub mod table;
pub mod target;
pub mod timestamp;
use rowan::Language;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OrgLanguage;
impl Language for OrgLanguage {
type Kind = SyntaxKind;
fn kind_from_raw(raw: rowan::SyntaxKind) -> SyntaxKind {
// SAFETY: SyntaxKind is `repr(u16)`
unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
}
fn kind_to_raw(kind: SyntaxKind) -> rowan::SyntaxKind {
rowan::SyntaxKind(kind as u16)
}
}
pub type SyntaxNode = rowan::SyntaxNode<OrgLanguage>;
pub type SyntaxToken = rowan::SyntaxToken<OrgLanguage>;
pub type SyntaxElement = rowan::SyntaxElement<OrgLanguage>;
pub type SyntaxNodeChildren = rowan::SyntaxNodeChildren<OrgLanguage>;
pub type SyntaxElementChildren = rowan::SyntaxElementChildren<OrgLanguage>;
#[allow(bad_style)]
#[allow(clippy::all)]
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
#[repr(u16)]
pub enum SyntaxKind {
//
// token
//
L_BRACKET, // '['
R_BRACKET, // ']'
L_BRACKET2, // '[['
R_BRACKET2, // ']]'
L_PARENS, // '('
R_PARENS, // ')'
L_ANGLE, // '<'
R_ANGLE, // '>'
L_CURLY, // '{'
R_CURLY, // '}'
L_CURLY3, // '{{{'
R_CURLY3, // '}}}'
L_ANGLE2, // '<<'
R_ANGLE2, // '>>'
L_ANGLE3, // '<<<'
R_ANGLE3, // '>>>'
AT, // '@'
AT2, // '@@'
PERCENT, // '%'
PERCENT2, // '%%'
SLASH, // '/'
UNDERSCORE, // '_'
STAR, // '*'
PLUS, // '+'
MINUS, // '-'
MINUS2, // '--'
COLON, // ':'
COLON2, // '::'
EQUAL, // '='
TILDE, // '~'
HASH, // '#'
HASH_PLUS, // '#+'
DOUBLE_ARROW, // '=>'
PIPE, // '|'
COMMA, // ','
TEXT,
BLANK_LINE,
WHITESPACE,
NEW_LINE,
DOCUMENT,
SECTION,
PARAGRAPH,
HEADLINE,
HEADLINE_STARS,
HEADLINE_TITLE,
HEADLINE_KEYWORD,
HEADLINE_PRIORITY,
HEADLINE_TAGS,
PROPERTY_DRAWER,
NODE_PROPERTY,
PLANNING,
PLANNING_DEADLINE,
PLANNING_SCHEDULED,
PLANNING_CLOSED,
//
// elements
//
/* table */
ORG_TABLE,
ORG_TABLE_RULE_ROW,
ORG_TABLE_STANDARD_ROW,
ORG_TABLE_CELL,
/* list */
LIST,
LIST_ITEM,
LIST_ITEM_INDENT,
LIST_ITEM_BULLET,
LIST_ITEM_COUNTER,
LIST_ITEM_CHECK_BOX,
LIST_ITEM_TAG,
LIST_ITEM_CONTENT,
/* drawer */
DRAWER,
DRAWER_BEGIN,
DRAWER_END,
KEYWORD,
BABEL_CALL,
TABLE_EL,
CLOCK,
FN_DEF,
COMMENT,
RULE,
FIXED_WIDTH,
/* dyn block */
DYN_BLOCK,
DYN_BLOCK_BEGIN,
DYN_BLOCK_END,
/* block */
SPECIAL_BLOCK,
QUOTE_BLOCK,
CENTER_BLOCK,
VERSE_BLOCK,
COMMENT_BLOCK,
EXAMPLE_BLOCK,
EXPORT_BLOCK,
SOURCE_BLOCK,
SOURCE_BLOCK_LANG,
BLOCK_BEGIN,
BLOCK_END,
BLOCK_CONTENT,
//
// objects
//
INLINE_CALL,
INLINE_SRC,
LINK,
LINK_PATH,
COOKIE,
RADIO_TARGET,
FN_REF,
LATEX_ENVIRONMENT,
MACROS,
MACROS_ARGUMENT,
SNIPPET,
TARGET,
BOLD,
STRIKE,
ITALIC,
UNDERLINE,
VERBATIM,
CODE,
/* timestamp */
TIMESTAMP_ACTIVE,
TIMESTAMP_INACTIVE,
TIMESTAMP_DIARY,
TIMESTAMP_YEAR,
TIMESTAMP_MONTH,
TIMESTAMP_DAY,
TIMESTAMP_HOUR,
TIMESTAMP_MINUTE,
TIMESTAMP_DAYNAME,
}
impl From<SyntaxKind> for rowan::SyntaxKind {
fn from(value: SyntaxKind) -> Self {
OrgLanguage::kind_to_raw(value)
}
}

194
src/syntax/object.rs Normal file
View file

@ -0,0 +1,194 @@
use nom::{AsBytes, IResult, InputLength, InputTake};
use super::{
combinator::GreenElement,
cookie::cookie_node,
emphasis::{bold_node, code_node, italic_node, strike_node, underline_node, verbatim_node},
fn_ref::fn_ref_node,
inline_call::inline_call_node,
inline_src::inline_src_node,
input::Input,
link::link_node,
macros::macros_node,
radio_target::radio_target_node,
snippet::snippet_node,
target::target_node,
timestamp::{timestamp_active_node, timestamp_diary_node, timestamp_inactive_node},
};
pub struct InlinePositions<'a> {
bytes: &'a [u8],
pos: usize,
next: Option<usize>,
}
impl InlinePositions<'_> {
pub fn new(bytes: &[u8]) -> InlinePositions {
InlinePositions {
bytes,
pos: 0,
next: Some(0),
}
}
}
impl Iterator for InlinePositions<'_> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
self.next.take().or_else(|| {
jetscii::bytes!(b'@', b'<', b'[', b' ', b'(', b'{', b'\'', b'"', b'\n')
.find(&self.bytes[self.pos..])
.map(|i| {
self.pos += i + 1;
match self.bytes[self.pos - 1] {
b'{' => {
self.next = Some(self.pos);
self.pos - 1
}
b' ' | b'(' | b'\'' | b'"' | b'\n' => self.pos,
_ => self.pos - 1,
}
})
})
}
}
pub fn object_nodes(input: Input) -> Vec<GreenElement> {
// debug_assert!(!input.is_empty());
let nodes = object_nodes_base(input);
debug_assert_eq!(
input.as_str(),
nodes.iter().fold(String::new(), |s, i| s + &i.to_string()),
"parser must be lossless"
);
nodes
}
fn object_nodes_base(input: Input) -> Vec<GreenElement> {
let mut children = vec![];
let mut i = input;
'l: loop {
for (input, head) in InlinePositions::new(i.as_bytes()).map(|idx| i.take_split(idx)) {
if let Ok((input, node)) = object_node(input) {
if !head.is_empty() {
children.push(head.text_token())
}
children.push(node);
i = input;
continue 'l;
}
}
break;
}
if !i.is_empty() {
children.push(i.text_token());
}
children
}
fn object_node(i: Input) -> IResult<Input, GreenElement, ()> {
if i.input_len() < 3 {
return Err(nom::Err::Error(()));
}
match &i.as_bytes()[0] {
b'*' => bold_node(i),
b'+' => strike_node(i),
b'/' => italic_node(i),
b'_' => underline_node(i),
b'=' => verbatim_node(i),
b'~' => code_node(i),
b'@' => snippet_node(i),
b'{' => macros_node(i),
b'<' => radio_target_node(i)
.or_else(|_| target_node(i))
.or_else(|_| timestamp_diary_node(i))
.or_else(|_| timestamp_active_node(i)),
b'[' => cookie_node(i)
.or_else(|_| link_node(i))
.or_else(|_| fn_ref_node(i))
.or_else(|_| timestamp_inactive_node(i)),
b'c' => inline_call_node(i),
b's' => inline_src_node(i),
_ => Err(nom::Err::Error(())),
}
}
#[test]
fn parse() {
use crate::{
syntax::{combinator::node, SyntaxKind, SyntaxNode},
ParseConfig,
};
let t = |input: &str| {
let config = &ParseConfig::default();
let children = object_nodes((input, config).into());
SyntaxNode::new_root(node(SyntaxKind::PARAGRAPH, children).into_node().unwrap())
};
insta::assert_debug_snapshot!(
t("~org-inlinetask-min-level~[fn:oiml:The default value of \n~org-inlinetask-min-level~ is =15=.]"),
@r###"
PARAGRAPH@0..93
CODE@0..26
TILDE@0..1 "~"
TEXT@1..25 "org-inlinetask-min-level"
TILDE@25..26 "~"
FN_REF@26..93
L_BRACKET@26..27 "["
TEXT@27..29 "fn"
COLON@29..30 ":"
TEXT@30..34 "oiml"
COLON@34..35 ":"
TEXT@35..57 "The default value of \n"
CODE@57..83
TILDE@57..58 "~"
TEXT@58..82 "org-inlinetask-min-level"
TILDE@82..83 "~"
TEXT@83..87 " is "
VERBATIM@87..91
EQUAL@87..88 "="
TEXT@88..90 "15"
EQUAL@90..91 "="
TEXT@91..92 "."
R_BRACKET@92..93 "]"
"###
);
insta::assert_debug_snapshot!(
t(r#"Org is a /plaintext markup syntax/ developed with *Emacs* in 2003.
The canonical parser is =org-element.el=, which provides a number of
functions starting with ~org-element-~."#),
@r###"
PARAGRAPH@0..175
TEXT@0..9 "Org is a "
ITALIC@9..34
SLASH@9..10 "/"
TEXT@10..33 "plaintext markup syntax"
SLASH@33..34 "/"
TEXT@34..50 " developed with "
BOLD@50..57
STAR@50..51 "*"
TEXT@51..56 "Emacs"
STAR@56..57 "*"
TEXT@57..91 " in 2003.\nThe canonic ..."
VERBATIM@91..107
EQUAL@91..92 "="
TEXT@92..106 "org-element.el"
EQUAL@106..107 "="
TEXT@107..160 ", which provides a nu ..."
CODE@160..174
TILDE@160..161 "~"
TEXT@161..173 "org-element-"
TILDE@173..174 "~"
TEXT@174..175 "."
"###
);
}

96
src/syntax/paragraph.rs Normal file
View file

@ -0,0 +1,96 @@
use nom::{IResult, InputTake};
use super::{
combinator::{blank_lines, debug_assert_lossless, line_ends_iter, node, GreenElement},
input::Input,
object::object_nodes,
SyntaxKind,
};
fn paragraph_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert!(!input.is_empty());
let mut start = 0;
for idx in line_ends_iter(input.as_str()) {
// stops at blank line
if input.s[start..idx].bytes().all(|c| c.is_ascii_whitespace()) {
break;
}
start = idx;
}
let (input, contents) = input.take_split(start);
let (input, post_blank) = blank_lines(input)?;
let mut children = vec![];
children.extend(object_nodes(contents));
children.extend(post_blank);
Ok((input, node(SyntaxKind::PARAGRAPH, children)))
}
pub fn paragraph_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(paragraph_node_base)(input)
}
pub fn paragraph_nodes(input: Input) -> Result<Vec<GreenElement>, nom::Err<()>> {
let mut i = input;
let mut children = vec![];
while !i.is_empty() {
let (input, node) = paragraph_node(i)?;
children.push(node);
i = input;
}
Ok(children)
}
#[test]
fn parse() {
use crate::{ast::Paragraph, tests::to_ast};
let to_paragraph = to_ast::<Paragraph>(paragraph_node);
insta::assert_debug_snapshot!(
to_paragraph(r#"a"#).syntax,
@r###"
PARAGRAPH@0..1
TEXT@0..1 "a"
"###
);
insta::assert_debug_snapshot!(
to_paragraph(r#"a
"#).syntax,
@r###"
PARAGRAPH@0..6
TEXT@0..2 "a\n"
BLANK_LINE@2..6
WHITESPACE@2..6 " "
"###
);
insta::assert_debug_snapshot!(
to_paragraph(r#"a
b
c
"#).syntax,
@r###"
PARAGRAPH@0..6
TEXT@0..6 "a\nb\nc\n"
"###
);
insta::assert_debug_snapshot!(
to_paragraph(r#"a
c
"#).syntax,
@r###"
PARAGRAPH@0..3
TEXT@0..2 "a\n"
BLANK_LINE@2..3
NEW_LINE@2..3 "\n"
"###
);
}

94
src/syntax/planning.rs Normal file
View file

@ -0,0 +1,94 @@
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{line_ending, space0},
combinator::{eof, iterator},
sequence::tuple,
IResult,
};
use super::{
combinator::{debug_assert_lossless, GreenElement, NodeBuilder},
input::Input,
timestamp::{timestamp_active_node, timestamp_inactive_node},
SyntaxKind::*,
};
pub fn planning_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(planning_node_base)(input)
}
fn planning_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let mut b = NodeBuilder::new();
let mut it = iterator(
input,
tuple((
space0,
alt((tag("DEADLINE:"), tag("SCHEDULED:"), tag("CLOSED:"))),
space0,
alt((timestamp_active_node, timestamp_inactive_node)),
)),
);
let start_len = b.len();
it.for_each(|(ws, text, ws_, timestamp)| {
let mut b_ = NodeBuilder::new();
b_.ws(ws);
b_.text(text);
b_.ws(ws_);
b_.push(timestamp);
b.push(b_.finish(match text.as_str() {
"DEADLINE:" => PLANNING_DEADLINE,
"SCHEDULED:" => PLANNING_SCHEDULED,
"CLOSED:" => PLANNING_CLOSED,
_ => unreachable!(),
}));
});
if b.len() == start_len {
return Err(nom::Err::Error(()));
}
let (input, _) = it.finish()?;
let (input, ws) = space0(input)?;
let (input, nl) = alt((line_ending, eof))(input)?;
b.ws(ws);
b.nl(nl);
Ok((input, b.finish(PLANNING)))
}
#[test]
fn prase() {
use crate::{ast::Planning, tests::to_ast, ParseConfig};
let to_planning = to_ast::<Planning>(planning_node);
insta::assert_debug_snapshot!(
to_planning("SCHEDULED: <2019-04-08 Mon>").syntax,
@r###"
PLANNING@0..27
PLANNING_SCHEDULED@0..27
TEXT@0..10 "SCHEDULED:"
WHITESPACE@10..11 " "
TIMESTAMP_ACTIVE@11..27
L_ANGLE@11..12 "<"
TIMESTAMP_YEAR@12..16 "2019"
MINUS@16..17 "-"
TIMESTAMP_MONTH@17..19 "04"
MINUS@19..20 "-"
TIMESTAMP_DAY@20..22 "08"
WHITESPACE@22..23 " "
TIMESTAMP_DAYNAME@23..26 "Mon"
R_ANGLE@26..27 ">"
"###
);
let config = &ParseConfig::default();
assert!(planning_node((" ", config).into()).is_err());
assert!(planning_node((" SCHEDULED: ", config).into()).is_err());
}

View file

@ -0,0 +1,68 @@
use nom::{
bytes::complete::take_while,
combinator::{map, verify},
sequence::tuple,
IResult,
};
use super::{
combinator::{debug_assert_lossless, l_angle3_token, node, r_angle3_token, GreenElement},
input::Input,
SyntaxKind::*,
};
// TODO: text-markup, entities, latex-fragments, subscript and superscript
pub fn radio_target_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
l_angle3_token,
verify(
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|s: &Input| {
s.as_str().starts_with(|c| c != ' ') && s.as_str().ends_with(|c| c != ' ')
},
),
r_angle3_token,
)),
|(l_angle3, contents, r_angle3)| {
node(RADIO_TARGET, [l_angle3, contents.text_token(), r_angle3])
},
))(input)
}
#[test]
fn parse() {
use crate::{ast::RadioTarget, tests::to_ast, ParseConfig};
let to_radio_target = to_ast::<RadioTarget>(radio_target_node);
insta::assert_debug_snapshot!(
to_radio_target("<<<target>>>").syntax,
@r###"
RADIO_TARGET@0..12
L_ANGLE3@0..3 "<<<"
TEXT@3..9 "target"
R_ANGLE3@9..12 ">>>"
"###
);
insta::assert_debug_snapshot!(
to_radio_target("<<<tar get>>>").syntax,
@r###"
RADIO_TARGET@0..13
L_ANGLE3@0..3 "<<<"
TEXT@3..10 "tar get"
R_ANGLE3@10..13 ">>>"
"###
);
let config = &ParseConfig::default();
assert!(radio_target_node(("<<<target >>>", config).into()).is_err());
assert!(radio_target_node(("<<< target>>>", config).into()).is_err());
assert!(radio_target_node(("<<<ta<get>>>", config).into()).is_err());
assert!(radio_target_node(("<<<ta>get>>>", config).into()).is_err());
assert!(radio_target_node(("<<<ta\nget>>>", config).into()).is_err());
assert!(radio_target_node(("<<<target>>", config).into()).is_err());
}

93
src/syntax/rule.rs Normal file
View file

@ -0,0 +1,93 @@
use nom::{
branch::alt,
bytes::complete::take_while_m_n,
character::complete::{line_ending, space0},
combinator::{eof, map},
sequence::tuple,
IResult,
};
use super::{
combinator::{blank_lines, debug_assert_lossless, GreenElement, NodeBuilder},
input::Input,
SyntaxKind::*,
};
pub fn rule_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
space0,
take_while_m_n(5, usize::max_value(), |c| c == '-'),
space0,
alt((line_ending, eof)),
blank_lines,
)),
|(ws, dashes, ws_, nl, post_blank)| {
let mut b = NodeBuilder::new();
b.ws(ws);
b.text(dashes);
b.ws(ws_);
b.nl(nl);
b.children.extend(post_blank);
b.finish(RULE)
},
))(input)
}
#[test]
fn parse() {
use crate::{ast::Rule, tests::to_ast, ParseConfig};
let to_rule = to_ast::<Rule>(rule_node);
insta::assert_debug_snapshot!(
to_rule("-----").syntax,
@r###"
RULE@0..5
TEXT@0..5 "-----"
"###
);
insta::assert_debug_snapshot!(
to_rule("--------").syntax,
@r###"
RULE@0..8
TEXT@0..8 "--------"
"###
);
insta::assert_debug_snapshot!(
to_rule("-----\n\n\n").syntax,
@r###"
RULE@0..8
TEXT@0..5 "-----"
NEW_LINE@5..6 "\n"
BLANK_LINE@6..7
NEW_LINE@6..7 "\n"
BLANK_LINE@7..8
NEW_LINE@7..8 "\n"
"###
);
insta::assert_debug_snapshot!(
to_rule("----- \n").syntax,
@r###"
RULE@0..8
TEXT@0..5 "-----"
WHITESPACE@5..7 " "
NEW_LINE@7..8 "\n"
"###
);
let config = &ParseConfig::default();
assert!(rule_node(("", config).into()).is_err());
assert!(rule_node(("----", config).into()).is_err());
assert!(rule_node(("None----", config).into()).is_err());
assert!(rule_node(("None ----", config).into()).is_err());
assert!(rule_node(("None------", config).into()).is_err());
assert!(rule_node(("----None----", config).into()).is_err());
assert!(rule_node(("\t\t----", config).into()).is_err());
assert!(rule_node(("------None", config).into()).is_err());
assert!(rule_node(("----- None", config).into()).is_err());
}

91
src/syntax/snippet.rs Normal file
View file

@ -0,0 +1,91 @@
use nom::{
bytes::complete::{take_until, take_while1},
combinator::map,
sequence::tuple,
IResult,
};
use super::{
combinator::{at2_token, colon_token, debug_assert_lossless, node, GreenElement},
input::Input,
SyntaxKind::*,
};
pub fn snippet_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
at2_token,
take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-'),
colon_token,
take_until("@@"),
at2_token,
)),
|(at2, name, colon, value, at2_)| {
node(
SNIPPET,
[at2, name.text_token(), colon, value.text_token(), at2_],
)
},
))(input)
}
#[test]
fn parse() {
use crate::{ast::Snippet, tests::to_ast, ParseConfig};
let to_snippet = to_ast::<Snippet>(snippet_node);
insta::assert_debug_snapshot!(
to_snippet("@@html:<b>@@").syntax,
@r###"
SNIPPET@0..12
AT2@0..2 "@@"
TEXT@2..6 "html"
COLON@6..7 ":"
TEXT@7..10 "<b>"
AT2@10..12 "@@"
"###
);
insta::assert_debug_snapshot!(
to_snippet("@@latex:any arbitrary LaTeX code@@").syntax,
@r###"
SNIPPET@0..34
AT2@0..2 "@@"
TEXT@2..7 "latex"
COLON@7..8 ":"
TEXT@8..32 "any arbitrary LaTeX code"
AT2@32..34 "@@"
"###
);
insta::assert_debug_snapshot!(
to_snippet("@@html:@@").syntax,
@r###"
SNIPPET@0..9
AT2@0..2 "@@"
TEXT@2..6 "html"
COLON@6..7 ":"
TEXT@7..7 ""
AT2@7..9 "@@"
"###
);
insta::assert_debug_snapshot!(
to_snippet("@@html:<p>@</p>@@").syntax,
@r###"
SNIPPET@0..17
AT2@0..2 "@@"
TEXT@2..6 "html"
COLON@6..7 ":"
TEXT@7..15 "<p>@</p>"
AT2@15..17 "@@"
"###
);
let config = &ParseConfig::default();
assert!(snippet_node(("@@html:<b>@", config).into()).is_err());
assert!(snippet_node(("@@html<b>@@", config).into()).is_err());
assert!(snippet_node(("@@:<b>@@", config).into()).is_err());
}

209
src/syntax/table.rs Normal file
View file

@ -0,0 +1,209 @@
use nom::{
bytes::complete::take_while,
character::complete::{multispace0, space0},
combinator::iterator,
sequence::tuple,
AsBytes, Err, IResult, InputTake, Slice,
};
use super::{
combinator::{
blank_lines, debug_assert_lossless, line_ends_iter, node, pipe_token, GreenElement,
NodeBuilder,
},
input::Input,
object::object_nodes,
SyntaxKind::*,
};
fn org_table_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let mut children = vec![];
let mut start = 0;
for i in line_ends_iter(input.as_str()) {
let line = input.slice(start..i);
let trimmed = line.as_str().trim_start();
// Org tables end at the first line not starting with a vertical bar.
if !trimmed.starts_with('|') {
if start == 0 {
return Err(nom::Err::Error(()));
} else {
break;
}
}
if trimmed.starts_with("|-") {
children.push(node(ORG_TABLE_RULE_ROW, [line.text_token()]));
} else {
children.push(table_standard_row_node(line)?);
}
start = i;
}
let (input, post_blank) = blank_lines(input.slice(start..))?;
children.extend(post_blank);
Ok((input, node(ORG_TABLE, children)))
}
fn table_standard_row_node(input: Input) -> Result<GreenElement, nom::Err<()>> {
let mut b = NodeBuilder::new();
let (input, ws) = space0(input)?;
b.ws(ws);
let mut it = iterator(
input,
tuple((pipe_token, multispace0, take_while(|c: char| c != '|'))),
);
it.for_each(|(pipe, ws, input)| {
b.push(pipe);
b.ws(ws);
if input.is_empty() {
return;
}
match input
.as_bytes()
.iter()
.rposition(|b| !b.is_ascii_whitespace())
{
Some(idx) => {
let (ws, cell) = input.take_split(idx + 1);
b.push(node(ORG_TABLE_CELL, object_nodes(cell)));
b.ws(ws);
}
_ => {
b.push(node(ORG_TABLE_CELL, object_nodes(input)));
}
}
});
it.finish()?;
Ok(b.finish(ORG_TABLE_STANDARD_ROW))
}
fn table_el_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let mut start = 0;
for i in line_ends_iter(input.as_str()) {
let line = &input.s[start..i];
let trimmed = line.trim();
if start == 0 {
// Table.el tables start at lines beginning with "+-" string and followed by plus or minus signs
if !trimmed.starts_with("+-") || trimmed.bytes().any(|c| c != b'+' && c != b'-') {
return Err(Err::Error(()));
}
}
// Table.el tables end at the first line not starting with either a vertical line or a plus sign.
if !trimmed.starts_with('|') && !trimmed.starts_with('+') {
break;
}
start = i;
}
let (input, contents) = input.take_split(start);
let (input, post_blank) = blank_lines(input)?;
let mut children = vec![];
children.push(contents.text_token());
children.extend(post_blank);
Ok((input, node(TABLE_EL, children)))
}
pub fn org_table_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(org_table_node_base)(input)
}
pub fn table_el_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(table_el_node_base)(input)
}
#[test]
fn parse_org_table() {
use crate::{ast::OrgTable, tests::to_ast};
let to_org_table = to_ast::<OrgTable>(org_table_node);
insta::assert_debug_snapshot!(
to_org_table("|").syntax,
@r###"
ORG_TABLE@0..1
ORG_TABLE_STANDARD_ROW@0..1
PIPE@0..1 "|"
"###
);
insta::assert_debug_snapshot!(
to_org_table(
r#"|
|-
|a
|-
| a |
"#
).syntax,
@r###"
ORG_TABLE@0..20
ORG_TABLE_STANDARD_ROW@0..2
PIPE@0..1 "|"
WHITESPACE@1..2 "\n"
ORG_TABLE_RULE_ROW@2..5
TEXT@2..5 "|-\n"
ORG_TABLE_STANDARD_ROW@5..8
PIPE@5..6 "|"
ORG_TABLE_CELL@6..7
TEXT@6..7 "a"
WHITESPACE@7..8 "\n"
ORG_TABLE_RULE_ROW@8..11
TEXT@8..11 "|-\n"
ORG_TABLE_STANDARD_ROW@11..20
PIPE@11..12 "|"
WHITESPACE@12..15 " "
ORG_TABLE_CELL@15..16
TEXT@15..16 "a"
WHITESPACE@16..18 " "
PIPE@18..19 "|"
WHITESPACE@19..20 "\n"
"###
);
}
#[test]
fn parse_table_el() {
use crate::{ast::TableEl, tests::to_ast, ParseConfig};
let to_table_el = to_ast::<TableEl>(table_el_node);
insta::assert_debug_snapshot!(
to_table_el(
r#" +---+
| |
+---+
"#
).syntax,
@r###"
TABLE_EL@0..37
TEXT@0..32 " +---+\n | |\n ..."
BLANK_LINE@32..33
NEW_LINE@32..33 "\n"
BLANK_LINE@33..37
WHITESPACE@33..37 " "
"###
);
let config = &ParseConfig::default();
assert!(table_el_node(("", config).into()).is_err());
assert!(table_el_node(("+----|---", config).into()).is_err());
}

64
src/syntax/target.rs Normal file
View file

@ -0,0 +1,64 @@
use nom::{
bytes::complete::take_while,
combinator::{map, verify},
sequence::tuple,
IResult,
};
use super::{
combinator::{debug_assert_lossless, l_angle2_token, node, r_angle2_token, GreenElement},
input::Input,
SyntaxKind::*,
};
pub fn target_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
l_angle2_token,
verify(
take_while(|c: char| c != '<' && c != '\n' && c != '>'),
|s: &Input| {
s.as_str().starts_with(|c| c != ' ') && s.as_str().ends_with(|c| c != ' ')
},
),
r_angle2_token,
)),
|(l_angle2, target, r_angle2)| node(TARGET, [l_angle2, target.text_token(), r_angle2]),
))(input)
}
#[test]
fn parse() {
use crate::{ast::Target, tests::to_ast, ParseConfig};
let to_target = to_ast::<Target>(target_node);
insta::assert_debug_snapshot!(
to_target("<<target>>").syntax,
@r###"
TARGET@0..10
L_ANGLE2@0..2 "<<"
TEXT@2..8 "target"
R_ANGLE2@8..10 ">>"
"###
);
insta::assert_debug_snapshot!(
to_target("<<tar get>>").syntax,
@r###"
TARGET@0..11
L_ANGLE2@0..2 "<<"
TEXT@2..9 "tar get"
R_ANGLE2@9..11 ">>"
"###
);
let config = &ParseConfig::default();
assert!(target_node(("<<target >>", config).into()).is_err());
assert!(target_node(("<< target>>", config).into()).is_err());
assert!(target_node(("<<ta<get>>", config).into()).is_err());
assert!(target_node(("<<ta>get>>", config).into()).is_err());
assert!(target_node(("<<ta\nget>>", config).into()).is_err());
assert!(target_node(("<<target>", config).into()).is_err());
}

326
src/syntax/timestamp.rs Normal file
View file

@ -0,0 +1,326 @@
use nom::{
bytes::complete::{take, take_till, take_while},
character::complete::{space0, space1},
combinator::{map, opt, verify},
sequence::tuple,
IResult,
};
use super::{
combinator::{
colon_token, debug_assert_lossless, l_angle_token, l_bracket_token, l_parens_token,
minus2_token, minus_token, node, percent2_token, r_angle_token, r_bracket_token,
r_parens_token, GreenElement, NodeBuilder,
},
input::Input,
SyntaxKind::*,
};
pub fn timestamp_diary_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(map(
tuple((
l_angle_token,
percent2_token,
l_parens_token,
take_till(|c| c == ')' || c == '>' || c == '\n'),
r_parens_token,
r_angle_token,
)),
|(l_angle, percent2, l_paren, value, r_paren, r_angle)| {
node(
TIMESTAMP_DIARY,
[
l_angle,
percent2,
l_paren,
value.text_token(),
r_paren,
r_angle,
],
)
},
))(input)
}
fn is_digit_str(s: &Input) -> bool {
s.as_str().bytes().all(|u| u.is_ascii_digit())
}
fn date(i: Input) -> IResult<Input, [GreenElement; 7], ()> {
map(
tuple((
verify(take(4usize), is_digit_str),
minus_token,
verify(take(2usize), is_digit_str),
minus_token,
verify(take(2usize), is_digit_str),
space1,
take_while(|c: char| {
!c.is_ascii_whitespace()
&& !c.is_ascii_digit()
&& c != '+'
&& c != '-'
&& c != ']'
&& c != '>'
}),
)),
|(year, minus, month, minus_, day, ws, dayname)| {
[
year.token(TIMESTAMP_YEAR),
minus,
month.token(TIMESTAMP_MONTH),
minus_,
day.token(TIMESTAMP_DAY),
ws.ws_token(),
dayname.token(TIMESTAMP_DAYNAME),
]
},
)(i)
}
fn time(i: Input) -> IResult<Input, [GreenElement; 3], ()> {
map(
tuple((
verify(take(2usize), is_digit_str),
colon_token,
verify(take(2usize), is_digit_str),
)),
|(hour, colon, minute)| {
[
hour.token(TIMESTAMP_HOUR),
colon,
minute.token(TIMESTAMP_MINUTE),
]
},
)(i)
}
fn timestamp_active_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, l_angle) = l_angle_token(input)?;
let (input, start_date) = date(input)?;
let (input, start_time) = opt(tuple((space1, time)))(input)?;
let mut b = NodeBuilder::new();
b.push(l_angle);
b.children.extend(start_date);
if input.as_str().starts_with('-') {
let (ws, start_time) = match start_time {
Some(start_time) => start_time,
None => return Err(nom::Err::Error(())),
};
let (input, minus) = minus_token(input)?;
let (input, end_time) = time(input)?;
let (input, space) = space0(input)?;
// TODO: delay-or-repeater
let (input, r_angle) = r_angle_token(input)?;
b.ws(ws);
b.children.extend(start_time);
b.push(minus);
b.children.extend(end_time);
b.ws(space);
b.push(r_angle);
return Ok((input, b.finish(TIMESTAMP_ACTIVE)));
}
let (input, space) = space0(input)?;
let (input, r_angle) = r_angle_token(input)?;
if let Some((ws, start_time)) = start_time {
b.ws(ws);
b.children.extend(start_time);
}
b.ws(space);
b.push(r_angle);
if input.as_str().starts_with("--<") {
let (input, minus2) = minus2_token(input)?;
let (input, l_angle) = l_angle_token(input)?;
let (input, end_date) = date(input)?;
let (input, end_time) = opt(tuple((space1, time)))(input)?;
let (input, space_) = space0(input)?;
// TODO: delay-or-repeater
let (input, r_angle) = r_angle_token(input)?;
b.children.extend([minus2, l_angle]);
b.children.extend(end_date);
if let Some((ws, end_time)) = end_time {
b.ws(ws);
b.children.extend(end_time);
}
b.ws(space_);
b.push(r_angle);
Ok((input, b.finish(TIMESTAMP_ACTIVE)))
} else {
Ok((input, b.finish(TIMESTAMP_ACTIVE)))
}
}
fn timestamp_inactive_node_base(input: Input) -> IResult<Input, GreenElement, ()> {
let (input, l_bracket) = l_bracket_token(input)?;
let (input, start_date) = date(input)?;
let (input, start_time) = opt(tuple((space1, time)))(input)?;
let mut b = NodeBuilder::new();
b.push(l_bracket);
b.children.extend(start_date);
if input.s.starts_with('-') {
let (ws, start_time) = match start_time {
Some(start_time) => start_time,
None => return Err(nom::Err::Error(())),
};
let (input, minus) = minus_token(input)?;
let (input, end_time) = time(input)?;
let (input, space) = space0(input)?;
// TODO: delay-or-repeater
let (input, r_bracket) = r_bracket_token(input)?;
b.ws(ws);
b.children.extend(start_time);
b.push(minus);
b.children.extend(end_time);
b.ws(space);
b.push(r_bracket);
return Ok((input, b.finish(TIMESTAMP_INACTIVE)));
}
let (input, space) = space0(input)?;
let (input, r_bracket) = r_bracket_token(input)?;
if let Some((ws, start_time)) = start_time {
b.ws(ws);
b.children.extend(start_time);
}
b.ws(space);
b.push(r_bracket);
if input.s.starts_with("--[") {
let (input, minus2) = minus2_token(input)?;
let (input, l_bracket) = l_bracket_token(input)?;
let (input, end_date) = date(input)?;
let (input, end_time) = opt(tuple((space1, time)))(input)?;
let (input, space_) = space0(input)?;
// TODO: delay-or-repeater
let (input, r_bracket) = r_bracket_token(input)?;
b.children.extend([minus2, l_bracket]);
b.children.extend(end_date);
if let Some((ws, end_time)) = end_time {
b.ws(ws);
b.children.extend(end_time);
}
b.ws(space_);
b.push(r_bracket);
Ok((input, b.finish(TIMESTAMP_INACTIVE)))
} else {
Ok((input, b.finish(TIMESTAMP_INACTIVE)))
}
}
pub fn timestamp_active_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(timestamp_active_node_base)(input)
}
pub fn timestamp_inactive_node(input: Input) -> IResult<Input, GreenElement, ()> {
debug_assert_lossless(timestamp_inactive_node_base)(input)
}
#[test]
fn parse() {
use crate::{ast::Timestamp, tests::to_ast};
let to_timestamp = to_ast::<Timestamp>(timestamp_inactive_node);
let ts = to_timestamp("[2003-09-16 Tue]");
assert!(!ts.is_range());
insta::assert_debug_snapshot!(
ts.syntax,
@r###"
TIMESTAMP_INACTIVE@0..16
L_BRACKET@0..1 "["
TIMESTAMP_YEAR@1..5 "2003"
MINUS@5..6 "-"
TIMESTAMP_MONTH@6..8 "09"
MINUS@8..9 "-"
TIMESTAMP_DAY@9..11 "16"
WHITESPACE@11..12 " "
TIMESTAMP_DAYNAME@12..15 "Tue"
R_BRACKET@15..16 "]"
"###
);
let ts = to_timestamp("[2003-09-16 Tue 09:39]--[2003-09-16 Tue 10:39]");
assert!(ts.is_range());
insta::assert_debug_snapshot!(
ts.syntax,
@r###"
TIMESTAMP_INACTIVE@0..46
L_BRACKET@0..1 "["
TIMESTAMP_YEAR@1..5 "2003"
MINUS@5..6 "-"
TIMESTAMP_MONTH@6..8 "09"
MINUS@8..9 "-"
TIMESTAMP_DAY@9..11 "16"
WHITESPACE@11..12 " "
TIMESTAMP_DAYNAME@12..15 "Tue"
WHITESPACE@15..16 " "
TIMESTAMP_HOUR@16..18 "09"
COLON@18..19 ":"
TIMESTAMP_MINUTE@19..21 "39"
R_BRACKET@21..22 "]"
MINUS2@22..24 "--"
L_BRACKET@24..25 "["
TIMESTAMP_YEAR@25..29 "2003"
MINUS@29..30 "-"
TIMESTAMP_MONTH@30..32 "09"
MINUS@32..33 "-"
TIMESTAMP_DAY@33..35 "16"
WHITESPACE@35..36 " "
TIMESTAMP_DAYNAME@36..39 "Tue"
WHITESPACE@39..40 " "
TIMESTAMP_HOUR@40..42 "10"
COLON@42..43 ":"
TIMESTAMP_MINUTE@43..45 "39"
R_BRACKET@45..46 "]"
"###
);
let ts = to_timestamp("[2003-09-16 Tue 09:39-10:39]");
assert!(ts.is_range());
insta::assert_debug_snapshot!(
ts.syntax,
@r###"
TIMESTAMP_INACTIVE@0..28
L_BRACKET@0..1 "["
TIMESTAMP_YEAR@1..5 "2003"
MINUS@5..6 "-"
TIMESTAMP_MONTH@6..8 "09"
MINUS@8..9 "-"
TIMESTAMP_DAY@9..11 "16"
WHITESPACE@11..12 " "
TIMESTAMP_DAYNAME@12..15 "Tue"
WHITESPACE@15..16 " "
TIMESTAMP_HOUR@16..18 "09"
COLON@18..19 ":"
TIMESTAMP_MINUTE@19..21 "39"
MINUS@21..22 "-"
TIMESTAMP_HOUR@22..24 "10"
COLON@24..25 ":"
TIMESTAMP_MINUTE@25..27 "39"
R_BRACKET@27..28 "]"
"###
);
}

24
src/tests.rs Normal file
View file

@ -0,0 +1,24 @@
//! test utils
use nom::IResult;
use rowan::{ast::AstNode, SyntaxNode};
use crate::{
syntax::{combinator::GreenElement, input::Input},
ParseConfig,
};
pub fn to_ast<N: AstNode>(
parser: impl Fn(Input) -> IResult<Input, GreenElement, ()>,
) -> impl Fn(&str) -> N {
move |s: &str| {
let input = Input {
s,
c: &ParseConfig::default(),
};
let element = parser(input).unwrap().1;
let node = element.into_node().unwrap();
let node = SyntaxNode::<N::Language>::new_root(node);
AstNode::cast(node).unwrap()
}
}

View file

@ -1,209 +0,0 @@
use indextree::NodeId;
use std::ops::RangeInclusive;
use crate::elements::{Element, Table, TableCell, TableRow};
use crate::Org;
/// Validation Error
#[derive(Debug)]
pub enum ValidationError {
/// Expected at least one child
ExpectedChildren {
at: NodeId,
},
/// Expected no children
UnexpectedChildren {
at: NodeId,
},
UnexpectedElement {
expected: &'static str,
at: NodeId,
},
/// Expected a detached element
ExpectedDetached {
at: NodeId,
},
/// Expected headline level in specify range
HeadlineLevelMismatch {
range: RangeInclusive<usize>,
at: NodeId,
},
}
impl ValidationError {
pub fn element<'a, 'b>(&self, org: &'a Org<'b>) -> &'a Element<'b> {
match self {
ValidationError::ExpectedChildren { at }
| ValidationError::UnexpectedChildren { at }
| ValidationError::UnexpectedElement { at, .. }
| ValidationError::ExpectedDetached { at }
| ValidationError::HeadlineLevelMismatch { at, .. } => &org[*at],
}
}
}
pub type ValidationResult<T> = Result<T, ValidationError>;
impl Org<'_> {
/// Validates an `Org` struct.
pub fn validate(&self) -> Vec<ValidationError> {
let mut errors = Vec::new();
macro_rules! expect_element {
($node:ident, $expect:expr, $($pattern:pat)|+) => {
match self[$node] {
$($pattern)|+ => (),
_ => errors.push(ValidationError::UnexpectedElement {
expected: $expect,
at: $node
}),
}
};
}
macro_rules! expect_children {
($node:ident) => {
if self.arena[$node].first_child().is_none() {
errors.push(ValidationError::ExpectedChildren { at: $node });
}
};
}
for node_id in self.root.descendants(&self.arena) {
let node = &self.arena[node_id];
match node.get() {
Element::Document { .. } => {
let mut children = node_id.children(&self.arena);
if let Some(child) = children.next() {
expect_element!(
child,
"Headline|Section",
Element::Headline { .. } | Element::Section
);
}
for child in children {
expect_element!(child, "Headline", Element::Headline { .. });
}
}
Element::Headline { .. } => {
expect_children!(node_id);
let mut children = node_id.children(&self.arena);
if let Some(child) = children.next() {
expect_element!(child, "Title", Element::Title(_));
}
if let Some(child) = children.next() {
expect_element!(
child,
"Headline|Section",
Element::Headline { .. } | Element::Section
);
}
for child in children {
expect_element!(child, "Headline", Element::Headline { .. });
}
}
Element::Title(title) => {
if !title.raw.is_empty() && node.first_child().is_none() {
errors.push(ValidationError::ExpectedChildren { at: node_id });
}
}
Element::List(_) => {
expect_children!(node_id);
for child in node_id.children(&self.arena) {
expect_element!(child, "ListItem", Element::ListItem(_));
}
}
Element::Table(Table::Org { .. }) => {
for child in node_id.children(&self.arena) {
expect_element!(child, "TableRow", Element::TableRow(_));
}
}
Element::TableRow(TableRow::Header) => {
for child in node_id.children(&self.arena) {
expect_element!(
child,
"TableCell::Header",
Element::TableCell(TableCell::Header)
);
}
}
Element::TableRow(TableRow::Body) => {
for child in node_id.children(&self.arena) {
expect_element!(
child,
"TableCell::Body",
Element::TableCell(TableCell::Body)
);
}
}
Element::CommentBlock(_)
| Element::ExampleBlock(_)
| Element::ExportBlock(_)
| Element::SourceBlock(_)
| Element::BabelCall(_)
| Element::InlineSrc(_)
| Element::Code { .. }
| Element::FnRef(_)
| Element::InlineCall(_)
| Element::Link(_)
| Element::Macros(_)
| Element::RadioTarget
| Element::Snippet(_)
| Element::Target(_)
| Element::Text { .. }
| Element::Timestamp(_)
| Element::Verbatim { .. }
| Element::FnDef(_)
| Element::Clock(_)
| Element::Comment { .. }
| Element::FixedWidth { .. }
| Element::Keyword(_)
| Element::Rule(_)
| Element::Cookie(_)
| Element::TableRow(TableRow::BodyRule)
| Element::TableRow(TableRow::HeaderRule) => {
if node.first_child().is_some() {
errors.push(ValidationError::UnexpectedChildren { at: node_id });
}
}
Element::SpecialBlock(_)
| Element::QuoteBlock(_)
| Element::CenterBlock(_)
| Element::VerseBlock(_)
| Element::Paragraph { .. }
| Element::Section
| Element::Bold
| Element::Italic
| Element::Underline
| Element::Strike
| Element::DynBlock(_) => {
expect_children!(node_id);
}
Element::ListItem(_)
| Element::Drawer(_)
| Element::TableCell(_)
| Element::Table(_) => (),
}
}
errors
}
pub(crate) fn debug_validate(&self) {
if cfg!(debug_assertions) {
let errors = self.validate();
if !errors.is_empty() {
eprintln!("Org validation failed. {} error(s) found:", errors.len());
for err in errors {
eprintln!("{:?} at {:?}", err, err.element(self));
}
panic!(
"Looks like there's a bug in orgize! Please report it with your org-mode content at https://github.com/PoiScript/orgize/issues."
);
}
}
}
}

View file

@ -1,189 +0,0 @@
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
use serde::Serialize;
use serde_wasm_bindgen::Serializer;
use wasm_bindgen::prelude::*;
use crate::{Element, Event};
#[wasm_bindgen]
pub struct Org(crate::Org<'static>);
#[wasm_bindgen]
impl Org {
#[wasm_bindgen]
pub fn parse(input: String) -> Self {
Org(crate::Org::parse_string(input))
}
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(&self) -> JsValue {
to_value(&self.0)
}
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(skip_typescript)]
pub type Handler;
#[wasm_bindgen(method)]
pub fn text(this: &Handler, text: JsValue);
#[wasm_bindgen(method)]
pub fn code(this: &Handler, item: JsValue);
#[wasm_bindgen(method)]
pub fn cookie(this: &Handler, item: JsValue);
#[wasm_bindgen(method)]
pub fn rule(this: &Handler);
#[wasm_bindgen(method, js_name = exampleBlock)]
pub fn example_block(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = exportBlock)]
pub fn export_block(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = sourceBlock)]
pub fn source_block(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = inlineSrc)]
pub fn inline_src(this: &Handler, item: JsValue);
#[wasm_bindgen(method)]
pub fn link(this: &Handler, item: JsValue);
#[wasm_bindgen(method)]
pub fn snippet(this: &Handler, item: JsValue);
#[wasm_bindgen(method)]
pub fn timestamp(this: &Handler, item: JsValue);
#[wasm_bindgen(method)]
pub fn verbatim(this: &Handler, item: JsValue);
#[wasm_bindgen(method)]
pub fn fixedWidth(this: &Handler, item: JsValue);
#[wasm_bindgen(method)]
pub fn keyword(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = listStart)]
pub fn list_start(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = listEnd)]
pub fn list_end(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = tableStart)]
pub fn table_start(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = tableEnd)]
pub fn table_end(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = tableRowStart)]
pub fn table_row_start(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = tableRowEnd)]
pub fn table_row_end(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = tableCellStart)]
pub fn table_cell_start(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = tableCellEnd)]
pub fn table_cell_end(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = titleStart)]
pub fn title_start(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = titleEnd)]
pub fn title_end(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = boldStart)]
pub fn bold_start(this: &Handler);
#[wasm_bindgen(method, js_name = boldEnd)]
pub fn bold_end(this: &Handler);
#[wasm_bindgen(method, js_name = centerBlockStart)]
pub fn center_block_start(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = centerBlockEnd)]
pub fn center_block_end(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = documentStart)]
pub fn document_start(this: &Handler);
#[wasm_bindgen(method, js_name = documentEnd)]
pub fn document_end(this: &Handler);
#[wasm_bindgen(method, js_name = italicStart)]
pub fn italic_start(this: &Handler);
#[wasm_bindgen(method, js_name = italicEnd)]
pub fn italic_end(this: &Handler);
#[wasm_bindgen(method, js_name = listItemStart)]
pub fn list_item_start(this: &Handler);
#[wasm_bindgen(method, js_name = listItemEnd)]
pub fn list_item_end(this: &Handler);
#[wasm_bindgen(method, js_name = paragraphStart)]
pub fn paragraph_start(this: &Handler);
#[wasm_bindgen(method, js_name = paragraphEnd)]
pub fn paragraph_end(this: &Handler);
#[wasm_bindgen(method, js_name = quoteBlockStart)]
pub fn quote_block_start(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = quoteBlockEnd)]
pub fn quote_block_end(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = sectionStart)]
pub fn section_start(this: &Handler);
#[wasm_bindgen(method, js_name = sectionEnd)]
pub fn section_end(this: &Handler);
#[wasm_bindgen(method, js_name = strikeStart)]
pub fn strike_start(this: &Handler);
#[wasm_bindgen(method, js_name = strikeEnd)]
pub fn strike_end(this: &Handler);
#[wasm_bindgen(method, js_name = underlineStart)]
pub fn underline_start(this: &Handler);
#[wasm_bindgen(method, js_name = underlineEnd)]
pub fn underline_end(this: &Handler);
#[wasm_bindgen(method, js_name = verseBlockStart)]
pub fn verse_block_start(this: &Handler, item: JsValue);
#[wasm_bindgen(method, js_name = verseBlockEnd)]
pub fn verse_block_end(this: &Handler, item: JsValue);
}
#[wasm_bindgen]
pub fn handle(org: &Org, handler: Handler) {
for event in org.0.iter() {
use Element::*;
match event {
Event::Start(Text { value }) => handler.text(JsValue::from_str(value)),
Event::Start(ExampleBlock(block)) => handler.example_block(to_value(block)),
Event::Start(ExportBlock(block)) => handler.export_block(to_value(block)),
Event::Start(SourceBlock(block)) => handler.source_block(to_value(block)),
Event::Start(InlineSrc(src)) => handler.inline_src(to_value(src)),
Event::Start(Code { value }) => handler.code(JsValue::from_str(value)),
Event::Start(Link(link)) => handler.link(to_value(link)),
Event::Start(Snippet(snippet)) => handler.snippet(to_value(snippet)),
Event::Start(Timestamp(timestamp)) => handler.timestamp(to_value(timestamp)),
Event::Start(Verbatim { value }) => handler.verbatim(JsValue::from_str(value)),
Event::Start(FixedWidth(fixed_width)) => handler.fixedWidth(to_value(fixed_width)),
Event::Start(Rule(_)) => handler.rule(),
Event::Start(Cookie(cookie)) => handler.cookie(to_value(cookie)),
Event::Start(Keyword(keyword)) => handler.keyword(to_value(keyword)),
Event::Start(Table(table)) => handler.table_start(to_value(table)),
Event::End(Table(table)) => handler.table_start(to_value(table)),
Event::Start(TableRow(row)) => handler.table_row_start(to_value(row)),
Event::End(TableRow(row)) => handler.table_row_start(to_value(row)),
Event::Start(TableCell(cell)) => handler.table_cell_start(to_value(cell)),
Event::End(TableCell(cell)) => handler.table_cell_start(to_value(cell)),
Event::Start(Title(title)) => handler.title_start(to_value(title)),
Event::End(Title(title)) => handler.title_end(to_value(title)),
Event::Start(QuoteBlock(block)) => handler.quote_block_start(to_value(block)),
Event::End(QuoteBlock(block)) => handler.quote_block_end(to_value(block)),
Event::Start(CenterBlock(block)) => handler.center_block_start(to_value(block)),
Event::End(CenterBlock(block)) => handler.center_block_end(to_value(block)),
Event::Start(VerseBlock(block)) => handler.verse_block_start(to_value(block)),
Event::End(VerseBlock(block)) => handler.verse_block_end(to_value(block)),
Event::Start(Bold) => handler.bold_start(),
Event::End(Bold) => handler.bold_end(),
Event::Start(Document { .. }) => handler.document_start(),
Event::End(Document { .. }) => handler.document_end(),
Event::Start(List(list)) => handler.list_start(to_value(list)),
Event::End(List(list)) => handler.list_end(to_value(list)),
Event::Start(Italic) => handler.italic_start(),
Event::End(Italic) => handler.italic_end(),
Event::Start(ListItem(_)) => handler.list_item_start(),
Event::End(ListItem(_)) => handler.list_item_end(),
Event::Start(Paragraph { .. }) => handler.paragraph_start(),
Event::End(Paragraph { .. }) => handler.paragraph_end(),
Event::Start(Section) => handler.section_start(),
Event::End(Section) => handler.section_end(),
Event::Start(Strike) => handler.strike_start(),
Event::End(Strike) => handler.strike_end(),
Event::Start(Underline) => handler.underline_start(),
Event::End(Underline) => handler.underline_end(),
_ => continue,
};
}
}
pub fn to_value<T: Serialize + ?Sized>(value: &T) -> JsValue {
value
.serialize(&Serializer::new().serialize_maps_as_objects(true))
.unwrap()
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -5,11 +5,7 @@ macro_rules! test_suite {
($name:ident, $content:expr, $expected:expr) => {
#[test]
fn $name() {
let mut writer = Vec::new();
let org = Org::parse($content);
org.write_html(&mut writer).unwrap();
let string = String::from_utf8(writer).unwrap();
assert_eq!(string, $expected);
assert_eq!(Org::parse($content).to_html(), $expected);
}
};
}

12
wasm/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "orgize-wasm"
version = "0.1.0"
publish = false
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
orgize = { path = ".." }
wasm-bindgen = "0.2"

View file

@ -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

22
wasm/build.rs Normal file
View file

@ -0,0 +1,22 @@
use std::process::Command;
fn main() {
{
let output = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=CARGO_GIT_HASH={}", git_hash);
}
{
let output = Command::new("date").args(["-R"]).output().unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=CARGO_BUILD_TIME={}", git_hash);
}
}

View file

@ -1,71 +1,274 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Orgize wasm demo</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script defer src="https://unpkg.com/alpinejs@3/dist/cdn.min.js"></script>
<title>Orgize</title>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown-light.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.22.0/ace.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.0/beautify-html.js"></script>
<script src="./orgize.umd.js"></script>
<style>
h3 {
body,
html {
height: 100%;
margin: 0;
}
body > * {
margin-bottom: 16px;
.bordered {
overflow: auto;
width: 50%;
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #dbdbdb;
border-radius: 4px;
}
#took {
color: #808080;
font-size: 14px;
padding: 0.5em 1em;
}
#tabsList {
align-items: center;
border-bottom-color: #dbdbdb;
border-bottom-style: solid;
border-bottom-width: 1px;
display: flex;
flex-grow: 0;
flex-shrink: 0;
justify-content: flex-start;
list-style: none;
margin: 0;
padding: 0;
}
#tabsList span {
text-align: center;
color: #4a4a4a;
padding: 0.5em 1em;
user-select: none;
cursor: pointer;
}
#spacer {
flex: 1;
}
#tabsList span.is-active {
color: #485fc7;
}
.markdown-body a[data-range] {
color: #485fc7;
cursor: pointer;
}
</style>
</head>
<body
x-data="{ loaded: false, org: '* Hello, /world/!', type: 'json' }"
x-init="orgize.init('./orgize_bg.wasm').then(() => loaded = true)"
>
<h2>Orgize wasm demo</h2>
<div>
<a href="https://github.com/PoiScript/orgize">GitHub</a>
<a href="https://www.npmjs.com/package/orgize">NPM</a>
<a href="https://crates.io/crates/orgize">crates.io</a>
</div>
<div>Input:</div>
<textarea
x-model="org"
style="width: 100%; height: 100px; margin-bottom: 16px"
></textarea>
<div>Type:</div>
<div>
<button @click="type = 'json'">JSON</button>
<button @click="type = 'html'">HTML</button>
<button @click="type = 'html-rendered'">HTML (Rendered)</button>
</div>
<div>Output:</div>
<noscript>
<p style="color: red">JavaScript is required.</p>
</noscript>
<pre
x-show="type === 'json'"
x-transition
x-text="loaded ? JSON.stringify(orgize.Org.parse(org).toJson(), null, 2) : 'Loading...'"
></pre>
<pre
x-show="type === 'html'"
x-transition
x-text="loaded ? html_beautify(orgize.renderHtml(org), { indent_size: 2 }) : 'Loading...'"
></pre>
<body style="height: 100%; display: flex">
<div
x-show="type === 'html-rendered'"
x-transition
x-html="loaded ? orgize.renderHtml(org) : 'Loading...'"
></div>
style="
margin: 16px;
display: flex;
flex-direction: row;
flex: 1;
min-height: 0;
gap: 16px;
"
>
<div class="bordered">
<div style="height: 100%" id="editor"></div>
</div>
<div class="bordered markdown-body">
<div id="tabsList">
<span class="is-active" data-value="html-rendered">
HTML (rendered)
</span>
<span data-value="html">HTML</span>
<span data-value="syntax">Syntax</span>
<div id="spacer"></div>
<div id="took"></div>
</div>
<div style="flex: 1; overflow: auto">
<div id="result"></div>
</div>
</div>
</div>
</body>
<script type="module">
import init, { Org } from "./dist/orgize.js";
let org;
const result = document.getElementById("result");
const took = document.getElementById("took");
let type = "html-rendered";
const editor = ace.edit("editor");
editor.setTheme("ace/theme/tomorrow");
// render by type
const render = () => {
const startTime = performance.now();
org.update(editor.session.getValue());
switch (type) {
case "html-rendered": {
const html = injectHeadingClass(org.html());
result.innerHTML =
"<div style='padding:1.25rem 1.5rem'>" + html + "</div>";
break;
}
case "html": {
const html = html_beautify(org.html(), { indent_size: 2 });
result.innerHTML = "<pre>" + escapeHtml(html) + "</pre>";
break;
}
case "syntax": {
const syntax = escapeHtml(org.syntax())
.split("\n")
.map((line) =>
line.replace(
/([A-Z0-9_-]*)\@(\d+)\.\.(\d+)/,
(text, name, start, end) =>
`<b>${name}</b><a data-range="${start}-${end}">@${start}..${end}</a>`
)
)
.join("\n");
result.innerHTML = "<pre>" + syntax + "</pre>";
break;
}
}
const finishTime = performance.now();
took.innerText = "took " + (finishTime - startTime).toFixed(2) + "ms";
};
// select token or node range by click
result.addEventListener("click", (ev) => {
if (!ev.target.dataset.range) return;
const [start, end] = ev.target.dataset.range.split("-");
const startPos = editor.session.doc.indexToPosition(parseInt(start));
const endPos = editor.session.doc.indexToPosition(parseInt(end));
const range = new ace.Range(
startPos.row,
startPos.column,
endPos.row,
endPos.column
);
editor.selection.setRange(range);
});
// switch render type
document.getElementById("tabsList").addEventListener("click", (ev) => {
const clicked = event.target.closest("span");
if (!clicked) return;
type = clicked.dataset.value;
document.querySelectorAll("#tabsList span").forEach((el) => {
if (el === clicked) {
el.classList.add("is-active");
} else {
el.classList.remove("is-active");
}
});
render();
});
// utils
const escapeHtml = (html) =>
html
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
const injectHeadingClass = (html) =>
html.replace(/<h(\d)>/g, (_, l) => `<h${l} class="title is-${l}">`);
// init
init().then(() => {
org = new Org("");
editor.session.setValue(`# git hash: ${Org.gitHash}
# build time: ${Org.buildTime}
*Orgize*, a /pure/ =Rust= library for
parsing +Emacs+ _org-mode_ files.
See also:
[[https://github.com/PoiScript/orgize][GitHub]] |
[[https://crates.io/crates/orgize][crates.io]] |
[[https://www.npmjs.com/package/orgize][NPM]]
-----
* Heading 1 *bold*
** Heading 2 =verbatim=
*** Heading 3 ~code~
**** Heading 4 /italic/
***** Heading 5 +strike+
****** Heading 6 _underline_
This's section
#+begin_quote
This is a quote
#+end_quote
#+begin_example
This is an example block
#+end_example
1. First item
2. Second item
3. Third item
* Indented item
* Indented item
4. Fourth item
Table:
|Syntax |Description|
|-----------|-----------|
|Header |Title |
|Paragraph |Text |
Image:
[[https://www.rust-lang.org/static/images/rust-logo-blk.svg]]
`);
editor.session.on("change", () => render());
render();
});
</script>
</html>

Some files were not shown because too many files have changed in this diff Show more