90 Commits

Author SHA1 Message Date
Tom Alexander
e673aa862e Publish version 0.1.9.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-03 00:18:00 -04:00
Tom Alexander
3b6659c5fd Merge branch 'table_properties'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-03 00:14:21 -04:00
Tom Alexander
68a3f8b87e Fix table rule row detection. 2023-10-03 00:13:15 -04:00
Tom Alexander
b1244de1dc Compare row type. 2023-10-03 00:03:58 -04:00
Tom Alexander
e5a402ee1b Compare type and value.
All checks were successful
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
Since we only support org-mode tables, type is always org. Value seems to always be nil, not sure why.
2023-10-02 23:57:17 -04:00
Tom Alexander
d4a2ad4a7f Merge branch 'node_property_properties'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 23:47:11 -04:00
Tom Alexander
3d1b2713ed Compare key and value. 2023-10-02 23:45:31 -04:00
Tom Alexander
60bec4695b Merge branch 'drawer_properties' 2023-10-02 23:38:34 -04:00
Tom Alexander
d992947ff1 Compare name. 2023-10-02 23:34:06 -04:00
Tom Alexander
76fb24d1d1 Merge branch 'comment_properties'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 23:30:29 -04:00
Tom Alexander
b56318fbe4 Add TODO comment. 2023-10-02 23:29:58 -04:00
Tom Alexander
8169499de3 Compare value. 2023-10-02 23:28:32 -04:00
Tom Alexander
29d9e76545 Merge branch 'footnote_definition_properties' 2023-10-02 22:50:26 -04:00
Tom Alexander
4d356b855e Compare label. 2023-10-02 22:48:54 -04:00
Tom Alexander
ae66d1bd89 Fix tracing build.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 22:45:25 -04:00
Tom Alexander
c551938904 Merge branch 'dynamic_block_properties' 2023-10-02 22:43:26 -04:00
Tom Alexander
0fb80e3fee Compare name and parameters. 2023-10-02 22:41:56 -04:00
Tom Alexander
590e7fba0e Merge branch 'greater_block_properties'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has failed
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 22:37:11 -04:00
Tom Alexander
4a72747dc9 Compare name and parameters. 2023-10-02 22:33:00 -04:00
Tom Alexander
2352636672 Split GreaterBlock into CenterBlock, QuoteBlock, and SpecialBlock.
Center and quote blocks do not have parameters nor do they store their name so I am separating them out.
2023-10-02 22:33:00 -04:00
Tom Alexander
36217f5704 Do not capture trailing whitespace in parameters. 2023-10-02 21:14:07 -04:00
Tom Alexander
0654b676f7 Merge branch 'planning_properties'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has failed
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 21:00:08 -04:00
Tom Alexander
a80d171e4d Bubble up planning variables to the headline. 2023-10-02 20:37:46 -04:00
Tom Alexander
2e1a946ac9 Compare scheduled, deadline, and closed. 2023-10-02 20:25:08 -04:00
Tom Alexander
01c2f1bf66 Add a test for a timestamp with a malformed repeater. 2023-10-02 20:04:39 -04:00
Tom Alexander
be483110ef Merge branch 'timestamp_properties'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 20:00:29 -04:00
Tom Alexander
94401dcf00 Allow REST despite no TIME. 2023-10-02 19:51:29 -04:00
Tom Alexander
2b5df83956 Format the code. 2023-10-02 19:24:47 -04:00
Tom Alexander
d53b9e1e1f Fix get_property.
This was returning the error when a token was not an atom whereas we only wanted to check to see if it was the atom nil.
2023-10-02 19:22:35 -04:00
Tom Alexander
5c929ffc13 Fix repeater type.
I had Cumulative and CatchUp backwards.
2023-10-02 19:18:25 -04:00
Tom Alexander
bc3224be7a Revert the rest_end functions. 2023-10-02 19:09:20 -04:00
Tom Alexander
54c66fb4d6 Change get_property to allow absent values.
We're returning an Option<> anyway so might as well handle absent values.
2023-10-02 19:07:12 -04:00
Tom Alexander
6a8ae9d838 Compare warning delay and repeater. 2023-10-02 18:58:30 -04:00
Tom Alexander
512432c5f0 Do not allow time range timestamps with REST on the first TIME. 2023-10-02 17:51:33 -04:00
Tom Alexander
890cd3e4fd Compare start/end time. 2023-10-02 17:17:05 -04:00
Tom Alexander
9846cde2f0 Trim whitespace from raw value. 2023-10-02 16:32:33 -04:00
Tom Alexander
dec3242e72 Implement the Time struct. 2023-10-02 16:24:51 -04:00
Tom Alexander
a8a34e2d9c Compare date start/end. 2023-10-02 16:16:19 -04:00
Tom Alexander
c55fae86f8 Improve lifetimes for get_property_numeric. 2023-10-02 15:51:29 -04:00
Tom Alexander
e7ec23af3d Move the Date struct into types and implement a get_property_numeric. 2023-10-02 15:49:51 -04:00
Tom Alexander
10ae36a419 Implement date types with basic validation. 2023-10-02 15:10:39 -04:00
Tom Alexander
ecdfd7087f Compare raw-value. 2023-10-02 14:45:20 -04:00
Tom Alexander
3ed9b552e2 Compare range type. 2023-10-02 14:35:45 -04:00
Tom Alexander
d04c8c832c Compare timestamp type. 2023-10-02 13:40:37 -04:00
Tom Alexander
9575ef30ac Fix compilation.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 13:19:43 -04:00
Tom Alexander
06ecf41663 Add notes about the fields for timestamps. 2023-10-02 13:19:43 -04:00
Tom Alexander
10d03fd432 Merge branch 'standard_ast_node'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has failed
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-test Build rust-test has succeeded
2023-10-02 13:14:22 -04:00
Tom Alexander
a62c3fc522 Move AstNode into the types crate.
Now that it is used for more than just iteration, it makes sense to promote it to the types crate.
2023-10-02 13:10:45 -04:00
Tom Alexander
25f664e69e Fix warnings. 2023-10-02 13:05:51 -04:00
Tom Alexander
52e0d305aa Remove compare_element and compare_object. 2023-10-02 13:05:29 -04:00
Tom Alexander
418c5c1ce8 Implement the traits for all ast node types. 2023-10-02 12:53:23 -04:00
Tom Alexander
ecd523fa8f Fix lifetimes in the compare functions. 2023-10-02 12:36:09 -04:00
Tom Alexander
c0555dec0b Fix lifetimes for DiffEntry/DiffResult. 2023-10-02 12:28:48 -04:00
Tom Alexander
1b788f3f21 Fix lifetimes on StandardProperties. 2023-10-02 12:11:05 -04:00
Tom Alexander
b3382c66cd Fix lifetimes on ElispFact.
This was listed as a yellow flag on https://quinedot.github.io/rust-learning/pf-shared-nested.html.
2023-10-02 12:11:05 -04:00
Tom Alexander
2a003b85fd Merge branch 'headline_properties'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has failed
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 11:41:04 -04:00
Tom Alexander
270ba53150 Set is_footnote_section during parsing. 2023-10-02 11:20:43 -04:00
Tom Alexander
de5788d8f3 Introduce a struct for the partially-parsed headline.
We are returning so many fields from that parser that managing a tuple is becoming unreadable. The struct should add some structure 😉 to the code.
2023-10-02 11:16:05 -04:00
Tom Alexander
5a254392cb Add more tests. 2023-10-02 10:53:52 -04:00
Tom Alexander
178894680b Compare footnote section. 2023-10-02 10:48:34 -04:00
Tom Alexander
599b3b8f0a Apply category even if there are radio targets.
Some checks failed
rustfmt Build rustfmt has failed
rust-build Build rust-build has failed
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-10-02 10:26:57 -04:00
Tom Alexander
d78ce10a0b Compare raw-value. 2023-10-02 10:26:57 -04:00
Tom Alexander
12ab9beada Merge branch 'document_properties'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-30 16:06:05 -04:00
Tom Alexander
186201a4b5 Remove category from global settings.
Some checks failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
This setting does not impact parsing so we can iterate over the final document to find the keywords.
2023-09-30 14:35:22 -04:00
Tom Alexander
d38b0a84f6 Fix handling file names with periods before the file extension.
Some checks failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has failed
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-30 01:26:24 -04:00
Tom Alexander
6ed35f4674 Minor cleanup. 2023-09-30 00:16:19 -04:00
Tom Alexander
846a8b3729 Support reading category from in-buffer-settings. 2023-09-30 00:14:26 -04:00
Tom Alexander
896250836b Add support for parsing quoted strings containing escaped octals. 2023-09-29 23:59:33 -04:00
Tom Alexander
6c77586960 Improve error message. 2023-09-29 23:59:32 -04:00
Tom Alexander
fc7d4bd949 Set Document path and category based on file path. 2023-09-29 23:59:32 -04:00
Tom Alexander
f1e35e317b Compare document path. 2023-09-29 21:20:23 -04:00
Tom Alexander
3fb2b5d31c Undo the getters change.
The getters were a good idea, but if we are going to support editing later, we will need to expose the fields or write A LOT of boiler-plate. The getters also would prevent people from moving values out of the AST without even more boiler-plate. It is simply not worth it at this stage, so we will need to tolerate frequently changing semver versions as the public interface changes since *every* field in the AST is public.
2023-09-29 21:14:55 -04:00
Tom Alexander
d1dac0b8de Compare document category. 2023-09-29 20:57:09 -04:00
Tom Alexander
93f1bcd744 Add getters for Document. 2023-09-29 20:57:09 -04:00
Tom Alexander
47674a6907 Merge branch 'initial_getters'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-29 20:40:45 -04:00
Tom Alexander
5d1582be4d Remove multi_field_getter_iter.
This was written because I originally intended to make the fields of the ast node types entirely private, but that made constructing them tedious so they are pub(crate) which coincidentally also allows them to be used by the iterator.
2023-09-29 20:40:31 -04:00
Tom Alexander
dae10c2eef Initial work for exposing getters and hiding the fields of the ast nodes.
Ultimately this is about semver and exposing a stable interface while allowing the internal representation to change. The fields are still pub(crate) to make constructing the types easier inside this crate, which should be fine because we can refactor the code inside this crate whenever the internal structure changes.
2023-09-29 20:40:31 -04:00
Tom Alexander
5e127fec11 Merge branch 'plain_list_item_properties'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-29 19:30:32 -04:00
Tom Alexander
064a4eeee7 Compare plain list item pre blank. 2023-09-29 19:30:02 -04:00
Tom Alexander
7727b5ef47 Compare plain list item counter. 2023-09-29 18:45:38 -04:00
Tom Alexander
967e74c147 Compare plain list item bullets. 2023-09-29 17:28:50 -04:00
Tom Alexander
13697df7ea Merge branch 'test_combinations'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-29 16:39:02 -04:00
Tom Alexander
07e11e359a Add tests for odd headline levels. 2023-09-29 16:37:22 -04:00
Tom Alexander
0c363c8dd6 Add tests for tab width. 2023-09-29 16:03:55 -04:00
Tom Alexander
9a479b33e0 Make the same changes we did for stdin compare to comparing files. 2023-09-29 15:42:07 -04:00
Tom Alexander
7a854838ef Clean up code duplication. 2023-09-29 15:35:57 -04:00
Tom Alexander
2012e5a6d5 Test org_mode_samples both with and without alphabetical lists enabled. 2023-09-29 15:30:38 -04:00
Tom Alexander
f1261ddce8 Remove "org_" prefix from list_allow_alphabetical.
These settings are exclusively for parsing org-mode so the prefix is redundant.
2023-09-29 14:33:52 -04:00
Tom Alexander
3a422e6435 Counter set always allows alphabetic values regardless of org-list-allow-alphabetical. 2023-09-29 14:32:41 -04:00
Tom Alexander
6670f8c768 Add tests for alphabetic counter sets. 2023-09-29 14:26:39 -04:00
60 changed files with 2902 additions and 971 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "organic"
version = "0.1.8"
version = "0.1.9"
authors = ["Tom Alexander <tom@fizz.buzz>"]
description = "An org-mode parser."
edition = "2021"

View File

@@ -19,8 +19,6 @@ fn main() {
// Re-generate the tests if any org-mode files change
println!("cargo:rerun-if-changed=org_mode_samples");
write_header(&mut test_file);
let test_files = WalkDir::new("org_mode_samples")
.into_iter()
.filter(|e| match e {
@@ -54,28 +52,15 @@ fn write_test(test_file: &mut File, test: &walkdir::DirEntry) {
.strip_suffix(".org")
.expect("Should have .org extension")
.replace("/", "_");
let test_name = format!("autogen_{}", test_name);
if let Some(_reason) = is_expect_fail(test_name.as_str()) {
write!(test_file, "#[ignore]\n").unwrap();
}
write!(
test_file,
include_str!("./tests/test_template"),
name = test_name,
path = test.path().display()
)
.unwrap();
}
#[cfg(feature = "compare")]
fn write_header(test_file: &mut File) {
write!(
test_file,
r#"
#[feature(exit_status_error)]
"#
path = test.path().display(),
expect_fail = is_expect_fail(test_name.as_str())
.map(|_| "#[ignore]\n")
.unwrap_or("")
)
.unwrap();
}
@@ -83,8 +68,8 @@ fn write_header(test_file: &mut File) {
#[cfg(feature = "compare")]
fn is_expect_fail(name: &str) -> Option<&str> {
match name {
"autogen_greater_element_drawer_drawer_with_headline_inside" => Some("Apparently lines with :end: become their own paragraph. This odd behavior needs to be investigated more."),
"autogen_element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."),
"greater_element_drawer_drawer_with_headline_inside" => Some("Apparently lines with :end: become their own paragraph. This odd behavior needs to be investigated more."),
"element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."),
_ => None,
}
}

View File

@@ -88,7 +88,7 @@ ARG DOOMEMACS_PATH=/foreign_documents/doomemacs
ARG DOOMEMACS_REPO=https://github.com/doomemacs/doomemacs.git
RUN mkdir -p $DOOMEMACS_PATH && git -C $DOOMEMACS_PATH init --initial-branch=main && git -C $DOOMEMACS_PATH remote add origin $DOOMEMACS_REPO && git -C $DOOMEMACS_PATH fetch origin $DOOMEMACS_VERSION && git -C $DOOMEMACS_PATH checkout FETCH_HEAD
ARG WORG_VERSION=0c8d5679b536af450b61812246a3e02b8103f4b8
ARG WORG_VERSION=ba6cda890f200d428a5d68e819eef15b5306055f
ARG WORG_PATH=/foreign_documents/worg
ARG WORG_REPO=https://git.sr.ht/~bzg/worg
RUN mkdir -p $WORG_PATH && git -C $WORG_PATH init --initial-branch=main && git -C $WORG_PATH remote add origin $WORG_REPO && git -C $WORG_PATH fetch origin $WORG_VERSION && git -C $WORG_PATH checkout FETCH_HEAD

7
notes/test_names.org Normal file
View File

@@ -0,0 +1,7 @@
* Autogen tests
The autogen tests are the tests automatically generated to compare the output of Organic vs the upstream Emacs Org-mode parser using the sample documents in the =org_mode_samples= folder. They will have a prefix based on the settings for each test.
- default :: The test is run with the default settings (The upstream Emacs Org-mode determines the default settings)
- la :: Short for "list alphabetic". Enables alphabetic plain lists.
- t# :: Sets the tab-width to # (as in t4 sets the tab-width to 4).
- odd :: Sets the org-odd-levels-only setting to true (meaning "odd" as opposed to "oddeven").

View File

@@ -0,0 +1 @@
#+CATEGORY: theory

View File

@@ -0,0 +1,5 @@
#+CATEGORY: foo
#+CATEGORY: bar
#+begin_src text
#+CATEGORY: baz
#+end_src

View File

@@ -0,0 +1,3 @@
#+begin_defun foo bar baz
lorem
#+end_defun

View File

@@ -0,0 +1,22 @@
# An ordered list starting at 3
1. [@3] foo
# An ordered list starting at 11
1. [@D] bar
# An ordered list starting at 1 with the contents of "[@kk] baz"
1. [@kk] baz
# A paragraph when org-list-allow-alphabetical is nil
m. lorem
# A paragraph when org-list-allow-alphabetical is nil
m. [@k] ipsum
# An unordered list with :counter set to 3
- [@3] dolar

View File

@@ -0,0 +1,30 @@
# Alphabetic lists larger than 26 elements should become numbered. From M-x describe-variable org-list-allow-alphabetical:
#
# > Lists with more than 26 items will fallback to standard numbering.
a. 1
a. 2
a. 3
a. 4
a. 5
a. 6
a. 7
a. 8
a. 9
a. 10
a. 11
a. 12
a. 13
a. 14
a. 15
a. 16
a. 17
a. 18
a. 19
a. 20
a. 21
a. 22
a. 23
a. 24
a. 25
a. 26
a. 27

View File

@@ -0,0 +1,5 @@
# "lorem" is prefixed by a tab instead of spaces, so the editor's tab-width value determines whether lorem is a sibling of baz (tab-width 8), a sibling of bar (tab-width < 8), or a child of baz (tab-width > 8).
1. foo
1. bar
1. baz
1. lorem

View File

@@ -1,4 +1,5 @@
# Comment
#
# indented line
# At the top of the file

View File

@@ -0,0 +1,7 @@
# All the marks for repeater and warning delay
[1970-01-01 Thu 8:15-13:15foo +1h -2h]
[1970-01-01 Thu 8:15-13:15foo ++1d -2d]
[1970-01-01 Thu 8:15-13:15foo .+1w -2w]
[1970-01-01 Thu 8:15-13:15foo +1m --2m]
[1970-01-01 Thu 8:15-13:15foo ++1y --2y]
[1970-01-01 Thu 8:15-13:15foo .+1d --2h]

View File

@@ -2,13 +2,17 @@
<%%(foo bar baz)>
# active
<1970-01-01 Thu 8:15rest +1w -1d>
# Any value for "REST" in the first timestamp makes this a regular timestamp rather than a time range.
<1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d>
# inactive
[1970-01-01 Thu 8:15rest +1w -1d]
# Any value for "REST" in the first timestamp makes this a regular timestamp rather than a time range.
[1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d]
# active date range
<1970-01-01 Thu 8:15rest +1w -1d>--<1970-01-01 Thu 8:15rest +1w -1d>
# active time range
<1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d>
<1970-01-01 Thu 8:15-13:15otherrest +1w -1d>
# inactive date range
[1970-01-01 Thu 8:15rest +1w -1d]--[1970-01-01 Thu 8:15rest +1w -1d]
# inactive time range
[1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d]
[1970-01-01 Thu 8:15-13:15otherrest +1w -1d]

View File

@@ -0,0 +1,2 @@
# This should be a malformed timestamp according to the current org-mode documentation but it is accepted anyway (with no repeater).
<1970-01-01 Thu ++y>

View File

@@ -0,0 +1,2 @@
<<<Footnotes>>> and stuff
* Footnotes

View File

@@ -0,0 +1,3 @@
* FOOTNOTES
* Footnotes
* footnotes

View File

@@ -0,0 +1,2 @@
* Footnotes
* Footnotes

View File

@@ -0,0 +1,3 @@
* Foo
* Footnotes :foo:bar:
* Footnotes and stuff

View File

@@ -6,21 +6,31 @@ use crate::compare::parse::emacs_parse_file_org_document;
use crate::compare::parse::get_emacs_version;
use crate::compare::parse::get_org_mode_version;
use crate::compare::sexp::sexp;
use crate::parser::parse;
use crate::context::GlobalSettings;
use crate::context::LocalFileAccessInterface;
use crate::parser::parse_file_with_settings;
use crate::parser::parse_with_settings;
use crate::GlobalSettings;
use crate::LocalFileAccessInterface;
pub fn run_anonymous_compare<P: AsRef<str>>(
org_contents: P,
) -> Result<(), Box<dyn std::error::Error>> {
run_anonymous_compare_with_settings(org_contents, &GlobalSettings::default())
}
pub fn run_compare_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::error::Error>> {
run_compare_on_file_with_settings(org_path, &GlobalSettings::default())
}
pub fn run_anonymous_compare_with_settings<P: AsRef<str>>(
org_contents: P,
global_settings: &GlobalSettings,
) -> Result<(), Box<dyn std::error::Error>> {
// TODO: This is a work-around to pretend that dos line endings do not exist. It would be better to handle the difference in line endings.
let org_contents = org_contents.as_ref().replace("\r\n", "\n");
let org_contents = org_contents.as_str();
eprintln!("Using emacs version: {}", get_emacs_version()?.trim());
eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim());
let rust_parsed = parse(org_contents)?;
let org_sexp = emacs_parse_anonymous_org_document(org_contents)?;
print_versions()?;
let rust_parsed = parse_with_settings(org_contents, global_settings)?;
let org_sexp = emacs_parse_anonymous_org_document(org_contents, global_settings)?;
let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?;
println!("{}\n\n\n", org_contents);
@@ -38,10 +48,12 @@ pub fn run_anonymous_compare<P: AsRef<str>>(
Ok(())
}
pub fn run_compare_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::error::Error>> {
pub fn run_compare_on_file_with_settings<P: AsRef<Path>>(
org_path: P,
global_settings: &GlobalSettings,
) -> Result<(), Box<dyn std::error::Error>> {
let org_path = org_path.as_ref();
eprintln!("Using emacs version: {}", get_emacs_version()?.trim());
eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim());
print_versions()?;
let parent_directory = org_path
.parent()
.ok_or("Should be contained inside a directory.")?;
@@ -53,12 +65,12 @@ pub fn run_compare_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn st
working_directory: Some(parent_directory.to_path_buf()),
};
let global_settings = {
let mut global_settings = GlobalSettings::default();
let mut global_settings = global_settings.clone();
global_settings.file_access = &file_access_interface;
global_settings
};
let rust_parsed = parse_with_settings(org_contents, &global_settings)?;
let org_sexp = emacs_parse_file_org_document(org_path)?;
let rust_parsed = parse_file_with_settings(org_contents, &global_settings, Some(org_path))?;
let org_sexp = emacs_parse_file_org_document(org_path, &global_settings)?;
let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?;
println!("{}\n\n\n", org_contents);
@@ -75,3 +87,9 @@ pub fn run_compare_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn st
Ok(())
}
fn print_versions() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Using emacs version: {}", get_emacs_version()?.trim());
eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim());
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
use std::borrow::Cow;
use crate::types::AngleLink;
use crate::types::AstNode;
use crate::types::BabelCall;
use crate::types::Bold;
use crate::types::CenterBlock;
use crate::types::Citation;
use crate::types::CitationReference;
use crate::types::Clock;
@@ -21,7 +23,6 @@ use crate::types::ExportSnippet;
use crate::types::FixedWidthArea;
use crate::types::FootnoteDefinition;
use crate::types::FootnoteReference;
use crate::types::GreaterBlock;
use crate::types::Heading;
use crate::types::HorizontalRule;
use crate::types::InlineBabelCall;
@@ -41,10 +42,12 @@ use crate::types::PlainListItem;
use crate::types::PlainText;
use crate::types::Planning;
use crate::types::PropertyDrawer;
use crate::types::QuoteBlock;
use crate::types::RadioLink;
use crate::types::RadioTarget;
use crate::types::RegularLink;
use crate::types::Section;
use crate::types::SpecialBlock;
use crate::types::SrcBlock;
use crate::types::StatisticsCookie;
use crate::types::StrikeThrough;
@@ -60,25 +63,92 @@ use crate::types::Verbatim;
use crate::types::VerseBlock;
pub(crate) trait ElispFact<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str>;
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str>;
}
pub(crate) trait GetElispFact<'s> {
fn get_elisp_fact(&'s self) -> &'s dyn ElispFact<'s>;
fn get_elisp_fact<'b>(&'b self) -> &'b dyn ElispFact<'s>;
}
impl<'s, I: ElispFact<'s>> GetElispFact<'s> for I {
fn get_elisp_fact(&'s self) -> &'s dyn ElispFact<'s> {
fn get_elisp_fact<'b>(&'b self) -> &'b dyn ElispFact<'s> {
self
}
}
impl<'r, 's> GetElispFact<'s> for AstNode<'r, 's> {
fn get_elisp_fact<'b>(&'b self) -> &'b dyn ElispFact<'s> {
match self {
AstNode::Document(inner) => *inner,
AstNode::Heading(inner) => *inner,
AstNode::Section(inner) => *inner,
AstNode::Paragraph(inner) => *inner,
AstNode::PlainList(inner) => *inner,
AstNode::PlainListItem(inner) => *inner,
AstNode::CenterBlock(inner) => *inner,
AstNode::QuoteBlock(inner) => *inner,
AstNode::SpecialBlock(inner) => *inner,
AstNode::DynamicBlock(inner) => *inner,
AstNode::FootnoteDefinition(inner) => *inner,
AstNode::Comment(inner) => *inner,
AstNode::Drawer(inner) => *inner,
AstNode::PropertyDrawer(inner) => *inner,
AstNode::NodeProperty(inner) => *inner,
AstNode::Table(inner) => *inner,
AstNode::TableRow(inner) => *inner,
AstNode::VerseBlock(inner) => *inner,
AstNode::CommentBlock(inner) => *inner,
AstNode::ExampleBlock(inner) => *inner,
AstNode::ExportBlock(inner) => *inner,
AstNode::SrcBlock(inner) => *inner,
AstNode::Clock(inner) => *inner,
AstNode::DiarySexp(inner) => *inner,
AstNode::Planning(inner) => *inner,
AstNode::FixedWidthArea(inner) => *inner,
AstNode::HorizontalRule(inner) => *inner,
AstNode::Keyword(inner) => *inner,
AstNode::BabelCall(inner) => *inner,
AstNode::LatexEnvironment(inner) => *inner,
AstNode::Bold(inner) => *inner,
AstNode::Italic(inner) => *inner,
AstNode::Underline(inner) => *inner,
AstNode::StrikeThrough(inner) => *inner,
AstNode::Code(inner) => *inner,
AstNode::Verbatim(inner) => *inner,
AstNode::PlainText(inner) => *inner,
AstNode::RegularLink(inner) => *inner,
AstNode::RadioLink(inner) => *inner,
AstNode::RadioTarget(inner) => *inner,
AstNode::PlainLink(inner) => *inner,
AstNode::AngleLink(inner) => *inner,
AstNode::OrgMacro(inner) => *inner,
AstNode::Entity(inner) => *inner,
AstNode::LatexFragment(inner) => *inner,
AstNode::ExportSnippet(inner) => *inner,
AstNode::FootnoteReference(inner) => *inner,
AstNode::Citation(inner) => *inner,
AstNode::CitationReference(inner) => *inner,
AstNode::InlineBabelCall(inner) => *inner,
AstNode::InlineSourceBlock(inner) => *inner,
AstNode::LineBreak(inner) => *inner,
AstNode::Target(inner) => *inner,
AstNode::StatisticsCookie(inner) => *inner,
AstNode::Subscript(inner) => *inner,
AstNode::Superscript(inner) => *inner,
AstNode::TableCell(inner) => *inner,
AstNode::Timestamp(inner) => *inner,
}
}
}
impl<'s> GetElispFact<'s> for Element<'s> {
fn get_elisp_fact(&'s self) -> &'s dyn ElispFact<'s> {
fn get_elisp_fact<'b>(&'b self) -> &'b dyn ElispFact<'s> {
match self {
Element::Paragraph(inner) => inner,
Element::PlainList(inner) => inner,
Element::GreaterBlock(inner) => inner,
Element::CenterBlock(inner) => inner,
Element::QuoteBlock(inner) => inner,
Element::SpecialBlock(inner) => inner,
Element::DynamicBlock(inner) => inner,
Element::FootnoteDefinition(inner) => inner,
Element::Comment(inner) => inner,
@@ -103,7 +173,7 @@ impl<'s> GetElispFact<'s> for Element<'s> {
}
impl<'s> GetElispFact<'s> for Object<'s> {
fn get_elisp_fact(&'s self) -> &'s dyn ElispFact<'s> {
fn get_elisp_fact<'b>(&'b self) -> &'b dyn ElispFact<'s> {
match self {
Object::Bold(inner) => inner,
Object::Italic(inner) => inner,
@@ -137,337 +207,345 @@ impl<'s> GetElispFact<'s> for Object<'s> {
}
impl<'s> ElispFact<'s> for Document<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"org-data".into()
}
}
impl<'s> ElispFact<'s> for Section<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"section".into()
}
}
impl<'s> ElispFact<'s> for Heading<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"headline".into()
}
}
impl<'s> ElispFact<'s> for PlainList<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"plain-list".into()
}
}
impl<'s> ElispFact<'s> for PlainListItem<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"item".into()
}
}
impl<'s> ElispFact<'s> for GreaterBlock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
match self.name.to_lowercase().as_str() {
"center" => "center-block".into(),
"quote" => "quote-block".into(),
_ => "special-block".into(),
}
impl<'s> ElispFact<'s> for CenterBlock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"center-block".into()
}
}
impl<'s> ElispFact<'s> for QuoteBlock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"quote-block".into()
}
}
impl<'s> ElispFact<'s> for SpecialBlock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"special-block".into()
}
}
impl<'s> ElispFact<'s> for DynamicBlock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"dynamic-block".into()
}
}
impl<'s> ElispFact<'s> for FootnoteDefinition<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"footnote-definition".into()
}
}
impl<'s> ElispFact<'s> for Drawer<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"drawer".into()
}
}
impl<'s> ElispFact<'s> for PropertyDrawer<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"property-drawer".into()
}
}
impl<'s> ElispFact<'s> for NodeProperty<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"node-property".into()
}
}
impl<'s> ElispFact<'s> for Table<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"table".into()
}
}
impl<'s> ElispFact<'s> for TableRow<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"table-row".into()
}
}
impl<'s> ElispFact<'s> for Paragraph<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"paragraph".into()
}
}
impl<'s> ElispFact<'s> for TableCell<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"table-cell".into()
}
}
impl<'s> ElispFact<'s> for Comment<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"comment".into()
}
}
impl<'s> ElispFact<'s> for VerseBlock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"verse-block".into()
}
}
impl<'s> ElispFact<'s> for CommentBlock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"comment-block".into()
}
}
impl<'s> ElispFact<'s> for ExampleBlock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"example-block".into()
}
}
impl<'s> ElispFact<'s> for ExportBlock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"export-block".into()
}
}
impl<'s> ElispFact<'s> for SrcBlock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"src-block".into()
}
}
impl<'s> ElispFact<'s> for Clock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"clock".into()
}
}
impl<'s> ElispFact<'s> for DiarySexp<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"diary-sexp".into()
}
}
impl<'s> ElispFact<'s> for Planning<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"planning".into()
}
}
impl<'s> ElispFact<'s> for FixedWidthArea<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"fixed-width".into()
}
}
impl<'s> ElispFact<'s> for HorizontalRule<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"horizontal-rule".into()
}
}
impl<'s> ElispFact<'s> for Keyword<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"keyword".into()
}
}
impl<'s> ElispFact<'s> for BabelCall<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"babel-call".into()
}
}
impl<'s> ElispFact<'s> for LatexEnvironment<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"latex-environment".into()
}
}
impl<'s> ElispFact<'s> for Bold<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"bold".into()
}
}
impl<'s> ElispFact<'s> for Italic<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"italic".into()
}
}
impl<'s> ElispFact<'s> for Underline<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"underline".into()
}
}
impl<'s> ElispFact<'s> for StrikeThrough<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"strike-through".into()
}
}
impl<'s> ElispFact<'s> for Code<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"code".into()
}
}
impl<'s> ElispFact<'s> for Verbatim<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"verbatim".into()
}
}
impl<'s> ElispFact<'s> for RegularLink<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"link".into()
}
}
impl<'s> ElispFact<'s> for RadioLink<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"link".into()
}
}
impl<'s> ElispFact<'s> for RadioTarget<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"radio-target".into()
}
}
impl<'s> ElispFact<'s> for PlainLink<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"link".into()
}
}
impl<'s> ElispFact<'s> for AngleLink<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"link".into()
}
}
impl<'s> ElispFact<'s> for OrgMacro<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"macro".into()
}
}
impl<'s> ElispFact<'s> for Entity<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"entity".into()
}
}
impl<'s> ElispFact<'s> for LatexFragment<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"latex-fragment".into()
}
}
impl<'s> ElispFact<'s> for ExportSnippet<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"export-snippet".into()
}
}
impl<'s> ElispFact<'s> for FootnoteReference<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"footnote-reference".into()
}
}
impl<'s> ElispFact<'s> for Citation<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"citation".into()
}
}
impl<'s> ElispFact<'s> for CitationReference<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"citation-reference".into()
}
}
impl<'s> ElispFact<'s> for InlineBabelCall<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"inline-babel-call".into()
}
}
impl<'s> ElispFact<'s> for InlineSourceBlock<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"inline-src-block".into()
}
}
impl<'s> ElispFact<'s> for LineBreak<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"line-break".into()
}
}
impl<'s> ElispFact<'s> for Target<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"target".into()
}
}
impl<'s> ElispFact<'s> for StatisticsCookie<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"statistics-cookie".into()
}
}
impl<'s> ElispFact<'s> for Subscript<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"subscript".into()
}
}
impl<'s> ElispFact<'s> for Superscript<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"superscript".into()
}
}
impl<'s> ElispFact<'s> for Timestamp<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"timestamp".into()
}
}
impl<'s> ElispFact<'s> for PlainText<'s> {
fn get_elisp_name(&'s self) -> Cow<'s, str> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
// plain text from upstream emacs does not actually have a name but this is included here to make rendering the status diff easier.
"plain-text".into()
}

View File

@@ -5,4 +5,6 @@ mod parse;
mod sexp;
mod util;
pub use compare::run_anonymous_compare;
pub use compare::run_anonymous_compare_with_settings;
pub use compare::run_compare_on_file;
pub use compare::run_compare_on_file_with_settings;

View File

@@ -1,8 +1,33 @@
use std::path::Path;
use std::process::Command;
pub fn emacs_parse_anonymous_org_document<C>(
use crate::context::HeadlineLevelFilter;
use crate::settings::GlobalSettings;
/// Generate elisp to configure org-mode parsing settings
///
/// Currently only org-list-allow-alphabetical is supported.
fn global_settings_elisp(global_settings: &GlobalSettings) -> String {
// This string concatenation is wildly inefficient but its only called in tests 🤷.
let mut ret = "".to_owned();
if global_settings.list_allow_alphabetical {
ret += "(setq org-list-allow-alphabetical t)\n"
}
if global_settings.tab_width != crate::settings::DEFAULT_TAB_WIDTH {
ret += format!("(setq-default tab-width {})", global_settings.tab_width).as_str();
}
if global_settings.odd_levels_only != HeadlineLevelFilter::default() {
ret += match global_settings.odd_levels_only {
HeadlineLevelFilter::Odd => "(setq org-odd-levels-only t)\n",
HeadlineLevelFilter::OddEven => "(setq org-odd-levels-only nil)\n",
};
}
ret
}
pub(crate) fn emacs_parse_anonymous_org_document<C>(
file_contents: C,
global_settings: &GlobalSettings,
) -> Result<String, Box<dyn std::error::Error>>
where
C: AsRef<str>,
@@ -14,10 +39,12 @@ where
(require 'org)
(defun org-table-align () t)
(insert "{escaped_file_contents}")
{global_settings}
(org-mode)
(message "%s" (pp-to-string (org-element-parse-buffer)))
)"#,
escaped_file_contents = escaped_file_contents
escaped_file_contents = escaped_file_contents,
global_settings = global_settings_elisp(global_settings)
);
let mut cmd = Command::new("emacs");
let cmd = cmd
@@ -33,7 +60,10 @@ where
Ok(String::from_utf8(org_sexp)?)
}
pub fn emacs_parse_file_org_document<P>(file_path: P) -> Result<String, Box<dyn std::error::Error>>
pub(crate) fn emacs_parse_file_org_document<P>(
file_path: P,
global_settings: &GlobalSettings,
) -> Result<String, Box<dyn std::error::Error>>
where
P: AsRef<Path>,
{
@@ -46,9 +76,17 @@ where
r#"(progn
(require 'org)
(defun org-table-align () t)
(setq vc-handled-backends nil)
{global_settings}
(find-file-read-only "{file_path}")
(org-mode)
(message "%s" (pp-to-string (org-element-parse-buffer)))
)"#
)"#,
global_settings = global_settings_elisp(global_settings),
file_path = file_path
.as_os_str()
.to_str()
.expect("File name should be valid utf-8.")
);
let mut cmd = Command::new("emacs");
let cmd = cmd
@@ -57,8 +95,6 @@ where
.arg("--no-site-file")
.arg("--no-splash")
.arg("--batch")
.arg("--insert")
.arg(file_path.as_os_str())
.arg("--eval")
.arg(elisp_script);
let out = cmd.output()?;

View File

@@ -1,9 +1,10 @@
use std::collections::HashMap;
use nom::branch::alt;
use nom::bytes::complete::escaped;
use nom::bytes::complete::tag;
use nom::bytes::complete::take_till1;
use nom::character::complete::anychar;
use nom::character::complete::digit1;
use nom::character::complete::multispace0;
use nom::character::complete::multispace1;
use nom::character::complete::one_of;
@@ -11,6 +12,7 @@ use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::multi::separated_list1;
use nom::sequence::delimited;
use nom::sequence::preceded;
@@ -18,6 +20,8 @@ use nom::sequence::tuple;
use crate::error::Res;
const MAX_OCTAL_LENGTH: usize = 3;
#[derive(Debug)]
pub enum Token<'s> {
Atom(&'s str),
@@ -35,6 +39,7 @@ pub struct TextWithProperties<'s> {
enum ParseState {
Normal,
Escape,
Octal(Vec<u8>),
}
impl<'s> Token<'s> {
@@ -116,7 +121,7 @@ fn get_consumed<'s>(input: &'s str, remaining: &'s str) -> &'s str {
}
pub(crate) fn unquote(text: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut out = String::with_capacity(text.len());
let mut out: Vec<u8> = Vec::with_capacity(text.len());
if !text.starts_with(r#"""#) {
return Err("Quoted text does not start with quote.".into());
}
@@ -125,30 +130,53 @@ pub(crate) fn unquote(text: &str) -> Result<String, Box<dyn std::error::Error>>
}
let interior_text = &text[1..(text.len() - 1)];
let mut state = ParseState::Normal;
for current_char in interior_text.chars().into_iter() {
for current_char in interior_text.bytes().into_iter() {
// Check to see if octal finished
state = match (state, current_char) {
(ParseState::Normal, '\\') => ParseState::Escape,
(ParseState::Octal(octal), b'0'..=b'7') if octal.len() < MAX_OCTAL_LENGTH => {
ParseState::Octal(octal)
}
(ParseState::Octal(octal), _) => {
let octal_number_string = String::from_utf8(octal)?;
let decoded_byte = u8::from_str_radix(&octal_number_string, 8)?;
out.push(decoded_byte);
ParseState::Normal
}
(state, _) => state,
};
state = match (state, current_char) {
(ParseState::Normal, b'\\') => ParseState::Escape,
(ParseState::Normal, _) => {
out.push(current_char);
ParseState::Normal
}
(ParseState::Escape, 'n') => {
out.push('\n');
(ParseState::Escape, b'n') => {
out.push(b'\n');
ParseState::Normal
}
(ParseState::Escape, '\\') => {
out.push('\\');
(ParseState::Escape, b'\\') => {
out.push(b'\\');
ParseState::Normal
}
(ParseState::Escape, '"') => {
out.push('"');
(ParseState::Escape, b'"') => {
out.push(b'"');
ParseState::Normal
}
_ => todo!(),
(ParseState::Escape, b'0'..=b'7') => {
let mut octal = Vec::with_capacity(MAX_OCTAL_LENGTH);
octal.push(current_char);
ParseState::Octal(octal)
}
(ParseState::Octal(mut octal), b'0'..=b'7') => {
octal.push(current_char);
ParseState::Octal(octal)
}
_ => panic!("Invalid state unquoting string."),
};
}
Ok(out)
Ok(String::from_utf8(out)?)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -210,15 +238,30 @@ fn unquoted_atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn quoted_atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (remaining, _) = tag(r#"""#)(input)?;
let (remaining, _) = escaped(
take_till1(|c| match c {
'\\' | '"' => true,
_ => false,
}),
'\\',
one_of(r#""n\\"#),
)(remaining)?;
let (mut remaining, _) = tag(r#"""#)(input)?;
let mut in_escape = false;
loop {
if in_escape {
let (remain, _) = alt((recognize(one_of(r#""n\\"#)), digit1))(remaining)?;
remaining = remain;
in_escape = false;
} else {
let end_quote = tag::<_, _, nom::error::Error<_>>(r#"""#)(remaining);
if end_quote.is_ok() {
break;
}
let escape_backslash = tag::<_, _, nom::error::Error<_>>("\\")(remaining);
if let Ok((remain, _)) = escape_backslash {
remaining = remain;
in_escape = true;
continue;
}
let (remain, _) = anychar(remaining)?;
remaining = remain;
}
}
let (remaining, _) = tag(r#"""#)(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, Token::Atom(source.into())))

View File

@@ -1,5 +1,8 @@
use std::str::FromStr;
use super::elisp_fact::GetElispFact;
use super::sexp::Token;
use crate::compare::sexp::unquote;
use crate::types::GetStandardProperties;
use crate::types::StandardProperties;
@@ -15,9 +18,9 @@ fn is_slice_of(parent: &str, child: &str) -> bool {
/// Get the byte offset into source that the rust object exists at.
///
/// These offsets are zero-based unlike the elisp ones.
fn get_rust_byte_offsets<'s, S: StandardProperties<'s> + ?Sized>(
fn get_rust_byte_offsets<'b, 's, S: StandardProperties<'s> + ?Sized>(
original_document: &'s str,
rust_ast_node: &'s S,
rust_ast_node: &'b S,
) -> (usize, usize) {
let rust_object_source = rust_ast_node.get_source();
debug_assert!(is_slice_of(original_document, rust_object_source));
@@ -27,20 +30,21 @@ fn get_rust_byte_offsets<'s, S: StandardProperties<'s> + ?Sized>(
}
pub(crate) fn compare_standard_properties<
'b,
's,
S: GetStandardProperties<'s> + GetElispFact<'s> + ?Sized,
>(
original_document: &'s str,
emacs: &'s Token<'s>,
rust: &'s S,
emacs: &'b Token<'s>,
rust: &'b S,
) -> Result<(), Box<dyn std::error::Error>> {
assert_name(emacs, rust.get_elisp_fact().get_elisp_name())?;
assert_bounds(original_document, emacs, rust.get_standard_properties())?;
Ok(())
}
pub(crate) fn assert_name<'s, S: AsRef<str>>(
emacs: &'s Token<'s>,
pub(crate) fn assert_name<'b, 's, S: AsRef<str>>(
emacs: &'b Token<'s>,
name: S,
) -> Result<(), Box<dyn std::error::Error>> {
let name = name.as_ref();
@@ -62,10 +66,10 @@ pub(crate) fn assert_name<'s, S: AsRef<str>>(
/// Assert that the character ranges defined by upstream org-mode's :standard-properties match the slices in Organic's StandardProperties.
///
/// This does **not** handle plain text because plain text is a special case.
pub(crate) fn assert_bounds<'s, S: StandardProperties<'s> + ?Sized>(
pub(crate) fn assert_bounds<'b, 's, S: StandardProperties<'s> + ?Sized>(
original_document: &'s str,
emacs: &'s Token<'s>,
rust: &'s S,
emacs: &'b Token<'s>,
rust: &'b S,
) -> Result<(), Box<dyn std::error::Error>> {
let standard_properties = get_emacs_standard_properties(emacs)?; // 1-based
let (begin, end) = (
@@ -98,8 +102,8 @@ struct EmacsStandardProperties {
post_blank: Option<usize>,
}
fn get_emacs_standard_properties<'s>(
emacs: &'s Token<'s>,
fn get_emacs_standard_properties<'b, 's>(
emacs: &'b Token<'s>,
) -> Result<EmacsStandardProperties, Box<dyn std::error::Error>> {
let children = emacs.as_list()?;
let attributes_child = children
@@ -168,37 +172,83 @@ fn maybe_token_to_usize(
/// Get a named property from the emacs token.
///
/// Returns Ok(None) if value is nil.
///
/// Returns error if the attribute is not specified on the token at all.
pub(crate) fn get_property<'s, 'x>(
emacs: &'s Token<'s>,
/// Returns Ok(None) if value is nil or absent.
pub(crate) fn get_property<'b, 's, 'x>(
emacs: &'b Token<'s>,
key: &'x str,
) -> Result<Option<&'s Token<'s>>, Box<dyn std::error::Error>> {
) -> Result<Option<&'b Token<'s>>, Box<dyn std::error::Error>> {
let children = emacs.as_list()?;
let attributes_child = children
.iter()
.nth(1)
.ok_or("Should have an attributes child.")?;
let attributes_map = attributes_child.as_map()?;
let prop = attributes_map
.get(key)
.ok_or(format!("Missing {} attribute.", key))?;
match prop.as_atom() {
Ok("nil") => return Ok(None),
let prop = attributes_map.get(key).map(|token| *token);
match prop.map(|token| token.as_atom()) {
Some(Ok("nil")) => return Ok(None),
_ => {}
};
Ok(Some(*prop))
Ok(prop)
}
/// Get a named property containing an unquoted atom from the emacs token.
///
/// Returns None if key is not found.
pub(crate) fn get_property_unquoted_atom<'s, 'x>(
emacs: &'s Token<'s>,
pub(crate) fn get_property_unquoted_atom<'b, 's, 'x>(
emacs: &'b Token<'s>,
key: &'x str,
) -> Result<Option<&'s str>, Box<dyn std::error::Error>> {
Ok(get_property(emacs, key)?
.map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))?)
}
/// Get a named property containing an quoted string from the emacs token.
///
/// Returns None if key is not found.
pub(crate) fn get_property_quoted_string<'b, 's, 'x>(
emacs: &'b Token<'s>,
key: &'x str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
Ok(get_property(emacs, key)?
.map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))?
.map(unquote)
.map_or(Ok(None), |r| r.map(Some))?)
}
/// Get a named property containing a boolean value.
///
/// This uses the elisp convention of nil == false, non-nil == true.
///
/// Returns false if key is not found.
pub(crate) fn get_property_boolean<'b, 's, 'x>(
emacs: &'b Token<'s>,
key: &'x str,
) -> Result<bool, Box<dyn std::error::Error>> {
Ok(get_property(emacs, key)?
.map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))?
.unwrap_or("nil")
!= "nil")
}
/// Get a named property containing an unquoted numeric value.
///
/// Returns None if key is not found.
pub(crate) fn get_property_numeric<'b, 's, 'x, N: FromStr>(
emacs: &'b Token<'s>,
key: &'x str,
) -> Result<Option<N>, Box<dyn std::error::Error + 's>>
where
<N as FromStr>::Err: std::error::Error,
<N as FromStr>::Err: 's,
{
let unparsed_string = get_property(emacs, key)?
.map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))?;
let parsed_number = unparsed_string
.map(|val| val.parse::<N>())
.map_or(Ok(None), |r| r.map(Some))?;
Ok(parsed_number)
}

View File

@@ -16,7 +16,7 @@ pub struct GlobalSettings<'g, 's> {
/// Set to true to allow for plain lists using single letters as the bullet in the same way that numbers are used.
///
/// Corresponds to the org-list-allow-alphabetical elisp variable.
pub org_list_allow_alphabetical: bool,
pub list_allow_alphabetical: bool,
/// How many spaces a tab should be equal to.
///
@@ -27,8 +27,15 @@ pub struct GlobalSettings<'g, 's> {
///
/// Corresponds to org-odd-levels-only elisp variable.
pub odd_levels_only: HeadlineLevelFilter,
/// If a headline title matches this string exactly, then that section will become a "footnote section".
///
/// Corresponds to org-footnote-section elisp variable.
pub footnote_section: &'g str,
}
pub const DEFAULT_TAB_WIDTH: IndentationLevel = 8;
impl<'g, 's> GlobalSettings<'g, 's> {
fn new() -> GlobalSettings<'g, 's> {
GlobalSettings {
@@ -38,9 +45,10 @@ impl<'g, 's> GlobalSettings<'g, 's> {
},
in_progress_todo_keywords: BTreeSet::new(),
complete_todo_keywords: BTreeSet::new(),
org_list_allow_alphabetical: false,
tab_width: 8,
odd_levels_only: HeadlineLevelFilter::OddEven,
list_allow_alphabetical: false,
tab_width: DEFAULT_TAB_WIDTH,
odd_levels_only: HeadlineLevelFilter::default(),
footnote_section: "Footnotes",
}
}
}
@@ -51,8 +59,14 @@ impl<'g, 's> Default for GlobalSettings<'g, 's> {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum HeadlineLevelFilter {
Odd,
OddEven,
}
impl Default for HeadlineLevelFilter {
fn default() -> Self {
HeadlineLevelFilter::OddEven
}
}

View File

@@ -26,5 +26,6 @@ pub use file_access_interface::FileAccessInterface;
pub use file_access_interface::LocalFileAccessInterface;
pub use global_settings::GlobalSettings;
pub use global_settings::HeadlineLevelFilter;
pub use global_settings::DEFAULT_TAB_WIDTH;
pub(crate) use list::List;
pub(crate) use parser_with_context::parser_with_context;

View File

@@ -9,6 +9,7 @@ pub enum CustomError<I> {
MyError(MyError<&'static str>),
Nom(I, ErrorKind),
IO(std::io::Error),
BoxedError(Box<dyn std::error::Error>),
}
#[derive(Debug)]
@@ -36,3 +37,9 @@ impl<I> From<&'static str> for CustomError<I> {
CustomError::MyError(MyError(value))
}
}
impl<I> From<Box<dyn std::error::Error>> for CustomError<I> {
fn from(value: Box<dyn std::error::Error>) -> Self {
CustomError::BoxedError(value)
}
}

View File

@@ -1,7 +1,7 @@
use std::collections::VecDeque;
use super::ast_node::AstNode;
use super::ast_node_iter::AstNodeIter;
use crate::types::AstNode;
pub struct AllAstNodeIter<'r, 's> {
root: Option<AstNode<'r, 's>>,
@@ -24,7 +24,9 @@ impl<'r, 's> Iterator for AllAstNodeIter<'r, 's> {
AstNodeIter::Paragraph(ref mut i) => i.next(),
AstNodeIter::PlainList(ref mut i) => i.next(),
AstNodeIter::PlainListItem(ref mut i) => i.next(),
AstNodeIter::GreaterBlock(ref mut i) => i.next(),
AstNodeIter::CenterBlock(ref mut i) => i.next(),
AstNodeIter::QuoteBlock(ref mut i) => i.next(),
AstNodeIter::SpecialBlock(ref mut i) => i.next(),
AstNodeIter::DynamicBlock(ref mut i) => i.next(),
AstNodeIter::FootnoteDefinition(ref mut i) => i.next(),
AstNodeIter::Comment(ref mut i) => i.next(),

View File

@@ -1,12 +1,13 @@
use std::marker::PhantomData;
use super::ast_node::AstNode;
use super::macros::children_iter;
use super::macros::empty_iter;
use super::macros::multi_field_iter;
use crate::types::AngleLink;
use crate::types::AstNode;
use crate::types::BabelCall;
use crate::types::Bold;
use crate::types::CenterBlock;
use crate::types::Citation;
use crate::types::CitationReference;
use crate::types::Clock;
@@ -26,7 +27,6 @@ use crate::types::ExportSnippet;
use crate::types::FixedWidthArea;
use crate::types::FootnoteDefinition;
use crate::types::FootnoteReference;
use crate::types::GreaterBlock;
use crate::types::Heading;
use crate::types::HorizontalRule;
use crate::types::InlineBabelCall;
@@ -46,10 +46,12 @@ use crate::types::PlainListItem;
use crate::types::PlainText;
use crate::types::Planning;
use crate::types::PropertyDrawer;
use crate::types::QuoteBlock;
use crate::types::RadioLink;
use crate::types::RadioTarget;
use crate::types::RegularLink;
use crate::types::Section;
use crate::types::SpecialBlock;
use crate::types::SrcBlock;
use crate::types::StatisticsCookie;
use crate::types::StrikeThrough;
@@ -78,7 +80,9 @@ pub(crate) enum AstNodeIter<'r, 's> {
Paragraph(ParagraphIter<'r, 's>),
PlainList(PlainListIter<'r, 's>),
PlainListItem(PlainListItemIter<'r, 's>),
GreaterBlock(GreaterBlockIter<'r, 's>),
CenterBlock(CenterBlockIter<'r, 's>),
QuoteBlock(QuoteBlockIter<'r, 's>),
SpecialBlock(SpecialBlockIter<'r, 's>),
DynamicBlock(DynamicBlockIter<'r, 's>),
FootnoteDefinition(FootnoteDefinitionIter<'r, 's>),
Comment(CommentIter<'r, 's>),
@@ -140,7 +144,9 @@ impl<'r, 's> AstNodeIter<'r, 's> {
AstNode::Paragraph(inner) => AstNodeIter::Paragraph(inner.into_iter()),
AstNode::PlainList(inner) => AstNodeIter::PlainList(inner.into_iter()),
AstNode::PlainListItem(inner) => AstNodeIter::PlainListItem(inner.into_iter()),
AstNode::GreaterBlock(inner) => AstNodeIter::GreaterBlock(inner.into_iter()),
AstNode::CenterBlock(inner) => AstNodeIter::CenterBlock(inner.into_iter()),
AstNode::QuoteBlock(inner) => AstNodeIter::QuoteBlock(inner.into_iter()),
AstNode::SpecialBlock(inner) => AstNodeIter::SpecialBlock(inner.into_iter()),
AstNode::DynamicBlock(inner) => AstNodeIter::DynamicBlock(inner.into_iter()),
AstNode::FootnoteDefinition(inner) => {
AstNodeIter::FootnoteDefinition(inner.into_iter())
@@ -232,8 +238,18 @@ multi_field_iter!(
std::slice::Iter<'r, Element<'s>>
);
children_iter!(
GreaterBlock<'s>,
GreaterBlockIter,
CenterBlock<'s>,
CenterBlockIter,
std::slice::Iter<'r, Element<'s>>
);
children_iter!(
QuoteBlock<'s>,
QuoteBlockIter,
std::slice::Iter<'r, Element<'s>>
);
children_iter!(
SpecialBlock<'s>,
SpecialBlockIter,
std::slice::Iter<'r, Element<'s>>
);
children_iter!(

View File

@@ -1,16 +1,3 @@
/// Write the implementation of From<> to convert a borrow of the type to an AstNode
macro_rules! to_ast_node {
($inp:ty, $enum:expr) => {
impl<'r, 's> From<$inp> for AstNode<'r, 's> {
fn from(value: $inp) -> Self {
$enum(value)
}
}
};
}
pub(crate) use to_ast_node;
/// Create iterators for ast nodes where it only has to iterate over children
macro_rules! children_iter {
($astnodetype:ty, $itertype:ident, $innertype:ty) => {

View File

@@ -1,5 +1,3 @@
mod all_ast_node_iter;
mod ast_node;
mod ast_node_iter;
mod macros;
pub(crate) use ast_node::AstNode;

View File

@@ -1,5 +1,6 @@
#![feature(exit_status_error)]
#![feature(trait_alias)]
#![feature(path_file_prefix)]
// TODO: #![warn(missing_docs)]
#[cfg(feature = "compare")]
@@ -11,6 +12,10 @@ mod iter;
pub mod parser;
pub mod types;
pub use context::FileAccessInterface;
pub use context::GlobalSettings;
pub use context::LocalFileAccessInterface;
pub mod settings {
pub use crate::context::FileAccessInterface;
pub use crate::context::GlobalSettings;
pub use crate::context::HeadlineLevelFilter;
pub use crate::context::LocalFileAccessInterface;
pub use crate::context::DEFAULT_TAB_WIDTH;
}

View File

@@ -5,8 +5,8 @@ use std::path::Path;
use ::organic::parser::parse;
use organic::parser::parse_with_settings;
use organic::GlobalSettings;
use organic::LocalFileAccessInterface;
use organic::settings::GlobalSettings;
use organic::settings::LocalFileAccessInterface;
#[cfg(feature = "tracing")]
use crate::init_tracing::init_telemetry;

View File

@@ -1,18 +1,19 @@
use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::preceded;
use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::util::get_consumed;
use super::util::org_line_ending;
use crate::context::parser_with_context;
use crate::context::ContextElement;
use crate::context::RefContext;
@@ -38,15 +39,29 @@ pub(crate) fn comment<'b, 'g, 'r, 's>(
let parser_context = context.with_additional_node(&parser_context);
let comment_line_matcher = parser_with_context!(comment_line)(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
let (remaining, _first_line) = comment_line_matcher(input)?;
let (remaining, _remaining_lines) =
let (remaining, first_line) = comment_line_matcher(input)?;
let (remaining, mut remaining_lines) =
many0(preceded(not(exit_matcher), comment_line_matcher))(remaining)?;
let source = get_consumed(input, remaining);
let mut value = Vec::with_capacity(remaining_lines.len() + 1);
let last_line = remaining_lines.pop();
if let Some(last_line) = last_line {
value.push(Into::<&str>::into(first_line));
value.extend(remaining_lines.into_iter().map(Into::<&str>::into));
let last_line = Into::<&str>::into(last_line);
// Trim the line ending from the final line.
value.push(&last_line[..(last_line.len() - 1)])
} else {
// Trim the line ending from the only line.
let only_line = Into::<&str>::into(first_line);
value.push(&only_line[..(only_line.len() - 1)])
}
Ok((
remaining,
Comment {
source: source.into(),
value,
},
))
}
@@ -57,14 +72,13 @@ fn comment_line<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
start_of_line(input)?;
let (remaining, _indent) = space0(input)?;
let (remaining, (_hash, _leading_whitespace_and_content, _line_ending)) = tuple((
tag("#"),
opt(tuple((space1, is_not("\r\n")))),
alt((line_ending, eof)),
))(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
let (remaining, _) = tuple((space0, tag("#")))(input)?;
if let Ok((remaining, line_break)) = org_line_ending(remaining) {
return Ok((remaining, line_break));
}
let (remaining, _) = tag(" ")(remaining)?;
let (remaining, value) = recognize(many_till(anychar, org_line_ending))(remaining)?;
Ok((remaining, value))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -1,9 +1,12 @@
use std::path::Path;
use nom::combinator::all_consuming;
use nom::combinator::opt;
use nom::multi::many0;
use super::headline::heading;
use super::in_buffer_settings::apply_in_buffer_settings;
use super::in_buffer_settings::apply_post_parse_in_buffer_settings;
use super::in_buffer_settings::scan_for_in_buffer_settings;
use super::org_source::OrgSource;
use super::section::zeroth_section;
@@ -17,38 +20,79 @@ use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::iter::AstNode;
use crate::parser::org_source::convert_error;
use crate::parser::util::blank_line;
use crate::types::AstNode;
use crate::types::Document;
use crate::types::Object;
/// Parse a full org-mode document.
///
/// This is the main entry point for Organic. It will parse the full contents of the input string as an org-mode document.
/// This is a main entry point for Organic. It will parse the full contents of the input string as an org-mode document without an underlying file attached.
#[allow(dead_code)]
pub fn parse<'s>(input: &'s str) -> Result<Document<'s>, Box<dyn std::error::Error>> {
parse_with_settings(input, &GlobalSettings::default())
parse_file_with_settings::<&Path>(input, &GlobalSettings::default(), None)
}
/// Parse a full org-mode document.
///
/// This is a main entry point for Organic. It will parse the full contents of the input string as an org-mode document at the file_path.
///
/// file_path is not used for reading the file contents. It is only used for determining the document category and filling in the path attribute on the Document.
#[allow(dead_code)]
pub fn parse_file<'s, P: AsRef<Path>>(
input: &'s str,
file_path: Option<P>,
) -> Result<Document<'s>, Box<dyn std::error::Error>> {
parse_file_with_settings(input, &GlobalSettings::default(), file_path)
}
/// Parse a full org-mode document with starting settings.
///
/// This is the secondary entry point for Organic. It will parse the full contents of the input string as an org-mode document starting with the settings you supplied.
/// This is a secondary entry point for Organic. It will parse the full contents of the input string as an org-mode document starting with the settings you supplied without an underlying file attached.
///
/// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO".
#[allow(dead_code)]
pub fn parse_with_settings<'g, 's>(
input: &'s str,
global_settings: &'g GlobalSettings<'g, 's>,
) -> Result<Document<'s>, Box<dyn std::error::Error>> {
parse_file_with_settings::<&Path>(input, global_settings, None)
}
/// Parse a full org-mode document with starting settings.
///
/// This is the secondary entry point for Organic. It will parse the full contents of the input string as an org-mode document at the file_path starting with the settings you supplied.
///
/// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO".
///
/// file_path is not used for reading the file contents. It is only used for determining the document category and filling in the path attribute on the Document.
#[allow(dead_code)]
pub fn parse_file_with_settings<'g, 's, P: AsRef<Path>>(
input: &'s str,
global_settings: &'g GlobalSettings<'g, 's>,
file_path: Option<P>,
) -> Result<Document<'s>, Box<dyn std::error::Error>> {
let initial_context = ContextElement::document_context();
let initial_context = Context::new(global_settings, List::new(&initial_context));
let wrapped_input = OrgSource::new(input);
let ret =
let mut doc =
all_consuming(parser_with_context!(document_org_source)(&initial_context))(wrapped_input)
.map_err(|err| err.to_string())
.map(|(_remaining, parsed_document)| parsed_document);
Ok(ret?)
.map(|(_remaining, parsed_document)| parsed_document)?;
if let Some(file_path) = file_path {
let full_path = file_path.as_ref().canonicalize()?;
if doc.category.is_none() {
let category = full_path
.file_stem()
.expect("File should have a name.")
.to_str()
.expect("File name should be valid utf-8.");
doc.category = Some(category.to_owned());
}
doc.path = Some(full_path);
}
Ok(doc)
}
/// Parse a full org-mode document.
@@ -106,7 +150,7 @@ fn document_org_source<'b, 'g, 'r, 's>(
let new_context = context.with_global_settings(&new_settings);
let context = &new_context;
let (remaining, document) =
let (remaining, mut document) =
_document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?;
{
// If there are radio targets in this document then we need to parse the entire document again with the knowledge of the radio targets.
@@ -125,11 +169,18 @@ fn document_org_source<'b, 'g, 'r, 's>(
let mut new_global_settings = context.get_global_settings().clone();
new_global_settings.radio_targets = all_radio_targets;
let parser_context = context.with_global_settings(&new_global_settings);
let (remaining, document) = _document(&parser_context, input)
let (remaining, mut document) = _document(&parser_context, input)
.map(|(rem, out)| (Into::<&str>::into(rem), out))?;
apply_post_parse_in_buffer_settings(&mut document)
.map_err(|err| nom::Err::<CustomError<OrgSource<'_>>>::Failure(err.into()))?;
return Ok((remaining.into(), document));
}
}
// Find final in-buffer settings that do not impact parsing
apply_post_parse_in_buffer_settings(&mut document)
.map_err(|err| nom::Err::<CustomError<OrgSource<'_>>>::Failure(err.into()))?;
Ok((remaining.into(), document))
}
@@ -148,6 +199,8 @@ fn _document<'b, 'g, 'r, 's>(
remaining,
Document {
source: source.into(),
category: None,
path: None,
zeroth_section,
children,
},

View File

@@ -2,6 +2,7 @@ use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::character::complete::space1;
@@ -9,6 +10,7 @@ use nom::combinator::consumed;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till;
@@ -47,10 +49,11 @@ pub(crate) fn dynamic_block<'b, 'g, 'r, 's>(
}
start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(input)?;
let (remaining, (_begin, name, parameters, _ws)) = tuple((
let (remaining, (_, name, parameters, _, _)) = tuple((
recognize(tuple((tag_no_case("#+begin:"), space1))),
name,
opt(tuple((space1, parameters))),
space0,
line_ending,
))(remaining)?;
let contexts = [
@@ -108,7 +111,7 @@ fn name<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn parameters<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
is_not("\r\n")(input)
recognize(many_till(anychar, peek(tuple((space0, line_ending)))))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -81,7 +81,7 @@ fn _element<'b, 'g, 'r, 's>(
let (remaining, mut affiliated_keywords) = many0(affiliated_keyword_matcher)(input)?;
let (remaining, mut element) = match alt((
map(plain_list_matcher, Element::PlainList),
map(greater_block_matcher, Element::GreaterBlock),
greater_block_matcher,
map(dynamic_block_matcher, Element::DynamicBlock),
map(footnote_definition_matcher, Element::FootnoteDefinition),
map(comment_matcher, Element::Comment),

View File

@@ -1,6 +1,7 @@
use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::character::complete::space1;
@@ -8,6 +9,8 @@ use nom::combinator::consumed;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many_till;
@@ -30,17 +33,18 @@ use crate::parser::util::blank_line;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed;
use crate::parser::util::start_of_line;
use crate::types::CenterBlock;
use crate::types::Element;
use crate::types::GreaterBlock;
use crate::types::Paragraph;
use crate::types::QuoteBlock;
use crate::types::SetSource;
use crate::types::SpecialBlock;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn greater_block<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, GreaterBlock<'s>> {
// TODO: Do I need to differentiate between different greater block types.
) -> Res<OrgSource<'s>, Element<'s>> {
start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(input)?;
let (remaining, (_begin, name)) = tuple((
@@ -52,22 +56,97 @@ pub(crate) fn greater_block<'b, 'g, 'r, 's>(
}
}),
))(remaining)?;
let context_name = match Into::<&str>::into(name).to_lowercase().as_str() {
"center" => "center block".to_owned(),
"quote" => "quote block".to_owned(),
name @ _ => format!("special block {}", name),
let name = Into::<&str>::into(name);
let (remaining, element) = match name.to_lowercase().as_str() {
"center" => center_block(context, remaining, input)?,
"quote" => quote_block(context, remaining, input)?,
_ => special_block(name)(context, remaining, input)?,
};
if in_section(context, context_name.as_str()) {
Ok((remaining, element))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn center_block<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
original_input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Element<'s>> {
let (remaining, (source, children)) =
greater_block_body(context, input, original_input, "center", "center block")?;
Ok((
remaining,
Element::CenterBlock(CenterBlock { source, children }),
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn quote_block<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
original_input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Element<'s>> {
let (remaining, (source, children)) =
greater_block_body(context, input, original_input, "quote", "quote block")?;
Ok((
remaining,
Element::QuoteBlock(QuoteBlock { source, children }),
))
}
fn special_block<'s>(
name: &'s str,
) -> impl for<'b, 'g, 'r> Fn(
RefContext<'b, 'g, 'r, 's>,
OrgSource<'s>,
OrgSource<'s>,
) -> Res<OrgSource<'s>, Element<'s>>
+ 's {
let context_name = format!("special block {}", name);
move |context, input, original_input| {
_special_block(context, input, original_input, name, context_name.as_str())
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _special_block<'c, 'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
original_input: OrgSource<'s>,
name: &'s str,
context_name: &'c str,
) -> Res<OrgSource<'s>, Element<'s>> {
let (remaining, parameters) = opt(tuple((space1, parameters)))(input)?;
let (remaining, (source, children)) =
greater_block_body(context, remaining, original_input, name, context_name)?;
Ok((
remaining,
Element::SpecialBlock(SpecialBlock {
source,
children,
name,
parameters: parameters.map(|(_, parameters)| Into::<&str>::into(parameters)),
}),
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn greater_block_body<'c, 'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
original_input: OrgSource<'s>,
name: &'c str,
context_name: &'c str,
) -> Res<OrgSource<'s>, (&'s str, Vec<Element<'s>>)> {
if in_section(context, context_name) {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(),
))));
}
let exit_with_name = greater_block_end(name.into());
let (remaining, parameters) = opt(tuple((space1, parameters)))(remaining)?;
let (remaining, _nl) = line_ending(remaining)?;
let exit_with_name = greater_block_end(name);
let (remaining, _nl) = tuple((space0, line_ending))(input)?;
let contexts = [
ContextElement::ConsumeTrailingWhitespace(true),
ContextElement::Context(context_name.as_str()),
ContextElement::Context(context_name),
ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Alpha,
exit_matcher: &exit_with_name,
@@ -76,11 +155,6 @@ pub(crate) fn greater_block<'b, 'g, 'r, 's>(
let parser_context = context.with_additional_node(&contexts[0]);
let parser_context = parser_context.with_additional_node(&contexts[1]);
let parser_context = parser_context.with_additional_node(&contexts[2]);
let parameters = match parameters {
Some((_ws, parameters)) => Some(parameters),
None => None,
};
let element_matcher = parser_with_context!(element(true))(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
not(exit_matcher)(remaining)?;
@@ -104,16 +178,8 @@ pub(crate) fn greater_block<'b, 'g, 'r, 's>(
// Not checking if parent exit matcher is causing exit because the greater_block_end matcher asserts we matched a full greater block
let source = get_consumed(input, remaining);
Ok((
remaining,
GreaterBlock {
source: source.into(),
name: name.into(),
parameters: parameters.map(|val| Into::<&str>::into(val)),
children,
},
))
let source = get_consumed(original_input, remaining);
Ok((remaining, (Into::<&str>::into(source), children)))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -123,7 +189,7 @@ fn name<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn parameters<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
is_not("\r\n")(input)
recognize(many_till(anychar, peek(tuple((space0, line_ending)))))(input)
}
fn greater_block_end<'c>(name: &'c str) -> impl ContextMatcher + 'c {

View File

@@ -4,6 +4,7 @@ use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::consumed;
use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::opt;
@@ -33,6 +34,7 @@ use crate::error::Res;
use crate::parser::object_parser::standard_set_object;
use crate::parser::util::blank_line;
use crate::types::DocumentElement;
use crate::types::Element;
use crate::types::Heading;
use crate::types::HeadlineLevel;
use crate::types::Object;
@@ -54,27 +56,27 @@ fn _heading<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
parent_star_count: HeadlineLevel,
) -> Res<OrgSource<'s>, Heading<'s>> {
let mut scheduled = None;
let mut deadline = None;
let mut closed = None;
not(|i| context.check_exit_matcher(i))(input)?;
let (
remaining,
(
headline_level,
star_count,
maybe_todo_keyword,
maybe_priority,
maybe_comment,
title,
heading_tags,
),
) = headline(context, input, parent_star_count)?;
let (remaining, pre_headline) = headline(context, input, parent_star_count)?;
let section_matcher = parser_with_context!(section)(context);
let heading_matcher = parser_with_context!(heading(star_count))(context);
let heading_matcher = parser_with_context!(heading(pre_headline.star_count))(context);
let (remaining, maybe_section) =
opt(map(section_matcher, DocumentElement::Section))(remaining)?;
let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?;
let (remaining, mut children) =
many0(map(heading_matcher, DocumentElement::Heading))(remaining)?;
if let Some(section) = maybe_section {
// If the section has a planning then the timestamp values are copied to the heading.
if let DocumentElement::Section(inner_section) = &section {
if let Some(Element::Planning(planning)) = inner_section.children.first() {
scheduled = planning.scheduled.clone();
deadline = planning.deadline.clone();
closed = planning.closed.clone();
}
}
children.insert(0, section);
}
let remaining = if children.is_empty() {
@@ -84,23 +86,29 @@ fn _heading<'b, 'g, 'r, 's>(
} else {
remaining
};
let is_archived = heading_tags.contains(&"ARCHIVE");
let is_archived = pre_headline.tags.contains(&"ARCHIVE");
let source = get_consumed(input, remaining);
Ok((
remaining,
Heading {
source: source.into(),
level: headline_level,
todo_keyword: maybe_todo_keyword.map(|(todo_keyword_type, todo_keyword)| {
(todo_keyword_type, Into::<&str>::into(todo_keyword))
}),
priority_cookie: maybe_priority.map(|(_, priority)| priority),
title,
tags: heading_tags,
level: pre_headline.headline_level,
todo_keyword: pre_headline
.todo_keyword
.map(|(todo_keyword_type, todo_keyword)| {
(todo_keyword_type, Into::<&str>::into(todo_keyword))
}),
priority_cookie: pre_headline.priority_cookie.map(|(_, priority)| priority),
title: pre_headline.title,
tags: pre_headline.tags,
children,
is_comment: maybe_comment.is_some(),
is_comment: pre_headline.comment.is_some(),
is_archived,
is_footnote_section: pre_headline.is_footnote_section,
scheduled,
deadline,
closed,
},
))
}
@@ -111,23 +119,27 @@ pub(crate) fn detect_headline<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()
Ok((input, ()))
}
/// Fields from a not-yet-fully-parsed Headline.
///
/// This struct exists to give names to the fields of a partially-parsed Headline to avoid returning a large tuple of nameless fields.
#[derive(Debug)]
struct PreHeadline<'s> {
headline_level: HeadlineLevel,
star_count: HeadlineLevel,
todo_keyword: Option<(TodoKeywordType, OrgSource<'s>)>,
priority_cookie: Option<(OrgSource<'s>, PriorityCookie)>,
comment: Option<OrgSource<'s>>,
title: Vec<Object<'s>>,
tags: Vec<&'s str>,
is_footnote_section: bool,
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn headline<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
parent_star_count: HeadlineLevel,
) -> Res<
OrgSource<'s>,
(
HeadlineLevel,
HeadlineLevel,
Option<(TodoKeywordType, OrgSource<'s>)>,
Option<(OrgSource<'s>, PriorityCookie)>,
Option<OrgSource<'s>>,
Vec<Object<'s>>,
Vec<&'s str>,
),
> {
) -> Res<OrgSource<'s>, PreHeadline<'s>> {
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Document,
exit_matcher: &headline_title_end,
@@ -159,30 +171,43 @@ fn headline<'b, 'g, 'r, 's>(
let (remaining, maybe_title) = opt(tuple((
space1,
many1(parser_with_context!(standard_set_object)(&parser_context)),
consumed(many1(parser_with_context!(standard_set_object)(
&parser_context,
))),
)))(remaining)?;
let (remaining, maybe_tags) = opt(tuple((space0, tags)))(remaining)?;
let (remaining, _) = tuple((space0, org_line_ending))(remaining)?;
let is_footnote_section = maybe_title
.as_ref()
.map(|(_, (raw_title, _))| raw_title)
.map(|raw_title| {
Into::<&str>::into(raw_title) == context.get_global_settings().footnote_section
})
.unwrap_or(false);
Ok((
remaining,
(
PreHeadline {
headline_level,
star_count,
maybe_todo_keyword.map(|(_, todo, _)| todo),
maybe_priority,
maybe_comment.map(|(_, comment, _)| comment),
maybe_title.map(|(_, title)| title).unwrap_or(Vec::new()),
maybe_tags
todo_keyword: maybe_todo_keyword.map(|(_, todo, _)| todo),
priority_cookie: maybe_priority,
comment: maybe_comment.map(|(_, comment, _)| comment),
title: maybe_title
.map(|(_, (_, title))| title)
.unwrap_or(Vec::new()),
tags: maybe_tags
.map(|(_ws, tags)| {
tags.into_iter()
.map(|single_tag| Into::<&str>::into(single_tag))
.collect()
})
.unwrap_or(Vec::new()),
),
is_footnote_section,
},
))
}

View File

@@ -11,8 +11,10 @@ use super::OrgSource;
use crate::context::HeadlineLevelFilter;
use crate::error::CustomError;
use crate::error::Res;
use crate::settings::GlobalSettings;
use crate::types::AstNode;
use crate::types::Document;
use crate::types::Keyword;
use crate::GlobalSettings;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn scan_for_in_buffer_settings<'s>(
@@ -114,6 +116,25 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
Ok(new_settings)
}
/// Apply in-buffer settings that do not impact parsing and therefore can be applied after parsing.
pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(
document: &mut Document<'s>,
) -> Result<(), &'static str> {
document.category = Into::<AstNode>::into(&*document)
.into_iter()
.filter_map(|ast_node| {
if let AstNode::Keyword(ast_node) = ast_node {
if ast_node.key.eq_ignore_ascii_case("category") {
return Some(ast_node);
}
}
None
})
.last()
.map(|kw| kw.value.to_owned());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -45,5 +45,7 @@ mod text_markup;
mod timestamp;
mod util;
pub use document::parse;
pub use document::parse_file;
pub use document::parse_file_with_settings;
pub use document::parse_with_settings;
pub(crate) use org_source::OrgSource;

View File

@@ -78,6 +78,12 @@ impl<'s> OrgSource<'s> {
self.slice(..(other.start - self.start))
}
pub(crate) fn get_until_end_of(&self, other: OrgSource<'s>) -> OrgSource<'s> {
debug_assert!(other.start >= self.start);
debug_assert!(other.end <= self.end);
self.slice(..(other.end - self.start))
}
pub(crate) fn get_start_of_line(&self) -> OrgSource<'s> {
let skipped_text = self.text_since_line_break();
let mut bracket_depth = self.bracket_depth;
@@ -382,6 +388,7 @@ impl<'s> From<CustomError<OrgSource<'s>>> for CustomError<&'s str> {
CustomError::MyError(err) => CustomError::MyError(err.into()),
CustomError::Nom(input, error_kind) => CustomError::Nom(input.into(), error_kind),
CustomError::IO(err) => CustomError::IO(err),
CustomError::BoxedError(err) => CustomError::BoxedError(err),
}
}
}

View File

@@ -44,6 +44,8 @@ use crate::types::IndentationLevel;
use crate::types::Object;
use crate::types::PlainList;
use crate::types::PlainListItem;
use crate::types::PlainListItemCounter;
use crate::types::PlainListItemPreBlank;
use crate::types::PlainListType;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -54,12 +56,12 @@ pub(crate) fn detect_plain_list<'b, 'g, 'r, 's>(
if verify(
tuple((
start_of_line,
space0,
parser_with_context!(indentation_level)(context),
parser_with_context!(bullet)(context),
alt((space1, line_ending, eof)),
)),
|(_start, indent, (_bullet_type, bull), _after_whitespace)| {
Into::<&str>::into(bull) != "*" || indent.len() > 0
|(_start, (indent_level, _), (_bullet_type, bull), _after_whitespace)| {
!Into::<&str>::into(bull).starts_with("*") || *indent_level > 0
},
)(input)
.is_ok()
@@ -163,15 +165,12 @@ fn plain_list_item<'b, 'g, 'r, 's>(
let (remaining, (indent_level, _leading_whitespace)) = indentation_level(context, input)?;
let (remaining, (bullet_type, bull)) = verify(
parser_with_context!(bullet)(context),
|(_bullet_type, bull)| Into::<&str>::into(bull) != "*" || indent_level > 0,
|(_bullet_type, bull)| !Into::<&str>::into(bull).starts_with("*") || indent_level > 0,
)(remaining)?;
let (remaining, _maybe_counter_set) = opt(tuple((
space1,
tag("[@"),
parser_with_context!(counter)(context),
tag("]"),
)))(remaining)?;
let (remaining, maybe_counter_set) =
opt(tuple((space1, tag("[@"), counter_set_value, tag("]"))))(remaining)?;
let maybe_counter_set = maybe_counter_set.map(|(_, _, val, _)| val);
let (remaining, maybe_checkbox) = opt(tuple((space1, item_checkbox)))(remaining)?;
@@ -217,10 +216,12 @@ fn plain_list_item<'b, 'g, 'r, 's>(
source: source.into(),
indentation: indent_level,
bullet: bull.into(),
counter: maybe_counter_set,
checkbox: None,
tag: maybe_tag
.map(|(_ws, item_tag)| item_tag)
.unwrap_or(Vec::new()),
pre_blank: 0,
children: Vec::new(),
},
),
@@ -228,7 +229,11 @@ fn plain_list_item<'b, 'g, 'r, 's>(
}
Err(_) => {}
};
let (remaining, _ws) = item_tag_post_gap(&parser_context, remaining)?;
let (remaining, pre_blank) = item_tag_post_gap(&parser_context, remaining)?;
let pre_blank = Into::<&str>::into(pre_blank)
.bytes()
.filter(|b| *b == b'\n')
.count();
let (mut remaining, (mut children, _exit_contents)) = many_till(
include_input(parser_with_context!(element(true))(&parser_context)),
@@ -260,12 +265,15 @@ fn plain_list_item<'b, 'g, 'r, 's>(
source: source.into(),
indentation: indent_level,
bullet: bull.into(),
counter: maybe_counter_set,
checkbox: maybe_checkbox.map(|(_, (checkbox_type, source))| {
(checkbox_type, Into::<&str>::into(source))
}),
tag: maybe_tag
.map(|(_ws, item_tag)| item_tag)
.unwrap_or(Vec::new()),
pre_blank: PlainListItemPreBlank::try_from(pre_blank)
.expect("pre-blank cannot be larger than 2."),
children: children.into_iter().map(|(_start, item)| item).collect(),
},
),
@@ -283,18 +291,23 @@ fn bullet<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (BulletType, OrgSource<'s>)> {
alt((
map(tag("*"), |bull| (BulletType::Unordered, bull)),
map(tag("-"), |bull| (BulletType::Unordered, bull)),
map(tag("+"), |bull| (BulletType::Unordered, bull)),
map(
recognize(tuple((
parser_with_context!(counter)(context),
alt((tag("."), tag(")"))),
))),
|bull| (BulletType::Ordered, bull),
),
))(input)
let (remaining, ((bullet_type, _without_space), peek_trailing_space)) = tuple((
alt((
map(tag("*"), |bull| (BulletType::Unordered, bull)),
map(tag("-"), |bull| (BulletType::Unordered, bull)),
map(tag("+"), |bull| (BulletType::Unordered, bull)),
map(
recognize(tuple((
parser_with_context!(counter)(context),
alt((tag("."), tag(")"))),
))),
|bull| (BulletType::Ordered, bull),
),
)),
peek(space0),
))(input)?;
let with_space = input.get_until_end_of(peek_trailing_space);
Ok((remaining, (bullet_type, with_space)))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -302,7 +315,7 @@ fn counter<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
if context.get_global_settings().org_list_allow_alphabetical {
if context.get_global_settings().list_allow_alphabetical {
alt((
recognize(one_of(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
@@ -314,6 +327,29 @@ fn counter<'b, 'g, 'r, 's>(
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn counter_set_value<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PlainListItemCounter> {
alt((
map(
one_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|letter| {
let num = match letter {
'a'..='z' => (letter as u32) - ('a' as u32) + 1,
'A'..='Z' => (letter as u32) - ('A' as u32) + 1,
_ => unreachable!(),
};
PlainListItemCounter::try_from(num)
.expect("Counter set value should be between 1 and 26 inclusive.")
},
),
map(digit1, |num: OrgSource<'_>| {
Into::<&str>::into(num)
.parse()
.expect("digit1 must parse to a number.")
}),
))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn plain_list_end<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,

View File

@@ -3,6 +3,7 @@ use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::map;
use nom::multi::many1;
use nom::sequence::tuple;
@@ -16,6 +17,7 @@ use crate::error::Res;
use crate::parser::util::get_consumed;
use crate::parser::util::start_of_line;
use crate::types::Planning;
use crate::types::Timestamp;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn planning<'b, 'g, 'r, 's>(
@@ -24,7 +26,7 @@ pub(crate) fn planning<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, Planning<'s>> {
start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(input)?;
let (remaining, _planning_parameters) =
let (remaining, planning_parameters) =
many1(parser_with_context!(planning_parameter)(context))(remaining)?;
let (remaining, _trailing_ws) = tuple((space0, org_line_ending))(remaining)?;
@@ -32,26 +34,55 @@ pub(crate) fn planning<'b, 'g, 'r, 's>(
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
let source = get_consumed(input, remaining);
let mut scheduled = None;
let mut deadline = None;
let mut closed = None;
for (timestamp_type, timestamp) in planning_parameters.into_iter() {
match timestamp_type {
PlanningTimestampType::Scheduled => {
scheduled = Some(timestamp);
}
PlanningTimestampType::Deadline => {
deadline = Some(timestamp);
}
PlanningTimestampType::Closed => {
closed = Some(timestamp);
}
}
}
Ok((
remaining,
Planning {
source: source.into(),
scheduled,
deadline,
closed,
},
))
}
#[derive(Debug)]
enum PlanningTimestampType {
Scheduled,
Deadline,
Closed,
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn planning_parameter<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _planning_type) = alt((
tag_no_case("DEADLINE"),
tag_no_case("SCHEDULED"),
tag_no_case("CLOSED"),
) -> Res<OrgSource<'s>, (PlanningTimestampType, Timestamp<'s>)> {
let (remaining, planning_type) = alt((
map(tag_no_case("DEADLINE"), |_| PlanningTimestampType::Deadline),
map(tag_no_case("SCHEDULED"), |_| {
PlanningTimestampType::Scheduled
}),
map(tag_no_case("CLOSED"), |_| PlanningTimestampType::Closed),
))(input)?;
let (remaining, _gap) = tuple((tag(":"), space1))(remaining)?;
let (remaining, _timestamp) = timestamp(context, remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
let (remaining, timestamp) = timestamp(context, remaining)?;
Ok((remaining, (planning_type, timestamp)))
}

View File

@@ -101,7 +101,7 @@ fn node_property<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, NodeProperty<'s>> {
let (remaining, (_start_of_line, _leading_whitespace, _open_colon, _name, _close_colon)) =
let (remaining, (_start_of_line, _leading_whitespace, _open_colon, name, _close_colon)) =
tuple((
start_of_line,
space0,
@@ -120,6 +120,7 @@ fn node_property<'b, 'g, 'r, 's>(
remaining,
NodeProperty {
source: source.into(),
name: Into::<&str>::into(name),
value: None,
},
))
@@ -132,6 +133,7 @@ fn node_property<'b, 'g, 'r, 's>(
remaining,
NodeProperty {
source: source.into(),
name: Into::<&str>::into(name),
value: Some(value.into()),
},
))

View File

@@ -3,8 +3,8 @@ use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
@@ -17,6 +17,7 @@ use super::keyword::table_formula_keyword;
use super::object_parser::table_cell_set_object;
use super::org_source::OrgSource;
use super::util::exit_matcher_parser;
use super::util::org_line_ending;
use crate::context::parser_with_context;
use crate::context::ContextElement;
use crate::context::ExitClass;
@@ -105,7 +106,7 @@ fn org_mode_table_row_rule<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, TableRow<'s>> {
start_of_line(input)?;
let (remaining, _) = tuple((space0, tag("|-"), is_not("\r\n"), line_ending))(input)?;
let (remaining, _) = tuple((space0, tag("|-"), opt(is_not("\r\n")), org_line_ending))(input)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
@@ -125,7 +126,7 @@ fn org_mode_table_row_regular<'b, 'g, 'r, 's>(
let (remaining, _) = tuple((space0, tag("|")))(input)?;
let (remaining, children) =
many1(parser_with_context!(org_mode_table_cell)(context))(remaining)?;
let (remaining, _tail) = recognize(tuple((space0, alt((line_ending, eof)))))(remaining)?;
let (remaining, _tail) = recognize(tuple((space0, org_line_ending)))(remaining)?;
let source = get_consumed(input, remaining);
Ok((
remaining,

View File

@@ -1,10 +1,10 @@
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::digit0;
use nom::character::complete::digit1;
use nom::character::complete::one_of;
use nom::character::complete::space1;
use nom::combinator::map;
use nom::combinator::opt;
use nom::combinator::recognize;
use nom::combinator::verify;
@@ -21,7 +21,21 @@ use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::Res;
use crate::parser::util::get_consumed;
use crate::types::Date;
use crate::types::DayOfMonth;
use crate::types::Hour;
use crate::types::Minute;
use crate::types::Month;
use crate::types::Repeater;
use crate::types::RepeaterType;
use crate::types::Time;
use crate::types::TimeUnit;
use crate::types::Timestamp;
use crate::types::TimestampRangeType;
use crate::types::TimestampType;
use crate::types::WarningDelay;
use crate::types::WarningDelayType;
use crate::types::Year;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn timestamp<'b, 'g, 'r, 's>(
@@ -57,6 +71,14 @@ fn diary_timestamp<'b, 'g, 'r, 's>(
remaining,
Timestamp {
source: source.into(),
timestamp_type: TimestampType::Diary,
range_type: TimestampRangeType::None,
start: None,
end: None,
start_time: None,
end_time: None,
repeater: None,
warning_delay: None,
},
))
}
@@ -97,17 +119,26 @@ fn active_timestamp<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _) = tag("<")(input)?;
let (remaining, _date) = date(context, remaining)?;
let (remaining, start) = date(context, remaining)?;
let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma,
exit_matcher: &active_time_rest_end,
});
let time_context = context.with_additional_node(&time_context);
let (remaining, _time) =
opt(tuple((space1, parser_with_context!(time)(&time_context))))(remaining)?;
let (remaining, _repeater) =
let (remaining, time) = opt(tuple((
space1,
parser_with_context!(time(true))(&time_context),
)))(remaining)?;
let remaining = if time.is_none() {
// Upstream org-mode accepts malformed timestamps. For example '<2016-02-14 Sun ++y>'.
let (remain, _) = opt(parser_with_context!(time_rest)(&time_context))(remaining)?;
remain
} else {
remaining
};
let (remaining, repeater) =
opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?;
let (remaining, _warning_delay) = opt(tuple((
let (remaining, warning_delay) = opt(tuple((
space1,
parser_with_context!(warning_delay)(context),
)))(remaining)?;
@@ -121,6 +152,14 @@ fn active_timestamp<'b, 'g, 'r, 's>(
remaining,
Timestamp {
source: source.into(),
timestamp_type: TimestampType::Active,
range_type: TimestampRangeType::None,
start: Some(start.clone()),
end: Some(start),
start_time: time.as_ref().map(|(_, time)| time.clone()),
end_time: time.map(|(_, time)| time),
repeater: repeater.map(|(_, repeater)| repeater),
warning_delay: warning_delay.map(|(_, warning_delay)| warning_delay),
},
))
}
@@ -131,17 +170,26 @@ fn inactive_timestamp<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _) = tag("[")(input)?;
let (remaining, _date) = date(context, remaining)?;
let (remaining, start) = date(context, remaining)?;
let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma,
exit_matcher: &inactive_time_rest_end,
});
let time_context = context.with_additional_node(&time_context);
let (remaining, _time) =
opt(tuple((space1, parser_with_context!(time)(&time_context))))(remaining)?;
let (remaining, _repeater) =
let (remaining, time) = opt(tuple((
space1,
parser_with_context!(time(true))(&time_context),
)))(remaining)?;
let remaining = if time.is_none() {
// Upstream org-mode accepts malformed timestamps. For example '<2016-02-14 Sun ++y>'.
let (remain, _) = opt(parser_with_context!(time_rest)(&time_context))(remaining)?;
remain
} else {
remaining
};
let (remaining, repeater) =
opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?;
let (remaining, _warning_delay) = opt(tuple((
let (remaining, warning_delay) = opt(tuple((
space1,
parser_with_context!(warning_delay)(context),
)))(remaining)?;
@@ -155,6 +203,14 @@ fn inactive_timestamp<'b, 'g, 'r, 's>(
remaining,
Timestamp {
source: source.into(),
timestamp_type: TimestampType::Inactive,
range_type: TimestampRangeType::None,
start: Some(start.clone()),
end: Some(start),
start_time: time.as_ref().map(|(_, time)| time.clone()),
end_time: time.map(|(_, time)| time),
repeater: repeater.map(|(_, repeater)| repeater),
warning_delay: warning_delay.map(|(_, warning_delay)| warning_delay),
},
))
}
@@ -164,10 +220,10 @@ fn active_date_range_timestamp<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _first_timestamp) = active_timestamp(context, input)?;
let (remaining, first_timestamp) = active_timestamp(context, input)?;
// TODO: Does the space0 at the end of the active/inactive timestamp parsers cause this to be incorrect? I could use a look-behind to make sure the preceding character is not whitespace
let (remaining, _separator) = tag("--")(remaining)?;
let (remaining, _second_timestamp) = active_timestamp(context, remaining)?;
let (remaining, second_timestamp) = active_timestamp(context, remaining)?;
let (remaining, _trailing_whitespace) =
maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?;
@@ -177,6 +233,16 @@ fn active_date_range_timestamp<'b, 'g, 'r, 's>(
remaining,
Timestamp {
source: source.into(),
timestamp_type: TimestampType::ActiveRange,
range_type: TimestampRangeType::DateRange,
start: first_timestamp.start,
end: second_timestamp.end,
start_time: first_timestamp.start_time,
end_time: second_timestamp.end_time,
repeater: first_timestamp.repeater.or(second_timestamp.repeater),
warning_delay: first_timestamp
.warning_delay
.or(second_timestamp.warning_delay),
},
))
}
@@ -187,7 +253,7 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _) = tag("<")(input)?;
let (remaining, _date) = date(context, remaining)?;
let (remaining, start_date) = date(context, remaining)?;
let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma,
exit_matcher: &active_time_rest_end,
@@ -198,13 +264,15 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>(
exit_matcher: &time_range_rest_end,
});
let first_time_context = time_context.with_additional_node(&first_time_context);
let (remaining, _first_time) =
tuple((space1, parser_with_context!(time)(&first_time_context)))(remaining)?;
let (remaining, (_, first_time)) = tuple((
space1,
parser_with_context!(time(false))(&first_time_context),
))(remaining)?;
let (remaining, _) = tag("-")(remaining)?;
let (remaining, _second_time) = parser_with_context!(time)(&time_context)(remaining)?;
let (remaining, _repeater) =
let (remaining, second_time) = parser_with_context!(time(true))(&time_context)(remaining)?;
let (remaining, repeater) =
opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?;
let (remaining, _warning_delay) = opt(tuple((
let (remaining, warning_delay) = opt(tuple((
space1,
parser_with_context!(warning_delay)(context),
)))(remaining)?;
@@ -218,6 +286,14 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>(
remaining,
Timestamp {
source: source.into(),
timestamp_type: TimestampType::ActiveRange,
range_type: TimestampRangeType::TimeRange,
start: Some(start_date.clone()),
end: Some(start_date),
start_time: Some(first_time),
end_time: Some(second_time),
repeater: repeater.map(|(_, repeater)| repeater),
warning_delay: warning_delay.map(|(_, warning_delay)| warning_delay),
},
))
}
@@ -227,10 +303,10 @@ fn inactive_date_range_timestamp<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _first_timestamp) = inactive_timestamp(context, input)?;
let (remaining, first_timestamp) = inactive_timestamp(context, input)?;
// TODO: Does the space0 at the end of the active/inactive timestamp parsers cause this to be incorrect? I could use a look-behind to make sure the preceding character is not whitespace
let (remaining, _separator) = tag("--")(remaining)?;
let (remaining, _second_timestamp) = inactive_timestamp(context, remaining)?;
let (remaining, second_timestamp) = inactive_timestamp(context, remaining)?;
let (remaining, _trailing_whitespace) =
maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?;
@@ -240,6 +316,17 @@ fn inactive_date_range_timestamp<'b, 'g, 'r, 's>(
remaining,
Timestamp {
source: source.into(),
timestamp_type: TimestampType::InactiveRange,
range_type: TimestampRangeType::DateRange,
start: first_timestamp.start,
end: second_timestamp.end,
start_time: first_timestamp.start_time,
end_time: second_timestamp.end_time,
repeater: first_timestamp.repeater.or(second_timestamp.repeater),
warning_delay: first_timestamp
.warning_delay
.or(second_timestamp.warning_delay),
},
))
}
@@ -250,7 +337,7 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _) = tag("[")(input)?;
let (remaining, _date) = date(context, remaining)?;
let (remaining, start_date) = date(context, remaining)?;
let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma,
exit_matcher: &inactive_time_rest_end,
@@ -261,13 +348,15 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>(
exit_matcher: &time_range_rest_end,
});
let first_time_context = time_context.with_additional_node(&first_time_context);
let (remaining, _first_time) =
tuple((space1, parser_with_context!(time)(&first_time_context)))(remaining)?;
let (remaining, (_, first_time)) = tuple((
space1,
parser_with_context!(time(false))(&first_time_context),
))(remaining)?;
let (remaining, _) = tag("-")(remaining)?;
let (remaining, _second_time) = parser_with_context!(time)(&time_context)(remaining)?;
let (remaining, _repeater) =
let (remaining, second_time) = parser_with_context!(time(true))(&time_context)(remaining)?;
let (remaining, repeater) =
opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?;
let (remaining, _warning_delay) = opt(tuple((
let (remaining, warning_delay) = opt(tuple((
space1,
parser_with_context!(warning_delay)(context),
)))(remaining)?;
@@ -281,6 +370,14 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>(
remaining,
Timestamp {
source: source.into(),
timestamp_type: TimestampType::InactiveRange,
range_type: TimestampRangeType::TimeRange,
start: Some(start_date.clone()),
end: Some(start_date),
start_time: Some(first_time),
end_time: Some(second_time),
repeater: repeater.map(|(_, repeater)| repeater),
warning_delay: warning_delay.map(|(_, warning_delay)| warning_delay),
},
))
}
@@ -289,18 +386,33 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>(
fn date<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _year) = verify(digit1, |year: &OrgSource<'_>| year.len() == 4)(input)?;
) -> Res<OrgSource<'s>, Date<'s>> {
let (remaining, year) = verify(digit1, |year: &OrgSource<'_>| year.len() == 4)(input)?;
let (remaining, _) = tag("-")(remaining)?;
let (remaining, _month) = verify(digit1, |month: &OrgSource<'_>| month.len() == 2)(remaining)?;
let (remaining, month) = verify(digit1, |month: &OrgSource<'_>| month.len() == 2)(remaining)?;
let (remaining, _) = tag("-")(remaining)?;
let (remaining, _day_of_month) = verify(digit1, |day_of_month: &OrgSource<'_>| {
let (remaining, day_of_month) = verify(digit1, |day_of_month: &OrgSource<'_>| {
day_of_month.len() == 2
})(remaining)?;
let (remaining, _dayname) =
let (remaining, day_name) =
opt(tuple((space1, parser_with_context!(dayname)(context))))(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
let year = Year::new(Into::<&str>::into(year))
.expect("TODO: I should be able to return CustomError from nom parsers.");
let month = Month::new(Into::<&str>::into(month))
.expect("TODO: I should be able to return CustomError from nom parsers.");
let day_of_month = DayOfMonth::new(Into::<&str>::into(day_of_month))
.expect("TODO: I should be able to return CustomError from nom parsers.");
let date = Date::new(
year,
month,
day_of_month,
day_name.map(|(_, day_name)| Into::<&str>::into(day_name)),
)
.expect("TODO: I should be able to return CustomError from nom parsers.");
Ok((remaining, date))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -335,20 +447,39 @@ fn dayname_end<'b, 'g, 'r, 's>(
}))(input)
}
const fn time<'c>(
allow_rest: bool,
) -> impl for<'b, 'g, 'r, 's> Fn(RefContext<'b, 'g, 'r, 's>, OrgSource<'s>) -> Res<OrgSource<'s>, Time<'s>>
{
move |context, input| _time(context, input, allow_rest)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn time<'b, 'g, 'r, 's>(
fn _time<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _hour) = verify(digit1, |hour: &OrgSource<'_>| {
allow_rest: bool,
) -> Res<OrgSource<'s>, Time<'s>> {
let (remaining, hour) = verify(digit1, |hour: &OrgSource<'_>| {
hour.len() >= 1 && hour.len() <= 2
})(input)?;
let (remaining, _) = tag(":")(remaining)?;
let (remaining, _minute) =
let (remaining, minute) =
verify(digit1, |minute: &OrgSource<'_>| minute.len() == 2)(remaining)?;
let (remaining, _time_rest) = opt(parser_with_context!(time_rest)(context))(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
let (remaining, time_rest) = if allow_rest {
opt(parser_with_context!(time_rest)(context))(remaining)?
} else {
(remaining, None)
};
let hour = Hour::new(Into::<&str>::into(hour))
.expect("TODO: I should be able to return CustomError from nom parsers.");
let minute = Minute::new(Into::<&str>::into(minute))
.expect("TODO: I should be able to return CustomError from nom parsers.");
let time = Time::new(hour, minute, time_rest.map(Into::<&str>::into))
.expect("TODO: I should be able to return CustomError from nom parsers.");
Ok((remaining, time))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -401,8 +532,10 @@ fn time_range_rest_end<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'s>> {
// We pop off the most recent context element to get a context tree with just the active/inactive_time_rest_end exit matcher (removing this function from the exit matcher chain) because the 2nd time in the range does not end when a "-TIME" pattern is found.
let parent_node = context.get_parent().expect("Two context elements are added to the tree when adding this exit matcher, so it should be impossible for this to return None.");
let exit_contents =
recognize(tuple((tag("-"), parser_with_context!(time)(&parent_node))))(input);
let exit_contents = recognize(tuple((
tag("-"),
parser_with_context!(time(true))(&parent_node),
)))(input);
exit_contents
}
@@ -410,29 +543,66 @@ fn time_range_rest_end<'b, 'g, 'r, 's>(
fn repeater<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
) -> Res<OrgSource<'s>, Repeater> {
// + for cumulative type
// ++ for catch-up type
// .+ for restart type
let (remaining, _mark) = alt((tag("++"), tag("+"), tag(".+")))(input)?;
let (remaining, _value) = digit0(remaining)?;
let (remaining, repeater_type) = alt((
map(tag("++"), |_| RepeaterType::CatchUp),
map(tag("+"), |_| RepeaterType::Cumulative),
map(tag(".+"), |_| RepeaterType::Restart),
))(input)?;
let (remaining, value) = digit1(remaining)?;
let value = Into::<&str>::into(value)
.parse()
.expect("digit1 ensures this will parse as a number.");
// h = hour, d = day, w = week, m = month, y = year
let (remaining, _unit) = recognize(one_of("hdwmy"))(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
let (remaining, unit) = alt((
map(tag("h"), |_| TimeUnit::Hour),
map(tag("d"), |_| TimeUnit::Day),
map(tag("w"), |_| TimeUnit::Week),
map(tag("m"), |_| TimeUnit::Month),
map(tag("y"), |_| TimeUnit::Year),
))(remaining)?;
Ok((
remaining,
Repeater {
repeater_type,
value,
unit,
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn warning_delay<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
) -> Res<OrgSource<'s>, WarningDelay> {
// - for all type
// -- for first type
let (remaining, _mark) = alt((tag("--"), tag("-")))(input)?;
let (remaining, _value) = digit0(remaining)?;
let (remaining, warning_delay_type) = alt((
map(tag("--"), |_| WarningDelayType::First),
map(tag("-"), |_| WarningDelayType::All),
))(input)?;
let (remaining, value) = digit1(remaining)?;
let value = Into::<&str>::into(value)
.parse()
.expect("digit1 ensures this will parse as a number.");
// h = hour, d = day, w = week, m = month, y = year
let (remaining, _unit) = recognize(one_of("hdwmy"))(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
let (remaining, unit) = alt((
map(tag("h"), |_| TimeUnit::Hour),
map(tag("d"), |_| TimeUnit::Day),
map(tag("w"), |_| TimeUnit::Week),
map(tag("m"), |_| TimeUnit::Month),
map(tag("y"), |_| TimeUnit::Year),
))(remaining)?;
Ok((
remaining,
WarningDelay {
warning_delay_type,
value,
unit,
},
))
}

View File

@@ -1,4 +1,7 @@
use super::macros::to_ast_node;
use super::CenterBlock;
use super::QuoteBlock;
use super::SpecialBlock;
use crate::types::AngleLink;
use crate::types::BabelCall;
use crate::types::Bold;
@@ -21,7 +24,7 @@ use crate::types::ExportSnippet;
use crate::types::FixedWidthArea;
use crate::types::FootnoteDefinition;
use crate::types::FootnoteReference;
use crate::types::GreaterBlock;
use crate::types::GetStandardProperties;
use crate::types::Heading;
use crate::types::HorizontalRule;
use crate::types::InlineBabelCall;
@@ -68,7 +71,9 @@ pub enum AstNode<'r, 's> {
Paragraph(&'r Paragraph<'s>),
PlainList(&'r PlainList<'s>),
PlainListItem(&'r PlainListItem<'s>),
GreaterBlock(&'r GreaterBlock<'s>),
CenterBlock(&'r CenterBlock<'s>),
QuoteBlock(&'r QuoteBlock<'s>),
SpecialBlock(&'r SpecialBlock<'s>),
DynamicBlock(&'r DynamicBlock<'s>),
FootnoteDefinition(&'r FootnoteDefinition<'s>),
Comment(&'r Comment<'s>),
@@ -135,7 +140,9 @@ impl<'r, 's> From<&'r Element<'s>> for AstNode<'r, 's> {
match value {
Element::Paragraph(inner) => inner.into(),
Element::PlainList(inner) => inner.into(),
Element::GreaterBlock(inner) => inner.into(),
Element::CenterBlock(inner) => inner.into(),
Element::QuoteBlock(inner) => inner.into(),
Element::SpecialBlock(inner) => inner.into(),
Element::DynamicBlock(inner) => inner.into(),
Element::FootnoteDefinition(inner) => inner.into(),
Element::Comment(inner) => inner.into(),
@@ -199,7 +206,9 @@ to_ast_node!(&'r Section<'s>, AstNode::Section);
to_ast_node!(&'r Paragraph<'s>, AstNode::Paragraph);
to_ast_node!(&'r PlainList<'s>, AstNode::PlainList);
to_ast_node!(&'r PlainListItem<'s>, AstNode::PlainListItem);
to_ast_node!(&'r GreaterBlock<'s>, AstNode::GreaterBlock);
to_ast_node!(&'r CenterBlock<'s>, AstNode::CenterBlock);
to_ast_node!(&'r QuoteBlock<'s>, AstNode::QuoteBlock);
to_ast_node!(&'r SpecialBlock<'s>, AstNode::SpecialBlock);
to_ast_node!(&'r DynamicBlock<'s>, AstNode::DynamicBlock);
to_ast_node!(&'r FootnoteDefinition<'s>, AstNode::FootnoteDefinition);
to_ast_node!(&'r Comment<'s>, AstNode::Comment);
@@ -249,3 +258,68 @@ to_ast_node!(&'r Subscript<'s>, AstNode::Subscript);
to_ast_node!(&'r Superscript<'s>, AstNode::Superscript);
to_ast_node!(&'r TableCell<'s>, AstNode::TableCell);
to_ast_node!(&'r Timestamp<'s>, AstNode::Timestamp);
impl<'r, 's> GetStandardProperties<'s> for AstNode<'r, 's> {
fn get_standard_properties<'b>(&'b self) -> &'b dyn crate::types::StandardProperties<'s> {
match self {
AstNode::Document(inner) => *inner,
AstNode::Heading(inner) => *inner,
AstNode::Section(inner) => *inner,
AstNode::Paragraph(inner) => *inner,
AstNode::PlainList(inner) => *inner,
AstNode::PlainListItem(inner) => *inner,
AstNode::CenterBlock(inner) => *inner,
AstNode::QuoteBlock(inner) => *inner,
AstNode::SpecialBlock(inner) => *inner,
AstNode::DynamicBlock(inner) => *inner,
AstNode::FootnoteDefinition(inner) => *inner,
AstNode::Comment(inner) => *inner,
AstNode::Drawer(inner) => *inner,
AstNode::PropertyDrawer(inner) => *inner,
AstNode::NodeProperty(inner) => *inner,
AstNode::Table(inner) => *inner,
AstNode::TableRow(inner) => *inner,
AstNode::VerseBlock(inner) => *inner,
AstNode::CommentBlock(inner) => *inner,
AstNode::ExampleBlock(inner) => *inner,
AstNode::ExportBlock(inner) => *inner,
AstNode::SrcBlock(inner) => *inner,
AstNode::Clock(inner) => *inner,
AstNode::DiarySexp(inner) => *inner,
AstNode::Planning(inner) => *inner,
AstNode::FixedWidthArea(inner) => *inner,
AstNode::HorizontalRule(inner) => *inner,
AstNode::Keyword(inner) => *inner,
AstNode::BabelCall(inner) => *inner,
AstNode::LatexEnvironment(inner) => *inner,
AstNode::Bold(inner) => *inner,
AstNode::Italic(inner) => *inner,
AstNode::Underline(inner) => *inner,
AstNode::StrikeThrough(inner) => *inner,
AstNode::Code(inner) => *inner,
AstNode::Verbatim(inner) => *inner,
AstNode::PlainText(inner) => *inner,
AstNode::RegularLink(inner) => *inner,
AstNode::RadioLink(inner) => *inner,
AstNode::RadioTarget(inner) => *inner,
AstNode::PlainLink(inner) => *inner,
AstNode::AngleLink(inner) => *inner,
AstNode::OrgMacro(inner) => *inner,
AstNode::Entity(inner) => *inner,
AstNode::LatexFragment(inner) => *inner,
AstNode::ExportSnippet(inner) => *inner,
AstNode::FootnoteReference(inner) => *inner,
AstNode::Citation(inner) => *inner,
AstNode::CitationReference(inner) => *inner,
AstNode::InlineBabelCall(inner) => *inner,
AstNode::InlineSourceBlock(inner) => *inner,
AstNode::LineBreak(inner) => *inner,
AstNode::Target(inner) => *inner,
AstNode::StatisticsCookie(inner) => *inner,
AstNode::Subscript(inner) => *inner,
AstNode::Superscript(inner) => *inner,
AstNode::TableCell(inner) => *inner,
AstNode::Timestamp(inner) => *inner,
}
}
}

View File

@@ -1,7 +1,10 @@
use std::path::PathBuf;
use super::Element;
use super::GetStandardProperties;
use super::Object;
use super::StandardProperties;
use super::Timestamp;
pub type PriorityCookie = u8;
pub type HeadlineLevel = u16;
@@ -9,6 +12,8 @@ pub type HeadlineLevel = u16;
#[derive(Debug)]
pub struct Document<'s> {
pub source: &'s str,
pub category: Option<String>,
pub path: Option<PathBuf>,
pub zeroth_section: Option<Section<'s>>,
pub children: Vec<Heading<'s>>,
}
@@ -24,6 +29,10 @@ pub struct Heading<'s> {
pub children: Vec<DocumentElement<'s>>,
pub is_comment: bool,
pub is_archived: bool,
pub is_footnote_section: bool,
pub scheduled: Option<Timestamp<'s>>,
pub deadline: Option<Timestamp<'s>>,
pub closed: Option<Timestamp<'s>>,
}
#[derive(Debug)]
@@ -45,7 +54,7 @@ pub enum TodoKeywordType {
}
impl<'s> GetStandardProperties<'s> for DocumentElement<'s> {
fn get_standard_properties(&'s self) -> &'s dyn StandardProperties {
fn get_standard_properties<'b>(&'b self) -> &'b dyn StandardProperties<'s> {
match self {
DocumentElement::Heading(inner) => inner,
DocumentElement::Section(inner) => inner,
@@ -54,19 +63,31 @@ impl<'s> GetStandardProperties<'s> for DocumentElement<'s> {
}
impl<'s> StandardProperties<'s> for Document<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Section<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Heading<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> Heading<'s> {
pub fn get_raw_value(&self) -> String {
// TODO: I think this could just return a string slice instead of an owned string.
let title_source: String = self
.title
.iter()
.map(|obj| obj.get_standard_properties().get_source())
.collect();
title_source
}
}

View File

@@ -1,6 +1,5 @@
use super::greater_element::DynamicBlock;
use super::greater_element::FootnoteDefinition;
use super::greater_element::GreaterBlock;
use super::greater_element::PlainList;
use super::greater_element::PropertyDrawer;
use super::greater_element::Table;
@@ -19,16 +18,21 @@ use super::lesser_element::Paragraph;
use super::lesser_element::Planning;
use super::lesser_element::SrcBlock;
use super::lesser_element::VerseBlock;
use super::CenterBlock;
use super::Drawer;
use super::GetStandardProperties;
use super::QuoteBlock;
use super::SetSource;
use super::SpecialBlock;
use super::StandardProperties;
#[derive(Debug)]
pub enum Element<'s> {
Paragraph(Paragraph<'s>),
PlainList(PlainList<'s>),
GreaterBlock(GreaterBlock<'s>),
CenterBlock(CenterBlock<'s>),
QuoteBlock(QuoteBlock<'s>),
SpecialBlock(SpecialBlock<'s>),
DynamicBlock(DynamicBlock<'s>),
FootnoteDefinition(FootnoteDefinition<'s>),
Comment(Comment<'s>),
@@ -56,7 +60,9 @@ impl<'s> SetSource<'s> for Element<'s> {
match self {
Element::Paragraph(obj) => obj.source = source,
Element::PlainList(obj) => obj.source = source,
Element::GreaterBlock(obj) => obj.source = source,
Element::CenterBlock(obj) => obj.source = source,
Element::QuoteBlock(obj) => obj.source = source,
Element::SpecialBlock(obj) => obj.source = source,
Element::DynamicBlock(obj) => obj.source = source,
Element::FootnoteDefinition(obj) => obj.source = source,
Element::Comment(obj) => obj.source = source,
@@ -81,11 +87,13 @@ impl<'s> SetSource<'s> for Element<'s> {
}
impl<'s> GetStandardProperties<'s> for Element<'s> {
fn get_standard_properties(&'s self) -> &'s dyn StandardProperties {
fn get_standard_properties<'b>(&'b self) -> &'b dyn StandardProperties<'s> {
match self {
Element::Paragraph(inner) => inner,
Element::PlainList(inner) => inner,
Element::GreaterBlock(inner) => inner,
Element::CenterBlock(inner) => inner,
Element::QuoteBlock(inner) => inner,
Element::SpecialBlock(inner) => inner,
Element::DynamicBlock(inner) => inner,
Element::FootnoteDefinition(inner) => inner,
Element::Comment(inner) => inner,

View File

@@ -2,11 +2,11 @@ use super::StandardProperties;
pub trait GetStandardProperties<'s> {
// TODO: Can I eliminate this dynamic dispatch, perhaps using nominal generic structs? Low prioritiy since this is not used during parsing.
fn get_standard_properties(&'s self) -> &'s dyn StandardProperties;
fn get_standard_properties<'b>(&'b self) -> &'b dyn StandardProperties<'s>;
}
impl<'s, I: StandardProperties<'s>> GetStandardProperties<'s> for I {
fn get_standard_properties(&'s self) -> &'s dyn StandardProperties {
fn get_standard_properties<'b>(&'b self) -> &'b dyn StandardProperties<'s> {
self
}
}

View File

@@ -26,11 +26,16 @@ pub struct PlainListItem<'s> {
pub source: &'s str,
pub indentation: IndentationLevel,
pub bullet: &'s str,
pub counter: Option<PlainListItemCounter>,
pub checkbox: Option<(CheckboxType, &'s str)>,
pub tag: Vec<Object<'s>>,
pub pre_blank: PlainListItemPreBlank,
pub children: Vec<Element<'s>>,
}
pub type PlainListItemCounter = u16;
pub type PlainListItemPreBlank = u8;
#[derive(Debug)]
pub enum CheckboxType {
On,
@@ -39,7 +44,19 @@ pub enum CheckboxType {
}
#[derive(Debug)]
pub struct GreaterBlock<'s> {
pub struct CenterBlock<'s> {
pub source: &'s str,
pub children: Vec<Element<'s>>,
}
#[derive(Debug)]
pub struct QuoteBlock<'s> {
pub source: &'s str,
pub children: Vec<Element<'s>>,
}
#[derive(Debug)]
pub struct SpecialBlock<'s> {
pub source: &'s str,
pub name: &'s str,
pub parameters: Option<&'s str>,
@@ -77,6 +94,7 @@ pub struct PropertyDrawer<'s> {
#[derive(Debug)]
pub struct NodeProperty<'s> {
pub source: &'s str,
pub name: &'s str,
pub value: Option<&'s str>,
}
@@ -93,62 +111,102 @@ pub struct TableRow<'s> {
pub children: Vec<TableCell<'s>>,
}
#[derive(Debug)]
pub enum TableRowType {
Standard,
Rule,
}
impl<'s> StandardProperties<'s> for PlainList<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for PlainListItem<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for GreaterBlock<'s> {
fn get_source(&'s self) -> &'s str {
impl<'s> StandardProperties<'s> for CenterBlock<'s> {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for QuoteBlock<'s> {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for SpecialBlock<'s> {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for DynamicBlock<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for FootnoteDefinition<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Drawer<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for PropertyDrawer<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for NodeProperty<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Table<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for TableRow<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> PlainListItem<'s> {
pub fn get_checkbox(&self) -> Option<&'s str> {
self.checkbox.as_ref().map(|(_, checkbox)| *checkbox)
}
pub fn get_checkbox_type(&self) -> Option<&CheckboxType> {
self.checkbox
.as_ref()
.map(|(checkbox_type, _)| checkbox_type)
}
}
impl<'s> TableRow<'s> {
pub fn get_type(&self) -> TableRowType {
if self.children.is_empty() {
TableRowType::Rule
} else {
TableRowType::Standard
}
}
}

View File

@@ -1,6 +1,7 @@
use super::object::Object;
use super::PlainText;
use super::StandardProperties;
use super::Timestamp;
#[derive(Debug)]
pub struct Paragraph<'s> {
@@ -11,6 +12,7 @@ pub struct Paragraph<'s> {
#[derive(Debug)]
pub struct Comment<'s> {
pub source: &'s str,
pub value: Vec<&'s str>,
}
#[derive(Debug)]
@@ -72,6 +74,9 @@ pub struct DiarySexp<'s> {
#[derive(Debug)]
pub struct Planning<'s> {
pub source: &'s str,
pub scheduled: Option<Timestamp<'s>>,
pub deadline: Option<Timestamp<'s>>,
pub closed: Option<Timestamp<'s>>,
}
#[derive(Debug)]
@@ -115,93 +120,106 @@ impl<'s> Paragraph<'s> {
}
impl<'s> StandardProperties<'s> for Paragraph<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for TableCell<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Comment<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for VerseBlock<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for CommentBlock<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for ExampleBlock<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for ExportBlock<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for SrcBlock<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Clock<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for DiarySexp<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Planning<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for FixedWidthArea<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for HorizontalRule<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Keyword<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for BabelCall<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for LatexEnvironment<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> Comment<'s> {
pub fn get_value(&self) -> String {
// TODO: maybe we should handle parsing here instead of storing the parsing result in the AST since I imagine getting the value of comments won't be a common operation.
let final_size = self.value.iter().map(|line| line.len()).sum();
let mut ret = String::with_capacity(final_size);
for line in &self.value {
ret.push_str(line);
}
ret
}
}

12
src/types/macros.rs Normal file
View File

@@ -0,0 +1,12 @@
/// Write the implementation of From<> to convert a borrow of the type to an AstNode
macro_rules! to_ast_node {
($inp:ty, $enum:expr) => {
impl<'r, 's> From<$inp> for AstNode<'r, 's> {
fn from(value: $inp) -> Self {
$enum(value)
}
}
};
}
pub(crate) use to_ast_node;

View File

@@ -1,11 +1,14 @@
mod ast_node;
mod document;
mod element;
mod get_standard_properties;
mod greater_element;
mod lesser_element;
mod macros;
mod object;
mod source;
mod standard_properties;
pub(crate) use ast_node::AstNode;
pub use document::Document;
pub use document::DocumentElement;
pub use document::Heading;
@@ -15,19 +18,24 @@ pub use document::Section;
pub use document::TodoKeywordType;
pub use element::Element;
pub use get_standard_properties::GetStandardProperties;
pub use greater_element::CenterBlock;
pub use greater_element::CheckboxType;
pub use greater_element::Drawer;
pub use greater_element::DynamicBlock;
pub use greater_element::FootnoteDefinition;
pub use greater_element::GreaterBlock;
pub use greater_element::IndentationLevel;
pub use greater_element::NodeProperty;
pub use greater_element::PlainList;
pub use greater_element::PlainListItem;
pub use greater_element::PlainListItemCounter;
pub use greater_element::PlainListItemPreBlank;
pub use greater_element::PlainListType;
pub use greater_element::PropertyDrawer;
pub use greater_element::QuoteBlock;
pub use greater_element::SpecialBlock;
pub use greater_element::Table;
pub use greater_element::TableRow;
pub use greater_element::TableRowType;
pub use lesser_element::BabelCall;
pub use lesser_element::Clock;
pub use lesser_element::Comment;
@@ -49,14 +57,23 @@ pub use object::Bold;
pub use object::Citation;
pub use object::CitationReference;
pub use object::Code;
pub use object::Date;
pub use object::DayOfMonth;
pub use object::DayOfMonthInner;
pub use object::Entity;
pub use object::ExportSnippet;
pub use object::FootnoteReference;
pub use object::Hour;
pub use object::HourInner;
pub use object::InlineBabelCall;
pub use object::InlineSourceBlock;
pub use object::Italic;
pub use object::LatexFragment;
pub use object::LineBreak;
pub use object::Minute;
pub use object::MinuteInner;
pub use object::Month;
pub use object::MonthInner;
pub use object::Object;
pub use object::OrgMacro;
pub use object::PlainLink;
@@ -64,13 +81,24 @@ pub use object::PlainText;
pub use object::RadioLink;
pub use object::RadioTarget;
pub use object::RegularLink;
pub use object::Repeater;
pub use object::RepeaterType;
pub use object::RepeaterWarningDelayValueType;
pub use object::StatisticsCookie;
pub use object::StrikeThrough;
pub use object::Subscript;
pub use object::Superscript;
pub use object::Target;
pub use object::Time;
pub use object::TimeUnit;
pub use object::Timestamp;
pub use object::TimestampRangeType;
pub use object::TimestampType;
pub use object::Underline;
pub use object::Verbatim;
pub use object::WarningDelay;
pub use object::WarningDelayType;
pub use object::Year;
pub use object::YearInner;
pub(crate) use source::SetSource;
pub use standard_properties::StandardProperties;

View File

@@ -1,6 +1,7 @@
use super::GetStandardProperties;
use super::StandardProperties;
// TODO: Why did we make Object implement PartialEq again? Was it just for tests?
#[derive(Debug, PartialEq)]
pub enum Object<'s> {
Bold(Bold<'s>),
@@ -181,13 +182,260 @@ pub struct Superscript<'s> {
pub source: &'s str,
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone)]
pub struct Timestamp<'s> {
pub source: &'s str,
pub timestamp_type: TimestampType,
pub range_type: TimestampRangeType,
pub start: Option<Date<'s>>,
pub end: Option<Date<'s>>,
pub start_time: Option<Time<'s>>,
pub end_time: Option<Time<'s>>,
pub repeater: Option<Repeater>,
pub warning_delay: Option<WarningDelay>,
}
#[derive(Debug, PartialEq, Clone)]
pub enum TimestampType {
Diary,
Active,
Inactive,
ActiveRange,
InactiveRange,
}
#[derive(Debug, PartialEq, Clone)]
pub enum TimestampRangeType {
None,
DateRange,
TimeRange,
}
pub type YearInner = u16;
pub type MonthInner = u8;
pub type DayOfMonthInner = u8;
pub type HourInner = u8;
pub type MinuteInner = u8;
#[derive(Debug, PartialEq, Clone)]
pub struct Year(YearInner);
#[derive(Debug, PartialEq, Clone)]
pub struct Month(MonthInner);
#[derive(Debug, PartialEq, Clone)]
pub struct DayOfMonth(DayOfMonthInner);
#[derive(Debug, PartialEq, Clone)]
pub struct Hour(HourInner);
#[derive(Debug, PartialEq, Clone)]
pub struct Minute(MinuteInner);
impl Year {
// TODO: Make a real error type instead of a boxed any error.
pub fn new<'s>(source: &'s str) -> Result<Self, Box<dyn std::error::Error>> {
let year = source.parse::<YearInner>()?;
Ok(Year(year))
}
pub fn get_value(&self) -> YearInner {
self.0
}
}
impl Month {
// TODO: Make a real error type instead of a boxed any error.
pub fn new<'s>(source: &'s str) -> Result<Self, Box<dyn std::error::Error>> {
let month = source.parse::<MonthInner>()?;
if month < 1 || month > 12 {
Err("Month exceeds possible range.")?;
}
Ok(Month(month))
}
pub fn get_value(&self) -> MonthInner {
self.0
}
}
impl DayOfMonth {
// TODO: Make a real error type instead of a boxed any error.
pub fn new<'s>(source: &'s str) -> Result<Self, Box<dyn std::error::Error>> {
let day_of_month = source.parse::<DayOfMonthInner>()?;
if day_of_month < 1 || day_of_month > 31 {
Err("Day of month exceeds possible range.")?;
}
Ok(DayOfMonth(day_of_month))
}
pub fn get_value(&self) -> DayOfMonthInner {
self.0
}
}
impl Hour {
// TODO: Make a real error type instead of a boxed any error.
pub fn new<'s>(source: &'s str) -> Result<Self, Box<dyn std::error::Error>> {
let hour = source.parse::<HourInner>()?;
if hour > 23 {
Err("Hour exceeds possible range.")?;
}
Ok(Hour(hour))
}
pub fn get_value(&self) -> HourInner {
self.0
}
}
impl Minute {
// TODO: Make a real error type instead of a boxed any error.
pub fn new<'s>(source: &'s str) -> Result<Self, Box<dyn std::error::Error>> {
let minute = source.parse::<MinuteInner>()?;
if minute > 59 {
Err("Minute exceeds possible range.")?;
}
Ok(Minute(minute))
}
pub fn get_value(&self) -> MinuteInner {
self.0
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Date<'s> {
year: Year,
month: Month,
day_of_month: DayOfMonth,
day_name: Option<&'s str>,
}
impl<'s> Date<'s> {
// TODO: Make a real error type instead of a boxed any error.
pub fn new(
year: Year,
month: Month,
day_of_month: DayOfMonth,
day_name: Option<&'s str>,
) -> Result<Self, Box<dyn std::error::Error>> {
// TODO: Does org-mode support non-gregorian calendars?
// TODO: Do I want to validate leap year?
match (month.get_value(), day_of_month.get_value()) {
(1, 1..=31) => {}
(2, 1..=29) => {}
(3, 1..=31) => {}
(4, 1..=30) => {}
(5, 1..=31) => {}
(6, 1..=30) => {}
(7, 1..=31) => {}
(8, 1..=31) => {}
(9, 1..=30) => {}
(10, 1..=31) => {}
(11, 1..=30) => {}
(12, 1..=31) => {}
_ => Err("Invalid day of month for the month.")?,
};
Ok(Date {
year,
month,
day_of_month,
day_name,
})
}
pub fn get_year(&self) -> &Year {
&self.year
}
pub fn get_month(&self) -> &Month {
&self.month
}
pub fn get_day_of_month(&self) -> &DayOfMonth {
&self.day_of_month
}
pub fn get_day_name(&self) -> Option<&'s str> {
self.day_name
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Time<'s> {
hour: Hour,
minute: Minute,
postfix: Option<&'s str>,
}
impl<'s> Time<'s> {
// TODO: Make a real error type instead of a boxed any error.
pub fn new(
hour: Hour,
minute: Minute,
postfix: Option<&'s str>,
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Time {
hour,
minute,
postfix,
})
}
pub fn get_hour(&self) -> &Hour {
&self.hour
}
pub fn get_minute(&self) -> &Minute {
&self.minute
}
pub fn get_postfix(&self) -> Option<&'s str> {
self.postfix
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum RepeaterType {
Cumulative,
CatchUp,
Restart,
}
#[derive(Debug, PartialEq, Clone)]
pub enum WarningDelayType {
All,
First,
}
#[derive(Debug, PartialEq, Clone)]
pub enum TimeUnit {
Hour,
Day,
Week,
Month,
Year,
}
pub type RepeaterWarningDelayValueType = u16;
#[derive(Debug, PartialEq, Clone)]
pub struct Repeater {
pub repeater_type: RepeaterType,
pub value: RepeaterWarningDelayValueType,
pub unit: TimeUnit,
}
#[derive(Debug, PartialEq, Clone)]
pub struct WarningDelay {
pub warning_delay_type: WarningDelayType,
pub value: RepeaterWarningDelayValueType,
pub unit: TimeUnit,
}
impl<'s> GetStandardProperties<'s> for Object<'s> {
fn get_standard_properties(&'s self) -> &'s dyn StandardProperties {
fn get_standard_properties<'b>(&'b self) -> &'b dyn StandardProperties<'s> {
match self {
Object::Bold(inner) => inner,
Object::Italic(inner) => inner,
@@ -221,163 +469,169 @@ impl<'s> GetStandardProperties<'s> for Object<'s> {
}
impl<'s> StandardProperties<'s> for Bold<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Italic<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Underline<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for StrikeThrough<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Code<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Verbatim<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for RegularLink<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for RadioLink<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for RadioTarget<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for PlainLink<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for AngleLink<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for OrgMacro<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Entity<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for LatexFragment<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for ExportSnippet<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for FootnoteReference<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Citation<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for CitationReference<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for InlineBabelCall<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for InlineSourceBlock<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for LineBreak<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Target<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for StatisticsCookie<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Subscript<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Superscript<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for Timestamp<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for PlainText<'s> {
fn get_source(&'s self) -> &'s str {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> Timestamp<'s> {
pub fn get_raw_value(&self) -> &'s str {
self.source.trim_end()
}
}

View File

@@ -3,7 +3,7 @@ pub trait StandardProperties<'s> {
/// Get the slice of the entire AST node.
///
/// This corresponds to :begin to :end in upstream org-mode's standard properties.
fn get_source(&'s self) -> &'s str;
fn get_source<'b>(&'b self) -> &'s str;
// Get the slice of the AST node's contents.
//

View File

@@ -1,2 +1,5 @@
#[cfg(feature = "compare")]
#![cfg(feature = "compare")]
#[feature(exit_status_error)]
include!(concat!(env!("OUT_DIR"), "/tests.rs"));

View File

@@ -1,7 +1,66 @@
// TODO: Investigate writing a proc macro to make specifying these combinations easier. For example, currently I am only setting 1 setting per test to keep the repetition reasonable when I should be mixing all the different combinations.
{expect_fail}
#[test]
fn {name}() -> Result<(), Box<dyn std::error::Error>> {{
fn autogen_default_{name}() -> Result<(), Box<dyn std::error::Error>> {{
let org_path = "{path}";
let org_contents = std::fs::read_to_string(org_path).expect("Read org file.");
organic::compare::run_anonymous_compare(org_contents.as_str())?;
Ok(())
}}
{expect_fail}
#[test]
fn autogen_la_{name}() -> Result<(), Box<dyn std::error::Error>> {{
let org_path = "{path}";
let org_contents = std::fs::read_to_string(org_path).expect("Read org file.");
let global_settings = {{
let mut global_settings = organic::settings::GlobalSettings::default();
global_settings.list_allow_alphabetical = true;
global_settings
}};
organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings)?;
Ok(())
}}
{expect_fail}
#[test]
fn autogen_t1_{name}() -> Result<(), Box<dyn std::error::Error>> {{
let org_path = "{path}";
let org_contents = std::fs::read_to_string(org_path).expect("Read org file.");
let global_settings = {{
let mut global_settings = organic::settings::GlobalSettings::default();
global_settings.tab_width = 1;
global_settings
}};
organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings)?;
Ok(())
}}
{expect_fail}
#[test]
fn autogen_t16_{name}() -> Result<(), Box<dyn std::error::Error>> {{
let org_path = "{path}";
let org_contents = std::fs::read_to_string(org_path).expect("Read org file.");
let global_settings = {{
let mut global_settings = organic::settings::GlobalSettings::default();
global_settings.tab_width = 16;
global_settings
}};
organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings)?;
Ok(())
}}
{expect_fail}
#[test]
fn autogen_odd_{name}() -> Result<(), Box<dyn std::error::Error>> {{
let org_path = "{path}";
let org_contents = std::fs::read_to_string(org_path).expect("Read org file.");
let global_settings = {{
let mut global_settings = organic::settings::GlobalSettings::default();
global_settings.odd_levels_only = organic::settings::HeadlineLevelFilter::Odd;
global_settings
}};
organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings)?;
Ok(())
}}