271 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
Tom Alexander
26f41b83aa Publish version 0.1.7.
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-12 16:15:53 -04:00
Tom Alexander
e4c0e32536 Change public interface to return boxed dynamic error instead of String.
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-12 15:52:01 -04:00
Tom Alexander
37e85158ea Reduce the exposed functions when compare feature is enabled. 2023-09-12 15:48:37 -04:00
Tom Alexander
6589a755a6 Merge branch 'reduce_pub'
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-11 15:37:33 -04:00
Tom Alexander
a651b79e77 Move sexp into compare. 2023-09-11 15:37:20 -04:00
Tom Alexander
98de5e4ec5 Remove unused sexp parser entry point. 2023-09-11 15:07:52 -04:00
Tom Alexander
cf383fa394 Only include sexp module if compare feature is enabled.
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-11 14:59:24 -04:00
Tom Alexander
84953c1669 Put back in needed pubs. 2023-09-11 14:59:23 -04:00
Tom Alexander
7650a9edff Remove all pub. 2023-09-11 13:11:08 -04:00
Tom Alexander
a74319d381 Add TODO. 2023-09-11 13:09:46 -04:00
Tom Alexander
7e57285ea7 Merge branch 'additional_detects'
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-11 12:59:18 -04:00
Tom Alexander
f103d168d5 Update diary sexp parser to match org-mode's behavior. 2023-09-11 12:34:57 -04:00
Tom Alexander
f54081437a Add detect element functions for all elements that can be reasonably detected more efficiently than just parsing normally. 2023-09-11 12:28:15 -04:00
Tom Alexander
aa5988bc2f Re-enable a test.
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-11 11:18:25 -04:00
Tom Alexander
76ca2b9762 Merge branch 'description_list_colon_in_tag'
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-11 10:42:25 -04:00
Tom Alexander
1561e1e580 Update plain list item tag parser to allow double colon as long as its not the last one on that line. 2023-09-11 10:29:15 -04:00
Tom Alexander
1f11bfa2ec Join the plain list item tag end matchers again. 2023-09-11 10:13:22 -04:00
Tom Alexander
8440a3b256 Update test to show that we are not parsing description lists with double colon inside the tag correctly. 2023-09-11 10:01:50 -04:00
Tom Alexander
de7ad182b3 Make parse and compare their own binaries.
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 ensures that both code paths are capable of being parsed by rust-analyzer simultaneously and I think it will be less confusing to newcomers.
2023-09-09 04:21:34 -04:00
Tom Alexander
b75d9f5c91 Remove an outdated comment. 2023-09-09 00:00:12 -04:00
Tom Alexander
612744ebd0 Publish version 0.1.6.
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-08 23:28:59 -04:00
Tom Alexander
1b4b8b4bdb Merge branch 'howard_abrams'
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-08 23:19:19 -04:00
Tom Alexander
5587e19f16 Cleanup. 2023-09-08 23:19:08 -04:00
Tom Alexander
80f7098f9b Compare table formulas. 2023-09-08 23:05:04 -04:00
Tom Alexander
84d2babda9 Parse table formulas. 2023-09-08 22:47:07 -04:00
Tom Alexander
cc56b79683 Add a test showing we're not handling table formulas. 2023-09-08 22:42:24 -04:00
Tom Alexander
0105b49d0d Handle empty statistics cookies. 2023-09-08 22:21:49 -04:00
Tom Alexander
d79035e14d Add a test showing we are not handling empty statistics cookies. 2023-09-08 22:21:19 -04:00
Tom Alexander
7545fb7e1a Support keywords with colons in the key and without a space between the colon and value. 2023-09-08 22:17:10 -04:00
Tom Alexander
f30069efe7 Add a test showing we're not handling colons in keyword keys correctly. 2023-09-08 21:59:02 -04:00
Tom Alexander
d1fe2f6b09 Update the rest of the scripts to work with relative paths. 2023-09-08 21:50:32 -04:00
Tom Alexander
21c60d1036 Do not consume trailing whitespace on the footnote definition's final element. 2023-09-08 21:30:03 -04:00
Tom Alexander
6a1bdd5fee Support blank lines before content in footnote definitions. 2023-09-08 21:11:47 -04:00
Tom Alexander
5d20d3e99b Add a test showing we are not handling empty space for footnote definitions correctly. 2023-09-08 20:28:21 -04:00
Tom Alexander
a8fbf01124 Handle tabs for plain list descriptions.
This bug probably exists in hundreds of places across the code base. I am going to have to write a "fuzzer" that replaces random whitespace with tabs to find them all.
2023-09-08 20:02:49 -04:00
Tom Alexander
344ef04453 Add tests showing we are not handling tabs appropriately for description list tags. 2023-09-08 19:53:58 -04:00
Tom Alexander
ceb722e476 Check exit matcher after each space consumed for object trailing whitespace.
Since description list tags need to end with a space unconsumed for " ::", we need to check the exit matcher after each space consumed.
2023-09-08 19:38:49 -04:00
Tom Alexander
b04341882c Add test showing that we are not handling trailing spaces in description list tags correctly. 2023-09-08 19:24:03 -04:00
Tom Alexander
494fe5cceb Handle contentless list items mid-document. 2023-09-08 19:01:46 -04:00
Tom Alexander
0110d23387 Update empty list test to show that we're not handling trailing whitespace for empty list items properly. 2023-09-08 18:41:57 -04:00
Tom Alexander
0d7a15bfeb Handle spaces after statistics cookies. 2023-09-08 18:35:33 -04:00
Tom Alexander
352c20d1d8 Fix run_docker_compare_bisect with relative paths. 2023-09-08 18:05:10 -04:00
Tom Alexander
f82d2aada1 Fix run_docker_compare with relative paths. 2023-09-08 18:03:50 -04:00
Tom Alexander
669da4073e Accept the end condition as a parameter to the plain text parser so it can adapt to the context. 2023-09-08 17:54:49 -04:00
Tom Alexander
0056657b65 Add a test showing the plain text parser is not handling subsets of objects like inside a table cell. 2023-09-08 17:27:02 -04:00
Tom Alexander
8780976c15 Consume trailing whitespace after planning. 2023-09-08 16:30:40 -04:00
Tom Alexander
dc8b8d08ab Add test showing we break on empty sections that contain a planning. 2023-09-08 16:25:18 -04:00
Tom Alexander
93d3d9471f Compare priority, archived, and commented in headlines. 2023-09-08 16:00:16 -04:00
Tom Alexander
c7c0deed74 Parse priority cookie and COMMENT from headlines. 2023-09-08 16:00:16 -04:00
Tom Alexander
b32c21eb1d Add a test for a comment heading. 2023-09-08 16:00:16 -04:00
Tom Alexander
2e6e6fdd2b Move sections to their own source file. 2023-09-08 15:08:16 -04:00
Tom Alexander
3cc2294387 Move headlines into their own file. 2023-09-08 15:05:42 -04:00
Tom Alexander
40f22034da Make the item tag exit matcher a lower class than all all others.
This is to allow for " :: " inside a description list item's tag if it is nested inside another object.
2023-09-08 14:37:30 -04:00
Tom Alexander
ab612f293f Update org-mode version. 2023-09-08 13:11:58 -04:00
Tom Alexander
57c2922e4a Add test showing problem is description list parser. 2023-09-08 12:50:51 -04:00
Tom Alexander
c2eb1f51c8 Support blank lines between nested headlines. 2023-09-08 12:41:48 -04:00
Tom Alexander
b0930df788 Support zero skipped text in OrgSource slicing. 2023-09-07 04:16:00 -04:00
Tom Alexander
69512f559a Fix end conditions for subscript and superscript. 2023-09-07 04:16:00 -04:00
Tom Alexander
76a81b73ac Add a detect object function similar to the detect element function. 2023-09-07 04:16:00 -04:00
Tom Alexander
ba291c6776 Unify two places checking if text was preceded by whitespace. 2023-09-07 04:16:00 -04:00
Tom Alexander
6b82b46e09 Prevent nesting of text markup of the same type.
This greatly reduces the amount of detect element calls that are occurring.
2023-09-07 04:15:59 -04:00
Tom Alexander
6676012eb1 Change footnote reference class to Gamma. 2023-09-07 04:15:59 -04:00
Tom Alexander
facbe716e9 Cleanup 2023-09-07 01:23:26 -04:00
Tom Alexander
827f3e1c98 Add the rest of the relevant howard abrams repos. 2023-09-06 21:37:09 -04:00
Tom Alexander
fcea7e5a4b Add howard abrams demo-it and the upstreeam doomemacs repo to compare. 2023-09-06 21:11:46 -04:00
Tom Alexander
dda2b1e69f Compare howard abrams hamacs. 2023-09-06 20:56:36 -04:00
Tom Alexander
f79d07a7c8 Compare howard abrams dotfiles. 2023-09-06 19:49:04 -04:00
Tom Alexander
45283b48d9 Update performance scripts to support taking input file parameters.
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-06 19:31:09 -04:00
Tom Alexander
08e4c646e5 Merge branch 'full_foreign_compare'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
rust-build Build rust-build has succeeded
2023-09-06 19:17:35 -04:00
Tom Alexander
f8b99ed235 Support counter set in plain list items. 2023-09-06 19:09:20 -04:00
Tom Alexander
6fc607cfe0 Compare node properties. 2023-09-06 18:54:47 -04:00
Tom Alexander
49afcf0db6 Support property nodes with colons in their key. 2023-09-06 18:54:01 -04:00
Tom Alexander
c4d7e646fc Support spaces after the end of a lesser block. 2023-09-06 18:54:01 -04:00
Tom Alexander
3fc3a5d1ef Add support for empty lesser blocks. 2023-09-06 18:11:57 -04:00
Tom Alexander
6e2fc362ea Add support for babel-call keywords. 2023-09-06 18:04:53 -04:00
Tom Alexander
90fa48661c Prefer the longer version of affiliated keywords. 2023-09-06 17:41:00 -04:00
Tom Alexander
5cefcd5fac Add in the emacs repository org-mode documents. 2023-09-06 17:06:17 -04:00
Tom Alexander
b83a103c17 Update the foreign document test to use all org-mode documents from the org-mode repository. 2023-09-06 17:02:56 -04:00
155 changed files with 7353 additions and 2838 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "organic"
version = "0.1.5"
version = "0.1.9"
authors = ["Tom Alexander <tom@fizz.buzz>"]
description = "An org-mode parser."
edition = "2021"
@@ -21,9 +21,15 @@ name = "organic"
path = "src/lib.rs"
[[bin]]
# This bin exists for development purposes only. The real target of this crate is the library.
name = "parse"
path = "src/main.rs"
# This bin exists for development purposes only. The real target of this crate is the library.
name = "parse"
path = "src/main.rs"
[[bin]]
# This bin exists for development purposes only. The real target of this crate is the library.
name = "compare"
path = "src/bin_compare.rs"
required-features = ["compare"]
[dependencies]
nom = "7.1.1"

View File

@@ -33,6 +33,10 @@ release:
clean:
> cargo clean
.PHONY: format
format:
> $(MAKE) -C docker/cargo_fmt run
.PHONY: test
test:
> 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.
## 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
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 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")
.into_iter()
@@ -51,32 +52,15 @@ fn write_test(test_file: &mut File, test: &walkdir::DirEntry) {
.strip_suffix(".org")
.expect("Should have .org extension")
.replace("/", "_");
let test_name = format!("autogen_{}", test_name);
if let Some(_reason) = is_expect_fail(test_name.as_str()) {
write!(test_file, "#[ignore]\n").unwrap();
}
write!(
test_file,
include_str!("./tests/test_template"),
name = test_name,
path = test.path().display()
)
.unwrap();
}
#[cfg(feature = "compare")]
fn write_header(test_file: &mut File) {
write!(
test_file,
r#"
#[feature(exit_status_error)]
use organic::compare_document;
use organic::parser::parse;
use organic::emacs_parse_anonymous_org_document;
use organic::parser::sexp::sexp_with_padding;
"#
path = test.path().display(),
expect_fail = is_expect_fail(test_name.as_str())
.map(|_| "#[ignore]\n")
.unwrap_or("")
)
.unwrap();
}
@@ -84,9 +68,8 @@ use organic::parser::sexp::sexp_with_padding;
#[cfg(feature = "compare")]
fn is_expect_fail(name: &str) -> Option<&str> {
match name {
"autogen_greater_element_drawer_drawer_with_headline_inside" => Some("Apparently lines with :end: become their own paragraph. This odd behavior needs to be investigated more."),
"autogen_element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."),
"autogen_lesser_element_paragraphs_paragraph_with_backslash_line_breaks" => Some("The text we're getting out of the parse tree is already processed to remove line breaks, so our comparison needs to take that into account."),
"greater_element_drawer_drawer_with_headline_inside" => Some("Apparently lines with :end: become their own paragraph. This odd behavior needs to be investigated more."),
"element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."),
_ => None,
}
}

View File

@@ -1,5 +1,5 @@
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
@@ -8,13 +8,13 @@ RUN git clone --depth 1 --branch $EMACS_VERSION https://git.savannah.gnu.org/git
WORKDIR /root/emacs
RUN mkdir /root/dist
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 DESTDIR="/root/dist" install
FROM build AS build-org-mode
ARG ORG_VERSION=7bdec435ff5d86220d13c431e799c5ed44a57da1
ARG ORG_VERSION=c703541ffcc14965e3567f928de1683a1c1e33f6
COPY --from=build-emacs /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.
@@ -27,7 +27,7 @@ RUN make DESTDIR="/root/dist" install
FROM rustlang/rust:nightly-alpine3.17 AS tester
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
COPY --from=build-emacs /root/dist/ /
COPY --from=build-org-mode /root/dist/ /
@@ -36,13 +36,72 @@ ENTRYPOINT ["cargo", "test"]
FROM build as foreign-document-gather
ARG HOWARD_ABRAMS_DOT_FILES_VERSION=1b54fe75d74670dc7bcbb6b01ea560c45528c628
ARG HOWARD_ABRAMS_DOT_FILES_PATH=/foreign_documents/howardabrams/dot-files
ARG HOWARD_ABRAMS_DOT_FILES_REPO=https://github.com/howardabrams/dot-files.git
RUN mkdir /foreign_documents
RUN mkdir -p $HOWARD_ABRAMS_DOT_FILES_PATH && git -C $HOWARD_ABRAMS_DOT_FILES_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_DOT_FILES_PATH remote add origin $HOWARD_ABRAMS_DOT_FILES_REPO && git -C $HOWARD_ABRAMS_DOT_FILES_PATH fetch origin $HOWARD_ABRAMS_DOT_FILES_VERSION && git -C $HOWARD_ABRAMS_DOT_FILES_PATH checkout FETCH_HEAD
ARG HOWARD_ABRAMS_HAMACS_VERSION=da51188cc195d41882175d412fe40a8bc5730c5c
ARG HOWARD_ABRAMS_HAMACS_PATH=/foreign_documents/howardabrams/hamacs
ARG HOWARD_ABRAMS_HAMACS_REPO=https://github.com/howardabrams/hamacs.git
RUN mkdir -p $HOWARD_ABRAMS_HAMACS_PATH && git -C $HOWARD_ABRAMS_HAMACS_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_HAMACS_PATH remote add origin $HOWARD_ABRAMS_HAMACS_REPO && git -C $HOWARD_ABRAMS_HAMACS_PATH fetch origin $HOWARD_ABRAMS_HAMACS_VERSION && git -C $HOWARD_ABRAMS_HAMACS_PATH checkout FETCH_HEAD
ARG HOWARD_ABRAMS_DEMO_IT_VERSION=e399fd7ceb73caeae7cb50b247359bafcaee2a3f
ARG HOWARD_ABRAMS_DEMO_IT_PATH=/foreign_documents/howardabrams/demo-it
ARG HOWARD_ABRAMS_DEMO_IT_REPO=https://github.com/howardabrams/demo-it.git
RUN mkdir -p $HOWARD_ABRAMS_DEMO_IT_PATH && git -C $HOWARD_ABRAMS_DEMO_IT_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_DEMO_IT_PATH remote add origin $HOWARD_ABRAMS_DEMO_IT_REPO && git -C $HOWARD_ABRAMS_DEMO_IT_PATH fetch origin $HOWARD_ABRAMS_DEMO_IT_VERSION && git -C $HOWARD_ABRAMS_DEMO_IT_PATH checkout FETCH_HEAD
ARG HOWARD_ABRAMS_MAGIT_DEMO_VERSION=59e82f6bc7c18f550478d86a8f680c3f2da66985
ARG HOWARD_ABRAMS_MAGIT_DEMO_PATH=/foreign_documents/howardabrams/magit-demo
ARG HOWARD_ABRAMS_MAGIT_DEMO_REPO=https://github.com/howardabrams/magit-demo.git
RUN mkdir -p $HOWARD_ABRAMS_MAGIT_DEMO_PATH && git -C $HOWARD_ABRAMS_MAGIT_DEMO_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_MAGIT_DEMO_PATH remote add origin $HOWARD_ABRAMS_MAGIT_DEMO_REPO && git -C $HOWARD_ABRAMS_MAGIT_DEMO_PATH fetch origin $HOWARD_ABRAMS_MAGIT_DEMO_VERSION && git -C $HOWARD_ABRAMS_MAGIT_DEMO_PATH checkout FETCH_HEAD
ARG HOWARD_ABRAMS_PDX_EMACS_HACKERS_VERSION=bfb7bd640fdf0ce3def21f9fc591ed35d776b26d
ARG HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH=/foreign_documents/howardabrams/pdx-emacs-hackers
ARG HOWARD_ABRAMS_PDX_EMACS_HACKERS_REPO=https://github.com/howardabrams/pdx-emacs-hackers.git
RUN mkdir -p $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH && git -C $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH remote add origin $HOWARD_ABRAMS_PDX_EMACS_HACKERS_REPO && git -C $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH fetch origin $HOWARD_ABRAMS_PDX_EMACS_HACKERS_VERSION && git -C $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH checkout FETCH_HEAD
ARG HOWARD_ABRAMS_FLORA_SIMULATOR_VERSION=50de13068722b9e3878f8598b749b7ccd14e7f8e
ARG HOWARD_ABRAMS_FLORA_SIMULATOR_PATH=/foreign_documents/howardabrams/flora-simulator
ARG HOWARD_ABRAMS_FLORA_SIMULATOR_REPO=https://github.com/howardabrams/flora-simulator.git
RUN mkdir -p $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH && git -C $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH remote add origin $HOWARD_ABRAMS_FLORA_SIMULATOR_REPO && git -C $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH fetch origin $HOWARD_ABRAMS_FLORA_SIMULATOR_VERSION && git -C $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH checkout FETCH_HEAD
ARG HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_VERSION=2d7a5e41001a1adf7ec24aeb6acc8525a72d7892
ARG HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH=/foreign_documents/howardabrams/literate-devops-demo
ARG HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_REPO=https://github.com/howardabrams/literate-devops-demo.git
RUN mkdir -p $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH && git -C $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH remote add origin $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_REPO && git -C $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH fetch origin $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_VERSION && git -C $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH checkout FETCH_HEAD
ARG HOWARD_ABRAMS_CLOJURE_YESQL_XP_VERSION=b651c7f8b47b2710e99fce9652980902bbc1c6c9
ARG HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH=/foreign_documents/howardabrams/clojure-yesql-xp
ARG HOWARD_ABRAMS_CLOJURE_YESQL_XP_REPO=https://github.com/howardabrams/clojure-yesql-xp.git
RUN mkdir -p $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH && git -C $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH remote add origin $HOWARD_ABRAMS_CLOJURE_YESQL_XP_REPO && git -C $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH fetch origin $HOWARD_ABRAMS_CLOJURE_YESQL_XP_VERSION && git -C $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH checkout FETCH_HEAD
ARG HOWARD_ABRAMS_VEEP_VERSION=e37fcf63a5c4a526255735ee34955528b3b280ae
ARG HOWARD_ABRAMS_VEEP_PATH=/foreign_documents/howardabrams/veep
ARG HOWARD_ABRAMS_VEEP_REPO=https://github.com/howardabrams/veep.git
RUN mkdir -p $HOWARD_ABRAMS_VEEP_PATH && git -C $HOWARD_ABRAMS_VEEP_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_VEEP_PATH remote add origin $HOWARD_ABRAMS_VEEP_REPO && git -C $HOWARD_ABRAMS_VEEP_PATH fetch origin $HOWARD_ABRAMS_VEEP_VERSION && git -C $HOWARD_ABRAMS_VEEP_PATH checkout FETCH_HEAD
ARG DOOMEMACS_VERSION=42d5fd83504f8aa80f3248036006fbcd49222943
ARG DOOMEMACS_PATH=/foreign_documents/doomemacs
ARG DOOMEMACS_REPO=https://github.com/doomemacs/doomemacs.git
RUN mkdir -p $DOOMEMACS_PATH && git -C $DOOMEMACS_PATH init --initial-branch=main && git -C $DOOMEMACS_PATH remote add origin $DOOMEMACS_REPO && git -C $DOOMEMACS_PATH fetch origin $DOOMEMACS_VERSION && git -C $DOOMEMACS_PATH checkout FETCH_HEAD
ARG WORG_VERSION=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
RUN apk add --no-cache bash
RUN apk add --no-cache bash coreutils
RUN mkdir /foreign_documents
COPY --from=build-org-mode /root/org-mode/doc /foreign_documents/org-mode
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/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
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -5,6 +5,8 @@ set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
REALPATH=$(command -v uu-realpath || command -v realpath)
function log {
(>&2 echo "${@}")
}
@@ -21,25 +23,29 @@ function main {
if [ "${CARGO_TARGET_DIR:-}" = "" ]; then
CARGO_TARGET_DIR=$(realpath target/)
fi
PARSE="${CARGO_TARGET_DIR}/release-lto/parse"
PARSE="${CARGO_TARGET_DIR}/release-lto/compare"
run_compare "org-mode/org-guide.org" "/foreign_documents/org-mode/org-guide.org"
run_compare "org-mode/org-manual.org" "/foreign_documents/org-mode/org-manual.org"
}
function run_compare {
local name="$1"
local target_document="$2"
local all_status=0
set +e
$PARSE "$target_document" &> /dev/null
local status=$?
(run_compare_function "org-mode" compare_all_org_document "/foreign_documents/org-mode")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "emacs" compare_all_org_document "/foreign_documents/emacs")
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)
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "doomemacs" compare_all_org_document "/foreign_documents/doomemacs")
if [ "$?" -ne 0 ]; then all_status=1; fi
set -e
if [ "$status" -eq 0 ]; then
echo "$(green_text "GOOD") $name"
if [ "$all_status" -ne 0 ]; then
red_text "Some tests failed."
else
echo "$(red_text "FAIL") $name"
return 1
green_text "All tests passed."
fi
return "$all_status"
}
function green_text {
@@ -54,4 +60,90 @@ function yellow_text {
(IFS=' '; printf '\x1b[38;2;255;255;0m%s\x1b[0m' "${*}")
}
function indent {
local depth="$1"
local scaled_depth=$((depth * 2))
shift 1
local prefix
prefix=$(printf -- "%${scaled_depth}s")
while read -r l; do
(IFS=' '; printf -- '%s%s\n' "$prefix" "$l")
done
}
function run_compare_function {
local name="$1"
local stdoutput
shift 1
set +e
stdoutput=$("${@}")
local status=$?
set -e
if [ "$status" -eq 0 ]; then
echo "$(green_text "GOOD") $name"
indent 1 <<<"$stdoutput"
else
echo "$(red_text "FAIL") $name"
indent 1 <<<"$stdoutput"
return 1
fi
}
function compare_all_org_document {
local root_dir="$1"
local target_document
local all_status=0
while read target_document; do
local relative_path
relative_path=$($REALPATH --relative-to "$root_dir" "$target_document")
set +e
(run_compare "$relative_path" "$target_document")
if [ "$?" -ne 0 ]; then all_status=1; fi
set -e
done<<<"$(find "$root_dir" -type f -iname '*.org' | sort)"
return "$all_status"
}
function run_compare {
local name="$1"
local target_document="$2"
set +e
($PARSE "$target_document" &> /dev/null)
local status=$?
set -e
if [ "$status" -eq 0 ]; then
echo "$(green_text "GOOD") $name"
else
echo "$(red_text "FAIL") $name"
return 1
fi
}
function compare_howard_abrams {
local all_status=0
set +e
(run_compare_function "dot-files" compare_all_org_document "/foreign_documents/howardabrams/dot-files")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "hamacs" compare_all_org_document "/foreign_documents/howardabrams/hamacs")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "demo-it" compare_all_org_document "/foreign_documents/howardabrams/demo-it")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "magit-demo" compare_all_org_document "/foreign_documents/howardabrams/magit-demo")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "pdx-emacs-hackers" compare_all_org_document "/foreign_documents/howardabrams/pdx-emacs-hackers")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "flora-simulator" compare_all_org_document "/foreign_documents/howardabrams/flora-simulator")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "literate-devops-demo" compare_all_org_document "/foreign_documents/howardabrams/literate-devops-demo")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "clojure-yesql-xp" compare_all_org_document "/foreign_documents/howardabrams/clojure-yesql-xp")
if [ "$?" -ne 0 ]; then all_status=1; fi
(run_compare_function "veep" compare_all_org_document "/foreign_documents/howardabrams/veep")
if [ "$?" -ne 0 ]; then all_status=1; fi
set -e
return "$all_status"
}
main "${@}"

View File

@@ -25,3 +25,4 @@ This could significantly reduce our calls to exit matchers.
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.
** 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,8 @@
* Footnotes
[fn:1]
#+BEGIN_EXAMPLE
baz
#+END_EXAMPLE

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,2 @@
3. [@3] foo
4. bar

View File

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

View File

@@ -1 +1,2 @@
- {{{foo(bar)}}} :: baz
- =foo= :: bar

View File

@@ -0,0 +1,3 @@
- foo :: bar
- foo :: bar
- foo :: bar

View File

@@ -0,0 +1,2 @@
- =foo :: bar= :: baz
- lorem :: ipsum :: dolar

View File

@@ -1,3 +1,5 @@
1.
2.
3.
* headline

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 @@
* Overwrite
:PROPERTIES:
:header-args: :var foo="lorem"
:header-args:emacs-lisp: :var bar="ipsum"
:header-args:emacs-lisp+: :results silent :var baz=7
:END:

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,6 @@
src_elisp{(bar)}
*src_elisp{(bar)}*
| foo *bar* |
| foo src_elisp{(bar)} |
| foo *src_elisp{(bar)}* |

View File

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

View File

@@ -0,0 +1,8 @@
| Name | Price | Quantity | Total |
|------+-------+----------+-------|
| foo | 7 | 4 | 28 |
| bar | 3.5 | 3 | 10.5 |
|------+-------+----------+-------|
| | | 7 | 38.5 |
#+tblfm: $4=$2*$3::@>$4=vsum(@2..@-1)
#+tblfm: @>$3=vsum(@2..@-1)

View File

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

View File

@@ -0,0 +1,6 @@
%%(foo
)
%%(bar ; baz
lorem

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,7 @@
# This test is to prove that the parser works with affiliated keywords that have both a shorter and longer version.
#+results:
#+result:
#+begin_latex
\foo
#+end_latex

View File

@@ -0,0 +1 @@
#+call: foo(bar="baz")

View File

@@ -0,0 +1 @@
#+title:foo:bar: baz: lorem: ipsum

View File

@@ -0,0 +1,2 @@
#+begin_src
#+end_src

View File

@@ -0,0 +1,4 @@
# There are trailing spaces after the begin and end src lines
#+begin_src
echo "this is a source block."
#+end_src

View File

@@ -0,0 +1,3 @@
*[fn:: /abcdef[fn::ghijklmnopqrstuvw]xyz/ r]*
*[fn:: /abcdef[fn::ghijk *lmnopq* rstuvw]xyz/ r]*

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,4 @@
[/]
[/2]
[3/]
[%]

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)>
# active
<1970-01-01 Thu 8:15rest +1w -1d>
# Any value for "REST" in the first timestamp makes this a regular timestamp rather than a time range.
<1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d>
# inactive
[1970-01-01 Thu 8:15rest +1w -1d]
# Any value for "REST" in the first timestamp makes this a regular timestamp rather than a time range.
[1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d]
# active date range
<1970-01-01 Thu 8:15rest +1w -1d>--<1970-01-01 Thu 8:15rest +1w -1d>
# active time range
<1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d>
<1970-01-01 Thu 8:15-13:15otherrest +1w -1d>
# inactive date range
[1970-01-01 Thu 8:15rest +1w -1d]--[1970-01-01 Thu 8:15rest +1w -1d]
# inactive time range
[1970-01-01 Thu 8:15rest-13:15otherrest +1w -1d]
[1970-01-01 Thu 8:15-13:15otherrest +1w -1d]

View File

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

View File

@@ -0,0 +1,2 @@
* TODO [#A] COMMENT foo bar
baz

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

@@ -0,0 +1,4 @@
* DONE foo
DEADLINE: <2023-09-08 Fri>
* DONE bar

View File

@@ -0,0 +1 @@
* [0/4] foo

View File

@@ -4,10 +4,23 @@ set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$DIR/../"
: ${PROFILE:="perf"}
RUSTFLAGS="-C opt-level=0" cargo build --no-default-features
valgrind --tool=callgrind --callgrind-out-file=callgrind.out target/debug/parse
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
echo "You probably want to run:"
echo "callgrind_annotate --auto=yes callgrind.out"
(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/${PROFILE}/parse" "${@}"
echo "You probably want to run:"
echo "callgrind_annotate --auto=yes '$DIR/../callgrind.out'"
}
main "${@}"

View File

@@ -6,8 +6,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
: ${PROFILE:="perf"}
cd "$DIR/../"
function main {
local additional_flags=()
if [ "$PROFILE" = "dev" ] || [ "$PROFILE" = "debug" ]; then
@@ -15,12 +13,12 @@ function main {
else
additional_flags+=(--profile "$PROFILE")
fi
cargo build --no-default-features "${additional_flags[@]}"
perf record --freq=2000 --call-graph dwarf --output=perf.data target/${PROFILE}/parse
(cd "$DIR/../" && cargo build --no-default-features "${additional_flags[@]}")
perf record --freq=2000 --call-graph dwarf --output="$DIR/../perf.data" "$DIR/../target/${PROFILE}/parse" "${@}"
# Convert to a format firefox will read
# flags to consider --show-info
perf script -F +pid --input perf.data > perf.firefox
perf script -F +pid --input "$DIR/../perf.data" > "$DIR/../perf.firefox"
echo "You probably want to go to https://profiler.firefox.com/"
echo "Either that or run hotspot"

View File

@@ -8,11 +8,26 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
: ${TRACE:="NO"} # or YES to send traces to jaeger
: ${BACKTRACE:="NO"} # or YES to print a rust backtrace when panicking
: ${NO_COLOR:=""} # Set to anything to disable color output
: ${PROFILE:="debug"}
cd "$DIR/../"
REALPATH=$(command -v uu-realpath || command -v realpath)
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 {
build_container
launch_container "${@}"
@@ -24,7 +39,6 @@ function build_container {
function launch_container {
local additional_flags=()
local additional_args=()
local features=(compare)
if [ "$NO_COLOR" != "" ]; then
@@ -38,11 +52,8 @@ function launch_container {
fi
if [ "$SHELL" != "YES" ]; then
local features_joined=$(IFS=","; echo "${features[*]}")
additional_args+=(cargo run --no-default-features --features "$features_joined")
additional_flags+=(--read-only)
else
additional_args+=(/bin/sh)
additional_flags+=(-t)
fi
@@ -50,16 +61,50 @@ function launch_container {
additional_flags+=(--env RUST_BACKTRACE=full)
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 we passed in args, we need to forward them along
for path in "${@}"; do
local full_path=$($REALPATH "$path")
local containing_folder=$(dirname "$full_path")
local file_name=$(basename "$full_path")
docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "${containing_folder}:/input:ro" -v "$($REALPATH ./):/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"
local full_path
full_path=$($REALPATH "$path")
init_script=$(cat <<EOF
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
else
docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "$($REALPATH ./):/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
}

View File

@@ -5,22 +5,10 @@ set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$DIR/../"
REALPATH=$(command -v uu-realpath || command -v realpath)
############## 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 {
local status_code="$1"
shift
@@ -35,18 +23,18 @@ function log {
############## Program #########################
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=$($REALPATH "$1")
local target_full_path
target_full_path=$($REALPATH "$1")
SOURCE_FOLDER=$(dirname "$target_full_path")
TARGET_DOCUMENT=$(basename "$target_full_path")
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
run_parse "$bad" &> /dev/null
(run_parse "$bad")
local status=$?
set -e
if [ $status -eq 0 ]; then
@@ -72,21 +60,12 @@ function main {
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 {
local lines="$1"
local temp_dir=$(setup_temp_dir)
folders+=("$temp_dir")
cat "$SOURCE_FOLDER/$TARGET_DOCUMENT" | head -n "$lines" > "$temp_dir/$TARGET_DOCUMENT"
"${DIR}/run_docker_compare.bash" "$temp_dir/$TARGET_DOCUMENT"
cd "$SOURCE_FOLDER"
head -n "$lines" "$SOURCE_FOLDER/$TARGET_DOCUMENT" | PROFILE=release-lto "${DIR}/run_docker_compare.bash"
local status=$?
rm -rf "$temp_dir"
# TODO: Remove temp_dir from folders
return "$status"
}

View File

@@ -6,7 +6,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
: ${NO_COLOR:=""} # Set to anything to disable color output
cd "$DIR/../"
REALPATH=$(command -v uu-realpath || command -v realpath)
MAKE=$(command -v gmake || command -v make)
@@ -56,7 +55,7 @@ cargo test --no-default-features --features compare --no-fail-fast --lib --test
EOF
)
docker run "${additional_flags[@]}" --init --rm --read-only --mount type=tmpfs,destination=/tmp -v "$($REALPATH ./):/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"
docker run "${additional_flags[@]}" --init --rm --read-only --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 sh -c "$init_script"
}

View File

@@ -4,7 +4,6 @@ set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$DIR/../"
REALPATH=$(command -v uu-realpath || command -v realpath)
function main {
@@ -12,7 +11,7 @@ function main {
local test
while read test; do
cargo test --no-default-features --features compare --no-fail-fast --test test_loader "$test" -- --show-output
(cd "$DIR/../" && cargo test --no-default-features --features compare --no-fail-fast --test test_loader "$test" -- --show-output)
done<<<"$test_names"
}

View File

@@ -7,8 +7,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
: ${PROFILE:="release-lto"}
cd "$DIR/../"
function main {
local additional_flags=()
if [ "$PROFILE" = "dev" ] || [ "$PROFILE" = "debug" ]; then
@@ -16,8 +14,8 @@ function main {
else
additional_flags+=(--profile "$PROFILE")
fi
cargo build --no-default-features "${additional_flags[@]}"
time ./target/${PROFILE}/parse "${@}"
(cd "$DIR/../" && cargo build --no-default-features "${additional_flags[@]}")
time "$DIR/../target/${PROFILE}/parse" "${@}"
}
main "${@}"

52
src/bin_compare.rs Normal file
View File

@@ -0,0 +1,52 @@
#![feature(round_char_boundary)]
#![feature(exact_size_is_empty)]
use std::io::Read;
use organic::compare::run_anonymous_compare;
use organic::compare::run_compare_on_file;
#[cfg(feature = "tracing")]
use crate::init_tracing::init_telemetry;
#[cfg(feature = "tracing")]
use crate::init_tracing::shutdown_telemetry;
#[cfg(feature = "tracing")]
mod init_tracing;
#[cfg(not(feature = "tracing"))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
main_body()
}
#[cfg(feature = "tracing")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
let result = rt.block_on(async {
init_telemetry()?;
let main_body_result = main_body();
shutdown_telemetry()?;
main_body_result
});
result
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn main_body() -> Result<(), Box<dyn std::error::Error>> {
let args = std::env::args().skip(1);
if args.is_empty() {
let org_contents = read_stdin_to_string()?;
run_anonymous_compare(org_contents)
} else {
for arg in args {
run_compare_on_file(arg)?
}
Ok(())
}
}
fn read_stdin_to_string() -> Result<String, Box<dyn std::error::Error>> {
let mut stdin_contents = String::new();
std::io::stdin()
.lock()
.read_to_string(&mut stdin_contents)?;
Ok(stdin_contents)
}

95
src/compare/compare.rs Normal file
View File

@@ -0,0 +1,95 @@
use std::path::Path;
use crate::compare::diff::compare_document;
use crate::compare::parse::emacs_parse_anonymous_org_document;
use crate::compare::parse::emacs_parse_file_org_document;
use crate::compare::parse::get_emacs_version;
use crate::compare::parse::get_org_mode_version;
use crate::compare::sexp::sexp;
use crate::context::GlobalSettings;
use crate::context::LocalFileAccessInterface;
use crate::parser::parse_file_with_settings;
use crate::parser::parse_with_settings;
pub fn run_anonymous_compare<P: AsRef<str>>(
org_contents: P,
) -> Result<(), Box<dyn std::error::Error>> {
run_anonymous_compare_with_settings(org_contents, &GlobalSettings::default())
}
pub fn run_compare_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::error::Error>> {
run_compare_on_file_with_settings(org_path, &GlobalSettings::default())
}
pub fn run_anonymous_compare_with_settings<P: AsRef<str>>(
org_contents: P,
global_settings: &GlobalSettings,
) -> Result<(), Box<dyn std::error::Error>> {
// TODO: This is a work-around to pretend that dos line endings do not exist. It would be better to handle the difference in line endings.
let org_contents = org_contents.as_ref().replace("\r\n", "\n");
let org_contents = org_contents.as_str();
print_versions()?;
let rust_parsed = parse_with_settings(org_contents, global_settings)?;
let org_sexp = emacs_parse_anonymous_org_document(org_contents, global_settings)?;
let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?;
println!("{}\n\n\n", org_contents);
println!("{}", org_sexp);
println!("{:#?}", rust_parsed);
// We do the diffing after printing out both parsed forms in case the diffing panics
let diff_result = compare_document(&parsed_sexp, &rust_parsed)?;
diff_result.print(org_contents)?;
if diff_result.is_bad() {
Err("Diff results do not match.")?;
}
Ok(())
}
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();
print_versions()?;
let parent_directory = org_path
.parent()
.ok_or("Should be contained inside a directory.")?;
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 file_access_interface = LocalFileAccessInterface {
working_directory: Some(parent_directory.to_path_buf()),
};
let global_settings = {
let mut global_settings = global_settings.clone();
global_settings.file_access = &file_access_interface;
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, &global_settings)?;
let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?;
println!("{}\n\n\n", org_contents);
println!("{}", org_sexp);
println!("{:#?}", rust_parsed);
// We do the diffing after printing out both parsed forms in case the diffing panics
let diff_result = compare_document(&parsed_sexp, &rust_parsed)?;
diff_result.print(org_contents)?;
if diff_result.is_bad() {
Err("Diff results do not match.")?;
}
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,8 +1,10 @@
mod compare;
mod diff;
mod elisp_fact;
mod parse;
mod sexp;
mod util;
pub use diff::compare_document;
pub use parse::emacs_parse_anonymous_org_document;
pub use parse::emacs_parse_file_org_document;
pub use parse::get_emacs_version;
pub use parse::get_org_mode_version;
pub use compare::run_anonymous_compare;
pub use compare::run_anonymous_compare_with_settings;
pub use compare::run_compare_on_file;
pub use compare::run_compare_on_file_with_settings;

View File

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

View File

@@ -1,9 +1,10 @@
use std::collections::HashMap;
use nom::branch::alt;
use nom::bytes::complete::escaped;
use nom::bytes::complete::tag;
use nom::bytes::complete::take_till1;
use nom::character::complete::anychar;
use nom::character::complete::digit1;
use nom::character::complete::multispace0;
use nom::character::complete::multispace1;
use nom::character::complete::one_of;
@@ -11,16 +12,16 @@ use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::multi::separated_list1;
use nom::sequence::delimited;
use nom::sequence::preceded;
use nom::sequence::tuple;
use super::org_source::convert_error;
use super::org_source::OrgSource;
use super::util::get_consumed;
use crate::error::Res;
const MAX_OCTAL_LENGTH: usize = 3;
#[derive(Debug)]
pub enum Token<'s> {
Atom(&'s str),
@@ -31,45 +32,50 @@ pub enum Token<'s> {
#[derive(Debug)]
pub struct TextWithProperties<'s> {
pub text: &'s str,
pub properties: Vec<Token<'s>>,
pub(crate) text: &'s str,
pub(crate) properties: Vec<Token<'s>>,
}
enum ParseState {
Normal,
Escape,
Octal(Vec<u8>),
}
impl<'s> Token<'s> {
pub fn as_vector<'p>(&'p self) -> Result<&'p Vec<Token<'s>>, Box<dyn std::error::Error>> {
pub(crate) fn as_vector<'p>(
&'p self,
) -> Result<&'p Vec<Token<'s>>, Box<dyn std::error::Error>> {
Ok(match self {
Token::Vector(children) => Ok(children),
_ => Err(format!("wrong token type, expected vector: {:?}", self)),
}?)
}
pub fn as_list<'p>(&'p self) -> Result<&'p Vec<Token<'s>>, Box<dyn std::error::Error>> {
pub(crate) fn as_list<'p>(&'p self) -> Result<&'p Vec<Token<'s>>, Box<dyn std::error::Error>> {
Ok(match self {
Token::List(children) => Ok(children),
_ => Err(format!("wrong token type, expected list: {:?}", self)),
}?)
}
pub fn as_atom<'p>(&'p self) -> Result<&'s str, Box<dyn std::error::Error>> {
pub(crate) fn as_atom<'p>(&'p self) -> Result<&'s str, Box<dyn std::error::Error>> {
Ok(match self {
Token::Atom(body) => Ok(*body),
_ => Err(format!("wrong token type, expected atom: {:?}", self)),
}?)
}
pub fn as_text<'p>(&'p self) -> Result<&'p TextWithProperties<'s>, Box<dyn std::error::Error>> {
pub(crate) fn as_text<'p>(
&'p self,
) -> Result<&'p TextWithProperties<'s>, Box<dyn std::error::Error>> {
Ok(match self {
Token::TextWithProperties(body) => Ok(body),
_ => Err(format!("wrong token type, expected text: {:?}", self)),
}?)
}
pub fn as_map<'p>(
pub(crate) fn as_map<'p>(
&'p self,
) -> Result<HashMap<&'s str, &'p Token<'s>>, Box<dyn std::error::Error>> {
let mut hashmap = HashMap::new();
@@ -95,8 +101,27 @@ impl<'s> Token<'s> {
}
}
pub fn unquote(text: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut out = String::with_capacity(text.len());
/// Check if the child string slice is a slice of the parent string slice.
fn is_slice_of(parent: &str, child: &str) -> bool {
let parent_start = parent.as_ptr() as usize;
let parent_end = parent_start + parent.len();
let child_start = child.as_ptr() as usize;
let child_end = child_start + child.len();
child_start >= parent_start && child_end <= parent_end
}
/// 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.
fn get_consumed<'s>(input: &'s str, remaining: &'s str) -> &'s str {
debug_assert!(is_slice_of(input, remaining));
let source = {
let offset = remaining.as_ptr() as usize - input.as_ptr() as usize;
&input[..offset]
};
source.into()
}
pub(crate) fn unquote(text: &str) -> Result<String, Box<dyn std::error::Error>> {
let mut out: Vec<u8> = Vec::with_capacity(text.len());
if !text.starts_with(r#"""#) {
return Err("Quoted text does not start with quote.".into());
}
@@ -105,56 +130,70 @@ pub fn unquote(text: &str) -> Result<String, Box<dyn std::error::Error>> {
}
let interior_text = &text[1..(text.len() - 1)];
let mut state = ParseState::Normal;
for current_char in interior_text.chars().into_iter() {
for current_char in interior_text.bytes().into_iter() {
// Check to see if octal finished
state = match (state, current_char) {
(ParseState::Normal, '\\') => ParseState::Escape,
(ParseState::Octal(octal), b'0'..=b'7') if octal.len() < MAX_OCTAL_LENGTH => {
ParseState::Octal(octal)
}
(ParseState::Octal(octal), _) => {
let octal_number_string = String::from_utf8(octal)?;
let decoded_byte = u8::from_str_radix(&octal_number_string, 8)?;
out.push(decoded_byte);
ParseState::Normal
}
(state, _) => state,
};
state = match (state, current_char) {
(ParseState::Normal, b'\\') => ParseState::Escape,
(ParseState::Normal, _) => {
out.push(current_char);
ParseState::Normal
}
(ParseState::Escape, 'n') => {
out.push('\n');
(ParseState::Escape, b'n') => {
out.push(b'\n');
ParseState::Normal
}
(ParseState::Escape, '\\') => {
out.push('\\');
(ParseState::Escape, b'\\') => {
out.push(b'\\');
ParseState::Normal
}
(ParseState::Escape, '"') => {
out.push('"');
(ParseState::Escape, b'"') => {
out.push(b'"');
ParseState::Normal
}
_ => todo!(),
(ParseState::Escape, b'0'..=b'7') => {
let mut octal = Vec::with_capacity(MAX_OCTAL_LENGTH);
octal.push(current_char);
ParseState::Octal(octal)
}
(ParseState::Octal(mut octal), b'0'..=b'7') => {
octal.push(current_char);
ParseState::Octal(octal)
}
_ => panic!("Invalid state unquoting string."),
};
}
Ok(out)
Ok(String::from_utf8(out)?)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn sexp_with_padding<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
pub fn sexp<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (remaining, _) = multispace0(input)?;
let remaining = OrgSource::new(remaining);
let (remaining, tkn) = token(remaining)
.map(|(rem, out)| (Into::<&str>::into(rem), out))
.map_err(convert_error)?;
let (remaining, tkn) = token(remaining).map(|(rem, out)| (Into::<&str>::into(rem), out))?;
let (remaining, _) = multispace0(remaining)?;
Ok((remaining, tkn))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn sexp<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
let (remaining, tkn) = token(input)?;
Ok((remaining, tkn))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn token<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
fn token<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
alt((list, vector, atom))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn list<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
fn list<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (remaining, _) = tag("(")(input)?;
let (remaining, children) = delimited(
multispace0,
@@ -166,7 +205,7 @@ fn list<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn vector<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
fn vector<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (remaining, _) = tag("[")(input)?;
let (remaining, children) = delimited(
multispace0,
@@ -178,7 +217,7 @@ fn vector<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn atom<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
fn atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
not(peek(one_of(")]")))(input)?;
alt((
text_with_properties,
@@ -189,7 +228,7 @@ fn atom<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn unquoted_atom<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
fn unquoted_atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (remaining, body) = take_till1(|c| match c {
' ' | '\t' | '\r' | '\n' | ')' | ']' => true,
_ => false,
@@ -198,23 +237,38 @@ fn unquoted_atom<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn quoted_atom<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
let (remaining, _) = tag(r#"""#)(input)?;
let (remaining, _) = escaped(
take_till1(|c| match c {
'\\' | '"' => true,
_ => false,
}),
'\\',
one_of(r#""n\\"#),
)(remaining)?;
fn quoted_atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (mut remaining, _) = tag(r#"""#)(input)?;
let mut in_escape = false;
loop {
if in_escape {
let (remain, _) = alt((recognize(one_of(r#""n\\"#)), digit1))(remaining)?;
remaining = remain;
in_escape = false;
} else {
let end_quote = tag::<_, _, nom::error::Error<_>>(r#"""#)(remaining);
if end_quote.is_ok() {
break;
}
let escape_backslash = tag::<_, _, nom::error::Error<_>>("\\")(remaining);
if let Ok((remain, _)) = escape_backslash {
remaining = remain;
in_escape = true;
continue;
}
let (remain, _) = anychar(remaining)?;
remaining = remain;
}
}
let (remaining, _) = tag(r#"""#)(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, Token::Atom(source.into())))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn hash_notation<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
fn hash_notation<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (remaining, _) = tag("#<")(input)?;
let (remaining, _body) = take_till1(|c| match c {
'>' => true,
@@ -225,7 +279,7 @@ fn hash_notation<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
Ok((remaining, Token::Atom(source.into())))
}
fn text_with_properties<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Token<'s>> {
fn text_with_properties<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
let (remaining, _) = tag("#(")(input)?;
let (remaining, (text, props)) = delimited(
multispace0,
@@ -255,7 +309,7 @@ mod tests {
#[test]
fn simple() {
let input = " (foo bar baz ) ";
let (remaining, parsed) = sexp_with_padding(input).expect("Parse the input");
let (remaining, parsed) = sexp(input).expect("Parse the input");
assert_eq!(remaining, "");
assert!(match parsed {
Token::Atom(_) => false,
@@ -268,7 +322,7 @@ mod tests {
#[test]
fn quoted() {
let input = r#" ("foo" bar baz ) "#;
let (remaining, parsed) = sexp_with_padding(input).expect("Parse the input");
let (remaining, parsed) = sexp(input).expect("Parse the input");
assert_eq!(remaining, "");
assert!(match parsed {
Token::Atom(_) => false,
@@ -292,7 +346,7 @@ mod tests {
#[test]
fn quoted_containing_paren() {
let input = r#" (foo "b(a)r" baz ) "#;
let (remaining, parsed) = sexp_with_padding(input).expect("Parse the input");
let (remaining, parsed) = sexp(input).expect("Parse the input");
assert_eq!(remaining, "");
assert!(match parsed {
Token::List(_) => true,
@@ -328,7 +382,7 @@ mod tests {
#[test]
fn string_containing_escaped_characters() {
let input = r#" (foo "\\( x=2 \\)" bar) "#;
let (remaining, parsed) = sexp_with_padding(input).expect("Parse the input");
let (remaining, parsed) = sexp(input).expect("Parse the input");
assert_eq!(remaining, "");
assert!(match parsed {
Token::Atom(_) => false,

View File

@@ -1,5 +1,10 @@
use crate::parser::sexp::Token;
use crate::types::Source;
use std::str::FromStr;
use super::elisp_fact::GetElispFact;
use super::sexp::Token;
use crate::compare::sexp::unquote;
use crate::types::GetStandardProperties;
use crate::types::StandardProperties;
/// Check if the child string slice is a slice of the parent string slice.
fn is_slice_of(parent: &str, child: &str) -> bool {
@@ -10,18 +15,39 @@ fn is_slice_of(parent: &str, child: &str) -> bool {
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.
fn get_offsets<'s, S: Source<'s>>(source: &'s str, rust_object: &'s S) -> (usize, usize) {
let rust_object_source = rust_object.get_source();
assert!(is_slice_of(source, rust_object_source));
let offset = rust_object_source.as_ptr() as usize - source.as_ptr() as usize;
fn get_rust_byte_offsets<'b, 's, S: StandardProperties<'s> + ?Sized>(
original_document: &'s str,
rust_ast_node: &'b S,
) -> (usize, usize) {
let rust_object_source = rust_ast_node.get_source();
debug_assert!(is_slice_of(original_document, rust_object_source));
let offset = rust_object_source.as_ptr() as usize - original_document.as_ptr() as usize;
let end = offset + rust_object_source.len();
(offset, end)
}
pub fn assert_name<'s>(emacs: &'s Token<'s>, name: &str) -> Result<(), Box<dyn std::error::Error>> {
pub(crate) fn compare_standard_properties<
'b,
's,
S: GetStandardProperties<'s> + GetElispFact<'s> + ?Sized,
>(
original_document: &'s str,
emacs: &'b Token<'s>,
rust: &'b S,
) -> Result<(), Box<dyn std::error::Error>> {
assert_name(emacs, rust.get_elisp_fact().get_elisp_name())?;
assert_bounds(original_document, emacs, rust.get_standard_properties())?;
Ok(())
}
pub(crate) fn assert_name<'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 first_child = children
.first()
@@ -29,7 +55,7 @@ pub fn assert_name<'s>(emacs: &'s Token<'s>, name: &str) -> Result<(), Box<dyn s
.as_atom()?;
if first_child != name {
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,
found = first_child
))?;
@@ -37,30 +63,33 @@ pub fn assert_name<'s>(emacs: &'s Token<'s>, name: &str) -> Result<(), Box<dyn s
Ok(())
}
pub fn assert_bounds<'s, S: Source<'s>>(
source: &'s str,
emacs: &'s Token<'s>,
rust: &'s S,
/// Assert that the character ranges defined by upstream org-mode's :standard-properties match the slices in Organic's StandardProperties.
///
/// This does **not** handle plain text because plain text is a special case.
pub(crate) fn assert_bounds<'b, 's, S: StandardProperties<'s> + ?Sized>(
original_document: &'s str,
emacs: &'b Token<'s>,
rust: &'b S,
) -> 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) = (
standard_properties
.begin
.ok_or("Token should have a begin.")?,
standard_properties.end.ok_or("Token should have an end.")?,
);
let (rust_begin, rust_end) = get_offsets(source, rust);
let rust_begin_char_offset = (&source[..rust_begin]).chars().count();
let (rust_begin, rust_end) = get_rust_byte_offsets(original_document, rust); // 0-based
let rust_begin_char_offset = (&original_document[..rust_begin]).chars().count() + 1; // 1-based
let rust_end_char_offset =
rust_begin_char_offset + (&source[rust_begin..rust_end]).chars().count();
if (rust_begin_char_offset + 1) != begin || (rust_end_char_offset + 1) != 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))?;
rust_begin_char_offset + (&original_document[rust_begin..rust_end]).chars().count(); // 1-based
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, rust_end = rust_end_char_offset, emacs_begin=begin, emacs_end=end))?;
}
Ok(())
}
struct StandardProperties {
struct EmacsStandardProperties {
begin: Option<usize>,
#[allow(dead_code)]
post_affiliated: Option<usize>,
@@ -73,9 +102,9 @@ struct StandardProperties {
post_blank: Option<usize>,
}
fn get_standard_properties<'s>(
emacs: &'s Token<'s>,
) -> Result<StandardProperties, Box<dyn std::error::Error>> {
fn get_emacs_standard_properties<'b, 's>(
emacs: &'b Token<'s>,
) -> Result<EmacsStandardProperties, Box<dyn std::error::Error>> {
let children = emacs.as_list()?;
let attributes_child = children
.iter()
@@ -94,7 +123,7 @@ fn get_standard_properties<'s>(
let contents_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())?;
StandardProperties {
EmacsStandardProperties {
begin,
post_affiliated,
contents_begin,
@@ -113,7 +142,7 @@ fn get_standard_properties<'s>(
maybe_token_to_usize(attributes_map.get(":post-blank").map(|token| *token))?;
let post_affiliated =
maybe_token_to_usize(attributes_map.get(":post-affiliated").map(|token| *token))?;
StandardProperties {
EmacsStandardProperties {
begin,
post_affiliated,
contents_begin,
@@ -141,22 +170,85 @@ fn maybe_token_to_usize(
.map_or(Ok(None), |r| r.map(Some))?)
}
pub fn get_property<'s, 'x>(
emacs: &'s Token<'s>,
/// Get a named property from the emacs token.
///
/// Returns Ok(None) if value is nil or absent.
pub(crate) fn get_property<'b, 's, 'x>(
emacs: &'b Token<'s>,
key: &'x str,
) -> Result<Option<&'s Token<'s>>, Box<dyn std::error::Error>> {
) -> Result<Option<&'b Token<'s>>, Box<dyn std::error::Error>> {
let children = emacs.as_list()?;
let attributes_child = children
.iter()
.nth(1)
.ok_or("Should have an attributes child.")?;
let attributes_map = attributes_child.as_map()?;
let prop = attributes_map
.get(key)
.ok_or(format!("Missing {} attribute.", key))?;
match prop.as_atom() {
Ok("nil") => return Ok(None),
let prop = attributes_map.get(key).map(|token| *token);
match prop.map(|token| token.as_atom()) {
Some(Ok("nil")) => return Ok(None),
_ => {}
};
Ok(Some(*prop))
Ok(prop)
}
/// Get a named property containing an unquoted atom from the emacs token.
///
/// Returns None if key is not found.
pub(crate) fn get_property_unquoted_atom<'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

@@ -14,24 +14,28 @@ use crate::error::Res;
use crate::parser::OrgSource;
#[derive(Debug)]
pub enum ContextElement<'r, 's> {
pub(crate) enum ContextElement<'r, 's> {
/// Stores a parser that indicates that children should exit upon matching an exit matcher.
ExitMatcherNode(ExitMatcherNode<'r>),
/// Stores the name of the current element to prevent directly nesting elements of the same type.
Context(&'r str),
/// Stores the name of the current object to prevent directly nesting elements of the same type.
ContextObject(&'r str),
/// Indicates if elements should consume the whitespace after them.
ConsumeTrailingWhitespace(bool),
/// This is just here to use the 's lifetime until I'm sure we can eliminate it from ContextElement.
#[allow(dead_code)]
Placeholder(PhantomData<&'s str>),
}
pub struct ExitMatcherNode<'r> {
pub(crate) struct ExitMatcherNode<'r> {
// TODO: Should this be "&'r DynContextMatcher<'c>" ?
pub exit_matcher: &'r DynContextMatcher<'r>,
pub class: ExitClass,
pub(crate) exit_matcher: &'r DynContextMatcher<'r>,
pub(crate) class: ExitClass,
}
impl<'r> std::fmt::Debug for ExitMatcherNode<'r> {
@@ -43,13 +47,13 @@ impl<'r> std::fmt::Debug for ExitMatcherNode<'r> {
}
#[derive(Debug)]
pub struct Context<'g, 'r, 's> {
pub(crate) struct Context<'g, 'r, 's> {
global_settings: &'g GlobalSettings<'g, 's>,
tree: List<'r, &'r ContextElement<'r, 's>>,
}
impl<'g, 'r, 's> Context<'g, 'r, 's> {
pub fn new(
pub(crate) fn new(
global_settings: &'g GlobalSettings<'g, 's>,
tree: List<'r, &'r ContextElement<'r, 's>>,
) -> Self {
@@ -59,38 +63,38 @@ impl<'g, 'r, 's> Context<'g, 'r, 's> {
}
}
pub fn with_additional_node(&'r self, new_element: &'r ContextElement<'r, 's>) -> Self {
pub(crate) fn with_additional_node(&'r self, new_element: &'r ContextElement<'r, 's>) -> Self {
let new_tree = self.tree.push(new_element);
Self::new(self.global_settings, new_tree)
}
pub fn iter(&'r self) -> super::list::Iter<'r, &'r ContextElement<'r, 's>> {
pub(crate) fn iter(&'r self) -> super::list::Iter<'r, &'r ContextElement<'r, 's>> {
self.tree.iter()
}
pub fn iter_context(&'r self) -> Iter<'g, 'r, 's> {
fn iter_context(&'r self) -> Iter<'g, 'r, 's> {
Iter {
next: self.tree.iter_list(),
global_settings: self.global_settings,
}
}
pub fn get_parent(&'r self) -> Option<Self> {
pub(crate) fn get_parent(&'r self) -> Option<Self> {
self.tree.get_parent().map(|parent_tree| Self {
global_settings: self.global_settings,
tree: parent_tree.clone(),
})
}
pub fn get_data(&self) -> &ContextElement<'r, 's> {
fn get_data(&self) -> &ContextElement<'r, 's> {
self.tree.get_data()
}
pub fn get_global_settings(&self) -> &'g GlobalSettings<'g, 's> {
pub(crate) fn get_global_settings(&self) -> &'g GlobalSettings<'g, 's> {
self.global_settings
}
pub fn with_global_settings<'gg>(
pub(crate) fn with_global_settings<'gg>(
&self,
new_settings: &'gg GlobalSettings<'gg, 's>,
) -> Context<'gg, 'r, 's> {
@@ -101,7 +105,7 @@ impl<'g, 'r, 's> Context<'g, 'r, 's> {
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn check_exit_matcher(
pub(crate) fn check_exit_matcher(
&'r self,
i: OrgSource<'s>,
) -> IResult<OrgSource<'s>, OrgSource<'s>, CustomError<OrgSource<'s>>> {
@@ -130,7 +134,7 @@ impl<'g, 'r, 's> Context<'g, 'r, 's> {
/// Indicates if elements should consume the whitespace after them.
///
/// Defaults to true.
pub fn should_consume_trailing_whitespace(&self) -> bool {
pub(crate) fn should_consume_trailing_whitespace(&self) -> bool {
self._should_consume_trailing_whitespace().unwrap_or(true)
}
@@ -155,7 +159,7 @@ fn document_end<'b, 'g, 'r, 's>(
eof(input)
}
pub struct Iter<'g, 'r, 's> {
struct Iter<'g, 'r, 's> {
global_settings: &'g GlobalSettings<'g, 's>,
next: super::list::IterList<'r, &'r ContextElement<'r, 's>>,
}
@@ -172,7 +176,7 @@ impl<'g, 'r, 's> Iterator for Iter<'g, 'r, 's> {
}
impl<'r, 's> ContextElement<'r, 's> {
pub fn document_context() -> Self {
pub(crate) fn document_context() -> Self {
Self::ExitMatcherNode(ExitMatcherNode {
exit_matcher: &document_end,
class: ExitClass::Document,

View File

@@ -1,16 +1,9 @@
#[derive(Debug, Copy, Clone)]
pub enum ExitClass {
/// Headlines and sections.
pub(crate) enum ExitClass {
Document = 1,
/// Elements who take priority over beta elements when matching.
Alpha = 20,
/// Elements who cede priority to alpha elements when matching.
Beta = 300,
/// Elements who cede priority to alpha and beta elements when matching.
Gamma = 4000,
Alpha = 2,
Beta = 3,
Gamma = 4,
}
impl std::fmt::Display for ExitClass {

View File

@@ -2,6 +2,7 @@ use std::collections::BTreeSet;
use super::FileAccessInterface;
use super::LocalFileAccessInterface;
use crate::types::IndentationLevel;
use crate::types::Object;
// TODO: Ultimately, I think we'll need most of this: https://orgmode.org/manual/In_002dbuffer-Settings.html
@@ -12,10 +13,31 @@ pub struct GlobalSettings<'g, 's> {
pub file_access: &'g dyn FileAccessInterface,
pub in_progress_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> {
pub fn new() -> GlobalSettings<'g, 's> {
fn new() -> GlobalSettings<'g, 's> {
GlobalSettings {
radio_targets: Vec::new(),
file_access: &LocalFileAccessInterface {
@@ -23,6 +45,10 @@ impl<'g, 's> GlobalSettings<'g, 's> {
},
in_progress_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()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum HeadlineLevelFilter {
Odd,
OddEven,
}
impl Default for HeadlineLevelFilter {
fn default() -> Self {
HeadlineLevelFilter::OddEven
}
}

View File

@@ -1,38 +1,39 @@
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct List<'parent, T> {
pub(crate) struct List<'parent, T> {
data: 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>>;
impl<'parent, T> List<'parent, T> {
pub fn new(first_item: T) -> Self {
pub(crate) fn new(first_item: T) -> Self {
Self {
data: first_item,
parent: None,
}
}
pub fn get_data(&self) -> &T {
pub(crate) fn get_data(&self) -> &T {
&self.data
}
pub fn get_parent(&'parent self) -> Link<'parent, T> {
pub(crate) fn get_parent(&'parent self) -> Link<'parent, T> {
self.parent
}
pub fn iter(&self) -> Iter<'_, T> {
pub(crate) fn iter(&self) -> Iter<'_, T> {
Iter { next: Some(self) }
}
pub fn iter_list(&self) -> IterList<'_, T> {
pub(crate) fn iter_list(&self) -> IterList<'_, T> {
IterList { next: Some(self) }
}
pub fn push(&'parent self, item: T) -> Self {
pub(crate) fn push(&'parent self, item: T) -> Self {
Self {
data: item,
parent: Some(self),
@@ -40,7 +41,7 @@ impl<'parent, T> List<'parent, T> {
}
}
pub struct Iter<'a, T> {
pub(crate) struct Iter<'a, T> {
next: Link<'a, T>,
}
@@ -54,7 +55,7 @@ impl<'a, T> Iterator for Iter<'a, T> {
}
}
pub struct IterList<'a, T> {
pub(crate) struct IterList<'a, T> {
next: Link<'a, T>,
}

View File

@@ -8,22 +8,24 @@ mod global_settings;
mod list;
mod parser_with_context;
pub type RefContext<'b, 'g, 'r, 's> = &'b Context<'g, 'r, 's>;
pub trait ContextMatcher = for<'b, 'g, 'r, 's> Fn(
pub(crate) type RefContext<'b, 'g, 'r, 's> = &'b Context<'g, 'r, 's>;
pub(crate) trait ContextMatcher = for<'b, 'g, 'r, 's> Fn(
RefContext<'b, 'g, 'r, 's>,
OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>>;
pub type DynContextMatcher<'c> = dyn ContextMatcher + 'c;
pub trait Matcher = for<'s> Fn(OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>>;
type DynContextMatcher<'c> = dyn ContextMatcher + 'c;
pub(crate) trait Matcher = for<'s> Fn(OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>>;
#[allow(dead_code)]
pub type DynMatcher<'c> = dyn Matcher + 'c;
type DynMatcher<'c> = dyn Matcher + 'c;
pub use context::Context;
pub use context::ContextElement;
pub use context::ExitMatcherNode;
pub use exiting::ExitClass;
pub(crate) use context::Context;
pub(crate) use context::ContextElement;
pub(crate) use context::ExitMatcherNode;
pub(crate) use exiting::ExitClass;
pub use file_access_interface::FileAccessInterface;
pub use file_access_interface::LocalFileAccessInterface;
pub use global_settings::GlobalSettings;
pub use list::List;
pub use global_settings::HeadlineLevelFilter;
pub use global_settings::DEFAULT_TAB_WIDTH;
pub(crate) use list::List;
pub(crate) use parser_with_context::parser_with_context;

View File

@@ -2,18 +2,18 @@ use nom::error::ErrorKind;
use nom::error::ParseError;
use nom::IResult;
pub 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)]
pub enum CustomError<I> {
MyError(MyError<I>),
MyError(MyError<&'static str>),
Nom(I, ErrorKind),
IO(std::io::Error),
BoxedError(Box<dyn std::error::Error>),
}
#[derive(Debug)]
pub struct MyError<I>(pub I);
pub struct MyError<I>(pub(crate) I);
impl<I> ParseError<I> for CustomError<I> {
fn from_error_kind(input: I, kind: ErrorKind) -> Self {
@@ -31,3 +31,15 @@ impl<I> From<std::io::Error> for CustomError<I> {
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

@@ -1,4 +1,4 @@
mod error;
pub use error::CustomError;
pub use error::MyError;
pub use error::Res;
pub(crate) use error::CustomError;
pub(crate) use error::MyError;
pub(crate) use error::Res;

View File

@@ -10,7 +10,7 @@ const SERVICE_NAME: &'static str = "organic";
// Despite the obvious verbosity that fully-qualifying everything causes, in these functions I am fully-qualifying everything relating to tracing. This is because the tracing feature involves multiple libraries working together and so I think it is beneficial to see which libraries contribute which bits.
#[cfg(feature = "tracing")]
pub fn init_telemetry() -> Result<(), Box<dyn std::error::Error>> {
pub(crate) fn init_telemetry() -> Result<(), Box<dyn std::error::Error>> {
// by default it will hit http://localhost:4317 with a gRPC payload
// TODO: I think the endpoint can be controlled by the OTEL_EXPORTER_OTLP_TRACES_ENDPOINT env variable instead of hard-coded into this code base. Regardless, I am the only developer right now so I am not too concerned.
let exporter = opentelemetry_otlp::new_exporter()
@@ -55,17 +55,17 @@ pub fn init_telemetry() -> Result<(), Box<dyn std::error::Error>> {
}
#[cfg(feature = "tracing")]
pub fn shutdown_telemetry() -> Result<(), Box<dyn std::error::Error>> {
pub(crate) fn shutdown_telemetry() -> Result<(), Box<dyn std::error::Error>> {
opentelemetry::global::shutdown_tracer_provider();
Ok(())
}
#[cfg(not(feature = "tracing"))]
pub fn init_telemetry() -> Result<(), Box<dyn std::error::Error>> {
pub(crate) fn init_telemetry() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(not(feature = "tracing"))]
pub fn shutdown_telemetry() -> Result<(), Box<dyn std::error::Error>> {
pub(crate) fn shutdown_telemetry() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

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,24 +1,21 @@
#![feature(round_char_boundary)]
#![feature(exit_status_error)]
#![feature(trait_alias)]
#![feature(path_file_prefix)]
// TODO: #![warn(missing_docs)]
#[cfg(feature = "compare")]
mod compare;
#[cfg(feature = "compare")]
pub use compare::compare_document;
#[cfg(feature = "compare")]
pub use compare::emacs_parse_anonymous_org_document;
#[cfg(feature = "compare")]
pub use compare::emacs_parse_file_org_document;
#[cfg(feature = "compare")]
pub use compare::get_emacs_version;
#[cfg(feature = "compare")]
pub use compare::get_org_mode_version;
pub mod compare;
mod context;
mod error;
mod iter;
pub mod parser;
pub mod types;
pub use context::GlobalSettings;
pub use context::LocalFileAccessInterface;
pub mod settings {
pub use crate::context::FileAccessInterface;
pub use crate::context::GlobalSettings;
pub use crate::context::HeadlineLevelFilter;
pub use crate::context::LocalFileAccessInterface;
pub use crate::context::DEFAULT_TAB_WIDTH;
}

View File

@@ -4,21 +4,9 @@ use std::io::Read;
use std::path::Path;
use ::organic::parser::parse;
#[cfg(feature = "compare")]
use organic::compare_document;
#[cfg(feature = "compare")]
use organic::emacs_parse_anonymous_org_document;
#[cfg(feature = "compare")]
use organic::emacs_parse_file_org_document;
#[cfg(feature = "compare")]
use organic::get_emacs_version;
#[cfg(feature = "compare")]
use organic::get_org_mode_version;
use organic::parser::parse_with_settings;
#[cfg(feature = "compare")]
use organic::parser::sexp::sexp_with_padding;
use organic::GlobalSettings;
use organic::LocalFileAccessInterface;
use organic::settings::GlobalSettings;
use organic::settings::LocalFileAccessInterface;
#[cfg(feature = "tracing")]
use crate::init_tracing::init_telemetry;
@@ -66,85 +54,14 @@ fn read_stdin_to_string() -> Result<String, Box<dyn std::error::Error>> {
Ok(stdin_contents)
}
#[cfg(feature = "compare")]
fn run_anonymous_parse<P: AsRef<str>>(org_contents: P) -> Result<(), Box<dyn std::error::Error>> {
let org_contents = org_contents.as_ref();
eprintln!("Using emacs version: {}", get_emacs_version()?.trim());
eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim());
let rust_parsed = parse(org_contents)?;
let org_sexp = emacs_parse_anonymous_org_document(org_contents)?;
let (_remaining, parsed_sexp) =
sexp_with_padding(org_sexp.as_str()).map_err(|e| e.to_string())?;
println!("{}\n\n\n", org_contents);
println!("{}", org_sexp);
println!("{:#?}", rust_parsed);
// We do the diffing after printing out both parsed forms in case the diffing panics
let diff_result = compare_document(&parsed_sexp, &rust_parsed)?;
diff_result.print(org_contents)?;
if diff_result.is_bad() {
Err("Diff results do not match.")?;
}
Ok(())
}
#[cfg(not(feature = "compare"))]
fn run_anonymous_parse<P: AsRef<str>>(org_contents: P) -> Result<(), Box<dyn std::error::Error>> {
eprintln!(
"This program was built with compare disabled. Only parsing with organic, not comparing."
);
let rust_parsed = parse(org_contents.as_ref())?;
println!("{:#?}", rust_parsed);
Ok(())
}
#[cfg(feature = "compare")]
fn run_parse_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::error::Error>> {
let org_path = org_path.as_ref();
eprintln!("Using emacs version: {}", get_emacs_version()?.trim());
eprintln!("Using org-mode version: {}", get_org_mode_version()?.trim());
let parent_directory = org_path
.parent()
.ok_or("Should be contained inside a directory.")?;
let org_contents = std::fs::read_to_string(org_path)?;
let org_contents = org_contents.as_str();
let file_access_interface = LocalFileAccessInterface {
working_directory: Some(parent_directory.to_path_buf()),
};
let global_settings = {
let mut global_settings = GlobalSettings::default();
global_settings.file_access = &file_access_interface;
global_settings
};
let rust_parsed = parse_with_settings(org_contents, &global_settings)?;
let org_sexp = emacs_parse_file_org_document(org_path)?;
let (_remaining, parsed_sexp) =
sexp_with_padding(org_sexp.as_str()).map_err(|e| e.to_string())?;
println!("{}\n\n\n", org_contents);
println!("{}", org_sexp);
println!("{:#?}", rust_parsed);
// We do the diffing after printing out both parsed forms in case the diffing panics
let diff_result = compare_document(&parsed_sexp, &rust_parsed)?;
diff_result.print(org_contents)?;
if diff_result.is_bad() {
Err("Diff results do not match.")?;
}
Ok(())
}
#[cfg(not(feature = "compare"))]
fn run_parse_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::error::Error>> {
let org_path = org_path.as_ref();
eprintln!(
"This program was built with compare disabled. Only parsing with organic, not comparing."
);
let parent_directory = org_path
.parent()
.ok_or("Should be contained inside a directory.")?;

View File

@@ -18,7 +18,7 @@ use crate::parser::util::get_consumed;
use crate::types::AngleLink;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn angle_link<'b, 'g, 'r, 's>(
pub(crate) fn angle_link<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, AngleLink<'s>> {

View File

@@ -31,7 +31,7 @@ use crate::types::Citation;
use crate::types::Object;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn citation<'b, 'g, 'r, 's>(
pub(crate) fn citation<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Citation<'s>> {
@@ -187,7 +187,7 @@ mod tests {
use crate::context::List;
use crate::parser::element_parser::element;
use crate::types::Element;
use crate::types::Source;
use crate::types::GetStandardProperties;
#[test]
fn citation_simple() {
@@ -202,7 +202,10 @@ mod tests {
_ => panic!("Should be a paragraph!"),
};
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

View File

@@ -29,7 +29,7 @@ use crate::types::CitationReference;
use crate::types::Object;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn citation_reference<'b, 'g, 'r, 's>(
pub(crate) fn citation_reference<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, CitationReference<'s>> {
@@ -49,7 +49,7 @@ pub fn citation_reference<'b, 'g, 'r, 's>(
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn citation_reference_key<'b, 'g, 'r, 's>(
pub(crate) fn citation_reference_key<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
@@ -161,7 +161,7 @@ fn _key_suffix_end<'b, 'g, 'r, 's>(
tag(";")(input)
}
pub fn must_balance_bracket<'s, F, O>(
pub(crate) fn must_balance_bracket<'s, F, O>(
mut inner: F,
) -> impl FnMut(OrgSource<'s>) -> Res<OrgSource<'s>, O>
where

View File

@@ -20,7 +20,7 @@ use crate::parser::util::start_of_line;
use crate::types::Clock;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn clock<'b, 'g, 'r, 's>(
pub(crate) fn clock<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Clock<'s>> {

View File

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

View File

@@ -1,16 +1,11 @@
use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::combinator::eof;
use nom::combinator::opt;
use nom::combinator::recognize;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::sexp::sexp;
use crate::context::RefContext;
use crate::error::Res;
use crate::parser::util::get_consumed;
@@ -18,22 +13,14 @@ use crate::parser::util::start_of_line;
use crate::types::DiarySexp;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub fn diary_sexp<'b, 'g, 'r, 's>(
pub(crate) fn diary_sexp<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, DiarySexp<'s>> {
start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(input)?;
let (remaining, _clock) = tag("%%")(remaining)?;
let (remaining, _gap_whitespace) = space0(remaining)?;
let (remaining, _sexp) = recognize(sexp)(remaining)?;
let (remaining, _trailing_comment) = opt(tuple((
space0,
tag(";"),
many_till(anychar, alt((line_ending, eof))),
)))(remaining)?;
let (remaining, _trailing_whitespace) =
recognize(tuple((space0, alt((line_ending, eof)))))(remaining)?;
let (remaining, _clock) = tag("%%(")(input)?;
let (remaining, _contents) = is_not("\r\n")(remaining)?;
let (remaining, _eol) = alt((line_ending, eof))(remaining)?;
let source = get_consumed(input, remaining);
Ok((
@@ -43,3 +30,9 @@ pub fn diary_sexp<'b, 'g, 'r, 's>(
},
))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn detect_diary_sexp<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
tuple((start_of_line, tag("%%(")))(input)?;
Ok((input, ()))
}

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