193 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
Tom Alexander
d7a36c8aca Merge branch 'plain_list_properties'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-29 13:05:03 -04:00
Tom Alexander
f820e27b17 Compare plain list type in diff.rs. 2023-09-29 13:03:25 -04:00
Tom Alexander
a4b1d462c3 Parse out the plain list type. 2023-09-29 12:49:10 -04:00
Tom Alexander
1b7326eafe Use static strings for CustomError.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
This resulted in a 20% speedup.
2023-09-29 12:02:26 -04:00
Tom Alexander
90433aa55f Update callgrind script to build with optimizations. 2023-09-29 12:02:25 -04:00
Tom Alexander
a5b4eb40f6 Merge branch 'reduce_heap_iter'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-27 19:48:07 -04:00
Tom Alexander
48d550e1fc Remove old implementation of iteration. 2023-09-27 19:45:40 -04:00
Tom Alexander
9ce042d5b6 Replace old iteration with new iteration. 2023-09-27 19:44:06 -04:00
Tom Alexander
8784da5179 Implement all ast node iteration. 2023-09-27 19:30:21 -04:00
Tom Alexander
875a50ae46 Finish implementing AstNodeIter for all types. 2023-09-27 19:02:33 -04:00
Tom Alexander
c4ea3fbf88 Implement the rest of the elements. 2023-09-27 18:55:50 -04:00
Tom Alexander
95fa834420 Switch to using the multi field macro for document and heading. 2023-09-27 18:38:51 -04:00
Tom Alexander
32a7ce3f36 Implement a macro for iterators with multiple fields. 2023-09-27 18:36:29 -04:00
Tom Alexander
d8c52568db Add PlainListItem to ast nodes. 2023-09-27 18:21:42 -04:00
Tom Alexander
c5be75ee8d Implement DocumentIter and SectionIter. 2023-09-27 18:05:53 -04:00
Tom Alexander
282417ee94 Implementing HeadingIter but I do not think it can be generic enough for a macro.
Hopefully most types won't need so much care.
2023-09-27 18:00:30 -04:00
Tom Alexander
ab46a9e5c6 Ran into issue with heading, naming this type is going to be a nightmare. 2023-09-27 15:56:45 -04:00
Tom Alexander
4359fc9266 Introduce a macro for empty iterators. 2023-09-27 15:47:01 -04:00
Tom Alexander
7419b75d76 Implement empty iterator for types with no ast node children. 2023-09-27 15:38:33 -04:00
Tom Alexander
e4cfc296e5 Introduce macro to simplify this. 2023-09-27 15:28:43 -04:00
Tom Alexander
9a1d91ae45 Manual implementation of BoldIter. 2023-09-27 15:17:56 -04:00
Tom Alexander
df5d699a39 Implement Into for AstNode. 2023-09-27 15:07:26 -04:00
Tom Alexander
9111408d83 Introduce AstNode and AstNodeIter enums. 2023-09-27 14:24:08 -04:00
Tom Alexander
35f058a354 Starting a new iteration implementation.
This implementation will reduce the use of heap by elimininating Box<> from the individual iterators but it will still need heap for maintaining a vector of iterators from nodes.
2023-09-27 13:48:17 -04:00
Tom Alexander
dd91e506bd Merge branch 'scan_optimization'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-24 03:11:46 -04:00
Tom Alexander
cd781a7dcf Add simple test to prove the scan for in-buffer settings is still working. 2023-09-24 03:09:51 -04:00
Tom Alexander
8cd0e4ec63 Optimize scanning for in-buffer settings by scanning forward for possible keywords.
Previously we stepped through the document character by character which involved a lot of extra processing inside OrgSource. By scanning for possible keywords, we can skip many of the intermediate steps.
2023-09-24 02:58:32 -04:00
Tom Alexander
f9460b88d7 Add a TODO for a performance optimization. 2023-09-24 01:59:26 -04:00
Tom Alexander
0b2a5f4fbf Change all runtime asserts in private functions to debug_assert.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
These functions aren't exposed to the public so we can confidently say that if they work in dev then they will work in production. Removing these asserts theoretically should result in a speedup.
2023-09-23 21:17:58 -04:00
Tom Alexander
6097e4df18 Merge branch 'standard_properties'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-23 21:12:40 -04:00
Tom Alexander
d5b1014fe4 Unify the standard properties checks in diff.
Instead of copy+pasting them into each compare function, we now call a shared function from a handful of places.
2023-09-23 21:05:56 -04:00
Tom Alexander
dd8a8207ce Move assert bounds for elements and objects (except PlainText) to the compare element/object functions. 2023-09-23 19:35:12 -04:00
Tom Alexander
b4c985071c Add a GetStandardProperties trait. 2023-09-23 19:13:01 -04:00
Tom Alexander
d4f27ef297 Remove only use of Source trait. 2023-09-23 17:59:13 -04:00
Tom Alexander
f25246556c Rename the existing StandardProperties struct to EmacsStandardProperties. 2023-09-23 17:44:54 -04:00
Tom Alexander
3fe56e9aa3 Implement StandardProperties for all the AST nodes and restrict the Source trait to this crate.
Currently this is a copy of the Source trait but it will grow to more functions. The Source trait is restricted to this crate in anticipation of its removal in favor of StandardProperties.
2023-09-23 17:42:27 -04:00
Tom Alexander
f180412ff3 Introduce a StandardProperties trait. 2023-09-23 17:33:46 -04:00
Tom Alexander
f0e28206ff Add a supported versions section to the README.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-23 14:54:56 -04:00
Tom Alexander
1f64e289a2 Add TODOs for all of the properties that need to be compared.
Some checks failed
rust-foreign-document-test Build rust-foreign-document-test has started
rustfmt Build rustfmt has failed
rust-build Build rust-build has failed
rust-test Build rust-test has failed
2023-09-23 14:46:36 -04:00
Tom Alexander
f7690ff64b Remove an allocation for lesser block end. 2023-09-22 00:55:10 -04:00
Tom Alexander
bd5e50d558 Remove TODO.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
I tested and we cannot nest different types of dynamic blocks.
2023-09-21 23:58:41 -04:00
Tom Alexander
de87b7df93 Publish version 0.1.8.
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-21 23:47:48 -04:00
Tom Alexander
a267d13fd7 Merge branch 'worg'
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-21 23:38:07 -04:00
Tom Alexander
a29973a110 Add a "format" makefile target.
All checks were successful
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-21 23:20:22 -04:00
Tom Alexander
31c782499e Do not match text markup end with empty contents. 2023-09-21 23:20:21 -04:00
Tom Alexander
b7c7057095 Add a test for double tilde. 2023-09-21 22:52:21 -04:00
Tom Alexander
49e3c90a3a Add a test showing a text markup condition we are not handling and significantly reduce allocations by using references for the captured marker for text markup. 2023-09-21 22:35:09 -04:00
Tom Alexander
129228c5c5 Require either eof or whitespace to line ending for valueless items.
Some checks failed
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-21 22:06:30 -04:00
Tom Alexander
f0a7493a89 Support blank lines for descriptive list with empty value before final list item. 2023-09-21 22:03:21 -04:00
Tom Alexander
dc5695ec9f Update description list test to ensure we match blank values properly for both final and non-final items. 2023-09-21 21:47:42 -04:00
Tom Alexander
4ff62fbfae Support backslash as a post character for text markup. 2023-09-21 21:25:33 -04:00
Tom Alexander
c892d406c3 Do not parse the tag for a plain list item if it is an ordered plain list item. 2023-09-21 20:58:03 -04:00
Tom Alexander
1a41cfc6c7 Support detecting line indentation when checking for contentless plain list items. 2023-09-21 20:08:04 -04:00
Tom Alexander
4f34ab9089 Support subscript/superscript wrapped in parenthesis. 2023-09-21 19:21:47 -04:00
Tom Alexander
9b2348c0ef Allow matched parenthesis inside plain links. 2023-09-21 18:51:11 -04:00
Tom Alexander
5716cbccea Remove unnecessary peak. 2023-09-21 16:34:24 -04:00
Tom Alexander
124cd50243 Add more test cases. 2023-09-21 15:36:55 -04:00
Tom Alexander
bac5d6e1d9 Add a test for parenthesis in regular links for good measure.
We are properly handling this currently, but it is good to have more test coverage.
2023-09-21 14:34:51 -04:00
Tom Alexander
ba15999534 Add a test showing we are not handling parenthesis in links properly.
Some checks failed
rust-test Build rust-test has failed
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-21 14:31:13 -04:00
Tom Alexander
61c3e6c10e Require table formulas have a value. 2023-09-21 14:12:18 -04:00
Tom Alexander
a7e130838d Add a test showing that table formulas with no value do not get associated with the table. 2023-09-21 14:10:20 -04:00
Tom Alexander
853adadf91 Do not allow unescaped opening bracket in path for link. 2023-09-21 13:41:48 -04:00
Tom Alexander
7b61329889 Add test showing we are not parsing links wrapped in brackets correctly.
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-20 03:48:22 -04:00
Tom Alexander
9bcfb2f1da Decide headline nesting by star count, not headline level.
It is possible to have two headlines that have the same level but different star counts when set to Odd because of rounding. Deciding nesting by star count instead of headline level avoids this issue.
2023-09-20 03:22:25 -04:00
Tom Alexander
4c8d9a3063 Do not require a colon to close dynamic blocks. 2023-09-20 02:37:26 -04:00
Tom Alexander
48cb3c4a02 Move the post-colon check into the item_tag_divider parser. 2023-09-19 23:57:40 -04:00
Tom Alexander
9e60ff6683 Support rematching on italic, underline, and strike-through. 2023-09-19 23:25:49 -04:00
Tom Alexander
c1de001786 Require a space after colon instead of tab for fixed width area. 2023-09-19 20:22:29 -04:00
Tom Alexander
716af5bb45 Update org-mode version.
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
2023-09-16 14:45:52 -04:00
Tom Alexander
6137a46231 Default to the release-lto profile for running compare in docker.
Since we're using docker volumes to cache the build, the extra build cost will only be paid once but the extra speed will be nice while investigating.
2023-09-16 14:15:19 -04:00
Tom Alexander
bdd04f4d5c Do not allow '<' as a pre-character for text-markup but do allow start of file. 2023-09-16 14:06:31 -04:00
Tom Alexander
36bdc54703 Update bisect script to work with any depth relative path for setupfile.
This also switches to using stdin rather than writing the file slices to the filesystem.
2023-09-16 13:34:33 -04:00
Tom Alexander
3031b6edd4 Support arbitrary relative paths for setupfiles in run_docker_compare script. 2023-09-16 12:51:38 -04:00
Tom Alexander
1a704dd312 Honor the odd startup setting from org-mode files.
Some checks failed
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-15 22:54:49 -04:00
Tom Alexander
a74ea730f4 Read the odd startup option from org-mode files. 2023-09-15 22:31:15 -04:00
Tom Alexander
8450785186 Add test showing we are not handling the odd startup option for headline depth.
Some checks failed
rust-test Build rust-test has failed
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-15 22:08:42 -04:00
Tom Alexander
d443dbd468 Introduce the tab_width setting and give tabs a greater value when counting indentation level. 2023-09-15 21:59:48 -04:00
Tom Alexander
c9ce32c881 Remve redundant org_spaces functions.
Turns out the nom space0/space1 parsers accept tab characters already.
2023-09-15 21:28:40 -04:00
Tom Alexander
85454a0a27 Fix footnote reference function label matcher.
Previously when a label started with a number but contained other characters, this parser would fail because it would not match the entire label.
2023-09-15 21:14:44 -04:00
Tom Alexander
fdebf6dec5 Delete already solved TODO. 2023-09-15 21:08:52 -04:00
Tom Alexander
444d6758aa Handle leading blank lines in greater blocks.
Some checks failed
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-15 21:03:35 -04:00
Tom Alexander
6c7203410e Add a test showing we're not handling leading blank lines in greater blocks. 2023-09-15 17:02:41 -04:00
Tom Alexander
bfe67b1f75 Parse plain list item checkboxes. 2023-09-15 16:09:57 -04:00
Tom Alexander
fd41ad9c29 Pretend dos line endings do not exist. 2023-09-15 14:13:17 -04:00
Tom Alexander
7f751d4f28 Allow no digit in repeater in timestamp. 2023-09-15 13:12:54 -04:00
Tom Alexander
52a4dab67c Use the timestamp parser in planning.
Previously we did not support inactive timestamps in planning. This fixes that.
2023-09-15 12:45:19 -04:00
Tom Alexander
3d86e75059 Always match the entire entity name.
Some checks failed
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-14 04:29:50 -04:00
Tom Alexander
ca6fdf1924 Support different cases in radio links. 2023-09-14 04:04:21 -04:00
Tom Alexander
66d16d89ed Support interchangeable whitespace in re-matching plain text. 2023-09-14 04:00:34 -04:00
Tom Alexander
ee5e0698b1 Add an optimization idea. 2023-09-14 03:25:12 -04:00
Tom Alexander
22681b6a58 Support trailing whitespace in fixed-width areas. 2023-09-14 03:20:44 -04:00
Tom Alexander
876d33239e Allow any character to be escaped in the path for links. 2023-09-14 03:05:11 -04:00
Tom Alexander
87941271a4 Handle headlines with trailing spaces without tags. 2023-09-14 02:43:40 -04:00
Tom Alexander
32b19d68d0 Support todo keywords with fast access. 2023-09-14 02:24:06 -04:00
Tom Alexander
830097b0a9 Add a test showing we are not handling fast access states in todo keywords. 2023-09-14 02:18:49 -04:00
Tom Alexander
44e9f708c9 Handle the possibility of a title-less headline. 2023-09-14 02:01:24 -04:00
Tom Alexander
fc4ff97c14 Add a test showing we are not handling empty headlines properly. 2023-09-14 00:50:31 -04:00
Tom Alexander
33372429dd Add a config option for org-list-allow-alphabetical.
This fixes an issue where lines in a paragraph were incorrectly getting identified as lists because I had defaulted to assuming alphabetical bullets were allowed.
2023-09-14 00:27:54 -04:00
Tom Alexander
ac0db64081 Add cargo directive to rebuild the auto-generated tests when files under org_mode_samples get updated.
Some checks failed
rust-test Build rust-test has failed
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-13 21:28:44 -04:00
Tom Alexander
b8a4876779 Disable auto-aligning tables when Emacs loads Org-mode.
Emacs will auto-align tables when org-mode is loaded if the document contains "#+STARTUP: align". Since Organic is just a parser, it has no business editing the input it receives so we are disabling this auto-align in Emacs to make the tests work properly.
2023-09-13 21:02:38 -04:00
Tom Alexander
925c42c8fb Add test showing we currently are letting emacs align tables at startup. 2023-09-13 21:02:38 -04:00
Tom Alexander
7d4100d956 Add worg to the foreign document test.
A lot of the documents are failing so there are going to be a lot of bug fixes in this branch.
2023-09-13 20:10:50 -04:00
Tom Alexander
53d90a2949 Update the README to have instructions on running the tests and development programs.
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
2023-09-13 20:10:14 -04:00
108 changed files with 5831 additions and 2110 deletions

View File

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

View File

@@ -33,6 +33,10 @@ release:
clean: clean:
> cargo clean > cargo clean
.PHONY: format
format:
> $(MAKE) -C docker/cargo_fmt run
.PHONY: test .PHONY: test
test: test:
> cargo test --no-default-features --features compare --no-fail-fast --lib --test test_loader -- --test-threads $(TESTJOBS) > cargo test --no-default-features --features compare --no-fail-fast --lib --test test_loader -- --test-threads $(TESTJOBS)

View File

@@ -2,12 +2,84 @@
Organic is an emacs-less implementation of an [org-mode](https://orgmode.org/) parser. Organic is an emacs-less implementation of an [org-mode](https://orgmode.org/) parser.
## Project Status ## Project Status
This project is a personal learning project to grow my experience in [rust](https://www.rust-lang.org/). It is under development and at this time I would not recommend anyone use this code. The goal is to turn this into a project others can use, at which point more information will appear in this README. This project is still under HEAVY development. While the version remains v0.1.x the API will be changing often. Once we hit v0.2.x we will start following semver.
Currently, the parser is able to correctly identify the start/end bounds of all the org-mode objects and elements (except table.el tables, org-mode tables are supported) but many of the interior properties are not yet populated.
### Project Goals
- We aim to provide perfect parity with the emacs org-mode parser. In that regard, any document that parses differently between Emacs and Organic is considered a bug.
- The parser should be fast. We're not doing anything special, but since this is written in Rust and natively compiled we should be able to beat the existing parsers.
- The parser should have minimal dependencies. This should reduce effort w.r.t.: security audits, legal compliance, portability.
- The parser should be usable everywhere. In the interest of getting org-mode used in as many places as possible, this parser should be usable by everyone everywhere. This means:
- It must have a permissive license for use in proprietary code bases.
- We will investigate compiling to WASM. This is an important goal of the project and will definitely happen, but only after the parser has a more stable API.
- We will investigate compiling to a C library for native linking to other code. This is more of a maybe-goal for the project.
### Project Non-Goals
- This project will not include an elisp engine since that would drastically increase the complexity of the code. Any features requiring an elisp engine will not be implemented (for example, Emacs supports embedded eval expressions in documents but this parser will never support that).
- This project is exclusively an org-mode **parser**. This limits its scope to roughly the output of `(org-element-parse-buffer)`. It will not render org-mode documents in other formats like HTML or LaTeX.
### Project Maybe-Goals
- table.el support. Currently we support org-mode tables but org-mode also allows table.el tables. So far, their use in org-mode documents seems rather uncommon so this is a low-priority feature.
- Document editing support. I do not anticipate any advanced editing features to make editing ergonomic, but it should be relatively easy to be able to parse an org-mode document and serialize it back into org-mode. This would enable cool features to be built on top of the library like auto-formatters. To accomplish this feature, We'd have to capture all of the various separators and whitespace that we are currently simply throwing away. This would add many additional fields to the parsed structs and it would add more noise to the parsers themselves, so I do not want to approach this feature until the parser is more complete since it would make modifications and refactoring more difficult.
### Supported Versions
This project targets the version of Emacs and Org-mode that are built into the [organic-test docker image](docker/organic_test/Dockerfile). This is newer than the version of Org-mode that shipped with Emacs 29.1. The parser itself does not depend on Emacs or Org-mode though, so this only matters for development purposes when running the automated tests that compare against upstream Org-mode.
## Using this library
TODO: Add section on using Organic as a library (which is the intended use for this project). This will be added when we have a bit more API stability since currently the library is under heavy development.
## Development
### The parse binary
This program takes org-mode input either streamed in on stdin or as paths to files passed in as arguments. It then parses them using Organic and dumps the result to stdout. This program is intended solely as a development tool. Examples:
```bash
cat /foo/bar.org | cargo run --bin parse
```
```bash
cargo build --profile release-lto
./target/release-lto/parse /foo/bar.org /lorem/ipsum.org
```
### The compare binary
This program takes org-mode input either streamed in on stdin or as paths to files passed in as arguments. It then parses them using Organic and the official Emacs Org-mode parser and compares the parse result. This program is intended solely as a development tool. Since org-mode is a moving target, it is recommended that you run this through docker since we pin the version of org-mode to a specific revision. Examples:
```bash
cat /foo/bar.org | ./scripts/run_docker_compare.bash
```
```bash
./scripts/run_docker_compare.bash /foo/bar.org /lorem/ipsum.org
```
Not recommended since it is not through docker:
```bash
cat /foo/bar.org | cargo run --features compare --bin compare
```
```bash
cargo build --profile release-lto --features compare
./target/release-lto/compare /foo/bar.org /lorem/ipsum.org
```
## Running the tests
There are three levels of tests for this repository: the standard tests, the autogenerated tests, and the foreign document tests.
### The standard tests
These are regular hand-written rust tests. These can be run with:
```bash
make unittest
```
### The auto-generated tests
These tests are automatically generated from the files in the `org_mode_samples` directory and they are still integrated with the rust/cargo testing framework. For each org-mode document in that folder, a test is generated that will parse the document with both Organic and the official Emacs Org-mode parser and then it will compare the parse results. Any deviation is considered a failure. Since org-mode is a moving target, it is recommended that you run these tests inside docker since the `organic-test` docker image is pinned to a specific revision of org-mode. These can be run with:
```bash
make dockertest
```
### The foreign document tests
These tests function the same as the auto-generated tests except they are **not** integrated with the rust/cargo testing framework and they involve comparing the parse of org-mode documents that live outside this repository. This allows us to test against a far greater variety of org-mode input documents without pulling massive sets of org-mode documents into this repository. The recommended way to run these tests is still through docker because it pins org-mode and the test documents to specific git revisions. These can be run with:
```bash
make foreign_document_test
```
## License ## License
This project is released under the public-domain-equivalent [0BSD license](https://www.tldrlegal.com/license/bsd-0-clause-license). This license puts no restrictions on the use of this code (you do not even have to include the copyright notice or license text when using it). HOWEVER, this project has a couple permissively licensed dependencies which do require their copyright notices and/or license texts to be included. I am not a lawyer and this is not legal advice but it is my layperson's understanding that if you distribute a binary with this library linked in, you will need to abide by their terms since their code will also be linked in your binary. I try to keep the dependencies to a minimum and the most restrictive dependency I will ever include is a permissively licensed one. This project is released under the public-domain-equivalent [0BSD license](https://www.tldrlegal.com/license/bsd-0-clause-license), however, this project has a couple permissively licensed non-public-domain-equivalent dependencies which require their copyright notices and/or license texts to be included. I am not a lawyer and this is not legal advice but it is my layperson's understanding that if you distribute a binary statically linking this library, you will need to abide by their terms since their code will also be linked in your binary.

View File

@@ -16,7 +16,8 @@ fn main() {
let destination = Path::new(&out_dir).join("tests.rs"); let destination = Path::new(&out_dir).join("tests.rs");
let mut test_file = File::create(&destination).unwrap(); let mut test_file = File::create(&destination).unwrap();
write_header(&mut test_file); // Re-generate the tests if any org-mode files change
println!("cargo:rerun-if-changed=org_mode_samples");
let test_files = WalkDir::new("org_mode_samples") let test_files = WalkDir::new("org_mode_samples")
.into_iter() .into_iter()
@@ -51,28 +52,15 @@ fn write_test(test_file: &mut File, test: &walkdir::DirEntry) {
.strip_suffix(".org") .strip_suffix(".org")
.expect("Should have .org extension") .expect("Should have .org extension")
.replace("/", "_"); .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!( write!(
test_file, test_file,
include_str!("./tests/test_template"), include_str!("./tests/test_template"),
name = test_name, name = test_name,
path = test.path().display() path = test.path().display(),
) expect_fail = is_expect_fail(test_name.as_str())
.unwrap(); .map(|_| "#[ignore]\n")
} .unwrap_or("")
#[cfg(feature = "compare")]
fn write_header(test_file: &mut File) {
write!(
test_file,
r#"
#[feature(exit_status_error)]
"#
) )
.unwrap(); .unwrap();
} }
@@ -80,8 +68,8 @@ fn write_header(test_file: &mut File) {
#[cfg(feature = "compare")] #[cfg(feature = "compare")]
fn is_expect_fail(name: &str) -> Option<&str> { fn is_expect_fail(name: &str) -> Option<&str> {
match name { 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."), "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."), "element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."),
_ => None, _ => None,
} }
} }

View File

@@ -1,5 +1,5 @@
FROM alpine:3.17 AS build FROM alpine:3.17 AS build
RUN apk add --no-cache build-base musl-dev git autoconf make texinfo gnutls-dev ncurses-dev gawk RUN apk add --no-cache build-base musl-dev git autoconf make texinfo gnutls-dev ncurses-dev gawk libgccjit-dev
FROM build AS build-emacs FROM build AS build-emacs
@@ -8,13 +8,13 @@ RUN git clone --depth 1 --branch $EMACS_VERSION https://git.savannah.gnu.org/git
WORKDIR /root/emacs WORKDIR /root/emacs
RUN mkdir /root/dist RUN mkdir /root/dist
RUN ./autogen.sh RUN ./autogen.sh
RUN ./configure --prefix /usr --without-x --without-sound RUN ./configure --prefix /usr --without-x --without-sound --with-native-compilation=aot
RUN make RUN make
RUN make DESTDIR="/root/dist" install RUN make DESTDIR="/root/dist" install
FROM build AS build-org-mode FROM build AS build-org-mode
ARG ORG_VERSION=163bafb43dcc2bc94a2c7ccaa77d3d1dd488f1af ARG ORG_VERSION=c703541ffcc14965e3567f928de1683a1c1e33f6
COPY --from=build-emacs /root/dist/ / COPY --from=build-emacs /root/dist/ /
RUN mkdir /root/dist RUN mkdir /root/dist
# Savannah does not allow fetching specific revisions, so we're going to have to put unnecessary load on their server by cloning main and then checking out the revision we want. # Savannah does not allow fetching specific revisions, so we're going to have to put unnecessary load on their server by cloning main and then checking out the revision we want.
@@ -27,7 +27,7 @@ RUN make DESTDIR="/root/dist" install
FROM rustlang/rust:nightly-alpine3.17 AS tester FROM rustlang/rust:nightly-alpine3.17 AS tester
ENV LANG=en_US.UTF-8 ENV LANG=en_US.UTF-8
RUN apk add --no-cache musl-dev ncurses gnutls RUN apk add --no-cache musl-dev ncurses gnutls libgccjit
RUN cargo install --locked --no-default-features --features ci-autoclean cargo-cache RUN cargo install --locked --no-default-features --features ci-autoclean cargo-cache
COPY --from=build-emacs /root/dist/ / COPY --from=build-emacs /root/dist/ /
COPY --from=build-org-mode /root/dist/ / COPY --from=build-org-mode /root/dist/ /
@@ -88,14 +88,20 @@ ARG DOOMEMACS_PATH=/foreign_documents/doomemacs
ARG DOOMEMACS_REPO=https://github.com/doomemacs/doomemacs.git 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 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=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
FROM tester as foreign-document-test FROM tester as foreign-document-test
RUN apk add --no-cache bash coreutils RUN apk add --no-cache bash coreutils
RUN mkdir /foreign_documents RUN mkdir /foreign_documents
COPY --from=build-org-mode /root/org-mode /foreign_documents/org-mode
COPY --from=build-emacs /root/emacs /foreign_documents/emacs
COPY --from=foreign-document-gather /foreign_documents/howardabrams /foreign_documents/howardabrams COPY --from=foreign-document-gather /foreign_documents/howardabrams /foreign_documents/howardabrams
COPY --from=foreign-document-gather /foreign_documents/doomemacs /foreign_documents/doomemacs COPY --from=foreign-document-gather /foreign_documents/doomemacs /foreign_documents/doomemacs
COPY --from=foreign-document-gather /foreign_documents/worg /foreign_documents/worg
COPY --from=build-org-mode /root/org-mode /foreign_documents/org-mode
COPY --from=build-emacs /root/emacs /foreign_documents/emacs
COPY foreign_document_test_entrypoint.sh /entrypoint.sh COPY foreign_document_test_entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -32,6 +32,8 @@ function main {
if [ "$?" -ne 0 ]; then all_status=1; fi if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "emacs" compare_all_org_document "/foreign_documents/emacs") (run_compare_function "emacs" compare_all_org_document "/foreign_documents/emacs")
if [ "$?" -ne 0 ]; then all_status=1; fi if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "worg" compare_all_org_document "/foreign_documents/worg")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "howard_abrams" compare_howard_abrams) (run_compare_function "howard_abrams" compare_howard_abrams)
if [ "$?" -ne 0 ]; then all_status=1; fi if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "doomemacs" compare_all_org_document "/foreign_documents/doomemacs") (run_compare_function "doomemacs" compare_all_org_document "/foreign_documents/doomemacs")
@@ -39,9 +41,9 @@ function main {
set -e set -e
if [ "$all_status" -ne 0 ]; then if [ "$all_status" -ne 0 ]; then
echo "$(red_text "Some tests failed.")" red_text "Some tests failed."
else else
echo "$(green_text "All tests passed.")" green_text "All tests passed."
fi fi
return "$all_status" return "$all_status"
} }
@@ -62,8 +64,9 @@ function indent {
local depth="$1" local depth="$1"
local scaled_depth=$((depth * 2)) local scaled_depth=$((depth * 2))
shift 1 shift 1
local prefix=$(printf -- "%${scaled_depth}s") local prefix
while read l; do prefix=$(printf -- "%${scaled_depth}s")
while read -r l; do
(IFS=' '; printf -- '%s%s\n' "$prefix" "$l") (IFS=' '; printf -- '%s%s\n' "$prefix" "$l")
done done
} }
@@ -91,12 +94,13 @@ function compare_all_org_document {
local target_document local target_document
local all_status=0 local all_status=0
while read target_document; do while read target_document; do
local relative_path=$($REALPATH --relative-to "$root_dir" "$target_document") local relative_path
relative_path=$($REALPATH --relative-to "$root_dir" "$target_document")
set +e set +e
(run_compare "$relative_path" "$target_document") (run_compare "$relative_path" "$target_document")
if [ "$?" -ne 0 ]; then all_status=1; fi if [ "$?" -ne 0 ]; then all_status=1; fi
set -e set -e
done<<<$(find "$root_dir" -type f -iname '*.org') done<<<"$(find "$root_dir" -type f -iname '*.org' | sort)"
return "$all_status" return "$all_status"
} }

View File

@@ -25,3 +25,4 @@ This could significantly reduce our calls to exit matchers.
I think targets would break this. I think targets would break this.
The exit matchers are already implicitly building this behavior since they should all exit very early when the starting character is wrong. Putting this logic in a centralized place, far away from where those characters are actually going to be used, is unfortunate for readability. The exit matchers are already implicitly building this behavior since they should all exit very early when the starting character is wrong. Putting this logic in a centralized place, far away from where those characters are actually going to be used, is unfortunate for readability.
** Use exit matcher to cut off trailing whitespace instead of re-matching in plain lists.

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: timestamp :format "%Y-%m-%d %H:%M"
#+END

View File

@@ -0,0 +1,5 @@
#+begin_quote
foo
#+end_quote

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,3 @@
# These are only allowed by configuring org-list-allow-alphabetical which the automated tests are not currently set up to do, so this will parse as a paragraph:
a. foo
b. bar

View File

@@ -0,0 +1,6 @@
- foo ::
- bar ::
baz

View File

@@ -0,0 +1,3 @@
1. foo
- bar
- lorem :: ipsum

View File

@@ -0,0 +1,2 @@
# Since this is an ordered list, the text before the " :: " is NOT parsed as a tag.
1. foo :: bar

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

@@ -0,0 +1,6 @@
# The STARTUP directive here instructs org-mode to align tables which emacs normally does when opening the file. Since Organic is solely a parser, we have no business editing the org-mode document so Organic does not handle aligning tables, so in order for this test to pass, we have to avoid that behavior in Emacs.
#+STARTUP: align
|foo|bar|
|-
|lorem|ipsum|

View File

@@ -0,0 +1,4 @@
| Name | Value |
|------+-------|
| foo | bar |
#+tblfm:

View File

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

View File

@@ -0,0 +1,2 @@
# Fixed width areas must begin with colon followed by a space, not a tab, so this is not a fixed width area.
: foo

View File

@@ -0,0 +1,11 @@
# Should be a link:
https://en.wikipedia.org/wiki/Shebang_(Unix)
# No closing parenthesis, so link ends at underscore.
https://en.wikipedia.org/wiki/Shebang_(Unix
# Parenthesis only allowed to depth of 2 so link ends at underscore.
https://en.wikipedia.org/wiki/Shebang_(((Unix)))
# Even though they eventually become balanced, we hit negative parenthesis depth so link ends at )
https://en.wikipedia.org/wiki/Shebang)Unix(

View File

@@ -0,0 +1,3 @@
<<<Foo Bar Baz>>>
foo bar baz

View File

@@ -0,0 +1,6 @@
<<<foo bar baz>>>
foo
bar
baz

View File

@@ -0,0 +1 @@
[[elisp:(local-set-key "\M-\x" 'foo-bar-baz)]]

View File

@@ -0,0 +1 @@
[[https://en.wikipedia.org/wiki/Shebang_(Unix)]]

View File

@@ -0,0 +1 @@
[[[http://foo.bar/baz][lorem]]]

View File

@@ -0,0 +1,7 @@
# Even though *exporting* honors the setting to require braces for subscript/superscript, the official org-mode parser still parses subscripts and superscripts.
#+OPTIONS: ^:{}
foo_this isn't a subscript when exported due to lack of braces (but its still a subscript during parsing)
bar_{this is a subscript}

View File

@@ -0,0 +1,13 @@
foo_(bar)
foo_(b(ar)
foo_(b{ar)
foo_{b(ar}
foo_(b(a)r)
foo_b(a)r
foo_(b+ar)

View File

@@ -0,0 +1 @@
foo ** bar ** baz

View File

@@ -0,0 +1 @@
foo ~~ bar ~~ baz

View File

@@ -0,0 +1,4 @@
# Since "foos" has an extra "s", this does not match the target.
the foos bar
The <<<foo>>> and stuff.

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)> <%%(foo bar baz)>
# active # active
<1970-01-01 Thu 8:15rest +1w -1d> <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 # inactive
[1970-01-01 Thu 8:15rest +1w -1d] [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 # active date range
<1970-01-01 Thu 8:15rest +1w -1d>--<1970-01-01 Thu 8:15rest +1w -1d> <1970-01-01 Thu 8:15rest +1w -1d>--<1970-01-01 Thu 8:15rest +1w -1d>
# active time range # 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 # inactive date range
[1970-01-01 Thu 8:15rest +1w -1d]--[1970-01-01 Thu 8:15rest +1w -1d] [1970-01-01 Thu 8:15rest +1w -1d]--[1970-01-01 Thu 8:15rest +1w -1d]
# inactive time range # 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 @@
* DONE
*

View File

@@ -0,0 +1,6 @@
#+TODO: TODO(t) INPROGRESS(i/!) | DONE(d!) CANCELED(c@/!)
# ! : Log changes leading to this state.
# @ : Log changes leading to this state and prompt for a comment to include.
# /! : Log changes leaving this state if and only if to a state that does not log. This can be combined with the above like WAIT(w!/!) or DELAYED(d@/!)
* INPROGRESS
- State "TODO" from "INPROGRESS" [2023-09-14 Thu 02:13]

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

@@ -0,0 +1,8 @@
#+STARTUP: odd
* Foo
***** Bar
* Baz
*** Lorem
* Ipsum
**** Dolar
***** Cat

View File

@@ -4,10 +4,23 @@ set -euo pipefail
IFS=$'\n\t' IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
: ${PROFILE:="perf"}
function main {
local additional_flags=()
if [ "$PROFILE" = "dev" ] || [ "$PROFILE" = "debug" ]; then
PROFILE="debug"
else
additional_flags+=(--profile "$PROFILE")
# We have to disable avx512 because valgrind does not yet support it.
export RUSTFLAGS="-C target-feature=-avx512"
fi
(cd "$DIR/../" && RUSTFLAGS="-C opt-level=0" cargo build --no-default-features) (cd "$DIR/../" && RUSTFLAGS="-C target-cpu=x86-64-v3" cargo build --no-default-features "${additional_flags[@]}")
valgrind --tool=callgrind --callgrind-out-file="$DIR/../callgrind.out" "$DIR/../target/debug/parse" "${@}" valgrind --tool=callgrind --callgrind-out-file="$DIR/../callgrind.out" "$DIR/../target/${PROFILE}/parse" "${@}"
echo "You probably want to run:" echo "You probably want to run:"
echo "callgrind_annotate --auto=yes '$DIR/../callgrind.out'" echo "callgrind_annotate --auto=yes '$DIR/../callgrind.out'"
}
main "${@}"

View File

@@ -8,10 +8,26 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
: ${TRACE:="NO"} # or YES to send traces to jaeger : ${TRACE:="NO"} # or YES to send traces to jaeger
: ${BACKTRACE:="NO"} # or YES to print a rust backtrace when panicking : ${BACKTRACE:="NO"} # or YES to print a rust backtrace when panicking
: ${NO_COLOR:=""} # Set to anything to disable color output : ${NO_COLOR:=""} # Set to anything to disable color output
: ${PROFILE:="debug"}
REALPATH=$(command -v uu-realpath || command -v realpath) REALPATH=$(command -v uu-realpath || command -v realpath)
MAKE=$(command -v gmake || command -v make) MAKE=$(command -v gmake || command -v make)
############## Setup #########################
function die {
local status_code="$1"
shift
(>&2 echo "${@}")
exit "$status_code"
}
function log {
(>&2 echo "${@}")
}
############## Program #########################
function main { function main {
build_container build_container
launch_container "${@}" launch_container "${@}"
@@ -23,7 +39,6 @@ function build_container {
function launch_container { function launch_container {
local additional_flags=() local additional_flags=()
local additional_args=()
local features=(compare) local features=(compare)
if [ "$NO_COLOR" != "" ]; then if [ "$NO_COLOR" != "" ]; then
@@ -37,11 +52,8 @@ function launch_container {
fi fi
if [ "$SHELL" != "YES" ]; then if [ "$SHELL" != "YES" ]; then
local features_joined=$(IFS=","; echo "${features[*]}")
additional_args+=(cargo run --bin compare --no-default-features --features "$features_joined")
additional_flags+=(--read-only) additional_flags+=(--read-only)
else else
additional_args+=(/bin/sh)
additional_flags+=(-t) additional_flags+=(-t)
fi fi
@@ -49,16 +61,50 @@ function launch_container {
additional_flags+=(--env RUST_BACKTRACE=full) additional_flags+=(--env RUST_BACKTRACE=full)
fi fi
if [ "$SHELL" = "YES" ]; then
exec docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "/:/input:ro" -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test /bin/sh
fi
local features_joined
features_joined=$(IFS=","; echo "${features[*]}")
local build_flags=()
if [ "$PROFILE" = "dev" ] || [ "$PROFILE" = "debug" ]; then
PROFILE="debug"
else
build_flags+=(--profile "$PROFILE")
fi
if [ $# -gt 0 ]; then if [ $# -gt 0 ]; then
# If we passed in args, we need to forward them along # If we passed in args, we need to forward them along
for path in "${@}"; do for path in "${@}"; do
local full_path=$($REALPATH "$path") local full_path
local containing_folder=$(dirname "$full_path") full_path=$($REALPATH "$path")
local file_name=$(basename "$full_path") init_script=$(cat <<EOF
docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "${containing_folder}:/input:ro" -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test "${additional_args[@]}" -- "/input/$file_name" set -euo pipefail
IFS=\$'\n\t'
cargo build --bin compare --no-default-features --features "$features_joined" ${build_flags[@]}
exec /target/${PROFILE}/compare "/input${full_path}"
EOF
)
docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "/:/input:ro" -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test sh -c "$init_script"
done done
else else
docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test "${additional_args[@]}" local current_directory init_script
current_directory=$(pwd)
init_script=$(cat <<EOF
set -euo pipefail
IFS=\$'\n\t'
cargo build --bin compare --no-default-features --features "$features_joined" ${build_flags[@]}
cd /input${current_directory}
exec /target/${PROFILE}/compare
EOF
)
docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "/:/input:ro" -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test sh -c "$init_script"
fi fi
} }

View File

@@ -9,17 +9,6 @@ REALPATH=$(command -v uu-realpath || command -v realpath)
############## Setup ######################### ############## Setup #########################
function cleanup {
for f in "${folders[@]}"; do
log "Deleting $f"
rm -rf "$f"
done
}
folders=()
for sig in EXIT INT QUIT HUP TERM; do
trap "set +e; cleanup" "$sig"
done
function die { function die {
local status_code="$1" local status_code="$1"
shift shift
@@ -34,18 +23,18 @@ function log {
############## Program ######################### ############## Program #########################
function main { function main {
log "Is is recommended that the output of \`mktemp -d -t 'compare_bisect.XXXXXXXX'\` is inside a tmpfs filesystem since this script will make many writes to these folders." local target_full_path
target_full_path=$($REALPATH "$1")
local target_full_path=$($REALPATH "$1")
SOURCE_FOLDER=$(dirname "$target_full_path") SOURCE_FOLDER=$(dirname "$target_full_path")
TARGET_DOCUMENT=$(basename "$target_full_path") TARGET_DOCUMENT=$(basename "$target_full_path")
local good=0 local good=0
local bad=$(wc -l "$SOURCE_FOLDER/$TARGET_DOCUMENT" | awk '{print $1}') local bad
bad=$(wc -l "$SOURCE_FOLDER/$TARGET_DOCUMENT" | awk '{print $1}')
set +e set +e
run_parse "$bad" &> /dev/null (run_parse "$bad")
local status=$? local status=$?
set -e set -e
if [ $status -eq 0 ]; then if [ $status -eq 0 ]; then
@@ -71,21 +60,12 @@ function main {
echo "Bad line: $bad" echo "Bad line: $bad"
} }
function setup_temp_dir {
local temp_dir=$(mktemp -d -t 'compare_bisect.XXXXXXXX')
cp -r "$SOURCE_FOLDER/"* "$temp_dir/"
echo "$temp_dir"
}
function run_parse { function run_parse {
local lines="$1" local lines="$1"
local temp_dir=$(setup_temp_dir)
folders+=("$temp_dir") cd "$SOURCE_FOLDER"
cat "$SOURCE_FOLDER/$TARGET_DOCUMENT" | head -n "$lines" > "$temp_dir/$TARGET_DOCUMENT" head -n "$lines" "$SOURCE_FOLDER/$TARGET_DOCUMENT" | PROFILE=release-lto "${DIR}/run_docker_compare.bash"
"${DIR}/run_docker_compare.bash" "$temp_dir/$TARGET_DOCUMENT"
local status=$? local status=$?
rm -rf "$temp_dir"
# TODO: Remove temp_dir from folders
return "$status" return "$status"
} }

View File

@@ -6,19 +6,31 @@ use crate::compare::parse::emacs_parse_file_org_document;
use crate::compare::parse::get_emacs_version; use crate::compare::parse::get_emacs_version;
use crate::compare::parse::get_org_mode_version; use crate::compare::parse::get_org_mode_version;
use crate::compare::sexp::sexp; 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::parser::parse_with_settings;
use crate::GlobalSettings;
use crate::LocalFileAccessInterface;
pub fn run_anonymous_compare<P: AsRef<str>>( pub fn run_anonymous_compare<P: AsRef<str>>(
org_contents: P, org_contents: P,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let org_contents = org_contents.as_ref(); run_anonymous_compare_with_settings(org_contents, &GlobalSettings::default())
eprintln!("Using emacs version: {}", get_emacs_version()?.trim()); }
eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim());
let rust_parsed = parse(org_contents)?; pub fn run_compare_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::error::Error>> {
let org_sexp = emacs_parse_anonymous_org_document(org_contents)?; 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();
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())?; let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?;
println!("{}\n\n\n", org_contents); println!("{}\n\n\n", org_contents);
@@ -36,25 +48,29 @@ pub fn run_anonymous_compare<P: AsRef<str>>(
Ok(()) 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(); let org_path = org_path.as_ref();
eprintln!("Using emacs version: {}", get_emacs_version()?.trim()); print_versions()?;
eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim());
let parent_directory = org_path let parent_directory = org_path
.parent() .parent()
.ok_or("Should be contained inside a directory.")?; .ok_or("Should be contained inside a directory.")?;
let org_contents = std::fs::read_to_string(org_path)?; let org_contents = std::fs::read_to_string(org_path)?;
// 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.replace("\r\n", "\n");
let org_contents = org_contents.as_str(); let org_contents = org_contents.as_str();
let file_access_interface = LocalFileAccessInterface { let file_access_interface = LocalFileAccessInterface {
working_directory: Some(parent_directory.to_path_buf()), working_directory: Some(parent_directory.to_path_buf()),
}; };
let global_settings = { 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.file_access = &file_access_interface;
global_settings global_settings
}; };
let rust_parsed = parse_with_settings(org_contents, &global_settings)?; let rust_parsed = parse_file_with_settings(org_contents, &global_settings, Some(org_path))?;
let org_sexp = emacs_parse_file_org_document(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())?; let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?;
println!("{}\n\n\n", org_contents); println!("{}\n\n\n", org_contents);
@@ -71,3 +87,9 @@ pub fn run_compare_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn st
Ok(()) 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

552
src/compare/elisp_fact.rs Normal file
View File

@@ -0,0 +1,552 @@
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;
use crate::types::Code;
use crate::types::Comment;
use crate::types::CommentBlock;
use crate::types::DiarySexp;
use crate::types::Document;
use crate::types::Drawer;
use crate::types::DynamicBlock;
use crate::types::Element;
use crate::types::Entity;
use crate::types::ExampleBlock;
use crate::types::ExportBlock;
use crate::types::ExportSnippet;
use crate::types::FixedWidthArea;
use crate::types::FootnoteDefinition;
use crate::types::FootnoteReference;
use crate::types::Heading;
use crate::types::HorizontalRule;
use crate::types::InlineBabelCall;
use crate::types::InlineSourceBlock;
use crate::types::Italic;
use crate::types::Keyword;
use crate::types::LatexEnvironment;
use crate::types::LatexFragment;
use crate::types::LineBreak;
use crate::types::NodeProperty;
use crate::types::Object;
use crate::types::OrgMacro;
use crate::types::Paragraph;
use crate::types::PlainLink;
use crate::types::PlainList;
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;
use crate::types::Subscript;
use crate::types::Superscript;
use crate::types::Table;
use crate::types::TableCell;
use crate::types::TableRow;
use crate::types::Target;
use crate::types::Timestamp;
use crate::types::Underline;
use crate::types::Verbatim;
use crate::types::VerseBlock;
pub(crate) trait ElispFact<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str>;
}
pub(crate) trait GetElispFact<'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<'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<'b>(&'b self) -> &'b dyn ElispFact<'s> {
match self {
Element::Paragraph(inner) => inner,
Element::PlainList(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,
Element::Drawer(inner) => inner,
Element::PropertyDrawer(inner) => inner,
Element::Table(inner) => inner,
Element::VerseBlock(inner) => inner,
Element::CommentBlock(inner) => inner,
Element::ExampleBlock(inner) => inner,
Element::ExportBlock(inner) => inner,
Element::SrcBlock(inner) => inner,
Element::Clock(inner) => inner,
Element::DiarySexp(inner) => inner,
Element::Planning(inner) => inner,
Element::FixedWidthArea(inner) => inner,
Element::HorizontalRule(inner) => inner,
Element::Keyword(inner) => inner,
Element::BabelCall(inner) => inner,
Element::LatexEnvironment(inner) => inner,
}
}
}
impl<'s> GetElispFact<'s> for Object<'s> {
fn get_elisp_fact<'b>(&'b self) -> &'b dyn ElispFact<'s> {
match self {
Object::Bold(inner) => inner,
Object::Italic(inner) => inner,
Object::Underline(inner) => inner,
Object::StrikeThrough(inner) => inner,
Object::Code(inner) => inner,
Object::Verbatim(inner) => inner,
Object::PlainText(inner) => inner,
Object::RegularLink(inner) => inner,
Object::RadioLink(inner) => inner,
Object::RadioTarget(inner) => inner,
Object::PlainLink(inner) => inner,
Object::AngleLink(inner) => inner,
Object::OrgMacro(inner) => inner,
Object::Entity(inner) => inner,
Object::LatexFragment(inner) => inner,
Object::ExportSnippet(inner) => inner,
Object::FootnoteReference(inner) => inner,
Object::Citation(inner) => inner,
Object::CitationReference(inner) => inner,
Object::InlineBabelCall(inner) => inner,
Object::InlineSourceBlock(inner) => inner,
Object::LineBreak(inner) => inner,
Object::Target(inner) => inner,
Object::StatisticsCookie(inner) => inner,
Object::Subscript(inner) => inner,
Object::Superscript(inner) => inner,
Object::Timestamp(inner) => inner,
}
}
}
impl<'s> ElispFact<'s> for Document<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"org-data".into()
}
}
impl<'s> ElispFact<'s> for Section<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"section".into()
}
}
impl<'s> ElispFact<'s> for Heading<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"headline".into()
}
}
impl<'s> ElispFact<'s> for PlainList<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"plain-list".into()
}
}
impl<'s> ElispFact<'s> for PlainListItem<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"item".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<'b>(&'b self) -> Cow<'s, str> {
"dynamic-block".into()
}
}
impl<'s> ElispFact<'s> for FootnoteDefinition<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"footnote-definition".into()
}
}
impl<'s> ElispFact<'s> for Drawer<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"drawer".into()
}
}
impl<'s> ElispFact<'s> for PropertyDrawer<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"property-drawer".into()
}
}
impl<'s> ElispFact<'s> for NodeProperty<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"node-property".into()
}
}
impl<'s> ElispFact<'s> for Table<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"table".into()
}
}
impl<'s> ElispFact<'s> for TableRow<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"table-row".into()
}
}
impl<'s> ElispFact<'s> for Paragraph<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"paragraph".into()
}
}
impl<'s> ElispFact<'s> for TableCell<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"table-cell".into()
}
}
impl<'s> ElispFact<'s> for Comment<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"comment".into()
}
}
impl<'s> ElispFact<'s> for VerseBlock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"verse-block".into()
}
}
impl<'s> ElispFact<'s> for CommentBlock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"comment-block".into()
}
}
impl<'s> ElispFact<'s> for ExampleBlock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"example-block".into()
}
}
impl<'s> ElispFact<'s> for ExportBlock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"export-block".into()
}
}
impl<'s> ElispFact<'s> for SrcBlock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"src-block".into()
}
}
impl<'s> ElispFact<'s> for Clock<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"clock".into()
}
}
impl<'s> ElispFact<'s> for DiarySexp<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"diary-sexp".into()
}
}
impl<'s> ElispFact<'s> for Planning<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"planning".into()
}
}
impl<'s> ElispFact<'s> for FixedWidthArea<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"fixed-width".into()
}
}
impl<'s> ElispFact<'s> for HorizontalRule<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"horizontal-rule".into()
}
}
impl<'s> ElispFact<'s> for Keyword<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"keyword".into()
}
}
impl<'s> ElispFact<'s> for BabelCall<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"babel-call".into()
}
}
impl<'s> ElispFact<'s> for LatexEnvironment<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"latex-environment".into()
}
}
impl<'s> ElispFact<'s> for Bold<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"bold".into()
}
}
impl<'s> ElispFact<'s> for Italic<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"italic".into()
}
}
impl<'s> ElispFact<'s> for Underline<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"underline".into()
}
}
impl<'s> ElispFact<'s> for StrikeThrough<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"strike-through".into()
}
}
impl<'s> ElispFact<'s> for Code<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"code".into()
}
}
impl<'s> ElispFact<'s> for Verbatim<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"verbatim".into()
}
}
impl<'s> ElispFact<'s> for RegularLink<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"link".into()
}
}
impl<'s> ElispFact<'s> for RadioLink<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"link".into()
}
}
impl<'s> ElispFact<'s> for RadioTarget<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"radio-target".into()
}
}
impl<'s> ElispFact<'s> for PlainLink<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"link".into()
}
}
impl<'s> ElispFact<'s> for AngleLink<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"link".into()
}
}
impl<'s> ElispFact<'s> for OrgMacro<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"macro".into()
}
}
impl<'s> ElispFact<'s> for Entity<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"entity".into()
}
}
impl<'s> ElispFact<'s> for LatexFragment<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"latex-fragment".into()
}
}
impl<'s> ElispFact<'s> for ExportSnippet<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"export-snippet".into()
}
}
impl<'s> ElispFact<'s> for FootnoteReference<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"footnote-reference".into()
}
}
impl<'s> ElispFact<'s> for Citation<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"citation".into()
}
}
impl<'s> ElispFact<'s> for CitationReference<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"citation-reference".into()
}
}
impl<'s> ElispFact<'s> for InlineBabelCall<'s> {
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<'b>(&'b self) -> Cow<'s, str> {
"inline-src-block".into()
}
}
impl<'s> ElispFact<'s> for LineBreak<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"line-break".into()
}
}
impl<'s> ElispFact<'s> for Target<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"target".into()
}
}
impl<'s> ElispFact<'s> for StatisticsCookie<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"statistics-cookie".into()
}
}
impl<'s> ElispFact<'s> for Subscript<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"subscript".into()
}
}
impl<'s> ElispFact<'s> for Superscript<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"superscript".into()
}
}
impl<'s> ElispFact<'s> for Timestamp<'s> {
fn get_elisp_name<'b>(&'b self) -> Cow<'s, str> {
"timestamp".into()
}
}
impl<'s> ElispFact<'s> for PlainText<'s> {
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

@@ -1,7 +1,10 @@
mod compare; mod compare;
mod diff; mod diff;
mod elisp_fact;
mod parse; mod parse;
mod sexp; mod sexp;
mod util; mod util;
pub use compare::run_anonymous_compare; 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;
pub use compare::run_compare_on_file_with_settings;

View File

@@ -1,8 +1,33 @@
use std::path::Path; use std::path::Path;
use std::process::Command; 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, file_contents: C,
global_settings: &GlobalSettings,
) -> Result<String, Box<dyn std::error::Error>> ) -> Result<String, Box<dyn std::error::Error>>
where where
C: AsRef<str>, C: AsRef<str>,
@@ -11,11 +36,15 @@ where
let elisp_script = format!( let elisp_script = format!(
r#"(progn r#"(progn
(erase-buffer) (erase-buffer)
(require 'org)
(defun org-table-align () t)
(insert "{escaped_file_contents}") (insert "{escaped_file_contents}")
{global_settings}
(org-mode) (org-mode)
(message "%s" (pp-to-string (org-element-parse-buffer))) (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 mut cmd = Command::new("emacs");
let cmd = cmd let cmd = cmd
@@ -31,7 +60,10 @@ where
Ok(String::from_utf8(org_sexp)?) 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 where
P: AsRef<Path>, P: AsRef<Path>,
{ {
@@ -42,9 +74,19 @@ where
))?; ))?;
let elisp_script = format!( let elisp_script = format!(
r#"(progn r#"(progn
(require 'org)
(defun org-table-align () t)
(setq vc-handled-backends nil)
{global_settings}
(find-file-read-only "{file_path}")
(org-mode) (org-mode)
(message "%s" (pp-to-string (org-element-parse-buffer))) (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 mut cmd = Command::new("emacs");
let cmd = cmd let cmd = cmd
@@ -53,8 +95,6 @@ where
.arg("--no-site-file") .arg("--no-site-file")
.arg("--no-splash") .arg("--no-splash")
.arg("--batch") .arg("--batch")
.arg("--insert")
.arg(file_path.as_os_str())
.arg("--eval") .arg("--eval")
.arg(elisp_script); .arg(elisp_script);
let out = cmd.output()?; let out = cmd.output()?;

View File

@@ -1,9 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::escaped;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::bytes::complete::take_till1; 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::multispace0;
use nom::character::complete::multispace1; use nom::character::complete::multispace1;
use nom::character::complete::one_of; use nom::character::complete::one_of;
@@ -11,6 +12,7 @@ use nom::combinator::map;
use nom::combinator::not; use nom::combinator::not;
use nom::combinator::opt; use nom::combinator::opt;
use nom::combinator::peek; use nom::combinator::peek;
use nom::combinator::recognize;
use nom::multi::separated_list1; use nom::multi::separated_list1;
use nom::sequence::delimited; use nom::sequence::delimited;
use nom::sequence::preceded; use nom::sequence::preceded;
@@ -18,6 +20,8 @@ use nom::sequence::tuple;
use crate::error::Res; use crate::error::Res;
const MAX_OCTAL_LENGTH: usize = 3;
#[derive(Debug)] #[derive(Debug)]
pub enum Token<'s> { pub enum Token<'s> {
Atom(&'s str), Atom(&'s str),
@@ -35,6 +39,7 @@ pub struct TextWithProperties<'s> {
enum ParseState { enum ParseState {
Normal, Normal,
Escape, Escape,
Octal(Vec<u8>),
} }
impl<'s> Token<'s> { impl<'s> Token<'s> {
@@ -106,8 +111,8 @@ fn is_slice_of(parent: &str, child: &str) -> bool {
} }
/// Get a slice of the string that was consumed in a parser using the original input to the parser and the remaining input after the parser. /// Get a slice of the string that was consumed in a parser using the original input to the parser and the remaining input after the parser.
pub fn get_consumed<'s>(input: &'s str, remaining: &'s str) -> &'s str { fn get_consumed<'s>(input: &'s str, remaining: &'s str) -> &'s str {
assert!(is_slice_of(input, remaining)); debug_assert!(is_slice_of(input, remaining));
let source = { let source = {
let offset = remaining.as_ptr() as usize - input.as_ptr() as usize; let offset = remaining.as_ptr() as usize - input.as_ptr() as usize;
&input[..offset] &input[..offset]
@@ -116,7 +121,7 @@ pub 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>> { 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#"""#) { if !text.starts_with(r#"""#) {
return Err("Quoted text does not start with quote.".into()); 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 interior_text = &text[1..(text.len() - 1)];
let mut state = ParseState::Normal; 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) { 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, _) => { (ParseState::Normal, _) => {
out.push(current_char); out.push(current_char);
ParseState::Normal ParseState::Normal
} }
(ParseState::Escape, 'n') => { (ParseState::Escape, b'n') => {
out.push('\n'); out.push(b'\n');
ParseState::Normal ParseState::Normal
} }
(ParseState::Escape, '\\') => { (ParseState::Escape, b'\\') => {
out.push('\\'); out.push(b'\\');
ParseState::Normal ParseState::Normal
} }
(ParseState::Escape, '"') => { (ParseState::Escape, b'"') => {
out.push('"'); out.push(b'"');
ParseState::Normal 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"))] #[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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn quoted_atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> { fn quoted_atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (remaining, _) = tag(r#"""#)(input)?; let (mut remaining, _) = tag(r#"""#)(input)?;
let (remaining, _) = escaped( let mut in_escape = false;
take_till1(|c| match c { loop {
'\\' | '"' => true, if in_escape {
_ => false, let (remain, _) = alt((recognize(one_of(r#""n\\"#)), digit1))(remaining)?;
}), remaining = remain;
'\\', in_escape = false;
one_of(r#""n\\"#), } else {
)(remaining)?; 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 (remaining, _) = tag(r#"""#)(remaining)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok((remaining, Token::Atom(source.into()))) Ok((remaining, Token::Atom(source.into())))

View File

@@ -1,5 +1,10 @@
use std::str::FromStr;
use super::elisp_fact::GetElispFact;
use super::sexp::Token; use super::sexp::Token;
use crate::types::Source; use crate::compare::sexp::unquote;
use crate::types::GetStandardProperties;
use crate::types::StandardProperties;
/// Check if the child string slice is a slice of the parent string slice. /// Check if the child string slice is a slice of the parent string slice.
fn is_slice_of(parent: &str, child: &str) -> bool { fn is_slice_of(parent: &str, child: &str) -> bool {
@@ -10,21 +15,39 @@ fn is_slice_of(parent: &str, child: &str) -> bool {
child_start >= parent_start && child_end <= parent_end child_start >= parent_start && child_end <= parent_end
} }
/// Get the offset into source that the rust object exists at. /// Get the byte offset into source that the rust object exists at.
/// ///
/// These offsets are zero-based unlike the elisp ones. /// These offsets are zero-based unlike the elisp ones.
fn get_offsets<'s, S: Source<'s>>(source: &'s str, rust_object: &'s S) -> (usize, usize) { fn get_rust_byte_offsets<'b, 's, S: StandardProperties<'s> + ?Sized>(
let rust_object_source = rust_object.get_source(); original_document: &'s str,
assert!(is_slice_of(source, rust_object_source)); rust_ast_node: &'b S,
let offset = rust_object_source.as_ptr() as usize - source.as_ptr() as usize; ) -> (usize, usize) {
let rust_object_source = rust_ast_node.get_source();
debug_assert!(is_slice_of(original_document, rust_object_source));
let offset = rust_object_source.as_ptr() as usize - original_document.as_ptr() as usize;
let end = offset + rust_object_source.len(); let end = offset + rust_object_source.len();
(offset, end) (offset, end)
} }
pub(crate) fn assert_name<'s>( pub(crate) fn compare_standard_properties<
emacs: &'s Token<'s>, 'b,
name: &str, 's,
S: GetStandardProperties<'s> + GetElispFact<'s> + ?Sized,
>(
original_document: &'s str,
emacs: &'b Token<'s>,
rust: &'b S,
) -> Result<(), Box<dyn std::error::Error>> { ) -> 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<'b, 's, S: AsRef<str>>(
emacs: &'b Token<'s>,
name: S,
) -> Result<(), Box<dyn std::error::Error>> {
let name = name.as_ref();
let children = emacs.as_list()?; let children = emacs.as_list()?;
let first_child = children let first_child = children
.first() .first()
@@ -32,7 +55,7 @@ pub(crate) fn assert_name<'s>(
.as_atom()?; .as_atom()?;
if first_child != name { if first_child != name {
Err(format!( Err(format!(
"Expected a {expected} cell, but found a {found} cell.", "AST node name mismatch. Expected a (rust) {expected} cell, but found a (emacs) {found} cell.",
expected = name, expected = name,
found = first_child found = first_child
))?; ))?;
@@ -40,30 +63,33 @@ pub(crate) fn assert_name<'s>(
Ok(()) Ok(())
} }
pub(crate) fn assert_bounds<'s, S: Source<'s>>( /// Assert that the character ranges defined by upstream org-mode's :standard-properties match the slices in Organic's StandardProperties.
source: &'s str, ///
emacs: &'s Token<'s>, /// This does **not** handle plain text because plain text is a special case.
rust: &'s S, pub(crate) fn assert_bounds<'b, 's, S: StandardProperties<'s> + ?Sized>(
original_document: &'s str,
emacs: &'b Token<'s>,
rust: &'b S,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let standard_properties = get_standard_properties(emacs)?; let standard_properties = get_emacs_standard_properties(emacs)?; // 1-based
let (begin, end) = ( let (begin, end) = (
standard_properties standard_properties
.begin .begin
.ok_or("Token should have a begin.")?, .ok_or("Token should have a begin.")?,
standard_properties.end.ok_or("Token should have an end.")?, standard_properties.end.ok_or("Token should have an end.")?,
); );
let (rust_begin, rust_end) = get_offsets(source, rust); let (rust_begin, rust_end) = get_rust_byte_offsets(original_document, rust); // 0-based
let rust_begin_char_offset = (&source[..rust_begin]).chars().count(); let rust_begin_char_offset = (&original_document[..rust_begin]).chars().count() + 1; // 1-based
let rust_end_char_offset = let rust_end_char_offset =
rust_begin_char_offset + (&source[rust_begin..rust_end]).chars().count(); rust_begin_char_offset + (&original_document[rust_begin..rust_end]).chars().count(); // 1-based
if (rust_begin_char_offset + 1) != begin || (rust_end_char_offset + 1) != end { if rust_begin_char_offset != begin || rust_end_char_offset != end {
Err(format!("Rust bounds (in chars) ({rust_begin}, {rust_end}) do not match emacs bounds ({emacs_begin}, {emacs_end})", rust_begin = rust_begin_char_offset + 1, rust_end = rust_end_char_offset + 1, emacs_begin=begin, emacs_end=end))?; Err(format!("Rust bounds (in chars) ({rust_begin}, {rust_end}) do not match emacs bounds ({emacs_begin}, {emacs_end})", rust_begin = rust_begin_char_offset, rust_end = rust_end_char_offset, emacs_begin=begin, emacs_end=end))?;
} }
Ok(()) Ok(())
} }
struct StandardProperties { struct EmacsStandardProperties {
begin: Option<usize>, begin: Option<usize>,
#[allow(dead_code)] #[allow(dead_code)]
post_affiliated: Option<usize>, post_affiliated: Option<usize>,
@@ -76,9 +102,9 @@ struct StandardProperties {
post_blank: Option<usize>, post_blank: Option<usize>,
} }
fn get_standard_properties<'s>( fn get_emacs_standard_properties<'b, 's>(
emacs: &'s Token<'s>, emacs: &'b Token<'s>,
) -> Result<StandardProperties, Box<dyn std::error::Error>> { ) -> Result<EmacsStandardProperties, Box<dyn std::error::Error>> {
let children = emacs.as_list()?; let children = emacs.as_list()?;
let attributes_child = children let attributes_child = children
.iter() .iter()
@@ -97,7 +123,7 @@ fn get_standard_properties<'s>(
let contents_end = maybe_token_to_usize(std_props.next())?; let contents_end = maybe_token_to_usize(std_props.next())?;
let end = maybe_token_to_usize(std_props.next())?; let end = maybe_token_to_usize(std_props.next())?;
let post_blank = maybe_token_to_usize(std_props.next())?; let post_blank = maybe_token_to_usize(std_props.next())?;
StandardProperties { EmacsStandardProperties {
begin, begin,
post_affiliated, post_affiliated,
contents_begin, contents_begin,
@@ -116,7 +142,7 @@ fn get_standard_properties<'s>(
maybe_token_to_usize(attributes_map.get(":post-blank").map(|token| *token))?; maybe_token_to_usize(attributes_map.get(":post-blank").map(|token| *token))?;
let post_affiliated = let post_affiliated =
maybe_token_to_usize(attributes_map.get(":post-affiliated").map(|token| *token))?; maybe_token_to_usize(attributes_map.get(":post-affiliated").map(|token| *token))?;
StandardProperties { EmacsStandardProperties {
begin, begin,
post_affiliated, post_affiliated,
contents_begin, contents_begin,
@@ -146,25 +172,83 @@ fn maybe_token_to_usize(
/// Get a named property from the emacs token. /// Get a named property from the emacs token.
/// ///
/// Returns Ok(None) if value is nil. /// Returns Ok(None) if value is nil or absent.
/// pub(crate) fn get_property<'b, 's, 'x>(
/// Returns error if the attribute is not specified on the token at all. emacs: &'b Token<'s>,
pub(crate) fn get_property<'s, 'x>(
emacs: &'s Token<'s>,
key: &'x str, 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 children = emacs.as_list()?;
let attributes_child = children let attributes_child = children
.iter() .iter()
.nth(1) .nth(1)
.ok_or("Should have an attributes child.")?; .ok_or("Should have an attributes child.")?;
let attributes_map = attributes_child.as_map()?; let attributes_map = attributes_child.as_map()?;
let prop = attributes_map let prop = attributes_map.get(key).map(|token| *token);
.get(key) match prop.map(|token| token.as_atom()) {
.ok_or(format!("Missing {} attribute.", key))?; Some(Ok("nil")) => return Ok(None),
match prop.as_atom() {
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<'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

@@ -2,6 +2,7 @@ use std::collections::BTreeSet;
use super::FileAccessInterface; use super::FileAccessInterface;
use super::LocalFileAccessInterface; use super::LocalFileAccessInterface;
use crate::types::IndentationLevel;
use crate::types::Object; use crate::types::Object;
// TODO: Ultimately, I think we'll need most of this: https://orgmode.org/manual/In_002dbuffer-Settings.html // TODO: Ultimately, I think we'll need most of this: https://orgmode.org/manual/In_002dbuffer-Settings.html
@@ -12,8 +13,29 @@ pub struct GlobalSettings<'g, 's> {
pub file_access: &'g dyn FileAccessInterface, pub file_access: &'g dyn FileAccessInterface,
pub in_progress_todo_keywords: BTreeSet<String>, pub in_progress_todo_keywords: BTreeSet<String>,
pub complete_todo_keywords: BTreeSet<String>, pub complete_todo_keywords: BTreeSet<String>,
/// 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 list_allow_alphabetical: bool,
/// How many spaces a tab should be equal to.
///
/// Corresponds to the tab-width elisp variable.
pub tab_width: IndentationLevel,
/// Whether to only allow odd headline levels.
///
/// 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> { impl<'g, 's> GlobalSettings<'g, 's> {
fn new() -> GlobalSettings<'g, 's> { fn new() -> GlobalSettings<'g, 's> {
GlobalSettings { GlobalSettings {
@@ -23,6 +45,10 @@ impl<'g, 's> GlobalSettings<'g, 's> {
}, },
in_progress_todo_keywords: BTreeSet::new(), in_progress_todo_keywords: BTreeSet::new(),
complete_todo_keywords: BTreeSet::new(), complete_todo_keywords: BTreeSet::new(),
list_allow_alphabetical: false,
tab_width: DEFAULT_TAB_WIDTH,
odd_levels_only: HeadlineLevelFilter::default(),
footnote_section: "Footnotes",
} }
} }
} }
@@ -32,3 +58,15 @@ impl<'g, 's> Default for GlobalSettings<'g, 's> {
GlobalSettings::new() GlobalSettings::new()
} }
} }
#[derive(Debug, Clone, PartialEq)]
pub enum HeadlineLevelFilter {
Odd,
OddEven,
}
impl Default for HeadlineLevelFilter {
fn default() -> Self {
HeadlineLevelFilter::OddEven
}
}

View File

@@ -6,6 +6,7 @@ pub(crate) struct List<'parent, T> {
parent: Link<'parent, T>, parent: Link<'parent, T>,
} }
// TODO: Should I be defining a lifetime for T in the generics here? Ref: https://quinedot.github.io/rust-learning/dyn-elision-advanced.html#iteraction-with-type-aliases
type Link<'parent, T> = Option<&'parent List<'parent, T>>; type Link<'parent, T> = Option<&'parent List<'parent, T>>;
impl<'parent, T> List<'parent, T> { impl<'parent, T> List<'parent, T> {

View File

@@ -25,5 +25,7 @@ pub(crate) use exiting::ExitClass;
pub use file_access_interface::FileAccessInterface; pub use file_access_interface::FileAccessInterface;
pub use file_access_interface::LocalFileAccessInterface; pub use file_access_interface::LocalFileAccessInterface;
pub use global_settings::GlobalSettings; 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 list::List;
pub(crate) use parser_with_context::parser_with_context; pub(crate) use parser_with_context::parser_with_context;

View File

@@ -4,12 +4,12 @@ use nom::IResult;
pub(crate) type Res<T, U> = IResult<T, U, CustomError<T>>; pub(crate) type Res<T, U> = IResult<T, U, CustomError<T>>;
// TODO: MyError probably shouldn't be based on the same type as the input type since it's used exclusively with static strings right now.
#[derive(Debug)] #[derive(Debug)]
pub enum CustomError<I> { pub enum CustomError<I> {
MyError(MyError<I>), MyError(MyError<&'static str>),
Nom(I, ErrorKind), Nom(I, ErrorKind),
IO(std::io::Error), IO(std::io::Error),
BoxedError(Box<dyn std::error::Error>),
} }
#[derive(Debug)] #[derive(Debug)]
@@ -31,3 +31,15 @@ impl<I> From<std::io::Error> for CustomError<I> {
CustomError::IO(value) CustomError::IO(value)
} }
} }
impl<I> From<&'static str> for CustomError<I> {
fn from(value: &'static str) -> Self {
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

@@ -0,0 +1,104 @@
use std::collections::VecDeque;
use super::ast_node_iter::AstNodeIter;
use crate::types::AstNode;
pub struct AllAstNodeIter<'r, 's> {
root: Option<AstNode<'r, 's>>,
queue: VecDeque<AstNodeIter<'r, 's>>,
}
impl<'r, 's> Iterator for AllAstNodeIter<'r, 's> {
type Item = AstNode<'r, 's>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(root) = self.root.take() {
self.queue.push_back(AstNodeIter::from_ast_node(&root));
return Some(root);
}
while let Some(child) = self.queue.front_mut() {
let next_elem_this_iter = match child {
AstNodeIter::Document(ref mut i) => i.next(),
AstNodeIter::Heading(ref mut i) => i.next(),
AstNodeIter::Section(ref mut i) => i.next(),
AstNodeIter::Paragraph(ref mut i) => i.next(),
AstNodeIter::PlainList(ref mut i) => i.next(),
AstNodeIter::PlainListItem(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(),
AstNodeIter::Drawer(ref mut i) => i.next(),
AstNodeIter::PropertyDrawer(ref mut i) => i.next(),
AstNodeIter::NodeProperty(ref mut i) => i.next(),
AstNodeIter::Table(ref mut i) => i.next(),
AstNodeIter::TableRow(ref mut i) => i.next(),
AstNodeIter::VerseBlock(ref mut i) => i.next(),
AstNodeIter::CommentBlock(ref mut i) => i.next(),
AstNodeIter::ExampleBlock(ref mut i) => i.next(),
AstNodeIter::ExportBlock(ref mut i) => i.next(),
AstNodeIter::SrcBlock(ref mut i) => i.next(),
AstNodeIter::Clock(ref mut i) => i.next(),
AstNodeIter::DiarySexp(ref mut i) => i.next(),
AstNodeIter::Planning(ref mut i) => i.next(),
AstNodeIter::FixedWidthArea(ref mut i) => i.next(),
AstNodeIter::HorizontalRule(ref mut i) => i.next(),
AstNodeIter::Keyword(ref mut i) => i.next(),
AstNodeIter::BabelCall(ref mut i) => i.next(),
AstNodeIter::LatexEnvironment(ref mut i) => i.next(),
AstNodeIter::Bold(ref mut i) => i.next(),
AstNodeIter::Italic(ref mut i) => i.next(),
AstNodeIter::Underline(ref mut i) => i.next(),
AstNodeIter::StrikeThrough(ref mut i) => i.next(),
AstNodeIter::Code(ref mut i) => i.next(),
AstNodeIter::Verbatim(ref mut i) => i.next(),
AstNodeIter::PlainText(ref mut i) => i.next(),
AstNodeIter::RegularLink(ref mut i) => i.next(),
AstNodeIter::RadioLink(ref mut i) => i.next(),
AstNodeIter::RadioTarget(ref mut i) => i.next(),
AstNodeIter::PlainLink(ref mut i) => i.next(),
AstNodeIter::AngleLink(ref mut i) => i.next(),
AstNodeIter::OrgMacro(ref mut i) => i.next(),
AstNodeIter::Entity(ref mut i) => i.next(),
AstNodeIter::LatexFragment(ref mut i) => i.next(),
AstNodeIter::ExportSnippet(ref mut i) => i.next(),
AstNodeIter::FootnoteReference(ref mut i) => i.next(),
AstNodeIter::Citation(ref mut i) => i.next(),
AstNodeIter::CitationReference(ref mut i) => i.next(),
AstNodeIter::InlineBabelCall(ref mut i) => i.next(),
AstNodeIter::InlineSourceBlock(ref mut i) => i.next(),
AstNodeIter::LineBreak(ref mut i) => i.next(),
AstNodeIter::Target(ref mut i) => i.next(),
AstNodeIter::StatisticsCookie(ref mut i) => i.next(),
AstNodeIter::Subscript(ref mut i) => i.next(),
AstNodeIter::Superscript(ref mut i) => i.next(),
AstNodeIter::TableCell(ref mut i) => i.next(),
AstNodeIter::Timestamp(ref mut i) => i.next(),
};
if let Some(next_elem_this_iter) = next_elem_this_iter {
self.queue
.push_back(AstNodeIter::from_ast_node(&next_elem_this_iter));
return Some(next_elem_this_iter);
} else {
self.queue.pop_front();
}
}
None
}
}
impl<'r, 's> IntoIterator for AstNode<'r, 's> {
type Item = AstNode<'r, 's>;
type IntoIter = AllAstNodeIter<'r, 's>;
fn into_iter(self) -> Self::IntoIter {
AllAstNodeIter {
root: Some(self),
queue: VecDeque::new(),
}
}
}

348
src/iter/ast_node_iter.rs Normal file
View File

@@ -0,0 +1,348 @@
use std::marker::PhantomData;
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;
use crate::types::Code;
use crate::types::Comment;
use crate::types::CommentBlock;
use crate::types::DiarySexp;
use crate::types::Document;
use crate::types::DocumentElement;
use crate::types::Drawer;
use crate::types::DynamicBlock;
use crate::types::Element;
use crate::types::Entity;
use crate::types::ExampleBlock;
use crate::types::ExportBlock;
use crate::types::ExportSnippet;
use crate::types::FixedWidthArea;
use crate::types::FootnoteDefinition;
use crate::types::FootnoteReference;
use crate::types::Heading;
use crate::types::HorizontalRule;
use crate::types::InlineBabelCall;
use crate::types::InlineSourceBlock;
use crate::types::Italic;
use crate::types::Keyword;
use crate::types::LatexEnvironment;
use crate::types::LatexFragment;
use crate::types::LineBreak;
use crate::types::NodeProperty;
use crate::types::Object;
use crate::types::OrgMacro;
use crate::types::Paragraph;
use crate::types::PlainLink;
use crate::types::PlainList;
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;
use crate::types::Subscript;
use crate::types::Superscript;
use crate::types::Table;
use crate::types::TableCell;
use crate::types::TableRow;
use crate::types::Target;
use crate::types::Timestamp;
use crate::types::Underline;
use crate::types::Verbatim;
use crate::types::VerseBlock;
/// Iterator over the AST nodes contained within the starting node.
///
/// This only iterates over the children, not the starting node itself. So an AstNodeIter::PlainList would only return PlainListItems, not the PlainList.
///
/// This only iterates over AST nodes, so an AstNodeIter::Heading would iterate over both the title and section contents, but it would not iterate over simple strings like the TODO keyword or priority.
pub(crate) enum AstNodeIter<'r, 's> {
// Document Nodes
Document(DocumentIter<'r, 's>),
Heading(HeadingIter<'r, 's>),
Section(SectionIter<'r, 's>),
// Elements
Paragraph(ParagraphIter<'r, 's>),
PlainList(PlainListIter<'r, 's>),
PlainListItem(PlainListItemIter<'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>),
Drawer(DrawerIter<'r, 's>),
PropertyDrawer(PropertyDrawerIter<'r, 's>),
NodeProperty(NodePropertyIter<'r, 's>),
Table(TableIter<'r, 's>),
TableRow(TableRowIter<'r, 's>),
VerseBlock(VerseBlockIter<'r, 's>),
CommentBlock(CommentBlockIter<'r, 's>),
ExampleBlock(ExampleBlockIter<'r, 's>),
ExportBlock(ExportBlockIter<'r, 's>),
SrcBlock(SrcBlockIter<'r, 's>),
Clock(ClockIter<'r, 's>),
DiarySexp(DiarySexpIter<'r, 's>),
Planning(PlanningIter<'r, 's>),
FixedWidthArea(FixedWidthAreaIter<'r, 's>),
HorizontalRule(HorizontalRuleIter<'r, 's>),
Keyword(KeywordIter<'r, 's>),
BabelCall(BabelCallIter<'r, 's>),
LatexEnvironment(LatexEnvironmentIter<'r, 's>),
// Objects
Bold(BoldIter<'r, 's>),
Italic(ItalicIter<'r, 's>),
Underline(UnderlineIter<'r, 's>),
StrikeThrough(StrikeThroughIter<'r, 's>),
Code(CodeIter<'r, 's>),
Verbatim(VerbatimIter<'r, 's>),
PlainText(PlainTextIter<'r, 's>),
RegularLink(RegularLinkIter<'r, 's>),
RadioLink(RadioLinkIter<'r, 's>),
RadioTarget(RadioTargetIter<'r, 's>),
PlainLink(PlainLinkIter<'r, 's>),
AngleLink(AngleLinkIter<'r, 's>),
OrgMacro(OrgMacroIter<'r, 's>),
Entity(EntityIter<'r, 's>),
LatexFragment(LatexFragmentIter<'r, 's>),
ExportSnippet(ExportSnippetIter<'r, 's>),
FootnoteReference(FootnoteReferenceIter<'r, 's>),
Citation(CitationIter<'r, 's>),
CitationReference(CitationReferenceIter<'r, 's>),
InlineBabelCall(InlineBabelCallIter<'r, 's>),
InlineSourceBlock(InlineSourceBlockIter<'r, 's>),
LineBreak(LineBreakIter<'r, 's>),
Target(TargetIter<'r, 's>),
StatisticsCookie(StatisticsCookieIter<'r, 's>),
Subscript(SubscriptIter<'r, 's>),
Superscript(SuperscriptIter<'r, 's>),
TableCell(TableCellIter<'r, 's>),
Timestamp(TimestampIter<'r, 's>),
}
impl<'r, 's> AstNodeIter<'r, 's> {
pub(crate) fn from_ast_node(node: &AstNode<'r, 's>) -> AstNodeIter<'r, 's> {
match node {
AstNode::Document(inner) => AstNodeIter::Document(inner.into_iter()),
AstNode::Heading(inner) => AstNodeIter::Heading(inner.into_iter()),
AstNode::Section(inner) => AstNodeIter::Section(inner.into_iter()),
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::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())
}
AstNode::Comment(inner) => AstNodeIter::Comment(inner.into_iter()),
AstNode::Drawer(inner) => AstNodeIter::Drawer(inner.into_iter()),
AstNode::PropertyDrawer(inner) => AstNodeIter::PropertyDrawer(inner.into_iter()),
AstNode::NodeProperty(inner) => AstNodeIter::NodeProperty(inner.into_iter()),
AstNode::Table(inner) => AstNodeIter::Table(inner.into_iter()),
AstNode::TableRow(inner) => AstNodeIter::TableRow(inner.into_iter()),
AstNode::VerseBlock(inner) => AstNodeIter::VerseBlock(inner.into_iter()),
AstNode::CommentBlock(inner) => AstNodeIter::CommentBlock(inner.into_iter()),
AstNode::ExampleBlock(inner) => AstNodeIter::ExampleBlock(inner.into_iter()),
AstNode::ExportBlock(inner) => AstNodeIter::ExportBlock(inner.into_iter()),
AstNode::SrcBlock(inner) => AstNodeIter::SrcBlock(inner.into_iter()),
AstNode::Clock(inner) => AstNodeIter::Clock(inner.into_iter()),
AstNode::DiarySexp(inner) => AstNodeIter::DiarySexp(inner.into_iter()),
AstNode::Planning(inner) => AstNodeIter::Planning(inner.into_iter()),
AstNode::FixedWidthArea(inner) => AstNodeIter::FixedWidthArea(inner.into_iter()),
AstNode::HorizontalRule(inner) => AstNodeIter::HorizontalRule(inner.into_iter()),
AstNode::Keyword(inner) => AstNodeIter::Keyword(inner.into_iter()),
AstNode::BabelCall(inner) => AstNodeIter::BabelCall(inner.into_iter()),
AstNode::LatexEnvironment(inner) => AstNodeIter::LatexEnvironment(inner.into_iter()),
AstNode::Bold(inner) => AstNodeIter::Bold(inner.into_iter()),
AstNode::Italic(inner) => AstNodeIter::Italic(inner.into_iter()),
AstNode::Underline(inner) => AstNodeIter::Underline(inner.into_iter()),
AstNode::StrikeThrough(inner) => AstNodeIter::StrikeThrough(inner.into_iter()),
AstNode::Code(inner) => AstNodeIter::Code(inner.into_iter()),
AstNode::Verbatim(inner) => AstNodeIter::Verbatim(inner.into_iter()),
AstNode::PlainText(inner) => AstNodeIter::PlainText(inner.into_iter()),
AstNode::RegularLink(inner) => AstNodeIter::RegularLink(inner.into_iter()),
AstNode::RadioLink(inner) => AstNodeIter::RadioLink(inner.into_iter()),
AstNode::RadioTarget(inner) => AstNodeIter::RadioTarget(inner.into_iter()),
AstNode::PlainLink(inner) => AstNodeIter::PlainLink(inner.into_iter()),
AstNode::AngleLink(inner) => AstNodeIter::AngleLink(inner.into_iter()),
AstNode::OrgMacro(inner) => AstNodeIter::OrgMacro(inner.into_iter()),
AstNode::Entity(inner) => AstNodeIter::Entity(inner.into_iter()),
AstNode::LatexFragment(inner) => AstNodeIter::LatexFragment(inner.into_iter()),
AstNode::ExportSnippet(inner) => AstNodeIter::ExportSnippet(inner.into_iter()),
AstNode::FootnoteReference(inner) => AstNodeIter::FootnoteReference(inner.into_iter()),
AstNode::Citation(inner) => AstNodeIter::Citation(inner.into_iter()),
AstNode::CitationReference(inner) => AstNodeIter::CitationReference(inner.into_iter()),
AstNode::InlineBabelCall(inner) => AstNodeIter::InlineBabelCall(inner.into_iter()),
AstNode::InlineSourceBlock(inner) => AstNodeIter::InlineSourceBlock(inner.into_iter()),
AstNode::LineBreak(inner) => AstNodeIter::LineBreak(inner.into_iter()),
AstNode::Target(inner) => AstNodeIter::Target(inner.into_iter()),
AstNode::StatisticsCookie(inner) => AstNodeIter::StatisticsCookie(inner.into_iter()),
AstNode::Subscript(inner) => AstNodeIter::Subscript(inner.into_iter()),
AstNode::Superscript(inner) => AstNodeIter::Superscript(inner.into_iter()),
AstNode::TableCell(inner) => AstNodeIter::TableCell(inner.into_iter()),
AstNode::Timestamp(inner) => AstNodeIter::Timestamp(inner.into_iter()),
}
}
}
multi_field_iter!(
Document<'s>,
DocumentIter,
zeroth_section,
std::option::Iter<'r, Section<'s>>,
children,
std::slice::Iter<'r, Heading<'s>>
);
multi_field_iter!(
Heading<'s>,
HeadingIter,
title,
std::slice::Iter<'r, Object<'s>>,
children,
std::slice::Iter<'r, DocumentElement<'s>>
);
children_iter!(Section<'s>, SectionIter, std::slice::Iter<'r, Element<'s>>);
children_iter!(
Paragraph<'s>,
ParagraphIter,
std::slice::Iter<'r, Object<'s>>
);
children_iter!(
PlainList<'s>,
PlainListIter,
std::slice::Iter<'r, PlainListItem<'s>>
);
multi_field_iter!(
PlainListItem<'s>,
PlainListItemIter,
tag,
std::slice::Iter<'r, Object<'s>>,
children,
std::slice::Iter<'r, Element<'s>>
);
children_iter!(
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!(
DynamicBlock<'s>,
DynamicBlockIter,
std::slice::Iter<'r, Element<'s>>
);
children_iter!(
FootnoteDefinition<'s>,
FootnoteDefinitionIter,
std::slice::Iter<'r, Element<'s>>
);
empty_iter!(Comment<'s>, CommentIter);
children_iter!(Drawer<'s>, DrawerIter, std::slice::Iter<'r, Element<'s>>);
children_iter!(
PropertyDrawer<'s>,
PropertyDrawerIter,
std::slice::Iter<'r, NodeProperty<'s>>
);
empty_iter!(NodeProperty<'s>, NodePropertyIter);
children_iter!(Table<'s>, TableIter, std::slice::Iter<'r, TableRow<'s>>);
children_iter!(
TableRow<'s>,
TableRowIter,
std::slice::Iter<'r, TableCell<'s>>
);
children_iter!(
VerseBlock<'s>,
VerseBlockIter,
std::slice::Iter<'r, Object<'s>>
);
empty_iter!(CommentBlock<'s>, CommentBlockIter);
empty_iter!(ExampleBlock<'s>, ExampleBlockIter);
empty_iter!(ExportBlock<'s>, ExportBlockIter);
empty_iter!(SrcBlock<'s>, SrcBlockIter);
empty_iter!(Clock<'s>, ClockIter);
empty_iter!(DiarySexp<'s>, DiarySexpIter);
empty_iter!(Planning<'s>, PlanningIter);
empty_iter!(FixedWidthArea<'s>, FixedWidthAreaIter);
empty_iter!(HorizontalRule<'s>, HorizontalRuleIter);
empty_iter!(Keyword<'s>, KeywordIter);
empty_iter!(BabelCall<'s>, BabelCallIter);
empty_iter!(LatexEnvironment<'s>, LatexEnvironmentIter);
children_iter!(Bold<'s>, BoldIter, std::slice::Iter<'r, Object<'s>>);
children_iter!(Italic<'s>, ItalicIter, std::slice::Iter<'r, Object<'s>>);
children_iter!(
Underline<'s>,
UnderlineIter,
std::slice::Iter<'r, Object<'s>>
);
children_iter!(
StrikeThrough<'s>,
StrikeThroughIter,
std::slice::Iter<'r, Object<'s>>
);
empty_iter!(Code<'s>, CodeIter);
empty_iter!(Verbatim<'s>, VerbatimIter);
empty_iter!(PlainText<'s>, PlainTextIter);
empty_iter!(RegularLink<'s>, RegularLinkIter);
children_iter!(
RadioLink<'s>,
RadioLinkIter,
std::slice::Iter<'r, Object<'s>>
);
children_iter!(
RadioTarget<'s>,
RadioTargetIter,
std::slice::Iter<'r, Object<'s>>
);
empty_iter!(PlainLink<'s>, PlainLinkIter);
empty_iter!(AngleLink<'s>, AngleLinkIter);
empty_iter!(OrgMacro<'s>, OrgMacroIter);
empty_iter!(Entity<'s>, EntityIter);
empty_iter!(LatexFragment<'s>, LatexFragmentIter);
empty_iter!(ExportSnippet<'s>, ExportSnippetIter);
multi_field_iter!(
FootnoteReference<'s>,
FootnoteReferenceIter,
definition,
std::slice::Iter<'r, Object<'s>>,
);
empty_iter!(Citation<'s>, CitationIter);
empty_iter!(CitationReference<'s>, CitationReferenceIter);
empty_iter!(InlineBabelCall<'s>, InlineBabelCallIter);
empty_iter!(InlineSourceBlock<'s>, InlineSourceBlockIter);
empty_iter!(LineBreak<'s>, LineBreakIter);
empty_iter!(Target<'s>, TargetIter);
empty_iter!(StatisticsCookie<'s>, StatisticsCookieIter);
empty_iter!(Subscript<'s>, SubscriptIter);
empty_iter!(Superscript<'s>, SuperscriptIter);
children_iter!(
TableCell<'s>,
TableCellIter,
std::slice::Iter<'r, Object<'s>>
);
empty_iter!(Timestamp<'s>, TimestampIter);

101
src/iter/macros.rs Normal file
View File

@@ -0,0 +1,101 @@
/// Create iterators for ast nodes where it only has to iterate over children
macro_rules! children_iter {
($astnodetype:ty, $itertype:ident, $innertype:ty) => {
pub struct $itertype<'r, 's> {
next: $innertype,
}
impl<'r, 's> Iterator for $itertype<'r, 's> {
type Item = AstNode<'r, 's>;
fn next(&mut self) -> Option<Self::Item> {
self.next.next().map(Into::<AstNode>::into)
}
}
impl<'r, 's> IntoIterator for &'r $astnodetype {
type Item = AstNode<'r, 's>;
type IntoIter = $itertype<'r, 's>;
fn into_iter(self) -> Self::IntoIter {
$itertype {
next: self.children.iter(),
}
}
}
};
}
pub(crate) use children_iter;
/// Create iterators for ast nodes that do not contain any ast node children.
macro_rules! empty_iter {
($astnodetype:ty, $itertype:ident) => {
pub struct $itertype<'r, 's> {
phantom: PhantomData<&'r $astnodetype>,
}
impl<'r, 's> Iterator for $itertype<'r, 's> {
type Item = AstNode<'r, 's>;
fn next(&mut self) -> Option<Self::Item> {
None
}
}
impl<'r, 's> IntoIterator for &'r $astnodetype {
type Item = AstNode<'r, 's>;
type IntoIter = $itertype<'r, 's>;
fn into_iter(self) -> Self::IntoIter {
$itertype {
phantom: PhantomData,
}
}
}
};
}
pub(crate) use empty_iter;
/// Create iterators for ast nodes where it has to iterate over multiple child lists.
macro_rules! multi_field_iter {
($astnodetype:ty, $itertype:ident, $firstfieldname: ident, $firstinnertype:ty, $($fieldname: ident, $innertype:ty),*) => {
pub struct $itertype<'r, 's> {
$firstfieldname: $firstinnertype,
$(
$fieldname: $innertype,
),*
}
impl<'r, 's> Iterator for $itertype<'r, 's> {
type Item = AstNode<'r, 's>;
fn next(&mut self) -> Option<Self::Item> {
self.$firstfieldname.next().map(Into::<AstNode>::into)
$(
.or_else(|| self.$fieldname.next().map(Into::<AstNode>::into))
),*
}
}
impl<'r, 's> IntoIterator for &'r $astnodetype {
type Item = AstNode<'r, 's>;
type IntoIter = $itertype<'r, 's>;
fn into_iter(self) -> Self::IntoIter {
$itertype {
$firstfieldname: self.$firstfieldname.iter(),
$(
$fieldname: self.$fieldname.iter(),
),*
}
}
}
};
}
pub(crate) use multi_field_iter;

3
src/iter/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod all_ast_node_iter;
mod ast_node_iter;
mod macros;

View File

@@ -1,6 +1,6 @@
#![feature(round_char_boundary)]
#![feature(exit_status_error)] #![feature(exit_status_error)]
#![feature(trait_alias)] #![feature(trait_alias)]
#![feature(path_file_prefix)]
// TODO: #![warn(missing_docs)] // TODO: #![warn(missing_docs)]
#[cfg(feature = "compare")] #[cfg(feature = "compare")]
@@ -8,9 +8,14 @@ pub mod compare;
mod context; mod context;
mod error; mod error;
mod iter;
pub mod parser; pub mod parser;
pub mod types; pub mod types;
pub use context::FileAccessInterface; pub mod settings {
pub use context::GlobalSettings; pub use crate::context::FileAccessInterface;
pub use context::LocalFileAccessInterface; 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;
use organic::parser::parse_with_settings; use organic::parser::parse_with_settings;
use organic::GlobalSettings; use organic::settings::GlobalSettings;
use organic::LocalFileAccessInterface; use organic::settings::LocalFileAccessInterface;
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
use crate::init_tracing::init_telemetry; use crate::init_tracing::init_telemetry;

View File

@@ -187,7 +187,7 @@ mod tests {
use crate::context::List; use crate::context::List;
use crate::parser::element_parser::element; use crate::parser::element_parser::element;
use crate::types::Element; use crate::types::Element;
use crate::types::Source; use crate::types::GetStandardProperties;
#[test] #[test]
fn citation_simple() { fn citation_simple() {
@@ -202,7 +202,10 @@ mod tests {
_ => panic!("Should be a paragraph!"), _ => panic!("Should be a paragraph!"),
}; };
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(first_paragraph.get_source(), "[cite:@foo]"); assert_eq!(
first_paragraph.get_standard_properties().get_source(),
"[cite:@foo]"
);
assert_eq!(first_paragraph.children.len(), 1); assert_eq!(first_paragraph.children.len(), 1);
assert_eq!( assert_eq!(
first_paragraph first_paragraph

View File

@@ -1,18 +1,19 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::line_ending; use nom::character::complete::line_ending;
use nom::character::complete::space0; use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::eof; use nom::combinator::eof;
use nom::combinator::not; use nom::combinator::not;
use nom::combinator::opt; use nom::combinator::recognize;
use nom::multi::many0; use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::preceded; use nom::sequence::preceded;
use nom::sequence::tuple; use nom::sequence::tuple;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::util::get_consumed; use super::util::get_consumed;
use super::util::org_line_ending;
use crate::context::parser_with_context; use crate::context::parser_with_context;
use crate::context::ContextElement; use crate::context::ContextElement;
use crate::context::RefContext; 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 parser_context = context.with_additional_node(&parser_context);
let comment_line_matcher = parser_with_context!(comment_line)(&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 exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
let (remaining, _first_line) = comment_line_matcher(input)?; let (remaining, first_line) = comment_line_matcher(input)?;
let (remaining, _remaining_lines) = let (remaining, mut remaining_lines) =
many0(preceded(not(exit_matcher), comment_line_matcher))(remaining)?; many0(preceded(not(exit_matcher), comment_line_matcher))(remaining)?;
let source = get_consumed(input, 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(( Ok((
remaining, remaining,
Comment { Comment {
source: source.into(), source: source.into(),
value,
}, },
)) ))
} }
@@ -57,14 +72,13 @@ fn comment_line<'b, 'g, 'r, 's>(
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
start_of_line(input)?; start_of_line(input)?;
let (remaining, _indent) = space0(input)?; let (remaining, _) = tuple((space0, tag("#")))(input)?;
let (remaining, (_hash, _leading_whitespace_and_content, _line_ending)) = tuple(( if let Ok((remaining, line_break)) = org_line_ending(remaining) {
tag("#"), return Ok((remaining, line_break));
opt(tuple((space1, is_not("\r\n")))), }
alt((line_ending, eof)), let (remaining, _) = tag(" ")(remaining)?;
))(remaining)?; let (remaining, value) = recognize(many_till(anychar, org_line_ending))(remaining)?;
let source = get_consumed(input, remaining); Ok((remaining, value))
Ok((remaining, source))
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -1,14 +1,15 @@
use std::path::Path;
use nom::combinator::all_consuming; use nom::combinator::all_consuming;
use nom::combinator::opt; use nom::combinator::opt;
use nom::multi::many0; use nom::multi::many0;
use super::headline::heading; use super::headline::heading;
use super::in_buffer_settings::apply_in_buffer_settings; 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::in_buffer_settings::scan_for_in_buffer_settings;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::section::zeroth_section; use super::section::zeroth_section;
use super::token::AllTokensIterator;
use super::token::Token;
use super::util::get_consumed; use super::util::get_consumed;
use crate::context::parser_with_context; use crate::context::parser_with_context;
use crate::context::Context; use crate::context::Context;
@@ -21,35 +22,77 @@ use crate::error::MyError;
use crate::error::Res; use crate::error::Res;
use crate::parser::org_source::convert_error; use crate::parser::org_source::convert_error;
use crate::parser::util::blank_line; use crate::parser::util::blank_line;
use crate::types::AstNode;
use crate::types::Document; use crate::types::Document;
use crate::types::Object; use crate::types::Object;
/// Parse a full org-mode document. /// 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)] #[allow(dead_code)]
pub fn parse<'s>(input: &'s str) -> Result<Document<'s>, Box<dyn std::error::Error>> { 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. /// 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". /// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO".
#[allow(dead_code)] #[allow(dead_code)]
pub fn parse_with_settings<'g, 's>( pub fn parse_with_settings<'g, 's>(
input: &'s str, input: &'s str,
global_settings: &'g GlobalSettings<'g, 's>, 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>> { ) -> Result<Document<'s>, Box<dyn std::error::Error>> {
let initial_context = ContextElement::document_context(); let initial_context = ContextElement::document_context();
let initial_context = Context::new(global_settings, List::new(&initial_context)); let initial_context = Context::new(global_settings, List::new(&initial_context));
let wrapped_input = OrgSource::new(input); let wrapped_input = OrgSource::new(input);
let ret = let mut doc =
all_consuming(parser_with_context!(document_org_source)(&initial_context))(wrapped_input) all_consuming(parser_with_context!(document_org_source)(&initial_context))(wrapped_input)
.map_err(|err| err.to_string()) .map_err(|err| err.to_string())
.map(|(_remaining, parsed_document)| parsed_document); .map(|(_remaining, parsed_document)| parsed_document)?;
Ok(ret?) 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. /// Parse a full org-mode document.
@@ -107,19 +150,18 @@ fn document_org_source<'b, 'g, 'r, 's>(
let new_context = context.with_global_settings(&new_settings); let new_context = context.with_global_settings(&new_settings);
let context = &new_context; let context = &new_context;
let (remaining, document) = let (remaining, mut document) =
_document(context, input).map(|(rem, out)| (Into::<&str>::into(rem), out))?; _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. // If there are radio targets in this document then we need to parse the entire document again with the knowledge of the radio targets.
let all_radio_targets: Vec<&Vec<Object<'_>>> = document let all_radio_targets: Vec<&Vec<Object<'_>>> = Into::<AstNode>::into(&document)
.iter_tokens() .into_iter()
.filter_map(|tkn| match tkn { .filter_map(|ast_node| {
Token::Object(obj) => Some(obj), if let AstNode::RadioTarget(ast_node) = ast_node {
_ => None, Some(ast_node)
}) } else {
.filter_map(|obj| match obj { None
Object::RadioTarget(rt) => Some(rt), }
_ => None,
}) })
.map(|rt| &rt.children) .map(|rt| &rt.children)
.collect(); .collect();
@@ -127,11 +169,18 @@ fn document_org_source<'b, 'g, 'r, 's>(
let mut new_global_settings = context.get_global_settings().clone(); let mut new_global_settings = context.get_global_settings().clone();
new_global_settings.radio_targets = all_radio_targets; new_global_settings.radio_targets = all_radio_targets;
let parser_context = context.with_global_settings(&new_global_settings); 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))?; .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)); 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)) Ok((remaining.into(), document))
} }
@@ -150,14 +199,10 @@ fn _document<'b, 'g, 'r, 's>(
remaining, remaining,
Document { Document {
source: source.into(), source: source.into(),
category: None,
path: None,
zeroth_section, zeroth_section,
children, children,
}, },
)) ))
} }
impl<'s> Document<'s> {
fn iter_tokens<'r>(&'r self) -> impl Iterator<Item = Token<'r, 's>> {
AllTokensIterator::new(Token::Document(self))
}
}

View File

@@ -1,14 +1,20 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::is_not; use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case; use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar;
use nom::character::complete::line_ending; use nom::character::complete::line_ending;
use nom::character::complete::space0; use nom::character::complete::space0;
use nom::character::complete::space1; use nom::character::complete::space1;
use nom::combinator::consumed;
use nom::combinator::eof; use nom::combinator::eof;
use nom::combinator::not; use nom::combinator::not;
use nom::combinator::opt; use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize; use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till; use nom::multi::many_till;
use nom::sequence::preceded;
use nom::sequence::tuple; use nom::sequence::tuple;
use super::org_source::OrgSource; use super::org_source::OrgSource;
@@ -36,7 +42,6 @@ pub(crate) fn dynamic_block<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, DynamicBlock<'s>> { ) -> Res<OrgSource<'s>, DynamicBlock<'s>> {
// TODO: Do I need to differentiate between different dynamic block types.
if immediate_in_section(context, "dynamic block") { if immediate_in_section(context, "dynamic block") {
return Err(nom::Err::Error(CustomError::MyError(MyError( return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(), "Cannot nest objects of the same element".into(),
@@ -44,10 +49,11 @@ pub(crate) fn dynamic_block<'b, 'g, 'r, 's>(
} }
start_of_line(input)?; start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(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))), recognize(tuple((tag_no_case("#+begin:"), space1))),
name, name,
opt(tuple((space1, parameters))), opt(tuple((space1, parameters))),
space0,
line_ending, line_ending,
))(remaining)?; ))(remaining)?;
let contexts = [ let contexts = [
@@ -67,24 +73,23 @@ pub(crate) fn dynamic_block<'b, 'g, 'r, 's>(
}; };
let element_matcher = parser_with_context!(element(true))(&parser_context); let element_matcher = parser_with_context!(element(true))(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
let (remaining, children) = match tuple(( not(exit_matcher)(remaining)?;
not(exit_matcher), let (remaining, leading_blank_lines) = opt(consumed(tuple((
blank_line, blank_line,
many_till(blank_line, exit_matcher), many0(preceded(not(exit_matcher), blank_line)),
))(remaining) ))))(remaining)?;
{ let leading_blank_lines =
Ok((remain, (_not_immediate_exit, first_line, (_trailing_whitespace, _exit_contents)))) => { leading_blank_lines.map(|(source, (first_line, _remaining_lines))| {
let mut element = Element::Paragraph(Paragraph::of_text(first_line.into())); let mut element = Element::Paragraph(Paragraph::of_text(first_line.into()));
let source = get_consumed(remaining, remain);
element.set_source(source.into()); element.set_source(source.into());
(remain, vec![element]) element
} });
Err(_) => { let (remaining, (mut children, _exit_contents)) =
let (remaining, (children, _exit_contents)) = many_till(element_matcher, exit_matcher)(remaining)?;
many_till(element_matcher, exit_matcher)(remaining)?; if let Some(lines) = leading_blank_lines {
(remaining, children) children.insert(0, lines);
} }
};
let (remaining, _end) = dynamic_block_end(&parser_context, remaining)?; let (remaining, _end) = dynamic_block_end(&parser_context, remaining)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
@@ -106,7 +111,7 @@ fn name<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn parameters<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> { 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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -117,7 +122,8 @@ fn dynamic_block_end<'b, 'g, 'r, 's>(
start_of_line(input)?; start_of_line(input)?;
let (remaining, source) = recognize(tuple(( let (remaining, source) = recognize(tuple((
space0, space0,
tag_no_case("#+end:"), tag_no_case("#+end"),
opt(tag(":")),
alt((eof, line_ending)), alt((eof, line_ending)),
)))(input)?; )))(input)?;
Ok((remaining, source)) Ok((remaining, source))

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 affiliated_keywords) = many0(affiliated_keyword_matcher)(input)?;
let (remaining, mut element) = match alt(( let (remaining, mut element) = match alt((
map(plain_list_matcher, Element::PlainList), map(plain_list_matcher, Element::PlainList),
map(greater_block_matcher, Element::GreaterBlock), greater_block_matcher,
map(dynamic_block_matcher, Element::DynamicBlock), map(dynamic_block_matcher, Element::DynamicBlock),
map(footnote_definition_matcher, Element::FootnoteDefinition), map(footnote_definition_matcher, Element::FootnoteDefinition),
map(comment_matcher, Element::Comment), map(comment_matcher, Element::Comment),
@@ -107,6 +107,7 @@ fn _element<'b, 'g, 'r, 's>(
match map(paragraph_matcher, Element::Paragraph)(remaining) { match map(paragraph_matcher, Element::Paragraph)(remaining) {
the_ok @ Ok(_) => the_ok, the_ok @ Ok(_) => the_ok,
Err(_) => { Err(_) => {
// TODO: Because this function expects a single element, if there are multiple affiliated keywords before an element that cannot have affiliated keywords, we end up re-parsing the affiliated keywords many times.
affiliated_keywords.clear(); affiliated_keywords.clear();
map(affiliated_keyword_matcher, Element::Keyword)(input) map(affiliated_keyword_matcher, Element::Keyword)(input)
} }
@@ -141,7 +142,7 @@ fn _detect_element<'b, 'g, 'r, 's>(
can_be_paragraph: bool, can_be_paragraph: bool,
) -> Res<OrgSource<'s>, ()> { ) -> Res<OrgSource<'s>, ()> {
if alt(( if alt((
detect_plain_list, parser_with_context!(detect_plain_list)(context),
detect_footnote_definition, detect_footnote_definition,
detect_diary_sexp, detect_diary_sexp,
detect_comment, detect_comment,

View File

@@ -1,10 +1,10 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::satisfy; use nom::character::complete::satisfy;
use nom::combinator::eof; use nom::combinator::eof;
use nom::combinator::peek; use nom::combinator::peek;
use nom::combinator::recognize; use nom::combinator::recognize;
use nom::sequence::tuple;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting; use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting;
@@ -439,7 +439,7 @@ pub(crate) fn entity<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, Entity<'s>> { ) -> Res<OrgSource<'s>, Entity<'s>> {
let (remaining, _) = tag("\\")(input)?; let (remaining, _) = tag("\\")(input)?;
let (remaining, entity_name) = name(context, remaining)?; let (remaining, entity_name) = name(context, remaining)?;
let (remaining, _) = alt((tag("{}"), peek(recognize(entity_end))))(remaining)?;
let (remaining, _trailing_whitespace) = let (remaining, _trailing_whitespace) =
maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?;
@@ -460,9 +460,12 @@ fn name<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
// TODO: This should be defined by org-entities and optionally org-entities-user // TODO: This should be defined by org-entities and optionally org-entities-user
for entity in ORG_ENTITIES { for entity in ORG_ENTITIES {
let result = tag_no_case::<_, _, CustomError<_>>(entity)(input); let result = tuple((
tag::<_, _, CustomError<_>>(entity),
alt((tag("{}"), peek(recognize(entity_end)))),
))(input);
match result { match result {
Ok((remaining, ent)) => { Ok((remaining, (ent, _))) => {
return Ok((remaining, ent)); return Ok((remaining, ent));
} }
Err(_) => {} Err(_) => {}

View File

@@ -3,15 +3,16 @@ use nom::bytes::complete::is_not;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::character::complete::line_ending; use nom::character::complete::line_ending;
use nom::character::complete::space0; use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::eof; use nom::combinator::eof;
use nom::combinator::not; use nom::combinator::not;
use nom::combinator::opt; use nom::combinator::recognize;
use nom::multi::many0; use nom::multi::many0;
use nom::sequence::preceded; use nom::sequence::preceded;
use nom::sequence::tuple; use nom::sequence::tuple;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::util::only_space1;
use super::util::org_line_ending;
use crate::context::parser_with_context; use crate::context::parser_with_context;
use crate::context::RefContext; use crate::context::RefContext;
use crate::error::Res; use crate::error::Res;
@@ -47,10 +48,10 @@ fn fixed_width_area_line<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
start_of_line(input)?; start_of_line(input)?;
let (remaining, _indent) = space0(input)?; let (remaining, _indent) = space0(input)?;
let (remaining, (_colon, _leading_whitespace_and_content, _line_ending)) = tuple(( let (remaining, _) = tuple((
tag(":"), tag(":"),
opt(tuple((space1, is_not("\r\n")))), alt((recognize(tuple((only_space1, is_not("\r\n")))), space0)),
alt((line_ending, eof)), org_line_ending,
))(remaining)?; ))(remaining)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok((remaining, source)) Ok((remaining, source))

View File

@@ -2,7 +2,6 @@ use nom::branch::alt;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case; use nom::bytes::complete::tag_no_case;
use nom::bytes::complete::take_while; use nom::bytes::complete::take_while;
use nom::character::complete::digit1;
use nom::character::complete::space0; use nom::character::complete::space0;
use nom::combinator::opt; use nom::combinator::opt;
use nom::combinator::recognize; use nom::combinator::recognize;
@@ -94,10 +93,7 @@ pub(crate) fn footnote_definition<'b, 'g, 'r, 's>(
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn label<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> { pub(crate) fn label<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt(( take_while(|c| WORD_CONSTITUENT_CHARACTERS.contains(c) || "-_".contains(c))(input)
digit1,
take_while(|c| WORD_CONSTITUENT_CHARACTERS.contains(c) || "-_".contains(c)),
))(input)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -133,7 +129,7 @@ mod tests {
use crate::context::Context; use crate::context::Context;
use crate::context::GlobalSettings; use crate::context::GlobalSettings;
use crate::context::List; use crate::context::List;
use crate::types::Source; use crate::types::GetStandardProperties;
#[test] #[test]
fn two_paragraphs() { fn two_paragraphs() {
@@ -154,13 +150,17 @@ line footnote.",
footnote_definition_matcher(remaining).expect("Parse second footnote_definition."); footnote_definition_matcher(remaining).expect("Parse second footnote_definition.");
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!( assert_eq!(
first_footnote_definition.get_source(), first_footnote_definition
.get_standard_properties()
.get_source(),
"[fn:1] A footnote. "[fn:1] A footnote.
" "
); );
assert_eq!( assert_eq!(
second_footnote_definition.get_source(), second_footnote_definition
.get_standard_properties()
.get_source(),
"[fn:2] A multi- "[fn:2] A multi-
line footnote." line footnote."
@@ -185,7 +185,9 @@ not in the footnote.",
footnote_definition_matcher(input).expect("Parse first footnote_definition"); footnote_definition_matcher(input).expect("Parse first footnote_definition");
assert_eq!(Into::<&str>::into(remaining), "not in the footnote."); assert_eq!(Into::<&str>::into(remaining), "not in the footnote.");
assert_eq!( assert_eq!(
first_footnote_definition.get_source(), first_footnote_definition
.get_standard_properties()
.get_source(),
"[fn:2] A multi- "[fn:2] A multi-
line footnote. line footnote.

View File

@@ -1,14 +1,20 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::is_not; use nom::bytes::complete::is_not;
use nom::bytes::complete::tag_no_case; use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar;
use nom::character::complete::line_ending; use nom::character::complete::line_ending;
use nom::character::complete::space0; use nom::character::complete::space0;
use nom::character::complete::space1; use nom::character::complete::space1;
use nom::combinator::consumed;
use nom::combinator::eof; use nom::combinator::eof;
use nom::combinator::not; use nom::combinator::not;
use nom::combinator::opt; use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify; use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many_till; use nom::multi::many_till;
use nom::sequence::preceded;
use nom::sequence::tuple; use nom::sequence::tuple;
use super::org_source::OrgSource; use super::org_source::OrgSource;
@@ -27,17 +33,18 @@ use crate::parser::util::blank_line;
use crate::parser::util::exit_matcher_parser; use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed; use crate::parser::util::get_consumed;
use crate::parser::util::start_of_line; use crate::parser::util::start_of_line;
use crate::types::CenterBlock;
use crate::types::Element; use crate::types::Element;
use crate::types::GreaterBlock;
use crate::types::Paragraph; use crate::types::Paragraph;
use crate::types::QuoteBlock;
use crate::types::SetSource; use crate::types::SetSource;
use crate::types::SpecialBlock;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn greater_block<'b, 'g, 'r, 's>( pub(crate) fn greater_block<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, GreaterBlock<'s>> { ) -> Res<OrgSource<'s>, Element<'s>> {
// TODO: Do I need to differentiate between different greater block types.
start_of_line(input)?; start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(input)?; let (remaining, _leading_whitespace) = space0(input)?;
let (remaining, (_begin, name)) = tuple(( let (remaining, (_begin, name)) = tuple((
@@ -49,22 +56,97 @@ pub(crate) fn greater_block<'b, 'g, 'r, 's>(
} }
}), }),
))(remaining)?; ))(remaining)?;
let context_name = match Into::<&str>::into(name).to_lowercase().as_str() { let name = Into::<&str>::into(name);
"center" => "center block".to_owned(), let (remaining, element) = match name.to_lowercase().as_str() {
"quote" => "quote block".to_owned(), "center" => center_block(context, remaining, input)?,
name @ _ => format!("special block {}", name), "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( return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(), "Cannot nest objects of the same element".into(),
)))); ))));
} }
let exit_with_name = greater_block_end(name.into()); let exit_with_name = greater_block_end(name);
let (remaining, parameters) = opt(tuple((space1, parameters)))(remaining)?; let (remaining, _nl) = tuple((space0, line_ending))(input)?;
let (remaining, _nl) = line_ending(remaining)?;
let contexts = [ let contexts = [
ContextElement::ConsumeTrailingWhitespace(true), ContextElement::ConsumeTrailingWhitespace(true),
ContextElement::Context(context_name.as_str()), ContextElement::Context(context_name),
ContextElement::ExitMatcherNode(ExitMatcherNode { ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Alpha, class: ExitClass::Alpha,
exit_matcher: &exit_with_name, exit_matcher: &exit_with_name,
@@ -73,46 +155,31 @@ pub(crate) fn greater_block<'b, 'g, 'r, 's>(
let parser_context = context.with_additional_node(&contexts[0]); 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[1]);
let parser_context = parser_context.with_additional_node(&contexts[2]); 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 element_matcher = parser_with_context!(element(true))(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
// Check for a completely empty block not(exit_matcher)(remaining)?;
let (remaining, children) = match tuple(( let (remaining, leading_blank_lines) = opt(consumed(tuple((
not(exit_matcher),
blank_line, blank_line,
many_till(blank_line, exit_matcher), many0(preceded(not(exit_matcher), blank_line)),
))(remaining) ))))(remaining)?;
{ let leading_blank_lines =
Ok((remain, (_not_immediate_exit, first_line, (_trailing_whitespace, _exit_contents)))) => { leading_blank_lines.map(|(source, (first_line, _remaining_lines))| {
let mut element = Element::Paragraph(Paragraph::of_text(first_line.into())); let mut element = Element::Paragraph(Paragraph::of_text(first_line.into()));
let source = get_consumed(remaining, remain);
element.set_source(source.into()); element.set_source(source.into());
(remain, vec![element]) element
} });
Err(_) => { let (remaining, (mut children, _exit_contents)) =
let (remaining, (children, _exit_contents)) = many_till(element_matcher, exit_matcher)(remaining)?;
many_till(element_matcher, exit_matcher)(remaining)?; if let Some(lines) = leading_blank_lines {
(remaining, children) children.insert(0, lines);
} }
};
let (remaining, _end) = exit_with_name(&parser_context, remaining)?; let (remaining, _end) = exit_with_name(&parser_context, remaining)?;
// Not checking if parent exit matcher is causing exit because the greater_block_end matcher asserts we matched a full greater block // 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); let source = get_consumed(original_input, remaining);
Ok(( Ok((remaining, (Into::<&str>::into(source), children)))
remaining,
GreaterBlock {
source: source.into(),
name: name.into(),
parameters: parameters.map(|val| Into::<&str>::into(val)),
children,
},
))
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -122,11 +189,10 @@ fn name<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn parameters<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> { 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 { fn greater_block_end<'c>(name: &'c str) -> impl ContextMatcher + 'c {
// TODO: Can this be done without making an owned copy?
move |context, input: OrgSource<'_>| _greater_block_end(context, input, name) move |context, input: OrgSource<'_>| _greater_block_end(context, input, name)
} }

View File

@@ -1,24 +1,27 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::is_a;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::character::complete::anychar; use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::space0; use nom::character::complete::space0;
use nom::character::complete::space1; use nom::character::complete::space1;
use nom::combinator::eof; use nom::combinator::consumed;
use nom::combinator::map; use nom::combinator::map;
use nom::combinator::not; use nom::combinator::not;
use nom::combinator::opt; use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize; use nom::combinator::recognize;
use nom::combinator::verify; use nom::combinator::verify;
use nom::multi::many0; use nom::multi::many0;
use nom::multi::many1; use nom::multi::many1;
use nom::multi::many1_count;
use nom::multi::separated_list1; use nom::multi::separated_list1;
use nom::sequence::tuple; use nom::sequence::tuple;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::section::section; use super::section::section;
use super::util::get_consumed; use super::util::get_consumed;
use super::util::org_line_ending;
use super::util::org_space;
use super::util::org_space_or_line_ending;
use super::util::start_of_line; use super::util::start_of_line;
use crate::context::parser_with_context; use crate::context::parser_with_context;
use crate::context::ContextElement; use crate::context::ContextElement;
@@ -31,39 +34,49 @@ use crate::error::Res;
use crate::parser::object_parser::standard_set_object; use crate::parser::object_parser::standard_set_object;
use crate::parser::util::blank_line; use crate::parser::util::blank_line;
use crate::types::DocumentElement; use crate::types::DocumentElement;
use crate::types::Element;
use crate::types::Heading; use crate::types::Heading;
use crate::types::HeadlineLevel;
use crate::types::Object; use crate::types::Object;
use crate::types::PriorityCookie; use crate::types::PriorityCookie;
use crate::types::TodoKeywordType; use crate::types::TodoKeywordType;
pub(crate) const fn heading( pub(crate) const fn heading(
parent_stars: usize, parent_level: HeadlineLevel,
) -> impl for<'b, 'g, 'r, 's> Fn( ) -> impl for<'b, 'g, 'r, 's> Fn(
RefContext<'b, 'g, 'r, 's>, RefContext<'b, 'g, 'r, 's>,
OrgSource<'s>, OrgSource<'s>,
) -> Res<OrgSource<'s>, Heading<'s>> { ) -> Res<OrgSource<'s>, Heading<'s>> {
move |context, input: OrgSource<'_>| _heading(context, input, parent_stars) move |context, input: OrgSource<'_>| _heading(context, input, parent_level)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _heading<'b, 'g, 'r, 's>( fn _heading<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
parent_stars: usize, parent_star_count: HeadlineLevel,
) -> Res<OrgSource<'s>, Heading<'s>> { ) -> 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)?; not(|i| context.check_exit_matcher(i))(input)?;
let ( let (remaining, pre_headline) = headline(context, input, parent_star_count)?;
remaining,
(star_count, maybe_todo_keyword, maybe_priority, maybe_comment, title, heading_tags),
) = headline(context, input, parent_stars)?;
let section_matcher = parser_with_context!(section)(context); 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) = let (remaining, maybe_section) =
opt(map(section_matcher, DocumentElement::Section))(remaining)?; opt(map(section_matcher, DocumentElement::Section))(remaining)?;
let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?; let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?;
let (remaining, mut children) = let (remaining, mut children) =
many0(map(heading_matcher, DocumentElement::Heading))(remaining)?; many0(map(heading_matcher, DocumentElement::Heading))(remaining)?;
if let Some(section) = maybe_section { 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); children.insert(0, section);
} }
let remaining = if children.is_empty() { let remaining = if children.is_empty() {
@@ -73,23 +86,29 @@ fn _heading<'b, 'g, 'r, 's>(
} else { } else {
remaining remaining
}; };
let is_archived = heading_tags.contains(&"ARCHIVE"); let is_archived = pre_headline.tags.contains(&"ARCHIVE");
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok(( Ok((
remaining, remaining,
Heading { Heading {
source: source.into(), source: source.into(),
stars: star_count, level: pre_headline.headline_level,
todo_keyword: maybe_todo_keyword.map(|((todo_keyword_type, todo_keyword), _ws)| { todo_keyword: pre_headline
(todo_keyword_type, Into::<&str>::into(todo_keyword)) .todo_keyword
}), .map(|(todo_keyword_type, todo_keyword)| {
priority_cookie: maybe_priority.map(|(priority, _)| priority), (todo_keyword_type, Into::<&str>::into(todo_keyword))
title, }),
tags: heading_tags, priority_cookie: pre_headline.priority_cookie.map(|(_, priority)| priority),
title: pre_headline.title,
tags: pre_headline.tags,
children, children,
is_comment: maybe_comment.is_some(), is_comment: pre_headline.comment.is_some(),
is_archived, is_archived,
is_footnote_section: pre_headline.is_footnote_section,
scheduled,
deadline,
closed,
}, },
)) ))
} }
@@ -100,75 +119,95 @@ pub(crate) fn detect_headline<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()
Ok((input, ())) 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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn headline<'b, 'g, 'r, 's>( fn headline<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
parent_stars: usize, parent_star_count: HeadlineLevel,
) -> Res< ) -> Res<OrgSource<'s>, PreHeadline<'s>> {
OrgSource<'s>,
(
usize,
Option<((TodoKeywordType, OrgSource<'s>), OrgSource<'s>)>,
Option<(PriorityCookie, OrgSource<'s>)>,
Option<(OrgSource<'s>, OrgSource<'s>)>,
Vec<Object<'s>>,
Vec<&'s str>,
),
> {
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Document, class: ExitClass::Document,
exit_matcher: &headline_title_end, exit_matcher: &headline_title_end,
}); });
let parser_context = context.with_additional_node(&parser_context); let parser_context = context.with_additional_node(&parser_context);
let ( let (remaining, (_, (headline_level, star_count, _), _)) = tuple((
remaining,
(
_,
star_count,
_,
maybe_todo_keyword,
maybe_priority,
maybe_comment,
title,
maybe_tags,
_,
_,
),
) = tuple((
start_of_line, start_of_line,
verify(many1_count(tag("*")), |star_count| { verify(
*star_count > parent_stars parser_with_context!(headline_level)(&parser_context),
}), |(_, count, _)| *count > parent_star_count,
space1, ),
opt(tuple(( peek(org_space),
parser_with_context!(heading_keyword)(&parser_context),
space1,
))),
opt(tuple((priority_cookie, space1))),
opt(tuple((tag("COMMENT"), space1))),
many1(parser_with_context!(standard_set_object)(&parser_context)),
opt(tuple((space0, tags))),
space0,
alt((line_ending, eof)),
))(input)?; ))(input)?;
let (remaining, maybe_todo_keyword) = opt(tuple((
space1,
parser_with_context!(heading_keyword)(&parser_context),
peek(org_space_or_line_ending),
)))(remaining)?;
let (remaining, maybe_priority) = opt(tuple((space1, priority_cookie)))(remaining)?;
let (remaining, maybe_comment) = opt(tuple((
space1,
tag("COMMENT"),
peek(org_space_or_line_ending),
)))(remaining)?;
let (remaining, maybe_title) = opt(tuple((
space1,
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(( Ok((
remaining, remaining,
( PreHeadline {
headline_level,
star_count, star_count,
maybe_todo_keyword, todo_keyword: maybe_todo_keyword.map(|(_, todo, _)| todo),
maybe_priority, priority_cookie: maybe_priority,
maybe_comment, comment: maybe_comment.map(|(_, comment, _)| comment),
title, title: maybe_title
maybe_tags .map(|(_, (_, title))| title)
.unwrap_or(Vec::new()),
tags: maybe_tags
.map(|(_ws, tags)| { .map(|(_ws, tags)| {
tags.into_iter() tags.into_iter()
.map(|single_tag| Into::<&str>::into(single_tag)) .map(|single_tag| Into::<&str>::into(single_tag))
.collect() .collect()
}) })
.unwrap_or(Vec::new()), .unwrap_or(Vec::new()),
), is_footnote_section,
},
)) ))
} }
@@ -177,10 +216,7 @@ fn headline_title_end<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(tuple(( recognize(tuple((space0, opt(tuple((tags, space0))), org_line_ending)))(input)
opt(tuple((space0, tags, space0))),
alt((line_ending, eof)),
)))(input)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -256,3 +292,23 @@ fn priority_cookie<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PriorityCooki
})?; })?;
Ok((remaining, cookie)) Ok((remaining, cookie))
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn headline_level<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (HeadlineLevel, HeadlineLevel, OrgSource<'s>)> {
let (remaining, stars) = is_a("*")(input)?;
let count = stars.len().try_into().unwrap();
let level = match context.get_global_settings().odd_levels_only {
crate::context::HeadlineLevelFilter::Odd => {
if count % 2 == 0 {
(count + 2) / 2
} else {
(count + 1) / 2
}
}
crate::context::HeadlineLevelFilter::OddEven => count,
};
Ok((remaining, (level, count, stars)))
}

View File

@@ -1,28 +1,59 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag_no_case; use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar; use nom::bytes::complete::take_until;
use nom::combinator::map; use nom::character::complete::space1;
use nom::multi::many0; use nom::multi::separated_list0;
use nom::multi::many_till;
use super::keyword::filtered_keyword; use super::keyword::filtered_keyword;
use super::keyword_todo::todo_keywords; use super::keyword_todo::todo_keywords;
use super::OrgSource; use super::OrgSource;
use crate::context::HeadlineLevelFilter;
use crate::error::CustomError;
use crate::error::Res; use crate::error::Res;
use crate::settings::GlobalSettings;
use crate::types::AstNode;
use crate::types::Document;
use crate::types::Keyword; use crate::types::Keyword;
use crate::GlobalSettings;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn scan_for_in_buffer_settings<'s>( pub(crate) fn scan_for_in_buffer_settings<'s>(
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Vec<Keyword<'s>>> { ) -> Res<OrgSource<'s>, Vec<Keyword<'s>>> {
// TODO: Optimization idea: since this is slicing the OrgSource at each character, it might be more efficient to do a parser that uses a search function like take_until, and wrap it in a function similar to consumed but returning the input along with the normal output, then pass all of that into a verify that confirms we were at the start of a line using the input we just returned. // TODO: Write some tests to make sure this is functioning properly.
let keywords = many0(map( let mut keywords = Vec::new();
many_till(anychar, filtered_keyword(in_buffer_settings_key)), let mut remaining = input;
|(_, kw)| kw, loop {
))(input); // Skip text until possible in_buffer_setting
keywords let start_of_pound = take_until::<_, _, CustomError<_>>("#+")(remaining);
let start_of_pound = if let Ok((start_of_pound, _)) = start_of_pound {
start_of_pound
} else {
break;
};
// Go backwards to the start of the line and run the filtered_keyword parser
let start_of_line = start_of_pound.get_start_of_line();
let (remain, maybe_kw) = match filtered_keyword(in_buffer_settings_key)(start_of_line) {
Ok((remain, kw)) => (remain, Some(kw)),
Err(_) => {
let end_of_line = take_until::<_, _, CustomError<_>>("\n")(start_of_pound);
if let Ok((end_of_line, _)) = end_of_line {
(end_of_line, None)
} else {
break;
}
}
};
if let Some(kw) = maybe_kw {
keywords.push(kw);
}
remaining = remain;
}
Ok((remaining, keywords))
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -50,6 +81,7 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
) -> Result<GlobalSettings<'g, 's>, String> { ) -> Result<GlobalSettings<'g, 's>, String> {
let mut new_settings = original_settings.clone(); let mut new_settings = original_settings.clone();
// Todo Keywords
for kw in keywords.iter().filter(|kw| { for kw in keywords.iter().filter(|kw| {
kw.key.eq_ignore_ascii_case("todo") kw.key.eq_ignore_ascii_case("todo")
|| kw.key.eq_ignore_ascii_case("seq_todo") || kw.key.eq_ignore_ascii_case("seq_todo")
@@ -65,5 +97,70 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
.extend(complete_words.into_iter().map(str::to_string)); .extend(complete_words.into_iter().map(str::to_string));
} }
// Startup settings
for kw in keywords
.iter()
.filter(|kw| kw.key.eq_ignore_ascii_case("startup"))
{
let (_remaining, settings) =
separated_list0(space1::<&str, nom::error::Error<_>>, is_not(" \t"))(kw.value)
.map_err(|err: nom::Err<_>| err.to_string())?;
if settings.contains(&"odd") {
new_settings.odd_levels_only = HeadlineLevelFilter::Odd;
}
if settings.contains(&"oddeven") {
new_settings.odd_levels_only = HeadlineLevelFilter::OddEven;
}
}
Ok(new_settings) 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::*;
#[test]
fn scan_test() -> Result<(), Box<dyn std::error::Error>> {
let input = OrgSource::new(
r#"
foo
#+archive: bar
baz #+category: lorem
#+label: ipsum
#+todo: dolar
cat
"#,
);
let (remaining, settings) = scan_for_in_buffer_settings(input)?;
assert_eq!(Into::<&str>::into(remaining), "cat\n");
let keys: Vec<_> = settings.iter().map(|kw| kw.key).collect();
// category is skipped because it is not the first non-whitespace on the line.
//
// label is skipped because it is not an in-buffer setting.
assert_eq!(keys, vec!["archive", "todo"]);
Ok(())
}
}

View File

@@ -12,6 +12,7 @@ use nom::combinator::eof;
use nom::combinator::not; use nom::combinator::not;
use nom::combinator::peek; use nom::combinator::peek;
use nom::combinator::recognize; use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many_till; use nom::multi::many_till;
use nom::sequence::tuple; use nom::sequence::tuple;
@@ -23,6 +24,7 @@ use crate::error::CustomError;
use crate::error::MyError; use crate::error::MyError;
use crate::error::Res; use crate::error::Res;
use crate::parser::util::start_of_line; use crate::parser::util::start_of_line;
use crate::types::BabelCall;
use crate::types::Keyword; use crate::types::Keyword;
const ORG_ELEMENT_AFFILIATED_KEYWORDS: [&'static str; 13] = [ const ORG_ELEMENT_AFFILIATED_KEYWORDS: [&'static str; 13] = [
@@ -102,8 +104,16 @@ pub(crate) fn affiliated_keyword<'b, 'g, 'r, 's>(
pub(crate) fn babel_call_keyword<'b, 'g, 'r, 's>( pub(crate) fn babel_call_keyword<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Keyword<'s>> { ) -> Res<OrgSource<'s>, BabelCall<'s>> {
filtered_keyword(babel_call_key)(input) let (remaining, kw) = filtered_keyword(babel_call_key)(input)?;
Ok((
remaining,
BabelCall {
source: kw.source,
key: kw.key,
value: kw.value,
},
))
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -116,7 +126,9 @@ pub(crate) fn table_formula_keyword<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Keyword<'s>> { ) -> Res<OrgSource<'s>, Keyword<'s>> {
filtered_keyword(table_formula_key)(input) verify(filtered_keyword(table_formula_key), |kw| {
!kw.value.is_empty()
})(input)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -44,9 +44,17 @@ pub(crate) fn todo_keywords<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, V
} }
fn todo_keyword_word<'s>(input: &'s str) -> Res<&'s str, &'s str> { fn todo_keyword_word<'s>(input: &'s str) -> Res<&'s str, &'s str> {
verify(take_till(|c| " \t\r\n|".contains(c)), |result: &str| { let (remaining, keyword) = verify(take_till(|c| "( \t\r\n|".contains(c)), |result: &str| {
!result.is_empty() !result.is_empty()
})(input) })(input)?;
let (remaining, _) = opt(tuple((
tag("("),
take_till(|c| "() \t\r\n|".contains(c)),
tag(")"),
)))(remaining)?;
Ok((remaining, keyword))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@@ -261,11 +261,10 @@ fn data<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
is_not("\r\n")(input) is_not("\r\n")(input)
} }
fn lesser_block_end(current_name: &str) -> impl ContextMatcher { fn lesser_block_end<'c>(current_name: &'c str) -> impl ContextMatcher + 'c {
let current_name_lower = current_name.to_lowercase(); // Since the lesser block names are statically defined in code, we can simply assert that the name is lowercase instead of causing an allocation by converting to lowercase.
move |context, input: OrgSource<'_>| { debug_assert!(current_name == current_name.to_lowercase());
_lesser_block_end(context, input, current_name_lower.as_str()) move |context, input: OrgSource<'_>| _lesser_block_end(context, input, current_name)
}
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -43,8 +43,9 @@ mod table;
mod target; mod target;
mod text_markup; mod text_markup;
mod timestamp; mod timestamp;
mod token;
mod util; mod util;
pub use document::parse; pub use document::parse;
pub use document::parse_file;
pub use document::parse_file_with_settings;
pub use document::parse_with_settings; pub use document::parse_with_settings;
pub(crate) use org_source::OrgSource; pub(crate) use org_source::OrgSource;

View File

@@ -1,6 +1,7 @@
use std::ops::RangeBounds; use std::ops::RangeBounds;
use nom::Compare; use nom::Compare;
use nom::FindSubstring;
use nom::InputIter; use nom::InputIter;
use nom::InputLength; use nom::InputLength;
use nom::InputTake; use nom::InputTake;
@@ -59,6 +60,10 @@ impl<'s> OrgSource<'s> {
self.end - self.start self.end - self.start
} }
pub(crate) fn get_byte_offset(&self) -> usize {
self.start
}
pub(crate) fn get_preceding_character(&self) -> Option<char> { pub(crate) fn get_preceding_character(&self) -> Option<char> {
self.preceding_character self.preceding_character
} }
@@ -68,11 +73,66 @@ impl<'s> OrgSource<'s> {
} }
pub(crate) fn get_until(&self, other: OrgSource<'s>) -> OrgSource<'s> { pub(crate) fn get_until(&self, other: OrgSource<'s>) -> OrgSource<'s> {
assert!(other.start >= self.start); debug_assert!(other.start >= self.start);
assert!(other.end <= self.end); debug_assert!(other.end <= self.end);
self.slice(..(other.start - self.start)) 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;
let mut brace_depth = self.brace_depth;
let mut parenthesis_depth = self.parenthesis_depth;
// Since we're going backwards, this does the opposite.
for byte in skipped_text.bytes() {
match byte {
b'\n' => {
panic!("Should not hit a line break when only going back to the start of the line.");
}
b'[' => {
bracket_depth -= 1;
}
b']' => {
bracket_depth += 1;
}
b'{' => {
brace_depth -= 1;
}
b'}' => {
brace_depth += 1;
}
b'(' => {
parenthesis_depth -= 1;
}
b')' => {
parenthesis_depth += 1;
}
_ => {}
};
}
OrgSource {
full_source: self.full_source,
start: self.start_of_line,
end: self.end,
start_of_line: self.start_of_line,
preceding_character: if self.start_of_line > 0 {
Some('\n')
} else {
None
},
bracket_depth,
brace_depth,
parenthesis_depth,
}
}
pub(crate) fn get_bracket_depth(&self) -> BracketDepth { pub(crate) fn get_bracket_depth(&self) -> BracketDepth {
self.bracket_depth self.bracket_depth
} }
@@ -306,6 +366,12 @@ impl<'s> InputTakeAtPosition for OrgSource<'s> {
} }
} }
impl<'n, 's> FindSubstring<&'n str> for OrgSource<'s> {
fn find_substring(&self, substr: &'n str) -> Option<usize> {
Into::<&str>::into(self).find(substr)
}
}
pub(crate) fn convert_error<'a, I: Into<CustomError<&'a str>>>( pub(crate) fn convert_error<'a, I: Into<CustomError<&'a str>>>(
err: nom::Err<I>, err: nom::Err<I>,
) -> nom::Err<CustomError<&'a str>> { ) -> nom::Err<CustomError<&'a str>> {
@@ -322,6 +388,7 @@ impl<'s> From<CustomError<OrgSource<'s>>> for CustomError<&'s str> {
CustomError::MyError(err) => CustomError::MyError(err.into()), CustomError::MyError(err) => CustomError::MyError(err.into()),
CustomError::Nom(input, error_kind) => CustomError::Nom(input.into(), error_kind), CustomError::Nom(input, error_kind) => CustomError::Nom(input.into(), error_kind),
CustomError::IO(err) => CustomError::IO(err), CustomError::IO(err) => CustomError::IO(err),
CustomError::BoxedError(err) => CustomError::BoxedError(err),
} }
} }
} }

View File

@@ -74,7 +74,7 @@ mod tests {
use crate::context::List; use crate::context::List;
use crate::parser::element_parser::element; use crate::parser::element_parser::element;
use crate::parser::org_source::OrgSource; use crate::parser::org_source::OrgSource;
use crate::types::Source; use crate::types::GetStandardProperties;
#[test] #[test]
fn two_paragraphs() { fn two_paragraphs() {
@@ -87,7 +87,13 @@ mod tests {
let (remaining, second_paragraph) = let (remaining, second_paragraph) =
paragraph_matcher(remaining).expect("Parse second paragraph."); paragraph_matcher(remaining).expect("Parse second paragraph.");
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(first_paragraph.get_source(), "foo bar baz\n\n"); assert_eq!(
assert_eq!(second_paragraph.get_source(), "lorem ipsum"); first_paragraph.get_standard_properties().get_source(),
"foo bar baz\n\n"
);
assert_eq!(
second_paragraph.get_standard_properties().get_source(),
"lorem ipsum"
);
} }
} }

View File

@@ -5,17 +5,24 @@ use nom::character::complete::anychar;
use nom::character::complete::none_of; use nom::character::complete::none_of;
use nom::character::complete::one_of; use nom::character::complete::one_of;
use nom::combinator::eof; use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::peek; use nom::combinator::peek;
use nom::combinator::recognize; use nom::combinator::recognize;
use nom::combinator::verify; use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many1;
use nom::multi::many_till; use nom::multi::many_till;
use nom::sequence::tuple;
use super::org_source::BracketDepth;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting; use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting;
use crate::context::parser_with_context; use crate::context::parser_with_context;
use crate::context::ContextElement; use crate::context::ContextElement;
use crate::context::ContextMatcher;
use crate::context::ExitClass; use crate::context::ExitClass;
use crate::context::ExitMatcherNode; use crate::context::ExitMatcherNode;
use crate::context::Matcher;
use crate::context::RefContext; use crate::context::RefContext;
use crate::error::CustomError; use crate::error::CustomError;
use crate::error::MyError; use crate::error::MyError;
@@ -130,17 +137,77 @@ fn path_plain<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
// TODO: "optionally containing parenthesis-wrapped non-whitespace non-bracket substrings up to a depth of two. The string must end with either a non-punctation non-whitespace character, a forwards slash, or a parenthesis-wrapped substring" let path_plain_end = path_plain_end(input.get_parenthesis_depth());
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma, class: ExitClass::Gamma,
exit_matcher: &path_plain_end, exit_matcher: &path_plain_end,
}); });
let parser_context = context.with_additional_node(&parser_context); let parser_context = context.with_additional_node(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); let (remaining, _components) = many1(alt((
parser_with_context!(path_plain_no_parenthesis)(&parser_context),
parser_with_context!(path_plain_parenthesis)(&parser_context),
)))(input)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
}
fn path_plain_end(starting_parenthesis_depth: BracketDepth) -> impl ContextMatcher {
move |context, input: OrgSource<'_>| _path_plain_end(context, input, starting_parenthesis_depth)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _path_plain_end<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
starting_parenthesis_depth: BracketDepth,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _leading_punctuation) = many0(verify(anychar, |c| {
!" \t\r\n[]<>()/".contains(*c) && c.is_ascii_punctuation()
}))(input)?;
let disallowed_character = recognize(one_of(" \t\r\n[]<>"))(remaining);
if disallowed_character.is_ok() {
return disallowed_character;
}
let current_depth = remaining.get_parenthesis_depth() - starting_parenthesis_depth;
if current_depth == 0 {
let close_parenthesis =
tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>(")")(remaining);
if close_parenthesis.is_ok() {
return close_parenthesis;
}
let open_parenthesis_without_match = recognize(tuple((
peek(tag("(")),
not(parser_with_context!(path_plain_parenthesis)(context)),
)))(remaining);
if open_parenthesis_without_match.is_ok() {
return open_parenthesis_without_match;
}
}
// many0 punctuation
Err(nom::Err::Error(CustomError::MyError(MyError(
"No path plain end".into(),
))))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn path_plain_no_parenthesis<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, path) = recognize(verify( let (remaining, path) = recognize(verify(
many_till(anychar, peek(exit_matcher)), many_till(
anychar,
alt((
peek(path_plain_no_parenthesis_disallowed_character),
parser_with_context!(exit_matcher_parser)(context),
)),
),
|(children, _exit_contents)| !children.is_empty(), |(children, _exit_contents)| !children.is_empty(),
))(input)?; ))(input)?;
@@ -148,14 +215,65 @@ fn path_plain<'b, 'g, 'r, 's>(
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn path_plain_end<'b, 'g, 'r, 's>( fn path_plain_no_parenthesis_disallowed_character<'s>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(many_till( recognize(verify(anychar, |c| {
verify(anychar, |c| { c.is_whitespace() || "()[]<>".contains(*c)
*c != '/' && (c.is_ascii_punctuation() || c.is_whitespace()) }))(input)
}), }
one_of(" \t\r\n()[]<>"),
))(input) #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn path_plain_parenthesis<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _opening) = tag("(")(input)?;
let starting_depth = remaining.get_parenthesis_depth();
let (remaining, _path) = recognize(verify(
many_till(
anychar,
alt((
peek(path_plain_parenthesis_end(starting_depth)),
parser_with_context!(exit_matcher_parser)(context),
)),
),
|(children, _exit_contents)| !children.is_empty(),
))(remaining)?;
let (remaining, _opening) = tag(")")(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
}
fn path_plain_parenthesis_end(starting_parenthesis_depth: BracketDepth) -> impl Matcher {
move |input: OrgSource<'_>| _path_plain_parenthesis_end(input, starting_parenthesis_depth)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _path_plain_parenthesis_end<'s>(
input: OrgSource<'s>,
starting_parenthesis_depth: BracketDepth,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let current_depth = input.get_parenthesis_depth() - starting_parenthesis_depth;
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing parenthesis should end the link.
unreachable!("Exceeded plain link parenthesis depth.")
}
if current_depth == 0 {
let close_parenthesis = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>(")")(input);
if close_parenthesis.is_ok() {
return close_parenthesis;
}
}
if current_depth == 1 {
let open_parenthesis = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("(")(input);
if open_parenthesis.is_ok() {
return open_parenthesis;
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"No closing parenthesis".into(),
))))
} }

View File

@@ -7,6 +7,7 @@ use nom::character::complete::one_of;
use nom::character::complete::space0; use nom::character::complete::space0;
use nom::character::complete::space1; use nom::character::complete::space1;
use nom::combinator::eof; use nom::combinator::eof;
use nom::combinator::map;
use nom::combinator::not; use nom::combinator::not;
use nom::combinator::opt; use nom::combinator::opt;
use nom::combinator::peek; use nom::combinator::peek;
@@ -21,6 +22,7 @@ use super::element_parser::element;
use super::object_parser::standard_set_object; use super::object_parser::standard_set_object;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::util::include_input; use super::util::include_input;
use super::util::indentation_level;
use super::util::non_whitespace_character; use super::util::non_whitespace_character;
use crate::context::parser_with_context; use crate::context::parser_with_context;
use crate::context::ContextElement; use crate::context::ContextElement;
@@ -35,22 +37,31 @@ use crate::parser::util::blank_line;
use crate::parser::util::exit_matcher_parser; use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed; use crate::parser::util::get_consumed;
use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting; use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting;
use crate::parser::util::org_space;
use crate::parser::util::start_of_line; use crate::parser::util::start_of_line;
use crate::types::CheckboxType;
use crate::types::IndentationLevel;
use crate::types::Object; use crate::types::Object;
use crate::types::PlainList; use crate::types::PlainList;
use crate::types::PlainListItem; 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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn detect_plain_list<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> { pub(crate) fn detect_plain_list<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> {
if verify( if verify(
tuple(( tuple((
start_of_line, start_of_line,
space0, parser_with_context!(indentation_level)(context),
bullet, parser_with_context!(bullet)(context),
alt((space1, line_ending, eof)), alt((space1, line_ending, eof)),
)), )),
|(_start, indent, bull, _after_whitespace)| { |(_start, (indent_level, _), (_bullet_type, bull), _after_whitespace)| {
Into::<&str>::into(bull) != "*" || indent.len() > 0 !Into::<&str>::into(bull).starts_with("*") || *indent_level > 0
}, },
)(input) )(input)
.is_ok() .is_ok()
@@ -81,7 +92,8 @@ pub(crate) fn plain_list<'b, 'g, 'r, 's>(
let parser_context = parser_context.with_additional_node(&contexts[2]); let parser_context = parser_context.with_additional_node(&contexts[2]);
// children stores tuple of (input string, parsed object) so we can re-parse the final item // children stores tuple of (input string, parsed object) so we can re-parse the final item
let mut children = Vec::new(); let mut children = Vec::new();
let mut first_item_indentation: Option<usize> = None; let mut first_item_indentation: Option<IndentationLevel> = None;
let mut first_item_list_type: Option<PlainListType> = None;
let mut remaining = input; let mut remaining = input;
// The final list item does not consume trailing blank lines (which instead get consumed by the list). We have three options here: // The final list item does not consume trailing blank lines (which instead get consumed by the list). We have three options here:
@@ -94,8 +106,15 @@ pub(crate) fn plain_list<'b, 'g, 'r, 's>(
loop { loop {
let list_item = parser_with_context!(plain_list_item)(&parser_context)(remaining); let list_item = parser_with_context!(plain_list_item)(&parser_context)(remaining);
match (&first_item_list_type, &list_item) {
(None, Ok((_remain, (list_type, _item)))) => {
let _ = first_item_list_type.insert(*list_type);
}
(None, Err(_)) => {}
(Some(_), _) => {}
};
match list_item { match list_item {
Ok((remain, item)) Ok((remain, (_list_type, item)))
if item.indentation == *first_item_indentation.get_or_insert(item.indentation) => if item.indentation == *first_item_indentation.get_or_insert(item.indentation) =>
{ {
children.push((remaining, item)); children.push((remaining, item));
@@ -122,7 +141,7 @@ pub(crate) fn plain_list<'b, 'g, 'r, 's>(
}; };
let final_item_context = ContextElement::ConsumeTrailingWhitespace(false); let final_item_context = ContextElement::ConsumeTrailingWhitespace(false);
let final_item_context = parser_context.with_additional_node(&final_item_context); let final_item_context = parser_context.with_additional_node(&final_item_context);
let (remaining, reparsed_final_item) = let (remaining, (_, reparsed_final_item)) =
parser_with_context!(plain_list_item)(&final_item_context)(final_child_start)?; parser_with_context!(plain_list_item)(&final_item_context)(final_child_start)?;
children.push((final_child_start, reparsed_final_item)); children.push((final_child_start, reparsed_final_item));
@@ -131,6 +150,7 @@ pub(crate) fn plain_list<'b, 'g, 'r, 's>(
remaining, remaining,
PlainList { PlainList {
source: source.into(), source: source.into(),
list_type: first_item_list_type.expect("Plain lists require at least one element."),
children: children.into_iter().map(|(_start, item)| item).collect(), children: children.into_iter().map(|(_start, item)| item).collect(),
}, },
)) ))
@@ -140,46 +160,32 @@ pub(crate) fn plain_list<'b, 'g, 'r, 's>(
fn plain_list_item<'b, 'g, 'r, 's>( fn plain_list_item<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, PlainListItem<'s>> { ) -> Res<OrgSource<'s>, (PlainListType, PlainListItem<'s>)> {
start_of_line(input)?; start_of_line(input)?;
let (remaining, leading_whitespace) = space0(input)?; let (remaining, (indent_level, _leading_whitespace)) = indentation_level(context, input)?;
// It is fine that we get the indent level using the number of bytes rather than the number of characters because nom's space0 only matches space and tab (0x20 and 0x09) let (remaining, (bullet_type, bull)) = verify(
let indent_level = leading_whitespace.len(); parser_with_context!(bullet)(context),
let (remaining, bull) = verify(bullet, |bull: &OrgSource<'_>| { |(_bullet_type, bull)| !Into::<&str>::into(bull).starts_with("*") || indent_level > 0,
Into::<&str>::into(bull) != "*" || indent_level > 0 )(remaining)?;
})(remaining)?;
let (remaining, _maybe_counter_set) = let (remaining, maybe_counter_set) =
opt(tuple((space1, tag("[@"), counter, tag("]"))))(remaining)?; opt(tuple((space1, tag("[@"), counter_set_value, tag("]"))))(remaining)?;
let maybe_counter_set = maybe_counter_set.map(|(_, _, val, _)| val);
// TODO: parse checkbox let (remaining, maybe_checkbox) = opt(tuple((space1, item_checkbox)))(remaining)?;
let (remaining, maybe_tag) = let (remaining, maybe_tag) = if let BulletType::Unordered = bullet_type {
opt(tuple((space1, parser_with_context!(item_tag)(context))))(remaining)?; opt(tuple((space1, parser_with_context!(item_tag)(context))))(remaining)?
} else {
let maybe_contentless_item: Res<OrgSource<'_>, ()> = peek(parser_with_context!( (remaining, None)
detect_contentless_item_contents
)(context))(remaining);
match maybe_contentless_item {
Ok((_rem, _ws)) => {
let (remaining, _trailing_ws) = opt(blank_line)(remaining)?;
let source = get_consumed(input, remaining);
return Ok((
remaining,
PlainListItem {
source: source.into(),
indentation: indent_level,
bullet: bull.into(),
tag: maybe_tag
.map(|(_ws, item_tag)| item_tag)
.unwrap_or(Vec::new()),
children: Vec::new(),
},
));
}
Err(_) => {}
}; };
let (remaining, _ws) = item_tag_post_gap(context, remaining)?; let list_type = match (&maybe_tag, bullet_type) {
(None, BulletType::Ordered) => PlainListType::Ordered,
(None, BulletType::Unordered) => PlainListType::Unordered,
(Some(_), BulletType::Ordered) => unreachable!(),
(Some(_), BulletType::Unordered) => PlainListType::Descriptive,
};
let exit_matcher = plain_list_item_end(indent_level); let exit_matcher = plain_list_item_end(indent_level);
let contexts = [ let contexts = [
ContextElement::ConsumeTrailingWhitespace(true), ContextElement::ConsumeTrailingWhitespace(true),
@@ -191,6 +197,44 @@ fn plain_list_item<'b, 'g, 'r, 's>(
let parser_context = context.with_additional_node(&contexts[0]); 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[1]);
let maybe_contentless_item: Res<OrgSource<'_>, ()> = peek(parser_with_context!(
detect_contentless_item_contents
)(&parser_context))(remaining);
match maybe_contentless_item {
Ok((_rem, _ws)) => {
let (remaining, _trailing_ws) = if context.should_consume_trailing_whitespace() {
recognize(alt((recognize(many1(blank_line)), eof)))(remaining)?
} else {
recognize(alt((blank_line, eof)))(remaining)?
};
let source = get_consumed(input, remaining);
return Ok((
remaining,
(
list_type,
PlainListItem {
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(),
},
),
));
}
Err(_) => {}
};
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( let (mut remaining, (mut children, _exit_contents)) = many_till(
include_input(parser_with_context!(element(true))(&parser_context)), include_input(parser_with_context!(element(true))(&parser_context)),
parser_with_context!(exit_matcher_parser)(&parser_context), parser_with_context!(exit_matcher_parser)(&parser_context),
@@ -215,31 +259,95 @@ fn plain_list_item<'b, 'g, 'r, 's>(
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
return Ok(( return Ok((
remaining, remaining,
PlainListItem { (
source: source.into(), list_type,
indentation: indent_level, PlainListItem {
bullet: bull.into(), source: source.into(),
tag: maybe_tag indentation: indent_level,
.map(|(_ws, item_tag)| item_tag) bullet: bull.into(),
.unwrap_or(Vec::new()), counter: maybe_counter_set,
children: children.into_iter().map(|(_start, item)| item).collect(), 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(),
},
),
)); ));
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[derive(Debug)]
fn bullet<'s>(i: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> { enum BulletType {
alt(( Ordered,
tag("*"), Unordered,
tag("-"),
tag("+"),
recognize(tuple((counter, alt((tag("."), tag(")")))))),
))(i)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn counter<'s>(i: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> { fn bullet<'b, 'g, 'r, 's>(
alt((recognize(one_of("abcdefghijklmnopqrstuvwxyz")), digit1))(i) context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (BulletType, OrgSource<'s>)> {
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"))]
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().list_allow_alphabetical {
alt((
recognize(one_of(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
)),
digit1,
))(input)
} else {
digit1(input)
}
}
#[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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -255,7 +363,7 @@ fn plain_list_end<'b, 'g, 'r, 's>(
)))(input) )))(input)
} }
const fn plain_list_item_end(indent_level: usize) -> impl ContextMatcher { const fn plain_list_item_end(indent_level: IndentationLevel) -> impl ContextMatcher {
let line_indented_lte_matcher = line_indented_lte(indent_level); let line_indented_lte_matcher = line_indented_lte(indent_level);
move |context, input: OrgSource<'_>| { move |context, input: OrgSource<'_>| {
_plain_list_item_end(context, input, &line_indented_lte_matcher) _plain_list_item_end(context, input, &line_indented_lte_matcher)
@@ -278,20 +386,23 @@ fn _plain_list_item_end<'b, 'g, 'r, 's>(
)))(input) )))(input)
} }
const fn line_indented_lte(indent_level: usize) -> impl ContextMatcher { const fn line_indented_lte(indent_level: IndentationLevel) -> impl ContextMatcher {
move |context, input: OrgSource<'_>| _line_indented_lte(context, input, indent_level) move |context, input: OrgSource<'_>| _line_indented_lte(context, input, indent_level)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _line_indented_lte<'b, 'g, 'r, 's>( fn _line_indented_lte<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
indent_level: usize, indent_level: IndentationLevel,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
let matched = recognize(verify( let matched = recognize(verify(
tuple((space0::<OrgSource<'_>, _>, non_whitespace_character)), tuple((
parser_with_context!(indentation_level)(context),
non_whitespace_character,
)),
// It is fine that we get the indent level using the number of bytes rather than the number of characters because nom's space0 only matches space and tab (0x20 and 0x09) // It is fine that we get the indent level using the number of bytes rather than the number of characters because nom's space0 only matches space and tab (0x20 and 0x09)
|(_space0, _anychar)| _space0.len() <= indent_level, |((indentation_level, _leading_whitespace), _anychar)| *indentation_level <= indent_level,
))(input)?; ))(input)?;
Ok(matched) Ok(matched)
@@ -324,22 +435,22 @@ fn item_tag_end<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt(( alt((item_tag_divider, line_ending))(input)
recognize(tuple(( }
item_tag_divider,
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn item_tag_divider<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(tuple((
one_of(" \t"),
tag("::"),
peek(tuple((
opt(tuple(( opt(tuple((
peek(one_of(" \t")), peek(one_of(" \t")),
many_till(anychar, peek(alt((item_tag_divider, line_ending, eof)))), many_till(anychar, peek(alt((item_tag_divider, line_ending, eof)))),
))), ))),
alt((line_ending, eof)), alt((line_ending, eof)),
))), ))),
line_ending, )))(input)
))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn item_tag_divider<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(tuple((one_of(" \t"), tag("::"))))(input)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -363,6 +474,18 @@ fn item_tag_post_gap<'b, 'g, 'r, 's>(
)(input) )(input)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn item_checkbox<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, (CheckboxType, OrgSource<'s>)> {
alt((
map(
recognize(tuple((tag("["), org_space, tag("]")))),
|capture| (CheckboxType::Off, capture),
),
map(tag("[-]"), |capture| (CheckboxType::Trans, capture)),
map(tag("[X]"), |capture| (CheckboxType::On, capture)),
))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn detect_contentless_item_contents<'b, 'g, 'r, 's>( fn detect_contentless_item_contents<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
@@ -381,7 +504,7 @@ mod tests {
use crate::context::Context; use crate::context::Context;
use crate::context::GlobalSettings; use crate::context::GlobalSettings;
use crate::context::List; use crate::context::List;
use crate::types::Source; use crate::types::GetStandardProperties;
#[test] #[test]
fn plain_list_item_empty() { fn plain_list_item_empty() {
@@ -390,9 +513,9 @@ mod tests {
let initial_context = ContextElement::document_context(); let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context)); let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_item_matcher = parser_with_context!(plain_list_item)(&initial_context); let plain_list_item_matcher = parser_with_context!(plain_list_item)(&initial_context);
let (remaining, result) = plain_list_item_matcher(input).unwrap(); let (remaining, (_, result)) = plain_list_item_matcher(input).unwrap();
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.source, "1."); assert_eq!(result.get_standard_properties().get_source(), "1.");
} }
#[test] #[test]
@@ -402,9 +525,9 @@ mod tests {
let initial_context = ContextElement::document_context(); let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context)); let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_item_matcher = parser_with_context!(plain_list_item)(&initial_context); let plain_list_item_matcher = parser_with_context!(plain_list_item)(&initial_context);
let (remaining, result) = plain_list_item_matcher(input).unwrap(); let (remaining, (_, result)) = plain_list_item_matcher(input).unwrap();
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.source, "1. foo"); assert_eq!(result.get_standard_properties().get_source(), "1. foo");
} }
#[test] #[test]
@@ -416,7 +539,7 @@ mod tests {
let plain_list_matcher = parser_with_context!(plain_list)(&initial_context); let plain_list_matcher = parser_with_context!(plain_list)(&initial_context);
let (remaining, result) = plain_list_matcher(input).unwrap(); let (remaining, result) = plain_list_matcher(input).unwrap();
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.source, "1."); assert_eq!(result.get_standard_properties().get_source(), "1.");
} }
#[test] #[test]
@@ -428,7 +551,7 @@ mod tests {
let plain_list_matcher = parser_with_context!(plain_list)(&initial_context); let plain_list_matcher = parser_with_context!(plain_list)(&initial_context);
let (remaining, result) = plain_list_matcher(input).unwrap(); let (remaining, result) = plain_list_matcher(input).unwrap();
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.source, "1. foo"); assert_eq!(result.get_standard_properties().get_source(), "1. foo");
} }
#[test] #[test]
@@ -475,7 +598,7 @@ mod tests {
plain_list_matcher(input).expect("Should parse the plain list successfully."); plain_list_matcher(input).expect("Should parse the plain list successfully.");
assert_eq!(Into::<&str>::into(remaining), " ipsum\n"); assert_eq!(Into::<&str>::into(remaining), " ipsum\n");
assert_eq!( assert_eq!(
result.get_source(), result.get_standard_properties().get_source(),
r#"1. foo r#"1. foo
2. bar 2. bar
baz baz
@@ -503,7 +626,7 @@ baz"#,
plain_list_matcher(input).expect("Should parse the plain list successfully."); plain_list_matcher(input).expect("Should parse the plain list successfully.");
assert_eq!(Into::<&str>::into(remaining), "baz"); assert_eq!(Into::<&str>::into(remaining), "baz");
assert_eq!( assert_eq!(
result.get_source(), result.get_standard_properties().get_source(),
r#"1. foo r#"1. foo
1. bar 1. bar
@@ -536,7 +659,7 @@ dolar"#,
plain_list_matcher(input).expect("Should parse the plain list successfully."); plain_list_matcher(input).expect("Should parse the plain list successfully.");
assert_eq!(Into::<&str>::into(remaining), "dolar"); assert_eq!(Into::<&str>::into(remaining), "dolar");
assert_eq!( assert_eq!(
result.get_source(), result.get_standard_properties().get_source(),
r#"1. foo r#"1. foo
bar bar
@@ -558,21 +681,30 @@ dolar"#,
r#"+ r#"+
"#, "#,
); );
let result = detect_plain_list(input); let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test] #[test]
fn detect_eof() { fn detect_eof() {
let input = OrgSource::new(r#"+"#); let input = OrgSource::new(r#"+"#);
let result = detect_plain_list(input); let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[test] #[test]
fn detect_no_gap() { fn detect_no_gap() {
let input = OrgSource::new(r#"+foo"#); let input = OrgSource::new(r#"+foo"#);
let result = detect_plain_list(input); let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
// Since there is no whitespace after the '+' this is a paragraph, not a plain list. // Since there is no whitespace after the '+' this is a paragraph, not a plain list.
assert!(result.is_err()); assert!(result.is_err());
} }
@@ -580,7 +712,10 @@ dolar"#,
#[test] #[test]
fn detect_with_gap() { fn detect_with_gap() {
let input = OrgSource::new(r#"+ foo"#); let input = OrgSource::new(r#"+ foo"#);
let result = detect_plain_list(input); let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
assert!(result.is_ok()); assert!(result.is_ok());
} }
} }

View File

@@ -1,17 +1,26 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::tag; use nom::bytes::complete::is_not;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar; use nom::character::complete::anychar;
use nom::combinator::map; use nom::character::complete::line_ending;
use nom::character::complete::one_of;
use nom::combinator::eof;
use nom::combinator::peek; use nom::combinator::peek;
use nom::combinator::recognize; use nom::combinator::recognize;
use nom::combinator::verify; use nom::combinator::verify;
use nom::multi::many1;
use nom::multi::many_till; use nom::multi::many_till;
use nom::sequence::tuple;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::radio_link::RematchObject; use super::radio_link::RematchObject;
use super::util::exit_matcher_parser; use super::util::exit_matcher_parser;
use super::util::get_consumed;
use super::util::org_space_or_line_ending;
use crate::context::parser_with_context; use crate::context::parser_with_context;
use crate::context::RefContext; use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res; use crate::error::Res;
use crate::types::Object; use crate::types::Object;
use crate::types::PlainText; use crate::types::PlainText;
@@ -72,11 +81,58 @@ impl<'x> RematchObject<'x> for PlainText<'x> {
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> { ) -> Res<OrgSource<'s>, Object<'s>> {
map(tag(self.source), |s| { let mut remaining = input;
let mut goal = self.source;
loop {
if goal.is_empty() {
break;
}
let is_not_whitespace = is_not::<&str, &str, CustomError<_>>(" \t\r\n")(goal);
match is_not_whitespace {
Ok((new_goal, payload)) => {
let (new_remaining, _) = tuple((
tag_no_case(payload),
// TODO: Test to see what the REAL condition is. Checking for not-alphabetic works fine for now, but the real criteria might be something like the plain text exit matcher.
peek(alt((
recognize(verify(anychar, |c| !c.is_alphanumeric())),
eof,
))),
))(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;
}
Err(_) => {}
};
let is_whitespace = recognize(many1(alt((
recognize(one_of::<&str, &str, CustomError<_>>(" \t")),
line_ending,
))))(goal);
match is_whitespace {
Ok((new_goal, _)) => {
let (new_remaining, _) = many1(org_space_or_line_ending)(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;
}
Err(_) => {}
};
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Target does not match.".into(),
))));
}
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::PlainText(PlainText { Object::PlainText(PlainText {
source: Into::<&str>::into(s), source: Into::<&str>::into(source),
}) }),
})(input) ))
} }
} }
@@ -90,7 +146,7 @@ mod tests {
use crate::context::GlobalSettings; use crate::context::GlobalSettings;
use crate::context::List; use crate::context::List;
use crate::parser::object_parser::detect_standard_set_object_sans_plain_text; use crate::parser::object_parser::detect_standard_set_object_sans_plain_text;
use crate::types::Source; use crate::types::GetStandardProperties;
#[test] #[test]
fn plain_text_simple() { fn plain_text_simple() {
@@ -103,6 +159,9 @@ mod tests {
))(&initial_context); ))(&initial_context);
let (remaining, result) = map(plain_text_matcher, Object::PlainText)(input).unwrap(); let (remaining, result) = map(plain_text_matcher, Object::PlainText)(input).unwrap();
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.get_source(), Into::<&str>::into(input)); assert_eq!(
result.get_standard_properties().get_source(),
Into::<&str>::into(input)
);
} }
} }

View File

@@ -1,21 +1,23 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case; use nom::bytes::complete::tag_no_case;
use nom::character::complete::line_ending;
use nom::character::complete::space0; use nom::character::complete::space0;
use nom::character::complete::space1; use nom::character::complete::space1;
use nom::combinator::eof; use nom::combinator::map;
use nom::multi::separated_list1; use nom::multi::many1;
use nom::sequence::tuple; use nom::sequence::tuple;
use super::org_source::OrgSource; use super::org_source::OrgSource;
use super::timestamp::timestamp;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting; use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use super::util::org_line_ending;
use crate::context::parser_with_context;
use crate::context::RefContext; use crate::context::RefContext;
use crate::error::Res; use crate::error::Res;
use crate::parser::util::get_consumed; use crate::parser::util::get_consumed;
use crate::parser::util::start_of_line; use crate::parser::util::start_of_line;
use crate::types::Planning; use crate::types::Planning;
use crate::types::Timestamp;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn planning<'b, 'g, 'r, 's>( pub(crate) fn planning<'b, 'g, 'r, 's>(
@@ -24,31 +26,63 @@ pub(crate) fn planning<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, Planning<'s>> { ) -> Res<OrgSource<'s>, Planning<'s>> {
start_of_line(input)?; start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(input)?; let (remaining, _leading_whitespace) = space0(input)?;
let (remaining, _planning_parameters) = separated_list1(space1, planning_parameter)(remaining)?; let (remaining, planning_parameters) =
let (remaining, _trailing_ws) = tuple((space0, alt((line_ending, eof))))(remaining)?; many1(parser_with_context!(planning_parameter)(context))(remaining)?;
let (remaining, _trailing_ws) = tuple((space0, org_line_ending))(remaining)?;
let (remaining, _trailing_ws) = let (remaining, _trailing_ws) =
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
let source = get_consumed(input, 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(( Ok((
remaining, remaining,
Planning { Planning {
source: source.into(), source: source.into(),
scheduled,
deadline,
closed,
}, },
)) ))
} }
#[derive(Debug)]
enum PlanningTimestampType {
Scheduled,
Deadline,
Closed,
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn planning_parameter<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> { fn planning_parameter<'b, 'g, 'r, 's>(
let (remaining, _planning_type) = alt(( context: RefContext<'b, 'g, 'r, 's>,
tag_no_case("DEADLINE"), input: OrgSource<'s>,
tag_no_case("SCHEDULED"), ) -> Res<OrgSource<'s>, (PlanningTimestampType, Timestamp<'s>)> {
tag_no_case("CLOSED"), 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)?; ))(input)?;
let (remaining, _gap) = tuple((tag(":"), space1))(remaining)?; let (remaining, _gap) = tuple((tag(":"), space1))(remaining)?;
// TODO: Make this invoke the real timestamp parser. let (remaining, timestamp) = timestamp(context, remaining)?;
let (remaining, _timestamp) = tuple((tag("<"), is_not("\r\n>"), tag(">")))(remaining)?; Ok((remaining, (planning_type, timestamp)))
let source = get_consumed(input, remaining);
Ok((remaining, source))
} }

View File

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

View File

@@ -62,6 +62,22 @@ pub(crate) fn rematch_target<'x, 'b, 'g, 'r, 's>(
remaining = new_remaining; remaining = new_remaining;
new_matches.push(new_match); new_matches.push(new_match);
} }
Object::Italic(italic) => {
let (new_remaining, new_match) = italic.rematch_object(context, remaining)?;
remaining = new_remaining;
new_matches.push(new_match);
}
Object::Underline(underline) => {
let (new_remaining, new_match) = underline.rematch_object(context, remaining)?;
remaining = new_remaining;
new_matches.push(new_match);
}
Object::StrikeThrough(strikethrough) => {
let (new_remaining, new_match) =
strikethrough.rematch_object(context, remaining)?;
remaining = new_remaining;
new_matches.push(new_match);
}
Object::PlainText(plaintext) => { Object::PlainText(plaintext) => {
let (new_remaining, new_match) = plaintext.rematch_object(context, remaining)?; let (new_remaining, new_match) = plaintext.rematch_object(context, remaining)?;
remaining = new_remaining; remaining = new_remaining;
@@ -135,8 +151,8 @@ mod tests {
use crate::parser::element_parser::element; use crate::parser::element_parser::element;
use crate::types::Bold; use crate::types::Bold;
use crate::types::Element; use crate::types::Element;
use crate::types::GetStandardProperties;
use crate::types::PlainText; use crate::types::PlainText;
use crate::types::Source;
#[test] #[test]
fn plain_text_radio_target() { fn plain_text_radio_target() {
@@ -156,7 +172,10 @@ mod tests {
_ => panic!("Should be a paragraph!"), _ => panic!("Should be a paragraph!"),
}; };
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(first_paragraph.get_source(), "foo bar baz"); assert_eq!(
first_paragraph.get_standard_properties().get_source(),
"foo bar baz"
);
assert_eq!(first_paragraph.children.len(), 3); assert_eq!(first_paragraph.children.len(), 3);
assert_eq!( assert_eq!(
first_paragraph first_paragraph
@@ -192,7 +211,10 @@ mod tests {
_ => panic!("Should be a paragraph!"), _ => panic!("Should be a paragraph!"),
}; };
assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(first_paragraph.get_source(), "foo *bar* baz"); assert_eq!(
first_paragraph.get_standard_properties().get_source(),
"foo *bar* baz"
);
assert_eq!(first_paragraph.children.len(), 3); assert_eq!(first_paragraph.children.len(), 3);
assert_eq!( assert_eq!(
first_paragraph first_paragraph

View File

@@ -2,7 +2,7 @@ use nom::branch::alt;
use nom::bytes::complete::escaped; use nom::bytes::complete::escaped;
use nom::bytes::complete::tag; use nom::bytes::complete::tag;
use nom::bytes::complete::take_till1; use nom::bytes::complete::take_till1;
use nom::character::complete::one_of; use nom::character::complete::anychar;
use nom::combinator::verify; use nom::combinator::verify;
use nom::multi::many_till; use nom::multi::many_till;
@@ -78,11 +78,11 @@ fn pathreg<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, path) = escaped( let (remaining, path) = escaped(
take_till1(|c| match c { take_till1(|c| match c {
'\\' | ']' => true, '\\' | '[' | ']' => true,
_ => false, _ => false,
}), }),
'\\', '\\',
one_of(r#"]"#), anychar,
)(input)?; )(input)?;
Ok((remaining, path)) Ok((remaining, path))
} }

View File

@@ -23,6 +23,7 @@ use crate::context::ContextElement;
use crate::context::ContextMatcher; use crate::context::ContextMatcher;
use crate::context::ExitClass; use crate::context::ExitClass;
use crate::context::ExitMatcherNode; use crate::context::ExitMatcherNode;
use crate::context::Matcher;
use crate::context::RefContext; use crate::context::RefContext;
use crate::error::CustomError; use crate::error::CustomError;
use crate::error::MyError; use crate::error::MyError;
@@ -112,6 +113,10 @@ fn script_body<'b, 'g, 'r, 's>(
map(parser_with_context!(script_with_braces)(context), |body| { map(parser_with_context!(script_with_braces)(context), |body| {
ScriptBody::WithBraces(body.into()) ScriptBody::WithBraces(body.into())
}), }),
map(
parser_with_context!(script_with_parenthesis)(context),
|body| ScriptBody::Braceless(body.into()),
),
))(input) ))(input)
} }
@@ -199,3 +204,49 @@ fn _script_with_braces_end<'b, 'g, 'r, 's>(
} }
tag("}")(input) tag("}")(input)
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn script_with_parenthesis<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let (remaining, _) = tag("(")(input)?;
let exit_with_depth = script_with_parenthesis_end(remaining.get_parenthesis_depth());
let (remaining, _) = many_till(
anychar,
alt((
peek(exit_with_depth),
parser_with_context!(exit_matcher_parser)(context),
)),
)(remaining)?;
let (remaining, _) = tag(")")(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))
}
fn script_with_parenthesis_end(starting_parenthesis_depth: BracketDepth) -> impl Matcher {
move |input: OrgSource<'_>| _script_with_parenthesis_end(input, starting_parenthesis_depth)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _script_with_parenthesis_end<'s>(
input: OrgSource<'s>,
starting_parenthesis_depth: BracketDepth,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let current_depth = input.get_parenthesis_depth() - starting_parenthesis_depth;
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing bracket should end the citation.
unreachable!("Exceeded citation key suffix bracket depth.")
}
if current_depth == 0 {
let close_parenthesis = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>(")")(input);
if close_parenthesis.is_ok() {
return close_parenthesis;
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"No script parenthesis end.".into(),
))))
}

View File

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

View File

@@ -20,6 +20,7 @@ use super::org_source::OrgSource;
use super::radio_link::RematchObject; use super::radio_link::RematchObject;
use super::util::in_object_section; use super::util::in_object_section;
use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting; use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting;
use super::util::start_of_line;
use crate::context::parser_with_context; use crate::context::parser_with_context;
use crate::context::ContextElement; use crate::context::ContextElement;
use crate::context::ContextMatcher; use crate::context::ContextMatcher;
@@ -64,8 +65,7 @@ fn bold<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Bold<'s>> { ) -> Res<OrgSource<'s>, Bold<'s>> {
let text_markup_object_specialized = text_markup_object("*"); let (remaining, children) = text_markup_object("*")(context, input)?;
let (remaining, children) = text_markup_object_specialized(context, input)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok(( Ok((
remaining, remaining,
@@ -81,8 +81,7 @@ fn italic<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Italic<'s>> { ) -> Res<OrgSource<'s>, Italic<'s>> {
let text_markup_object_specialized = text_markup_object("/"); let (remaining, children) = text_markup_object("/")(context, input)?;
let (remaining, children) = text_markup_object_specialized(context, input)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok(( Ok((
remaining, remaining,
@@ -98,8 +97,7 @@ fn underline<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Underline<'s>> { ) -> Res<OrgSource<'s>, Underline<'s>> {
let text_markup_object_specialized = text_markup_object("_"); let (remaining, children) = text_markup_object("_")(context, input)?;
let (remaining, children) = text_markup_object_specialized(context, input)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok(( Ok((
remaining, remaining,
@@ -115,8 +113,7 @@ fn strike_through<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, StrikeThrough<'s>> { ) -> Res<OrgSource<'s>, StrikeThrough<'s>> {
let text_markup_object_specialized = text_markup_object("+"); let (remaining, children) = text_markup_object("+")(context, input)?;
let (remaining, children) = text_markup_object_specialized(context, input)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok(( Ok((
remaining, remaining,
@@ -132,8 +129,7 @@ fn verbatim<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Verbatim<'s>> { ) -> Res<OrgSource<'s>, Verbatim<'s>> {
let text_markup_string_specialized = text_markup_string("="); let (remaining, contents) = text_markup_string("=")(context, input)?;
let (remaining, contents) = text_markup_string_specialized(context, input)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok(( Ok((
remaining, remaining,
@@ -149,8 +145,7 @@ fn code<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Code<'s>> { ) -> Res<OrgSource<'s>, Code<'s>> {
let text_markup_string_specialized = text_markup_string("~"); let (remaining, contents) = text_markup_string("~")(context, input)?;
let (remaining, contents) = text_markup_string_specialized(context, input)?;
let source = get_consumed(input, remaining); let source = get_consumed(input, remaining);
Ok(( Ok((
remaining, remaining,
@@ -168,8 +163,7 @@ fn text_markup_object<'c>(
OrgSource<'s>, OrgSource<'s>,
) -> Res<OrgSource<'s>, Vec<Object<'s>>> ) -> Res<OrgSource<'s>, Vec<Object<'s>>>
+ 'c { + 'c {
let marker_symbol = marker_symbol.to_owned(); move |context, input: OrgSource<'_>| _text_markup_object(context, input, marker_symbol)
move |context, input: OrgSource<'_>| _text_markup_object(context, input, marker_symbol.as_str())
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -188,7 +182,7 @@ fn _text_markup_object<'b, 'g, 'r, 's, 'c>(
let (remaining, open) = tag(marker_symbol)(remaining)?; let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) = let (remaining, _peek_not_whitespace) =
peek(verify(anychar, |c| !c.is_whitespace() && *c != '\u{200B}'))(remaining)?; peek(verify(anychar, |c| !c.is_whitespace() && *c != '\u{200B}'))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into()); let text_markup_end_specialized = text_markup_end(open.into(), remaining.get_byte_offset());
let contexts = [ let contexts = [
ContextElement::ContextObject(marker_symbol), ContextElement::ContextObject(marker_symbol),
ContextElement::ExitMatcherNode(ExitMatcherNode { ContextElement::ExitMatcherNode(ExitMatcherNode {
@@ -250,7 +244,7 @@ fn _text_markup_string<'b, 'g, 'r, 's, 'c>(
let (remaining, open) = tag(marker_symbol)(remaining)?; let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) = let (remaining, _peek_not_whitespace) =
peek(verify(anychar, |c| !c.is_whitespace() && *c != '\u{200B}'))(remaining)?; peek(verify(anychar, |c| !c.is_whitespace() && *c != '\u{200B}'))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into()); let text_markup_end_specialized = text_markup_end(open.into(), remaining.get_byte_offset());
let contexts = [ let contexts = [
ContextElement::ContextObject(marker_symbol), ContextElement::ContextObject(marker_symbol),
ContextElement::ExitMatcherNode(ExitMatcherNode { ContextElement::ExitMatcherNode(ExitMatcherNode {
@@ -292,16 +286,22 @@ fn pre<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> { ) -> Res<OrgSource<'s>, ()> {
if start_of_line(input).is_ok() {
return Ok((input, ()));
}
if preceded_by_whitespace(true)(input).is_ok() {
return Ok((input, ()));
}
let preceding_character = input.get_preceding_character(); let preceding_character = input.get_preceding_character();
match preceding_character { match preceding_character {
// If None, we are at the start of the file which is technically the beginning of a line. // If None, we are at the start of the file which is technically the beginning of a line.
None | Some('\r') | Some('\n') | Some(' ') | Some('\t') | Some('-') | Some('(') Some('-') | Some('(') | Some('{') | Some('\'') | Some('"') => {}
| Some('{') | Some('\'') | Some('"') | Some('<') => {}
Some(_) => { Some(_) => {
return Err(nom::Err::Error(CustomError::MyError(MyError( return Err(nom::Err::Error(CustomError::MyError(MyError(
"Not a valid pre character for text markup.".into(), "Not a valid pre character for text markup.".into(),
)))); ))));
} }
None => unreachable!(), // None is for start of file, which should already be handled by the start_of_line matcher above.
}; };
Ok((input, ())) Ok((input, ()))
} }
@@ -311,12 +311,17 @@ fn post<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> { ) -> Res<OrgSource<'s>, ()> {
let (remaining, _) = alt((recognize(one_of(" \r\n\t-.,;:!?')}[\"")), line_ending))(input)?; let (remaining, _) = alt((recognize(one_of(" \r\n\t-.,;:!?')}[\"\\")), line_ending))(input)?;
Ok((remaining, ())) Ok((remaining, ()))
} }
fn text_markup_end<'c>(marker_symbol: &'c str) -> impl ContextMatcher + 'c { fn text_markup_end<'c>(
move |context, input: OrgSource<'_>| _text_markup_end(context, input, marker_symbol) marker_symbol: &'c str,
contents_start_offset: usize,
) -> impl ContextMatcher + 'c {
move |context, input: OrgSource<'_>| {
_text_markup_end(context, input, marker_symbol, contents_start_offset)
}
} }
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -324,7 +329,13 @@ fn _text_markup_end<'b, 'g, 'r, 's, 'c>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
marker_symbol: &'c str, marker_symbol: &'c str,
contents_start_offset: usize,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, OrgSource<'s>> {
if input.get_byte_offset() == contents_start_offset {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Text markup cannot be empty".into(),
))));
}
not(preceded_by_whitespace(false))(input)?; not(preceded_by_whitespace(false))(input)?;
let (remaining, _marker) = terminated( let (remaining, _marker) = terminated(
tag(marker_symbol), tag(marker_symbol),
@@ -354,6 +365,66 @@ impl<'x> RematchObject<'x> for Bold<'x> {
} }
} }
impl<'x> RematchObject<'x> for Italic<'x> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn rematch_object<'b, 'g, 'r, 's>(
&'x self,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, children) =
_rematch_text_markup_object(_context, input, "/", &self.children)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::Italic(Italic {
source: source.into(),
children,
}),
))
}
}
impl<'x> RematchObject<'x> for Underline<'x> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn rematch_object<'b, 'g, 'r, 's>(
&'x self,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, children) =
_rematch_text_markup_object(_context, input, "_", &self.children)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::Underline(Underline {
source: source.into(),
children,
}),
))
}
}
impl<'x> RematchObject<'x> for StrikeThrough<'x> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn rematch_object<'b, 'g, 'r, 's>(
&'x self,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, children) =
_rematch_text_markup_object(_context, input, "+", &self.children)?;
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::StrikeThrough(StrikeThrough {
source: source.into(),
children,
}),
))
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn _rematch_text_markup_object<'b, 'g, 'r, 's, 'x>( fn _rematch_text_markup_object<'b, 'g, 'r, 's, 'x>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
@@ -364,7 +435,7 @@ fn _rematch_text_markup_object<'b, 'g, 'r, 's, 'x>(
let (remaining, _) = pre(context, input)?; let (remaining, _) = pre(context, input)?;
let (remaining, open) = tag(marker_symbol)(remaining)?; let (remaining, open) = tag(marker_symbol)(remaining)?;
let (remaining, _peek_not_whitespace) = peek(not(multispace1))(remaining)?; let (remaining, _peek_not_whitespace) = peek(not(multispace1))(remaining)?;
let text_markup_end_specialized = text_markup_end(open.into()); let text_markup_end_specialized = text_markup_end(open.into(), remaining.get_byte_offset());
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma, class: ExitClass::Gamma,
exit_matcher: &text_markup_end_specialized, exit_matcher: &text_markup_end_specialized,

View File

@@ -4,6 +4,7 @@ use nom::character::complete::anychar;
use nom::character::complete::digit1; use nom::character::complete::digit1;
use nom::character::complete::one_of; use nom::character::complete::one_of;
use nom::character::complete::space1; use nom::character::complete::space1;
use nom::combinator::map;
use nom::combinator::opt; use nom::combinator::opt;
use nom::combinator::recognize; use nom::combinator::recognize;
use nom::combinator::verify; use nom::combinator::verify;
@@ -20,7 +21,21 @@ use crate::context::ExitMatcherNode;
use crate::context::RefContext; use crate::context::RefContext;
use crate::error::Res; use crate::error::Res;
use crate::parser::util::get_consumed; 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::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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn timestamp<'b, 'g, 'r, 's>( pub(crate) fn timestamp<'b, 'g, 'r, 's>(
@@ -56,6 +71,14 @@ fn diary_timestamp<'b, 'g, 'r, 's>(
remaining, remaining,
Timestamp { Timestamp {
source: source.into(), 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,
}, },
)) ))
} }
@@ -96,17 +119,26 @@ fn active_timestamp<'b, 'g, 'r, 's>(
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> { ) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _) = tag("<")(input)?; let (remaining, _) = tag("<")(input)?;
let (remaining, _date) = date(context, remaining)?; let (remaining, start) = date(context, remaining)?;
let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode { let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma, class: ExitClass::Gamma,
exit_matcher: &active_time_rest_end, exit_matcher: &active_time_rest_end,
}); });
let time_context = context.with_additional_node(&time_context); let time_context = context.with_additional_node(&time_context);
let (remaining, _time) = let (remaining, time) = opt(tuple((
opt(tuple((space1, parser_with_context!(time)(&time_context))))(remaining)?; space1,
let (remaining, _repeater) = 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)?; opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?;
let (remaining, _warning_delay) = opt(tuple(( let (remaining, warning_delay) = opt(tuple((
space1, space1,
parser_with_context!(warning_delay)(context), parser_with_context!(warning_delay)(context),
)))(remaining)?; )))(remaining)?;
@@ -120,6 +152,14 @@ fn active_timestamp<'b, 'g, 'r, 's>(
remaining, remaining,
Timestamp { Timestamp {
source: source.into(), 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),
}, },
)) ))
} }
@@ -130,17 +170,26 @@ fn inactive_timestamp<'b, 'g, 'r, 's>(
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> { ) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _) = tag("[")(input)?; let (remaining, _) = tag("[")(input)?;
let (remaining, _date) = date(context, remaining)?; let (remaining, start) = date(context, remaining)?;
let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode { let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma, class: ExitClass::Gamma,
exit_matcher: &inactive_time_rest_end, exit_matcher: &inactive_time_rest_end,
}); });
let time_context = context.with_additional_node(&time_context); let time_context = context.with_additional_node(&time_context);
let (remaining, _time) = let (remaining, time) = opt(tuple((
opt(tuple((space1, parser_with_context!(time)(&time_context))))(remaining)?; space1,
let (remaining, _repeater) = 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)?; opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?;
let (remaining, _warning_delay) = opt(tuple(( let (remaining, warning_delay) = opt(tuple((
space1, space1,
parser_with_context!(warning_delay)(context), parser_with_context!(warning_delay)(context),
)))(remaining)?; )))(remaining)?;
@@ -154,6 +203,14 @@ fn inactive_timestamp<'b, 'g, 'r, 's>(
remaining, remaining,
Timestamp { Timestamp {
source: source.into(), 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),
}, },
)) ))
} }
@@ -163,10 +220,10 @@ fn active_date_range_timestamp<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'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 // 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, _separator) = tag("--")(remaining)?;
let (remaining, _second_timestamp) = active_timestamp(context, remaining)?; let (remaining, second_timestamp) = active_timestamp(context, remaining)?;
let (remaining, _trailing_whitespace) = let (remaining, _trailing_whitespace) =
maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?;
@@ -176,6 +233,16 @@ fn active_date_range_timestamp<'b, 'g, 'r, 's>(
remaining, remaining,
Timestamp { Timestamp {
source: source.into(), 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),
}, },
)) ))
} }
@@ -186,7 +253,7 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>(
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> { ) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _) = tag("<")(input)?; let (remaining, _) = tag("<")(input)?;
let (remaining, _date) = date(context, remaining)?; let (remaining, start_date) = date(context, remaining)?;
let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode { let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma, class: ExitClass::Gamma,
exit_matcher: &active_time_rest_end, exit_matcher: &active_time_rest_end,
@@ -197,13 +264,15 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>(
exit_matcher: &time_range_rest_end, exit_matcher: &time_range_rest_end,
}); });
let first_time_context = time_context.with_additional_node(&first_time_context); let first_time_context = time_context.with_additional_node(&first_time_context);
let (remaining, _first_time) = let (remaining, (_, first_time)) = tuple((
tuple((space1, parser_with_context!(time)(&first_time_context)))(remaining)?; space1,
parser_with_context!(time(false))(&first_time_context),
))(remaining)?;
let (remaining, _) = tag("-")(remaining)?; let (remaining, _) = tag("-")(remaining)?;
let (remaining, _second_time) = parser_with_context!(time)(&time_context)(remaining)?; let (remaining, second_time) = parser_with_context!(time(true))(&time_context)(remaining)?;
let (remaining, _repeater) = let (remaining, repeater) =
opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?; opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?;
let (remaining, _warning_delay) = opt(tuple(( let (remaining, warning_delay) = opt(tuple((
space1, space1,
parser_with_context!(warning_delay)(context), parser_with_context!(warning_delay)(context),
)))(remaining)?; )))(remaining)?;
@@ -217,6 +286,14 @@ fn active_time_range_timestamp<'b, 'g, 'r, 's>(
remaining, remaining,
Timestamp { Timestamp {
source: source.into(), 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),
}, },
)) ))
} }
@@ -226,10 +303,10 @@ fn inactive_date_range_timestamp<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'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 // 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, _separator) = tag("--")(remaining)?;
let (remaining, _second_timestamp) = inactive_timestamp(context, remaining)?; let (remaining, second_timestamp) = inactive_timestamp(context, remaining)?;
let (remaining, _trailing_whitespace) = let (remaining, _trailing_whitespace) =
maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?;
@@ -239,6 +316,17 @@ fn inactive_date_range_timestamp<'b, 'g, 'r, 's>(
remaining, remaining,
Timestamp { Timestamp {
source: source.into(), 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),
}, },
)) ))
} }
@@ -249,7 +337,7 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>(
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Timestamp<'s>> { ) -> Res<OrgSource<'s>, Timestamp<'s>> {
let (remaining, _) = tag("[")(input)?; let (remaining, _) = tag("[")(input)?;
let (remaining, _date) = date(context, remaining)?; let (remaining, start_date) = date(context, remaining)?;
let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode { let time_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma, class: ExitClass::Gamma,
exit_matcher: &inactive_time_rest_end, exit_matcher: &inactive_time_rest_end,
@@ -260,13 +348,15 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>(
exit_matcher: &time_range_rest_end, exit_matcher: &time_range_rest_end,
}); });
let first_time_context = time_context.with_additional_node(&first_time_context); let first_time_context = time_context.with_additional_node(&first_time_context);
let (remaining, _first_time) = let (remaining, (_, first_time)) = tuple((
tuple((space1, parser_with_context!(time)(&first_time_context)))(remaining)?; space1,
parser_with_context!(time(false))(&first_time_context),
))(remaining)?;
let (remaining, _) = tag("-")(remaining)?; let (remaining, _) = tag("-")(remaining)?;
let (remaining, _second_time) = parser_with_context!(time)(&time_context)(remaining)?; let (remaining, second_time) = parser_with_context!(time(true))(&time_context)(remaining)?;
let (remaining, _repeater) = let (remaining, repeater) =
opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?; opt(tuple((space1, parser_with_context!(repeater)(context))))(remaining)?;
let (remaining, _warning_delay) = opt(tuple(( let (remaining, warning_delay) = opt(tuple((
space1, space1,
parser_with_context!(warning_delay)(context), parser_with_context!(warning_delay)(context),
)))(remaining)?; )))(remaining)?;
@@ -280,6 +370,14 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>(
remaining, remaining,
Timestamp { Timestamp {
source: source.into(), 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),
}, },
)) ))
} }
@@ -288,18 +386,33 @@ fn inactive_time_range_timestamp<'b, 'g, 'r, 's>(
fn date<'b, 'g, 'r, 's>( fn date<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, Date<'s>> {
let (remaining, _year) = verify(digit1, |year: &OrgSource<'_>| year.len() == 4)(input)?; let (remaining, year) = verify(digit1, |year: &OrgSource<'_>| year.len() == 4)(input)?;
let (remaining, _) = tag("-")(remaining)?; 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, _) = 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 day_of_month.len() == 2
})(remaining)?; })(remaining)?;
let (remaining, _dayname) = let (remaining, day_name) =
opt(tuple((space1, parser_with_context!(dayname)(context))))(remaining)?; 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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -334,20 +447,39 @@ fn dayname_end<'b, 'g, 'r, 's>(
}))(input) }))(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"))] #[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>, context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { allow_rest: bool,
let (remaining, _hour) = verify(digit1, |hour: &OrgSource<'_>| { ) -> Res<OrgSource<'s>, Time<'s>> {
let (remaining, hour) = verify(digit1, |hour: &OrgSource<'_>| {
hour.len() >= 1 && hour.len() <= 2 hour.len() >= 1 && hour.len() <= 2
})(input)?; })(input)?;
let (remaining, _) = tag(":")(remaining)?; let (remaining, _) = tag(":")(remaining)?;
let (remaining, _minute) = let (remaining, minute) =
verify(digit1, |minute: &OrgSource<'_>| minute.len() == 2)(remaining)?; verify(digit1, |minute: &OrgSource<'_>| minute.len() == 2)(remaining)?;
let (remaining, _time_rest) = opt(parser_with_context!(time_rest)(context))(remaining)?; let (remaining, time_rest) = if allow_rest {
let source = get_consumed(input, remaining); opt(parser_with_context!(time_rest)(context))(remaining)?
Ok((remaining, source)) } 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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -400,8 +532,10 @@ fn time_range_rest_end<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'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. // 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 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 = let exit_contents = recognize(tuple((
recognize(tuple((tag("-"), parser_with_context!(time)(&parent_node))))(input); tag("-"),
parser_with_context!(time(true))(&parent_node),
)))(input);
exit_contents exit_contents
} }
@@ -409,29 +543,66 @@ fn time_range_rest_end<'b, 'g, 'r, 's>(
fn repeater<'b, 'g, 'r, 's>( fn repeater<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, Repeater> {
// + for cumulative type // + for cumulative type
// ++ for catch-up type // ++ for catch-up type
// .+ for restart type // .+ for restart type
let (remaining, _mark) = alt((tag("++"), tag("+"), tag(".+")))(input)?; let (remaining, repeater_type) = alt((
let (remaining, _value) = digit1(remaining)?; 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 // h = hour, d = day, w = week, m = month, y = year
let (remaining, _unit) = recognize(one_of("hdwmy"))(remaining)?; let (remaining, unit) = alt((
let source = get_consumed(input, remaining); map(tag("h"), |_| TimeUnit::Hour),
Ok((remaining, source)) 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"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn warning_delay<'b, 'g, 'r, 's>( fn warning_delay<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>, _context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>, input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> { ) -> Res<OrgSource<'s>, WarningDelay> {
// - for all type // - for all type
// -- for first type // -- for first type
let (remaining, _mark) = alt((tag("--"), tag("-")))(input)?; let (remaining, warning_delay_type) = alt((
let (remaining, _value) = digit1(remaining)?; 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 // h = hour, d = day, w = week, m = month, y = year
let (remaining, _unit) = recognize(one_of("hdwmy"))(remaining)?; let (remaining, unit) = alt((
let source = get_consumed(input, remaining); map(tag("h"), |_| TimeUnit::Hour),
Ok((remaining, source)) 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,135 +0,0 @@
use std::collections::VecDeque;
use crate::types::Document;
use crate::types::DocumentElement;
use crate::types::Element;
use crate::types::Heading;
use crate::types::NodeProperty;
use crate::types::Object;
use crate::types::PlainListItem;
use crate::types::Section;
use crate::types::TableCell;
use crate::types::TableRow;
pub(crate) enum Token<'r, 's> {
Document(&'r Document<'s>),
Heading(&'r Heading<'s>),
Section(&'r Section<'s>),
Object(&'r Object<'s>),
Element(&'r Element<'s>),
PlainListItem(&'r PlainListItem<'s>),
TableRow(&'r TableRow<'s>),
TableCell(&'r TableCell<'s>),
NodeProperty(&'r NodeProperty<'s>),
}
impl<'r, 's> Token<'r, 's> {
fn iter_tokens(&self) -> Box<dyn Iterator<Item = Token<'r, 's>> + '_> {
match self {
Token::Document(document) => Box::new(
document
.zeroth_section
.iter()
.map(Token::Section)
.chain(document.children.iter().map(Token::Heading)),
),
Token::Heading(heading) => Box::new(heading.title.iter().map(Token::Object).chain(
heading.children.iter().map(|de| match de {
DocumentElement::Heading(ref obj) => Token::Heading(obj),
DocumentElement::Section(ref obj) => Token::Section(obj),
}),
)),
Token::Section(section) => Box::new(section.children.iter().map(Token::Element)),
Token::Object(obj) => match obj {
Object::Bold(inner) => Box::new(inner.children.iter().map(Token::Object)),
Object::Italic(inner) => Box::new(inner.children.iter().map(Token::Object)),
Object::Underline(inner) => Box::new(inner.children.iter().map(Token::Object)),
Object::StrikeThrough(inner) => Box::new(inner.children.iter().map(Token::Object)),
Object::Code(_) => Box::new(std::iter::empty()),
Object::Verbatim(_) => Box::new(std::iter::empty()),
Object::PlainText(_) => Box::new(std::iter::empty()),
Object::RegularLink(_) => Box::new(std::iter::empty()),
Object::RadioLink(inner) => Box::new(inner.children.iter().map(Token::Object)),
Object::RadioTarget(inner) => Box::new(inner.children.iter().map(Token::Object)),
Object::PlainLink(_) => Box::new(std::iter::empty()),
Object::AngleLink(_) => Box::new(std::iter::empty()),
Object::OrgMacro(_) => Box::new(std::iter::empty()),
Object::Entity(_) => Box::new(std::iter::empty()),
Object::LatexFragment(_) => Box::new(std::iter::empty()),
Object::ExportSnippet(_) => Box::new(std::iter::empty()),
Object::FootnoteReference(inner) => {
Box::new(inner.definition.iter().map(Token::Object))
}
Object::Citation(_) => Box::new(std::iter::empty()), // TODO: Iterate over children
Object::CitationReference(_) => Box::new(std::iter::empty()), // TODO: Iterate over children
Object::InlineBabelCall(_) => Box::new(std::iter::empty()),
Object::InlineSourceBlock(_) => Box::new(std::iter::empty()),
Object::LineBreak(_) => Box::new(std::iter::empty()),
Object::Target(_) => Box::new(std::iter::empty()),
Object::StatisticsCookie(_) => Box::new(std::iter::empty()),
Object::Subscript(_) => Box::new(std::iter::empty()), // TODO: Iterate over children
Object::Superscript(_) => Box::new(std::iter::empty()), // TODO: Iterate over children
Object::Timestamp(_) => Box::new(std::iter::empty()),
},
Token::Element(elem) => match elem {
Element::Paragraph(inner) => Box::new(inner.children.iter().map(Token::Object)),
Element::PlainList(inner) => {
Box::new(inner.children.iter().map(Token::PlainListItem))
}
Element::GreaterBlock(inner) => Box::new(inner.children.iter().map(Token::Element)),
Element::DynamicBlock(inner) => Box::new(inner.children.iter().map(Token::Element)),
Element::FootnoteDefinition(inner) => {
Box::new(inner.children.iter().map(Token::Element))
}
Element::Comment(_) => Box::new(std::iter::empty()),
Element::Drawer(inner) => Box::new(inner.children.iter().map(Token::Element)),
Element::PropertyDrawer(inner) => {
Box::new(inner.children.iter().map(Token::NodeProperty))
}
Element::Table(inner) => Box::new(inner.children.iter().map(Token::TableRow)),
Element::VerseBlock(inner) => Box::new(inner.children.iter().map(Token::Object)),
Element::CommentBlock(_) => Box::new(std::iter::empty()),
Element::ExampleBlock(_) => Box::new(std::iter::empty()),
Element::ExportBlock(_) => Box::new(std::iter::empty()),
Element::SrcBlock(_) => Box::new(std::iter::empty()),
Element::Clock(_) => Box::new(std::iter::empty()),
Element::DiarySexp(_) => Box::new(std::iter::empty()),
Element::Planning(_) => Box::new(std::iter::empty()),
Element::FixedWidthArea(_) => Box::new(std::iter::empty()),
Element::HorizontalRule(_) => Box::new(std::iter::empty()),
Element::Keyword(_) => Box::new(std::iter::empty()),
Element::BabelCall(_) => Box::new(std::iter::empty()),
Element::LatexEnvironment(_) => Box::new(std::iter::empty()),
},
Token::PlainListItem(elem) => Box::new(elem.children.iter().map(Token::Element)),
Token::TableRow(elem) => Box::new(elem.children.iter().map(Token::TableCell)),
Token::TableCell(elem) => Box::new(elem.children.iter().map(Token::Object)),
Token::NodeProperty(_) => Box::new(std::iter::empty()),
}
}
}
pub(crate) struct AllTokensIterator<'r, 's> {
queued_tokens: VecDeque<Token<'r, 's>>,
}
impl<'r, 's> AllTokensIterator<'r, 's> {
pub(crate) fn new(tkn: Token<'r, 's>) -> Self {
let mut queued_tokens = VecDeque::new();
queued_tokens.push_back(tkn);
AllTokensIterator { queued_tokens }
}
}
impl<'r, 's> Iterator for AllTokensIterator<'r, 's> {
type Item = Token<'r, 's>;
fn next(&mut self) -> Option<Self::Item> {
let next_token = match self.queued_tokens.pop_front() {
Some(tkn) => tkn,
None => return None,
};
self.queued_tokens.extend(next_token.iter_tokens());
Some(next_token)
}
}

View File

@@ -1,4 +1,5 @@
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::is_a;
use nom::character::complete::anychar; use nom::character::complete::anychar;
use nom::character::complete::line_ending; use nom::character::complete::line_ending;
use nom::character::complete::none_of; use nom::character::complete::none_of;
@@ -20,6 +21,7 @@ use crate::context::RefContext;
use crate::error::CustomError; use crate::error::CustomError;
use crate::error::MyError; use crate::error::MyError;
use crate::error::Res; use crate::error::Res;
use crate::types::IndentationLevel;
pub(crate) const WORD_CONSTITUENT_CHARACTERS: &str = pub(crate) const WORD_CONSTITUENT_CHARACTERS: &str =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
@@ -212,6 +214,9 @@ fn text_until_eol<'r, 's>(
Ok(line.trim()) Ok(line.trim())
} }
/// Return a tuple of (input, output) from a nom parser.
///
/// This is similar to recognize except it returns the input instead of the portion of the input that was consumed.
pub(crate) fn include_input<'s, F, O>( pub(crate) fn include_input<'s, F, O>(
mut inner: F, mut inner: F,
) -> impl FnMut(OrgSource<'s>) -> Res<OrgSource<'s>, (OrgSource<'s>, O)> ) -> impl FnMut(OrgSource<'s>) -> Res<OrgSource<'s>, (OrgSource<'s>, O)>
@@ -223,3 +228,50 @@ where
Ok((remaining, (input, output))) Ok((remaining, (input, output)))
} }
} }
/// Match at least one space character.
///
/// This is similar to nom's space1 parser except space1 matches both spaces and tabs whereas this only matches spaces.
pub(crate) fn only_space1<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
is_a(" ")(input)
}
/// Match single space or tab.
///
/// In org-mode syntax, spaces and tabs are often (but not always!) interchangeable.
pub(crate) fn org_space<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, char> {
one_of(" \t")(input)
}
/// Matches a single space, tab, line ending, or end of file.
///
/// In org-mode syntax there are often delimiters that could be any whitespace at all or the end of file.
pub(crate) fn org_space_or_line_ending<'s>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt((recognize(org_space), org_line_ending))(input)
}
/// Match a line break or the end of the file.
///
/// In org-mode syntax, the end of the file can serve the same purpose as a line break syntactically.
pub(crate) fn org_line_ending<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt((line_ending, eof))(input)
}
/// Match the whitespace at the beginning of a line and give it an indentation level.
pub(crate) fn indentation_level<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, (IndentationLevel, OrgSource<'s>)> {
let (remaining, leading_whitespace) = space0(input)?;
let indentation_level = Into::<&str>::into(leading_whitespace)
.chars()
.map(|c| match c {
' ' => 1,
'\t' => context.get_global_settings().tab_width,
_ => unreachable!(),
})
.sum();
Ok((remaining, (indentation_level, leading_whitespace)))
}

325
src/types/ast_node.rs Normal file
View File

@@ -0,0 +1,325 @@
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;
use crate::types::Citation;
use crate::types::CitationReference;
use crate::types::Clock;
use crate::types::Code;
use crate::types::Comment;
use crate::types::CommentBlock;
use crate::types::DiarySexp;
use crate::types::Document;
use crate::types::DocumentElement;
use crate::types::Drawer;
use crate::types::DynamicBlock;
use crate::types::Element;
use crate::types::Entity;
use crate::types::ExampleBlock;
use crate::types::ExportBlock;
use crate::types::ExportSnippet;
use crate::types::FixedWidthArea;
use crate::types::FootnoteDefinition;
use crate::types::FootnoteReference;
use crate::types::GetStandardProperties;
use crate::types::Heading;
use crate::types::HorizontalRule;
use crate::types::InlineBabelCall;
use crate::types::InlineSourceBlock;
use crate::types::Italic;
use crate::types::Keyword;
use crate::types::LatexEnvironment;
use crate::types::LatexFragment;
use crate::types::LineBreak;
use crate::types::NodeProperty;
use crate::types::Object;
use crate::types::OrgMacro;
use crate::types::Paragraph;
use crate::types::PlainLink;
use crate::types::PlainList;
use crate::types::PlainListItem;
use crate::types::PlainText;
use crate::types::Planning;
use crate::types::PropertyDrawer;
use crate::types::RadioLink;
use crate::types::RadioTarget;
use crate::types::RegularLink;
use crate::types::Section;
use crate::types::SrcBlock;
use crate::types::StatisticsCookie;
use crate::types::StrikeThrough;
use crate::types::Subscript;
use crate::types::Superscript;
use crate::types::Table;
use crate::types::TableCell;
use crate::types::TableRow;
use crate::types::Target;
use crate::types::Timestamp;
use crate::types::Underline;
use crate::types::Verbatim;
use crate::types::VerseBlock;
pub enum AstNode<'r, 's> {
// Document Nodes
Document(&'r Document<'s>),
Heading(&'r Heading<'s>),
Section(&'r Section<'s>),
// Elements
Paragraph(&'r Paragraph<'s>),
PlainList(&'r PlainList<'s>),
PlainListItem(&'r PlainListItem<'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>),
Drawer(&'r Drawer<'s>),
PropertyDrawer(&'r PropertyDrawer<'s>),
NodeProperty(&'r NodeProperty<'s>),
Table(&'r Table<'s>),
TableRow(&'r TableRow<'s>),
VerseBlock(&'r VerseBlock<'s>),
CommentBlock(&'r CommentBlock<'s>),
ExampleBlock(&'r ExampleBlock<'s>),
ExportBlock(&'r ExportBlock<'s>),
SrcBlock(&'r SrcBlock<'s>),
Clock(&'r Clock<'s>),
DiarySexp(&'r DiarySexp<'s>),
Planning(&'r Planning<'s>),
FixedWidthArea(&'r FixedWidthArea<'s>),
HorizontalRule(&'r HorizontalRule<'s>),
Keyword(&'r Keyword<'s>),
BabelCall(&'r BabelCall<'s>),
LatexEnvironment(&'r LatexEnvironment<'s>),
// Objects
Bold(&'r Bold<'s>),
Italic(&'r Italic<'s>),
Underline(&'r Underline<'s>),
StrikeThrough(&'r StrikeThrough<'s>),
Code(&'r Code<'s>),
Verbatim(&'r Verbatim<'s>),
PlainText(&'r PlainText<'s>),
RegularLink(&'r RegularLink<'s>),
RadioLink(&'r RadioLink<'s>),
RadioTarget(&'r RadioTarget<'s>),
PlainLink(&'r PlainLink<'s>),
AngleLink(&'r AngleLink<'s>),
OrgMacro(&'r OrgMacro<'s>),
Entity(&'r Entity<'s>),
LatexFragment(&'r LatexFragment<'s>),
ExportSnippet(&'r ExportSnippet<'s>),
FootnoteReference(&'r FootnoteReference<'s>),
Citation(&'r Citation<'s>),
CitationReference(&'r CitationReference<'s>),
InlineBabelCall(&'r InlineBabelCall<'s>),
InlineSourceBlock(&'r InlineSourceBlock<'s>),
LineBreak(&'r LineBreak<'s>),
Target(&'r Target<'s>),
StatisticsCookie(&'r StatisticsCookie<'s>),
Subscript(&'r Subscript<'s>),
Superscript(&'r Superscript<'s>),
TableCell(&'r TableCell<'s>),
Timestamp(&'r Timestamp<'s>),
}
impl<'r, 's> From<&'r DocumentElement<'s>> for AstNode<'r, 's> {
fn from(value: &'r DocumentElement<'s>) -> Self {
match value {
DocumentElement::Heading(inner) => inner.into(),
DocumentElement::Section(inner) => inner.into(),
}
}
}
impl<'r, 's> From<&'r Element<'s>> for AstNode<'r, 's> {
fn from(value: &'r Element<'s>) -> Self {
match value {
Element::Paragraph(inner) => inner.into(),
Element::PlainList(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(),
Element::Drawer(inner) => inner.into(),
Element::PropertyDrawer(inner) => inner.into(),
Element::Table(inner) => inner.into(),
Element::VerseBlock(inner) => inner.into(),
Element::CommentBlock(inner) => inner.into(),
Element::ExampleBlock(inner) => inner.into(),
Element::ExportBlock(inner) => inner.into(),
Element::SrcBlock(inner) => inner.into(),
Element::Clock(inner) => inner.into(),
Element::DiarySexp(inner) => inner.into(),
Element::Planning(inner) => inner.into(),
Element::FixedWidthArea(inner) => inner.into(),
Element::HorizontalRule(inner) => inner.into(),
Element::Keyword(inner) => inner.into(),
Element::BabelCall(inner) => inner.into(),
Element::LatexEnvironment(inner) => inner.into(),
}
}
}
impl<'r, 's> From<&'r Object<'s>> for AstNode<'r, 's> {
fn from(value: &'r Object<'s>) -> Self {
match value {
Object::Bold(inner) => inner.into(),
Object::Italic(inner) => inner.into(),
Object::Underline(inner) => inner.into(),
Object::StrikeThrough(inner) => inner.into(),
Object::Code(inner) => inner.into(),
Object::Verbatim(inner) => inner.into(),
Object::PlainText(inner) => inner.into(),
Object::RegularLink(inner) => inner.into(),
Object::RadioLink(inner) => inner.into(),
Object::RadioTarget(inner) => inner.into(),
Object::PlainLink(inner) => inner.into(),
Object::AngleLink(inner) => inner.into(),
Object::OrgMacro(inner) => inner.into(),
Object::Entity(inner) => inner.into(),
Object::LatexFragment(inner) => inner.into(),
Object::ExportSnippet(inner) => inner.into(),
Object::FootnoteReference(inner) => inner.into(),
Object::Citation(inner) => inner.into(),
Object::CitationReference(inner) => inner.into(),
Object::InlineBabelCall(inner) => inner.into(),
Object::InlineSourceBlock(inner) => inner.into(),
Object::LineBreak(inner) => inner.into(),
Object::Target(inner) => inner.into(),
Object::StatisticsCookie(inner) => inner.into(),
Object::Subscript(inner) => inner.into(),
Object::Superscript(inner) => inner.into(),
Object::Timestamp(inner) => inner.into(),
}
}
}
to_ast_node!(&'r Document<'s>, AstNode::Document);
to_ast_node!(&'r Heading<'s>, AstNode::Heading);
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 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);
to_ast_node!(&'r Drawer<'s>, AstNode::Drawer);
to_ast_node!(&'r PropertyDrawer<'s>, AstNode::PropertyDrawer);
to_ast_node!(&'r NodeProperty<'s>, AstNode::NodeProperty);
to_ast_node!(&'r Table<'s>, AstNode::Table);
to_ast_node!(&'r TableRow<'s>, AstNode::TableRow);
to_ast_node!(&'r VerseBlock<'s>, AstNode::VerseBlock);
to_ast_node!(&'r CommentBlock<'s>, AstNode::CommentBlock);
to_ast_node!(&'r ExampleBlock<'s>, AstNode::ExampleBlock);
to_ast_node!(&'r ExportBlock<'s>, AstNode::ExportBlock);
to_ast_node!(&'r SrcBlock<'s>, AstNode::SrcBlock);
to_ast_node!(&'r Clock<'s>, AstNode::Clock);
to_ast_node!(&'r DiarySexp<'s>, AstNode::DiarySexp);
to_ast_node!(&'r Planning<'s>, AstNode::Planning);
to_ast_node!(&'r FixedWidthArea<'s>, AstNode::FixedWidthArea);
to_ast_node!(&'r HorizontalRule<'s>, AstNode::HorizontalRule);
to_ast_node!(&'r Keyword<'s>, AstNode::Keyword);
to_ast_node!(&'r BabelCall<'s>, AstNode::BabelCall);
to_ast_node!(&'r LatexEnvironment<'s>, AstNode::LatexEnvironment);
to_ast_node!(&'r Bold<'s>, AstNode::Bold);
to_ast_node!(&'r Italic<'s>, AstNode::Italic);
to_ast_node!(&'r Underline<'s>, AstNode::Underline);
to_ast_node!(&'r StrikeThrough<'s>, AstNode::StrikeThrough);
to_ast_node!(&'r Code<'s>, AstNode::Code);
to_ast_node!(&'r Verbatim<'s>, AstNode::Verbatim);
to_ast_node!(&'r PlainText<'s>, AstNode::PlainText);
to_ast_node!(&'r RegularLink<'s>, AstNode::RegularLink);
to_ast_node!(&'r RadioLink<'s>, AstNode::RadioLink);
to_ast_node!(&'r RadioTarget<'s>, AstNode::RadioTarget);
to_ast_node!(&'r PlainLink<'s>, AstNode::PlainLink);
to_ast_node!(&'r AngleLink<'s>, AstNode::AngleLink);
to_ast_node!(&'r OrgMacro<'s>, AstNode::OrgMacro);
to_ast_node!(&'r Entity<'s>, AstNode::Entity);
to_ast_node!(&'r LatexFragment<'s>, AstNode::LatexFragment);
to_ast_node!(&'r ExportSnippet<'s>, AstNode::ExportSnippet);
to_ast_node!(&'r FootnoteReference<'s>, AstNode::FootnoteReference);
to_ast_node!(&'r Citation<'s>, AstNode::Citation);
to_ast_node!(&'r CitationReference<'s>, AstNode::CitationReference);
to_ast_node!(&'r InlineBabelCall<'s>, AstNode::InlineBabelCall);
to_ast_node!(&'r InlineSourceBlock<'s>, AstNode::InlineSourceBlock);
to_ast_node!(&'r LineBreak<'s>, AstNode::LineBreak);
to_ast_node!(&'r Target<'s>, AstNode::Target);
to_ast_node!(&'r StatisticsCookie<'s>, AstNode::StatisticsCookie);
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,12 +1,19 @@
use std::path::PathBuf;
use super::Element; use super::Element;
use super::GetStandardProperties;
use super::Object; use super::Object;
use super::Source; use super::StandardProperties;
use super::Timestamp;
pub type PriorityCookie = u8; pub type PriorityCookie = u8;
pub type HeadlineLevel = u16;
#[derive(Debug)] #[derive(Debug)]
pub struct Document<'s> { pub struct Document<'s> {
pub source: &'s str, pub source: &'s str,
pub category: Option<String>,
pub path: Option<PathBuf>,
pub zeroth_section: Option<Section<'s>>, pub zeroth_section: Option<Section<'s>>,
pub children: Vec<Heading<'s>>, pub children: Vec<Heading<'s>>,
} }
@@ -14,7 +21,7 @@ pub struct Document<'s> {
#[derive(Debug)] #[derive(Debug)]
pub struct Heading<'s> { pub struct Heading<'s> {
pub source: &'s str, pub source: &'s str,
pub stars: usize, pub level: HeadlineLevel,
pub todo_keyword: Option<(TodoKeywordType, &'s str)>, pub todo_keyword: Option<(TodoKeywordType, &'s str)>,
pub priority_cookie: Option<PriorityCookie>, pub priority_cookie: Option<PriorityCookie>,
pub title: Vec<Object<'s>>, pub title: Vec<Object<'s>>,
@@ -22,6 +29,10 @@ pub struct Heading<'s> {
pub children: Vec<DocumentElement<'s>>, pub children: Vec<DocumentElement<'s>>,
pub is_comment: bool, pub is_comment: bool,
pub is_archived: 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)] #[derive(Debug)]
@@ -42,29 +53,41 @@ pub enum TodoKeywordType {
Done, Done,
} }
impl<'s> Source<'s> for Document<'s> { impl<'s> GetStandardProperties<'s> for DocumentElement<'s> {
fn get_source(&'s self) -> &'s str { fn get_standard_properties<'b>(&'b self) -> &'b dyn StandardProperties<'s> {
self.source
}
}
impl<'s> Source<'s> for DocumentElement<'s> {
fn get_source(&'s self) -> &'s str {
match self { match self {
DocumentElement::Heading(obj) => obj.source, DocumentElement::Heading(inner) => inner,
DocumentElement::Section(obj) => obj.source, DocumentElement::Section(inner) => inner,
} }
} }
} }
impl<'s> Source<'s> for Section<'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 self.source
} }
} }
impl<'s> Source<'s> for Heading<'s> { impl<'s> StandardProperties<'s> for Section<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> StandardProperties<'s> for Heading<'s> {
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,9 +1,9 @@
use super::greater_element::DynamicBlock; use super::greater_element::DynamicBlock;
use super::greater_element::FootnoteDefinition; use super::greater_element::FootnoteDefinition;
use super::greater_element::GreaterBlock;
use super::greater_element::PlainList; use super::greater_element::PlainList;
use super::greater_element::PropertyDrawer; use super::greater_element::PropertyDrawer;
use super::greater_element::Table; use super::greater_element::Table;
use super::lesser_element::BabelCall;
use super::lesser_element::Clock; use super::lesser_element::Clock;
use super::lesser_element::Comment; use super::lesser_element::Comment;
use super::lesser_element::CommentBlock; use super::lesser_element::CommentBlock;
@@ -18,15 +18,21 @@ use super::lesser_element::Paragraph;
use super::lesser_element::Planning; use super::lesser_element::Planning;
use super::lesser_element::SrcBlock; use super::lesser_element::SrcBlock;
use super::lesser_element::VerseBlock; use super::lesser_element::VerseBlock;
use super::CenterBlock;
use super::Drawer; use super::Drawer;
use super::GetStandardProperties;
use super::QuoteBlock;
use super::SetSource; use super::SetSource;
use super::Source; use super::SpecialBlock;
use super::StandardProperties;
#[derive(Debug)] #[derive(Debug)]
pub enum Element<'s> { pub enum Element<'s> {
Paragraph(Paragraph<'s>), Paragraph(Paragraph<'s>),
PlainList(PlainList<'s>), PlainList(PlainList<'s>),
GreaterBlock(GreaterBlock<'s>), CenterBlock(CenterBlock<'s>),
QuoteBlock(QuoteBlock<'s>),
SpecialBlock(SpecialBlock<'s>),
DynamicBlock(DynamicBlock<'s>), DynamicBlock(DynamicBlock<'s>),
FootnoteDefinition(FootnoteDefinition<'s>), FootnoteDefinition(FootnoteDefinition<'s>),
Comment(Comment<'s>), Comment(Comment<'s>),
@@ -44,46 +50,19 @@ pub enum Element<'s> {
FixedWidthArea(FixedWidthArea<'s>), FixedWidthArea(FixedWidthArea<'s>),
HorizontalRule(HorizontalRule<'s>), HorizontalRule(HorizontalRule<'s>),
Keyword(Keyword<'s>), Keyword(Keyword<'s>),
BabelCall(Keyword<'s>), BabelCall(BabelCall<'s>),
LatexEnvironment(LatexEnvironment<'s>), LatexEnvironment(LatexEnvironment<'s>),
} }
impl<'s> Source<'s> for Element<'s> {
fn get_source(&'s self) -> &'s str {
match self {
Element::Paragraph(obj) => obj.get_source(),
Element::PlainList(obj) => obj.get_source(),
Element::GreaterBlock(obj) => obj.get_source(),
Element::DynamicBlock(obj) => obj.get_source(),
Element::FootnoteDefinition(obj) => obj.get_source(),
Element::Comment(obj) => obj.get_source(),
Element::Drawer(obj) => obj.get_source(),
Element::PropertyDrawer(obj) => obj.get_source(),
Element::Table(obj) => obj.get_source(),
Element::VerseBlock(obj) => obj.get_source(),
Element::CommentBlock(obj) => obj.get_source(),
Element::ExampleBlock(obj) => obj.get_source(),
Element::ExportBlock(obj) => obj.get_source(),
Element::SrcBlock(obj) => obj.get_source(),
Element::Clock(obj) => obj.get_source(),
Element::DiarySexp(obj) => obj.get_source(),
Element::Planning(obj) => obj.get_source(),
Element::FixedWidthArea(obj) => obj.get_source(),
Element::HorizontalRule(obj) => obj.get_source(),
Element::Keyword(obj) => obj.get_source(),
Element::BabelCall(obj) => obj.get_source(),
Element::LatexEnvironment(obj) => obj.get_source(),
}
}
}
impl<'s> SetSource<'s> for Element<'s> { impl<'s> SetSource<'s> for Element<'s> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn set_source(&mut self, source: &'s str) { fn set_source(&mut self, source: &'s str) {
match self { match self {
Element::Paragraph(obj) => obj.source = source, Element::Paragraph(obj) => obj.source = source,
Element::PlainList(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::DynamicBlock(obj) => obj.source = source,
Element::FootnoteDefinition(obj) => obj.source = source, Element::FootnoteDefinition(obj) => obj.source = source,
Element::Comment(obj) => obj.source = source, Element::Comment(obj) => obj.source = source,
@@ -106,3 +85,34 @@ impl<'s> SetSource<'s> for Element<'s> {
} }
} }
} }
impl<'s> GetStandardProperties<'s> for Element<'s> {
fn get_standard_properties<'b>(&'b self) -> &'b dyn StandardProperties<'s> {
match self {
Element::Paragraph(inner) => inner,
Element::PlainList(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,
Element::Drawer(inner) => inner,
Element::PropertyDrawer(inner) => inner,
Element::Table(inner) => inner,
Element::VerseBlock(inner) => inner,
Element::CommentBlock(inner) => inner,
Element::ExampleBlock(inner) => inner,
Element::ExportBlock(inner) => inner,
Element::SrcBlock(inner) => inner,
Element::Clock(inner) => inner,
Element::DiarySexp(inner) => inner,
Element::Planning(inner) => inner,
Element::FixedWidthArea(inner) => inner,
Element::HorizontalRule(inner) => inner,
Element::Keyword(inner) => inner,
Element::BabelCall(inner) => inner,
Element::LatexEnvironment(inner) => inner,
}
}
}

View File

@@ -0,0 +1,12 @@
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<'b>(&'b self) -> &'b dyn StandardProperties<'s>;
}
impl<'s, I: StandardProperties<'s>> GetStandardProperties<'s> for I {
fn get_standard_properties<'b>(&'b self) -> &'b dyn StandardProperties<'s> {
self
}
}

View File

@@ -2,25 +2,61 @@ use super::element::Element;
use super::lesser_element::TableCell; use super::lesser_element::TableCell;
use super::Keyword; use super::Keyword;
use super::Object; use super::Object;
use super::Source; use super::StandardProperties;
#[derive(Debug)] #[derive(Debug)]
pub struct PlainList<'s> { pub struct PlainList<'s> {
pub source: &'s str, pub source: &'s str,
pub list_type: PlainListType,
pub children: Vec<PlainListItem<'s>>, pub children: Vec<PlainListItem<'s>>,
} }
#[derive(Debug, Copy, Clone)]
pub enum PlainListType {
Unordered,
Ordered,
Descriptive,
}
/// The width that something is indented. For example, a single tab character could be a value of 4 or 8.
pub type IndentationLevel = u16;
#[derive(Debug)] #[derive(Debug)]
pub struct PlainListItem<'s> { pub struct PlainListItem<'s> {
pub source: &'s str, pub source: &'s str,
pub indentation: usize, pub indentation: IndentationLevel,
pub bullet: &'s str, pub bullet: &'s str,
pub counter: Option<PlainListItemCounter>,
pub checkbox: Option<(CheckboxType, &'s str)>,
pub tag: Vec<Object<'s>>, 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,
Trans,
Off,
}
#[derive(Debug)]
pub struct CenterBlock<'s> {
pub source: &'s str,
pub children: Vec<Element<'s>>, pub children: Vec<Element<'s>>,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct GreaterBlock<'s> { pub struct QuoteBlock<'s> {
pub source: &'s str,
pub children: Vec<Element<'s>>,
}
#[derive(Debug)]
pub struct SpecialBlock<'s> {
pub source: &'s str, pub source: &'s str,
pub name: &'s str, pub name: &'s str,
pub parameters: Option<&'s str>, pub parameters: Option<&'s str>,
@@ -58,6 +94,7 @@ pub struct PropertyDrawer<'s> {
#[derive(Debug)] #[derive(Debug)]
pub struct NodeProperty<'s> { pub struct NodeProperty<'s> {
pub source: &'s str, pub source: &'s str,
pub name: &'s str,
pub value: Option<&'s str>, pub value: Option<&'s str>,
} }
@@ -74,62 +111,102 @@ pub struct TableRow<'s> {
pub children: Vec<TableCell<'s>>, pub children: Vec<TableCell<'s>>,
} }
impl<'s> Source<'s> for PlainList<'s> { #[derive(Debug)]
fn get_source(&'s self) -> &'s str { pub enum TableRowType {
Standard,
Rule,
}
impl<'s> StandardProperties<'s> for PlainList<'s> {
fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for PlainListItem<'s> { impl<'s> StandardProperties<'s> for PlainListItem<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for GreaterBlock<'s> { impl<'s> StandardProperties<'s> for CenterBlock<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for DynamicBlock<'s> { impl<'s> StandardProperties<'s> for QuoteBlock<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for FootnoteDefinition<'s> { impl<'s> StandardProperties<'s> for SpecialBlock<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for Drawer<'s> { impl<'s> StandardProperties<'s> for DynamicBlock<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for PropertyDrawer<'s> { impl<'s> StandardProperties<'s> for FootnoteDefinition<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for NodeProperty<'s> { impl<'s> StandardProperties<'s> for Drawer<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for Table<'s> { impl<'s> StandardProperties<'s> for PropertyDrawer<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> Source<'s> for TableRow<'s> { impl<'s> StandardProperties<'s> for NodeProperty<'s> {
fn get_source(&'s self) -> &'s str { fn get_source<'b>(&'b self) -> &'s str {
self.source self.source
} }
} }
impl<'s> StandardProperties<'s> for Table<'s> {
fn get_source<'b>(&'b self) -> &'s str {
self.source
}
}
impl<'s> StandardProperties<'s> for TableRow<'s> {
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
}
}
}

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