158 Commits

Author SHA1 Message Date
Tom Alexander
503db94b2c Publish version 0.1.12.
All checks were successful
rustfmt Build rustfmt has succeeded
clippy Build clippy has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-18 19:01:43 -04:00
Tom Alexander
a4381e5e39 Merge branch 'keyword_constants' 2023-10-18 18:48:52 -04:00
Tom Alexander
e11de60def Clippy fixes.
All checks were successful
clippy Build clippy has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-18 18:39:04 -04:00
Tom Alexander
b2479e9de8 Remove Debug from the context variables.
Now that entities are stored in the settings struct, these variables are massive which makes them balloon trace sizes while being mostly unreadable. This removes Debug from them to serve as a static-analysis check that context is ALWAYS ignored in tracing calls.
2023-10-18 18:36:25 -04:00
Tom Alexander
49d1cef7ae Remove context from functions that no longer need it. 2023-10-18 18:28:24 -04:00
Tom Alexander
ba72cc1b29 The variables for keywords are actually constants.
These settings do not need to exist in GlobalSettings because they are actually constants in upstream Org-Mode.
2023-10-18 18:22:01 -04:00
Tom Alexander
c58b0e7c35 Add a script to dump an AST using docker.
All checks were successful
rust-test Build rust-test has succeeded
clippy Build clippy has succeeded
rustfmt Build rustfmt has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
rust-build Build rust-build has succeeded
2023-10-18 15:39:52 -04:00
Tom Alexander
f19d262825 Merge branch 'bullshitium'
All checks were successful
rustfmt Build rustfmt has succeeded
clippy Build clippy 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-10-18 13:04:16 -04:00
Tom Alexander
68f3f2e159 Clippy fixes.
All checks were successful
clippy Build clippy has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-18 12:42:09 -04:00
Tom Alexander
269e23c1b1 No more expect-fail tests! 2023-10-18 12:41:12 -04:00
Tom Alexander
e111b8b9b8 Performance optimization. 2023-10-18 12:39:08 -04:00
Tom Alexander
353ff07420 Handle bullshitium for broken dynamic blocks. 2023-10-18 12:32:48 -04:00
Tom Alexander
94dec31130 Consuming trailing whitespace for 🔚 bullshitium. 2023-10-18 12:17:57 -04:00
Tom Alexander
cf5d3ed745 Add tests for the 🔚 bullshitium. 2023-10-18 11:59:55 -04:00
Tom Alexander
b0b287cd47 Handle bullshitium for 🔚. 2023-10-18 11:57:39 -04:00
Tom Alexander
bcdf1f5e9d Merge branch 'entity_special_case'
All checks were successful
rustfmt Build rustfmt has succeeded
clippy Build clippy has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded
rust-test Build rust-test has succeeded
2023-10-18 08:53:57 -04:00
Tom Alexander
17d8e76e05 Do not match POST for entities that end with a space.
This is a special case for en-spaces.
2023-10-18 08:52:18 -04:00
Tom Alexander
8db9038c53 Merge branch 'list_perf_improvement'
Some checks failed
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-test Build rust-test has failed
rustfmt Build rustfmt has succeeded
clippy Build clippy has succeeded
2023-10-18 08:40:19 -04:00
Tom Alexander
a276ba70e0 Fix empty content items with final item whitespace cut-off before headlines.
Some checks failed
clippy Build clippy has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-17 15:56:02 -04:00
Tom Alexander
b7442c1e92 Do not match headlines as plain list items. 2023-10-17 15:35:43 -04:00
Tom Alexander
364ba79517 It actually worked on trailing whitespace ownership test case 2. 2023-10-17 15:22:31 -04:00
Tom Alexander
47408763e5 A first stab at a final item whitespace cut-off exit matcher. 2023-10-17 15:08:36 -04:00
Tom Alexander
bd187ebfe7 Remove re-parsing of the final list child. 2023-10-17 14:17:47 -04:00
Tom Alexander
59cb3c2bbf Remove unnecessary closures in plain lists. 2023-10-17 13:59:33 -04:00
Tom Alexander
44f7412a5c Merge branch 'perf_improvement'
Some checks failed
rustfmt Build rustfmt has succeeded
clippy Build clippy has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-test Build rust-test has succeeded
2023-10-17 13:54:44 -04:00
Tom Alexander
01464057ad Remove unused event types. 2023-10-17 13:43:33 -04:00
Tom Alexander
0208020e3e Also print byte offset.
Some checks failed
clippy Build clippy has failed
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-17 13:35:40 -04:00
Tom Alexander
a2f53361eb Record element start events and report them when the event_count feature is enabled. 2023-10-17 13:32:01 -04:00
Tom Alexander
17db05c2c7 Unify more error handling. 2023-10-17 12:42:34 -04:00
Tom Alexander
6139ea328d Unify some more error handling. 2023-10-17 12:22:52 -04:00
Tom Alexander
d20b4a410b Remove pointless map_err calls. 2023-10-17 11:56:36 -04:00
Tom Alexander
05c64f53b1 Remove boxed error from CustomError. 2023-10-17 11:40:11 -04:00
Tom Alexander
f65d0bb82d Remove redundant call to space0. 2023-10-17 11:33:26 -04:00
Tom Alexander
50d2831081 Cleanup. 2023-10-17 11:30:23 -04:00
Tom Alexander
bc9bd4f97b Eliminate some closures. 2023-10-17 11:10:18 -04:00
Tom Alexander
369d3e8c50 Add a full-document parse benchmark. 2023-10-17 10:57:04 -04:00
Tom Alexander
7d73eb6bd4 Merge branch 'error_rework'
Some checks failed
clippy Build clippy has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has failed
rustfmt Build rustfmt has failed
rust-test Build rust-test has succeeded
2023-10-17 10:40:46 -04:00
Tom Alexander
f59f153ee7 Clean up. 2023-10-17 10:39:21 -04:00
Tom Alexander
20c4a0f8f7 Continue removing MyError. 2023-10-17 10:35:33 -04:00
Tom Alexander
e776a051ad Continue removing MyError. 2023-10-17 10:13:00 -04:00
Tom Alexander
77e6c22ad8 Continue removing MyError. 2023-10-17 10:09:37 -04:00
Tom Alexander
c9d7251e3b Begin removing the MyError type. 2023-10-17 09:45:18 -04:00
Tom Alexander
8417b5fc9d Add an owned string entry for CustomError. 2023-10-17 09:27:15 -04:00
Tom Alexander
acc29e7977 Publish version 0.1.11.
Some checks failed
rustfmt Build rustfmt has succeeded
clippy Build clippy has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-test Build rust-test has succeeded
2023-10-16 19:53:46 -04:00
Tom Alexander
ebc0a30035 Merge branch 'clippy_ci_job' 2023-10-16 19:50:01 -04:00
Tom Alexander
e2d55e13d3 Fix some clippy errors that didn't appear on my host version of clippy.
Some checks failed
clippy Build clippy has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-16 19:43:15 -04:00
Tom Alexander
e4d9c5f467 Add makefile command to run clippy through docker. 2023-10-16 19:38:45 -04:00
Tom Alexander
d8e3a85ef7 We need to add dependencies so we are now building a container. 2023-10-16 19:34:53 -04:00
Tom Alexander
464685b52b Use a cargo cache for the clippy CI job.
Some checks failed
clippy Build clippy has failed
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-16 19:27:15 -04:00
Tom Alexander
5fed4e80a7 Add a CI job to run clippy for every push. 2023-10-16 19:22:59 -04:00
Tom Alexander
e53140426f Merge branch 'clippy'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-16 19:14:19 -04:00
Tom Alexander
9a4d290cf8 Apply more suggestions. 2023-10-16 19:12:25 -04:00
Tom Alexander
acd24d6198 Apply more suggestions. 2023-10-16 19:02:34 -04:00
Tom Alexander
880b00ef3f Apply more suggestions. 2023-10-16 18:54:41 -04:00
Tom Alexander
3069711447 Apply more suggestions. 2023-10-16 18:29:21 -04:00
Tom Alexander
4b6c717812 Apply more suggestions. 2023-10-16 17:58:52 -04:00
Tom Alexander
1d329cc310 Apply more suggestions. 2023-10-16 17:28:28 -04:00
Tom Alexander
b4f9a3b9b6 Apply more suggestions. 2023-10-16 17:14:44 -04:00
Tom Alexander
2dd5246506 Apply more suggestions. 2023-10-16 17:03:39 -04:00
Tom Alexander
4ba0e3611b Apply more suggestions. 2023-10-16 17:03:39 -04:00
Tom Alexander
728f79b86c Apply more suggestions. 2023-10-16 17:03:39 -04:00
Tom Alexander
192a4a2891 Remove unnecessary lifetimes. 2023-10-16 17:03:39 -04:00
Tom Alexander
fafd85fb30 Apply some clippy fixes. 2023-10-16 17:03:39 -04:00
Tom Alexander
1c23065329 Add a clippy command to the makefile. 2023-10-16 17:03:39 -04:00
Tom Alexander
ed105b04ad Merge branch 'cargo_bench' 2023-10-16 16:03:56 -04:00
Tom Alexander
f10efec21d No performance change switching affiliated_key to using element macro. 2023-10-16 15:57:18 -04:00
Tom Alexander
72b4cf8e71 Add the first use of the rust benchmark tests. 2023-10-16 15:50:08 -04:00
Tom Alexander
547fc40dbe No measurable performance improvement with native builds over LTO release builds.
Some checks failed
rustfmt Build rustfmt has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
Leaving the code for this commented out because it involved an unstable cargo feature without showing any benefit. I would like to revisit this later.
2023-10-16 15:21:36 -04:00
Tom Alexander
9f1671658d Merge branch 'object_parser_perf'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-16 15:04:04 -04:00
Tom Alexander
18d0676fad Clean up. 2023-10-16 15:03:23 -04:00
Tom Alexander
7833a58461 Apply a similar optimization to the detect element parser but also unify detection of affiliated keywords. 2023-10-16 14:55:40 -04:00
Tom Alexander
0020d71089 Extend that optimization to more object parsers. 2023-10-16 14:41:12 -04:00
Tom Alexander
cfdf39d1fa Significantly reduce the use of closures in the object parsers. 2023-10-16 14:25:02 -04:00
Tom Alexander
26f1eae9a1 Merge branch 'planning_before_property_drawer'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-16 13:48:03 -04:00
Tom Alexander
3eff85059a Add support for planning before property drawer when calculating additional properties for headlines. 2023-10-16 13:35:03 -04:00
Tom Alexander
d2d0e9e5dd Merge branch 'optval_affiliated_keywords'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
2023-10-16 13:16:44 -04:00
Tom Alexander
c86d1000c0 Do not clear values in lists of strings.
Some checks failed
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
This is a hold-over from when I had list of single string which was a misunderstanding of the optional pair type.
2023-10-16 12:58:20 -04:00
Tom Alexander
911634cb42 Attr_ affiliated keywords should be lists of strings. 2023-10-16 12:55:18 -04:00
Tom Alexander
0aa746fb1e Implement comparison for object tree. 2023-10-16 12:50:53 -04:00
Tom Alexander
33800c4a88 Implement comparison for optional pair. 2023-10-16 12:05:36 -04:00
Tom Alexander
909ccadfa1 Beginning update to compare_affiliated_keywords. 2023-10-16 11:45:54 -04:00
Tom Alexander
e352deb989 Update parse_affiliated_keywords for handling optional pairs. 2023-10-16 11:42:20 -04:00
Tom Alexander
f5a6a26c43 Disable the existing handling of affiliated keywords. 2023-10-15 20:31:14 -04:00
Tom Alexander
dd7184da54 Add analysis from test. 2023-10-15 20:22:46 -04:00
Tom Alexander
1168ddb1fe Start an investigation into affiliated keyword behavior. 2023-10-15 17:38:56 -04:00
Tom Alexander
77ab636e6a Merge branch 'unify_keyword_constants' 2023-10-15 15:59:21 -04:00
Tom Alexander
f5dcacc79d Do not match keyword name if a longer keyword name would match. 2023-10-15 15:55:19 -04:00
Tom Alexander
e7c3c7aab6 Switch the keyword parsers over to using the settings from GlobalSettings. 2023-10-15 15:17:08 -04:00
Tom Alexander
7603b0a1cc Add a test showing we are not handling optval properly. 2023-10-15 15:16:23 -04:00
Tom Alexander
dea3721b1c Fix reporting errors in tests.
Some checks failed
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-test Build rust-test has succeeded
2023-10-15 15:16:06 -04:00
Tom Alexander
4b54f95087 Publish version 0.1.10.
Some checks failed
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-test Build rust-test has succeeded
2023-10-14 19:31:41 -04:00
Tom Alexander
17c2e9fefe Add a build of compare and foreign document test without tracing. 2023-10-14 19:31:41 -04:00
Tom Alexander
d2d8c1ffcf Fix build for compare with tracing. 2023-10-14 19:20:49 -04:00
Tom Alexander
b9c638c280 Fix tracing.
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-14 19:02:35 -04:00
Tom Alexander
8ac8f9fe6e Merge branch 'foreign_document_test'
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-14 18:53:52 -04:00
Tom Alexander
ddb3144e66 Fix the tests.
Some checks failed
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-14 18:25:26 -04:00
Tom Alexander
ad5efc4b0f Only require sync on FileAccessInterface when compiling for compare utilities.
Some checks failed
rust-test Build rust-test has failed
rust-build Build rust-build has failed
rust-foreign-document-test Build rust-foreign-document-test has failed
Otherwise async compatibility would impact sync users of the plain library.
2023-10-14 18:09:50 -04:00
Tom Alexander
2de33b8150 Enable release-lto profile on foreign document test. 2023-10-14 18:00:20 -04:00
Tom Alexander
e1fde88a60 Switch to using tokio too invoke emacs async. 2023-10-14 17:58:50 -04:00
Tom Alexander
123da9cca3 Make the compare functions async. 2023-10-14 17:51:12 -04:00
Tom Alexander
c20e7b5f2f Make compare an always-async program.
This program is used as a development tool, so it is more valuable that we make it simple by only supporting one mode of operation (async) than making it widely compatible by supporting both.
2023-10-14 17:50:53 -04:00
Tom Alexander
74a3512038 Report whether all the tests passed. 2023-10-14 17:50:53 -04:00
Tom Alexander
ff04c4a131 Sort files in a compare_all_org_document. 2023-10-14 17:50:53 -04:00
Tom Alexander
00611e05c2 Remove old bash script. 2023-10-14 15:32:36 -04:00
Tom Alexander
3e7e54a1bd Add the foreign_document_test to the build tests. 2023-10-14 15:26:04 -04:00
Tom Alexander
aa35d1dc03 Copy over the rest of the foreign document test config. 2023-10-14 15:18:57 -04:00
Tom Alexander
92afdc0ea6 Strip prefix from file path. 2023-10-14 15:18:57 -04:00
Tom Alexander
f43920fc7c Add a silent mode for running the diff. 2023-10-14 15:18:57 -04:00
Tom Alexander
dde4bc7920 Add code for structured printing of test results. 2023-10-14 14:30:24 -04:00
Tom Alexander
3d68e1fd00 Switch the docker container over to invoking the rust-based foreign document test. 2023-10-13 13:28:24 -04:00
Tom Alexander
8271f6b44a Start invoking the tests. 2023-10-13 13:20:21 -04:00
Tom Alexander
8a26965e14 Write a function to compare all org-mode files in a directory. 2023-10-13 13:02:17 -04:00
Tom Alexander
3927889e66 Change test status to pass/fail. 2023-10-13 12:14:46 -04:00
Tom Alexander
5ecd7b8bef Launch tests in parallel. 2023-10-13 12:07:52 -04:00
Tom Alexander
b0b795d13b Limit concurrency of running tests. 2023-10-13 11:41:49 -04:00
Tom Alexander
182c2737cd Add futures. 2023-10-13 11:27:43 -04:00
Tom Alexander
5f93cabff5 Hit recursive async. 2023-10-13 11:27:42 -04:00
Tom Alexander
a1f8cbe079 Working on invoking the tests. 2023-10-13 11:27:42 -04:00
Tom Alexander
d7e870cba1 Starting to make result structs. 2023-10-13 11:27:42 -04:00
Tom Alexander
591b5ed382 Starting to define a TestConfig enum. 2023-10-13 11:27:42 -04:00
Tom Alexander
fd141762f0 Start a rust-based foreign document test.
Instead of using a simple bash script, this will use async rust to run multiple tests in parallel.
2023-10-13 11:27:42 -04:00
Tom Alexander
d59bbfa7d2 Update org-mode.
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 failed
2023-10-13 11:26:41 -04:00
Tom Alexander
1d9f91cdd2 Merge branch 'paragraph_without_affiliated_keyword'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has failed
rust-test Build rust-test has failed
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-10-12 17:46:56 -04:00
Tom Alexander
68f8e04ee8 Add tests for affiliated keywords before elements that cannot have affiliated keywords. 2023-10-12 17:46:25 -04:00
Tom Alexander
c039e0d62c Keywords without affiliated keywords should be lower priority than paragraphs with affiliated keywords. 2023-10-12 17:32:29 -04:00
Tom Alexander
e6e3783ec6 Support parsing affiliated keywords as keywords. 2023-10-12 17:23:54 -04:00
Tom Alexander
5ae19e455d Remove unused imports. 2023-10-12 17:14:18 -04:00
Tom Alexander
f475754a71 Port the rest of the element parsers over. 2023-10-12 17:12:55 -04:00
Tom Alexander
767f44f94d Port comment. 2023-10-12 16:52:49 -04:00
Tom Alexander
1411aca7b5 Port footnote definition. 2023-10-12 16:48:50 -04:00
Tom Alexander
9ccdcaac24 Port dynamic block. 2023-10-12 16:46:15 -04:00
Tom Alexander
d1223dcdb7 Port paragraph parser to use ak_element. 2023-10-12 16:37:31 -04:00
Tom Alexander
59448a4f2c Port greater blocker over to ak_element!(). 2023-10-12 16:27:57 -04:00
Tom Alexander
5136880532 Fix tests. 2023-10-12 16:14:23 -04:00
Tom Alexander
8654cf5507 Fully port over the first parser. 2023-10-12 16:09:35 -04:00
Tom Alexander
6ca4dc8ffc Add a macro for calling parsers that take in the affiliated keywords. 2023-10-12 16:06:59 -04:00
Tom Alexander
a6f36ba679 Create a template for new element functions that will take in the affiliated keywords instead of re-parsing them multiple times. 2023-10-12 15:47:06 -04:00
Tom Alexander
176e37874e Remove the parser to parse affiliated keywords as regular keywords. 2023-10-12 15:25:26 -04:00
Tom Alexander
3208a04f7a Update project status in the README.
Some checks failed
rustfmt Build rustfmt has succeeded
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-10-12 14:13:54 -04:00
Tom Alexander
0d579263cb Remove affiliated keyword non-exit for paragraph. 2023-10-12 14:07:57 -04:00
Tom Alexander
c022b30110 Update org-mode. 2023-10-12 13:38:11 -04:00
Tom Alexander
7da4e4a29b Merge branch 'optional_value'
Some checks failed
rustfmt Build rustfmt has succeeded
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-10-11 19:14:46 -04:00
Tom Alexander
8bc942a26f Create artificial scopes for the optional value and mandatory value. 2023-10-11 19:14:32 -04:00
Tom Alexander
dff7550038 Remove prints. 2023-10-11 19:09:51 -04:00
Tom Alexander
c4edcb8c24 Fix parsing the optional value. 2023-10-11 19:07:47 -04:00
Tom Alexander
4a44d88461 Better error messages. 2023-10-11 19:05:39 -04:00
Tom Alexander
efc6bd11d9 Do not exit on first loop. 2023-10-11 18:59:13 -04:00
Tom Alexander
51429e3155 Handle optval when translating names. 2023-10-11 18:50:22 -04:00
Tom Alexander
b1a0fa4acf Compare optional value. 2023-10-11 18:44:21 -04:00
Tom Alexander
aeb2b6fe68 Parse out the optional value objects. 2023-10-11 18:29:07 -04:00
Tom Alexander
6679db98a8 Add comments. 2023-10-11 17:53:32 -04:00
Tom Alexander
5363324bbf Add a test showing we are not handling optional values properly. 2023-10-11 17:37:39 -04:00
Tom Alexander
dc9188dffc Better error logging when emacs errors out.
Some checks failed
rustfmt Build rustfmt has succeeded
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-10-11 17:22:01 -04:00
Tom Alexander
bd620ccd0d Format. 2023-10-11 16:32:08 -04:00
Tom Alexander
1947ae9f22 Merge branch 'document_properties'
Some checks failed
rustfmt Build rustfmt has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
rust-build Build rust-build has succeeded
rust-test Build rust-test has failed
2023-10-11 16:28:53 -04:00
Tom Alexander
3fcf1b3864 Compare Document additional properties. 2023-10-11 16:27:25 -04:00
Tom Alexander
d965dd6fd1 Implement get_additional_properties for Document. 2023-10-11 16:14:03 -04:00
Tom Alexander
42dcd41e48 Add tests for document properties. 2023-10-11 16:02:44 -04:00
117 changed files with 3235 additions and 1796 deletions

View File

@@ -0,0 +1,191 @@
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
name: clippy
spec:
pipelineSpec:
params:
- name: image-name
description: The name for the built image
type: string
- name: path-to-image-context
description: The path to the build context
type: string
- name: path-to-dockerfile
description: The path to the Dockerfile
type: string
- name: GIT_USER_NAME
description: The username for git
type: string
default: "fluxcdbot"
- name: GIT_USER_EMAIL
description: The email for git
type: string
default: "fluxcdbot@users.noreply.github.com"
tasks:
- name: do-stuff
taskSpec:
metadata: {}
stepTemplate:
image: alpine:3.18
name: ""
resources:
requests:
cpu: 10m
memory: 600Mi
workingDir: /workspace/source
steps:
- image: alpine:3.18
name: do-stuff-step
script: |
#!/usr/bin/env sh
echo "hello world"
- name: report-pending
taskRef:
name: gitea-set-status
runAfter:
- fetch-repository
params:
- name: CONTEXT
value: "$(params.JOB_NAME)"
- name: REPO_FULL_NAME
value: "$(params.REPO_OWNER)/$(params.REPO_NAME)"
- name: GITEA_HOST_URL
value: code.fizz.buzz
- name: SHA
value: "$(tasks.fetch-repository.results.commit)"
- name: DESCRIPTION
value: "Build $(params.JOB_NAME) has started"
- name: STATE
value: pending
- name: TARGET_URL
value: "https://tekton.fizz.buzz/#/namespaces/$(context.pipelineRun.namespace)/pipelineruns/$(context.pipelineRun.name)"
- name: fetch-repository
taskRef:
name: git-clone
workspaces:
- name: output
workspace: git-source
params:
- name: url
value: $(params.REPO_URL)
- name: revision
value: $(params.PULL_BASE_SHA)
- name: deleteExisting
value: "true"
- name: build-image
taskRef:
name: kaniko
params:
- name: IMAGE
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
- name: CONTEXT
value: $(params.path-to-image-context)
- name: DOCKERFILE
value: $(params.path-to-dockerfile)
- name: BUILDER_IMAGE
value: "gcr.io/kaniko-project/executor:v1.12.1"
- name: EXTRA_ARGS
value:
- --cache=true
- --cache-copy-layers
- --cache-repo=harbor.fizz.buzz/kanikocache/cache
- --use-new-run # Should result in a speed-up
- --reproducible # To remove timestamps so layer caching works.
- --snapshot-mode=redo
- --skip-unused-stages=true
- --registry-mirror=dockerhub.dockerhub.svc.cluster.local
workspaces:
- name: source
workspace: git-source
- name: dockerconfig
workspace: docker-credentials
runAfter:
- fetch-repository
- name: clippy
taskRef:
name: run-docker-image
workspaces:
- name: source
workspace: git-source
- name: cargo-cache
workspace: cargo-cache
runAfter:
- build-image
params:
- name: docker-image
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
finally:
- name: report-success
when:
- input: "$(tasks.status)"
operator: in
values: ["Succeeded", "Completed"]
taskRef:
name: gitea-set-status
params:
- name: CONTEXT
value: "$(params.JOB_NAME)"
- name: REPO_FULL_NAME
value: "$(params.REPO_OWNER)/$(params.REPO_NAME)"
- name: GITEA_HOST_URL
value: code.fizz.buzz
- name: SHA
value: "$(tasks.fetch-repository.results.commit)"
- name: DESCRIPTION
value: "Build $(params.JOB_NAME) has succeeded"
- name: STATE
value: success
- name: TARGET_URL
value: "https://tekton.fizz.buzz/#/namespaces/$(context.pipelineRun.namespace)/pipelineruns/$(context.pipelineRun.name)"
- name: report-failure
when:
- input: "$(tasks.status)"
operator: in
values: ["Failed"]
taskRef:
name: gitea-set-status
params:
- name: CONTEXT
value: "$(params.JOB_NAME)"
- name: REPO_FULL_NAME
value: "$(params.REPO_OWNER)/$(params.REPO_NAME)"
- name: GITEA_HOST_URL
value: code.fizz.buzz
- name: SHA
value: "$(tasks.fetch-repository.results.commit)"
- name: DESCRIPTION
value: "Build $(params.JOB_NAME) has failed"
- name: STATE
value: failure
- name: TARGET_URL
value: "https://tekton.fizz.buzz/#/namespaces/$(context.pipelineRun.namespace)/pipelineruns/$(context.pipelineRun.name)"
workspaces:
- name: git-source
- name: docker-credentials
workspaces:
- name: git-source
volumeClaimTemplate:
spec:
storageClassName: "nfs-client"
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
subPath: rust-source
- name: cargo-cache
persistentVolumeClaim:
claimName: organic-cargo-cache-clippy
- name: docker-credentials
secret:
secretName: harbor-plain
serviceAccountName: build-bot
timeout: 240h0m0s
params:
- name: image-name
value: "harbor.fizz.buzz/private/organic-clippy"
- name: path-to-image-context
value: docker/organic_clippy/
- name: path-to-dockerfile
value: docker/organic_clippy/Dockerfile

View File

@@ -137,7 +137,7 @@ spec:
value: []
- name: docker-image
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
- name: run-image-all
- name: run-image-tracing-compare
taskRef:
name: run-docker-image
workspaces:
@@ -152,6 +152,46 @@ spec:
value: ["--no-default-features", "--features", "tracing,compare"]
- name: docker-image
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
- name: run-image-compare-foreign
taskRef:
name: run-docker-image
workspaces:
- name: source
workspace: git-source
- name: cargo-cache
workspace: cargo-cache
runAfter:
- run-image-tracing-compare
params:
- name: args
value:
[
"--no-default-features",
"--features",
"compare,foreign_document_test",
]
- name: docker-image
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
- name: run-image-all
taskRef:
name: run-docker-image
workspaces:
- name: source
workspace: git-source
- name: cargo-cache
workspace: cargo-cache
runAfter:
- run-image-compare-foreign
params:
- name: args
value:
[
"--no-default-features",
"--features",
"tracing,compare,foreign_document_test",
]
- name: docker-image
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
finally:
- name: report-success
when:

View File

@@ -30,3 +30,10 @@ spec:
skip_branches:
# We already run on every commit, so running when the semver tags get pushed is causing needless double-processing.
- "^v[0-9]+\\.[0-9]+\\.[0-9]+$"
- name: clippy
source: "pipeline-clippy.yaml"
# Override https-based url from lighthouse events.
clone_uri: "git@code.fizz.buzz:talexander/organic.git"
skip_branches:
# We already run on every commit, so running when the semver tags get pushed is causing needless double-processing.
- "^v[0-9]+\\.[0-9]+\\.[0-9]+$"

View File

@@ -1,6 +1,8 @@
# cargo-features = ["profile-rustflags"]
[package]
name = "organic"
version = "0.1.9"
version = "0.1.12"
authors = ["Tom Alexander <tom@fizz.buzz>"]
description = "An org-mode parser."
edition = "2021"
@@ -31,7 +33,14 @@ path = "src/lib.rs"
path = "src/bin_compare.rs"
required-features = ["compare"]
[[bin]]
# This bin exists for development purposes only. The real target of this crate is the library.
name = "foreign_document_test"
path = "src/bin_foreign_document_test.rs"
required-features = ["foreign_document_test"]
[dependencies]
futures = { version = "0.3.28", optional = true }
nom = "7.1.1"
opentelemetry = { version = "0.20.0", optional = true, default-features = false, features = ["trace", "rt-tokio"] }
opentelemetry-otlp = { version = "0.13.0", optional = true }
@@ -40,14 +49,17 @@ tokio = { version = "1.30.0", optional = true, default-features = false, feature
tracing = { version = "0.1.37", optional = true }
tracing-opentelemetry = { version = "0.20.0", optional = true }
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
walkdir = { version = "2.3.3", optional = true }
[build-dependencies]
walkdir = "2.3.3"
[features]
default = []
compare = []
compare = ["tokio/process", "tokio/macros"]
foreign_document_test = ["compare", "dep:futures", "tokio/sync", "dep:walkdir", "tokio/process"]
tracing = ["dep:opentelemetry", "dep:opentelemetry-otlp", "dep:opentelemetry-semantic-conventions", "dep:tokio", "dep:tracing", "dep:tracing-opentelemetry", "dep:tracing-subscriber"]
event_count = []
# Optimized build for any sort of release.
[profile.release-lto]
@@ -55,6 +67,13 @@ inherits = "release"
lto = true
strip = "symbols"
# Optimized build for local execution.
# [profile.native]
# inherits = "release"
# lto = true
# strip = "symbols"
# rustflags = ["-C", "target-cpu=native"]
# Profile for performance testing with the "perf" tool. Notably keeps debug enabled and does not strip symbols to make reading the perf output easier.
[profile.perf]
inherits = "release"

View File

@@ -37,6 +37,18 @@ clean:
format:
> $(MAKE) -C docker/cargo_fmt run
.PHONY: dockerclippy
dockerclippy:
> $(MAKE) -C docker/organic_clippy run
.PHONY: clippy
clippy:
> cargo clippy --no-deps --all-targets --all-features -- -D warnings
.PHONY: clippyfix
clippyfix:
> cargo clippy --fix --lib -p organic --all-features
.PHONY: test
test:
> cargo test --no-default-features --features compare --no-fail-fast --lib --test test_loader -- --test-threads $(TESTJOBS)
@@ -52,6 +64,8 @@ buildtest:
> cargo build --no-default-features --features compare
> cargo build --no-default-features --features tracing
> cargo build --no-default-features --features compare,tracing
> cargo build --no-default-features --features compare,foreign_document_test
> cargo build --no-default-features --features compare,tracing,foreign_document_test
.PHONY: foreign_document_test
foreign_document_test:

View File

@@ -6,14 +6,13 @@ Organic is an emacs-less implementation of an [org-mode](https://orgmode.org/) p
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.
Currently, Organic parses most documents the same as the official org-mode parser. Most of the development right now is finding documents where the parsers differ and fixing those issues.
### 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.
- It must have a permissive license.
- 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

View File

@@ -14,7 +14,7 @@ use walkdir::WalkDir;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let destination = Path::new(&out_dir).join("tests.rs");
let mut test_file = File::create(&destination).unwrap();
let mut test_file = File::create(destination).unwrap();
// Re-generate the tests if any org-mode files change
println!("cargo:rerun-if-changed=org_mode_samples");
@@ -51,7 +51,7 @@ fn write_test(test_file: &mut File, test: &walkdir::DirEntry) {
.to_lowercase()
.strip_suffix(".org")
.expect("Should have .org extension")
.replace("/", "_");
.replace('/', "_");
write!(
test_file,
@@ -66,10 +66,6 @@ fn write_test(test_file: &mut File, test: &walkdir::DirEntry) {
}
#[cfg(feature = "compare")]
fn is_expect_fail(name: &str) -> Option<&str> {
match name {
"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,
}
fn is_expect_fail(_name: &str) -> Option<&str> {
None
}

View File

@@ -0,0 +1,5 @@
FROM rustlang/rust:nightly-alpine3.17
RUN apk add --no-cache musl-dev
ENTRYPOINT ["cargo", "clippy", "--no-deps", "--all-targets", "--all-features", "--", "-D", "warnings"]

View File

@@ -0,0 +1,37 @@
IMAGE_NAME:=organic-clippy
# REMOTE_REPO:=harbor.fizz.buzz/private
.PHONY: all
all: build push
.PHONY: build
build:
docker build -t $(IMAGE_NAME) -f Dockerfile .
.PHONY: push
push:
ifdef REMOTE_REPO
docker tag $(IMAGE_NAME) $(REMOTE_REPO)/$(IMAGE_NAME)
docker push $(REMOTE_REPO)/$(IMAGE_NAME)
else
@echo "REMOTE_REPO not defined, not pushing to a remote repo."
endif
.PHONY: clean
clean:
docker rmi $(IMAGE_NAME)
ifdef REMOTE_REPO
docker rmi $(REMOTE_REPO)/$(IMAGE_NAME)
else
@echo "REMOTE_REPO not defined, not removing from remote repo."
endif
docker volume rm cargo-cache
# NOTE: This target will write to folders underneath the git-root
.PHONY: run
run: build
docker run --rm --init --read-only --mount type=tmpfs,destination=/tmp -v "$$(readlink -f ../../):/source" --workdir=/source --mount source=cargo-cache,target=/usr/local/cargo/registry $(IMAGE_NAME)
.PHONY: shell
shell: build
docker run --rm -i -t --entrypoint /bin/sh --mount type=tmpfs,destination=/tmp -v "$$(readlink -f ../../):/source" --workdir=/source --mount source=cargo-cache,target=/usr/local/cargo/registry $(IMAGE_NAME)

View File

@@ -14,7 +14,7 @@ RUN make DESTDIR="/root/dist" install
FROM build AS build-org-mode
ARG ORG_VERSION=f32f5982a76217629dad8b8fd82d53212576aee6
ARG ORG_VERSION=abf5156096c06ee5aa05795c3dc5a065f76ada97
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.
@@ -102,6 +102,4 @@ COPY --from=foreign-document-gather /foreign_documents/doomemacs /foreign_docume
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"]
ENTRYPOINT ["cargo", "run", "--bin", "foreign_document_test", "--features", "compare,foreign_document_test", "--profile", "release-lto"]

View File

@@ -1,149 +0,0 @@
#!/usr/bin/env bash
#
# Run the Organic compare script against a series of documents sourced from exterior places.
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 "${@}")
}
function die {
local status_code="$1"
shift
(>&2 echo "${@}")
exit "$status_code"
}
function main {
cargo build --no-default-features --features compare --profile release-lto
if [ "${CARGO_TARGET_DIR:-}" = "" ]; then
CARGO_TARGET_DIR=$(realpath target/)
fi
PARSE="${CARGO_TARGET_DIR}/release-lto/compare"
local all_status=0
set +e
(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 [ "$all_status" -ne 0 ]; then
red_text "Some tests failed."
else
green_text "All tests passed."
fi
return "$all_status"
}
function green_text {
(IFS=' '; printf '\x1b[38;2;0;255;0m%s\x1b[0m' "${*}")
}
function red_text {
(IFS=' '; printf '\x1b[38;2;255;0;0m%s\x1b[0m' "${*}")
}
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

@@ -0,0 +1,42 @@
* Elisp Structure
| Keyword | Single | Double | Single Optval | Double Optval |
|---------+---------------+---------------+---------------+---------------|
| CAPTION | objtree | objtree | objtree | objtree |
| DATA | quoted(:name) | quoted(:name) | - | - |
| HEADER | list(quoted) | list(quoted) | - | - |
| NAME | quoted(:name) | quoted(:name) | - | - |
| PLOT | quoted(:plot) | quoted(:plot) | - | - |
| RESULTS | optional pair | optional pair | optional pair | optional pair |
* types
** objtree
Outer list: 1 per keyword
next list: first entry = list of objects for value. remaining entries = optval
** list(quoted)
List of quoted strings, 1 per keyword
** quoted(NAME)
Quoted string under the NAME property (for example quoted(:name))
** optional pair
When optval is supplied this is an alist with the field value being the real value and the 3nd value being the optval.
#+begin_src elisp
("*f*" . "*bar*")
#+end_src
When optval is not supplied this is a list containing a single string of the last occurrence of this keyword.
#+begin_src elisp
("*c*")
#+end_src
* Default settings
#+begin_src text
org-element-dual-keywords ("CAPTION" "RESULTS")
org-element-parsed-keywords ("CAPTION")
org-element-multiple-keywords ("CAPTION" "HEADER")
org-babel-results-keyword "RESULTS"
#+end_src
* Analysis
We don't have an example of a parsed non-dual keyword
Looks like multiple triggers list 1 per keyword
dual triggers support for optval
parsed triggers objects

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
#
set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
file_path="${DIR}/test_document.org"
for TARGET_VARIABLE in RESULTS CAPTION HEADER DATA NAME PLOT; do
INIT_SCRIPT=$(cat <<EOF
(progn
(erase-buffer)
(require 'org)
(defun org-table-align () t)
(setq vc-handled-backends nil)
(find-file "/input/${file_path}")
(org-mode)
(replace-regexp-in-region "foo" "${TARGET_VARIABLE}")
(message "%s" (pp-to-string (org-element-parse-buffer)))
)
EOF
)
docker run --init --rm -i --mount type=tmpfs,destination=/tmp -v "/:/input:ro" -w /input --entrypoint "" organic-test emacs -q --no-site-file --no-splash --batch --eval "$INIT_SCRIPT" 2> "${DIR}/${TARGET_VARIABLE}"
done
# exec docker run --init --rm -i -t --mount type=tmpfs,destination=/tmp -v "/:/input:ro" -w /input --entrypoint "" organic-test emacs -q --no-site-file --no-splash --eval "$INIT_SCRIPT"
# org-element-dual-keywords ("CAPTION" "RESULTS")
# org-element-parsed-keywords ("CAPTION")
# org-element-multiple-keywords ("CAPTION" "HEADER")
# org-babel-results-keyword "RESULTS"

View File

@@ -0,0 +1,25 @@
# Single instance
#+foo: *a*
#+begin_example
#+end_example
# Two instances
#+foo: *b*
#+foo: *c*
#+begin_example
#+end_example
# Single with optval
#+foo[*bar*]: *d*
#+begin_example
#+end_example
# Two with optval
#+foo[*bar*]: *e*
#+foo[*bar*]: *f*
#+begin_example
#+end_example

View File

@@ -0,0 +1,8 @@
#+CAPTION[foo]: *bar*
#+CAPTION[*lorem* ipsum]: dolar
1. baz
#+CAPTION[foo]: *bar*
#+CAPTION[*lorem* ipsum]: dolar
# Comments cannot have affiliated keywords so those become regular keywords.

View File

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

View File

@@ -0,0 +1,2 @@
foo
:end:

View File

@@ -0,0 +1,3 @@
1. foo
#+NAME: bar
2. baz

View File

@@ -0,0 +1,5 @@
* baz
# lorem
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,6 @@
* baz
# lorem
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,4 @@
* baz
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,7 @@
** Foo
DEADLINE: <2023-10-16 Mon>
:PROPERTIES:
:foo: *a*
:Bar: *b*
:BAZ: *c*
:END:

View File

@@ -0,0 +1,5 @@
* baz
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,13 @@
* baz
# lorem
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,19 @@
:PROPERTIES:
:foo: bar
:foo+: baz
:cat: dog
:END:
# Even though these are inheriting the properties and overwriting and/or appending to them, this is not represented in the parser AST so Organic does not do any special handling of this.
* Overwrite
:PROPERTIES:
:foo: lorem
:bat: car
:END:
* Append
:PROPERTIES:
:foo+: ipsum
:cake: lie
:END:

View File

@@ -0,0 +1,5 @@
* Overwrite
#+NAME: foo
:PROPERTIES:
:header-args: :var foo="lorem"
:END:

View File

@@ -1,25 +0,0 @@
# Blank lines and comments can come before property drawers in the zeroth section
:PROPERTIES:
:FOO: bar
:END:
* Spaces turn property drawers into regular drawers
:PROPERTIES:
:FOO: bar
:END:
* Comments turn property drawers into regular drawers
# Comment
:PROPERTIES:
:FOO: bar
:END:
* Baseline
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,4 @@
# lorem
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,5 @@
# lorem
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,3 @@
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,4 @@
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,12 @@
# lorem
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -1,12 +0,0 @@
# Blank lines and comments can come before property drawers in the zeroth section
:PROPERTIES:
:FOO: bar
:END:

View File

@@ -0,0 +1,3 @@
|foo|
#+NAME: bar
|baz |

View File

@@ -0,0 +1,16 @@
#+results[foo]: bar
#+results[lorem]: ipsum
#+begin_example
baz
#+end_example
#+caption[lorem]: ipsum
#+caption[foo]: bar
#+begin_example
baz
#+end_example
#+header[foo]: bar
#+begin_example
baz
#+end_example

View File

@@ -0,0 +1,3 @@
#+NAME: foo
* bar
#+NAME: baz

58
scripts/dump_ast.bash Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
#
# Dump the AST of an org-mode document from emacs
set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
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 {
if [ $# -eq 0 ]; then
dump_ast_stdin "${@}"
else
dump_ast_file "${@}"
fi
}
function dump_ast_stdin {
# Until we can find a good way to encode stdin as an elisp string in bash, I cannot operate on stdin.
die 1 "This script only works on files."
}
function dump_ast_file {
local target_file mounted_file elisp_script
target_file=$($REALPATH "$1")
mounted_file="/input${target_file}"
elisp_script=$(cat <<EOF
(progn
(erase-buffer)
(require 'org)
(defun org-table-align () t)
(find-file-read-only "${mounted_file}")
(org-mode)
(message "%s" (pp-to-string (org-element-parse-buffer)))
)
EOF
)
exec docker run --init --rm -i --mount type=tmpfs,destination=/tmp -v "/:/input:ro" --entrypoint "" organic-test emacs -q --no-site-file --no-splash --batch --eval "$elisp_script"
}
main "${@}"

View File

@@ -14,7 +14,7 @@ function main {
additional_flags+=(--profile "$PROFILE")
fi
(cd "$DIR/../" && cargo build --no-default-features "${additional_flags[@]}")
perf record --freq=2000 --call-graph dwarf --output="$DIR/../perf.data" "$DIR/../target/${PROFILE}/parse" "${@}"
perf record --freq=70000 --call-graph dwarf --output="$DIR/../perf.data" "$DIR/../target/${PROFILE}/parse" "${@}"
# Convert to a format firefox will read
# flags to consider --show-info

View File

@@ -14,30 +14,40 @@ mod init_tracing;
#[cfg(not(feature = "tracing"))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
main_body()
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let main_body_result = main_body().await;
main_body_result
})
}
#[cfg(feature = "tracing")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
let result = rt.block_on(async {
rt.block_on(async {
init_telemetry()?;
let main_body_result = main_body();
let main_body_result = main_body().await;
shutdown_telemetry()?;
main_body_result
});
result
})
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn main_body() -> Result<(), Box<dyn std::error::Error>> {
async 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)
if run_anonymous_compare(org_contents).await? {
} else {
Err("Diff results do not match.")?;
}
Ok(())
} else {
for arg in args {
run_compare_on_file(arg)?
if run_compare_on_file(arg).await? {
} else {
Err("Diff results do not match.")?;
}
}
Ok(())
}

View File

@@ -0,0 +1,394 @@
#![feature(round_char_boundary)]
#![feature(exact_size_is_empty)]
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use futures::future::BoxFuture;
use futures::future::FutureExt;
use organic::compare::silent_compare_on_file;
use tokio::sync::Semaphore;
use tokio::task::JoinError;
use walkdir::WalkDir;
#[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<ExitCode, Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
let main_body_result = main_body().await;
main_body_result
})
}
#[cfg(feature = "tracing")]
fn main() -> Result<ExitCode, Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async {
init_telemetry()?;
let main_body_result = main_body().await;
shutdown_telemetry()?;
main_body_result
})
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
async fn main_body() -> Result<ExitCode, Box<dyn std::error::Error>> {
let layer = compare_group("org-mode", || {
compare_all_org_document("/foreign_documents/org-mode")
});
let layer = layer.chain(compare_group("emacs", || {
compare_all_org_document("/foreign_documents/emacs")
}));
let layer = layer.chain(compare_group("worg", || {
compare_all_org_document("/foreign_documents/worg")
}));
let layer = layer.chain(compare_group("howard_abrams", compare_howard_abrams));
let layer = layer.chain(compare_group("doomemacs", || {
compare_all_org_document("/foreign_documents/doomemacs")
}));
let running_tests: Vec<_> = layer.map(|c| tokio::spawn(c.run_test())).collect();
let mut any_failed = false;
for test in running_tests.into_iter() {
let test_result = test.await??;
if test_result.is_immediately_bad() || test_result.has_bad_children() {
any_failed = true;
}
test_result.print();
}
if any_failed {
println!(
"{color}Some tests failed.{reset}",
color = TestResult::foreground_color(255, 0, 0),
reset = TestResult::reset_color(),
);
Ok(ExitCode::FAILURE)
} else {
println!(
"{color}All tests passed.{reset}",
color = TestResult::foreground_color(0, 255, 0),
reset = TestResult::reset_color(),
);
Ok(ExitCode::SUCCESS)
}
}
fn compare_howard_abrams() -> impl Iterator<Item = TestConfig> {
let layer = compare_group("dot-files", || {
compare_all_org_document("/foreign_documents/howardabrams/dot-files")
});
let layer = layer.chain(compare_group("hamacs", || {
compare_all_org_document("/foreign_documents/howardabrams/hamacs")
}));
let layer = layer.chain(compare_group("demo-it", || {
compare_all_org_document("/foreign_documents/howardabrams/demo-it")
}));
let layer = layer.chain(compare_group("magit-demo", || {
compare_all_org_document("/foreign_documents/howardabrams/magit-demo")
}));
let layer = layer.chain(compare_group("pdx-emacs-hackers", || {
compare_all_org_document("/foreign_documents/howardabrams/pdx-emacs-hackers")
}));
let layer = layer.chain(compare_group("flora-simulator", || {
compare_all_org_document("/foreign_documents/howardabrams/flora-simulator")
}));
let layer = layer.chain(compare_group("literate-devops-demo", || {
compare_all_org_document("/foreign_documents/howardabrams/literate-devops-demo")
}));
let layer = layer.chain(compare_group("clojure-yesql-xp", || {
compare_all_org_document("/foreign_documents/howardabrams/clojure-yesql-xp")
}));
layer.chain(compare_group("veep", || {
compare_all_org_document("/foreign_documents/howardabrams/veep")
}))
}
fn compare_group<N: Into<String>, F: Fn() -> I, I: Iterator<Item = TestConfig>>(
name: N,
inner: F,
) -> impl Iterator<Item = TestConfig> {
std::iter::once(TestConfig::TestLayer(TestLayer {
name: name.into(),
children: inner().collect(),
}))
}
fn compare_all_org_document<P: AsRef<Path>>(root_dir: P) -> impl Iterator<Item = TestConfig> {
let root_dir = root_dir.as_ref();
let mut test_files = WalkDir::new(root_dir)
.into_iter()
.filter(|e| match e {
Ok(dir_entry) => {
dir_entry.file_type().is_file()
&& Path::new(dir_entry.file_name())
.extension()
.map(|ext| ext.to_ascii_lowercase() == "org")
.unwrap_or(false)
}
Err(_) => true,
})
.collect::<Result<Vec<_>, _>>()
.unwrap();
test_files.sort_by_cached_key(|test_file| PathBuf::from(test_file.path()));
let test_configs: Vec<_> = test_files
.into_iter()
.map(|test_file| {
let name = test_file
.path()
.strip_prefix(root_dir)
.expect("Result is from walkdir so it must be below the root directory.")
.as_os_str()
.to_string_lossy()
.into_owned();
TestConfig::SingleFile(SingleFile {
name,
file_path: test_file.into_path(),
})
})
.collect();
test_configs.into_iter()
}
static TEST_PERMITS: Semaphore = Semaphore::const_new(8);
#[derive(Debug)]
enum TestConfig {
TestLayer(TestLayer),
SingleFile(SingleFile),
}
#[derive(Debug)]
struct TestLayer {
name: String,
children: Vec<TestConfig>,
}
#[derive(Debug)]
struct SingleFile {
name: String,
file_path: PathBuf,
}
#[derive(Debug)]
enum TestResult {
ResultLayer(ResultLayer),
SingleFileResult(SingleFileResult),
}
#[derive(Debug)]
struct ResultLayer {
name: String,
children: Vec<TestResult>,
}
#[derive(Debug)]
struct SingleFileResult {
name: String,
status: TestStatus,
}
#[derive(Debug)]
pub(crate) enum TestStatus {
Pass,
Fail,
}
impl TestConfig {
fn run_test(self) -> BoxFuture<'static, Result<TestResult, JoinError>> {
async move {
match self {
TestConfig::TestLayer(test) => Ok(TestResult::ResultLayer(test.run_test().await?)),
TestConfig::SingleFile(test) => {
Ok(TestResult::SingleFileResult(test.run_test().await?))
}
}
}
.boxed()
}
}
impl SingleFile {
async fn run_test(self) -> Result<SingleFileResult, JoinError> {
let _permit = TEST_PERMITS.acquire().await.unwrap();
let result = silent_compare_on_file(&self.file_path).await;
Ok(SingleFileResult {
name: self.name,
status: if let Ok(true) = result {
TestStatus::Pass
} else {
TestStatus::Fail
},
})
}
}
impl TestLayer {
async fn run_test(self) -> Result<ResultLayer, JoinError> {
let running_children: Vec<_> = self
.children
.into_iter()
.map(|c| tokio::spawn(c.run_test()))
.collect();
let mut children = Vec::with_capacity(running_children.len());
for c in running_children {
children.push(c.await??);
}
Ok(ResultLayer {
name: self.name,
children,
})
}
}
impl TestResult {
pub fn print(&self) {
self.print_indented(0);
}
fn print_indented(&self, indentation: usize) {
match self {
TestResult::ResultLayer(result) => result.print_indented(indentation),
TestResult::SingleFileResult(result) => result.print_indented(indentation),
}
}
fn has_bad_children(&self) -> bool {
match self {
TestResult::ResultLayer(result) => result.has_bad_children(),
TestResult::SingleFileResult(result) => result.has_bad_children(),
}
}
fn is_immediately_bad(&self) -> bool {
match self {
TestResult::ResultLayer(result) => result.is_immediately_bad(),
TestResult::SingleFileResult(result) => result.is_immediately_bad(),
}
}
pub(crate) fn foreground_color(red: u8, green: u8, blue: u8) -> String {
if TestResult::should_use_color() {
format!(
"\x1b[38;2;{red};{green};{blue}m",
red = red,
green = green,
blue = blue
)
} else {
String::new()
}
}
#[allow(dead_code)]
pub(crate) fn background_color(red: u8, green: u8, blue: u8) -> String {
if TestResult::should_use_color() {
format!(
"\x1b[48;2;{red};{green};{blue}m",
red = red,
green = green,
blue = blue
)
} else {
String::new()
}
}
pub(crate) fn reset_color() -> &'static str {
if TestResult::should_use_color() {
"\x1b[0m"
} else {
""
}
}
fn should_use_color() -> bool {
!std::env::var("NO_COLOR").is_ok_and(|val| !val.is_empty())
}
}
impl SingleFileResult {
fn print_indented(&self, indentation: usize) {
match self.status {
TestStatus::Pass => {
println!(
"{indentation}{color}PASS{reset} {name}",
indentation = " ".repeat(indentation),
color = TestResult::foreground_color(0, 255, 0),
reset = TestResult::reset_color(),
name = self.name
);
}
TestStatus::Fail => {
println!(
"{indentation}{color}FAIL{reset} {name}",
indentation = " ".repeat(indentation),
color = TestResult::foreground_color(255, 0, 0),
reset = TestResult::reset_color(),
name = self.name
);
}
}
}
fn has_bad_children(&self) -> bool {
false
}
fn is_immediately_bad(&self) -> bool {
match self.status {
TestStatus::Pass => false,
TestStatus::Fail => true,
}
}
}
impl ResultLayer {
fn print_indented(&self, indentation: usize) {
if self.is_immediately_bad() {
println!(
"{indentation}{color}FAIL{reset} {name}",
indentation = " ".repeat(indentation),
color = TestResult::foreground_color(255, 0, 0),
reset = TestResult::reset_color(),
name = self.name
);
} else if self.has_bad_children() {
println!(
"{indentation}{color}BADCHILD{reset} {name}",
indentation = " ".repeat(indentation),
color = TestResult::foreground_color(255, 255, 0),
reset = TestResult::reset_color(),
name = self.name
);
} else {
println!(
"{indentation}{color}PASS{reset} {name}",
indentation = " ".repeat(indentation),
color = TestResult::foreground_color(0, 255, 0),
reset = TestResult::reset_color(),
name = self.name
);
}
self.children
.iter()
.for_each(|result| result.print_indented(indentation + 1));
}
fn has_bad_children(&self) -> bool {
self.children
.iter()
.any(|result| result.is_immediately_bad() || result.has_bad_children())
}
fn is_immediately_bad(&self) -> bool {
false
}
}

View File

@@ -12,39 +12,60 @@ 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>>(
pub async 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())
) -> Result<bool, Box<dyn std::error::Error>> {
run_anonymous_compare_with_settings(org_contents, &GlobalSettings::default(), false).await
}
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 async fn run_compare_on_file<P: AsRef<Path>>(
org_path: P,
) -> Result<bool, Box<dyn std::error::Error>> {
run_compare_on_file_with_settings(org_path, &GlobalSettings::default(), false).await
}
pub fn run_anonymous_compare_with_settings<P: AsRef<str>>(
pub async fn silent_anonymous_compare<P: AsRef<str>>(
org_contents: P,
global_settings: &GlobalSettings,
) -> Result<(), Box<dyn std::error::Error>> {
) -> Result<bool, Box<dyn std::error::Error>> {
run_anonymous_compare_with_settings(org_contents, &GlobalSettings::default(), true).await
}
pub async fn silent_compare_on_file<P: AsRef<Path>>(
org_path: P,
) -> Result<bool, Box<dyn std::error::Error>> {
run_compare_on_file_with_settings(org_path, &GlobalSettings::default(), true).await
}
pub async fn run_anonymous_compare_with_settings<'g, 's, P: AsRef<str>>(
org_contents: P,
global_settings: &GlobalSettings<'g, 's>,
silent: bool,
) -> Result<bool, 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()?;
if !silent {
print_versions().await?;
}
let rust_parsed = parse_with_settings(org_contents, global_settings)?;
let org_sexp = emacs_parse_anonymous_org_document(org_contents, global_settings)?;
let org_sexp = emacs_parse_anonymous_org_document(org_contents, global_settings).await?;
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);
if !silent {
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 !silent {
diff_result.print(org_contents)?;
}
if diff_result.is_bad() {
Err("Diff results do not match.")?;
} else {
return Ok(false);
} else if !silent {
println!(
"{color}Entire document passes.{reset}",
color = DiffResult::foreground_color(0, 255, 0),
@@ -52,15 +73,18 @@ pub fn run_anonymous_compare_with_settings<P: AsRef<str>>(
);
}
Ok(())
Ok(true)
}
pub fn run_compare_on_file_with_settings<P: AsRef<Path>>(
pub async fn run_compare_on_file_with_settings<'g, 's, P: AsRef<Path>>(
org_path: P,
global_settings: &GlobalSettings,
) -> Result<(), Box<dyn std::error::Error>> {
global_settings: &GlobalSettings<'g, 's>,
silent: bool,
) -> Result<bool, Box<dyn std::error::Error>> {
let org_path = org_path.as_ref();
print_versions()?;
if !silent {
print_versions().await?;
}
let parent_directory = org_path
.parent()
.ok_or("Should be contained inside a directory.")?;
@@ -77,20 +101,24 @@ pub fn run_compare_on_file_with_settings<P: AsRef<Path>>(
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 org_sexp = emacs_parse_file_org_document(org_path, &global_settings).await?;
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);
if !silent {
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 !silent {
diff_result.print(org_contents)?;
}
if diff_result.is_bad() {
Err("Diff results do not match.")?;
} else {
return Ok(false);
} else if !silent {
println!(
"{color}Entire document passes.{reset}",
color = DiffResult::foreground_color(0, 255, 0),
@@ -98,11 +126,14 @@ pub fn run_compare_on_file_with_settings<P: AsRef<Path>>(
);
}
Ok(())
Ok(true)
}
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());
async fn print_versions() -> Result<(), Box<dyn std::error::Error>> {
eprintln!("Using emacs version: {}", get_emacs_version().await?.trim());
eprintln!(
"Using org-mode version: {}",
get_org_mode_version().await?.trim()
);
Ok(())
}

View File

@@ -16,7 +16,6 @@ use super::util::get_property_unquoted_atom;
use crate::types::AstNode;
use crate::types::CharOffsetInLine;
use crate::types::LineNumber;
use crate::types::Object;
use crate::types::RetainLabels;
use crate::types::SwitchNumberLines;
@@ -57,11 +56,11 @@ impl<'b, 's> ComparePropertiesResult<'b, 's> {
/// Do no comparison.
///
/// This is for when you want to acknowledge that a field exists in the emacs token, but you do not have any validation for it when using the compare_properties!() macro. Ideally, this should be kept to a minimum since this represents untested values.
pub(crate) fn compare_noop<'b, 's, 'x, R, RG>(
pub(crate) fn compare_noop<'b, 's, R, RG>(
_source: &'s str,
_emacs: &'b Token<'s>,
_rust_node: R,
_emacs_field: &'x str,
_emacs_field: &str,
_rust_value_getter: RG,
) -> Result<ComparePropertiesResult<'b, 's>, Box<dyn std::error::Error>> {
Ok(ComparePropertiesResult::NoChange)
@@ -70,18 +69,16 @@ pub(crate) fn compare_noop<'b, 's, 'x, R, RG>(
/// Do no comparison.
///
/// This is for when you want to acknowledge that a field exists in the emacs token, but you do not have any validation for it when using the compare_properties!() macro. Ideally, this should be kept to a minimum since this represents untested values.
pub(crate) fn compare_identity() -> () {
()
}
pub(crate) fn compare_identity() {}
/// Assert that the emacs value is always nil or absent.
///
/// This is usually used for fields which, in my testing, are always nil. Using this compare function instead of simply doing a compare_noop will enable us to be alerted when we finally come across an org-mode document that has a value other than nil for the property.
pub(crate) fn compare_property_always_nil<'b, 's, 'x, R, RG>(
pub(crate) fn compare_property_always_nil<'b, 's, R, RG>(
_source: &'s str,
emacs: &'b Token<'s>,
_rust_node: R,
emacs_field: &'x str,
emacs_field: &str,
_rust_value_getter: RG,
) -> Result<ComparePropertiesResult<'b, 's>, Box<dyn std::error::Error>> {
let value = get_property(emacs, emacs_field)?;
@@ -100,7 +97,6 @@ pub(crate) fn compare_property_always_nil<'b, 's, 'x, R, RG>(
pub(crate) fn compare_property_quoted_string<
'b,
's,
'x,
R,
RV: AsRef<str> + std::fmt::Debug,
RG: Fn(R) -> Option<RV>,
@@ -108,12 +104,12 @@ pub(crate) fn compare_property_quoted_string<
_source: &'s str,
emacs: &'b Token<'s>,
rust_node: R,
emacs_field: &'x str,
emacs_field: &str,
rust_value_getter: RG,
) -> Result<ComparePropertiesResult<'b, 's>, Box<dyn std::error::Error>> {
let value = get_property_quoted_string(emacs, emacs_field)?;
let rust_value = rust_value_getter(rust_node);
if rust_value.as_ref().map(|s| s.as_ref()) != value.as_ref().map(String::as_str) {
if rust_value.as_ref().map(|s| s.as_ref()) != value.as_deref() {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
@@ -181,7 +177,6 @@ where
pub(crate) fn compare_property_list_of_quoted_string<
'b,
's,
'x,
R,
RV: AsRef<str> + std::fmt::Debug,
RI: Iterator<Item = RV>,
@@ -190,7 +185,7 @@ pub(crate) fn compare_property_list_of_quoted_string<
_source: &'s str,
emacs: &'b Token<'s>,
rust_node: R,
emacs_field: &'x str,
emacs_field: &str,
rust_value_getter: RG,
) -> Result<ComparePropertiesResult<'b, 's>, Box<dyn std::error::Error>> {
let value = get_property(emacs, emacs_field)?
@@ -272,10 +267,7 @@ pub(crate) fn compare_property_set_of_quoted_string<
.map(unquote)
.collect::<Result<Vec<_>, _>>()?;
let value: BTreeSet<&str> = value.iter().map(|e| e.as_str()).collect();
let mismatched: Vec<_> = value
.symmetric_difference(&rust_value)
.map(|val| *val)
.collect();
let mismatched: Vec<_> = value.symmetric_difference(&rust_value).copied().collect();
if !mismatched.is_empty() {
let this_status = DiffStatus::Bad;
let message = Some(format!(
@@ -288,11 +280,111 @@ pub(crate) fn compare_property_set_of_quoted_string<
Ok(ComparePropertiesResult::NoChange)
}
pub(crate) fn compare_property_boolean<'b, 's, 'x, R, RG: Fn(R) -> bool>(
pub(crate) fn compare_property_optional_pair<
'b,
's,
R,
RV: AsRef<str> + std::fmt::Debug,
ROV: AsRef<str> + std::fmt::Debug,
RG: Fn(R) -> Option<(Option<ROV>, RV)>,
>(
_source: &'s str,
emacs: &'b Token<'s>,
rust_node: R,
emacs_field: &'x str,
emacs_field: &str,
rust_value_getter: RG,
) -> Result<ComparePropertiesResult<'b, 's>, Box<dyn std::error::Error>> {
let value = get_property(emacs, emacs_field)?
.map(Token::as_list)
.map_or(Ok(None), |r| r.map(Some))?;
let rust_value = rust_value_getter(rust_node);
match (value, &rust_value) {
(None, None) => {}
(None, Some(_)) | (Some(_), None) => {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
(Some(el), Some((Some(_), _))) if el.len() != 3 => {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
(Some(el), Some((None, _))) if el.len() != 1 => {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
(Some(el), Some((Some(orl), rl))) => {
let e = el
.first()
.map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))?
.map(unquote)
.map_or(Ok(None), |r| r.map(Some))?
.expect("Above match proved length to be 3.");
let oe = el
.get(2)
.map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))?
.map(unquote)
.map_or(Ok(None), |r| r.map(Some))?
.expect("Above match proved length to be 3.");
let r = rl.as_ref();
let or = orl.as_ref();
if e != r {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}. Full list: {:?} != {:?}",
emacs_field, e, r, value, rust_value
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
if oe != or {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}. Full list: {:?} != {:?}",
emacs_field, e, r, value, rust_value
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
}
(Some(el), Some((None, rl))) => {
let e = el
.first()
.map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))?
.map(unquote)
.map_or(Ok(None), |r| r.map(Some))?
.expect("Above match proved length to be 1.");
let r = rl.as_ref();
if e != r {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}. Full list: {:?} != {:?}",
emacs_field, e, r, value, rust_value
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
}
}
Ok(ComparePropertiesResult::NoChange)
}
pub(crate) fn compare_property_boolean<'b, 's, R, RG: Fn(R) -> bool>(
_source: &'s str,
emacs: &'b Token<'s>,
rust_node: R,
emacs_field: &str,
rust_value_getter: RG,
) -> Result<ComparePropertiesResult<'b, 's>, Box<dyn std::error::Error>> {
// get_property already converts nil to None.
@@ -376,14 +468,7 @@ where
match (value, rust_value) {
(None, None) => {}
(Some(el), None)
if el.len() == 1
&& el.into_iter().all(|t| {
if let Ok(r#""""#) = t.as_atom() {
true
} else {
false
}
}) => {}
if el.len() == 1 && el.iter().all(|t| matches!(t.as_atom(), Ok(r#""""#))) => {}
(None, rv @ Some(_)) | (Some(_), rv @ None) => {
let this_status = DiffStatus::Bad;
let message = Some(format!(
@@ -412,79 +497,139 @@ where
Ok(ComparePropertiesResult::NoChange)
}
/// Special compare used for affiliate keywords that are parsed as objects.
///
/// Org-mode seems to store these as a 3-deep list:
/// - Outer list with 1 element per #+caption keyword (or other parsed keyword).
/// - Middle list that seems to always have 1 element.
/// - Inner list of the objects from each #+caption keyword (or other parsed keyword).
pub(crate) fn compare_property_list_of_list_of_list_of_ast_nodes<
pub(crate) fn compare_property_object_tree<
'b,
's,
'x,
R,
RG: Fn(R) -> Option<&'b Vec<Vec<Object<'s>>>>,
RV: std::fmt::Debug + 'b,
ROV: std::fmt::Debug + 'b,
RI: Iterator<Item = &'b (Option<Vec<ROV>>, Vec<RV>)> + ExactSizeIterator + std::fmt::Debug,
RG: Fn(R) -> Option<RI>,
>(
source: &'s str,
emacs: &'b Token<'s>,
rust_node: R,
emacs_field: &'x str,
rust_value_getter: RG,
) -> Result<ComparePropertiesResult<'b, 's>, Box<dyn std::error::Error>> {
// TODO: Replace Object<'s> with generics. I hard-coded Object in to make lifetimes easier.
let rust_value = rust_value_getter(rust_node);
) -> Result<ComparePropertiesResult<'b, 's>, Box<dyn std::error::Error>>
where
AstNode<'b, 's>: From<&'b RV>,
AstNode<'b, 's>: From<&'b ROV>,
{
let value = get_property(emacs, emacs_field)?
.map(Token::as_list)
.map_or(Ok(None), |r| r.map(Some))?;
let (value, rust_value) = match (value, rust_value) {
let rust_value = rust_value_getter(rust_node);
let (outer_emacs_list, outer_rust_list) = match (value, rust_value) {
(None, None) => {
return Ok(ComparePropertiesResult::NoChange);
}
(None, Some(_)) | (Some(_), None) => {
(None, rv @ Some(_)) | (Some(_), rv @ None) => {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
emacs_field, value, rv
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
(Some(value), Some(rust_value)) if value.len() != rust_value.len() => {
(Some(el), Some(rl)) if el.len() != rl.len() => {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
emacs_field, el, rl
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
(Some(value), Some(rust_value)) => (value, rust_value),
(Some(el), Some(rl)) => (el, rl),
};
let mut full_status: Vec<DiffEntry<'b, 's>> = Vec::with_capacity(outer_rust_list.len());
// Iterate the outer lists
for (value, rust_value) in value.iter().zip(rust_value.iter()) {
// Assert the middle list is a length of 1 because I've never seen it any other way.
let value = value.as_list()?;
if value.len() != 1 {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
for (kw_e, kw_r) in outer_emacs_list.iter().zip(outer_rust_list) {
let kw_e = kw_e.as_list()?;
let child_status_length = kw_r.1.len() + kw_r.0.as_ref().map(|opt| opt.len()).unwrap_or(0);
let mut child_status: Vec<DiffEntry<'b, 's>> = Vec::with_capacity(child_status_length);
if let Some(or) = &kw_r.0 {
// if optional value
let mut kw_e = kw_e.iter();
// First element is a list representing the mandatory value.
if let Some(val_e) = kw_e.next() {
let el = val_e.as_list()?;
if el.len() != kw_r.1.len() {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, kw_e, kw_r
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
for (e, r) in el.iter().zip(kw_r.1.iter()) {
child_status.push(compare_ast_node(source, e, r.into())?);
}
} else {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, kw_e, kw_r
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
// Remaining elements are the optional value.
if kw_e.len() != or.len() {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, kw_e, kw_r
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
for (e, r) in kw_e.zip(or.iter()) {
child_status.push(compare_ast_node(source, e, r.into())?);
}
} else {
// if no optional value
if !kw_e.len() == 1 {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, kw_e, kw_r
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
let e = kw_e
.first()
.map(Token::as_list)
.map_or(Ok(None), |r| r.map(Some))?
.expect("The above if-statement proves this will be Some.")
.iter();
let r = kw_r.1.iter();
if e.len() != r.len() {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, kw_e, kw_r
));
return Ok(ComparePropertiesResult::SelfChange(this_status, message));
}
for (e, r) in e.zip(r) {
child_status.push(compare_ast_node(source, e, r.into())?);
}
}
// Drill past the middle list to the inner list.
let value = value
.first()
.expect("The above if-statement asserts this exists.");
let value = value.as_list()?;
// Compare inner lists
let mut child_status: Vec<DiffEntry<'b, 's>> = Vec::with_capacity(rust_value.len());
for (e, r) in value.iter().zip(rust_value) {
child_status.push(compare_ast_node(source, e, r.into())?);
if !child_status.is_empty() {
let diff_scope = artificial_diff_scope("mandatory value", child_status)?;
full_status.push(diff_scope);
}
let diff_scope = artificial_owned_diff_scope(emacs_field, child_status)?;
return Ok(ComparePropertiesResult::DiffEntry(diff_scope));
}
Ok(ComparePropertiesResult::NoChange)
if full_status.is_empty() {
Ok(ComparePropertiesResult::NoChange)
} else {
let diff_scope = artificial_owned_diff_scope(emacs_field, full_status)?;
Ok(ComparePropertiesResult::DiffEntry(diff_scope))
}
}
pub(crate) fn compare_property_number_lines<

View File

@@ -227,7 +227,7 @@ impl<'b, 's> DiffResult<'b, 's> {
status_text = status_text,
name = self.name,
char_offset = preceding_text.chars().count() + 1,
message = self.message.as_ref().map(|m| m.as_str()).unwrap_or("")
message = self.message.as_deref().unwrap_or("")
);
for child in self.children.iter() {
child.print_indented(indentation + 1, original_document)?;
@@ -330,8 +330,8 @@ pub(crate) fn artificial_diff_scope<'b, 's>(
.into())
}
pub(crate) fn artificial_owned_diff_scope<'b, 's, 'x>(
name: &'x str,
pub(crate) fn artificial_owned_diff_scope<'b, 's>(
name: &str,
children: Vec<DiffEntry<'b, 's>>,
) -> Result<DiffEntry<'b, 's>, Box<dyn std::error::Error>> {
Ok(DiffLayer {
@@ -426,14 +426,9 @@ pub(crate) fn compare_ast_node<'b, 's>(
// PlainText is a special case because upstream Org-Mode uses relative values for the bounds in plaintext rather than absolute so the below checks do not account for that.
if let AstNode::PlainText(_) = rust {
} else {
match compare_standard_properties(source, emacs, &rust) {
Err(err) => {
compare_result.status = DiffStatus::Bad;
compare_result.message = Some(err.to_string())
}
Ok(_) => {}
}
} else if let Err(err) = compare_standard_properties(source, emacs, &rust) {
compare_result.status = DiffStatus::Bad;
compare_result.message = Some(err.to_string())
}
Ok(compare_result.into())
@@ -455,6 +450,27 @@ fn _compare_document<'b, 's>(
let mut child_status = Vec::new();
let mut message = None;
let additional_property_names: Vec<String> = rust
.get_additional_properties()
.map(|node_property| format!(":{}", node_property.property_name.to_uppercase()))
.collect();
let additional_properties: Vec<(String, &str)> = rust
.get_additional_properties()
.map(|node_property| {
(
format!(":{}", node_property.property_name.to_uppercase()),
node_property.value.unwrap_or(""),
)
})
.collect();
compare_additional_properties(emacs, additional_properties.into_iter())?.apply(
&mut child_status,
&mut this_status,
&mut message,
);
compare_children_iter(
source,
emacs,
@@ -468,9 +484,13 @@ fn _compare_document<'b, 's>(
source,
emacs,
rust,
additional_property_names
.iter()
.map(String::as_str)
.map(EmacsField::Required),
(
EmacsField::Required(":path"),
|r| r.path.as_ref().map(|p| p.to_str()).flatten(),
|r| r.path.as_ref().and_then(|p| p.to_str()),
compare_property_quoted_string
),
(

View File

@@ -1,3 +1,4 @@
#[allow(clippy::module_inception)]
mod compare;
mod compare_field;
mod diff;
@@ -10,3 +11,5 @@ 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;
pub use compare::silent_anonymous_compare;
pub use compare::silent_compare_on_file;

View File

@@ -1,5 +1,6 @@
use std::path::Path;
use std::process::Command;
use tokio::process::Command;
use crate::context::HeadlineLevelFilter;
use crate::settings::GlobalSettings;
@@ -25,9 +26,9 @@ fn global_settings_elisp(global_settings: &GlobalSettings) -> String {
ret
}
pub(crate) fn emacs_parse_anonymous_org_document<C>(
pub(crate) async fn emacs_parse_anonymous_org_document<'g, 's, C>(
file_contents: C,
global_settings: &GlobalSettings,
global_settings: &GlobalSettings<'g, 's>,
) -> Result<String, Box<dyn std::error::Error>>
where
C: AsRef<str>,
@@ -54,15 +55,24 @@ where
.arg("--batch")
.arg("--eval")
.arg(elisp_script);
let out = cmd.output()?;
out.status.exit_ok()?;
let out = cmd.output().await?;
let status = out.status.exit_ok();
if status.is_err() {
eprintln!(
"Emacs errored out: {}\n{}",
String::from_utf8(out.stdout)?,
String::from_utf8(out.stderr)?
);
status?;
unreachable!();
}
let org_sexp = out.stderr;
Ok(String::from_utf8(org_sexp)?)
}
pub(crate) fn emacs_parse_file_org_document<P>(
pub(crate) async fn emacs_parse_file_org_document<'g, 's, P>(
file_path: P,
global_settings: &GlobalSettings,
global_settings: &GlobalSettings<'g, 's>,
) -> Result<String, Box<dyn std::error::Error>>
where
P: AsRef<Path>,
@@ -97,8 +107,17 @@ where
.arg("--batch")
.arg("--eval")
.arg(elisp_script);
let out = cmd.output()?;
out.status.exit_ok()?;
let out = cmd.output().await?;
let status = out.status.exit_ok();
if status.is_err() {
eprintln!(
"Emacs errored out: {}\n{}",
String::from_utf8(out.stdout)?,
String::from_utf8(out.stderr)?
);
status?;
unreachable!();
}
let org_sexp = out.stderr;
Ok(String::from_utf8(org_sexp)?)
}
@@ -125,7 +144,7 @@ where
output
}
pub fn get_emacs_version() -> Result<String, Box<dyn std::error::Error>> {
pub async fn get_emacs_version() -> Result<String, Box<dyn std::error::Error>> {
let elisp_script = r#"(progn
(message "%s" (version))
)"#;
@@ -138,12 +157,12 @@ pub fn get_emacs_version() -> Result<String, Box<dyn std::error::Error>> {
.arg("--eval")
.arg(elisp_script);
let out = cmd.output()?;
let out = cmd.output().await?;
out.status.exit_ok()?;
Ok(String::from_utf8(out.stderr)?)
}
pub fn get_org_mode_version() -> Result<String, Box<dyn std::error::Error>> {
pub async fn get_org_mode_version() -> Result<String, Box<dyn std::error::Error>> {
let elisp_script = r#"(progn
(org-mode)
(message "%s" (org-version nil t nil))
@@ -157,7 +176,7 @@ pub fn get_org_mode_version() -> Result<String, Box<dyn std::error::Error>> {
.arg("--eval")
.arg(elisp_script);
let out = cmd.output()?;
let out = cmd.output().await?;
out.status.exit_ok()?;
Ok(String::from_utf8(out.stderr)?)
}

View File

@@ -113,24 +113,21 @@ fn is_slice_of(parent: &str, child: &str) -> bool {
/// Get a slice of the string that was consumed in a parser using the original input to the parser and the remaining input after the parser.
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()
let offset = remaining.as_ptr() as usize - input.as_ptr() as usize;
&input[..offset]
}
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#"""#) {
if !text.starts_with('"') {
return Err("Quoted text does not start with quote.".into());
}
if !text.ends_with(r#"""#) {
if !text.ends_with('"') {
return Err("Quoted text does not end with quote.".into());
}
let interior_text = &text[1..(text.len() - 1)];
let mut state = ParseState::Normal;
for current_char in interior_text.bytes().into_iter() {
for current_char in interior_text.bytes() {
// Check to see if octal finished
state = match (state, current_char) {
(ParseState::Octal(octal), b'0'..=b'7') if octal.len() < MAX_OCTAL_LENGTH => {
@@ -229,11 +226,9 @@ fn atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
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,
})(input)?;
Ok((remaining, Token::Atom(body.into())))
let (remaining, body) =
take_till1(|c| matches!(c, ' ' | '\t' | '\r' | '\n' | ')' | ']'))(input)?;
Ok((remaining, Token::Atom(body)))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -264,22 +259,19 @@ fn quoted_atom<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
}
let (remaining, _) = tag(r#"""#)(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, Token::Atom(source.into())))
Ok((remaining, Token::Atom(source)))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
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,
_ => false,
})(remaining)?;
let (remaining, _body) = take_till1(|c| matches!(c, '>'))(remaining)?;
let (remaining, _) = tag(">")(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, Token::Atom(source.into())))
Ok((remaining, Token::Atom(source)))
}
fn text_with_properties<'s>(input: &'s str) -> Res<&'s str, Token<'s>> {
fn text_with_properties(input: &str) -> Res<&str, Token<'_>> {
let (remaining, _) = tag("#(")(input)?;
let (remaining, (text, props)) = delimited(
multispace0,
@@ -348,10 +340,7 @@ mod tests {
let input = r#" (foo "b(a)r" baz ) "#;
let (remaining, parsed) = sexp(input).expect("Parse the input");
assert_eq!(remaining, "");
assert!(match parsed {
Token::List(_) => true,
_ => false,
});
assert!(matches!(parsed, Token::List(_)));
let children = match parsed {
Token::List(children) => children,
_ => panic!("Should be a list."),
@@ -364,14 +353,14 @@ mod tests {
r#"foo"#
);
assert_eq!(
match children.iter().nth(1) {
match children.get(1) {
Some(Token::Atom(body)) => *body,
_ => panic!("Second child should be an atom."),
},
r#""b(a)r""#
);
assert_eq!(
match children.iter().nth(2) {
match children.get(2) {
Some(Token::Atom(body)) => *body,
_ => panic!("Third child should be an atom."),
},

View File

@@ -1,7 +1,8 @@
use std::str::FromStr;
use super::compare_field::compare_property_list_of_list_of_list_of_ast_nodes;
use super::compare_field::compare_property_list_of_quoted_string;
use super::compare_field::compare_property_object_tree;
use super::compare_field::compare_property_optional_pair;
use super::compare_field::compare_property_quoted_string;
use super::compare_field::ComparePropertiesResult;
use super::diff::DiffEntry;
@@ -53,8 +54,8 @@ pub(crate) fn compare_standard_properties<
Ok(())
}
pub(crate) fn assert_name<'b, 's, S: AsRef<str>>(
emacs: &'b Token<'s>,
pub(crate) fn assert_name<S: AsRef<str>>(
emacs: &Token<'_>,
name: S,
) -> Result<(), Box<dyn std::error::Error>> {
let name = name.as_ref();
@@ -89,9 +90,9 @@ pub(crate) fn assert_bounds<'b, 's, S: StandardProperties<'s> + ?Sized>(
standard_properties.end.ok_or("Token should have an end.")?,
);
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_begin_char_offset = original_document[..rust_begin].chars().count() + 1; // 1-based
let rust_end_char_offset =
rust_begin_char_offset + (&original_document[rust_begin..rust_end]).chars().count(); // 1-based
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))?;
}
@@ -112,21 +113,18 @@ struct EmacsStandardProperties {
post_blank: Option<usize>,
}
fn get_emacs_standard_properties<'b, 's>(
emacs: &'b Token<'s>,
fn get_emacs_standard_properties(
emacs: &Token<'_>,
) -> Result<EmacsStandardProperties, 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_child = children.get(1).ok_or("Should have an attributes child.")?;
let attributes_map = attributes_child.as_map()?;
let standard_properties = attributes_map.get(":standard-properties");
Ok(if standard_properties.is_some() {
let mut std_props = standard_properties
.expect("if statement proves its Some")
.as_vector()?
.into_iter();
.iter();
let begin = maybe_token_to_usize(std_props.next())?;
let post_affiliated = maybe_token_to_usize(std_props.next())?;
let contents_begin = maybe_token_to_usize(std_props.next())?;
@@ -142,16 +140,13 @@ fn get_emacs_standard_properties<'b, 's>(
post_blank,
}
} else {
let begin = maybe_token_to_usize(attributes_map.get(":begin").map(|token| *token))?;
let end = maybe_token_to_usize(attributes_map.get(":end").map(|token| *token))?;
let contents_begin =
maybe_token_to_usize(attributes_map.get(":contents-begin").map(|token| *token))?;
let contents_end =
maybe_token_to_usize(attributes_map.get(":contents-end").map(|token| *token))?;
let post_blank =
maybe_token_to_usize(attributes_map.get(":post-blank").map(|token| *token))?;
let begin = maybe_token_to_usize(attributes_map.get(":begin").copied())?;
let end = maybe_token_to_usize(attributes_map.get(":end").copied())?;
let contents_begin = maybe_token_to_usize(attributes_map.get(":contents-begin").copied())?;
let contents_end = maybe_token_to_usize(attributes_map.get(":contents-end").copied())?;
let post_blank = maybe_token_to_usize(attributes_map.get(":post-blank").copied())?;
let post_affiliated =
maybe_token_to_usize(attributes_map.get(":post-affiliated").map(|token| *token))?;
maybe_token_to_usize(attributes_map.get(":post-affiliated").copied())?;
EmacsStandardProperties {
begin,
post_affiliated,
@@ -169,62 +164,57 @@ fn maybe_token_to_usize(
Ok(token
.map(|token| token.as_atom())
.map_or(Ok(None), |r| r.map(Some))?
.map(|val| {
.and_then(|val| {
if val == "nil" {
None
} else {
Some(val.parse::<usize>())
}
})
.flatten() // Outer option is whether or not the param exists, inner option is whether or not it is nil
.map_or(Ok(None), |r| r.map(Some))?)
}
/// 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>(
pub(crate) fn get_property<'b, 's>(
emacs: &'b Token<'s>,
key: &'x str,
key: &str,
) -> 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_child = children.get(1).ok_or("Should have an attributes child.")?;
let attributes_map = attributes_child.as_map()?;
let prop = attributes_map.get(key).map(|token| *token);
match prop.map(|token| token.as_atom()) {
Some(Ok("nil")) => return Ok(None),
_ => {}
};
let prop = attributes_map.get(key).copied();
if let Some(Ok("nil")) = prop.map(Token::as_atom) {
return Ok(None);
}
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,
pub(crate) fn get_property_unquoted_atom<'s>(
emacs: &Token<'s>,
key: &str,
) -> Result<Option<&'s str>, Box<dyn std::error::Error>> {
Ok(get_property(emacs, key)?
get_property(emacs, key)?
.map(Token::as_atom)
.map_or(Ok(None), |r| r.map(Some))?)
.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,
pub(crate) fn get_property_quoted_string(
emacs: &Token<'_>,
key: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
Ok(get_property(emacs, key)?
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))?)
.map_or(Ok(None), |r| r.map(Some))
}
/// Get a named property containing an unquoted numeric value.
@@ -301,8 +291,8 @@ where
Ok(())
}
pub(crate) fn assert_no_children<'b, 's>(
emacs: &'b Token<'s>,
pub(crate) fn assert_no_children(
emacs: &Token<'_>,
this_status: &mut DiffStatus,
message: &mut Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
@@ -331,7 +321,7 @@ where
let rust_key = rust_key.as_ref();
let rust_value = rust_value.as_ref();
let emacs_value = get_property_quoted_string(emacs, rust_key)?;
if Some(rust_value) != emacs_value.as_ref().map(String::as_str) {
if Some(rust_value) != emacs_value.as_deref() {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
@@ -376,13 +366,23 @@ where
)?;
ret.push(diff);
}
AffiliatedKeywordValue::ListOfListsOfObjects(rust_value) => {
let diff = compare_property_list_of_list_of_list_of_ast_nodes(
AffiliatedKeywordValue::OptionalPair { optval, val } => {
let diff = compare_property_optional_pair(
source,
emacs,
rust,
emacs_property_name.as_str(),
|_| Some(rust_value),
|_| Some((*optval, *val)),
)?;
ret.push(diff);
}
AffiliatedKeywordValue::ObjectTree(rust_value) => {
let diff = compare_property_object_tree(
source,
emacs,
rust,
emacs_property_name.as_str(),
|_| Some(rust_value.iter()),
)?;
ret.push(diff);
}

View File

@@ -1,15 +1,27 @@
use super::global_settings::EntityDefinition;
pub(crate) const DEFAULT_ORG_ELEMENT_PARSED_KEYWORDS: [&'static str; 1] = ["CAPTION"];
/// Keywords that contain the standard set of objects (excluding footnote references).
///
/// Corresponds to org-element-parsed-keywords elisp variable.
pub(crate) const ORG_ELEMENT_PARSED_KEYWORDS: [&str; 1] = ["CAPTION"];
pub(crate) const DEFAULT_ORG_ELEMENT_DUAL_KEYWORDS: [&'static str; 2] = ["CAPTION", "RESULTS"];
/// Keywords that can have a secondary value in square brackets.
///
/// Corresponds to org-element-dual-keywords elisp variable.
pub(crate) const ORG_ELEMENT_DUAL_KEYWORDS: [&str; 2] = ["CAPTION", "RESULTS"];
pub(crate) const DEFAULT_ORG_ELEMENT_AFFILIATED_KEYWORDS: [&'static str; 13] = [
/// Keywords that can be affiliated with an element.
///
/// Corresponds to org-element-affiliated-keywords elisp variable.
pub(crate) const ORG_ELEMENT_AFFILIATED_KEYWORDS: [&str; 13] = [
"CAPTION", "DATA", "HEADER", "HEADERS", "LABEL", "NAME", "PLOT", "RESNAME", "RESULT",
"RESULTS", "SOURCE", "SRCNAME", "TBLNAME",
];
pub(crate) const DEFAULT_ORG_ELEMENT_KEYWORD_TRANSLATION_ALIST: [(&'static str, &'static str); 8] = [
/// Mapping of keyword names.
///
/// Corresponds to org-element-keyword-translation-alist elisp variable.
pub(crate) const ORG_ELEMENT_KEYWORD_TRANSLATION_ALIST: [(&str, &str); 8] = [
("DATA", "NAME"),
("LABEL", "NAME"),
("RESNAME", "NAME"),
@@ -20,7 +32,7 @@ pub(crate) const DEFAULT_ORG_ELEMENT_KEYWORD_TRANSLATION_ALIST: [(&'static str,
("HEADERS", "HEADER"),
];
pub(crate) const DEFAULT_ORG_LINK_PARAMETERS: [&'static str; 23] = [
pub(crate) const DEFAULT_ORG_LINK_PARAMETERS: [&str; 23] = [
"id",
"eww",
"rmail",

View File

@@ -9,12 +9,9 @@ use super::list::List;
use super::DynContextMatcher;
use super::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::OrgSource;
use crate::types::Keyword;
#[derive(Debug)]
pub(crate) enum ContextElement<'r, 's> {
/// Stores a parser that indicates that children should exit upon matching an exit matcher.
ExitMatcherNode(ExitMatcherNode<'r>),
@@ -25,37 +22,17 @@ pub(crate) enum ContextElement<'r, 's> {
/// Indicates if elements should consume the whitespace after them.
ConsumeTrailingWhitespace(bool),
/// Indicate that we are parsing a paragraph that already has affiliated keywords.
///
/// The value stored is the start of the element after the affiliated keywords. In this way, we can ensure that we do not exit an element immediately after the affiliated keyword had been consumed.
HasAffiliatedKeyword(HasAffiliatedKeywordInner<'r, 's>),
/// 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>),
}
#[derive(Debug, Clone)]
pub(crate) struct HasAffiliatedKeywordInner<'r, 's> {
pub(crate) start_after_affiliated_keywords: OrgSource<'s>,
pub(crate) keywords: &'r Vec<Keyword<'s>>,
}
pub(crate) struct ExitMatcherNode<'r> {
// TODO: Should this be "&'r DynContextMatcher<'c>" ?
pub(crate) exit_matcher: &'r DynContextMatcher<'r>,
pub(crate) class: ExitClass,
}
impl<'r> std::fmt::Debug for ExitMatcherNode<'r> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut formatter = f.debug_struct("ExitMatcherNode");
formatter.field("class", &self.class.to_string());
formatter.finish()
}
}
#[derive(Debug)]
pub(crate) struct Context<'g, 'r, 's> {
global_settings: &'g GlobalSettings<'g, 's>,
tree: List<'r, &'r ContextElement<'r, 's>>,
@@ -120,27 +97,22 @@ impl<'g, 'r, 's> Context<'g, 'r, 's> {
pub(crate) fn check_exit_matcher(
&'r self,
i: OrgSource<'s>,
) -> IResult<OrgSource<'s>, OrgSource<'s>, CustomError<OrgSource<'s>>> {
) -> IResult<OrgSource<'s>, OrgSource<'s>, CustomError> {
let mut current_class_filter = ExitClass::Gamma;
for current_node in self.iter_context() {
let context_element = current_node.get_data();
match context_element {
ContextElement::ExitMatcherNode(exit_matcher) => {
if exit_matcher.class as u32 <= current_class_filter as u32 {
current_class_filter = exit_matcher.class;
let local_result = (exit_matcher.exit_matcher)(&current_node, i);
if local_result.is_ok() {
return local_result;
}
if let ContextElement::ExitMatcherNode(exit_matcher) = context_element {
if exit_matcher.class as u32 <= current_class_filter as u32 {
current_class_filter = exit_matcher.class;
let local_result = (exit_matcher.exit_matcher)(&current_node, i);
if local_result.is_ok() {
return local_result;
}
}
_ => {}
};
}
}
// TODO: Make this a specific error instead of just a generic MyError
return Err(nom::Err::Error(CustomError::MyError(MyError(
"NoExit".into(),
))));
return Err(nom::Err::Error(CustomError::Static("NoExit")));
}
/// Indicates if elements should consume the whitespace after them.
@@ -152,11 +124,8 @@ impl<'g, 'r, 's> Context<'g, 'r, 's> {
fn _should_consume_trailing_whitespace(&self) -> Option<bool> {
for current_node in self.iter() {
match current_node {
ContextElement::ConsumeTrailingWhitespace(should) => {
return Some(*should);
}
_ => {}
if let ContextElement::ConsumeTrailingWhitespace(should) = current_node {
return Some(*should);
}
}
None

View File

@@ -1,13 +1,7 @@
#[derive(Debug, Copy, Clone)]
#[derive(Copy, Clone)]
pub(crate) enum ExitClass {
Document = 1,
Alpha = 2,
Beta = 3,
Gamma = 4,
}
impl std::fmt::Display for ExitClass {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}

View File

@@ -1,11 +1,16 @@
use std::fmt::Debug;
use std::path::PathBuf;
pub trait FileAccessInterface: Debug {
#[cfg(any(feature = "compare", feature = "foreign_document_test"))]
pub trait FileAccessInterface: Sync {
fn read_file(&self, path: &str) -> Result<String, std::io::Error>;
}
#[derive(Debug, Clone)]
#[cfg(not(any(feature = "compare", feature = "foreign_document_test")))]
pub trait FileAccessInterface {
fn read_file(&self, path: &str) -> Result<String, std::io::Error>;
}
#[derive(Clone)]
pub struct LocalFileAccessInterface {
pub working_directory: Option<PathBuf>,
}
@@ -14,10 +19,9 @@ impl FileAccessInterface for LocalFileAccessInterface {
fn read_file(&self, path: &str) -> Result<String, std::io::Error> {
let final_path = self
.working_directory
.as_ref()
.map(PathBuf::as_path)
.as_deref()
.map(|pb| pb.join(path))
.unwrap_or_else(|| PathBuf::from(path));
Ok(std::fs::read_to_string(final_path)?)
std::fs::read_to_string(final_path)
}
}

View File

@@ -5,16 +5,12 @@ use super::constants::DEFAULT_ORG_ENTITIES;
use super::constants::DEFAULT_ORG_LINK_PARAMETERS;
use super::FileAccessInterface;
use super::LocalFileAccessInterface;
use crate::context::constants::DEFAULT_ORG_ELEMENT_AFFILIATED_KEYWORDS;
use crate::context::constants::DEFAULT_ORG_ELEMENT_DUAL_KEYWORDS;
use crate::context::constants::DEFAULT_ORG_ELEMENT_KEYWORD_TRANSLATION_ALIST;
use crate::context::constants::DEFAULT_ORG_ELEMENT_PARSED_KEYWORDS;
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
#[derive(Debug, Clone)]
#[derive(Clone)]
pub struct GlobalSettings<'g, 's> {
pub radio_targets: Vec<&'g Vec<Object<'s>>>,
pub file_access: &'g dyn FileAccessInterface,
@@ -58,26 +54,6 @@ pub struct GlobalSettings<'g, 's> {
///
/// Corresponds to org-entities elisp variable.
pub entities: &'g [EntityDefinition<'s>],
/// Keywords that contain the standard set of objects (excluding footnote references).
///
/// Corresponds to org-element-parsed-keywords elisp variable.
pub element_parsed_keywords: &'g [&'s str],
/// Keywords that can have a secondary value in square brackets.
///
/// Corresponds to org-element-dual-keywords elisp variable.
pub element_dual_keywords: &'g [&'s str],
/// Keywords that can be affiliated with an element.
///
/// Corresponds to org-element-affiliated-keywords elisp variable.
pub element_affiliated_keywords: &'g [&'s str],
/// Mapping of keyword names.
///
/// Corresponds to org-element-keyword-translation-alist elisp variable.
pub element_keyword_translation_alist: &'g [(&'s str, &'s str)],
}
pub const DEFAULT_TAB_WIDTH: IndentationLevel = 8;
@@ -95,7 +71,9 @@ pub struct EntityDefinition<'a> {
impl<'g, 's> GlobalSettings<'g, 's> {
fn new() -> GlobalSettings<'g, 's> {
debug_assert!(DEFAULT_ORG_ENTITIES.is_sorted_by(|a, b| b.name.len().partial_cmp(&a.name.len())));
debug_assert!(
DEFAULT_ORG_ENTITIES.is_sorted_by(|a, b| b.name.len().partial_cmp(&a.name.len()))
);
GlobalSettings {
radio_targets: Vec::new(),
file_access: &LocalFileAccessInterface {
@@ -110,10 +88,6 @@ impl<'g, 's> GlobalSettings<'g, 's> {
link_parameters: &DEFAULT_ORG_LINK_PARAMETERS,
link_templates: BTreeMap::new(),
entities: &DEFAULT_ORG_ENTITIES,
element_parsed_keywords: &DEFAULT_ORG_ELEMENT_PARSED_KEYWORDS,
element_dual_keywords: &DEFAULT_ORG_ELEMENT_DUAL_KEYWORDS,
element_affiliated_keywords: &DEFAULT_ORG_ELEMENT_AFFILIATED_KEYWORDS,
element_keyword_translation_alist: &DEFAULT_ORG_ELEMENT_KEYWORD_TRANSLATION_ALIST,
}
}
}
@@ -124,14 +98,10 @@ impl<'g, 's> Default for GlobalSettings<'g, 's> {
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Clone, PartialEq, Default)]
pub enum HeadlineLevelFilter {
Odd,
#[default]
OddEven,
}
impl Default for HeadlineLevelFilter {
fn default() -> Self {
HeadlineLevelFilter::OddEven
}
}

View File

@@ -50,7 +50,7 @@ impl<'a, T> Iterator for Iter<'a, T> {
fn next(&mut self) -> Option<Self::Item> {
let ret = self.next.map(|link| link.get_data());
self.next = self.next.map(|link| link.get_parent()).flatten();
self.next = self.next.and_then(|link| link.get_parent());
ret
}
}
@@ -64,7 +64,7 @@ impl<'a, T> Iterator for IterList<'a, T> {
fn next(&mut self) -> Option<Self::Item> {
let ret = self.next;
self.next = self.next.map(|this| this.get_parent()).flatten();
self.next = self.next.and_then(|link| link.get_parent());
ret
}
}

View File

@@ -1,7 +1,8 @@
use crate::error::Res;
use crate::parser::OrgSource;
mod constants;
pub(crate) mod constants;
#[allow(clippy::module_inception)]
mod context;
mod exiting;
mod file_access_interface;
@@ -22,7 +23,6 @@ type DynMatcher<'c> = dyn Matcher + 'c;
pub(crate) use context::Context;
pub(crate) use context::ContextElement;
pub(crate) use context::ExitMatcherNode;
pub(crate) use context::HasAffiliatedKeywordInner;
pub(crate) use exiting::ExitClass;
pub use file_access_interface::FileAccessInterface;
pub use file_access_interface::LocalFileAccessInterface;
@@ -31,4 +31,5 @@ pub use global_settings::GlobalSettings;
pub use global_settings::HeadlineLevelFilter;
pub use global_settings::DEFAULT_TAB_WIDTH;
pub(crate) use list::List;
pub(crate) use parser_with_context::bind_context;
pub(crate) use parser_with_context::parser_with_context;

View File

@@ -4,3 +4,10 @@ macro_rules! parser_with_context {
};
}
pub(crate) use parser_with_context;
macro_rules! bind_context {
($target:expr, $context:expr) => {
|i| $target($context, i)
};
}
pub(crate) use bind_context;

View File

@@ -2,22 +2,20 @@ use nom::error::ErrorKind;
use nom::error::ParseError;
use nom::IResult;
pub(crate) type Res<T, U> = IResult<T, U, CustomError<T>>;
pub(crate) type Res<T, U> = IResult<T, U, CustomError>;
#[derive(Debug)]
pub enum CustomError<I> {
MyError(MyError<&'static str>),
Nom(I, ErrorKind),
pub enum CustomError {
#[allow(dead_code)]
Text(String),
Static(&'static str),
IO(std::io::Error),
BoxedError(Box<dyn std::error::Error>),
Parser(ErrorKind),
}
#[derive(Debug)]
pub struct MyError<I>(pub(crate) I);
impl<I> ParseError<I> for CustomError<I> {
fn from_error_kind(input: I, kind: ErrorKind) -> Self {
CustomError::Nom(input, kind)
impl<I: std::fmt::Debug> ParseError<I> for CustomError {
fn from_error_kind(_input: I, kind: ErrorKind) -> Self {
CustomError::Parser(kind)
}
fn append(_input: I, _kind: ErrorKind, /*mut*/ other: Self) -> Self {
@@ -26,20 +24,20 @@ impl<I> ParseError<I> for CustomError<I> {
}
}
impl<I> From<std::io::Error> for CustomError<I> {
impl From<std::io::Error> for CustomError {
fn from(value: std::io::Error) -> Self {
CustomError::IO(value)
}
}
impl<I> From<&'static str> for CustomError<I> {
impl From<&'static str> for CustomError {
fn from(value: &'static str) -> Self {
CustomError::MyError(MyError(value))
CustomError::Static(value)
}
}
impl<I> From<Box<dyn std::error::Error>> for CustomError<I> {
fn from(value: Box<dyn std::error::Error>) -> Self {
CustomError::BoxedError(value)
impl From<String> for CustomError {
fn from(value: String) -> Self {
CustomError::Text(value)
}
}

View File

@@ -1,4 +1,4 @@
#[allow(clippy::module_inception)]
mod error;
pub(crate) use error::CustomError;
pub(crate) use error::MyError;
pub(crate) use error::Res;

View File

@@ -0,0 +1,43 @@
use std::collections::HashMap;
use std::sync::Mutex;
use super::EventType;
use crate::parser::OrgSource;
#[derive(Debug, Eq, Hash, PartialEq)]
struct EventKey {
event_type: EventType,
byte_offset: usize,
}
pub(crate) type EventCount = usize;
static GLOBAL_DATA: Mutex<Option<HashMap<EventKey, EventCount>>> = Mutex::new(None);
pub(crate) fn record_event(event_type: EventType, input: OrgSource<'_>) {
let mut db = GLOBAL_DATA.lock().unwrap();
let db = db.get_or_insert_with(HashMap::new);
let key = EventKey {
event_type,
byte_offset: input.get_byte_offset(),
};
*db.entry(key).or_insert(0) += 1;
}
pub fn report(original_document: &str) {
let mut db = GLOBAL_DATA.lock().unwrap();
let db = db.get_or_insert_with(HashMap::new);
let mut results: Vec<_> = db.iter().map(|(k, v)| (k, v)).collect();
results.sort_by_key(|(_k, v)| *v);
// This would put the most common at the top, but that is a pain when there is already a lot of output from the parser.
// results.sort_by(|(_ak, av), (_bk, bv)| bv.cmp(av));
for (key, count) in results {
println!(
"{:?} {} character offset: {} byte offset: {}",
key.event_type,
count,
original_document[..key.byte_offset].chars().count() + 1,
key.byte_offset
)
}
}

View File

@@ -0,0 +1,4 @@
#[derive(Debug, Eq, Hash, PartialEq)]
pub(crate) enum EventType {
ElementStart,
}

6
src/event_count/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
mod database;
mod event_type;
pub(crate) use database::record_event;
pub use database::report;
pub(crate) use event_type::EventType;

View File

@@ -5,7 +5,7 @@ use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt;
#[cfg(feature = "tracing")]
use tracing_subscriber::util::SubscriberInitExt;
const SERVICE_NAME: &'static str = "organic";
const SERVICE_NAME: &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.

View File

@@ -2,13 +2,19 @@
#![feature(trait_alias)]
#![feature(path_file_prefix)]
#![feature(is_sorted)]
#![feature(test)]
// TODO: #![warn(missing_docs)]
#![allow(clippy::bool_assert_comparison)] // Sometimes you want the long form because its easier to see at a glance.
extern crate test;
#[cfg(feature = "compare")]
pub mod compare;
mod context;
mod error;
#[cfg(feature = "event_count")]
pub mod event_count;
mod iter;
pub mod parser;
pub mod types;

View File

@@ -23,13 +23,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "tracing")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let rt = tokio::runtime::Runtime::new()?;
let result = rt.block_on(async {
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"))]
@@ -55,8 +54,11 @@ fn read_stdin_to_string() -> Result<String, Box<dyn std::error::Error>> {
}
fn run_anonymous_parse<P: AsRef<str>>(org_contents: P) -> Result<(), Box<dyn std::error::Error>> {
let rust_parsed = parse(org_contents.as_ref())?;
let org_contents = org_contents.as_ref();
let rust_parsed = parse(org_contents)?;
println!("{:#?}", rust_parsed);
#[cfg(feature = "event_count")]
organic::event_count::report(org_contents);
Ok(())
}
@@ -70,12 +72,13 @@ fn run_parse_on_file<P: AsRef<Path>>(org_path: P) -> Result<(), Box<dyn std::err
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 global_settings = GlobalSettings {
file_access: &file_access_interface,
..Default::default()
};
let rust_parsed = parse_with_settings(org_contents, &global_settings)?;
println!("{:#?}", rust_parsed);
#[cfg(feature = "event_count")]
organic::event_count::report(org_contents);
Ok(())
}

View File

@@ -1,122 +1,193 @@
use std::collections::BTreeMap;
use nom::bytes::complete::tag;
use nom::bytes::complete::take_until;
use nom::character::complete::anychar;
use nom::combinator::all_consuming;
use nom::combinator::eof;
use nom::combinator::map;
use nom::combinator::map_parser;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::keyword::affiliated_keyword;
use super::object_parser::standard_set_object;
use crate::context::parser_with_context;
use super::util::confine_context;
use super::OrgSource;
use crate::context::bind_context;
use crate::context::constants::ORG_ELEMENT_DUAL_KEYWORDS;
use crate::context::constants::ORG_ELEMENT_KEYWORD_TRANSLATION_ALIST;
use crate::context::constants::ORG_ELEMENT_PARSED_KEYWORDS;
use crate::context::Context;
use crate::context::ContextElement;
use crate::context::GlobalSettings;
use crate::context::List;
use crate::error::Res;
use crate::types::AffiliatedKeywordValue;
use crate::types::AffiliatedKeywords;
use crate::types::Keyword;
pub(crate) fn parse_affiliated_keywords<'g, 's>(
global_settings: &'g GlobalSettings<'g, 's>,
input: Vec<Keyword<'s>>,
) -> AffiliatedKeywords<'s> {
let mut ret = BTreeMap::new();
for kw in input.into_iter() {
let translated_name = translate_name(global_settings, kw.key);
if is_single_string_keyword(global_settings, translated_name.as_str()) {
ret.insert(
translated_name,
AffiliatedKeywordValue::SingleString(kw.value),
);
} else if is_list_of_single_string_keyword(global_settings, translated_name.as_str()) {
let list_of_strings = ret
.entry(translated_name)
.or_insert_with(|| AffiliatedKeywordValue::ListOfStrings(Vec::with_capacity(1)));
match list_of_strings {
AffiliatedKeywordValue::ListOfStrings(list_of_strings)
if list_of_strings.is_empty() =>
{
list_of_strings.push(kw.value);
}
AffiliatedKeywordValue::ListOfStrings(list_of_strings) => {
list_of_strings.clear();
list_of_strings.push(kw.value);
}
_ => panic!("Invalid AffiliatedKeywordValue type."),
}
} else if is_list_of_objects_keyword(global_settings, translated_name.as_str()) {
let initial_context = ContextElement::document_context();
let initial_context = Context::new(global_settings, List::new(&initial_context));
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn affiliated_keywords<'s>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Vec<Keyword<'s>>> {
let mut ret = Vec::new();
let mut remaining = input;
// TODO: This should be omitting footnote references
let (_remaining, objects) = all_consuming(many0(parser_with_context!(
standard_set_object
)(&initial_context)))(kw.value.into())
.expect("Object parser should always succeed.");
let list_of_lists = ret.entry(translated_name).or_insert_with(|| {
AffiliatedKeywordValue::ListOfListsOfObjects(Vec::with_capacity(1))
});
match list_of_lists {
AffiliatedKeywordValue::ListOfListsOfObjects(list_of_lists) => {
list_of_lists.push(objects);
}
_ => panic!("Invalid AffiliatedKeywordValue type."),
loop {
let result = affiliated_keyword(remaining);
match result {
Ok((remain, kw)) => {
remaining = remain;
ret.push(kw);
}
} else {
let list_of_strings = ret
.entry(translated_name)
.or_insert_with(|| AffiliatedKeywordValue::ListOfStrings(Vec::with_capacity(1)));
match list_of_strings {
AffiliatedKeywordValue::ListOfStrings(list_of_strings) => {
list_of_strings.push(kw.value);
}
_ => panic!("Invalid AffiliatedKeywordValue type."),
Err(_) => {
break;
}
}
}
Ok((remaining, ret))
}
pub(crate) fn parse_affiliated_keywords<'g, 's, AK>(
global_settings: &'g GlobalSettings<'g, 's>,
input: AK,
) -> AffiliatedKeywords<'s>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let mut ret = BTreeMap::new();
for kw in input {
let translated_name = translate_name(kw.key);
let keyword_type = identify_keyword_type(translated_name.as_str());
match keyword_type {
AffiliatedKeywordType::SingleString => {
ret.insert(
translated_name,
AffiliatedKeywordValue::SingleString(kw.value),
);
}
AffiliatedKeywordType::ListOfStrings => {
let list_of_strings = ret.entry(translated_name).or_insert_with(|| {
AffiliatedKeywordValue::ListOfStrings(Vec::with_capacity(1))
});
match list_of_strings {
AffiliatedKeywordValue::ListOfStrings(list_of_strings) => {
list_of_strings.push(kw.value);
}
_ => panic!("Invalid AffiliatedKeywordValue type."),
}
}
AffiliatedKeywordType::OptionalPair => {
let (_remaining, optional_string) = opt(all_consuming(map(
tuple((
take_until::<_, &str, nom::error::Error<_>>("["),
tag("["),
recognize(many_till(anychar, peek(tuple((tag("]"), eof))))),
tag("]"),
eof,
)),
|(_, _, objects, _, _)| objects,
)))(kw.key)
.expect("Parser should always succeed.");
ret.insert(
translated_name,
AffiliatedKeywordValue::OptionalPair {
optval: optional_string,
val: kw.value,
},
);
}
AffiliatedKeywordType::ObjectTree => {
let initial_context = ContextElement::document_context();
let initial_context = Context::new(global_settings, List::new(&initial_context));
let (_remaining, optional_objects) = opt(all_consuming(map(
tuple((
take_until("["),
tag("["),
map_parser(
recognize(many_till(anychar, peek(tuple((tag("]"), eof))))),
confine_context(|i| {
all_consuming(many0(bind_context!(
standard_set_object,
&initial_context
)))(i)
}),
),
tag("]"),
eof,
)),
|(_, _, objects, _, _)| objects,
)))(kw.key.into())
.expect("Object parser should always succeed.");
// TODO: This should be omitting footnote references
let (_remaining, objects) = all_consuming(many0(bind_context!(
standard_set_object,
&initial_context
)))(kw.value.into())
.expect("Object parser should always succeed.");
let entry_per_keyword_list = ret
.entry(translated_name)
.or_insert_with(|| AffiliatedKeywordValue::ObjectTree(Vec::with_capacity(1)));
match entry_per_keyword_list {
AffiliatedKeywordValue::ObjectTree(entry_per_keyword_list) => {
entry_per_keyword_list.push((optional_objects, objects));
}
_ => panic!("Invalid AffiliatedKeywordValue type."),
}
}
};
}
AffiliatedKeywords { keywords: ret }
}
fn translate_name<'g, 's>(global_settings: &'g GlobalSettings<'g, 's>, name: &'s str) -> String {
for (src, dst) in global_settings.element_keyword_translation_alist {
if name.eq_ignore_ascii_case(src) {
fn translate_name(name: &str) -> String {
let name_until_optval = name
.split_once('[')
.map(|(before, _after)| before)
.unwrap_or(name);
for (src, dst) in ORG_ELEMENT_KEYWORD_TRANSLATION_ALIST {
if name_until_optval.eq_ignore_ascii_case(src) {
return dst.to_lowercase();
}
}
name.to_lowercase()
name_until_optval.to_lowercase()
}
fn is_single_string_keyword<'g, 's>(
_global_settings: &'g GlobalSettings<'g, 's>,
name: &'s str,
) -> bool {
// TODO: Is this defined by an elisp variable?
for single_string_name in ["plot", "name"] {
if name.eq_ignore_ascii_case(single_string_name) {
return true;
}
}
false
enum AffiliatedKeywordType {
SingleString,
ListOfStrings,
OptionalPair,
ObjectTree,
}
fn is_list_of_single_string_keyword<'g, 's>(
_global_settings: &'g GlobalSettings<'g, 's>,
name: &'s str,
) -> bool {
// TODO: Is this defined by an elisp variable?
for single_string_name in ["results"] {
if name.eq_ignore_ascii_case(single_string_name) {
return true;
}
fn identify_keyword_type(name: &str) -> AffiliatedKeywordType {
let is_multiple = ["CAPTION", "HEADER"]
.into_iter()
.any(|candidate| name.eq_ignore_ascii_case(candidate))
|| name.to_lowercase().starts_with("attr_");
let is_parsed = ORG_ELEMENT_PARSED_KEYWORDS
.iter()
.any(|candidate| name.eq_ignore_ascii_case(candidate));
let can_have_optval = ORG_ELEMENT_DUAL_KEYWORDS
.iter()
.any(|candidate| name.eq_ignore_ascii_case(candidate));
match (is_multiple, is_parsed, can_have_optval) {
(true, true, true) => AffiliatedKeywordType::ObjectTree,
(true, true, false) => unreachable!("Nothing like this exists in upstream org-mode."),
(true, false, true) => unreachable!("Nothing like this exists in upstream org-mode."),
(true, false, false) => AffiliatedKeywordType::ListOfStrings,
(false, true, true) => unreachable!("Nothing like this exists in upstream org-mode."),
(false, true, false) => unreachable!("Nothing like this exists in upstream org-mode."),
(false, false, true) => AffiliatedKeywordType::OptionalPair,
(false, false, false) => AffiliatedKeywordType::SingleString,
}
false
}
fn is_list_of_objects_keyword<'g, 's>(
global_settings: &'g GlobalSettings<'g, 's>,
name: &'s str,
) -> bool {
for parsed_keyword in global_settings.element_parsed_keywords {
if name.eq_ignore_ascii_case(parsed_keyword) {
return true;
}
}
false
}

View File

@@ -9,13 +9,11 @@ use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::tuple;
use nom::InputTake;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::BracketDepth;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use super::util::start_of_line;
@@ -23,22 +21,25 @@ use super::OrgSource;
use crate::context::Matcher;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::get_consumed;
use crate::parser::util::org_line_ending;
use crate::types::BabelCall;
use crate::types::Keyword;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn babel_call<'b, 'g, 'r, 's>(
pub(crate) fn babel_call<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, BabelCall<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, BabelCall<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
start_of_line(remaining)?;
let (remaining, _) = tuple((space0, tag("#+"), tag_no_case("call"), tag(":")))(remaining)?;
@@ -64,8 +65,7 @@ pub(crate) fn babel_call<'b, 'g, 'r, 's>(
}
let (remaining, _ws) = space0(remaining)?;
let (remaining, (value, (call, inside_header, arguments, end_header))) =
consumed(babel_call_value)(remaining)?;
let (remaining, (value, babel_call_value)) = consumed(babel_call_value)(remaining)?;
let (remaining, _ws) = tuple((space0, org_line_ending))(remaining)?;
let (remaining, _trailing_ws) =
@@ -81,33 +81,36 @@ pub(crate) fn babel_call<'b, 'g, 'r, 's>(
affiliated_keywords,
),
value: Into::<&str>::into(value).trim_end(),
call: call.map(Into::<&str>::into),
inside_header: inside_header.map(Into::<&str>::into),
arguments: arguments.map(Into::<&str>::into),
end_header: end_header.map(Into::<&str>::into),
call: babel_call_value.call.map(Into::<&str>::into),
inside_header: babel_call_value.inside_header.map(Into::<&str>::into),
arguments: babel_call_value.arguments.map(Into::<&str>::into),
end_header: babel_call_value.end_header.map(Into::<&str>::into),
},
))
}
#[derive(Debug)]
struct BabelCallValue<'s> {
call: Option<OrgSource<'s>>,
inside_header: Option<OrgSource<'s>>,
arguments: Option<OrgSource<'s>>,
end_header: Option<OrgSource<'s>>,
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn babel_call_value<'s>(
input: OrgSource<'s>,
) -> Res<
OrgSource<'s>,
(
Option<OrgSource<'s>>,
Option<OrgSource<'s>>,
Option<OrgSource<'s>>,
Option<OrgSource<'s>>,
),
> {
fn babel_call_value<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, BabelCallValue<'s>> {
let (remaining, call) = opt(babel_call_call)(input)?;
let (remaining, inside_header) = opt(inside_header)(remaining)?;
let (remaining, arguments) = opt(arguments)(remaining)?;
let (remaining, end_header) = opt(end_header)(remaining)?;
Ok((
remaining,
(call, inside_header, arguments.flatten(), end_header),
BabelCallValue {
call,
inside_header,
arguments: arguments.flatten(),
end_header,
},
))
}
@@ -213,9 +216,7 @@ fn impl_balanced_bracket<
}
if fail_parser(remaining).is_ok() {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Fail parser matched.",
))));
return Err(nom::Err::Error(CustomError::Static("Fail parser matched.")));
}
let (remain, _) = anychar(remaining)?;

165
src/parser/bullshitium.rs Normal file
View File

@@ -0,0 +1,165 @@
use nom::branch::alt;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar;
use nom::character::complete::space0;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::paragraph::paragraph;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use super::util::org_line_ending;
use super::util::start_of_line;
use super::OrgSource;
use crate::context::bind_context;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::Res;
use crate::parser::macros::element;
use crate::types::AffiliatedKeywords;
use crate::types::Object;
use crate::types::Paragraph;
use crate::types::PlainText;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
)]
pub(crate) fn bullshitium<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Paragraph<'s>> {
alt((
bind_context!(broken_end, context),
bind_context!(broken_dynamic_block, context),
))(input)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
)]
pub(crate) fn detect_bullshitium<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> {
element!(detect_broken_end, context, input);
element!(detect_broken_dynamic_block, context, input);
Err(nom::Err::Error(CustomError::Static("No bullshitium.")))
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
)]
pub(crate) fn broken_end<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Paragraph<'s>> {
start_of_line(input)?;
let (remaining, _) = space0(input)?;
let (remaining, _) = tag_no_case(":end:")(remaining)?;
let (lead_in_remaining, _) = tuple((space0, org_line_ending))(remaining)?;
if let Ok((remaining, mut paragraph)) =
paragraph(std::iter::empty(), lead_in_remaining, context, input)
{
match paragraph.children.first_mut() {
Some(Object::PlainText(plain_text)) => {
plain_text.source = input.get_until_end_of_str(plain_text.source).into();
}
Some(obj) => {
panic!("Unhandled first object type inside bullshitium {:?}", obj);
}
None => {
unreachable!("Paragraph must have children.");
}
};
Ok((remaining, paragraph))
} else {
let (remaining, _trailing_ws) =
maybe_consume_trailing_whitespace_if_not_exiting(context, lead_in_remaining)?;
Ok((
remaining,
Paragraph {
source: input.get_until(remaining).into(),
affiliated_keywords: AffiliatedKeywords::default(),
children: vec![Object::PlainText(PlainText {
source: input.get_until(lead_in_remaining).into(),
})],
},
))
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(_context))
)]
pub(crate) fn detect_broken_end<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> {
start_of_line(input)?;
let (remaining, _) = space0(input)?;
let (remaining, _) = tag_no_case(":end:")(remaining)?;
let (_remaining, _) = tuple((space0, org_line_ending))(remaining)?;
Ok((input, ()))
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
)]
pub(crate) fn broken_dynamic_block<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Paragraph<'s>> {
start_of_line(input)?;
let (remaining, _) = space0(input)?;
let (remaining, _) = tag_no_case("#+BEGIN:")(remaining)?;
let (lead_in_remaining, _) = many_till(anychar, org_line_ending)(remaining)?;
if let Ok((remaining, mut paragraph)) =
paragraph(std::iter::empty(), lead_in_remaining, context, input)
{
match paragraph.children.first_mut() {
Some(Object::PlainText(plain_text)) => {
plain_text.source = input.get_until_end_of_str(plain_text.source).into();
}
Some(obj) => {
panic!("Unhandled first object type inside bullshitium {:?}", obj);
}
None => {
unreachable!("Paragraph must have children.");
}
};
Ok((remaining, paragraph))
} else {
let (remaining, _trailing_ws) =
maybe_consume_trailing_whitespace_if_not_exiting(context, lead_in_remaining)?;
Ok((
remaining,
Paragraph {
source: input.get_until(remaining).into(),
affiliated_keywords: AffiliatedKeywords::default(),
children: vec![Object::PlainText(PlainText {
source: input.get_until(lead_in_remaining).into(),
})],
},
))
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(_context))
)]
pub(crate) fn detect_broken_dynamic_block<'b, 'g, 'r, 's>(
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> {
start_of_line(input)?;
let (remaining, _) = space0(input)?;
let (_remaining, _) = tag_no_case("#+BEGIN:")(remaining)?;
Ok((input, ()))
}

View File

@@ -137,7 +137,7 @@ fn _global_prefix_end<'b, 'g, 'r, 's>(
unreachable!("Exceeded citation global prefix bracket depth.")
}
if current_depth == 0 {
let close_bracket = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("]")(input);
let close_bracket = tag::<_, _, CustomError>("]")(input);
if close_bracket.is_ok() {
return close_bracket;
}
@@ -191,7 +191,7 @@ fn _global_suffix_end<'b, 'g, 'r, 's>(
unreachable!("Exceeded citation global suffix bracket depth.")
}
if current_depth == 0 {
let close_bracket = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("]")(input);
let close_bracket = tag::<_, _, CustomError>("]")(input);
if close_bracket.is_ok() {
return close_bracket;
}
@@ -205,6 +205,7 @@ fn _global_suffix_end<'b, 'g, 'r, 's>(
#[cfg(test)]
mod tests {
use super::*;
use crate::context::bind_context;
use crate::context::Context;
use crate::context::GlobalSettings;
use crate::context::List;
@@ -219,7 +220,7 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let paragraph_matcher = parser_with_context!(element(true))(&initial_context);
let paragraph_matcher = bind_context!(element(true), &initial_context);
let (remaining, first_paragraph) = paragraph_matcher(input).expect("Parse first paragraph");
let first_paragraph = match first_paragraph {
Element::Paragraph(paragraph) => paragraph,

View File

@@ -20,7 +20,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::object_parser::minimal_set_object;
use crate::parser::util::exit_matcher_parser;
@@ -151,7 +150,7 @@ fn _key_prefix_end<'b, 'g, 'r, 's>(
unreachable!("Exceeded citation key prefix bracket depth.")
}
if current_depth == 0 {
let close_bracket = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("]")(input);
let close_bracket = tag::<_, _, CustomError>("]")(input);
if close_bracket.is_ok() {
return close_bracket;
}
@@ -181,7 +180,7 @@ fn _key_suffix_end<'b, 'g, 'r, 's>(
unreachable!("Exceeded citation key suffix bracket depth.")
}
if current_depth == 0 {
let close_bracket = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("]")(input);
let close_bracket = tag::<_, _, CustomError>("]")(input);
if close_bracket.is_ok() {
return close_bracket;
}
@@ -199,9 +198,7 @@ where
let pre_bracket_depth = input.get_bracket_depth();
let (remaining, output) = inner(input)?;
if remaining.get_bracket_depth() - pre_bracket_depth != 0 {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"UnbalancedBrackets".into(),
))));
return Err(nom::Err::Error(CustomError::Static("UnbalancedBrackets")));
}
Ok((remaining, output))
}

View File

@@ -19,7 +19,6 @@ use crate::context::parser_with_context;
use crate::context::ContextElement;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::immediate_in_section;
@@ -35,9 +34,9 @@ pub(crate) fn comment<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Comment<'s>> {
if immediate_in_section(context, "comment") {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Cannot nest objects of the same element",
)));
}
let parser_context = ContextElement::Context("comment");
let parser_context = context.with_additional_node(&parser_context);
@@ -104,6 +103,7 @@ pub(crate) fn detect_comment<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()>
#[cfg(test)]
mod tests {
use super::*;
use crate::context::bind_context;
use crate::context::Context;
use crate::context::ContextElement;
use crate::context::GlobalSettings;
@@ -119,7 +119,7 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let comment_matcher = parser_with_context!(comment)(&initial_context);
let comment_matcher = bind_context!(comment, &initial_context);
let (remaining, first_comment) = comment_matcher(input).expect("Parse first comment");
assert_eq!(
Into::<&str>::into(remaining),

View File

@@ -1,11 +1,9 @@
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use super::util::org_line_ending;
@@ -14,16 +12,21 @@ use crate::error::Res;
use crate::parser::util::get_consumed;
use crate::parser::util::start_of_line;
use crate::types::DiarySexp;
use crate::types::Keyword;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn diary_sexp<'b, 'g, 'r, 's>(
pub(crate) fn diary_sexp<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, DiarySexp<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, DiarySexp<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
start_of_line(remaining)?;
let (remaining, value) = recognize(tuple((tag("%%("), is_not("\r\n"))))(remaining)?;
let (remaining, _eol) = org_line_ending(remaining)?;
@@ -44,9 +47,19 @@ pub(crate) 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>, ()> {
let (input, _) = many0(affiliated_keyword)(input)?;
tuple((start_of_line, tag("%%(")))(input)?;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(_context, _affiliated_keywords))
)]
pub(crate) fn detect_diary_sexp<'b, 'g, 'r, 's, AK>(
_affiliated_keywords: AK,
remaining: OrgSource<'s>,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
tuple((start_of_line, tag("%%(")))(remaining)?;
Ok((input, ()))
}

View File

@@ -11,6 +11,7 @@ use super::in_buffer_settings::scan_for_in_buffer_settings;
use super::org_source::OrgSource;
use super::section::zeroth_section;
use super::util::get_consumed;
use crate::context::bind_context;
use crate::context::parser_with_context;
use crate::context::Context;
use crate::context::ContextElement;
@@ -18,9 +19,7 @@ use crate::context::GlobalSettings;
use crate::context::List;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::org_source::convert_error;
use crate::parser::util::blank_line;
use crate::types::AstNode;
use crate::types::Document;
@@ -30,7 +29,7 @@ use crate::types::Object;
///
/// This is a main entry point for Organic. It will parse the full contents of the input string as an org-mode document without an underlying file attached.
#[allow(dead_code)]
pub fn parse<'s>(input: &'s str) -> Result<Document<'s>, Box<dyn std::error::Error>> {
pub fn parse(input: &str) -> Result<Document<'_>, Box<dyn std::error::Error>> {
parse_file_with_settings::<&Path>(input, &GlobalSettings::default(), None)
}
@@ -40,10 +39,10 @@ pub fn parse<'s>(input: &'s str) -> Result<Document<'s>, Box<dyn std::error::Err
///
/// file_path is not used for reading the file contents. It is only used for determining the document category and filling in the path attribute on the Document.
#[allow(dead_code)]
pub fn parse_file<'s, P: AsRef<Path>>(
input: &'s str,
pub fn parse_file<P: AsRef<Path>>(
input: &str,
file_path: Option<P>,
) -> Result<Document<'s>, Box<dyn std::error::Error>> {
) -> Result<Document<'_>, Box<dyn std::error::Error>> {
parse_file_with_settings(input, &GlobalSettings::default(), file_path)
}
@@ -77,7 +76,7 @@ pub fn parse_file_with_settings<'g, 's, P: AsRef<Path>>(
let initial_context = Context::new(global_settings, List::new(&initial_context));
let wrapped_input = OrgSource::new(input);
let mut doc =
all_consuming(parser_with_context!(document_org_source)(&initial_context))(wrapped_input)
all_consuming(bind_context!(document_org_source, &initial_context))(wrapped_input)
.map_err(|err| err.to_string())
.map(|(_remaining, parsed_document)| parsed_document)?;
if let Some(file_path) = file_path {
@@ -101,11 +100,8 @@ pub fn parse_file_with_settings<'g, 's, P: AsRef<Path>>(
///
/// This will not prevent additional settings from being learned during parsing, for example when encountering a "#+TODO".
#[allow(dead_code)]
fn document<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: &'s str,
) -> Res<&'s str, Document<'s>> {
let (remaining, doc) = document_org_source(context, input.into()).map_err(convert_error)?;
fn document<'s>(context: RefContext<'_, '_, '_, 's>, input: &'s str) -> Res<&'s str, Document<'s>> {
let (remaining, doc) = document_org_source(context, input.into())?;
Ok((Into::<&str>::into(remaining), doc))
}
@@ -129,29 +125,16 @@ fn document_org_source<'b, 'g, 'r, 's>(
.get_global_settings()
.file_access
.read_file(setup_file)
.map_err(|err| nom::Err::<CustomError<OrgSource<'_>>>::Failure(err.into()))
.map_err(|err| nom::Err::<CustomError>::Failure(err.into()))
})
.collect::<Result<Vec<_>, _>>()?;
for setup_file in setup_files.iter().map(String::as_str) {
let (_, setup_file_settings) =
scan_for_in_buffer_settings(setup_file.into()).map_err(|err| {
eprintln!("{}", err);
nom::Err::Error(CustomError::MyError(MyError(
"TODO: make this take an owned string so I can dump err.to_string() into it."
.into(),
)))
})?;
let (_, setup_file_settings) = scan_for_in_buffer_settings(setup_file.into())?;
final_settings.extend(setup_file_settings);
}
final_settings.extend(document_settings);
let new_settings = apply_in_buffer_settings(final_settings, context.get_global_settings())
.map_err(|err| {
eprintln!("{}", err);
nom::Err::Error(CustomError::MyError(MyError(
"TODO: make this take an owned string so I can dump err.to_string() into it."
.into(),
)))
})?;
.map_err(nom::Err::Error)?;
let new_context = context.with_global_settings(&new_settings);
let context = &new_context;
@@ -176,15 +159,13 @@ fn document_org_source<'b, 'g, 'r, 's>(
let parser_context = context.with_global_settings(&new_global_settings);
let (remaining, mut document) = _document(&parser_context, input)
.map(|(rem, out)| (Into::<&str>::into(rem), out))?;
apply_post_parse_in_buffer_settings(&mut document)
.map_err(|err| nom::Err::<CustomError<OrgSource<'_>>>::Failure(err.into()))?;
apply_post_parse_in_buffer_settings(&mut document);
return Ok((remaining.into(), document));
}
}
// Find final in-buffer settings that do not impact parsing
apply_post_parse_in_buffer_settings(&mut document)
.map_err(|err| nom::Err::<CustomError<OrgSource<'_>>>::Failure(err.into()))?;
apply_post_parse_in_buffer_settings(&mut document);
Ok((remaining.into(), document))
}
@@ -214,3 +195,17 @@ fn _document<'b, 'g, 'r, 's>(
},
))
}
#[cfg(test)]
mod tests {
use test::Bencher;
use super::*;
#[bench]
fn bench_full_document(b: &mut Bencher) {
let input = include_str!("../../org_mode_samples/element_container_priority/README.org");
b.iter(|| assert!(parse(input).is_ok()));
}
}

View File

@@ -7,12 +7,10 @@ use nom::character::complete::space0;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use crate::context::parser_with_context;
@@ -21,7 +19,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::element_parser::element;
use crate::parser::util::blank_line;
@@ -32,23 +29,28 @@ use crate::parser::util::start_of_line;
use crate::parser::util::WORD_CONSTITUENT_CHARACTERS;
use crate::types::Drawer;
use crate::types::Element;
use crate::types::Keyword;
use crate::types::Paragraph;
use crate::types::SetSource;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn drawer<'b, 'g, 'r, 's>(
pub(crate) fn drawer<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Drawer<'s>> {
) -> Res<OrgSource<'s>, Drawer<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
if immediate_in_section(context, "drawer") {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Cannot nest objects of the same element",
)));
}
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
start_of_line(remaining)?;
let (remaining, _leading_whitespace) = space0(remaining)?;
let (remaining, (_open_colon, drawer_name, _close_colon, _new_line)) = tuple((

View File

@@ -18,7 +18,6 @@ use nom::sequence::preceded;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use crate::context::parser_with_context;
@@ -27,7 +26,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::element_parser::element;
use crate::parser::util::blank_line;
@@ -37,23 +35,28 @@ use crate::parser::util::immediate_in_section;
use crate::parser::util::start_of_line;
use crate::types::DynamicBlock;
use crate::types::Element;
use crate::types::Keyword;
use crate::types::Paragraph;
use crate::types::SetSource;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn dynamic_block<'b, 'g, 'r, 's>(
pub(crate) fn dynamic_block<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, DynamicBlock<'s>> {
) -> Res<OrgSource<'s>, DynamicBlock<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
if immediate_in_section(context, "dynamic block") {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Cannot nest objects of the same element",
)));
}
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
start_of_line(remaining)?;
let (remaining, _leading_whitespace) = space0(remaining)?;
@@ -75,10 +78,7 @@ pub(crate) fn dynamic_block<'b, 'g, 'r, 's>(
let parser_context = context.with_additional_node(&contexts[0]);
let parser_context = parser_context.with_additional_node(&contexts[1]);
let parser_context = parser_context.with_additional_node(&contexts[2]);
let parameters = match parameters {
Some((_ws, parameters)) => Some(parameters),
None => None,
};
let parameters = parameters.map(|(_ws, parameters)| parameters);
let element_matcher = parser_with_context!(element(true))(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
not(exit_matcher)(remaining)?;

View File

@@ -1,11 +1,3 @@
use nom::branch::alt;
use nom::combinator::map;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::sequence::tuple;
#[cfg(feature = "tracing")]
use tracing::span;
use super::babel_call::babel_call;
use super::clock::clock;
use super::comment::comment;
@@ -20,8 +12,6 @@ use super::footnote_definition::detect_footnote_definition;
use super::footnote_definition::footnote_definition;
use super::greater_block::greater_block;
use super::horizontal_rule::horizontal_rule;
use super::keyword::affiliated_keyword;
use super::keyword::affiliated_keyword_as_regular_keyword;
use super::keyword::keyword;
use super::latex_environment::latex_environment;
use super::lesser_block::comment_block;
@@ -34,11 +24,18 @@ use super::paragraph::paragraph;
use super::plain_list::detect_plain_list;
use super::plain_list::plain_list;
use super::table::detect_table;
use crate::context::parser_with_context;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
#[cfg(feature = "event_count")]
use crate::event_count::record_event;
#[cfg(feature = "event_count")]
use crate::event_count::EventType;
use crate::parser::affiliated_keyword::affiliated_keywords;
use crate::parser::bullshitium::bullshitium;
use crate::parser::bullshitium::detect_bullshitium;
use crate::parser::macros::ak_element;
use crate::parser::macros::element;
use crate::parser::table::org_mode_table;
use crate::types::Element;
@@ -60,117 +57,207 @@ fn _element<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
can_be_paragraph: bool,
) -> Res<OrgSource<'s>, Element<'s>> {
let plain_list_matcher = parser_with_context!(plain_list)(context);
let greater_block_matcher = parser_with_context!(greater_block)(context);
let dynamic_block_matcher = parser_with_context!(dynamic_block)(context);
let footnote_definition_matcher = parser_with_context!(footnote_definition)(context);
let comment_matcher = parser_with_context!(comment)(context);
let drawer_matcher = parser_with_context!(drawer)(context);
let table_matcher = parser_with_context!(org_mode_table)(context);
let verse_block_matcher = parser_with_context!(verse_block)(context);
let comment_block_matcher = parser_with_context!(comment_block)(context);
let example_block_matcher = parser_with_context!(example_block)(context);
let export_block_matcher = parser_with_context!(export_block)(context);
let src_block_matcher = parser_with_context!(src_block)(context);
let clock_matcher = parser_with_context!(clock)(context);
let diary_sexp_matcher = parser_with_context!(diary_sexp)(context);
let fixed_width_area_matcher = parser_with_context!(fixed_width_area)(context);
let horizontal_rule_matcher = parser_with_context!(horizontal_rule)(context);
let keyword_matcher = parser_with_context!(keyword)(context);
let babel_keyword_matcher = parser_with_context!(babel_call)(context);
let paragraph_matcher = parser_with_context!(paragraph)(context);
let latex_environment_matcher = parser_with_context!(latex_environment)(context);
#[cfg(feature = "event_count")]
record_event(EventType::ElementStart, input);
let (post_affiliated_keywords_input, affiliated_keywords) = affiliated_keywords(input)?;
let (mut remaining, mut maybe_element) = {
#[cfg(feature = "tracing")]
let span = span!(tracing::Level::DEBUG, "Main element block");
#[cfg(feature = "tracing")]
let _enter = span.enter();
let mut affiliated_keywords = affiliated_keywords.into_iter();
opt(alt((
map(plain_list_matcher, Element::PlainList),
greater_block_matcher,
map(dynamic_block_matcher, Element::DynamicBlock),
map(footnote_definition_matcher, Element::FootnoteDefinition),
map(comment_matcher, Element::Comment),
map(drawer_matcher, Element::Drawer),
map(table_matcher, Element::Table),
map(verse_block_matcher, Element::VerseBlock),
map(comment_block_matcher, Element::CommentBlock),
map(example_block_matcher, Element::ExampleBlock),
map(export_block_matcher, Element::ExportBlock),
map(src_block_matcher, Element::SrcBlock),
map(clock_matcher, Element::Clock),
map(diary_sexp_matcher, Element::DiarySexp),
map(fixed_width_area_matcher, Element::FixedWidthArea),
map(horizontal_rule_matcher, Element::HorizontalRule),
map(latex_environment_matcher, Element::LatexEnvironment),
map(babel_keyword_matcher, Element::BabelCall),
map(keyword_matcher, Element::Keyword),
)))(input)?
};
ak_element!(
plain_list,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::PlainList
);
if maybe_element.is_none() && can_be_paragraph {
#[cfg(feature = "tracing")]
let span = span!(tracing::Level::DEBUG, "Paragraph with affiliated keyword.");
#[cfg(feature = "tracing")]
let _enter = span.enter();
ak_element!(
greater_block,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input
);
let (remain, paragraph_with_affiliated_keyword) = opt(map(
tuple((
peek(affiliated_keyword),
map(paragraph_matcher, Element::Paragraph),
)),
|(_, paragraph)| paragraph,
))(remaining)?;
if paragraph_with_affiliated_keyword.is_some() {
remaining = remain;
maybe_element = paragraph_with_affiliated_keyword;
}
}
ak_element!(
dynamic_block,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::DynamicBlock
);
if maybe_element.is_none() {
#[cfg(feature = "tracing")]
let span = span!(
tracing::Level::DEBUG,
"Affiliated keyword as regular keyword."
ak_element!(
footnote_definition,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::FootnoteDefinition
);
element!(comment, context, input, Element::Comment);
ak_element!(
drawer,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::Drawer
);
ak_element!(
org_mode_table,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::Table
);
ak_element!(
verse_block,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::VerseBlock
);
ak_element!(
comment_block,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::CommentBlock
);
ak_element!(
example_block,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::ExampleBlock
);
ak_element!(
export_block,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::ExportBlock
);
ak_element!(
src_block,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::SrcBlock
);
element!(clock, context, input, Element::Clock);
ak_element!(
diary_sexp,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::DiarySexp
);
ak_element!(
fixed_width_area,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::FixedWidthArea
);
ak_element!(
horizontal_rule,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::HorizontalRule
);
ak_element!(
latex_environment,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::LatexEnvironment
);
ak_element!(
babel_call,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::BabelCall
);
// Keyword with affiliated keywords
ak_element!(
keyword,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::Keyword
);
if can_be_paragraph {
// Paragraph with affiliated keyword
ak_element!(
paragraph,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input,
Element::Paragraph
);
#[cfg(feature = "tracing")]
let _enter = span.enter();
let (remain, kw) = opt(map(
parser_with_context!(affiliated_keyword_as_regular_keyword)(context),
Element::Keyword,
))(remaining)?;
if kw.is_some() {
maybe_element = kw;
remaining = remain;
}
}
if maybe_element.is_none() && can_be_paragraph {
#[cfg(feature = "tracing")]
let span = span!(
tracing::Level::DEBUG,
"Paragraph without affiliated keyword."
// Keyword without affiliated keywords
ak_element!(
keyword,
std::iter::empty(),
input,
context,
input,
Element::Keyword
);
if can_be_paragraph {
// Fake paragraphs
element!(bullshitium, context, input, Element::Paragraph);
// Paragraph without affiliated keyword
ak_element!(
paragraph,
std::iter::empty(),
input,
context,
input,
Element::Paragraph
);
#[cfg(feature = "tracing")]
let _enter = span.enter();
let (remain, paragraph_without_affiliated_keyword) =
map(paragraph_matcher, Element::Paragraph)(remaining)?;
remaining = remain;
maybe_element = Some(paragraph_without_affiliated_keyword);
}
if maybe_element.is_none() {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"No element.",
))));
}
let element = maybe_element.expect("The above if-statement ensures this is Some().");
Ok((remaining, element))
Err(nom::Err::Error(CustomError::Static("No element.")))
}
pub(crate) const fn detect_element(
@@ -189,22 +276,60 @@ fn _detect_element<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
can_be_paragraph: bool,
) -> Res<OrgSource<'s>, ()> {
if alt((
parser_with_context!(detect_plain_list)(context),
let (post_affiliated_keywords_input, affiliated_keywords) = affiliated_keywords(input)?;
let mut affiliated_keywords = affiliated_keywords.into_iter();
ak_element!(
detect_plain_list,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input
);
ak_element!(
detect_footnote_definition,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input
);
ak_element!(
detect_diary_sexp,
detect_comment,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input
);
element!(detect_comment, input);
ak_element!(
detect_fixed_width_area,
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input
);
ak_element!(
detect_table,
))(input)
.is_ok()
{
return Ok((input, ()));
&mut affiliated_keywords,
post_affiliated_keywords_input,
context,
input
);
// Fake paragraphs
if !can_be_paragraph {
element!(detect_bullshitium, context, input);
}
if _element(context, input, can_be_paragraph).is_ok() {
return Ok((input, ()));
}
return Err(nom::Err::Error(CustomError::MyError(MyError(
"No element detected.".into(),
))));
Err(nom::Err::Error(CustomError::Static("No element detected.")))
}

View File

@@ -1,11 +1,11 @@
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::satisfy;
use nom::combinator::cond;
use nom::combinator::eof;
use nom::combinator::map;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::sequence::tuple;
use super::org_source::OrgSource;
@@ -13,7 +13,6 @@ use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting;
use crate::context::EntityDefinition;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::get_consumed;
use crate::types::Entity;
@@ -58,23 +57,21 @@ fn name<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, (&'g EntityDefinition<'s>, OrgSource<'s>, bool)> {
for entity in context.get_global_settings().entities {
let result = tuple((
tag::<_, _, CustomError<_>>(entity.name),
alt((
verify(map(tag("{}"), |_| true), |_| !entity.name.ends_with(" ")),
map(peek(recognize(entity_end)), |_| false),
)),
tag::<_, _, CustomError>(entity.name),
cond(
!entity.name.ends_with(' '),
alt((
map(tag("{}"), |_| true),
map(peek(recognize(entity_end)), |_| false),
)),
),
))(input);
match result {
Ok((remaining, (ent, use_brackets))) => {
return Ok((remaining, (entity, ent, use_brackets)));
}
Err(_) => {}
if let Ok((remaining, (ent, use_brackets))) = result {
return Ok((remaining, (entity, ent, use_brackets.unwrap_or(false))));
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"NoEntity".into(),
))))
Err(nom::Err::Error(CustomError::Static("NoEntity")))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -10,7 +10,6 @@ use nom::sequence::preceded;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use super::util::org_line_ending;
@@ -21,16 +20,21 @@ use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed;
use crate::parser::util::start_of_line;
use crate::types::FixedWidthArea;
use crate::types::Keyword;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn fixed_width_area<'b, 'g, 'r, 's>(
pub(crate) fn fixed_width_area<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, FixedWidthArea<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, FixedWidthArea<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let fixed_width_area_line_matcher = parser_with_context!(fixed_width_area_line)(context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(context);
let (remaining, first_line) = fixed_width_area_line_matcher(remaining)?;
@@ -84,14 +88,24 @@ fn fixed_width_area_line<'b, 'g, 'r, 's>(
Ok((remaining, value))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn detect_fixed_width_area<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
let (input, _) = many0(affiliated_keyword)(input)?;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(_context, _affiliated_keywords))
)]
pub(crate) fn detect_fixed_width_area<'b, 'g, 'r, 's, AK>(
_affiliated_keywords: AK,
remaining: OrgSource<'s>,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
tuple((
start_of_line,
space0,
tag(":"),
alt((tag(" "), org_line_ending)),
))(input)?;
))(remaining)?;
Ok((input, ()))
}

View File

@@ -12,7 +12,6 @@ use nom::multi::many_till;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::include_input;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
@@ -23,7 +22,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::element_parser::element;
use crate::parser::util::blank_line;
@@ -33,21 +31,26 @@ use crate::parser::util::immediate_in_section;
use crate::parser::util::maybe_consume_trailing_whitespace;
use crate::parser::util::start_of_line;
use crate::types::FootnoteDefinition;
use crate::types::Keyword;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn footnote_definition<'b, 'g, 'r, 's>(
pub(crate) fn footnote_definition<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, FootnoteDefinition<'s>> {
) -> Res<OrgSource<'s>, FootnoteDefinition<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
if immediate_in_section(context, "footnote definition") {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Cannot nest objects of the same element",
)));
}
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
start_of_line(remaining)?;
// Cannot be indented.
let (remaining, (_, lbl, _, _, _)) = tuple((
@@ -120,7 +123,7 @@ fn footnote_definition_end<'b, 'g, 'r, 's>(
let (remaining, source) = alt((
recognize(tuple((
parser_with_context!(maybe_consume_trailing_whitespace)(context),
detect_footnote_definition,
|i| detect_footnote_definition(std::iter::empty(), i, context, i),
))),
recognize(tuple((
start_of_line,
@@ -133,16 +136,27 @@ fn footnote_definition_end<'b, 'g, 'r, 's>(
Ok((remaining, source))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn detect_footnote_definition<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
let (input, _) = many0(affiliated_keyword)(input)?;
tuple((start_of_line, tag_no_case("[fn:"), label, tag("]")))(input)?;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(_context, _affiliated_keywords))
)]
pub(crate) fn detect_footnote_definition<'b, 'g, 'r, 's, AK>(
_affiliated_keywords: AK,
remaining: OrgSource<'s>,
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
tuple((start_of_line, tag_no_case("[fn:"), label, tag("]")))(remaining)?;
Ok((input, ()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::bind_context;
use crate::context::Context;
use crate::context::GlobalSettings;
use crate::context::List;
@@ -160,7 +174,7 @@ line footnote.",
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let footnote_definition_matcher = parser_with_context!(element(true))(&initial_context);
let footnote_definition_matcher = bind_context!(element(true), &initial_context);
let (remaining, first_footnote_definition) =
footnote_definition_matcher(input).expect("Parse first footnote_definition");
let (remaining, second_footnote_definition) =
@@ -197,7 +211,7 @@ not in the footnote.",
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let footnote_definition_matcher = parser_with_context!(element(true))(&initial_context);
let footnote_definition_matcher = bind_context!(element(true), &initial_context);
let (remaining, first_footnote_definition) =
footnote_definition_matcher(input).expect("Parse first footnote_definition");
assert_eq!(Into::<&str>::into(remaining), "not in the footnote.");

View File

@@ -20,7 +20,6 @@ use crate::context::ExitMatcherNode;
use crate::context::List;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::footnote_definition::label;
use crate::parser::object_parser::standard_set_object;
@@ -176,9 +175,9 @@ fn _footnote_definition_end<'b, 'g, 'r, 's>(
let current_depth = input.get_bracket_depth() - starting_bracket_depth;
if current_depth > 0 {
// Its impossible for the next character to end the footnote reference definition if we're any amount of brackets deep
return Err(nom::Err::Error(CustomError::MyError(MyError(
"NoFootnoteReferenceDefinitionEnd".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"NoFootnoteReferenceDefinitionEnd",
)));
}
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing bracket should end the footnote definition.

View File

@@ -18,7 +18,6 @@ use nom::sequence::preceded;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::in_section;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
@@ -29,7 +28,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::element_parser::element;
use crate::parser::util::blank_line;
@@ -46,23 +44,26 @@ use crate::types::SpecialBlock;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn greater_block<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
pub(crate) fn greater_block<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Element<'s>> {
let pre_affiliated_keywords_input = input;
let (input, affiliated_keywords) = many0(affiliated_keyword)(input)?;
context: RefContext<'b, 'g, 'r, 's>,
pre_affiliated_keywords_input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Element<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(input)?;
let (remaining, (_begin, name)) = tuple((
tag_no_case("#+begin_"),
verify(name, |name: &OrgSource<'_>| {
match Into::<&str>::into(name).to_lowercase().as_str() {
"comment" | "example" | "export" | "src" | "verse" => false,
_ => true,
}
!matches!(
Into::<&str>::into(name).to_lowercase().as_str(),
"comment" | "example" | "export" | "src" | "verse",
)
}),
))(remaining)?;
let name = Into::<&str>::into(name);
@@ -91,14 +92,17 @@ pub(crate) fn greater_block<'b, 'g, 'r, 's>(
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
fn center_block<'b, 'g, 'r, 's>(
fn center_block<'b, 'g, 'r, 's, AK>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
pre_affiliated_keywords_input: OrgSource<'s>,
affiliated_keywords: Vec<Keyword<'s>>,
) -> Res<OrgSource<'s>, Element<'s>> {
affiliated_keywords: AK,
) -> Res<OrgSource<'s>, Element<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, (source, children)) = greater_block_body(
context,
input,
@@ -121,14 +125,17 @@ fn center_block<'b, 'g, 'r, 's>(
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
fn quote_block<'b, 'g, 'r, 's>(
fn quote_block<'b, 'g, 'r, 's, AK>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
pre_affiliated_keywords_input: OrgSource<'s>,
affiliated_keywords: Vec<Keyword<'s>>,
) -> Res<OrgSource<'s>, Element<'s>> {
affiliated_keywords: AK,
) -> Res<OrgSource<'s>, Element<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, (source, children)) = greater_block_body(
context,
input,
@@ -149,15 +156,18 @@ fn quote_block<'b, 'g, 'r, 's>(
))
}
fn special_block<'s>(
fn special_block<'s, AK>(
name: &'s str,
) -> impl for<'b, 'g, 'r> Fn(
RefContext<'b, 'g, 'r, 's>,
OrgSource<'s>,
OrgSource<'s>,
Vec<Keyword<'s>>,
AK,
) -> Res<OrgSource<'s>, Element<'s>>
+ 's {
+ 's
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let context_name = format!("special block {}", name);
move |context, input, pre_affiliated_keywords_input, affiliated_keywords| {
_special_block(
@@ -173,16 +183,19 @@ fn special_block<'s>(
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
fn _special_block<'c, 'b, 'g, 'r, 's>(
fn _special_block<'c, 'b, 'g, 'r, 's, AK>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
pre_affiliated_keywords_input: OrgSource<'s>,
name: &'s str,
context_name: &'c str,
affiliated_keywords: Vec<Keyword<'s>>,
) -> Res<OrgSource<'s>, Element<'s>> {
affiliated_keywords: AK,
) -> Res<OrgSource<'s>, Element<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, parameters) = opt(tuple((space1, parameters)))(input)?;
let (remaining, (source, children)) = greater_block_body(
context,
@@ -218,9 +231,9 @@ fn greater_block_body<'c, 'b, 'g, 'r, 's>(
context_name: &'c str,
) -> Res<OrgSource<'s>, (&'s str, Vec<Element<'s>>)> {
if in_section(context, context_name) {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Cannot nest objects of the same element",
)));
}
let exit_with_name = greater_block_end(name);
let (remaining, _nl) = tuple((space0, line_ending))(input)?;
@@ -274,7 +287,7 @@ fn parameters<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(many_till(anychar, peek(tuple((space0, line_ending)))))(input)
}
fn greater_block_end<'c>(name: &'c str) -> impl ContextMatcher + 'c {
fn greater_block_end(name: &str) -> impl ContextMatcher + '_ {
move |context, input: OrgSource<'_>| _greater_block_end(context, input, name)
}

View File

@@ -18,18 +18,18 @@ use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::section::section;
use super::util::exit_matcher_parser;
use super::util::get_consumed;
use super::util::org_line_ending;
use super::util::org_space;
use super::util::org_space_or_line_ending;
use super::util::start_of_line;
use crate::context::parser_with_context;
use crate::context::bind_context;
use crate::context::ContextElement;
use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::object_parser::standard_set_object;
use crate::parser::util::blank_line;
@@ -62,10 +62,10 @@ fn _heading<'b, 'g, 'r, 's>(
let mut scheduled = None;
let mut deadline = None;
let mut closed = None;
not(|i| context.check_exit_matcher(i))(input)?;
not(bind_context!(exit_matcher_parser, context))(input)?;
let (remaining, pre_headline) = headline(context, input, parent_star_count)?;
let section_matcher = parser_with_context!(section)(context);
let heading_matcher = parser_with_context!(heading(pre_headline.star_count))(context);
let section_matcher = bind_context!(section, context);
let heading_matcher = bind_context!(heading(pre_headline.star_count), context);
let (remaining, maybe_section) =
opt(map(section_matcher, DocumentElement::Section))(remaining)?;
let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?;
@@ -155,7 +155,7 @@ fn headline<'b, 'g, 'r, 's>(
let (remaining, (_, (headline_level, star_count, _), _)) = tuple((
start_of_line,
verify(
parser_with_context!(headline_level)(&parser_context),
bind_context!(headline_level, &parser_context),
|(_, count, _)| *count > parent_star_count,
),
peek(org_space),
@@ -163,7 +163,7 @@ fn headline<'b, 'g, 'r, 's>(
let (remaining, maybe_todo_keyword) = opt(tuple((
space1,
parser_with_context!(heading_keyword)(&parser_context),
bind_context!(heading_keyword, &parser_context),
peek(org_space_or_line_ending),
)))(remaining)?;
@@ -177,9 +177,7 @@ fn headline<'b, 'g, 'r, 's>(
let (remaining, maybe_title) = opt(tuple((
space1,
consumed(many1(parser_with_context!(standard_set_object)(
&parser_context,
))),
consumed(many1(bind_context!(standard_set_object, &parser_context))),
)))(remaining)?;
let (remaining, maybe_tags) = opt(tuple((space0, tags)))(remaining)?;
@@ -206,11 +204,7 @@ fn headline<'b, 'g, 'r, 's>(
.map(|(_, (_, title))| title)
.unwrap_or(Vec::new()),
tags: maybe_tags
.map(|(_ws, tags)| {
tags.into_iter()
.map(|single_tag| Into::<&str>::into(single_tag))
.collect()
})
.map(|(_ws, tags)| tags.into_iter().map(Into::<&str>::into).collect())
.unwrap_or(Vec::new()),
is_footnote_section,
},
@@ -264,12 +258,9 @@ fn heading_keyword<'b, 'g, 'r, 's>(
.iter()
.map(String::as_str)
{
let result = tag::<_, _, CustomError<_>>(todo_keyword)(input);
match result {
Ok((remaining, ent)) => {
return Ok((remaining, (TodoKeywordType::Todo, ent)));
}
Err(_) => {}
let result = tag::<_, _, CustomError>(todo_keyword)(input);
if let Ok((remaining, ent)) = result {
return Ok((remaining, (TodoKeywordType::Todo, ent)));
}
}
for todo_keyword in global_settings
@@ -277,30 +268,25 @@ fn heading_keyword<'b, 'g, 'r, 's>(
.iter()
.map(String::as_str)
{
let result = tag::<_, _, CustomError<_>>(todo_keyword)(input);
match result {
Ok((remaining, ent)) => {
return Ok((remaining, (TodoKeywordType::Done, ent)));
}
Err(_) => {}
let result = tag::<_, _, CustomError>(todo_keyword)(input);
if let Ok((remaining, ent)) = result {
return Ok((remaining, (TodoKeywordType::Done, ent)));
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"NoTodoKeyword".into(),
))))
Err(nom::Err::Error(CustomError::Static("NoTodoKeyword")))
}
}
fn priority_cookie<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PriorityCookie> {
fn priority_cookie(input: OrgSource<'_>) -> Res<OrgSource<'_>, PriorityCookie> {
let (remaining, (_, priority_character, _)) = tuple((
tag("[#"),
verify(anychar, |c| c.is_alphanumeric()),
tag("]"),
))(input)?;
let cookie = PriorityCookie::try_from(priority_character).map_err(|_| {
nom::Err::Error(CustomError::MyError(MyError(
"Failed to cast priority cookie to number.".into(),
)))
nom::Err::Error(CustomError::Static(
"Failed to cast priority cookie to number.",
))
})?;
Ok((remaining, cookie))
}

View File

@@ -5,12 +5,10 @@ use nom::character::complete::space0;
use nom::combinator::eof;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many1_count;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::get_consumed;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
@@ -18,16 +16,21 @@ use crate::context::RefContext;
use crate::error::Res;
use crate::parser::util::start_of_line;
use crate::types::HorizontalRule;
use crate::types::Keyword;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn horizontal_rule<'b, 'g, 'r, 's>(
pub(crate) fn horizontal_rule<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, HorizontalRule<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, HorizontalRule<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
start_of_line(remaining)?;
let (remaining, _rule) = recognize(tuple((
space0,

View File

@@ -35,7 +35,7 @@ pub(crate) fn scan_for_in_buffer_settings<'s>(
let mut remaining = input;
loop {
// Skip text until possible in_buffer_setting
let start_of_pound = take_until::<_, _, CustomError<_>>("#+")(remaining);
let start_of_pound = take_until::<_, _, CustomError>("#+")(remaining);
let start_of_pound = if let Ok((start_of_pound, _)) = start_of_pound {
start_of_pound
} else {
@@ -47,7 +47,7 @@ pub(crate) fn scan_for_in_buffer_settings<'s>(
let (remain, maybe_kw) = match filtered_keyword(in_buffer_settings_key)(start_of_line) {
Ok((remain, kw)) => (remain, Some(kw)),
Err(_) => {
let end_of_line = take_until::<_, _, CustomError<_>>("\n")(start_of_pound);
let end_of_line = take_until::<_, _, CustomError>("\n")(start_of_pound);
if let Ok((end_of_line, _)) = end_of_line {
(end_of_line, None)
} else {
@@ -84,11 +84,14 @@ fn in_buffer_settings_key<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSou
))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug"))]
#[cfg_attr(
feature = "tracing",
tracing::instrument(level = "debug", skip(original_settings))
)]
pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
keywords: Vec<Keyword<'sf>>,
original_settings: &'g GlobalSettings<'g, 's>,
) -> Result<GlobalSettings<'g, 's>, String> {
) -> Result<GlobalSettings<'g, 's>, CustomError> {
let mut new_settings = original_settings.clone();
// Todo Keywords
@@ -98,7 +101,11 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
|| kw.key.eq_ignore_ascii_case("typ_todo")
}) {
let (_, (in_progress_words, complete_words)) =
todo_keywords(kw.value).map_err(|err| err.to_string())?;
todo_keywords(kw.value).map_err(|err| match err {
nom::Err::Incomplete(_) => CustomError::Text(err.to_string()),
nom::Err::Error(e) => e,
nom::Err::Failure(e) => e,
})?;
new_settings
.in_progress_todo_keywords
.extend(in_progress_words.into_iter().map(str::to_string));
@@ -112,9 +119,14 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
.iter()
.filter(|kw| kw.key.eq_ignore_ascii_case("startup"))
{
let (_remaining, settings) =
separated_list0(space1::<&str, nom::error::Error<_>>, is_not(" \t"))(kw.value)
.map_err(|err: nom::Err<_>| err.to_string())?;
let (_remaining, settings) = separated_list0(space1::<&str, CustomError>, is_not(" \t"))(
kw.value,
)
.map_err(|err: nom::Err<_>| match err {
nom::Err::Incomplete(_) => CustomError::Text(err.to_string()),
nom::Err::Error(e) => e,
nom::Err::Failure(e) => e,
})?;
if settings.contains(&"odd") {
new_settings.odd_levels_only = HeadlineLevelFilter::Odd;
}
@@ -128,7 +140,11 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
.iter()
.filter(|kw| kw.key.eq_ignore_ascii_case("link"))
{
let (_, (link_key, link_value)) = link_template(kw.value).map_err(|e| e.to_string())?;
let (_, (link_key, link_value)) = link_template(kw.value).map_err(|err| match err {
nom::Err::Incomplete(_) => CustomError::Text(err.to_string()),
nom::Err::Error(e) => e,
nom::Err::Failure(e) => e,
})?;
new_settings
.link_templates
.insert(link_key.to_owned(), link_value.to_owned());
@@ -139,9 +155,7 @@ pub(crate) fn apply_in_buffer_settings<'g, 's, 'sf>(
/// Apply in-buffer settings that do not impact parsing and therefore can be applied after parsing.
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(
document: &mut Document<'s>,
) -> Result<(), &'static str> {
pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(document: &mut Document<'s>) {
document.category = Into::<AstNode>::into(&*document)
.into_iter()
.filter_map(|ast_node| {
@@ -154,7 +168,6 @@ pub(crate) fn apply_post_parse_in_buffer_settings<'g, 's, 'sf>(
})
.last()
.map(|kw| kw.value.to_owned());
Ok(())
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]

View File

@@ -19,7 +19,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed;
@@ -131,9 +130,7 @@ fn _header_end<'b, 'g, 'r, 's>(
let current_depth = input.get_bracket_depth() - starting_bracket_depth;
if current_depth > 0 {
// Its impossible for the next character to end the header if we're any amount of bracket deep
return Err(nom::Err::Error(CustomError::MyError(MyError(
"NoHeaderEnd".into(),
))));
return Err(nom::Err::Error(CustomError::Static("NoHeaderEnd")));
}
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing bracket should end the header.
@@ -183,9 +180,7 @@ fn _argument_end<'b, 'g, 'r, 's>(
let current_depth = input.get_parenthesis_depth() - starting_parenthesis_depth;
if current_depth > 0 {
// Its impossible for the next character to end the argument if we're any amount of parenthesis deep
return Err(nom::Err::Error(CustomError::MyError(MyError(
"NoArgumentEnd".into(),
))));
return Err(nom::Err::Error(CustomError::Static("NoArgumentEnd")));
}
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing parenthesis should end the argument.

View File

@@ -21,7 +21,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed;
@@ -125,9 +124,7 @@ fn _header_end<'b, 'g, 'r, 's>(
let current_depth = input.get_bracket_depth() - starting_bracket_depth;
if current_depth > 0 {
// Its impossible for the next character to end the header if we're any amount of bracket deep
return Err(nom::Err::Error(CustomError::MyError(MyError(
"NoHeaderEnd".into(),
))));
return Err(nom::Err::Error(CustomError::Static("NoHeaderEnd")));
}
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing bracket should end the header.
@@ -187,9 +184,7 @@ fn _body_end<'b, 'g, 'r, 's>(
let current_depth = input.get_brace_depth() - starting_brace_depth;
if current_depth > 0 {
// Its impossible for the next character to end the body if we're any amount of brace deep
return Err(nom::Err::Error(CustomError::MyError(MyError(
"NoBodyEnd".into(),
))));
return Err(nom::Err::Error(CustomError::Static("NoBodyEnd")));
}
if current_depth < 0 {
// This shouldn't be possible because if depth is 0 then a closing brace should end the body.

View File

@@ -4,16 +4,14 @@ use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case;
use nom::bytes::complete::take_while1;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::one_of;
use nom::character::complete::space0;
use nom::combinator::consumed;
use nom::combinator::eof;
use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::tuple;
@@ -22,24 +20,20 @@ use super::org_source::BracketDepth;
use super::org_source::OrgSource;
use super::util::get_consumed;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use crate::context::Matcher;
use super::util::org_line_ending;
use crate::context::constants::ORG_ELEMENT_AFFILIATED_KEYWORDS;
use crate::context::constants::ORG_ELEMENT_DUAL_KEYWORDS;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::macros::element;
use crate::parser::util::start_of_line;
use crate::types::AffiliatedKeywords;
use crate::types::Keyword;
const ORG_ELEMENT_AFFILIATED_KEYWORDS: [&'static str; 13] = [
"caption", "data", "headers", "header", "label", "name", "plot", "resname", "results",
"result", "source", "srcname", "tblname",
];
const ORG_ELEMENT_DUAL_KEYWORDS: [&'static str; 2] = ["caption", "results"];
pub(crate) fn filtered_keyword<F: Matcher>(
pub(crate) fn filtered_keyword<'s, F: Fn(OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>>>(
key_parser: F,
) -> impl for<'s> Fn(OrgSource<'s>) -> Res<OrgSource<'s>, Keyword<'s>> {
) -> impl Fn(OrgSource<'s>) -> Res<OrgSource<'s>, Keyword<'s>> {
move |input| _filtered_keyword(&key_parser, input)
}
@@ -47,7 +41,7 @@ pub(crate) fn filtered_keyword<F: Matcher>(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(key_parser))
)]
fn _filtered_keyword<'s, F: Matcher>(
fn _filtered_keyword<'s, F: Fn(OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>>>(
key_parser: F,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Keyword<'s>> {
@@ -55,30 +49,21 @@ fn _filtered_keyword<'s, F: Matcher>(
// TODO: When key is a member of org-element-parsed-keywords, value can contain the standard set objects, excluding footnote references.
let (remaining, (consumed_input, (_, _, parsed_key, _))) =
consumed(tuple((space0, tag("#+"), key_parser, tag(":"))))(input)?;
match tuple((
space0::<OrgSource<'_>, CustomError<OrgSource<'_>>>,
alt((line_ending, eof)),
))(remaining)
{
Ok((remaining, _)) => {
return Ok((
remaining,
Keyword {
source: consumed_input.into(),
affiliated_keywords: AffiliatedKeywords::default(), // To be populated by the caller if this keyword is in a context to support affiliated keywords.
key: parsed_key.into(),
value: "".into(),
},
));
}
Err(_) => {}
};
let (remaining, _ws) = space0(remaining)?;
let (remaining, parsed_value) = recognize(many_till(
anychar,
peek(tuple((space0, alt((line_ending, eof))))),
))(remaining)?;
let (remaining, _ws) = tuple((space0, alt((line_ending, eof))))(remaining)?;
if let Ok((remaining, _)) = org_line_ending(remaining) {
return Ok((
remaining,
Keyword {
source: consumed_input.into(),
affiliated_keywords: AffiliatedKeywords::default(), // To be populated by the caller if this keyword is in a context to support affiliated keywords.
key: parsed_key.into(),
value: "",
},
));
}
let (remaining, parsed_value) =
recognize(many_till(anychar, peek(tuple((space0, org_line_ending)))))(remaining)?;
let (remaining, _ws) = tuple((space0, org_line_ending))(remaining)?;
Ok((
remaining,
Keyword {
@@ -92,13 +77,17 @@ fn _filtered_keyword<'s, F: Matcher>(
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn keyword<'b, 'g, 'r, 's>(
pub(crate) fn keyword<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Keyword<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, Keyword<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, mut kw) = filtered_keyword(regular_keyword_key)(remaining)?;
let (remaining, _trailing_ws) =
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
@@ -109,22 +98,6 @@ pub(crate) fn keyword<'b, 'g, 'r, 's>(
Ok((remaining, kw))
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
)]
pub(crate) fn affiliated_keyword_as_regular_keyword<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Keyword<'s>> {
let (remaining, mut kw) = affiliated_keyword(input)?;
let (remaining, _trailing_ws) =
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
let source = get_consumed(input, remaining);
kw.source = Into::<&str>::into(source);
Ok((remaining, kw))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn affiliated_keyword<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, Keyword<'s>> {
filtered_keyword(affiliated_key)(input)
@@ -163,45 +136,43 @@ fn regular_keyword_key<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn affiliated_key<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt((
recognize(tuple((dual_affiliated_key, tag("["), optval, tag("]")))),
plain_affiliated_key,
export_keyword,
))(input)
element!(dual_affiliated_key, input);
element!(plain_affiliated_key, input);
element!(export_keyword, input);
Err(nom::Err::Error(CustomError::Static("No affiliated key.")))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn plain_affiliated_key<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
for keyword in ORG_ELEMENT_AFFILIATED_KEYWORDS {
let result = tag_no_case::<_, _, CustomError<_>>(keyword)(input);
match result {
Ok((remaining, ent)) => {
return Ok((remaining, ent));
}
Err(_) => {}
let result = map(
tuple((tag_no_case::<_, _, CustomError>(keyword), peek(tag(":")))),
|(key, _)| key,
)(input);
if let Ok((remaining, ent)) = result {
return Ok((remaining, ent));
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"NoKeywordKey".into(),
))))
Err(nom::Err::Error(CustomError::Static("NoKeywordKey")))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn dual_affiliated_key<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
for keyword in ORG_ELEMENT_DUAL_KEYWORDS {
let result = tag_no_case::<_, _, CustomError<_>>(keyword)(input);
match result {
Ok((remaining, ent)) => {
return Ok((remaining, ent));
}
Err(_) => {}
let result = recognize(tuple((
tag_no_case::<_, _, CustomError>(keyword),
tag("["),
optval,
tag("]"),
peek(tag(":")),
)))(input);
if let Ok((remaining, ent)) = result {
return Ok((remaining, ent));
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"NoKeywordKey".into(),
))))
Err(nom::Err::Error(CustomError::Static("NoKeywordKey")))
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@@ -229,7 +200,7 @@ fn _optval_end<'s>(
unreachable!("Exceeded optval bracket depth.")
}
if current_depth == 0 {
let close_bracket = tag::<_, _, CustomError<_>>("]")(input);
let close_bracket = tag::<_, _, CustomError>("]")(input);
if close_bracket.is_ok() {
return close_bracket;
}
@@ -244,3 +215,18 @@ fn export_keyword<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>>
take_while1(|c: char| c.is_alphanumeric() || "-_".contains(c)),
)))(input)
}
#[cfg(test)]
mod tests {
use test::Bencher;
use super::*;
use crate::parser::OrgSource;
#[bench]
fn bench_affiliated_keyword(b: &mut Bencher) {
let input = OrgSource::new("#+CAPTION[*foo*]: bar *baz*");
b.iter(|| assert!(affiliated_keyword(input).is_ok()));
}
}

View File

@@ -19,7 +19,7 @@ use crate::error::Res;
/// Parses the text in the value of a #+TODO keyword.
///
/// Example input: "foo bar baz | lorem ipsum"
pub(crate) fn todo_keywords<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, Vec<&'s str>)> {
pub(crate) fn todo_keywords(input: &str) -> Res<&str, (Vec<&str>, Vec<&str>)> {
let (remaining, mut before_pipe_words) = separated_list0(space1, todo_keyword_word)(input)?;
let (remaining, after_pipe_words) = opt(tuple((
tuple((space0, tag("|"), space0)),
@@ -30,12 +30,9 @@ pub(crate) fn todo_keywords<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, V
Ok((remaining, (before_pipe_words, after_pipe_words)))
} else if !before_pipe_words.is_empty() {
// If there was no pipe, then the last word becomes a completion state instead.
let mut after_pipe_words = Vec::with_capacity(1);
after_pipe_words.push(
before_pipe_words
.pop()
.expect("If-statement proves this is Some."),
);
let after_pipe_words = vec![before_pipe_words
.pop()
.expect("If-statement proves this is Some.")];
Ok((remaining, (before_pipe_words, after_pipe_words)))
} else {
// No words founds
@@ -43,7 +40,7 @@ pub(crate) fn todo_keywords<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, V
}
}
fn todo_keyword_word<'s>(input: &'s str) -> Res<&'s str, &'s str> {
fn todo_keyword_word(input: &str) -> Res<&str, &str> {
let (remaining, keyword) = verify(take_till(|c| "( \t\r\n|".contains(c)), |result: &str| {
!result.is_empty()
})(input)?;

View File

@@ -8,12 +8,10 @@ use nom::character::complete::space0;
use nom::combinator::eof;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::get_consumed;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
@@ -26,27 +24,27 @@ use crate::context::RefContext;
use crate::error::Res;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::start_of_line;
use crate::types::Keyword;
use crate::types::LatexEnvironment;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn latex_environment<'b, 'g, 'r, 's>(
pub(crate) fn latex_environment<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, LatexEnvironment<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, LatexEnvironment<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let value_start = remaining;
start_of_line(remaining)?;
let (remaining, _leading_whitespace) = space0(remaining)?;
let (remaining, (_opening, name, _open_close_brace, _ws, _line_ending)) = tuple((
tag_no_case(r#"\begin{"#),
name,
tag("}"),
space0,
line_ending,
))(remaining)?;
let (remaining, (_opening, name, _open_close_brace, _ws, _line_ending)) =
tuple((tag_no_case(r"\begin{"), name, tag("}"), space0, line_ending))(remaining)?;
let latex_environment_end_specialized = latex_environment_end(name.into());
let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
@@ -124,7 +122,7 @@ fn _latex_environment_end<'b, 'g, 'r, 's, 'c>(
start_of_line(input)?;
let (remaining, _leading_whitespace) = space0(input)?;
let (remaining, (_begin, _name, _close_brace, _ws, _line_ending)) = tuple((
tag_no_case(r#"\end{"#),
tag_no_case(r"\end{"),
tag_no_case(current_name_lower),
tag("}"),
space0,

View File

@@ -17,7 +17,6 @@ use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting;
use crate::context::parser_with_context;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed;
@@ -209,9 +208,9 @@ fn pre<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, ()> {
let preceding_character = input.get_preceding_character();
if let Some('$') = preceding_character {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Not a valid pre character for dollar char fragment.".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Not a valid pre character for dollar char fragment.",
)));
}
Ok((input, ()))
}
@@ -283,9 +282,9 @@ fn close_border<'b, 'g, 'r, 's>(
match preceding_character {
Some(c) if !c.is_whitespace() && !".,;$".contains(c) => Ok((input, ())),
_ => {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Not a valid pre character for dollar char fragment.".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Not a valid pre character for dollar char fragment.",
)));
}
}
}

View File

@@ -13,13 +13,11 @@ use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::tuple;
use nom::InputTake;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use crate::context::parser_with_context;
@@ -29,7 +27,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::object_parser::standard_set_object;
use crate::parser::util::blank_line;
@@ -41,6 +38,7 @@ use crate::types::CharOffsetInLine;
use crate::types::CommentBlock;
use crate::types::ExampleBlock;
use crate::types::ExportBlock;
use crate::types::Keyword;
use crate::types::LineNumber;
use crate::types::Object;
use crate::types::PlainText;
@@ -51,13 +49,17 @@ use crate::types::VerseBlock;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn verse_block<'b, 'g, 'r, 's>(
pub(crate) fn verse_block<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, VerseBlock<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, VerseBlock<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, _) = lesser_block_begin("verse")(context, remaining)?;
let (remaining, parameters) = opt(tuple((space1, data)))(remaining)?;
let (remaining, _nl) = recognize(tuple((space0, line_ending)))(remaining)?;
@@ -73,10 +75,7 @@ pub(crate) fn verse_block<'b, 'g, 'r, 's>(
let parser_context = context.with_additional_node(&contexts[0]);
let parser_context = parser_context.with_additional_node(&contexts[1]);
let parser_context = parser_context.with_additional_node(&contexts[2]);
let parameters = match parameters {
Some((_ws, parameters)) => Some(parameters),
None => None,
};
let parameters = parameters.map(|(_ws, parameters)| parameters);
let object_matcher = parser_with_context!(standard_set_object)(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
@@ -107,7 +106,7 @@ pub(crate) fn verse_block<'b, 'g, 'r, 's>(
context.get_global_settings(),
affiliated_keywords,
),
data: parameters.map(|parameters| Into::<&str>::into(parameters)),
data: parameters.map(Into::<&str>::into),
children,
},
))
@@ -115,13 +114,17 @@ pub(crate) fn verse_block<'b, 'g, 'r, 's>(
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn comment_block<'b, 'g, 'r, 's>(
pub(crate) fn comment_block<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, CommentBlock<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, CommentBlock<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, _) = lesser_block_begin("comment")(context, remaining)?;
let (remaining, _parameters) = opt(tuple((space1, data)))(remaining)?;
let (remaining, _nl) = recognize(tuple((space0, line_ending)))(remaining)?;
@@ -159,13 +162,17 @@ pub(crate) fn comment_block<'b, 'g, 'r, 's>(
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn example_block<'b, 'g, 'r, 's>(
pub(crate) fn example_block<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ExampleBlock<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, ExampleBlock<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, _) = lesser_block_begin("example")(context, remaining)?;
let (remaining, parameters) = opt(alt((
map(tuple((space1, example_switches)), |(_, switches)| switches),
@@ -236,13 +243,17 @@ pub(crate) fn example_block<'b, 'g, 'r, 's>(
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn export_block<'b, 'g, 'r, 's>(
pub(crate) fn export_block<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ExportBlock<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, ExportBlock<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, _) = lesser_block_begin("export")(context, remaining)?;
// https://orgmode.org/worg/org-syntax.html#Blocks claims that export blocks must have a single word for data but testing shows no data and multi-word data still parses as an export block.
let (remaining, export_type) = opt(map(
@@ -280,7 +291,7 @@ pub(crate) fn export_block<'b, 'g, 'r, 's>(
affiliated_keywords,
),
export_type: export_type.map(Into::<&str>::into),
data: parameters.map(|parameters| Into::<&str>::into(parameters)),
data: parameters.map(Into::<&str>::into),
contents,
},
))
@@ -288,13 +299,17 @@ pub(crate) fn export_block<'b, 'g, 'r, 's>(
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn src_block<'b, 'g, 'r, 's>(
pub(crate) fn src_block<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, SrcBlock<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, SrcBlock<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let (remaining, _) = lesser_block_begin("src")(context, remaining)?;
// https://orgmode.org/worg/org-syntax.html#Blocks claims that data is mandatory and must follow the LANGUAGE SWITCHES ARGUMENTS pattern but testing has shown that no data and incorrect data here will still parse to a src block.
let (remaining, language) =
@@ -325,7 +340,7 @@ pub(crate) fn src_block<'b, 'g, 'r, 's>(
let (switches, number_lines, preserve_indent, retain_labels, use_labels, label_format) = {
if let Some(switches) = switches {
(
if switches.source.len() == 0 {
if switches.source.is_empty() {
None
} else {
Some(switches.source)
@@ -371,7 +386,7 @@ fn data<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
is_not("\r\n")(input)
}
fn lesser_block_end<'c>(current_name: &'c str) -> impl ContextMatcher + 'c {
fn lesser_block_end(current_name: &str) -> impl ContextMatcher + '_ {
// Since the lesser block names are statically defined in code, we can simply assert that the name is lowercase instead of causing an allocation by converting to lowercase.
debug_assert!(current_name == current_name.to_lowercase());
move |context, input: OrgSource<'_>| _lesser_block_end(context, input, current_name)
@@ -401,7 +416,7 @@ fn _lesser_block_end<'b, 'g, 'r, 's, 'c>(
/// Parser for the beginning of a lesser block
///
/// current_name MUST be lowercase. We do not do the conversion ourselves because it is not allowed in a const fn.
const fn lesser_block_begin<'c>(current_name: &'c str) -> impl ContextMatcher + 'c {
const fn lesser_block_begin(current_name: &str) -> impl ContextMatcher + '_ {
// TODO: Since this is a const fn, is there ANY way to "generate" functions at compile time?
move |context, input: OrgSource<'_>| _lesser_block_begin(context, input, current_name)
}
@@ -512,11 +527,8 @@ fn _example_src_switches<'s>(
(SwitchState::Normal, "-r") => {
saw_r = true;
use_labels = false;
match retain_labels {
RetainLabels::Yes => {
retain_labels = RetainLabels::No;
}
_ => {}
if let RetainLabels::Yes = retain_labels {
retain_labels = RetainLabels::No;
}
}
(SwitchState::Normal, "-l") => {
@@ -591,7 +603,7 @@ fn _example_src_switches<'s>(
}
}
if !matched_a_word {
return Err(nom::Err::Error(CustomError::MyError(MyError("No words."))));
return Err(nom::Err::Error(CustomError::Static("No words.")));
}
let remaining = last_match_remaining;
@@ -656,7 +668,9 @@ pub(crate) fn content<'b, 'g, 'r, 's>(
}
let (remain, (pre_escape_whitespace, line)) = content_line(remaining)?;
pre_escape_whitespace.map(|val| ret.push_str(Into::<&str>::into(val)));
if let Some(val) = pre_escape_whitespace {
ret.push_str(Into::<&str>::into(val));
}
ret.push_str(line.into());
remaining = remain;
}

View File

@@ -7,7 +7,6 @@ use nom::multi::many0;
use super::org_source::OrgSource;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::get_consumed;
use crate::types::LineBreak;
@@ -21,7 +20,7 @@ pub(crate) fn line_break<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, LineBreak<'s>> {
let (remaining, _) = pre(context, input)?;
let (remaining, _) = tag(r#"\\"#)(remaining)?;
let (remaining, _) = tag(r"\\")(remaining)?;
let (remaining, _) = recognize(many0(one_of(" \t")))(remaining)?;
let (remaining, _) = line_ending(remaining)?;
let source = get_consumed(input, remaining);
@@ -45,9 +44,9 @@ fn pre<'b, 'g, 'r, 's>(
match preceding_character {
// If None, we are at the start of the file
None | Some('\\') => {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Not a valid pre character for line break.".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Not a valid pre character for line break.",
)));
}
_ => {}
};
@@ -55,9 +54,9 @@ fn pre<'b, 'g, 'r, 's>(
let current_line = input.text_since_line_break();
let is_non_empty_line = current_line.chars().any(|c| !c.is_whitespace());
if !is_non_empty_line {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Not a valid pre line for line break.".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Not a valid pre line for line break.",
)));
}
Ok((input, ()))

45
src/parser/macros.rs Normal file
View File

@@ -0,0 +1,45 @@
/// Parse an element that has affiliated keywords.
macro_rules! ak_element {
($parser:expr, $affiliated_keywords:expr, $post_affiliated_keywords_input: expr, $context: expr, $input: expr, $wrapper: expr) => {
if let Ok((remaining, ele)) = $parser(
$affiliated_keywords,
$post_affiliated_keywords_input,
$context,
$input,
) {
return Ok((remaining, $wrapper(ele)));
}
};
($parser:expr, $affiliated_keywords:expr, $post_affiliated_keywords_input: expr, $context: expr, $input: expr) => {
if let Ok((remaining, ele)) = $parser(
$affiliated_keywords,
$post_affiliated_keywords_input,
$context,
$input,
) {
return Ok((remaining, ele));
}
};
}
pub(crate) use ak_element;
macro_rules! element {
($parser:expr, $context: expr, $input: expr, $wrapper: expr) => {
if let Ok((remaining, ele)) = $parser($context, $input) {
return Ok((remaining, $wrapper(ele)));
}
};
($parser:expr, $context: expr, $input: expr) => {
if let Ok((remaining, ele)) = $parser($context, $input) {
return Ok((remaining, ele));
}
};
($parser:expr, $input: expr) => {
if let Ok((remaining, ele)) = $parser($input) {
return Ok((remaining, ele));
}
};
}
pub(crate) use element;

View File

@@ -1,6 +1,7 @@
mod affiliated_keyword;
mod angle_link;
mod babel_call;
mod bullshitium;
mod citation;
mod citation_reference;
mod clock;
@@ -27,6 +28,7 @@ mod latex_environment;
mod latex_fragment;
mod lesser_block;
mod line_break;
mod macros;
mod object_parser;
mod org_macro;
mod org_source;

View File

@@ -1,14 +1,9 @@
use nom::branch::alt;
use nom::combinator::map;
use super::org_source::OrgSource;
use super::plain_text::plain_text;
use super::regular_link::regular_link;
use super::subscript_and_superscript::detect_subscript_or_superscript;
use crate::context::parser_with_context;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::angle_link::angle_link;
use crate::parser::citation::citation;
@@ -19,6 +14,7 @@ use crate::parser::inline_babel_call::inline_babel_call;
use crate::parser::inline_source_block::inline_source_block;
use crate::parser::latex_fragment::latex_fragment;
use crate::parser::line_break::line_break;
use crate::parser::macros::element;
use crate::parser::org_macro::org_macro;
use crate::parser::plain_link::plain_link;
use crate::parser::radio_link::radio_link;
@@ -39,14 +35,14 @@ pub(crate) fn standard_set_object<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, object) = alt((
parser_with_context!(standard_set_object_sans_plain_text)(context),
map(
parser_with_context!(plain_text(detect_standard_set_object_sans_plain_text))(context),
Object::PlainText,
),
))(input)?;
Ok((remaining, object))
element!(standard_set_object_sans_plain_text, context, input);
element!(
plain_text(detect_standard_set_object_sans_plain_text),
context,
input,
Object::PlainText
);
Err(nom::Err::Error(CustomError::Static("No object.")))
}
#[cfg_attr(
@@ -57,14 +53,14 @@ pub(crate) fn minimal_set_object<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, object) = alt((
parser_with_context!(minimal_set_object_sans_plain_text)(context),
map(
parser_with_context!(plain_text(detect_minimal_set_object_sans_plain_text))(context),
Object::PlainText,
),
))(input)?;
Ok((remaining, object))
element!(minimal_set_object_sans_plain_text, context, input);
element!(
plain_text(detect_minimal_set_object_sans_plain_text),
context,
input,
Object::PlainText
);
Err(nom::Err::Error(CustomError::Static("No object.")))
}
#[cfg_attr(
@@ -75,56 +71,38 @@ fn standard_set_object_sans_plain_text<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, object) = alt((
map(parser_with_context!(timestamp)(context), Object::Timestamp),
map(parser_with_context!(subscript)(context), Object::Subscript),
map(
parser_with_context!(superscript)(context),
Object::Superscript,
),
map(
parser_with_context!(statistics_cookie)(context),
Object::StatisticsCookie,
),
map(parser_with_context!(target)(context), Object::Target),
map(parser_with_context!(line_break)(context), Object::LineBreak),
map(
parser_with_context!(inline_source_block)(context),
Object::InlineSourceBlock,
),
map(
parser_with_context!(inline_babel_call)(context),
Object::InlineBabelCall,
),
map(parser_with_context!(citation)(context), Object::Citation),
map(
parser_with_context!(footnote_reference)(context),
Object::FootnoteReference,
),
map(
parser_with_context!(export_snippet)(context),
Object::ExportSnippet,
),
map(parser_with_context!(entity)(context), Object::Entity),
map(
parser_with_context!(latex_fragment)(context),
Object::LatexFragment,
),
map(parser_with_context!(radio_link)(context), Object::RadioLink),
map(
parser_with_context!(radio_target)(context),
Object::RadioTarget,
),
parser_with_context!(text_markup)(context),
map(
parser_with_context!(regular_link)(context),
Object::RegularLink,
),
map(parser_with_context!(plain_link)(context), Object::PlainLink),
map(parser_with_context!(angle_link)(context), Object::AngleLink),
map(parser_with_context!(org_macro)(context), Object::OrgMacro),
))(input)?;
Ok((remaining, object))
element!(timestamp, context, input, Object::Timestamp);
element!(subscript, context, input, Object::Subscript);
element!(superscript, context, input, Object::Superscript);
element!(statistics_cookie, context, input, Object::StatisticsCookie);
element!(target, context, input, Object::Target);
element!(line_break, context, input, Object::LineBreak);
element!(
inline_source_block,
context,
input,
Object::InlineSourceBlock
);
element!(inline_babel_call, context, input, Object::InlineBabelCall);
element!(citation, context, input, Object::Citation);
element!(
footnote_reference,
context,
input,
Object::FootnoteReference
);
element!(export_snippet, context, input, Object::ExportSnippet);
element!(entity, context, input, Object::Entity);
element!(latex_fragment, context, input, Object::LatexFragment);
element!(radio_link, context, input, Object::RadioLink);
element!(radio_target, context, input, Object::RadioTarget);
element!(text_markup, context, input);
element!(regular_link, context, input, Object::RegularLink);
element!(plain_link, context, input, Object::PlainLink);
element!(angle_link, context, input, Object::AngleLink);
element!(org_macro, context, input, Object::OrgMacro);
Err(nom::Err::Error(CustomError::Static("No object.")))
}
#[cfg_attr(
@@ -135,20 +113,12 @@ fn minimal_set_object_sans_plain_text<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, object) = alt((
map(parser_with_context!(subscript)(context), Object::Subscript),
map(
parser_with_context!(superscript)(context),
Object::Superscript,
),
map(parser_with_context!(entity)(context), Object::Entity),
map(
parser_with_context!(latex_fragment)(context),
Object::LatexFragment,
),
parser_with_context!(text_markup)(context),
))(input)?;
Ok((remaining, object))
element!(subscript, context, input, Object::Subscript);
element!(superscript, context, input, Object::Superscript);
element!(entity, context, input, Object::Entity);
element!(latex_fragment, context, input, Object::LatexFragment);
element!(text_markup, context, input);
Err(nom::Err::Error(CustomError::Static("No object.")))
}
#[cfg_attr(
@@ -166,9 +136,7 @@ pub(crate) fn detect_standard_set_object_sans_plain_text<'b, 'g, 'r, 's>(
return Ok((input, ()));
}
return Err(nom::Err::Error(CustomError::MyError(MyError(
"No object detected.".into(),
))));
return Err(nom::Err::Error(CustomError::Static("No object detected.")));
}
#[cfg_attr(
@@ -186,9 +154,7 @@ fn detect_minimal_set_object_sans_plain_text<'b, 'g, 'r, 's>(
return Ok((input, ()));
}
return Err(nom::Err::Error(CustomError::MyError(MyError(
"No object detected.".into(),
))));
return Err(nom::Err::Error(CustomError::Static("No object detected.")));
}
#[cfg_attr(
@@ -200,16 +166,18 @@ pub(crate) fn regular_link_description_set_object<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
// TODO: It can also contain another link, but only when it is a plain or angle link. It can contain square brackets, but not ]]
let (remaining, object) = alt((
parser_with_context!(regular_link_description_set_object_sans_plain_text)(context),
map(
parser_with_context!(plain_text(
detect_regular_link_description_set_object_sans_plain_text
))(context),
Object::PlainText,
),
))(input)?;
Ok((remaining, object))
element!(
regular_link_description_set_object_sans_plain_text,
context,
input
);
element!(
plain_text(detect_regular_link_description_set_object_sans_plain_text),
context,
input,
Object::PlainText
);
Err(nom::Err::Error(CustomError::Static("No object.")))
}
#[cfg_attr(
@@ -221,27 +189,18 @@ fn regular_link_description_set_object_sans_plain_text<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
// TODO: It can also contain another link, but only when it is a plain or angle link. It can contain square brackets, but not ]]
let (remaining, object) = alt((
map(
parser_with_context!(export_snippet)(context),
Object::ExportSnippet,
),
map(
parser_with_context!(statistics_cookie)(context),
Object::StatisticsCookie,
),
map(
parser_with_context!(inline_source_block)(context),
Object::InlineSourceBlock,
),
map(
parser_with_context!(inline_babel_call)(context),
Object::InlineBabelCall,
),
map(parser_with_context!(org_macro)(context), Object::OrgMacro),
parser_with_context!(minimal_set_object_sans_plain_text)(context),
))(input)?;
Ok((remaining, object))
element!(export_snippet, context, input, Object::ExportSnippet);
element!(statistics_cookie, context, input, Object::StatisticsCookie);
element!(
inline_source_block,
context,
input,
Object::InlineSourceBlock
);
element!(inline_babel_call, context, input, Object::InlineBabelCall);
element!(org_macro, context, input, Object::OrgMacro);
element!(minimal_set_object_sans_plain_text, context, input);
Err(nom::Err::Error(CustomError::Static("No object.")))
}
#[cfg_attr(
@@ -259,9 +218,7 @@ fn detect_regular_link_description_set_object_sans_plain_text<'b, 'g, 'r, 's>(
return Ok((input, ()));
}
return Err(nom::Err::Error(CustomError::MyError(MyError(
"No object detected.".into(),
))));
Err(nom::Err::Error(CustomError::Static("No object detected.")))
}
#[cfg_attr(
@@ -272,14 +229,14 @@ pub(crate) fn table_cell_set_object<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, object) = alt((
parser_with_context!(table_cell_set_object_sans_plain_text)(context),
map(
parser_with_context!(plain_text(detect_table_cell_set_object_sans_plain_text))(context),
Object::PlainText,
),
))(input)?;
Ok((remaining, object))
element!(table_cell_set_object_sans_plain_text, context, input);
element!(
plain_text(detect_table_cell_set_object_sans_plain_text),
context,
input,
Object::PlainText
);
Err(nom::Err::Error(CustomError::Static("No object.")))
}
#[cfg_attr(
@@ -290,33 +247,24 @@ fn table_cell_set_object_sans_plain_text<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
let (remaining, object) = alt((
map(parser_with_context!(citation)(context), Object::Citation),
map(
parser_with_context!(export_snippet)(context),
Object::ExportSnippet,
),
map(
parser_with_context!(footnote_reference)(context),
Object::FootnoteReference,
),
map(parser_with_context!(radio_link)(context), Object::RadioLink),
map(
parser_with_context!(regular_link)(context),
Object::RegularLink,
),
map(parser_with_context!(plain_link)(context), Object::PlainLink),
map(parser_with_context!(angle_link)(context), Object::AngleLink),
map(parser_with_context!(org_macro)(context), Object::OrgMacro),
map(
parser_with_context!(radio_target)(context),
Object::RadioTarget,
),
map(parser_with_context!(target)(context), Object::Target),
map(parser_with_context!(timestamp)(context), Object::Timestamp),
parser_with_context!(minimal_set_object_sans_plain_text)(context),
))(input)?;
Ok((remaining, object))
element!(citation, context, input, Object::Citation);
element!(export_snippet, context, input, Object::ExportSnippet);
element!(
footnote_reference,
context,
input,
Object::FootnoteReference
);
element!(radio_link, context, input, Object::RadioLink);
element!(regular_link, context, input, Object::RegularLink);
element!(plain_link, context, input, Object::PlainLink);
element!(angle_link, context, input, Object::AngleLink);
element!(org_macro, context, input, Object::OrgMacro);
element!(radio_target, context, input, Object::RadioTarget);
element!(target, context, input, Object::Target);
element!(timestamp, context, input, Object::Timestamp);
element!(minimal_set_object_sans_plain_text, context, input);
Err(nom::Err::Error(CustomError::Static("No object.")))
}
#[cfg_attr(
@@ -334,7 +282,5 @@ fn detect_table_cell_set_object_sans_plain_text<'b, 'g, 'r, 's>(
return Ok((input, ()));
}
return Err(nom::Err::Error(CustomError::MyError(MyError(
"No object detected.".into(),
))));
return Err(nom::Err::Error(CustomError::Static("No object detected.")));
}

View File

@@ -96,7 +96,7 @@ fn org_macro_arg<'b, 'g, 'r, 's>(
loop {
not(parser_with_context!(exit_matcher_parser)(context))(remaining)?;
not(peek(tag("}}}")))(remaining)?;
if peek(tuple((space0::<OrgSource<'_>, CustomError<_>>, tag(")"))))(remaining).is_ok() {
if peek(tuple((space0::<_, CustomError>, tag(")"))))(remaining).is_ok() {
break;
}
@@ -108,7 +108,7 @@ fn org_macro_arg<'b, 'g, 'r, 's>(
}
if next_char == '\\' {
escaping = true;
if peek(tag::<_, _, CustomError<_>>(")"))(new_remaining).is_ok() {
if peek(tag::<_, _, CustomError>(")"))(new_remaining).is_ok() {
// Special case for backslash at the end of a macro
remaining = new_remaining;
break;

View File

@@ -10,9 +10,6 @@ use nom::InputTakeAtPosition;
use nom::Offset;
use nom::Slice;
use crate::error::CustomError;
use crate::error::MyError;
pub(crate) type BracketDepth = i16;
#[derive(Copy, Clone, PartialEq)]
@@ -85,6 +82,15 @@ impl<'s> OrgSource<'s> {
self.slice(..(other.end - self.start))
}
pub(crate) fn get_until_end_of_str(&self, other: &'s str) -> OrgSource<'s> {
let full_source_start = self.full_source.as_ptr() as usize;
let other_start = other.as_ptr() as usize - full_source_start;
let other_end = other_start + other.len();
debug_assert!(other_start >= self.start);
debug_assert!(other_end <= self.end);
self.slice(..(other_end - self.start))
}
pub(crate) fn get_start_of_line(&self) -> OrgSource<'s> {
let skipped_text = self.text_since_line_break();
let mut bracket_depth = self.bracket_depth;
@@ -219,7 +225,7 @@ where
panic!("Attempted to extend past the end of the WrappedInput.")
}
if new_start == self.start && new_end == self.end {
return self.clone();
return *self;
}
let skipped_text = &self.full_source[self.start..new_start];
@@ -337,7 +343,7 @@ impl<'s> InputTakeAtPosition for OrgSource<'s> {
P: Fn(Self::Item) -> bool,
{
match Into::<&str>::into(self).position(predicate) {
Some(0) => Err(nom::Err::Error(E::from_error_kind(self.clone(), e))),
Some(0) => Err(nom::Err::Error(E::from_error_kind(*self, e))),
Some(idx) => Ok(self.take_split(idx)),
None => Err(nom::Err::Incomplete(nom::Needed::new(1))),
}
@@ -366,11 +372,11 @@ impl<'s> InputTakeAtPosition for OrgSource<'s> {
{
let window = Into::<&str>::into(self);
match window.position(predicate) {
Some(0) => Err(nom::Err::Error(E::from_error_kind(self.clone(), e))),
Some(0) => Err(nom::Err::Error(E::from_error_kind(*self, e))),
Some(n) => Ok(self.take_split(n)),
None => {
if window.input_len() == 0 {
Err(nom::Err::Error(E::from_error_kind(self.clone(), e)))
Err(nom::Err::Error(E::from_error_kind(*self, e)))
} else {
Ok(self.take_split(self.input_len()))
}
@@ -385,33 +391,6 @@ impl<'n, 's> FindSubstring<&'n str> for OrgSource<'s> {
}
}
pub(crate) fn convert_error<'a, I: Into<CustomError<&'a str>>>(
err: nom::Err<I>,
) -> nom::Err<CustomError<&'a str>> {
match err {
nom::Err::Incomplete(needed) => nom::Err::Incomplete(needed),
nom::Err::Error(err) => nom::Err::Error(err.into()),
nom::Err::Failure(err) => nom::Err::Failure(err.into()),
}
}
impl<'s> From<CustomError<OrgSource<'s>>> for CustomError<&'s str> {
fn from(value: CustomError<OrgSource<'s>>) -> Self {
match value {
CustomError::MyError(err) => CustomError::MyError(err.into()),
CustomError::Nom(input, error_kind) => CustomError::Nom(input.into(), error_kind),
CustomError::IO(err) => CustomError::IO(err),
CustomError::BoxedError(err) => CustomError::BoxedError(err),
}
}
}
impl<'s> From<MyError<OrgSource<'s>>> for MyError<&'s str> {
fn from(value: MyError<OrgSource<'s>>) -> Self {
MyError(value.0.into())
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -2,54 +2,46 @@ use nom::branch::alt;
use nom::combinator::eof;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many1;
use nom::multi::many_till;
use nom::sequence::tuple;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::element_parser::detect_element;
use super::keyword::affiliated_keyword;
use super::org_source::OrgSource;
use super::util::blank_line;
use super::util::get_consumed;
use super::util::get_has_affiliated_keyword;
use super::util::maybe_consume_trailing_whitespace_if_not_exiting;
use crate::context::parser_with_context;
use crate::context::ContextElement;
use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::HasAffiliatedKeywordInner;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::object_parser::standard_set_object;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::start_of_line;
use crate::types::Keyword;
use crate::types::Paragraph;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn paragraph<'b, 'g, 'r, 's>(
pub(crate) fn paragraph<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Paragraph<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
let contexts = [
ContextElement::HasAffiliatedKeyword(HasAffiliatedKeywordInner {
start_after_affiliated_keywords: remaining,
keywords: &affiliated_keywords,
}),
ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma,
exit_matcher: &paragraph_end,
}),
];
) -> Res<OrgSource<'s>, Paragraph<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let contexts = [ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Gamma,
exit_matcher: &paragraph_end,
})];
let parser_context = context.with_additional_node(&contexts[0]);
let parser_context = parser_context.with_additional_node(&contexts[1]);
let standard_set_object_matcher = parser_with_context!(standard_set_object)(&parser_context);
let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context);
@@ -89,15 +81,6 @@ fn paragraph_end<'b, 'g, 'r, 's>(
if regular_end.is_ok() {
return regular_end;
}
match get_has_affiliated_keyword(context) {
Some(start_post_affiliated_keywords) if input == start_post_affiliated_keywords => {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"No exit due to affiliated keywords.",
))));
}
_ => {}
}
// Check to see if input is the start of a HasAffiliatedKeyword
alt((
recognize(parser_with_context!(detect_element(false))(context)),
eof,
@@ -106,7 +89,7 @@ fn paragraph_end<'b, 'g, 'r, 's>(
#[cfg(test)]
mod tests {
use crate::context::parser_with_context;
use crate::context::bind_context;
use crate::context::Context;
use crate::context::ContextElement;
use crate::context::GlobalSettings;
@@ -121,7 +104,7 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let paragraph_matcher = parser_with_context!(element(true))(&initial_context);
let paragraph_matcher = bind_context!(element(true), &initial_context);
let (remaining, first_paragraph) = paragraph_matcher(input).expect("Parse first paragraph");
let (remaining, second_paragraph) =
paragraph_matcher(remaining).expect("Parse second paragraph.");

View File

@@ -36,7 +36,6 @@ use crate::context::ExitMatcherNode;
use crate::context::Matcher;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::exit_matcher_parser;
use crate::parser::util::get_consumed;
@@ -95,9 +94,9 @@ fn pre<'b, 'g, 'r, 's>(
Some(x) if !WORD_CONSTITUENT_CHARACTERS.contains(x) => {}
Some(_) => {
// Not at start of line, cannot be a heading
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Not a valid pre character for plain link.".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Not a valid pre character for plain link.",
)));
}
};
Ok((input, ()))
@@ -262,18 +261,13 @@ pub(crate) fn protocol<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
for link_parameter in context.get_global_settings().link_parameters {
let result = tag_no_case::<_, _, CustomError<_>>(*link_parameter)(input);
match result {
Ok((remaining, ent)) => {
return Ok((remaining, ent));
}
Err(_) => {}
let result = tag_no_case::<_, _, CustomError>(*link_parameter)(input);
if let Ok((remaining, ent)) = result {
return Ok((remaining, ent));
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"NoLinkProtocol".into(),
))))
Err(nom::Err::Error(CustomError::Static("NoLinkProtocol")))
}
#[cfg_attr(
@@ -330,8 +324,7 @@ fn impl_path_plain_end<'b, 'g, 'r, 's>(
}
if current_depth == 0 {
let close_parenthesis =
tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>(")")(remaining);
let close_parenthesis = tag::<_, _, CustomError>(")")(remaining);
if close_parenthesis.is_ok() {
return close_parenthesis;
}
@@ -345,9 +338,7 @@ fn impl_path_plain_end<'b, 'g, 'r, 's>(
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"No path plain end".into(),
))))
Err(nom::Err::Error(CustomError::Static("No path plain end")))
}
#[cfg_attr(
@@ -423,18 +414,18 @@ fn _path_plain_parenthesis_end<'s>(
unreachable!("Exceeded plain link parenthesis depth.")
}
if current_depth == 0 {
let close_parenthesis = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>(")")(input);
let close_parenthesis = tag::<_, _, CustomError>(")")(input);
if close_parenthesis.is_ok() {
return close_parenthesis;
}
}
if current_depth == 1 {
let open_parenthesis = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("(")(input);
let open_parenthesis = tag::<_, _, CustomError>("(")(input);
if open_parenthesis.is_ok() {
return open_parenthesis;
}
}
Err(nom::Err::Error(CustomError::MyError(MyError(
"No closing parenthesis".into(),
))))
Err(nom::Err::Error(CustomError::Static(
"No closing parenthesis",
)))
}

View File

@@ -3,6 +3,7 @@ use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::digit1;
use nom::character::complete::line_ending;
use nom::character::complete::multispace1;
use nom::character::complete::one_of;
use nom::character::complete::space0;
use nom::character::complete::space1;
@@ -17,15 +18,16 @@ use nom::multi::many0;
use nom::multi::many1;
use nom::multi::many_till;
use nom::sequence::tuple;
use nom::InputTake;
use super::affiliated_keyword::parse_affiliated_keywords;
use super::element_parser::element;
use super::keyword::affiliated_keyword;
use super::object_parser::standard_set_object;
use super::org_source::OrgSource;
use super::util::include_input;
use super::util::indentation_level;
use super::util::non_whitespace_character;
use crate::context::bind_context;
use crate::context::parser_with_context;
use crate::context::ContextElement;
use crate::context::ContextMatcher;
@@ -33,7 +35,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::blank_line;
use crate::parser::util::exit_matcher_parser;
@@ -43,6 +44,7 @@ use crate::parser::util::org_space;
use crate::parser::util::start_of_line;
use crate::types::CheckboxType;
use crate::types::IndentationLevel;
use crate::types::Keyword;
use crate::types::Object;
use crate::types::PlainList;
use crate::types::PlainListItem;
@@ -52,13 +54,17 @@ use crate::types::PlainListType;
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
tracing::instrument(ret, level = "debug", skip(context, _affiliated_keywords))
)]
pub(crate) fn detect_plain_list<'b, 'g, 'r, 's>(
pub(crate) fn detect_plain_list<'b, 'g, 'r, 's, AK>(
_affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> {
let (input, _) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, ()>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
if verify(
tuple((
start_of_line,
@@ -67,28 +73,69 @@ pub(crate) fn detect_plain_list<'b, 'g, 'r, 's>(
alt((space1, line_ending, eof)),
)),
|(_start, (indent_level, _), (_bullet_type, bull), _after_whitespace)| {
!Into::<&str>::into(bull).starts_with("*") || *indent_level > 0
!Into::<&str>::into(bull).starts_with('*') || *indent_level > 0
},
)(input)
)(remaining)
.is_ok()
{
return Ok((input, ()));
}
return Err(nom::Err::Error(CustomError::MyError(MyError(
"No element detected.".into(),
))));
return Err(nom::Err::Error(CustomError::Static("No element detected.")));
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
)]
pub(crate) fn plain_list<'b, 'g, 'r, 's>(
pub(crate) fn detect_not_plain_list_item_indent<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, PlainList<'s>> {
let (remaining, affiliated_keywords) = many0(affiliated_keyword)(input)?;
) -> Res<OrgSource<'s>, (u16, OrgSource<'s>)> {
if let Ok((_remaining, (_, indent, _))) = tuple((
start_of_line,
parser_with_context!(indentation_level)(context),
not(tuple((
parser_with_context!(bullet)(context),
alt((space1, line_ending, eof)),
))),
))(input)
{
return Ok((input, indent));
}
// Headlines are not plain list items.
if let Ok((_remaining, (_, indent, _))) = verify(
tuple((
start_of_line,
parser_with_context!(indentation_level)(context),
tuple((
parser_with_context!(bullet)(context),
alt((space1, line_ending, eof)),
)),
)),
|(_, (depth, _), ((_, bullet), _))| {
*depth == 0 && Into::<&str>::into(bullet).starts_with('*')
},
)(input)
{
return Ok((input, indent));
}
return Err(nom::Err::Error(CustomError::Static("No element detected.")));
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context, affiliated_keywords))
)]
pub(crate) fn plain_list<'b, 'g, 'r, 's, AK>(
affiliated_keywords: AK,
remaining: OrgSource<'s>,
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, PlainList<'s>>
where
AK: IntoIterator<Item = Keyword<'s>>,
{
let contexts = [
ContextElement::Context("plain list"),
ContextElement::ConsumeTrailingWhitespace(true),
@@ -116,7 +163,7 @@ pub(crate) fn plain_list<'b, 'g, 'r, 's>(
// While #3 is the most slow, it also seems to cleanest and involves the least manual mutation of already-parsed objects so I am going with #3 for now, but we should revisit #1 or #2 when the parser is more developed.
loop {
let list_item = parser_with_context!(plain_list_item)(&parser_context)(remaining);
let list_item = plain_list_item(&parser_context, remaining);
match (&first_item_list_type, &list_item) {
(None, Ok((_remain, (list_type, _item)))) => {
let _ = first_item_list_type.insert(*list_type);
@@ -136,25 +183,17 @@ pub(crate) fn plain_list<'b, 'g, 'r, 's>(
}
};
let maybe_exit = parser_with_context!(exit_matcher_parser)(&parser_context)(remaining);
let maybe_exit = exit_matcher_parser(&parser_context, remaining);
if maybe_exit.is_ok() {
break;
}
}
let (final_child_start, _final_item_first_parse) = match children.pop() {
Some(final_child) => final_child,
None => {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Plain lists require at least one element.".into(),
))));
}
};
let final_item_context = ContextElement::ConsumeTrailingWhitespace(false);
let final_item_context = parser_context.with_additional_node(&final_item_context);
let (remaining, (_, reparsed_final_item)) =
parser_with_context!(plain_list_item)(&final_item_context)(final_child_start)?;
children.push((final_child_start, reparsed_final_item));
if children.is_empty() {
return Err(nom::Err::Error(CustomError::Static(
"Plain lists require at least one element.",
)));
}
let (remaining, _trailing_ws) =
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
@@ -183,10 +222,10 @@ fn plain_list_item<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, (PlainListType, PlainListItem<'s>)> {
start_of_line(input)?;
let (remaining, (indent_level, _leading_whitespace)) = indentation_level(context, input)?;
let (remaining, (bullet_type, bull)) = verify(
parser_with_context!(bullet)(context),
|(_bullet_type, bull)| !Into::<&str>::into(bull).starts_with("*") || indent_level > 0,
)(remaining)?;
let (remaining, (bullet_type, bull)) =
verify(bind_context!(bullet, context), |(_bullet_type, bull)| {
!Into::<&str>::into(bull).starts_with('*') || indent_level > 0
})(remaining)?;
let (remaining, maybe_counter_set) =
opt(tuple((space1, tag("[@"), counter_set_value, tag("]"))))(remaining)?;
@@ -195,7 +234,7 @@ fn plain_list_item<'b, 'g, 'r, 's>(
let (remaining, maybe_checkbox) = opt(tuple((space1, item_checkbox)))(remaining)?;
let (remaining, maybe_tag) = if let BulletType::Unordered = bullet_type {
opt(tuple((space1, parser_with_context!(item_tag)(context))))(remaining)?
opt(tuple((space1, bind_context!(item_tag, context))))(remaining)?
} else {
(remaining, None)
};
@@ -207,6 +246,12 @@ fn plain_list_item<'b, 'g, 'r, 's>(
};
let exit_matcher = plain_list_item_end(indent_level);
let final_item_whitespace_cutoff = final_item_whitespace_cutoff(indent_level);
let final_whitespace_context = ContextElement::ExitMatcherNode(ExitMatcherNode {
class: ExitClass::Beta,
exit_matcher: &final_item_whitespace_cutoff,
});
let final_whitespace_context = context.with_additional_node(&final_whitespace_context);
let contexts = [
ContextElement::ConsumeTrailingWhitespace(true),
ContextElement::ExitMatcherNode(ExitMatcherNode {
@@ -214,67 +259,56 @@ fn plain_list_item<'b, 'g, 'r, 's>(
exit_matcher: &exit_matcher,
}),
];
let parser_context = context.with_additional_node(&contexts[0]);
let parser_context = final_whitespace_context.with_additional_node(&contexts[0]);
let parser_context = parser_context.with_additional_node(&contexts[1]);
let maybe_contentless_item: Res<OrgSource<'_>, ()> = peek(parser_with_context!(
detect_contentless_item_contents
)(&parser_context))(remaining);
match maybe_contentless_item {
Ok((_rem, _ws)) => {
let (remaining, _trailing_ws) = if context.should_consume_trailing_whitespace() {
recognize(alt((recognize(many1(blank_line)), eof)))(remaining)?
} else {
recognize(alt((blank_line, eof)))(remaining)?
};
let source = get_consumed(input, remaining);
return Ok((
remaining,
(
list_type,
PlainListItem {
source: source.into(),
indentation: indent_level,
bullet: bull.into(),
counter: maybe_counter_set,
checkbox: None,
tag: maybe_tag
.map(|(_ws, item_tag)| item_tag)
.unwrap_or(Vec::new()),
pre_blank: 0,
children: Vec::new(),
},
),
));
}
Err(_) => {}
};
let maybe_contentless_item: Res<OrgSource<'_>, ()> =
detect_contentless_item_contents(&parser_context, remaining);
if let Ok((_rem, _ws)) = maybe_contentless_item {
let (remaining, _trailing_ws) = if tuple((
blank_line,
bind_context!(final_item_whitespace_cutoff, context),
))(remaining)
.is_ok()
{
recognize(alt((blank_line, eof)))(remaining)?
} else {
recognize(alt((recognize(many1(blank_line)), eof)))(remaining)?
};
let source = get_consumed(input, remaining);
return Ok((
remaining,
(
list_type,
PlainListItem {
source: source.into(),
indentation: indent_level,
bullet: bull.into(),
counter: maybe_counter_set,
checkbox: None,
tag: maybe_tag
.map(|(_ws, item_tag)| item_tag)
.unwrap_or(Vec::new()),
pre_blank: 0,
children: Vec::new(),
},
),
));
}
let (remaining, pre_blank) = item_tag_post_gap(&parser_context, remaining)?;
let pre_blank = Into::<&str>::into(pre_blank)
.bytes()
.filter(|b| *b == b'\n')
.count();
let (mut remaining, (mut children, _exit_contents)) = many_till(
include_input(parser_with_context!(element(true))(&parser_context)),
parser_with_context!(exit_matcher_parser)(&parser_context),
let (remaining, (children, _exit_contents)) = many_till(
include_input(bind_context!(element(true), &parser_context)),
bind_context!(exit_matcher_parser, &parser_context),
)(remaining)?;
if !children.is_empty() && !context.should_consume_trailing_whitespace() {
let final_item_context = ContextElement::ConsumeTrailingWhitespace(false);
let final_item_context = parser_context.with_additional_node(&final_item_context);
let (final_child_start, _original_final_child) = children
.pop()
.expect("if-statement already checked that children was non-empty.");
let (remain, reparsed_final_element) = include_input(parser_with_context!(element(true))(
&final_item_context,
))(final_child_start)?;
remaining = remain;
children.push(reparsed_final_element);
}
// We have to use the parser_context here to include the whitespace cut-off
let (remaining, _trailing_ws) =
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
maybe_consume_trailing_whitespace_if_not_exiting(&final_whitespace_context, remaining)?;
let source = get_consumed(input, remaining);
return Ok((
@@ -321,7 +355,7 @@ fn bullet<'b, 'g, 'r, 's>(
map(tag("+"), |bull| (BulletType::Unordered, bull)),
map(
recognize(tuple((
parser_with_context!(counter)(context),
bind_context!(counter, context),
alt((tag("."), tag(")"))),
))),
|bull| (BulletType::Ordered, bull),
@@ -376,6 +410,52 @@ fn counter_set_value<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, PlainListIt
))(input)
}
const fn final_item_whitespace_cutoff(indent_level: IndentationLevel) -> impl ContextMatcher {
move |context, input: OrgSource<'_>| {
impl_final_item_whitespace_cutoff(context, input, indent_level)
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(context))
)]
fn impl_final_item_whitespace_cutoff<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
indent_level: IndentationLevel,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
start_of_line(input)?;
// element!(plain_list_end, context, input);
if let Ok((_remaining, _)) = verify(
tuple((
opt(blank_line),
bind_context!(indentation_level, context),
not(multispace1),
)),
|(_, (depth, _stars), _not_whitespace)| *depth < indent_level,
)(input)
{
return Ok((input, input.take(0)));
}
if let Ok((_remaining, _)) = tuple((
opt(blank_line),
verify(
bind_context!(detect_not_plain_list_item_indent, context),
|(depth, _)| *depth == indent_level,
),
))(input)
{
return Ok((input, input.take(0)));
}
Err(nom::Err::Error(CustomError::Static(
"No whitespace cut-off.",
)))
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(ret, level = "debug", skip(_context))
@@ -411,7 +491,7 @@ fn _plain_list_item_end<'b, 'g, 'r, 's>(
start_of_line(input)?;
recognize(tuple((
opt(blank_line),
parser_with_context!(line_indented_lte_matcher)(context),
bind_context!(line_indented_lte_matcher, context),
)))(input)
}
@@ -430,7 +510,7 @@ fn _line_indented_lte<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'s>> {
let matched = recognize(verify(
tuple((
parser_with_context!(indentation_level)(context),
bind_context!(indentation_level, context),
non_whitespace_character,
)),
// It is fine that we get the indent level using the number of bytes rather than the number of characters because nom's space0 only matches space and tab (0x20 and 0x09)
@@ -456,8 +536,8 @@ fn item_tag<'b, 'g, 'r, 's>(
let (remaining, (children, _exit_contents)) = verify(
many_till(
// TODO: Should this be using a different set like the minimal set?
parser_with_context!(standard_set_object)(&parser_context),
parser_with_context!(exit_matcher_parser)(&parser_context),
bind_context!(standard_set_object, &parser_context),
bind_context!(exit_matcher_parser, &parser_context),
),
|(children, _exit_contents)| !children.is_empty(),
)(input)?;
@@ -507,7 +587,7 @@ fn item_tag_post_gap<'b, 'g, 'r, 's>(
alt((
peek(recognize(not(blank_line))),
peek(recognize(tuple((many0(blank_line), eof)))),
parser_with_context!(exit_matcher_parser)(context),
bind_context!(exit_matcher_parser, context),
)),
),
))),
@@ -537,7 +617,7 @@ fn detect_contentless_item_contents<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, ()> {
let (remaining, _) = recognize(many_till(
blank_line,
parser_with_context!(exit_matcher_parser)(context),
bind_context!(exit_matcher_parser, context),
))(input)?;
Ok((remaining, ()))
}
@@ -545,6 +625,7 @@ fn detect_contentless_item_contents<'b, 'g, 'r, 's>(
#[cfg(test)]
mod tests {
use super::*;
use crate::context::bind_context;
use crate::context::Context;
use crate::context::GlobalSettings;
use crate::context::List;
@@ -556,7 +637,7 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_item_matcher = parser_with_context!(plain_list_item)(&initial_context);
let plain_list_item_matcher = bind_context!(plain_list_item, &initial_context);
let (remaining, (_, result)) = plain_list_item_matcher(input).unwrap();
assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.get_standard_properties().get_source(), "1.");
@@ -568,7 +649,7 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_item_matcher = parser_with_context!(plain_list_item)(&initial_context);
let plain_list_item_matcher = bind_context!(plain_list_item, &initial_context);
let (remaining, (_, result)) = plain_list_item_matcher(input).unwrap();
assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.get_standard_properties().get_source(), "1. foo");
@@ -580,8 +661,8 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_matcher = parser_with_context!(plain_list)(&initial_context);
let (remaining, result) = plain_list_matcher(input).unwrap();
let (remaining, result) =
plain_list(std::iter::empty(), input, &initial_context, input).unwrap();
assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.get_standard_properties().get_source(), "1.");
}
@@ -592,8 +673,8 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_matcher = parser_with_context!(plain_list)(&initial_context);
let (remaining, result) = plain_list_matcher(input).unwrap();
let (remaining, result) =
plain_list(std::iter::empty(), input, &initial_context, input).unwrap();
assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(result.get_standard_properties().get_source(), "1. foo");
}
@@ -605,8 +686,7 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_matcher = parser_with_context!(plain_list)(&initial_context);
let result = plain_list_matcher(input);
let result = plain_list(std::iter::empty(), input, &initial_context, input);
assert!(result.is_err());
}
@@ -617,8 +697,7 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_matcher = parser_with_context!(plain_list)(&initial_context);
let result = plain_list_matcher(input);
let result = plain_list(std::iter::empty(), input, &initial_context, input);
assert!(result.is_ok());
}
@@ -637,7 +716,7 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_matcher = parser_with_context!(element(true))(&initial_context);
let plain_list_matcher = bind_context!(element(true), &initial_context);
let (remaining, result) =
plain_list_matcher(input).expect("Should parse the plain list successfully.");
assert_eq!(Into::<&str>::into(remaining), " ipsum\n");
@@ -665,7 +744,7 @@ baz"#,
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_matcher = parser_with_context!(element(true))(&initial_context);
let plain_list_matcher = bind_context!(element(true), &initial_context);
let (remaining, result) =
plain_list_matcher(input).expect("Should parse the plain list successfully.");
assert_eq!(Into::<&str>::into(remaining), "baz");
@@ -698,7 +777,7 @@ dolar"#,
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_list_matcher = parser_with_context!(element(true))(&initial_context);
let plain_list_matcher = bind_context!(element(true), &initial_context);
let (remaining, result) =
plain_list_matcher(input).expect("Should parse the plain list successfully.");
assert_eq!(Into::<&str>::into(remaining), "dolar");
@@ -728,7 +807,7 @@ dolar"#,
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
let result = detect_plain_list(std::iter::empty(), input, &initial_context, input);
assert!(result.is_ok());
}
@@ -738,7 +817,7 @@ dolar"#,
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
let result = detect_plain_list(std::iter::empty(), input, &initial_context, input);
assert!(result.is_ok());
}
@@ -748,7 +827,7 @@ dolar"#,
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
let result = detect_plain_list(std::iter::empty(), input, &initial_context, input);
// Since there is no whitespace after the '+' this is a paragraph, not a plain list.
assert!(result.is_err());
}
@@ -759,7 +838,7 @@ dolar"#,
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
let result = detect_plain_list(std::iter::empty(), input, &initial_context, input);
assert!(result.is_ok());
}
}

View File

@@ -20,7 +20,6 @@ use super::util::org_space_or_line_ending;
use crate::context::parser_with_context;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::types::Object;
use crate::types::PlainText;
@@ -92,41 +91,35 @@ impl<'x> RematchObject<'x> for PlainText<'x> {
break;
}
let is_not_whitespace = is_not::<&str, &str, CustomError<_>>(" \t\r\n")(goal);
match is_not_whitespace {
Ok((new_goal, payload)) => {
let (new_remaining, _) = tuple((
tag_no_case(payload),
// TODO: Test to see what the REAL condition is. Checking for not-alphabetic works fine for now, but the real criteria might be something like the plain text exit matcher.
peek(alt((
recognize(verify(anychar, |c| !c.is_alphanumeric())),
eof,
))),
))(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;
}
Err(_) => {}
};
let is_not_whitespace = is_not::<_, _, CustomError>(" \t\r\n")(goal);
if let Ok((new_goal, payload)) = is_not_whitespace {
let (new_remaining, _) = tuple((
tag_no_case(payload),
// TODO: Test to see what the REAL condition is. Checking for not-alphabetic works fine for now, but the real criteria might be something like the plain text exit matcher.
peek(alt((
recognize(verify(anychar, |c| !c.is_alphanumeric())),
eof,
))),
))(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;
}
let is_whitespace = recognize(many1(alt((
recognize(one_of::<&str, &str, CustomError<_>>(" \t")),
recognize(one_of::<_, _, CustomError>(" \t")),
line_ending,
))))(goal);
match is_whitespace {
Ok((new_goal, _)) => {
let (new_remaining, _) = many1(org_space_or_line_ending)(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;
}
Err(_) => {}
};
if let Ok((new_goal, _)) = is_whitespace {
let (new_remaining, _) = many1(org_space_or_line_ending)(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;
}
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Target does not match.".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Target does not match.",
)));
}
let source = get_consumed(input, remaining);
@@ -144,6 +137,7 @@ mod tests {
use nom::combinator::map;
use super::*;
use crate::context::bind_context;
use crate::context::Context;
use crate::context::ContextElement;
use crate::context::GlobalSettings;
@@ -157,10 +151,14 @@ mod tests {
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let plain_text_matcher = parser_with_context!(plain_text(
detect_standard_set_object_sans_plain_text
))(&initial_context);
let (remaining, result) = map(plain_text_matcher, Object::PlainText)(input).unwrap();
let (remaining, result) = map(
bind_context!(
plain_text(detect_standard_set_object_sans_plain_text),
&initial_context
),
Object::PlainText,
)(input)
.unwrap();
assert_eq!(Into::<&str>::into(remaining), "");
assert_eq!(
result.get_standard_properties().get_source(),

View File

@@ -21,7 +21,6 @@ use crate::context::ExitClass;
use crate::context::ExitMatcherNode;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::parser::util::get_consumed;
use crate::parser::util::immediate_in_section;
@@ -39,9 +38,9 @@ pub(crate) fn property_drawer<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, PropertyDrawer<'s>> {
if immediate_in_section(context, "property-drawer") {
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Cannot nest objects of the same element".into(),
))));
return Err(nom::Err::Error(CustomError::Static(
"Cannot nest objects of the same element",
)));
}
let (
remaining,

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