diff --git a/docker/organic_test/Dockerfile b/docker/organic_test/Dockerfile index 246f1677..f0c31d70 100644 --- a/docker/organic_test/Dockerfile +++ b/docker/organic_test/Dockerfile @@ -14,7 +14,7 @@ RUN make DESTDIR="/root/dist" install FROM build AS build-org-mode -ARG ORG_VERSION=7bdec435ff5d86220d13c431e799c5ed44a57da1 +ARG ORG_VERSION=163bafb43dcc2bc94a2c7ccaa77d3d1dd488f1af 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. @@ -36,7 +36,57 @@ ENTRYPOINT ["cargo", "test"] FROM build as foreign-document-gather + +ARG HOWARD_ABRAMS_DOT_FILES_VERSION=1b54fe75d74670dc7bcbb6b01ea560c45528c628 +ARG HOWARD_ABRAMS_DOT_FILES_PATH=/foreign_documents/howardabrams/dot-files +ARG HOWARD_ABRAMS_DOT_FILES_REPO=https://github.com/howardabrams/dot-files.git RUN mkdir /foreign_documents +RUN mkdir -p $HOWARD_ABRAMS_DOT_FILES_PATH && git -C $HOWARD_ABRAMS_DOT_FILES_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_DOT_FILES_PATH remote add origin $HOWARD_ABRAMS_DOT_FILES_REPO && git -C $HOWARD_ABRAMS_DOT_FILES_PATH fetch origin $HOWARD_ABRAMS_DOT_FILES_VERSION && git -C $HOWARD_ABRAMS_DOT_FILES_PATH checkout FETCH_HEAD + +ARG HOWARD_ABRAMS_HAMACS_VERSION=da51188cc195d41882175d412fe40a8bc5730c5c +ARG HOWARD_ABRAMS_HAMACS_PATH=/foreign_documents/howardabrams/hamacs +ARG HOWARD_ABRAMS_HAMACS_REPO=https://github.com/howardabrams/hamacs.git +RUN mkdir -p $HOWARD_ABRAMS_HAMACS_PATH && git -C $HOWARD_ABRAMS_HAMACS_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_HAMACS_PATH remote add origin $HOWARD_ABRAMS_HAMACS_REPO && git -C $HOWARD_ABRAMS_HAMACS_PATH fetch origin $HOWARD_ABRAMS_HAMACS_VERSION && git -C $HOWARD_ABRAMS_HAMACS_PATH checkout FETCH_HEAD + +ARG HOWARD_ABRAMS_DEMO_IT_VERSION=e399fd7ceb73caeae7cb50b247359bafcaee2a3f +ARG HOWARD_ABRAMS_DEMO_IT_PATH=/foreign_documents/howardabrams/demo-it +ARG HOWARD_ABRAMS_DEMO_IT_REPO=https://github.com/howardabrams/demo-it.git +RUN mkdir -p $HOWARD_ABRAMS_DEMO_IT_PATH && git -C $HOWARD_ABRAMS_DEMO_IT_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_DEMO_IT_PATH remote add origin $HOWARD_ABRAMS_DEMO_IT_REPO && git -C $HOWARD_ABRAMS_DEMO_IT_PATH fetch origin $HOWARD_ABRAMS_DEMO_IT_VERSION && git -C $HOWARD_ABRAMS_DEMO_IT_PATH checkout FETCH_HEAD + +ARG HOWARD_ABRAMS_MAGIT_DEMO_VERSION=59e82f6bc7c18f550478d86a8f680c3f2da66985 +ARG HOWARD_ABRAMS_MAGIT_DEMO_PATH=/foreign_documents/howardabrams/magit-demo +ARG HOWARD_ABRAMS_MAGIT_DEMO_REPO=https://github.com/howardabrams/magit-demo.git +RUN mkdir -p $HOWARD_ABRAMS_MAGIT_DEMO_PATH && git -C $HOWARD_ABRAMS_MAGIT_DEMO_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_MAGIT_DEMO_PATH remote add origin $HOWARD_ABRAMS_MAGIT_DEMO_REPO && git -C $HOWARD_ABRAMS_MAGIT_DEMO_PATH fetch origin $HOWARD_ABRAMS_MAGIT_DEMO_VERSION && git -C $HOWARD_ABRAMS_MAGIT_DEMO_PATH checkout FETCH_HEAD + +ARG HOWARD_ABRAMS_PDX_EMACS_HACKERS_VERSION=bfb7bd640fdf0ce3def21f9fc591ed35d776b26d +ARG HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH=/foreign_documents/howardabrams/pdx-emacs-hackers +ARG HOWARD_ABRAMS_PDX_EMACS_HACKERS_REPO=https://github.com/howardabrams/pdx-emacs-hackers.git +RUN mkdir -p $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH && git -C $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH remote add origin $HOWARD_ABRAMS_PDX_EMACS_HACKERS_REPO && git -C $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH fetch origin $HOWARD_ABRAMS_PDX_EMACS_HACKERS_VERSION && git -C $HOWARD_ABRAMS_PDX_EMACS_HACKERS_PATH checkout FETCH_HEAD + +ARG HOWARD_ABRAMS_FLORA_SIMULATOR_VERSION=50de13068722b9e3878f8598b749b7ccd14e7f8e +ARG HOWARD_ABRAMS_FLORA_SIMULATOR_PATH=/foreign_documents/howardabrams/flora-simulator +ARG HOWARD_ABRAMS_FLORA_SIMULATOR_REPO=https://github.com/howardabrams/flora-simulator.git +RUN mkdir -p $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH && git -C $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH remote add origin $HOWARD_ABRAMS_FLORA_SIMULATOR_REPO && git -C $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH fetch origin $HOWARD_ABRAMS_FLORA_SIMULATOR_VERSION && git -C $HOWARD_ABRAMS_FLORA_SIMULATOR_PATH checkout FETCH_HEAD + +ARG HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_VERSION=2d7a5e41001a1adf7ec24aeb6acc8525a72d7892 +ARG HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH=/foreign_documents/howardabrams/literate-devops-demo +ARG HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_REPO=https://github.com/howardabrams/literate-devops-demo.git +RUN mkdir -p $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH && git -C $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH remote add origin $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_REPO && git -C $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH fetch origin $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_VERSION && git -C $HOWARD_ABRAMS_LITERATE_DEVOPS_DEMO_PATH checkout FETCH_HEAD + +ARG HOWARD_ABRAMS_CLOJURE_YESQL_XP_VERSION=b651c7f8b47b2710e99fce9652980902bbc1c6c9 +ARG HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH=/foreign_documents/howardabrams/clojure-yesql-xp +ARG HOWARD_ABRAMS_CLOJURE_YESQL_XP_REPO=https://github.com/howardabrams/clojure-yesql-xp.git +RUN mkdir -p $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH && git -C $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH remote add origin $HOWARD_ABRAMS_CLOJURE_YESQL_XP_REPO && git -C $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH fetch origin $HOWARD_ABRAMS_CLOJURE_YESQL_XP_VERSION && git -C $HOWARD_ABRAMS_CLOJURE_YESQL_XP_PATH checkout FETCH_HEAD + +ARG HOWARD_ABRAMS_VEEP_VERSION=e37fcf63a5c4a526255735ee34955528b3b280ae +ARG HOWARD_ABRAMS_VEEP_PATH=/foreign_documents/howardabrams/veep +ARG HOWARD_ABRAMS_VEEP_REPO=https://github.com/howardabrams/veep.git +RUN mkdir -p $HOWARD_ABRAMS_VEEP_PATH && git -C $HOWARD_ABRAMS_VEEP_PATH init --initial-branch=main && git -C $HOWARD_ABRAMS_VEEP_PATH remote add origin $HOWARD_ABRAMS_VEEP_REPO && git -C $HOWARD_ABRAMS_VEEP_PATH fetch origin $HOWARD_ABRAMS_VEEP_VERSION && git -C $HOWARD_ABRAMS_VEEP_PATH checkout FETCH_HEAD + +ARG DOOMEMACS_VERSION=42d5fd83504f8aa80f3248036006fbcd49222943 +ARG DOOMEMACS_PATH=/foreign_documents/doomemacs +ARG DOOMEMACS_REPO=https://github.com/doomemacs/doomemacs.git +RUN mkdir -p $DOOMEMACS_PATH && git -C $DOOMEMACS_PATH init --initial-branch=main && git -C $DOOMEMACS_PATH remote add origin $DOOMEMACS_REPO && git -C $DOOMEMACS_PATH fetch origin $DOOMEMACS_VERSION && git -C $DOOMEMACS_PATH checkout FETCH_HEAD FROM tester as foreign-document-test @@ -44,6 +94,8 @@ RUN apk add --no-cache bash coreutils RUN mkdir /foreign_documents COPY --from=build-org-mode /root/org-mode /foreign_documents/org-mode COPY --from=build-emacs /root/emacs /foreign_documents/emacs +COPY --from=foreign-document-gather /foreign_documents/howardabrams /foreign_documents/howardabrams +COPY --from=foreign-document-gather /foreign_documents/doomemacs /foreign_documents/doomemacs COPY foreign_document_test_entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/organic_test/foreign_document_test_entrypoint.sh b/docker/organic_test/foreign_document_test_entrypoint.sh index f722c4da..b1755d3a 100644 --- a/docker/organic_test/foreign_document_test_entrypoint.sh +++ b/docker/organic_test/foreign_document_test_entrypoint.sh @@ -25,8 +25,25 @@ function main { fi PARSE="${CARGO_TARGET_DIR}/release-lto/parse" - run_compare_function "org-mode" compare_all_org_document "/foreign_documents/org-mode" - run_compare_function "emacs" compare_all_org_document "/foreign_documents/emacs" + 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 "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 + echo "$(red_text "Some tests failed.")" + else + echo "$(green_text "All tests passed.")" + fi + return "$all_status" } function green_text { @@ -72,17 +89,22 @@ function run_compare_function { function compare_all_org_document { local root_dir="$1" local target_document - find "$root_dir" -type f -iname '*.org' | while read target_document; do + local all_status=0 + while read target_document; do local relative_path=$($REALPATH --relative-to "$root_dir" "$target_document") + set +e (run_compare "$relative_path" "$target_document") - done + if [ "$?" -ne 0 ]; then all_status=1; fi + set -e + done<<<$(find "$root_dir" -type f -iname '*.org') + return "$all_status" } function run_compare { local name="$1" local target_document="$2" set +e - $PARSE "$target_document" &> /dev/null + ($PARSE "$target_document" &> /dev/null) local status=$? set -e if [ "$status" -eq 0 ]; then @@ -93,4 +115,31 @@ function run_compare { 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 "${@}" diff --git a/org_mode_samples/greater_element/footnote_definition/empty_space_before_and_after_content.org b/org_mode_samples/greater_element/footnote_definition/empty_space_before_and_after_content.org new file mode 100644 index 00000000..68622132 --- /dev/null +++ b/org_mode_samples/greater_element/footnote_definition/empty_space_before_and_after_content.org @@ -0,0 +1,8 @@ +* Footnotes + +[fn:1] + +#+BEGIN_EXAMPLE +baz +#+END_EXAMPLE + diff --git a/org_mode_samples/greater_element/plain_list/description_list_object_key.org b/org_mode_samples/greater_element/plain_list/description_list_object_key.org index fcf945a1..737aede0 100644 --- a/org_mode_samples/greater_element/plain_list/description_list_object_key.org +++ b/org_mode_samples/greater_element/plain_list/description_list_object_key.org @@ -1 +1,2 @@ - {{{foo(bar)}}} :: baz +- =foo= :: bar diff --git a/org_mode_samples/greater_element/plain_list/description_list_tabs.org b/org_mode_samples/greater_element/plain_list/description_list_tabs.org new file mode 100644 index 00000000..2d05493d --- /dev/null +++ b/org_mode_samples/greater_element/plain_list/description_list_tabs.org @@ -0,0 +1,3 @@ +- foo :: bar +- foo :: bar +- foo :: bar diff --git a/org_mode_samples/greater_element/plain_list/description_list_with_double_colon_in_tag.org b/org_mode_samples/greater_element/plain_list/description_list_with_double_colon_in_tag.org new file mode 100644 index 00000000..4ba3143e --- /dev/null +++ b/org_mode_samples/greater_element/plain_list/description_list_with_double_colon_in_tag.org @@ -0,0 +1 @@ +- =foo :: bar= :: baz diff --git a/org_mode_samples/greater_element/plain_list/empty_list_item.org b/org_mode_samples/greater_element/plain_list/empty_list_item.org index 0fe3a9ab..397d6b8b 100644 --- a/org_mode_samples/greater_element/plain_list/empty_list_item.org +++ b/org_mode_samples/greater_element/plain_list/empty_list_item.org @@ -1,3 +1,5 @@ 1. 2. 3. + +* headline diff --git a/org_mode_samples/greater_element/table/cells_with_objects.org b/org_mode_samples/greater_element/table/cells_with_objects.org new file mode 100644 index 00000000..ffc814bc --- /dev/null +++ b/org_mode_samples/greater_element/table/cells_with_objects.org @@ -0,0 +1,6 @@ +src_elisp{(bar)} +*src_elisp{(bar)}* + +| foo *bar* | +| foo src_elisp{(bar)} | +| foo *src_elisp{(bar)}* | diff --git a/org_mode_samples/greater_element/table/with_formulas.org b/org_mode_samples/greater_element/table/with_formulas.org new file mode 100644 index 00000000..a48f48a4 --- /dev/null +++ b/org_mode_samples/greater_element/table/with_formulas.org @@ -0,0 +1,8 @@ +| Name | Price | Quantity | Total | +|------+-------+----------+-------| +| foo | 7 | 4 | 28 | +| bar | 3.5 | 3 | 10.5 | +|------+-------+----------+-------| +| | | 7 | 38.5 | +#+tblfm: $4=$2*$3::@>$4=vsum(@2..@-1) +#+tblfm: @>$3=vsum(@2..@-1) diff --git a/org_mode_samples/lesser_element/keyword/keyword_with_colon_in_key.org b/org_mode_samples/lesser_element/keyword/keyword_with_colon_in_key.org new file mode 100644 index 00000000..997a5d72 --- /dev/null +++ b/org_mode_samples/lesser_element/keyword/keyword_with_colon_in_key.org @@ -0,0 +1 @@ +#+title:foo:bar: baz: lorem: ipsum diff --git a/org_mode_samples/object/footnote_reference/nested_footnote_references.org b/org_mode_samples/object/footnote_reference/nested_footnote_references.org new file mode 100644 index 00000000..310f174b --- /dev/null +++ b/org_mode_samples/object/footnote_reference/nested_footnote_references.org @@ -0,0 +1,3 @@ +*[fn:: /abcdef[fn::ghijklmnopqrstuvw]xyz/ r]* + +*[fn:: /abcdef[fn::ghijk *lmnopq* rstuvw]xyz/ r]* diff --git a/org_mode_samples/object/statistics_cookie/empty.org b/org_mode_samples/object/statistics_cookie/empty.org new file mode 100644 index 00000000..f0168ae6 --- /dev/null +++ b/org_mode_samples/object/statistics_cookie/empty.org @@ -0,0 +1,4 @@ +[/] +[/2] +[3/] +[%] diff --git a/org_mode_samples/sections_and_headings/comment_heading.org b/org_mode_samples/sections_and_headings/comment_heading.org new file mode 100644 index 00000000..76a4ce9d --- /dev/null +++ b/org_mode_samples/sections_and_headings/comment_heading.org @@ -0,0 +1,2 @@ +* TODO [#A] COMMENT foo bar +baz diff --git a/org_mode_samples/sections_and_headings/section_with_planning_and_whitespace.org b/org_mode_samples/sections_and_headings/section_with_planning_and_whitespace.org new file mode 100644 index 00000000..24ba56dc --- /dev/null +++ b/org_mode_samples/sections_and_headings/section_with_planning_and_whitespace.org @@ -0,0 +1,4 @@ +* DONE foo + DEADLINE: <2023-09-08 Fri> + +* DONE bar diff --git a/org_mode_samples/sections_and_headings/statistics_cookie_with_space.org b/org_mode_samples/sections_and_headings/statistics_cookie_with_space.org new file mode 100644 index 00000000..6e000eff --- /dev/null +++ b/org_mode_samples/sections_and_headings/statistics_cookie_with_space.org @@ -0,0 +1 @@ +* [0/4] foo diff --git a/scripts/callgrind.bash b/scripts/callgrind.bash index 3bc909c4..9f13c7b2 100755 --- a/scripts/callgrind.bash +++ b/scripts/callgrind.bash @@ -4,10 +4,10 @@ set -euo pipefail IFS=$'\n\t' DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$DIR/../" -RUSTFLAGS="-C opt-level=0" cargo build --no-default-features -valgrind --tool=callgrind --callgrind-out-file=callgrind.out target/debug/parse "${@}" + +(cd "$DIR/../" && RUSTFLAGS="-C opt-level=0" cargo build --no-default-features) +valgrind --tool=callgrind --callgrind-out-file="$DIR/../callgrind.out" "$DIR/../target/debug/parse" "${@}" echo "You probably want to run:" -echo "callgrind_annotate --auto=yes callgrind.out" +echo "callgrind_annotate --auto=yes '$DIR/../callgrind.out'" diff --git a/scripts/perf.bash b/scripts/perf.bash index f40e58be..aa7ae329 100755 --- a/scripts/perf.bash +++ b/scripts/perf.bash @@ -6,8 +6,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" : ${PROFILE:="perf"} -cd "$DIR/../" - function main { local additional_flags=() if [ "$PROFILE" = "dev" ] || [ "$PROFILE" = "debug" ]; then @@ -15,12 +13,12 @@ function main { else additional_flags+=(--profile "$PROFILE") fi - cargo build --no-default-features "${additional_flags[@]}" - perf record --freq=2000 --call-graph dwarf --output=perf.data target/${PROFILE}/parse "${@}" + (cd "$DIR/../" && cargo build --no-default-features "${additional_flags[@]}") + perf record --freq=2000 --call-graph dwarf --output="$DIR/../perf.data" "$DIR/../target/${PROFILE}/parse" "${@}" # Convert to a format firefox will read # flags to consider --show-info - perf script -F +pid --input perf.data > perf.firefox + perf script -F +pid --input "$DIR/../perf.data" > "$DIR/../perf.firefox" echo "You probably want to go to https://profiler.firefox.com/" echo "Either that or run hotspot" diff --git a/scripts/run_docker_compare.bash b/scripts/run_docker_compare.bash index 7ac76e64..bfd25e15 100755 --- a/scripts/run_docker_compare.bash +++ b/scripts/run_docker_compare.bash @@ -9,7 +9,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" : ${BACKTRACE:="NO"} # or YES to print a rust backtrace when panicking : ${NO_COLOR:=""} # Set to anything to disable color output -cd "$DIR/../" REALPATH=$(command -v uu-realpath || command -v realpath) MAKE=$(command -v gmake || command -v make) @@ -56,10 +55,10 @@ function launch_container { local full_path=$($REALPATH "$path") local containing_folder=$(dirname "$full_path") local file_name=$(basename "$full_path") - docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "${containing_folder}:/input:ro" -v "$($REALPATH ./):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test "${additional_args[@]}" -- "/input/$file_name" + docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "${containing_folder}:/input:ro" -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test "${additional_args[@]}" -- "/input/$file_name" done else - docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "$($REALPATH ./):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test "${additional_args[@]}" + docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test "${additional_args[@]}" fi } diff --git a/scripts/run_docker_compare_bisect.bash b/scripts/run_docker_compare_bisect.bash index 0fa011a1..fd93ba49 100755 --- a/scripts/run_docker_compare_bisect.bash +++ b/scripts/run_docker_compare_bisect.bash @@ -5,7 +5,6 @@ set -euo pipefail IFS=$'\n\t' DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$DIR/../" REALPATH=$(command -v uu-realpath || command -v realpath) ############## Setup ######################### diff --git a/scripts/run_docker_integration_test.bash b/scripts/run_docker_integration_test.bash index 5ba5a373..bcf2646d 100755 --- a/scripts/run_docker_integration_test.bash +++ b/scripts/run_docker_integration_test.bash @@ -6,7 +6,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" : ${NO_COLOR:=""} # Set to anything to disable color output -cd "$DIR/../" REALPATH=$(command -v uu-realpath || command -v realpath) MAKE=$(command -v gmake || command -v make) @@ -56,7 +55,7 @@ cargo test --no-default-features --features compare --no-fail-fast --lib --test EOF ) - docker run "${additional_flags[@]}" --init --rm --read-only --mount type=tmpfs,destination=/tmp -v "$($REALPATH ./):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test sh -c "$init_script" + docker run "${additional_flags[@]}" --init --rm --read-only --mount type=tmpfs,destination=/tmp -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test sh -c "$init_script" } diff --git a/scripts/run_integration_test.bash b/scripts/run_integration_test.bash index 32da81da..095bee99 100755 --- a/scripts/run_integration_test.bash +++ b/scripts/run_integration_test.bash @@ -4,7 +4,6 @@ set -euo pipefail IFS=$'\n\t' DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$DIR/../" REALPATH=$(command -v uu-realpath || command -v realpath) function main { @@ -12,7 +11,7 @@ function main { local test while read test; do - cargo test --no-default-features --features compare --no-fail-fast --test test_loader "$test" -- --show-output + (cd "$DIR/../" && cargo test --no-default-features --features compare --no-fail-fast --test test_loader "$test" -- --show-output) done<<<"$test_names" } diff --git a/scripts/time_parse.bash b/scripts/time_parse.bash index 817bada2..6409f3f3 100755 --- a/scripts/time_parse.bash +++ b/scripts/time_parse.bash @@ -7,8 +7,6 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" : ${PROFILE:="release-lto"} -cd "$DIR/../" - function main { local additional_flags=() if [ "$PROFILE" = "dev" ] || [ "$PROFILE" = "debug" ]; then @@ -16,8 +14,8 @@ function main { else additional_flags+=(--profile "$PROFILE") fi - cargo build --no-default-features "${additional_flags[@]}" - time ./target/${PROFILE}/parse "${@}" + (cd "$DIR/../" && cargo build --no-default-features "${additional_flags[@]}") + time "$DIR/../target/${PROFILE}/parse" "${@}" } main "${@}" diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 0c881a59..c7289d1f 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; use std::collections::HashSet; use super::util::assert_bounds; @@ -45,6 +46,7 @@ use crate::types::PlainList; use crate::types::PlainListItem; use crate::types::PlainText; use crate::types::Planning; +use crate::types::PriorityCookie; use crate::types::PropertyDrawer; use crate::types::RadioLink; use crate::types::RadioTarget; @@ -490,7 +492,7 @@ fn compare_heading<'s>( if rust.stars.to_string() != level { this_status = DiffStatus::Bad; message = Some(format!( - "Headline level do not much (emacs != rust): {} != {}", + "Headline level do not match (emacs != rust): {} != {}", level, rust.stars )) } @@ -553,7 +555,57 @@ fn compare_heading<'s>( .collect::, _>>()?; child_status.push(artificial_diff_scope("title".to_owned(), title_status)?); - // TODO: Compare priority, :footnote-section-p, :archivedp, :commentedp + // Compare priority + let priority = get_property(emacs, ":priority")?; + match (priority, rust.priority_cookie) { + (None, None) => {} + (None, Some(_)) | (Some(_), None) => { + this_status = DiffStatus::Bad; + message = Some(format!( + "Priority cookie mismatch (emacs != rust) {:?} != {:?}", + priority, rust.priority_cookie + )); + } + (Some(emacs_priority_cookie), Some(rust_priority_cookie)) => { + let emacs_priority_cookie = + emacs_priority_cookie.as_atom()?.parse::()?; + if emacs_priority_cookie != rust_priority_cookie { + this_status = DiffStatus::Bad; + message = Some(format!( + "Priority cookie mismatch (emacs != rust) {:?} != {:?}", + emacs_priority_cookie, rust_priority_cookie + )); + } + } + } + + // Compare archived + let archived = get_property(emacs, ":archivedp")?; + match (archived, rust.is_archived) { + (None, true) | (Some(_), false) => { + this_status = DiffStatus::Bad; + message = Some(format!( + "archived mismatch (emacs != rust) {:?} != {:?}", + archived, rust.is_archived + )); + } + (None, false) | (Some(_), true) => {} + } + + // Compare commented + let commented = get_property(emacs, ":commentedp")?; + match (commented, rust.is_comment) { + (None, true) | (Some(_), false) => { + this_status = DiffStatus::Bad; + message = Some(format!( + "commented mismatch (emacs != rust) {:?} != {:?}", + commented, rust.is_comment + )); + } + (None, false) | (Some(_), true) => {} + } + + // TODO: Compare :footnote-section-p // Compare section let section_status = children @@ -1024,6 +1076,44 @@ fn compare_table<'s>( Ok(_) => {} }; + // Compare formulas + // + // :tblfm is either nil or a list () filled with quoted strings containing the value for any tblfm keywords at the end of the table. + let emacs_formulas = get_property(emacs, ":tblfm")?; + if let Some(emacs_formulas) = emacs_formulas { + let emacs_formulas = emacs_formulas.as_list()?; + if emacs_formulas.len() != rust.formulas.len() { + this_status = DiffStatus::Bad; + message = Some(format!( + "Formulas do not match (emacs != rust): {:?} != {:?}", + emacs_formulas, rust.formulas + )) + } else { + let atoms = emacs_formulas + .into_iter() + .map(Token::as_atom) + .collect::, _>>()?; + let unquoted = atoms + .into_iter() + .map(unquote) + .collect::, _>>()?; + for kw in &rust.formulas { + if !unquoted.contains(kw.value) { + this_status = DiffStatus::Bad; + message = Some(format!("Could not find formula in emacs: {}", kw.value)) + } + } + } + } else { + if !rust.formulas.is_empty() { + this_status = DiffStatus::Bad; + message = Some(format!( + "Formulas do not match (emacs != rust): {:?} != {:?}", + emacs_formulas, rust.formulas + )) + } + } + for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) { child_status.push(compare_table_row(source, emacs_child, rust_child)?); } @@ -1061,6 +1151,10 @@ fn compare_table_row<'s>( Ok(_) => {} }; + // TODO: Compare :type + // + // :type is an unquoted atom of either standard or rule + for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) { child_status.push(compare_table_cell(source, emacs_child, rust_child)?); } diff --git a/src/compare/util.rs b/src/compare/util.rs index 6367bf98..173ecd02 100644 --- a/src/compare/util.rs +++ b/src/compare/util.rs @@ -141,6 +141,11 @@ fn maybe_token_to_usize( .map_or(Ok(None), |r| r.map(Some))?) } +/// Get a named property from the emacs token. +/// +/// Returns Ok(None) if value is nil. +/// +/// Returns error if the attribute is not specified on the token at all. pub fn get_property<'s, 'x>( emacs: &'s Token<'s>, key: &'x str, diff --git a/src/context/context.rs b/src/context/context.rs index 0f41963b..a485cdf7 100644 --- a/src/context/context.rs +++ b/src/context/context.rs @@ -21,6 +21,9 @@ pub enum ContextElement<'r, 's> { /// Stores the name of the current element to prevent directly nesting elements of the same type. Context(&'r str), + /// Stores the name of the current object to prevent directly nesting elements of the same type. + ContextObject(&'r str), + /// Indicates if elements should consume the whitespace after them. ConsumeTrailingWhitespace(bool), @@ -105,7 +108,7 @@ impl<'g, 'r, 's> Context<'g, 'r, 's> { &'r self, i: OrgSource<'s>, ) -> IResult, OrgSource<'s>, CustomError>> { - let mut current_class_filter = ExitClass::Gamma; + let mut current_class_filter = ExitClass::Delta; for current_node in self.iter_context() { let context_element = current_node.get_data(); match context_element { diff --git a/src/context/exiting.rs b/src/context/exiting.rs index 6f8c359d..c989a335 100644 --- a/src/context/exiting.rs +++ b/src/context/exiting.rs @@ -1,16 +1,10 @@ #[derive(Debug, Copy, Clone)] pub enum ExitClass { - /// Headlines and sections. Document = 1, - - /// Elements who take priority over beta elements when matching. - Alpha = 20, - - /// Elements who cede priority to alpha elements when matching. - Beta = 300, - - /// Elements who cede priority to alpha and beta elements when matching. - Gamma = 4000, + Alpha = 2, + Beta = 3, + Gamma = 4, + Delta = 5, } impl std::fmt::Display for ExitClass { diff --git a/src/parser/document.rs b/src/parser/document.rs index 8c50dbd2..027ba70b 100644 --- a/src/parser/document.rs +++ b/src/parser/document.rs @@ -1,57 +1,28 @@ -use nom::branch::alt; -use nom::bytes::complete::tag; -use nom::character::complete::anychar; -use nom::character::complete::line_ending; -use nom::character::complete::space0; -use nom::character::complete::space1; use nom::combinator::all_consuming; -use nom::combinator::eof; -use nom::combinator::map; -use nom::combinator::not; use nom::combinator::opt; -use nom::combinator::recognize; -use nom::combinator::verify; use nom::multi::many0; -use nom::multi::many1; -use nom::multi::many1_count; -use nom::multi::many_till; -use nom::multi::separated_list1; -use nom::sequence::tuple; +use super::headline::heading; use super::in_buffer_settings::apply_in_buffer_settings; use super::in_buffer_settings::scan_for_in_buffer_settings; use super::org_source::OrgSource; +use super::section::zeroth_section; use super::token::AllTokensIterator; use super::token::Token; -use super::util::exit_matcher_parser; use super::util::get_consumed; -use super::util::start_of_line; use crate::context::parser_with_context; use crate::context::Context; use crate::context::ContextElement; -use crate::context::ExitClass; -use crate::context::ExitMatcherNode; 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::comment::comment; -use crate::parser::element_parser::element; -use crate::parser::object_parser::standard_set_object; use crate::parser::org_source::convert_error; -use crate::parser::planning::planning; -use crate::parser::property_drawer::property_drawer; use crate::parser::util::blank_line; -use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting; use crate::types::Document; -use crate::types::DocumentElement; -use crate::types::Element; -use crate::types::Heading; use crate::types::Object; -use crate::types::Section; -use crate::types::TodoKeywordType; /// Parse a full org-mode document. /// @@ -185,312 +156,6 @@ fn _document<'b, 'g, 'r, 's>( )) } -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn zeroth_section<'b, 'g, 'r, 's>( - context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, -) -> Res, Section<'s>> { - // TODO: The zeroth section is specialized so it probably needs its own parser - let contexts = [ - ContextElement::ConsumeTrailingWhitespace(true), - ContextElement::Context("section"), - ContextElement::ExitMatcherNode(ExitMatcherNode { - class: ExitClass::Document, - exit_matcher: §ion_end, - }), - ]; - 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 without_consuming_whitespace_context = ContextElement::ConsumeTrailingWhitespace(false); - let without_consuming_whitespace_context = - parser_context.with_additional_node(&without_consuming_whitespace_context); - - let element_matcher = parser_with_context!(element(true))(&parser_context); - let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); - - let (remaining, comment_and_property_drawer_element) = opt(tuple(( - opt(parser_with_context!(comment)( - &without_consuming_whitespace_context, - )), - parser_with_context!(property_drawer)(context), - many0(blank_line), - )))(input)?; - - let (remaining, (mut children, _exit_contents)) = verify( - many_till(element_matcher, exit_matcher), - |(children, _exit_contents)| { - !children.is_empty() || comment_and_property_drawer_element.is_some() - }, - )(remaining)?; - - comment_and_property_drawer_element.map(|(comment, property_drawer, _ws)| { - children.insert(0, Element::PropertyDrawer(property_drawer)); - comment - .map(Element::Comment) - .map(|ele| children.insert(0, ele)); - }); - - let (remaining, _trailing_ws) = - maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; - - let source = get_consumed(input, remaining); - Ok(( - remaining, - Section { - source: source.into(), - children, - }, - )) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn section<'b, 'g, 'r, 's>( - context: RefContext<'b, 'g, 'r, 's>, - mut input: OrgSource<'s>, -) -> Res, Section<'s>> { - // TODO: The zeroth section is specialized so it probably needs its own parser - let contexts = [ - ContextElement::ConsumeTrailingWhitespace(true), - ContextElement::Context("section"), - ContextElement::ExitMatcherNode(ExitMatcherNode { - class: ExitClass::Document, - exit_matcher: §ion_end, - }), - ]; - 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 element_matcher = parser_with_context!(element(true))(&parser_context); - let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); - let (mut remaining, (planning_element, property_drawer_element)) = tuple(( - opt(parser_with_context!(planning)(&parser_context)), - opt(parser_with_context!(property_drawer)(&parser_context)), - ))(input)?; - if planning_element.is_none() && property_drawer_element.is_none() { - let (remain, _ws) = many0(blank_line)(remaining)?; - remaining = remain; - input = remain; - } - let (remaining, (mut children, _exit_contents)) = verify( - many_till(element_matcher, exit_matcher), - |(children, _exit_contents)| { - !children.is_empty() || property_drawer_element.is_some() || planning_element.is_some() - }, - )(remaining)?; - property_drawer_element - .map(Element::PropertyDrawer) - .map(|ele| children.insert(0, ele)); - planning_element - .map(Element::Planning) - .map(|ele| children.insert(0, ele)); - - let (remaining, _trailing_ws) = - maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; - - let source = get_consumed(input, remaining); - Ok(( - remaining, - Section { - source: source.into(), - children, - }, - )) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn section_end<'b, 'g, 'r, 's>( - _context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { - recognize(detect_headline)(input) -} - -const fn heading( - parent_stars: usize, -) -> impl for<'b, 'g, 'r, 's> Fn( - RefContext<'b, 'g, 'r, 's>, - OrgSource<'s>, -) -> Res, Heading<'s>> { - move |context, input: OrgSource<'_>| _heading(context, input, parent_stars) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn _heading<'b, 'g, 'r, 's>( - context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, - parent_stars: usize, -) -> Res, Heading<'s>> { - not(|i| context.check_exit_matcher(i))(input)?; - let (remaining, (star_count, _ws, maybe_todo_keyword, title, heading_tags)) = - headline(context, input, parent_stars)?; - let section_matcher = parser_with_context!(section)(context); - let heading_matcher = parser_with_context!(heading(star_count))(context); - let (remaining, maybe_section) = - opt(map(section_matcher, DocumentElement::Section))(remaining)?; - let (remaining, mut children) = - many0(map(heading_matcher, DocumentElement::Heading))(remaining)?; - if let Some(section) = maybe_section { - children.insert(0, section); - } - let remaining = if children.is_empty() { - // Support empty headings - let (remain, _ws) = many0(blank_line)(remaining)?; - remain - } else { - remaining - }; - - let source = get_consumed(input, remaining); - Ok(( - remaining, - Heading { - source: source.into(), - stars: star_count, - todo_keyword: maybe_todo_keyword.map(|((todo_keyword_type, todo_keyword), _ws)| { - (todo_keyword_type, Into::<&str>::into(todo_keyword)) - }), - title, - tags: heading_tags, - children, - }, - )) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn detect_headline<'s>(input: OrgSource<'s>) -> Res, ()> { - tuple((start_of_line, many1(tag("*")), space1))(input)?; - Ok((input, ())) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn headline<'b, 'g, 'r, 's>( - context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, - parent_stars: usize, -) -> Res< - OrgSource<'s>, - ( - usize, - OrgSource<'s>, - Option<((TodoKeywordType, OrgSource<'s>), OrgSource<'s>)>, - Vec>, - Vec<&'s str>, - ), -> { - let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { - class: ExitClass::Document, - exit_matcher: &headline_title_end, - }); - let parser_context = context.with_additional_node(&parser_context); - - let ( - remaining, - (_sol, star_count, ws, maybe_todo_keyword, title, maybe_tags, _ws, _line_ending), - ) = tuple(( - start_of_line, - verify(many1_count(tag("*")), |star_count| { - *star_count > parent_stars - }), - space1, - opt(tuple(( - parser_with_context!(heading_keyword)(&parser_context), - space1, - ))), - many1(parser_with_context!(standard_set_object)(&parser_context)), - opt(tuple((space0, tags))), - space0, - alt((line_ending, eof)), - ))(input)?; - Ok(( - remaining, - ( - star_count, - ws, - maybe_todo_keyword, - title, - maybe_tags - .map(|(_ws, tags)| { - tags.into_iter() - .map(|single_tag| Into::<&str>::into(single_tag)) - .collect() - }) - .unwrap_or(Vec::new()), - ), - )) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn headline_title_end<'b, 'g, 'r, 's>( - _context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { - recognize(tuple(( - opt(tuple((space0, tags, space0))), - alt((line_ending, eof)), - )))(input) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn tags<'s>(input: OrgSource<'s>) -> Res, Vec>> { - let (remaining, (_open, tags, _close)) = - tuple((tag(":"), separated_list1(tag(":"), single_tag), tag(":")))(input)?; - Ok((remaining, tags)) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { - recognize(many1(verify(anychar, |c| { - c.is_alphanumeric() || "_@#%".contains(*c) - })))(input) -} - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn heading_keyword<'b, 'g, 'r, 's>( - context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, -) -> Res, (TodoKeywordType, OrgSource<'s>)> { - let global_settings = context.get_global_settings(); - if global_settings.in_progress_todo_keywords.is_empty() - && global_settings.complete_todo_keywords.is_empty() - { - alt(( - map(tag("TODO"), |capture| (TodoKeywordType::Todo, capture)), - map(tag("DONE"), |capture| (TodoKeywordType::Done, capture)), - ))(input) - } else { - for todo_keyword in global_settings - .in_progress_todo_keywords - .iter() - .map(String::as_str) - { - let result = tag::<_, _, CustomError<_>>(todo_keyword)(input); - match result { - Ok((remaining, ent)) => { - return Ok((remaining, (TodoKeywordType::Todo, ent))); - } - Err(_) => {} - } - } - for todo_keyword in global_settings - .complete_todo_keywords - .iter() - .map(String::as_str) - { - let result = tag::<_, _, CustomError<_>>(todo_keyword)(input); - match result { - Ok((remaining, ent)) => { - return Ok((remaining, (TodoKeywordType::Done, ent))); - } - Err(_) => {} - } - } - Err(nom::Err::Error(CustomError::MyError(MyError( - "NoTodoKeyword".into(), - )))) - } -} - impl<'s> Document<'s> { pub fn iter_tokens<'r>(&'r self) -> impl Iterator> { AllTokensIterator::new(Token::Document(self)) diff --git a/src/parser/footnote_definition.rs b/src/parser/footnote_definition.rs index e413d9d6..9d5d79a6 100644 --- a/src/parser/footnote_definition.rs +++ b/src/parser/footnote_definition.rs @@ -4,13 +4,16 @@ use nom::bytes::complete::tag_no_case; use nom::bytes::complete::take_while; use nom::character::complete::digit1; use nom::character::complete::space0; +use nom::combinator::opt; 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::org_source::OrgSource; +use super::util::include_input; use super::util::WORD_CONSTITUENT_CHARACTERS; use crate::context::parser_with_context; use crate::context::ContextElement; @@ -41,8 +44,15 @@ pub fn footnote_definition<'b, 'g, 'r, 's>( } start_of_line(input)?; // Cannot be indented. - let (remaining, (_lead_in, lbl, _lead_out, _ws)) = - tuple((tag_no_case("[fn:"), label, tag("]"), space0))(input)?; + let (remaining, (_, lbl, _, _, _)) = tuple(( + tag_no_case("[fn:"), + label, + tag("]"), + space0, + opt(verify(many0(blank_line), |lines: &Vec>| { + lines.len() <= 2 + })), + ))(input)?; let contexts = [ ContextElement::ConsumeTrailingWhitespace(true), ContextElement::Context("footnote definition"), @@ -54,11 +64,22 @@ pub fn footnote_definition<'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]); - // TODO: The problem is we are not accounting for trailing whitespace like we do in section. Maybe it would be easier if we passed down whether or not to parse trailing whitespace into the element matcher similar to how tag takes in parameters. let element_matcher = parser_with_context!(element(true))(&parser_context); let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); - let (remaining, (children, _exit_contents)) = - many_till(element_matcher, exit_matcher)(remaining)?; + let (mut remaining, (mut children, _exit_contents)) = + many_till(include_input(element_matcher), exit_matcher)(remaining)?; + + // Re-parse the last element of the footnote definition with consume trailing whitespace off because the trailing whitespace needs to belong to the footnote definition, not the contents. + if context.should_consume_trailing_whitespace() { + if let Some((final_item_input, _)) = children.pop() { + let final_item_context = ContextElement::ConsumeTrailingWhitespace(false); + let final_item_context = parser_context.with_additional_node(&final_item_context); + let (remain, reparsed_final_item) = + parser_with_context!(element(true))(&final_item_context)(final_item_input)?; + children.push((final_item_input, reparsed_final_item)); + remaining = remain; + } + } let source = get_consumed(input, remaining); Ok(( @@ -66,7 +87,7 @@ pub fn footnote_definition<'b, 'g, 'r, 's>( FootnoteDefinition { source: source.into(), label: lbl.into(), - children, + children: children.into_iter().map(|(_, item)| item).collect(), }, )) } diff --git a/src/parser/footnote_reference.rs b/src/parser/footnote_reference.rs index 0f3738c2..28f8b527 100644 --- a/src/parser/footnote_reference.rs +++ b/src/parser/footnote_reference.rs @@ -42,7 +42,7 @@ fn anonymous_footnote<'b, 'g, 'r, 's>( let (remaining, _) = tag_no_case("[fn::")(input)?; let exit_with_depth = footnote_definition_end(remaining.get_bracket_depth()); let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { - class: ExitClass::Beta, + class: ExitClass::Gamma, exit_matcher: &exit_with_depth, }); let parser_context = context.with_additional_node(&parser_context); @@ -78,7 +78,7 @@ fn inline_footnote<'b, 'g, 'r, 's>( let (remaining, _) = tag(":")(remaining)?; let exit_with_depth = footnote_definition_end(remaining.get_bracket_depth()); let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { - class: ExitClass::Beta, + class: ExitClass::Gamma, exit_matcher: &exit_with_depth, }); let parser_context = context.with_additional_node(&parser_context); diff --git a/src/parser/headline.rs b/src/parser/headline.rs new file mode 100644 index 00000000..6be91599 --- /dev/null +++ b/src/parser/headline.rs @@ -0,0 +1,258 @@ +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::character::complete::anychar; +use nom::character::complete::line_ending; +use nom::character::complete::space0; +use nom::character::complete::space1; +use nom::combinator::eof; +use nom::combinator::map; +use nom::combinator::not; +use nom::combinator::opt; +use nom::combinator::recognize; +use nom::combinator::verify; +use nom::multi::many0; +use nom::multi::many1; +use nom::multi::many1_count; +use nom::multi::separated_list1; +use nom::sequence::tuple; + +use super::org_source::OrgSource; +use super::section::section; +use super::util::get_consumed; +use super::util::start_of_line; +use crate::context::parser_with_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; +use crate::types::DocumentElement; +use crate::types::Heading; +use crate::types::Object; +use crate::types::PriorityCookie; +use crate::types::TodoKeywordType; + +pub const fn heading( + parent_stars: usize, +) -> impl for<'b, 'g, 'r, 's> Fn( + RefContext<'b, 'g, 'r, 's>, + OrgSource<'s>, +) -> Res, Heading<'s>> { + move |context, input: OrgSource<'_>| _heading(context, input, parent_stars) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn _heading<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, + parent_stars: usize, +) -> Res, Heading<'s>> { + not(|i| context.check_exit_matcher(i))(input)?; + let ( + remaining, + (star_count, maybe_todo_keyword, maybe_priority, maybe_comment, title, heading_tags), + ) = headline(context, input, parent_stars)?; + let section_matcher = parser_with_context!(section)(context); + let heading_matcher = parser_with_context!(heading(star_count))(context); + let (remaining, maybe_section) = + opt(map(section_matcher, DocumentElement::Section))(remaining)?; + let (remaining, _ws) = opt(tuple((start_of_line, many0(blank_line))))(remaining)?; + let (remaining, mut children) = + many0(map(heading_matcher, DocumentElement::Heading))(remaining)?; + if let Some(section) = maybe_section { + children.insert(0, section); + } + let remaining = if children.is_empty() { + // Support empty headings + let (remain, _ws) = many0(blank_line)(remaining)?; + remain + } else { + remaining + }; + let is_archived = heading_tags.contains(&"ARCHIVE"); + + let source = get_consumed(input, remaining); + Ok(( + remaining, + Heading { + source: source.into(), + stars: star_count, + todo_keyword: maybe_todo_keyword.map(|((todo_keyword_type, todo_keyword), _ws)| { + (todo_keyword_type, Into::<&str>::into(todo_keyword)) + }), + priority_cookie: maybe_priority.map(|(priority, _)| priority), + title, + tags: heading_tags, + children, + is_comment: maybe_comment.is_some(), + is_archived, + }, + )) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn detect_headline<'s>(input: OrgSource<'s>) -> Res, ()> { + tuple((start_of_line, many1(tag("*")), space1))(input)?; + Ok((input, ())) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn headline<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, + parent_stars: usize, +) -> Res< + OrgSource<'s>, + ( + usize, + Option<((TodoKeywordType, OrgSource<'s>), OrgSource<'s>)>, + Option<(PriorityCookie, OrgSource<'s>)>, + Option<(OrgSource<'s>, OrgSource<'s>)>, + Vec>, + Vec<&'s str>, + ), +> { + let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Document, + exit_matcher: &headline_title_end, + }); + let parser_context = context.with_additional_node(&parser_context); + + let ( + remaining, + ( + _, + star_count, + _, + maybe_todo_keyword, + maybe_priority, + maybe_comment, + title, + maybe_tags, + _, + _, + ), + ) = tuple(( + start_of_line, + verify(many1_count(tag("*")), |star_count| { + *star_count > parent_stars + }), + space1, + opt(tuple(( + parser_with_context!(heading_keyword)(&parser_context), + space1, + ))), + opt(tuple((priority_cookie, space1))), + opt(tuple((tag("COMMENT"), space1))), + many1(parser_with_context!(standard_set_object)(&parser_context)), + opt(tuple((space0, tags))), + space0, + alt((line_ending, eof)), + ))(input)?; + Ok(( + remaining, + ( + star_count, + maybe_todo_keyword, + maybe_priority, + maybe_comment, + title, + maybe_tags + .map(|(_ws, tags)| { + tags.into_iter() + .map(|single_tag| Into::<&str>::into(single_tag)) + .collect() + }) + .unwrap_or(Vec::new()), + ), + )) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn headline_title_end<'b, 'g, 'r, 's>( + _context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, OrgSource<'s>> { + recognize(tuple(( + opt(tuple((space0, tags, space0))), + alt((line_ending, eof)), + )))(input) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn tags<'s>(input: OrgSource<'s>) -> Res, Vec>> { + let (remaining, (_open, tags, _close)) = + tuple((tag(":"), separated_list1(tag(":"), single_tag), tag(":")))(input)?; + Ok((remaining, tags)) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn single_tag<'r, 's>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + recognize(many1(verify(anychar, |c| { + c.is_alphanumeric() || "_@#%".contains(*c) + })))(input) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn heading_keyword<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, (TodoKeywordType, OrgSource<'s>)> { + let global_settings = context.get_global_settings(); + if global_settings.in_progress_todo_keywords.is_empty() + && global_settings.complete_todo_keywords.is_empty() + { + alt(( + map(tag("TODO"), |capture| (TodoKeywordType::Todo, capture)), + map(tag("DONE"), |capture| (TodoKeywordType::Done, capture)), + ))(input) + } else { + for todo_keyword in global_settings + .in_progress_todo_keywords + .iter() + .map(String::as_str) + { + let result = tag::<_, _, CustomError<_>>(todo_keyword)(input); + match result { + Ok((remaining, ent)) => { + return Ok((remaining, (TodoKeywordType::Todo, ent))); + } + Err(_) => {} + } + } + for todo_keyword in global_settings + .complete_todo_keywords + .iter() + .map(String::as_str) + { + let result = tag::<_, _, CustomError<_>>(todo_keyword)(input); + match result { + Ok((remaining, ent)) => { + return Ok((remaining, (TodoKeywordType::Done, ent))); + } + Err(_) => {} + } + } + Err(nom::Err::Error(CustomError::MyError(MyError( + "NoTodoKeyword".into(), + )))) + } +} + +fn priority_cookie<'s>(input: OrgSource<'s>) -> Res, 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(), + ))) + })?; + Ok((remaining, cookie)) +} diff --git a/src/parser/keyword.rs b/src/parser/keyword.rs index 200670a2..c5cc4763 100644 --- a/src/parser/keyword.rs +++ b/src/parser/keyword.rs @@ -5,8 +5,8 @@ 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::character::complete::space1; use nom::combinator::consumed; use nom::combinator::eof; use nom::combinator::not; @@ -66,7 +66,7 @@ fn _filtered_keyword<'s, F: Matcher>( } Err(_) => {} }; - let (remaining, _ws) = space1(remaining)?; + let (remaining, _ws) = space0(remaining)?; let (remaining, parsed_value) = recognize(many_till( anychar, peek(tuple((space0, alt((line_ending, eof))))), @@ -111,13 +111,30 @@ fn babel_call_key<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> tag_no_case("call")(input) } +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn table_formula_keyword<'b, 'g, 'r, 's>( + _context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, Keyword<'s>> { + filtered_keyword(table_formula_key)(input) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn table_formula_key<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { + tag_no_case("tblfm")(input) +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn regular_keyword_key<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { - recognize(tuple(( - not(peek(tag_no_case("call"))), - not(peek(tag_no_case("begin"))), - is_not(" \t\r\n:"), - )))(input) + not(peek(alt((tag_no_case("call"), tag_no_case("begin")))))(input)?; + recognize(many_till( + anychar, + peek(alt(( + recognize(one_of(" \t\r\n")), // Give up if we hit whitespace + recognize(tuple((tag(":"), one_of(" \t\r\n")))), // Stop if we see a colon followed by whitespace + recognize(tuple((tag(":"), is_not(" \t\r\n:"), not(tag(":"))))), // Stop if we see a colon that is the last colon before whitespace. This is for keywords like "#+foo:bar:baz: lorem: ipsum" which would have the key "foo:bar:baz". + ))), + ))(input) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0b959743..30b685a0 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -14,6 +14,7 @@ mod fixed_width_area; mod footnote_definition; mod footnote_reference; mod greater_block; +mod headline; mod horizontal_rule; mod in_buffer_settings; mod inline_babel_call; @@ -35,6 +36,7 @@ mod planning; mod property_drawer; mod radio_link; mod regular_link; +mod section; pub mod sexp; mod statistics_cookie; mod subscript_and_superscript; diff --git a/src/parser/object_parser.rs b/src/parser/object_parser.rs index e3db5531..5542618b 100644 --- a/src/parser/object_parser.rs +++ b/src/parser/object_parser.rs @@ -4,8 +4,11 @@ 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; @@ -34,54 +37,11 @@ pub fn standard_set_object<'b, 'g, 'r, 's>( input: OrgSource<'s>, ) -> Res, Object<'s>> { let (remaining, object) = alt(( - map(parser_with_context!(timestamp)(context), Object::Timestamp), - map(parser_with_context!(subscript)(context), Object::Subscript), + parser_with_context!(standard_set_object_sans_plain_text)(context), map( - parser_with_context!(superscript)(context), - Object::Superscript, + parser_with_context!(plain_text(detect_standard_set_object_sans_plain_text))(context), + Object::PlainText, ), - 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), - map(parser_with_context!(plain_text)(context), Object::PlainText), ))(input)?; Ok((remaining, object)) } @@ -92,24 +52,17 @@ pub fn minimal_set_object<'b, 'g, 'r, 's>( input: OrgSource<'s>, ) -> Res, Object<'s>> { let (remaining, object) = alt(( - map(parser_with_context!(subscript)(context), Object::Subscript), + parser_with_context!(minimal_set_object_sans_plain_text)(context), map( - parser_with_context!(superscript)(context), - Object::Superscript, + parser_with_context!(plain_text(detect_minimal_set_object_sans_plain_text))(context), + Object::PlainText, ), - map(parser_with_context!(entity)(context), Object::Entity), - map( - parser_with_context!(latex_fragment)(context), - Object::LatexFragment, - ), - parser_with_context!(text_markup)(context), - map(parser_with_context!(plain_text)(context), Object::PlainText), ))(input)?; Ok((remaining, object)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -pub fn any_object_except_plain_text<'b, 'g, 'r, 's>( +fn standard_set_object_sans_plain_text<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Object<'s>> { @@ -166,7 +119,80 @@ pub fn any_object_except_plain_text<'b, 'g, 'r, 's>( } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -pub fn regular_link_description_object_set<'b, 'g, 'r, 's>( +fn minimal_set_object_sans_plain_text<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, 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)) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn detect_standard_set_object_sans_plain_text<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, ()> { + if detect_subscript_or_superscript(input).is_ok() { + return Ok((input, ())); + } + if standard_set_object_sans_plain_text(context, input).is_ok() { + return Ok((input, ())); + } + + return Err(nom::Err::Error(CustomError::MyError(MyError( + "No object detected.".into(), + )))); +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn detect_minimal_set_object_sans_plain_text<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, ()> { + if detect_subscript_or_superscript(input).is_ok() { + return Ok((input, ())); + } + if minimal_set_object_sans_plain_text(context, input).is_ok() { + return Ok((input, ())); + } + + return Err(nom::Err::Error(CustomError::MyError(MyError( + "No object detected.".into(), + )))); +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn regular_link_description_set_object<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, 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)) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn regular_link_description_set_object_sans_plain_text<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Object<'s>> { @@ -189,15 +215,47 @@ pub fn regular_link_description_object_set<'b, 'g, 'r, 's>( Object::InlineBabelCall, ), map(parser_with_context!(org_macro)(context), Object::OrgMacro), - parser_with_context!(minimal_set_object)(context), + parser_with_context!(minimal_set_object_sans_plain_text)(context), ))(input)?; Ok((remaining, object)) } +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn detect_regular_link_description_set_object_sans_plain_text<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, ()> { + if detect_subscript_or_superscript(input).is_ok() { + return Ok((input, ())); + } + if regular_link_description_set_object_sans_plain_text(context, input).is_ok() { + return Ok((input, ())); + } + + return Err(nom::Err::Error(CustomError::MyError(MyError( + "No object detected.".into(), + )))); +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub fn table_cell_set_object<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, +) -> Res, 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)) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn table_cell_set_object_sans_plain_text<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, ) -> Res, Object<'s>> { let (remaining, object) = alt(( map(parser_with_context!(citation)(context), Object::Citation), @@ -223,7 +281,24 @@ pub fn table_cell_set_object<'b, 'g, 'r, 's>( ), map(parser_with_context!(target)(context), Object::Target), map(parser_with_context!(timestamp)(context), Object::Timestamp), - parser_with_context!(minimal_set_object)(context), + parser_with_context!(minimal_set_object_sans_plain_text)(context), ))(input)?; Ok((remaining, object)) } + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn detect_table_cell_set_object_sans_plain_text<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, ()> { + if detect_subscript_or_superscript(input).is_ok() { + return Ok((input, ())); + } + if table_cell_set_object_sans_plain_text(context, input).is_ok() { + return Ok((input, ())); + } + + return Err(nom::Err::Error(CustomError::MyError(MyError( + "No object detected.".into(), + )))); +} diff --git a/src/parser/org_source.rs b/src/parser/org_source.rs index 87f93da6..820c01c1 100644 --- a/src/parser/org_source.rs +++ b/src/parser/org_source.rs @@ -145,6 +145,9 @@ where if new_end > self.end { panic!("Attempted to extend past the end of the WrappedInput.") } + if new_start == self.start && new_end == self.end { + return self.clone(); + } let skipped_text = &self.full_source[self.start..new_start]; let mut start_of_line = self.start_of_line; @@ -183,7 +186,7 @@ where start: new_start, end: new_end, start_of_line, - preceding_character: skipped_text.chars().last(), + preceding_character: skipped_text.chars().last().or(self.preceding_character), bracket_depth, brace_depth, parenthesis_depth, diff --git a/src/parser/plain_list.rs b/src/parser/plain_list.rs index 9dbc117a..4f4bc498 100644 --- a/src/parser/plain_list.rs +++ b/src/parser/plain_list.rs @@ -19,6 +19,7 @@ use nom::sequence::tuple; use super::element_parser::element; use super::object_parser::standard_set_object; use super::org_source::OrgSource; +use super::util::include_input; use super::util::non_whitespace_character; use crate::context::parser_with_context; use crate::context::ContextElement; @@ -152,13 +153,12 @@ pub fn plain_list_item<'b, 'g, 'r, 's>( // TODO: parse checkbox - let (remaining, maybe_tag) = opt(tuple(( - space1, - parser_with_context!(item_tag)(context), - tag(" ::"), - )))(remaining)?; - let maybe_contentless_item: Res, OrgSource<'_>> = - peek(recognize(tuple((many0(blank_line), eof))))(remaining); + let (remaining, maybe_tag) = + opt(tuple((space1, parser_with_context!(item_tag)(context))))(remaining)?; + + let maybe_contentless_item: Res, ()> = peek(parser_with_context!( + detect_contentless_item_contents + )(context))(remaining); match maybe_contentless_item { Ok((_rem, _ws)) => { let (remaining, _trailing_ws) = opt(blank_line)(remaining)?; @@ -170,7 +170,7 @@ pub fn plain_list_item<'b, 'g, 'r, 's>( indentation: indent_level, bullet: bull.into(), tag: maybe_tag - .map(|(_ws, item_tag, _divider)| item_tag) + .map(|(_ws, item_tag)| item_tag) .unwrap_or(Vec::new()), children: Vec::new(), }, @@ -219,25 +219,13 @@ pub fn plain_list_item<'b, 'g, 'r, 's>( indentation: indent_level, bullet: bull.into(), tag: maybe_tag - .map(|(_ws, item_tag, _divider)| item_tag) + .map(|(_ws, item_tag)| item_tag) .unwrap_or(Vec::new()), children: children.into_iter().map(|(_start, item)| item).collect(), }, )); } -fn include_input<'s, F, O>( - mut inner: F, -) -> impl FnMut(OrgSource<'s>) -> Res, (OrgSource<'s>, O)> -where - F: FnMut(OrgSource<'s>) -> Res, O>, -{ - move |input: OrgSource<'_>| { - let (remaining, output) = inner(input)?; - Ok((remaining, (input, output))) - } -} - #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn bullet<'s>(i: OrgSource<'s>) -> Res, OrgSource<'s>> { alt(( @@ -313,11 +301,18 @@ fn item_tag<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Vec>> { - let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { - class: ExitClass::Gamma, - exit_matcher: &item_tag_end, - }); - let parser_context = context.with_additional_node(&parser_context); + let contexts = [ + ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Gamma, + exit_matcher: &item_tag_line_ending_end, + }), + ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Delta, + exit_matcher: &item_tag_end, + }), + ]; + let parser_context = context.with_additional_node(&contexts[0]); + let parser_context = parser_context.with_additional_node(&contexts[1]); let (remaining, (children, _exit_contents)) = verify( many_till( // TODO: Should this be using a different set like the minimal set? @@ -326,6 +321,7 @@ fn item_tag<'b, 'g, 'r, 's>( ), |(children, _exit_contents)| !children.is_empty(), )(input)?; + let (remaining, _) = tuple((one_of(" \t"), tag("::")))(remaining)?; Ok((remaining, children)) } @@ -334,13 +330,21 @@ fn item_tag_end<'b, 'g, 'r, 's>( _context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, OrgSource<'s>> { - recognize(alt(( - line_ending, - tag(" :: "), - recognize(tuple((tag(" ::"), alt((line_ending, eof))))), + recognize(tuple(( + one_of(" \t"), + tag("::"), + alt((recognize(one_of(" \t")), line_ending, eof)), )))(input) } +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn item_tag_line_ending_end<'b, 'g, 'r, 's>( + _context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, OrgSource<'s>> { + line_ending(input) +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn item_tag_post_gap<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, @@ -362,6 +366,18 @@ fn item_tag_post_gap<'b, 'g, 'r, 's>( )(input) } +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn detect_contentless_item_contents<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, ()> { + let (remaining, _) = recognize(many_till( + blank_line, + parser_with_context!(exit_matcher_parser)(context), + ))(input)?; + Ok((remaining, ())) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/parser/plain_text.rs b/src/parser/plain_text.rs index 1dbc2956..b50bd139 100644 --- a/src/parser/plain_text.rs +++ b/src/parser/plain_text.rs @@ -7,7 +7,6 @@ use nom::combinator::recognize; use nom::combinator::verify; use nom::multi::many_till; -use super::object_parser::any_object_except_plain_text; use super::org_source::OrgSource; use super::radio_link::RematchObject; use super::util::exit_matcher_parser; @@ -17,17 +16,42 @@ use crate::error::Res; use crate::types::Object; use crate::types::PlainText; -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -pub fn plain_text<'b, 'g, 'r, 's>( +pub fn plain_text( + end_condition: F, +) -> impl for<'b, 'g, 'r, 's> Fn( + RefContext<'b, 'g, 'r, 's>, + OrgSource<'s>, +) -> Res, PlainText<'s>> +where + F: for<'bb, 'gg, 'rr, 'ss> Fn( + RefContext<'bb, 'gg, 'rr, 'ss>, + OrgSource<'ss>, + ) -> Res, ()>, +{ + move |context, input| _plain_text(&end_condition, context, input) +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(ret, level = "debug", skip(end_condition)) +)] +fn _plain_text<'b, 'g, 'r, 's, F>( + end_condition: F, context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, -) -> Res, PlainText<'s>> { +) -> Res, PlainText<'s>> +where + F: for<'bb, 'gg, 'rr, 'ss> Fn( + RefContext<'bb, 'gg, 'rr, 'ss>, + OrgSource<'ss>, + ) -> Res, ()>, +{ let (remaining, source) = recognize(verify( many_till( anychar, peek(alt(( parser_with_context!(exit_matcher_parser)(context), - parser_with_context!(plain_text_end)(context), + recognize(parser_with_context!(end_condition)(context)), ))), ), |(children, _exit_contents)| !children.is_empty(), @@ -41,14 +65,6 @@ pub fn plain_text<'b, 'g, 'r, 's>( )) } -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn plain_text_end<'b, 'g, 'r, 's>( - context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { - recognize(parser_with_context!(any_object_except_plain_text)(context))(input) -} - impl<'x> RematchObject<'x> for PlainText<'x> { #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn rematch_object<'b, 'g, 'r, 's>( @@ -73,6 +89,7 @@ mod tests { use crate::context::ContextElement; use crate::context::GlobalSettings; use crate::context::List; + use crate::parser::object_parser::detect_standard_set_object_sans_plain_text; use crate::types::Source; #[test] @@ -81,7 +98,9 @@ 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)(&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(); assert_eq!(Into::<&str>::into(remaining), ""); assert_eq!(result.get_source(), Into::<&str>::into(input)); diff --git a/src/parser/planning.rs b/src/parser/planning.rs index 77e70c1e..10864abd 100644 --- a/src/parser/planning.rs +++ b/src/parser/planning.rs @@ -10,6 +10,7 @@ use nom::multi::separated_list1; use nom::sequence::tuple; use super::org_source::OrgSource; +use super::util::maybe_consume_trailing_whitespace_if_not_exiting; use crate::context::RefContext; use crate::error::Res; use crate::parser::util::get_consumed; @@ -18,7 +19,7 @@ use crate::types::Planning; #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub fn planning<'b, 'g, 'r, 's>( - _context: RefContext<'b, 'g, 'r, 's>, + context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Planning<'s>> { start_of_line(input)?; @@ -26,6 +27,8 @@ pub fn planning<'b, 'g, 'r, 's>( let (remaining, _planning_parameters) = separated_list1(space1, planning_parameter)(remaining)?; let (remaining, _trailing_ws) = tuple((space0, alt((line_ending, eof))))(remaining)?; + let (remaining, _trailing_ws) = + maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; let source = get_consumed(input, remaining); Ok(( diff --git a/src/parser/regular_link.rs b/src/parser/regular_link.rs index 615e77f5..e959a28f 100644 --- a/src/parser/regular_link.rs +++ b/src/parser/regular_link.rs @@ -6,7 +6,7 @@ use nom::character::complete::one_of; use nom::combinator::verify; use nom::multi::many_till; -use super::object_parser::regular_link_description_object_set; +use super::object_parser::regular_link_description_set_object; use super::org_source::OrgSource; use super::util::exit_matcher_parser; use super::util::get_consumed; @@ -99,7 +99,7 @@ pub fn description<'b, 'g, 'r, 's>( let parser_context = context.with_additional_node(&parser_context); let (remaining, (children, _exit_contents)) = verify( many_till( - parser_with_context!(regular_link_description_object_set)(&parser_context), + parser_with_context!(regular_link_description_set_object)(&parser_context), parser_with_context!(exit_matcher_parser)(&parser_context), ), |(children, _exit_contents)| !children.is_empty(), diff --git a/src/parser/section.rs b/src/parser/section.rs new file mode 100644 index 00000000..bcc4f36c --- /dev/null +++ b/src/parser/section.rs @@ -0,0 +1,144 @@ +use nom::combinator::opt; +use nom::combinator::recognize; +use nom::combinator::verify; +use nom::multi::many0; +use nom::multi::many_till; +use nom::sequence::tuple; + +use super::headline::detect_headline; +use super::org_source::OrgSource; +use super::util::exit_matcher_parser; +use super::util::get_consumed; +use crate::context::parser_with_context; +use crate::context::ContextElement; +use crate::context::ExitClass; +use crate::context::ExitMatcherNode; +use crate::context::RefContext; +use crate::error::Res; +use crate::parser::comment::comment; +use crate::parser::element_parser::element; +use crate::parser::planning::planning; +use crate::parser::property_drawer::property_drawer; +use crate::parser::util::blank_line; +use crate::parser::util::maybe_consume_trailing_whitespace_if_not_exiting; +use crate::types::Element; +use crate::types::Section; + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn zeroth_section<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, Section<'s>> { + let contexts = [ + ContextElement::ConsumeTrailingWhitespace(true), + ContextElement::Context("section"), + ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Document, + exit_matcher: §ion_end, + }), + ]; + 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 without_consuming_whitespace_context = ContextElement::ConsumeTrailingWhitespace(false); + let without_consuming_whitespace_context = + parser_context.with_additional_node(&without_consuming_whitespace_context); + + let element_matcher = parser_with_context!(element(true))(&parser_context); + let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); + + let (remaining, comment_and_property_drawer_element) = opt(tuple(( + opt(parser_with_context!(comment)( + &without_consuming_whitespace_context, + )), + parser_with_context!(property_drawer)(context), + many0(blank_line), + )))(input)?; + + let (remaining, (mut children, _exit_contents)) = verify( + many_till(element_matcher, exit_matcher), + |(children, _exit_contents)| { + !children.is_empty() || comment_and_property_drawer_element.is_some() + }, + )(remaining)?; + + comment_and_property_drawer_element.map(|(comment, property_drawer, _ws)| { + children.insert(0, Element::PropertyDrawer(property_drawer)); + comment + .map(Element::Comment) + .map(|ele| children.insert(0, ele)); + }); + + let (remaining, _trailing_ws) = + maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; + + let source = get_consumed(input, remaining); + Ok(( + remaining, + Section { + source: source.into(), + children, + }, + )) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn section<'b, 'g, 'r, 's>( + context: RefContext<'b, 'g, 'r, 's>, + mut input: OrgSource<'s>, +) -> Res, Section<'s>> { + let contexts = [ + ContextElement::ConsumeTrailingWhitespace(true), + ContextElement::Context("section"), + ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Document, + exit_matcher: §ion_end, + }), + ]; + 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 element_matcher = parser_with_context!(element(true))(&parser_context); + let exit_matcher = parser_with_context!(exit_matcher_parser)(&parser_context); + let (mut remaining, (planning_element, property_drawer_element)) = tuple(( + opt(parser_with_context!(planning)(&parser_context)), + opt(parser_with_context!(property_drawer)(&parser_context)), + ))(input)?; + if planning_element.is_none() && property_drawer_element.is_none() { + let (remain, _ws) = many0(blank_line)(remaining)?; + remaining = remain; + input = remain; + } + let (remaining, (mut children, _exit_contents)) = verify( + many_till(element_matcher, exit_matcher), + |(children, _exit_contents)| { + !children.is_empty() || property_drawer_element.is_some() || planning_element.is_some() + }, + )(remaining)?; + property_drawer_element + .map(Element::PropertyDrawer) + .map(|ele| children.insert(0, ele)); + planning_element + .map(Element::Planning) + .map(|ele| children.insert(0, ele)); + + let (remaining, _trailing_ws) = + maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?; + + let source = get_consumed(input, remaining); + Ok(( + remaining, + Section { + source: source.into(), + children, + }, + )) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +fn section_end<'b, 'g, 'r, 's>( + _context: RefContext<'b, 'g, 'r, 's>, + input: OrgSource<'s>, +) -> Res, OrgSource<'s>> { + recognize(detect_headline)(input) +} diff --git a/src/parser/statistics_cookie.rs b/src/parser/statistics_cookie.rs index 22ec3f5a..d83d0ead 100644 --- a/src/parser/statistics_cookie.rs +++ b/src/parser/statistics_cookie.rs @@ -1,9 +1,11 @@ use nom::branch::alt; use nom::bytes::complete::tag; +use nom::combinator::opt; use nom::combinator::recognize; use nom::sequence::tuple; use super::org_source::OrgSource; +use super::util::get_consumed; use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting; use crate::context::parser_with_context; use crate::context::RefContext; @@ -26,10 +28,14 @@ pub fn percent_statistics_cookie<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, StatisticsCookie<'s>> { - let (remaining, source) = - recognize(tuple((tag("["), nom::character::complete::u64, tag("%]"))))(input)?; + let (remaining, _) = recognize(tuple(( + tag("["), + opt(nom::character::complete::u64), + tag("%]"), + )))(input)?; let (remaining, _trailing_whitespace) = maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; + let source = get_consumed(input, remaining); Ok(( remaining, StatisticsCookie { @@ -43,15 +49,16 @@ pub fn fraction_statistics_cookie<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, StatisticsCookie<'s>> { - let (remaining, source) = recognize(tuple(( + let (remaining, _) = recognize(tuple(( tag("["), - nom::character::complete::u64, + opt(nom::character::complete::u64), tag("/"), - nom::character::complete::u64, + opt(nom::character::complete::u64), tag("]"), )))(input)?; let (remaining, _trailing_whitespace) = maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; + let source = get_consumed(input, remaining); Ok(( remaining, StatisticsCookie { diff --git a/src/parser/subscript_and_superscript.rs b/src/parser/subscript_and_superscript.rs index 9ad89069..c026c6a4 100644 --- a/src/parser/subscript_and_superscript.rs +++ b/src/parser/subscript_and_superscript.rs @@ -1,5 +1,6 @@ use nom::branch::alt; use nom::bytes::complete::tag; +use nom::bytes::complete::take_while; use nom::character::complete::anychar; use nom::character::complete::one_of; use nom::combinator::map; @@ -9,12 +10,14 @@ use nom::combinator::peek; use nom::combinator::recognize; use nom::combinator::verify; use nom::multi::many_till; +use nom::sequence::tuple; use super::object_parser::standard_set_object; use super::org_source::BracketDepth; use super::org_source::OrgSource; use super::util::exit_matcher_parser; use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting; +use super::util::preceded_by_whitespace; use crate::context::parser_with_context; use crate::context::ContextElement; use crate::context::ContextMatcher; @@ -29,6 +32,19 @@ use crate::types::Object; use crate::types::Subscript; use crate::types::Superscript; +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn detect_subscript_or_superscript<'s>(input: OrgSource<'s>) -> Res, ()> { + // This does not have to detect all valid subscript/superscript but all that it detects must be valid. + let (remaining, _) = one_of("_^")(input)?; + pre(input)?; + if tag::<_, _, CustomError<_>>("*")(remaining).is_ok() { + return Ok((input, ())); + } + let (remaining, _) = opt(one_of("+-"))(remaining)?; + let (_remaining, _) = verify(anychar, |c| c.is_alphanumeric())(remaining)?; + Ok((input, ())) +} + #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] pub fn subscript<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, @@ -36,7 +52,7 @@ pub fn subscript<'b, 'g, 'r, 's>( ) -> Res, Subscript<'s>> { // We check for the underscore first before checking the pre-character as a minor optimization to avoid walking up the context tree to find the document root unnecessarily. let (remaining, _) = tag("_")(input)?; - pre(context, input)?; + pre(input)?; let (remaining, _body) = script_body(context, remaining)?; let (remaining, _trailing_whitespace) = maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; @@ -56,7 +72,7 @@ pub fn superscript<'b, 'g, 'r, 's>( ) -> Res, Superscript<'s>> { // We check for the circumflex first before checking the pre-character as a minor optimization to avoid walking up the context tree to find the document root unnecessarily. let (remaining, _) = tag("^")(input)?; - pre(context, input)?; + pre(input)?; let (remaining, _body) = script_body(context, remaining)?; let (remaining, _trailing_whitespace) = maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?; @@ -70,19 +86,8 @@ pub fn superscript<'b, 'g, 'r, 's>( } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn pre<'b, 'g, 'r, 's>( - _context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, -) -> Res, ()> { - let preceding_character = input.get_preceding_character(); - match preceding_character { - Some(c) if !c.is_whitespace() => {} - _ => { - return Err(nom::Err::Error(CustomError::MyError(MyError( - "Must be preceded by a non-whitespace character.".into(), - )))); - } - }; +fn pre<'s>(input: OrgSource<'s>) -> Res, ()> { + not(preceded_by_whitespace(true))(input)?; Ok((input, ())) } @@ -120,36 +125,29 @@ fn script_asterisk<'b, 'g, 'r, 's>( #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] fn script_alphanum<'b, 'g, 'r, 's>( - context: RefContext<'b, 'g, 'r, 's>, + _context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, OrgSource<'s>> { let (remaining, _sign) = opt(recognize(one_of("+-")))(input)?; - let (remaining, _script) = many_till( - parser_with_context!(script_alphanum_character)(context), - parser_with_context!(end_script_alphanum_character)(context), - )(remaining)?; + let (remaining, _script) = + many_till(script_alphanum_character, end_script_alphanum_character)(remaining)?; let source = get_consumed(input, remaining); Ok((remaining, source)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn script_alphanum_character<'b, 'g, 'r, 's>( - _context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { +fn script_alphanum_character<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { recognize(verify(anychar, |c| { c.is_alphanumeric() || r#",.\"#.contains(*c) }))(input) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn end_script_alphanum_character<'b, 'g, 'r, 's>( - context: RefContext<'b, 'g, 'r, 's>, - input: OrgSource<'s>, -) -> Res, OrgSource<'s>> { +fn end_script_alphanum_character<'s>(input: OrgSource<'s>) -> Res, OrgSource<'s>> { let (remaining, final_char) = recognize(verify(anychar, |c| c.is_alphanumeric()))(input)?; - peek(not(parser_with_context!(script_alphanum_character)( - context, + peek(tuple(( + take_while(|c| r#",.\"#.contains(c)), + not(script_alphanum_character), )))(remaining)?; Ok((remaining, final_char)) } diff --git a/src/parser/table.rs b/src/parser/table.rs index 2dbe014d..e5fd7ea5 100644 --- a/src/parser/table.rs +++ b/src/parser/table.rs @@ -8,10 +8,12 @@ use nom::combinator::not; use nom::combinator::peek; 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::keyword::table_formula_keyword; use super::object_parser::table_cell_set_object; use super::org_source::OrgSource; use super::util::exit_matcher_parser; @@ -56,6 +58,9 @@ pub fn org_mode_table<'b, 'g, 'r, 's>( let (remaining, (children, _exit_contents)) = many_till(org_mode_table_row_matcher, exit_matcher)(input)?; + let (remaining, formulas) = + many0(parser_with_context!(table_formula_keyword)(context))(remaining)?; + // TODO: Consume trailing formulas let source = get_consumed(input, remaining); @@ -63,6 +68,7 @@ pub fn org_mode_table<'b, 'g, 'r, 's>( remaining, Table { source: source.into(), + formulas, children, }, )) diff --git a/src/parser/text_markup.rs b/src/parser/text_markup.rs index 48f6eea9..e36d1794 100644 --- a/src/parser/text_markup.rs +++ b/src/parser/text_markup.rs @@ -18,6 +18,7 @@ use tracing::span; use super::object_parser::standard_set_object; use super::org_source::OrgSource; use super::radio_link::RematchObject; +use super::util::in_object_section; use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting; use crate::context::parser_with_context; use crate::context::ContextElement; @@ -177,15 +178,26 @@ fn _text_markup_object<'b, 'g, 'r, 's, 'c>( input: OrgSource<'s>, marker_symbol: &'c str, ) -> Res, Vec>> { + if in_object_section(context, marker_symbol) { + return Err(nom::Err::Error(CustomError::MyError(MyError( + "Cannot nest objects of the same type".into(), + )))); + } + let (remaining, _) = pre(context, input)?; let (remaining, open) = tag(marker_symbol)(remaining)?; - let (remaining, _peek_not_whitespace) = peek(not(multispace1))(remaining)?; + let (remaining, _peek_not_whitespace) = + peek(verify(anychar, |c| !c.is_whitespace() && *c != '\u{200B}'))(remaining)?; let text_markup_end_specialized = text_markup_end(open.into()); - let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { - class: ExitClass::Gamma, - exit_matcher: &text_markup_end_specialized, - }); - let parser_context = context.with_additional_node(&parser_context); + let contexts = [ + ContextElement::ContextObject(marker_symbol), + ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Gamma, + exit_matcher: &text_markup_end_specialized, + }), + ]; + let parser_context = context.with_additional_node(&contexts[0]); + let parser_context = parser_context.with_additional_node(&contexts[1]); let (remaining, (children, _exit_contents)) = verify( many_till( @@ -229,16 +241,25 @@ fn _text_markup_string<'b, 'g, 'r, 's, 'c>( input: OrgSource<'s>, marker_symbol: &'c str, ) -> Res, OrgSource<'s>> { + if in_object_section(context, marker_symbol) { + return Err(nom::Err::Error(CustomError::MyError(MyError( + "Cannot nest objects of the same type".into(), + )))); + } let (remaining, _) = pre(context, input)?; let (remaining, open) = tag(marker_symbol)(remaining)?; let (remaining, _peek_not_whitespace) = peek(verify(anychar, |c| !c.is_whitespace() && *c != '\u{200B}'))(remaining)?; let text_markup_end_specialized = text_markup_end(open.into()); - let parser_context = ContextElement::ExitMatcherNode(ExitMatcherNode { - class: ExitClass::Gamma, - exit_matcher: &text_markup_end_specialized, - }); - let parser_context = context.with_additional_node(&parser_context); + let contexts = [ + ContextElement::ContextObject(marker_symbol), + ContextElement::ExitMatcherNode(ExitMatcherNode { + class: ExitClass::Gamma, + exit_matcher: &text_markup_end_specialized, + }), + ]; + let parser_context = context.with_additional_node(&contexts[0]); + let parser_context = parser_context.with_additional_node(&contexts[1]); let (remaining, contents) = recognize(verify( many_till( @@ -277,7 +298,6 @@ pub fn pre<'b, 'g, 'r, 's>( None | Some('\r') | Some('\n') | Some(' ') | Some('\t') | Some('-') | Some('(') | Some('{') | Some('\'') | Some('"') | Some('<') => {} Some(_) => { - // Not at start of line, cannot be a heading return Err(nom::Err::Error(CustomError::MyError(MyError( "Not a valid pre character for text markup.".into(), )))); @@ -305,7 +325,7 @@ fn _text_markup_end<'b, 'g, 'r, 's, 'c>( input: OrgSource<'s>, marker_symbol: &'c str, ) -> Res, OrgSource<'s>> { - not(preceded_by_whitespace)(input)?; + not(preceded_by_whitespace(false))(input)?; let (remaining, _marker) = terminated( tag(marker_symbol), peek(parser_with_context!(post)(context)), diff --git a/src/parser/util.rs b/src/parser/util.rs index 32576ad2..53deb7c2 100644 --- a/src/parser/util.rs +++ b/src/parser/util.rs @@ -2,6 +2,7 @@ use nom::branch::alt; use nom::character::complete::anychar; use nom::character::complete::line_ending; use nom::character::complete::none_of; +use nom::character::complete::one_of; use nom::character::complete::space0; use nom::combinator::eof; use nom::combinator::not; @@ -24,7 +25,6 @@ pub const WORD_CONSTITUENT_CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; /// Check if we are below a section of the given section type regardless of depth -#[allow(dead_code)] pub fn in_section<'b, 'g, 'r, 's, 'x>( context: RefContext<'b, 'g, 'r, 's>, section_name: &'x str, @@ -53,6 +53,20 @@ pub fn immediate_in_section<'b, 'g, 'r, 's, 'x>( false } +/// Check if we are below a section of the given section type regardless of depth +pub fn in_object_section<'b, 'g, 'r, 's, 'x>( + context: RefContext<'b, 'g, 'r, 's>, + section_name: &'x str, +) -> bool { + for thing in context.iter() { + match thing { + ContextElement::ContextObject(name) if *name == section_name => return true, + _ => {} + } + } + false +} + /// Get a slice of the string that was consumed in a parser using the original input to the parser and the remaining input after the parser. pub fn get_consumed<'s>(input: OrgSource<'s>, remaining: OrgSource<'s>) -> OrgSource<'s> { input.get_until(remaining) @@ -78,11 +92,15 @@ pub fn maybe_consume_object_trailing_whitespace_if_not_exiting<'b, 'g, 'r, 's>( context: RefContext<'b, 'g, 'r, 's>, input: OrgSource<'s>, ) -> Res, Option>> { - if exit_matcher_parser(context, input).is_err() { - opt(space0)(input) - } else { - Ok((input, None)) - } + // We have to check exit matcher after each character because description list tags need to end with a space unconsumed (" ::"). + let (remaining, _) = many_till( + one_of(" \t"), + alt(( + peek(recognize(none_of(" \t"))), + parser_with_context!(exit_matcher_parser)(context), + )), + )(input)?; + Ok((remaining, None)) } #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] @@ -122,16 +140,25 @@ pub fn start_of_line<'s>(input: OrgSource<'s>) -> Res, ()> { } } +pub fn preceded_by_whitespace( + allow_start_of_file: bool, +) -> impl for<'s> Fn(OrgSource<'s>) -> Res, ()> { + move |input| _preceded_by_whitespace(allow_start_of_file, input) +} + /// Check that we are at the start of a line #[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -pub fn preceded_by_whitespace<'s>(input: OrgSource<'s>) -> Res, ()> { +fn _preceded_by_whitespace<'s>( + allow_start_of_file: bool, + input: OrgSource<'s>, +) -> Res, ()> { let preceding_character = input.get_preceding_character(); if !preceding_character .map(|c| c.is_whitespace() || c == '\u{200B}') // 200B = Zero-width space - .unwrap_or(false) + .unwrap_or(allow_start_of_file) { return Err(nom::Err::Error(CustomError::MyError(MyError( - "Not preceded by whitespace.".into(), + "Must be preceded by a whitespace character.".into(), )))); } Ok((input, ())) @@ -184,3 +211,15 @@ pub fn text_until_eol<'r, 's>( .map(|(_remaining, line)| Into::<&str>::into(line))?; Ok(line.trim()) } + +pub fn include_input<'s, F, O>( + mut inner: F, +) -> impl FnMut(OrgSource<'s>) -> Res, (OrgSource<'s>, O)> +where + F: FnMut(OrgSource<'s>) -> Res, O>, +{ + move |input: OrgSource<'_>| { + let (remaining, output) = inner(input)?; + Ok((remaining, (input, output))) + } +} diff --git a/src/types/document.rs b/src/types/document.rs index 654377a9..142762da 100644 --- a/src/types/document.rs +++ b/src/types/document.rs @@ -2,6 +2,8 @@ use super::Element; use super::Object; use super::Source; +pub type PriorityCookie = u8; + #[derive(Debug)] pub struct Document<'s> { pub source: &'s str, @@ -14,10 +16,12 @@ pub struct Heading<'s> { pub source: &'s str, pub stars: usize, pub todo_keyword: Option<(TodoKeywordType, &'s str)>, - // TODO: add todo-type enum + pub priority_cookie: Option, pub title: Vec>, pub tags: Vec<&'s str>, pub children: Vec>, + pub is_comment: bool, + pub is_archived: bool, } #[derive(Debug)] diff --git a/src/types/greater_element.rs b/src/types/greater_element.rs index 9a129006..e897945f 100644 --- a/src/types/greater_element.rs +++ b/src/types/greater_element.rs @@ -1,5 +1,6 @@ use super::element::Element; use super::lesser_element::TableCell; +use super::Keyword; use super::Object; use super::Source; @@ -63,6 +64,7 @@ pub struct NodeProperty<'s> { #[derive(Debug)] pub struct Table<'s> { pub source: &'s str, + pub formulas: Vec>, pub children: Vec>, } diff --git a/src/types/mod.rs b/src/types/mod.rs index efd1b047..9cf5b596 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -7,6 +7,7 @@ mod source; pub use document::Document; pub use document::DocumentElement; pub use document::Heading; +pub use document::PriorityCookie; pub use document::Section; pub use document::TodoKeywordType; pub use element::Element;