Compare commits
90 Commits
v0.1.2
...
feature_ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
678106bb65 | ||
|
|
19432d91ab | ||
|
|
16a107eebb | ||
|
|
77348b560c | ||
|
|
fc79507ef3 | ||
|
|
9c1e6ccc97 | ||
|
|
0dbc8f0925 | ||
|
|
02fe10fba3 | ||
|
|
33d7ae03d1 | ||
|
|
03faa7257f | ||
|
|
ae3510abd5 | ||
|
|
ad3f47864a | ||
|
|
533ef2a9a8 | ||
|
|
cf37bc4111 | ||
|
|
e5224cda63 | ||
|
|
64e3481660 | ||
|
|
32071ce74d | ||
|
|
e84e2b5147 | ||
|
|
3348807a05 | ||
|
|
720afa5d32 | ||
|
|
dab598e5e7 | ||
|
|
b7a5dd48ea | ||
|
|
c475dce6da | ||
|
|
6d1675fa00 | ||
|
|
cda49c628c | ||
|
|
65b87bd65d | ||
|
|
5a7f34b63e | ||
|
|
edff1e089d | ||
|
|
bc29f1dfc0 | ||
|
|
e4656cddf6 | ||
|
|
1e3dadd458 | ||
|
|
2ec055af5a | ||
|
|
6823db5c60 | ||
|
|
21e1ceb8e0 | ||
|
|
655af88cdf | ||
|
|
8561fdc1bd | ||
|
|
f2089257b0 | ||
|
|
09821c8898 | ||
|
|
69ecfd2646 | ||
|
|
8162f03051 | ||
|
|
d8c3285e3c | ||
|
|
5db6cd617e | ||
|
|
4cd3697fb0 | ||
|
|
2cd6f736c2 | ||
|
|
5686256039 | ||
|
|
7cf1b2d2b8 | ||
|
|
b848d7be73 | ||
|
|
74f4aa8d33 | ||
|
|
4776898894 | ||
|
|
8e95ce6368 | ||
|
|
6c9c304f37 | ||
|
|
7fafbfb6bb | ||
|
|
56281633f3 | ||
|
|
823c33ef8e | ||
|
|
e5e5120a10 | ||
|
|
7df393f31d | ||
|
|
72d5f8f35c | ||
|
|
dae46adc12 | ||
|
|
d0dc737c79 | ||
|
|
1c9877015d | ||
|
|
2938d5809a | ||
|
|
f7ec89858d | ||
|
|
67b4dfdce6 | ||
|
|
63d092c83d | ||
|
|
a7b298eeec | ||
|
|
1bbfbc3164 | ||
|
|
2bcc3f0599 | ||
|
|
b93a12c32c | ||
|
|
df3045e424 | ||
|
|
72b8fec1be | ||
|
|
ab17904b1c | ||
|
|
306878c95d | ||
|
|
5768c8acda | ||
|
|
e28290ed79 | ||
|
|
fbabf60559 | ||
|
|
92abac37e2 | ||
|
|
899073e54f | ||
|
|
eb379af78d | ||
|
|
422804d846 | ||
|
|
cc83431d62 | ||
|
|
00354ccc20 | ||
|
|
b75eed6b1e | ||
|
|
e33ec4a02c | ||
|
|
f7afcec824 | ||
|
|
cf0991fdff | ||
|
|
d1e0ee831c | ||
|
|
34985c9045 | ||
|
|
7da09fea74 | ||
|
|
fc28e3b514 | ||
|
|
df5ee5af16 |
@@ -1,3 +1,4 @@
|
||||
**/.git
|
||||
target
|
||||
Cargo.lock
|
||||
notes/
|
||||
|
||||
@@ -14,10 +14,6 @@ spec:
|
||||
- name: path-to-dockerfile
|
||||
description: The path to the Dockerfile
|
||||
type: string
|
||||
- name: command
|
||||
type: array
|
||||
description: Command to run.
|
||||
default: []
|
||||
tasks:
|
||||
- name: report-pending
|
||||
taskRef:
|
||||
@@ -81,9 +77,19 @@ spec:
|
||||
workspace: docker-credentials
|
||||
runAfter:
|
||||
- fetch-repository
|
||||
- name: run-image-none
|
||||
- name: build-organic
|
||||
taskRef:
|
||||
name: run-docker-image
|
||||
matrix:
|
||||
params:
|
||||
- name: feature-compare
|
||||
value:
|
||||
- "true"
|
||||
- "false"
|
||||
- name: feature-tracing
|
||||
value:
|
||||
- "true"
|
||||
- "false"
|
||||
workspaces:
|
||||
- name: source
|
||||
workspace: git-source
|
||||
@@ -93,77 +99,21 @@ spec:
|
||||
- build-image
|
||||
params:
|
||||
- name: command
|
||||
value: ["$(params.command[*])"]
|
||||
value: ["/bin/sh", "-c"]
|
||||
- name: args
|
||||
value: ["--no-default-features"]
|
||||
- name: docker-image
|
||||
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
|
||||
- name: run-image-tracing
|
||||
taskRef:
|
||||
name: run-docker-image
|
||||
workspaces:
|
||||
- name: source
|
||||
workspace: git-source
|
||||
- name: cargo-cache
|
||||
workspace: cargo-cache
|
||||
runAfter:
|
||||
- run-image-none
|
||||
params:
|
||||
- name: command
|
||||
value: ["$(params.command[*])"]
|
||||
- name: args
|
||||
value: ["--no-default-features", "--features", "tracing"]
|
||||
- name: docker-image
|
||||
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
|
||||
- name: run-image-compare
|
||||
taskRef:
|
||||
name: run-docker-image
|
||||
workspaces:
|
||||
- name: source
|
||||
workspace: git-source
|
||||
- name: cargo-cache
|
||||
workspace: cargo-cache
|
||||
runAfter:
|
||||
- run-image-tracing
|
||||
params:
|
||||
- name: command
|
||||
value: ["$(params.command[*])"]
|
||||
- name: args
|
||||
value: ["--no-default-features", "--features", "compare"]
|
||||
- name: docker-image
|
||||
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
|
||||
- name: run-image-default
|
||||
taskRef:
|
||||
name: run-docker-image
|
||||
workspaces:
|
||||
- name: source
|
||||
workspace: git-source
|
||||
- name: cargo-cache
|
||||
workspace: cargo-cache
|
||||
runAfter:
|
||||
- run-image-compare
|
||||
params:
|
||||
- name: command
|
||||
value: ["$(params.command[*])"]
|
||||
- name: args
|
||||
value: []
|
||||
- name: docker-image
|
||||
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
|
||||
- name: run-image-all
|
||||
taskRef:
|
||||
name: run-docker-image
|
||||
workspaces:
|
||||
- name: source
|
||||
workspace: git-source
|
||||
- name: cargo-cache
|
||||
workspace: cargo-cache
|
||||
runAfter:
|
||||
- run-image-default
|
||||
params:
|
||||
- name: command
|
||||
value: ["$(params.command[*])"]
|
||||
- name: args
|
||||
value: ["--no-default-features", "--features", "tracing,compare"]
|
||||
value:
|
||||
- |
|
||||
set -euo pipefail
|
||||
IFS=$$'\n\t'
|
||||
features=()
|
||||
if [ $(params.feature-compare) = "true" ]; then features+=(compare); fi
|
||||
if [ $(params.feature-tracing) = "true" ]; then features+=(tracing); fi
|
||||
if [ $${#features[@]} -eq 0 ]; then
|
||||
exec cargo build --no-default-features
|
||||
else
|
||||
featurelist=$$(IFS="," ; echo "$${features[*]}")
|
||||
exec cargo build --no-default-features --features "$$featurelist"
|
||||
fi
|
||||
- name: docker-image
|
||||
value: "$(params.image-name):$(tasks.fetch-repository.results.commit)"
|
||||
finally:
|
||||
@@ -256,5 +206,3 @@ spec:
|
||||
value: docker/organic_build/
|
||||
- name: path-to-dockerfile
|
||||
value: docker/organic_build/Dockerfile
|
||||
- name: command
|
||||
value: [cargo, build]
|
||||
|
||||
@@ -4,6 +4,10 @@ metadata:
|
||||
name: rust-test
|
||||
spec:
|
||||
pipelineSpec:
|
||||
timeouts:
|
||||
pipeline: "2h0m0s"
|
||||
tasks: "1h0m40s"
|
||||
finally: "0h30m0s"
|
||||
params:
|
||||
- name: image-name
|
||||
description: The name for the built image
|
||||
@@ -201,7 +205,6 @@ spec:
|
||||
secret:
|
||||
secretName: harbor-plain
|
||||
serviceAccountName: build-bot
|
||||
timeout: 240h0m0s
|
||||
params:
|
||||
- name: image-name
|
||||
value: "harbor.fizz.buzz/private/organic-test"
|
||||
|
||||
16
Cargo.toml
16
Cargo.toml
@@ -10,6 +10,12 @@ readme = "README.md"
|
||||
keywords = ["emacs", "org-mode"]
|
||||
categories = ["parsing"]
|
||||
resolver = "2"
|
||||
include = [
|
||||
"LICENSE",
|
||||
"**/*.rs",
|
||||
"Cargo.toml",
|
||||
"tests/*"
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "organic"
|
||||
@@ -34,10 +40,16 @@ tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-fil
|
||||
walkdir = "2.3.3"
|
||||
|
||||
[features]
|
||||
default = ["compare", "tracing"]
|
||||
default = ["compare"]
|
||||
compare = []
|
||||
tracing = ["dep:opentelemetry", "dep:opentelemetry-otlp", "dep:opentelemetry-semantic-conventions", "dep:tokio", "dep:tracing", "dep:tracing-opentelemetry", "dep:tracing-subscriber"]
|
||||
|
||||
[profile.release]
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
strip = "symbols"
|
||||
|
||||
[profile.perf]
|
||||
inherits = "release"
|
||||
lto = true
|
||||
debug = true
|
||||
|
||||
16
Makefile
16
Makefile
@@ -35,7 +35,17 @@ clean:
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
> cargo test --lib --test test_loader -- --test-threads $(TESTJOBS)
|
||||
> cargo test --no-fail-fast --lib --test test_loader -- --test-threads $(TESTJOBS)
|
||||
|
||||
.PHONY: dockertest
|
||||
dockertest:
|
||||
> $(MAKE) -C docker/organic_test
|
||||
> docker run --init --rm -i -t -v "$$(readlink -f ./):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source organic-test cargo test --no-fail-fast --lib --test test_loader -- --test-threads $(TESTJOBS)
|
||||
|
||||
.PHONY: dockerclean
|
||||
dockerclean:
|
||||
# Delete volumes created for running the tests in docker. This does not touch anything related to the jaeger docker container.
|
||||
> docker volume rm cargo-cache rust-cache
|
||||
|
||||
.PHONY: integrationtest
|
||||
integrationtest:
|
||||
@@ -49,8 +59,8 @@ unittest:
|
||||
jaeger:
|
||||
# 4317 for OTLP gRPC, 4318 for OTLP HTTP. We currently use gRPC but I forward both ports regardless.
|
||||
#
|
||||
# These flags didn't help even though they seem like they would: --collector.otlp.grpc.max-message-size=10000000 --collector.queue-size=20000 --collector.num-workers=100
|
||||
> docker run -d --rm --name organicdocker -p 4317:4317 -p 4318:4318 -p 16686:16686 -e COLLECTOR_OTLP_ENABLED=true jaegertracing/all-in-one:1.47 --collector.grpc-server.max-message-size=10000000
|
||||
# These flags didn't help even though they seem like they would: --collector.queue-size=20000 --collector.num-workers=100
|
||||
> docker run -d --rm --name organicdocker -p 4317:4317 -p 4318:4318 -p 16686:16686 -e COLLECTOR_OTLP_ENABLED=true jaegertracing/all-in-one:1.47 --collector.grpc-server.max-message-size=20000000 --collector.otlp.grpc.max-message-size=20000000
|
||||
|
||||
.PHONY: jaegerweb
|
||||
jaegerweb:
|
||||
|
||||
8
build.rs
8
build.rs
@@ -41,6 +41,7 @@ fn write_test(test_file: &mut File, test: &walkdir::DirEntry) {
|
||||
.strip_suffix(".org")
|
||||
.expect("Should have .org extension")
|
||||
.replace("/", "_");
|
||||
let test_name = format!("autogen_{}", test_name);
|
||||
|
||||
if let Some(_reason) = is_expect_fail(test_name.as_str()) {
|
||||
write!(test_file, "#[ignore]\n").unwrap();
|
||||
@@ -71,10 +72,9 @@ use organic::parser::sexp::sexp_with_padding;
|
||||
|
||||
fn is_expect_fail(name: &str) -> Option<&str> {
|
||||
match name {
|
||||
"drawer_drawer_with_headline_inside" => Some("Apparently lines with :end: become their own paragraph. This odd behavior needs to be investigated more."),
|
||||
"element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."),
|
||||
"paragraphs_paragraph_with_backslash_line_breaks" => Some("The text we're getting out of the parse tree is already processed to remove line breaks, so our comparison needs to take that into account."),
|
||||
"export_snippet_paragraph_break_precedent" => Some("Emacs 28 has broken behavior so the tests in the CI fail."),
|
||||
"autogen_greater_element_drawer_drawer_with_headline_inside" => Some("Apparently lines with :end: become their own paragraph. This odd behavior needs to be investigated more."),
|
||||
"autogen_element_container_priority_footnote_definition_dynamic_block" => Some("Apparently broken begin lines become their own paragraph."),
|
||||
"autogen_lesser_element_paragraphs_paragraph_with_backslash_line_breaks" => Some("The text we're getting out of the parse tree is already processed to remove line breaks, so our comparison needs to take that into account."),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ all: build push
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
docker build -t $(IMAGE_NAME) -f Dockerfile ../
|
||||
docker build -t $(IMAGE_NAME) -f Dockerfile ../../
|
||||
|
||||
.PHONY: push
|
||||
push:
|
||||
|
||||
@@ -6,7 +6,7 @@ all: build push
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
docker build -t $(IMAGE_NAME) -f Dockerfile ../
|
||||
docker build -t $(IMAGE_NAME) -f Dockerfile ../../
|
||||
|
||||
.PHONY: push
|
||||
push:
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
FROM rustlang/rust:nightly-alpine3.17
|
||||
FROM alpine:3.17 AS build
|
||||
RUN apk add --no-cache build-base musl-dev git autoconf make texinfo gnutls-dev ncurses-dev gawk
|
||||
|
||||
RUN apk add --no-cache musl-dev emacs
|
||||
|
||||
FROM build AS build-emacs
|
||||
ARG EMACS_VERSION=emacs-29.1
|
||||
RUN git clone --depth 1 --branch $EMACS_VERSION https://git.savannah.gnu.org/git/emacs.git /root/emacs
|
||||
WORKDIR /root/emacs
|
||||
RUN mkdir /root/dist
|
||||
RUN ./autogen.sh
|
||||
RUN ./configure --prefix /usr --without-x --without-sound
|
||||
RUN make
|
||||
RUN make DESTDIR="/root/dist" install
|
||||
|
||||
|
||||
FROM build AS build-org-mode
|
||||
ARG ORG_VERSION=7bdec435ff5d86220d13c431e799c5ed44a57da1
|
||||
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.
|
||||
RUN git clone https://git.savannah.gnu.org/git/emacs/org-mode.git /root/org-mode && git -C /root/org-mode checkout $ORG_VERSION
|
||||
# RUN mkdir /root/org-mode && git -C /root/org-mode init --initial-branch=main && git -C /root/org-mode remote add origin https://git.savannah.gnu.org/git/emacs/org-mode.git && git -C /root/org-mode fetch origin $ORG_VERSION && git -C /root/org-mode checkout FETCH_HEAD
|
||||
WORKDIR /root/org-mode
|
||||
RUN make compile
|
||||
RUN make DESTDIR="/root/dist" install
|
||||
|
||||
|
||||
FROM rustlang/rust:nightly-alpine3.17
|
||||
RUN apk add --no-cache musl-dev ncurses gnutls
|
||||
RUN cargo install --locked --no-default-features --features ci-autoclean cargo-cache
|
||||
COPY --from=build-emacs /root/dist/ /
|
||||
COPY --from=build-org-mode /root/dist/ /
|
||||
|
||||
@@ -6,7 +6,7 @@ all: build push
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
docker build -t $(IMAGE_NAME) -f Dockerfile ../
|
||||
docker build -t $(IMAGE_NAME) -f Dockerfile ../../
|
||||
|
||||
.PHONY: push
|
||||
push:
|
||||
|
||||
27
notes/optimization_ideas.org
Normal file
27
notes/optimization_ideas.org
Normal file
@@ -0,0 +1,27 @@
|
||||
* Analysis
|
||||
** Parse start per character
|
||||
It might help analysis to record how often we start a specific type of parse for each character. For example, at the start of a plain list, if we had a count of how often each character was the start of a parse of a list we could use that to see how often that list is getting re-parsed.
|
||||
* Optimizations
|
||||
** Edit whitespace for list items
|
||||
Whether or not a list item owns the trailing whitespace depends on if it is the last list item in that list. Since we do not know ahead of time if an item is the last item in the list, we have to either re-parse the list item or modify it after parsing.
|
||||
|
||||
*** For
|
||||
We already are modifying the source of some elements after-the-fact with src_rust{set_source()} so this would be more of the same.
|
||||
*** Against
|
||||
I'd like to phase out such modifications because they seem hacky and fragile.
|
||||
** Make detect element function
|
||||
Some exit matchers are based on when the next element is found. Some elements do not need to be fully parsed to identify that they are a valid element. For example, src_org{1. foo} can already be identified as the start of a plain list (in the right context) without needing to parse the entire element.
|
||||
*** For
|
||||
Avoiding parsing the entire element for an exit matcher would reduce redundant parses.
|
||||
*** Against
|
||||
This adds code complexity and introduces the potential for bugs.
|
||||
|
||||
How many elements can be reasonably early-detected? For example, src_org{#+begin_src foo} is not enough to detect the start of a source block because without the src_org{#+end_src} it is just plain text.
|
||||
** Grab multiple characters in plaintext parser before checking exit matcher
|
||||
Currently we check the exit matcher after each character inside the plain text parser (and many others). Are there character sequences we can assume no exit matcher will trigger between? For example, a contiguous string of latin-alphabet letters?
|
||||
*** For
|
||||
This could significantly reduce our calls to exit matchers.
|
||||
*** Against
|
||||
I think targets would break this.
|
||||
|
||||
The exit matchers are already implicitly building this behavior since they should all exit very early when the starting character is wrong. Putting this logic in a centralized place, far away from where those characters are actually going to be used, is unfortunate for readability.
|
||||
@@ -0,0 +1,9 @@
|
||||
1. foo
|
||||
|
||||
1. bar
|
||||
|
||||
2. baz
|
||||
|
||||
2. lorem
|
||||
|
||||
ipsum
|
||||
@@ -0,0 +1,11 @@
|
||||
1. foo
|
||||
|
||||
1. bar
|
||||
|
||||
2. baz
|
||||
|
||||
cat
|
||||
|
||||
2. lorem
|
||||
|
||||
ipsum
|
||||
@@ -0,0 +1,10 @@
|
||||
1. cat
|
||||
1. foo
|
||||
|
||||
1. bar
|
||||
|
||||
2. baz
|
||||
|
||||
2. lorem
|
||||
|
||||
ipsum
|
||||
@@ -0,0 +1,12 @@
|
||||
1. cat
|
||||
1. foo
|
||||
|
||||
1. bar
|
||||
|
||||
2. baz
|
||||
|
||||
2. lorem
|
||||
|
||||
2. dog
|
||||
|
||||
ipsum
|
||||
@@ -0,0 +1,18 @@
|
||||
foo bar.
|
||||
|
||||
|
||||
|
||||
|
||||
* Lorem
|
||||
baz
|
||||
|
||||
|
||||
|
||||
|
||||
* Ipsum
|
||||
alpha
|
||||
|
||||
|
||||
|
||||
|
||||
beta
|
||||
13
scripts/callgrind.bash
Executable file
13
scripts/callgrind.bash
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
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/compare
|
||||
|
||||
echo "You probably want to run:"
|
||||
echo "callgrind_annotate --auto=yes callgrind.out"
|
||||
18
scripts/perf.bash
Executable file
18
scripts/perf.bash
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
: ${PROFILE:="perf"}
|
||||
|
||||
cd "$DIR/../"
|
||||
|
||||
cargo build --profile "$PROFILE" --no-default-features
|
||||
perf record --freq=2000 --call-graph dwarf --output=perf.data target/${PROFILE}/compare
|
||||
# Convert to a format firefox will read
|
||||
# flags to consider --show-info
|
||||
perf script -F +pid --input perf.data > perf.firefox
|
||||
|
||||
echo "You probably want to go to https://profiler.firefox.com/"
|
||||
echo "Either that or run hotspot"
|
||||
46
scripts/run_docker_compare.bash
Executable file
46
scripts/run_docker_compare.bash
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
: ${SHELL:="NO"} # or YES to launch a shell instead of running the test
|
||||
: ${TRACE:="NO"} # or YES to send traces to jaeger
|
||||
: ${BACKTRACE:="NO"} # or YES to print a rust backtrace when panicking
|
||||
|
||||
cd "$DIR/../"
|
||||
REALPATH=$(command -v uu-realpath || command -v realpath)
|
||||
MAKE=$(command -v gmake || command -v make)
|
||||
|
||||
function main {
|
||||
build_container
|
||||
launch_container
|
||||
}
|
||||
|
||||
function build_container {
|
||||
$MAKE -C "$DIR/../docker/organic_test"
|
||||
}
|
||||
|
||||
function launch_container {
|
||||
local additional_flags=()
|
||||
local additional_args=()
|
||||
|
||||
if [ "$SHELL" != "YES" ]; then
|
||||
additional_args+=(cargo run)
|
||||
else
|
||||
additional_flags+=(-t)
|
||||
fi
|
||||
|
||||
if [ "$TRACE" = "YES" ]; then
|
||||
# We use the host network so it can talk to jaeger hosted at 127.0.0.1
|
||||
additional_flags+=(--network=host --env RUST_LOG=debug)
|
||||
fi
|
||||
|
||||
if [ "$BACKTRACE" = "YES" ]; then
|
||||
additional_flags+=(--env RUST_BACKTRACE=full)
|
||||
fi
|
||||
|
||||
docker run "${additional_flags[@]}" --init --rm -i -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 organic-test "${additional_args[@]}"
|
||||
}
|
||||
|
||||
main "${@}"
|
||||
57
scripts/run_docker_integration_test.bash
Executable file
57
scripts/run_docker_integration_test.bash
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
cd "$DIR/../"
|
||||
REALPATH=$(command -v uu-realpath || command -v realpath)
|
||||
MAKE=$(command -v gmake || command -v make)
|
||||
|
||||
function main {
|
||||
local test_names=$(get_test_names "${@}")
|
||||
build_container
|
||||
|
||||
local test
|
||||
while read test; do
|
||||
launch_container "$test"
|
||||
done<<<"$test_names"
|
||||
}
|
||||
|
||||
function build_container {
|
||||
$MAKE -C "$DIR/../docker/organic_test"
|
||||
}
|
||||
|
||||
function get_test_names {
|
||||
local test_file
|
||||
local samples_dir=$($REALPATH "$DIR/../org_mode_samples")
|
||||
for test_file in "$@"
|
||||
do
|
||||
if [ -e "$test_file" ]; then
|
||||
local test_file_full_path=$($REALPATH "$test_file")
|
||||
local relative_to_samples=$($REALPATH --relative-to "$samples_dir" "$test_file_full_path")
|
||||
local without_extension="${relative_to_samples%.org}"
|
||||
echo "${without_extension/\//_}" | tr '[:upper:]' '[:lower:]'
|
||||
else
|
||||
echo "$test_file" | tr '[:upper:]' '[:lower:]'
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function launch_container {
|
||||
local test="$1"
|
||||
local additional_args=()
|
||||
|
||||
local init_script=$(cat <<EOF
|
||||
set -euo pipefail
|
||||
IFS=\$'\n\t'
|
||||
|
||||
cargo test --no-fail-fast --lib --test test_loader "$test" -- --show-output
|
||||
EOF
|
||||
)
|
||||
|
||||
docker run --init --rm -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 organic-test sh -c "$init_script"
|
||||
}
|
||||
|
||||
|
||||
main "${@}"
|
||||
@@ -4,17 +4,27 @@ set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
cd "$DIR/../"
|
||||
REALPATH=$(command -v uu-realpath || command -v realpath)
|
||||
|
||||
samples_dir=$(readlink -f "$DIR/../org_mode_samples")
|
||||
function main {
|
||||
local test_names=$(get_test_names "${@}")
|
||||
|
||||
local test
|
||||
while read test; do
|
||||
cargo test --no-fail-fast --test test_loader "$test" -- --show-output
|
||||
done<<<"$test_names"
|
||||
}
|
||||
|
||||
function get_test_names {
|
||||
local test_file
|
||||
local samples_dir=$($REALPATH "$DIR/../org_mode_samples")
|
||||
for test_file in "$@"
|
||||
do
|
||||
if [ -e "$test_file" ]; then
|
||||
test_file_full_path=$(readlink -f "$test_file")
|
||||
relative_to_samples=$($REALPATH --relative-to "$samples_dir" "$test_file_full_path")
|
||||
without_extension="${relative_to_samples%.org}"
|
||||
local test_file_full_path=$($REALPATH "$test_file")
|
||||
local relative_to_samples=$($REALPATH --relative-to "$samples_dir" "$test_file_full_path")
|
||||
local without_extension="${relative_to_samples%.org}"
|
||||
echo "${without_extension/\//_}" | tr '[:upper:]' '[:lower:]'
|
||||
else
|
||||
echo "$test_file" | tr '[:upper:]' '[:lower:]'
|
||||
@@ -22,6 +32,4 @@ function get_test_names {
|
||||
done
|
||||
}
|
||||
|
||||
get_test_names "$@" | while read test; do
|
||||
(cd "$DIR/../" && cargo test --no-fail-fast --test test_loader "$test" -- --show-output)
|
||||
done
|
||||
main "${@}"
|
||||
|
||||
@@ -3,3 +3,5 @@ mod parse;
|
||||
mod util;
|
||||
pub use diff::compare_document;
|
||||
pub use parse::emacs_parse_org_document;
|
||||
pub use parse::get_emacs_version;
|
||||
pub use parse::get_org_mode_version;
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn emacs_parse_org_document<'a, C>(file_path: C) -> Result<String, Box<dyn std::error::Error>>
|
||||
pub fn emacs_parse_org_document<C>(file_contents: C) -> Result<String, Box<dyn std::error::Error>>
|
||||
where
|
||||
C: AsRef<Path>,
|
||||
C: AsRef<str>,
|
||||
{
|
||||
let elisp_script = r#"(progn
|
||||
let escaped_file_contents = escape_elisp_string(file_contents);
|
||||
let elisp_script = format!(
|
||||
r#"(progn
|
||||
(erase-buffer)
|
||||
(insert "{escaped_file_contents}")
|
||||
(org-mode)
|
||||
(message "%s" (pp-to-string (org-element-parse-buffer)))
|
||||
)"#;
|
||||
)"#,
|
||||
escaped_file_contents = escaped_file_contents
|
||||
);
|
||||
let mut cmd = Command::new("emacs");
|
||||
let proc = cmd
|
||||
.arg("-q")
|
||||
.arg("--no-site-file")
|
||||
.arg("--no-splash")
|
||||
.arg("--batch")
|
||||
.arg("--insert")
|
||||
.arg(file_path.as_ref().as_os_str())
|
||||
.arg("--eval")
|
||||
.arg(elisp_script);
|
||||
let out = proc.output()?;
|
||||
@@ -24,3 +27,62 @@ where
|
||||
let org_sexp = out.stderr;
|
||||
Ok(String::from_utf8(org_sexp)?)
|
||||
}
|
||||
|
||||
fn escape_elisp_string<C>(file_contents: C) -> String
|
||||
where
|
||||
C: AsRef<str>,
|
||||
{
|
||||
let source = file_contents.as_ref();
|
||||
let source_len = source.len();
|
||||
// We allocate a string 10% larger than the source to account for escape characters. Without this, we would have more allocations during processing.
|
||||
let mut output = String::with_capacity(source_len + (source_len / 10));
|
||||
for c in source.chars() {
|
||||
match c {
|
||||
'"' | '\\' => {
|
||||
output.push('\\');
|
||||
output.push(c);
|
||||
}
|
||||
_ => {
|
||||
output.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub fn get_emacs_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let elisp_script = r#"(progn
|
||||
(message "%s" (version))
|
||||
)"#;
|
||||
let mut cmd = Command::new("emacs");
|
||||
let proc = cmd
|
||||
.arg("-q")
|
||||
.arg("--no-site-file")
|
||||
.arg("--no-splash")
|
||||
.arg("--batch")
|
||||
.arg("--eval")
|
||||
.arg(elisp_script);
|
||||
|
||||
let out = proc.output()?;
|
||||
out.status.exit_ok()?;
|
||||
Ok(String::from_utf8(out.stderr)?)
|
||||
}
|
||||
|
||||
pub fn get_org_mode_version() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let elisp_script = r#"(progn
|
||||
(org-mode)
|
||||
(message "%s" (org-version nil t nil))
|
||||
)"#;
|
||||
let mut cmd = Command::new("emacs");
|
||||
let proc = cmd
|
||||
.arg("-q")
|
||||
.arg("--no-site-file")
|
||||
.arg("--no-splash")
|
||||
.arg("--batch")
|
||||
.arg("--eval")
|
||||
.arg(elisp_script);
|
||||
|
||||
let out = proc.output()?;
|
||||
out.status.exit_ok()?;
|
||||
Ok(String::from_utf8(out.stderr)?)
|
||||
}
|
||||
|
||||
@@ -42,24 +42,100 @@ pub fn assert_bounds<'s, S: Source<'s>>(
|
||||
emacs: &'s Token<'s>,
|
||||
rust: &'s S,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let standard_properties = get_standard_properties(emacs)?;
|
||||
let (begin, end) = (
|
||||
standard_properties
|
||||
.begin
|
||||
.ok_or("Token should have a begin.")?,
|
||||
standard_properties
|
||||
.end
|
||||
.ok_or("Token should have a begin.")?,
|
||||
);
|
||||
let (rust_begin, rust_end) = get_offsets(source, rust);
|
||||
if (rust_begin + 1) != begin || (rust_end + 1) != end {
|
||||
Err(format!("Rust bounds ({rust_begin}, {rust_end}) do not match emacs bounds ({emacs_begin}, {emacs_end})", rust_begin = rust_begin + 1, rust_end = rust_end + 1, emacs_begin=begin, emacs_end=end))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct StandardProperties {
|
||||
begin: Option<usize>,
|
||||
#[allow(dead_code)]
|
||||
post_affiliated: Option<usize>,
|
||||
#[allow(dead_code)]
|
||||
contents_begin: Option<usize>,
|
||||
#[allow(dead_code)]
|
||||
contents_end: Option<usize>,
|
||||
end: Option<usize>,
|
||||
#[allow(dead_code)]
|
||||
post_blank: Option<usize>,
|
||||
}
|
||||
|
||||
fn get_standard_properties<'s>(
|
||||
emacs: &'s Token<'s>,
|
||||
) -> Result<StandardProperties, Box<dyn std::error::Error>> {
|
||||
let children = emacs.as_list()?;
|
||||
let attributes_child = children
|
||||
.iter()
|
||||
.nth(1)
|
||||
.ok_or("Should have an attributes child.")?;
|
||||
let attributes_map = attributes_child.as_map()?;
|
||||
let begin = attributes_map
|
||||
.get(":begin")
|
||||
.ok_or("Missing :begin attribute.")?
|
||||
.as_atom()?;
|
||||
let end = attributes_map
|
||||
.get(":end")
|
||||
.ok_or("Missing :end attribute.")?
|
||||
.as_atom()?;
|
||||
let (rust_begin, rust_end) = get_offsets(source, rust);
|
||||
if (rust_begin + 1).to_string() != begin || (rust_end + 1).to_string() != end {
|
||||
Err(format!("Rust bounds ({rust_begin}, {rust_end}) do not match emacs bounds ({emacs_begin}, {emacs_end})", rust_begin = rust_begin + 1, rust_end = rust_end + 1, emacs_begin=begin, emacs_end=end))?;
|
||||
let standard_properties = attributes_map.get(":standard-properties");
|
||||
Ok(if standard_properties.is_some() {
|
||||
let mut std_props = standard_properties
|
||||
.expect("if statement proves its Some")
|
||||
.as_vector()?
|
||||
.into_iter();
|
||||
let begin = maybe_token_to_usize(std_props.next())?;
|
||||
let post_affiliated = maybe_token_to_usize(std_props.next())?;
|
||||
let contents_begin = maybe_token_to_usize(std_props.next())?;
|
||||
let contents_end = maybe_token_to_usize(std_props.next())?;
|
||||
let end = maybe_token_to_usize(std_props.next())?;
|
||||
let post_blank = maybe_token_to_usize(std_props.next())?;
|
||||
StandardProperties {
|
||||
begin,
|
||||
post_affiliated,
|
||||
contents_begin,
|
||||
contents_end,
|
||||
end,
|
||||
post_blank,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
let begin = maybe_token_to_usize(attributes_map.get(":begin").map(|token| *token))?;
|
||||
let end = maybe_token_to_usize(attributes_map.get(":end").map(|token| *token))?;
|
||||
let contents_begin =
|
||||
maybe_token_to_usize(attributes_map.get(":contents-begin").map(|token| *token))?;
|
||||
let contents_end =
|
||||
maybe_token_to_usize(attributes_map.get(":contents-end").map(|token| *token))?;
|
||||
let post_blank =
|
||||
maybe_token_to_usize(attributes_map.get(":post-blank").map(|token| *token))?;
|
||||
let post_affiliated =
|
||||
maybe_token_to_usize(attributes_map.get(":post-affiliated").map(|token| *token))?;
|
||||
StandardProperties {
|
||||
begin,
|
||||
post_affiliated,
|
||||
contents_begin,
|
||||
contents_end,
|
||||
end,
|
||||
post_blank,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn maybe_token_to_usize(
|
||||
token: Option<&Token<'_>>,
|
||||
) -> Result<Option<usize>, Box<dyn std::error::Error>> {
|
||||
Ok(token
|
||||
.map(|token| token.as_atom())
|
||||
.map_or(Ok(None), |r| r.map(Some))?
|
||||
.map(|val| {
|
||||
if val == "nil" {
|
||||
None
|
||||
} else {
|
||||
Some(val.parse::<usize>())
|
||||
}
|
||||
})
|
||||
.flatten() // Outer option is whether or not the param exists, inner option is whether or not it is nil
|
||||
.map_or(Ok(None), |r| r.map(Some))?)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use nom::IResult;
|
||||
|
||||
pub type Res<T, U> = IResult<T, U, CustomError<T>>;
|
||||
|
||||
// TODO: MyError probably shouldn't be based on the same type as the input type since it's used exclusively with static strings right now.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CustomError<I> {
|
||||
MyError(MyError<I>),
|
||||
|
||||
@@ -15,7 +15,8 @@ pub fn init_telemetry() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// TODO: I think the endpoint can be controlled by the OTEL_EXPORTER_OTLP_TRACES_ENDPOINT env variable instead of hard-coded into this code base. Regardless, I am the only developer right now so I am not too concerned.
|
||||
let exporter = opentelemetry_otlp::new_exporter()
|
||||
.tonic()
|
||||
.with_endpoint("http://localhost:4317/v1/traces");
|
||||
// Using "localhost" is broken inside the docker container when tracing
|
||||
.with_endpoint("http://127.0.0.1:4317/v1/traces");
|
||||
|
||||
let tracer = opentelemetry_otlp::new_pipeline()
|
||||
.tracing()
|
||||
|
||||
@@ -7,6 +7,10 @@ mod compare;
|
||||
pub use compare::compare_document;
|
||||
#[cfg(feature = "compare")]
|
||||
pub use compare::emacs_parse_org_document;
|
||||
#[cfg(feature = "compare")]
|
||||
pub use compare::get_emacs_version;
|
||||
#[cfg(feature = "compare")]
|
||||
pub use compare::get_org_mode_version;
|
||||
|
||||
mod error;
|
||||
pub mod parser;
|
||||
|
||||
67
src/main.rs
67
src/main.rs
@@ -1,17 +1,23 @@
|
||||
#![feature(round_char_boundary)]
|
||||
use std::path::Path;
|
||||
use std::io::Read;
|
||||
|
||||
#[cfg(feature = "compare")]
|
||||
use ::organic::parser::document;
|
||||
#[cfg(feature = "compare")]
|
||||
use organic::compare_document;
|
||||
#[cfg(feature = "compare")]
|
||||
use organic::emacs_parse_org_document;
|
||||
#[cfg(feature = "compare")]
|
||||
use organic::get_emacs_version;
|
||||
#[cfg(feature = "compare")]
|
||||
use organic::get_org_mode_version;
|
||||
#[cfg(feature = "compare")]
|
||||
use organic::parser::sexp::sexp_with_padding;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use crate::init_tracing::init_telemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
use crate::init_tracing::shutdown_telemetry;
|
||||
#[cfg(feature = "tracing")]
|
||||
mod init_tracing;
|
||||
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
@@ -22,40 +28,47 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(feature = "tracing")]
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rt = tokio::runtime::Runtime::new()?;
|
||||
let result = rt.block_on(async { main_body() });
|
||||
let result = rt.block_on(async {
|
||||
init_telemetry()?;
|
||||
let main_body_result = main_body();
|
||||
shutdown_telemetry()?;
|
||||
main_body_result
|
||||
});
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn main_body() -> Result<(), Box<dyn std::error::Error>> {
|
||||
init_telemetry()?;
|
||||
run_compare(
|
||||
std::env::args()
|
||||
.nth(1)
|
||||
.expect("Pass a single file into this script."),
|
||||
)?;
|
||||
shutdown_telemetry()?;
|
||||
Ok(())
|
||||
let org_contents = read_stdin_to_string()?;
|
||||
run_compare(org_contents)
|
||||
}
|
||||
|
||||
fn read_stdin_to_string() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let mut stdin_contents = String::new();
|
||||
std::io::stdin()
|
||||
.lock()
|
||||
.read_to_string(&mut stdin_contents)?;
|
||||
Ok(stdin_contents)
|
||||
}
|
||||
|
||||
#[cfg(feature = "compare")]
|
||||
fn run_compare<P: AsRef<Path>>(todo_org_path: P) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let org_contents = std::fs::read_to_string(todo_org_path.as_ref()).expect("Read org file.");
|
||||
let (remaining, rust_parsed) = document(org_contents.as_str()).expect("Org Parse failure");
|
||||
let org_sexp =
|
||||
emacs_parse_org_document(todo_org_path.as_ref()).expect("Use emacs to parse org file.");
|
||||
fn run_compare<P: AsRef<str>>(org_contents: P) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let emacs_version = get_emacs_version()?;
|
||||
let org_mode_version = get_org_mode_version()?;
|
||||
eprintln!("Using emacs version: {}", emacs_version.trim());
|
||||
eprintln!("Using org-mode version: {}", org_mode_version.trim());
|
||||
let (remaining, rust_parsed) = document(org_contents.as_ref()).map_err(|e| e.to_string())?;
|
||||
let org_sexp = emacs_parse_org_document(org_contents.as_ref())?;
|
||||
let (_remaining, parsed_sexp) =
|
||||
sexp_with_padding(org_sexp.as_str()).expect("Sexp Parse failure");
|
||||
sexp_with_padding(org_sexp.as_str()).map_err(|e| e.to_string())?;
|
||||
|
||||
println!("{}\n\n\n", org_contents.as_str());
|
||||
println!("{}\n\n\n", org_contents.as_ref());
|
||||
println!("{}", org_sexp);
|
||||
println!("{:#?}", rust_parsed);
|
||||
|
||||
// We do the diffing after printing out both parsed forms in case the diffing panics
|
||||
let diff_result =
|
||||
compare_document(&parsed_sexp, &rust_parsed).expect("Compare parsed documents.");
|
||||
diff_result
|
||||
.print()
|
||||
.expect("Print document parse tree diff.");
|
||||
let diff_result = compare_document(&parsed_sexp, &rust_parsed)?;
|
||||
diff_result.print()?;
|
||||
|
||||
if diff_result.is_bad() {
|
||||
Err("Diff results do not match.")?;
|
||||
@@ -68,7 +81,11 @@ fn run_compare<P: AsRef<Path>>(todo_org_path: P) -> Result<(), Box<dyn std::erro
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "compare"))]
|
||||
fn run_compare<P: AsRef<Path>>(_todo_org_path: P) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("This program was built with compare disabled. Doing nothing.");
|
||||
fn run_compare<P: AsRef<str>>(org_contents: P) -> Result<(), Box<dyn std::error::Error>> {
|
||||
eprintln!(
|
||||
"This program was built with compare disabled. Only parsing with organic, not comparing."
|
||||
);
|
||||
let (remaining, rust_parsed) = document(org_contents.as_ref()).map_err(|e| e.to_string())?;
|
||||
println!("{:#?}", rust_parsed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use nom::combinator::peek;
|
||||
use nom::combinator::recognize;
|
||||
use nom::multi::many_till;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::Context;
|
||||
use crate::error::Res;
|
||||
use crate::parser::exiting::ExitClass;
|
||||
@@ -16,7 +17,10 @@ use crate::parser::util::get_consumed;
|
||||
use crate::parser::AngleLink;
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn angle_link<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, AngleLink<'s>> {
|
||||
pub fn angle_link<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, AngleLink<'s>> {
|
||||
let (remaining, _) = tag("<")(input)?;
|
||||
let (remaining, proto) = protocol(context, remaining)?;
|
||||
let (remaining, _separator) = tag(":")(remaining)?;
|
||||
@@ -26,18 +30,21 @@ pub fn angle_link<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s s
|
||||
Ok((
|
||||
remaining,
|
||||
AngleLink {
|
||||
source,
|
||||
link_type: proto,
|
||||
path,
|
||||
source: source.into(),
|
||||
link_type: proto.into(),
|
||||
path: path.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn path_angle<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn path_angle<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
let parser_context =
|
||||
context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
|
||||
class: ExitClass::Beta,
|
||||
class: ExitClass::Gamma,
|
||||
exit_matcher: &path_angle_end,
|
||||
}));
|
||||
|
||||
@@ -48,6 +55,9 @@ fn path_angle<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn path_angle_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn path_angle_end<'r, 's>(
|
||||
_context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
tag(">")(input)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use nom::multi::many_till;
|
||||
use nom::multi::separated_list1;
|
||||
use nom::sequence::tuple;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::Context;
|
||||
use crate::error::CustomError;
|
||||
use crate::error::Res;
|
||||
@@ -29,7 +30,10 @@ use crate::parser::util::get_consumed;
|
||||
use crate::parser::Object;
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn citation<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Citation<'s>> {
|
||||
pub fn citation<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Citation<'s>> {
|
||||
// TODO: Despite being a standard object, citations cannot exist inside the global prefix/suffix for other citations because citations must contain something that matches @key which is forbidden inside the global prefix/suffix. This TODO is to evaluate if its worth putting in an explicit check for this (which can be easily accomplished by checking the output of `get_bracket_depth()`). I suspect its not worth it because I expect, outside of intentionally crafted inputs, this parser will exit immediately inside a citation since it is unlikely to find the "[cite" substring inside a citation global prefix/suffix.
|
||||
let (remaining, _) = tag_no_case("[cite")(input)?;
|
||||
let (remaining, _) = opt(citestyle)(remaining)?;
|
||||
@@ -44,11 +48,16 @@ pub fn citation<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str
|
||||
let (remaining, _) = tag("]")(remaining)?;
|
||||
let (remaining, _) = space0(remaining)?;
|
||||
let source = get_consumed(input, remaining);
|
||||
Ok((remaining, Citation { source }))
|
||||
Ok((
|
||||
remaining,
|
||||
Citation {
|
||||
source: source.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn citestyle<'r, 's>(input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn citestyle<'r, 's>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
let (remaining, _) = tuple((tag("/"), style))(input)?;
|
||||
let (remaining, _) = opt(tuple((tag("/"), variant)))(remaining)?;
|
||||
let source = get_consumed(input, remaining);
|
||||
@@ -56,14 +65,14 @@ fn citestyle<'r, 's>(input: &'s str) -> Res<&'s str, &'s str> {
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn style<'r, 's>(input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn style<'r, 's>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
recognize(many1(verify(anychar, |c| {
|
||||
c.is_alphanumeric() || "_-".contains(*c)
|
||||
})))(input)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn variant<'r, 's>(input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn variant<'r, 's>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
recognize(many1(verify(anychar, |c| {
|
||||
c.is_alphanumeric() || "_-/".contains(*c)
|
||||
})))(input)
|
||||
@@ -72,8 +81,8 @@ fn variant<'r, 's>(input: &'s str) -> Res<&'s str, &'s str> {
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn global_prefix<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: &'s str,
|
||||
) -> Res<&'s str, Vec<Object<'s>>> {
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Vec<Object<'s>>> {
|
||||
// TODO: I could insert CitationBracket entries in the context after each matched object to reduce the scanning done for counting brackets which should be more efficient.
|
||||
let parser_context = context
|
||||
.with_additional_node(ContextElement::CitationBracket(CitationBracket {
|
||||
@@ -81,7 +90,7 @@ fn global_prefix<'r, 's>(
|
||||
depth: 0,
|
||||
}))
|
||||
.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
|
||||
class: ExitClass::Beta,
|
||||
class: ExitClass::Gamma,
|
||||
exit_matcher: &global_prefix_end,
|
||||
}));
|
||||
let (remaining, (children, _exit_contents)) = verify(
|
||||
@@ -96,12 +105,15 @@ fn global_prefix<'r, 's>(
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn global_prefix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn global_prefix_end<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
let context_depth = get_bracket_depth(context)
|
||||
.expect("This function should only be called from inside a citation.");
|
||||
let text_since_context_entry = get_consumed(context_depth.position, input);
|
||||
let mut current_depth = context_depth.depth;
|
||||
for c in text_since_context_entry.chars() {
|
||||
for c in Into::<&str>::into(text_since_context_entry).chars() {
|
||||
match c {
|
||||
'[' => {
|
||||
current_depth += 1;
|
||||
@@ -116,7 +128,7 @@ fn global_prefix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'
|
||||
}
|
||||
}
|
||||
if current_depth == 0 {
|
||||
let close_bracket = tag::<&str, &str, CustomError<&str>>("]")(input);
|
||||
let close_bracket = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("]")(input);
|
||||
if close_bracket.is_ok() {
|
||||
return close_bracket;
|
||||
}
|
||||
@@ -130,8 +142,8 @@ fn global_prefix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn global_suffix<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: &'s str,
|
||||
) -> Res<&'s str, Vec<Object<'s>>> {
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Vec<Object<'s>>> {
|
||||
// TODO: I could insert CitationBracket entries in the context after each matched object to reduce the scanning done for counting brackets which should be more efficient.
|
||||
let parser_context = context
|
||||
.with_additional_node(ContextElement::CitationBracket(CitationBracket {
|
||||
@@ -139,7 +151,7 @@ fn global_suffix<'r, 's>(
|
||||
depth: 0,
|
||||
}))
|
||||
.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
|
||||
class: ExitClass::Beta,
|
||||
class: ExitClass::Gamma,
|
||||
exit_matcher: &global_suffix_end,
|
||||
}));
|
||||
let (remaining, (children, _exit_contents)) = verify(
|
||||
@@ -153,12 +165,15 @@ fn global_suffix<'r, 's>(
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn global_suffix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn global_suffix_end<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
let context_depth = get_bracket_depth(context)
|
||||
.expect("This function should only be called from inside a citation.");
|
||||
let text_since_context_entry = get_consumed(context_depth.position, input);
|
||||
let mut current_depth = context_depth.depth;
|
||||
for c in text_since_context_entry.chars() {
|
||||
for c in Into::<&str>::into(text_since_context_entry).chars() {
|
||||
match c {
|
||||
'[' => {
|
||||
current_depth += 1;
|
||||
@@ -173,7 +188,7 @@ fn global_suffix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'
|
||||
}
|
||||
}
|
||||
if current_depth == 0 {
|
||||
let close_bracket = tag::<&str, &str, CustomError<&str>>("]")(input);
|
||||
let close_bracket = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("]")(input);
|
||||
if close_bracket.is_ok() {
|
||||
return close_bracket;
|
||||
}
|
||||
@@ -188,24 +203,21 @@ fn global_suffix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::parser::element_parser::element;
|
||||
use crate::parser::parser_context::ContextElement;
|
||||
use crate::parser::parser_context::ContextTree;
|
||||
use crate::parser::parser_with_context::parser_with_context;
|
||||
use crate::parser::source::Source;
|
||||
|
||||
#[test]
|
||||
fn citation_simple() {
|
||||
let input = "[cite:@foo]";
|
||||
let input = OrgSource::new("[cite:@foo]");
|
||||
let initial_context: ContextTree<'_, '_> = ContextTree::new();
|
||||
let document_context =
|
||||
initial_context.with_additional_node(ContextElement::DocumentRoot(input));
|
||||
let paragraph_matcher = parser_with_context!(element(true))(&document_context);
|
||||
let paragraph_matcher = parser_with_context!(element(true))(&initial_context);
|
||||
let (remaining, first_paragraph) = paragraph_matcher(input).expect("Parse first paragraph");
|
||||
let first_paragraph = match first_paragraph {
|
||||
crate::parser::Element::Paragraph(paragraph) => paragraph,
|
||||
_ => panic!("Should be a paragraph!"),
|
||||
};
|
||||
assert_eq!(remaining, "");
|
||||
assert_eq!(Into::<&str>::into(remaining), "");
|
||||
assert_eq!(first_paragraph.get_source(), "[cite:@foo]");
|
||||
assert_eq!(first_paragraph.children.len(), 1);
|
||||
assert_eq!(
|
||||
|
||||
@@ -10,6 +10,7 @@ use nom::multi::many_till;
|
||||
use nom::sequence::preceded;
|
||||
use nom::sequence::tuple;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::Context;
|
||||
use crate::error::CustomError;
|
||||
use crate::error::Res;
|
||||
@@ -28,21 +29,26 @@ use crate::parser::Object;
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn citation_reference<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: &'s str,
|
||||
) -> Res<&'s str, CitationReference<'s>> {
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, CitationReference<'s>> {
|
||||
let (remaining, _prefix) = opt(parser_with_context!(key_prefix)(context))(input)?;
|
||||
let (remaining, _key) = parser_with_context!(citation_reference_key)(context)(remaining)?;
|
||||
let (remaining, _suffix) = opt(parser_with_context!(key_suffix)(context))(remaining)?;
|
||||
let source = get_consumed(input, remaining);
|
||||
|
||||
Ok((remaining, CitationReference { source }))
|
||||
Ok((
|
||||
remaining,
|
||||
CitationReference {
|
||||
source: source.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn citation_reference_key<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: &'s str,
|
||||
) -> Res<&'s str, &'s str> {
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
let (remaining, source) = recognize(tuple((
|
||||
tag("@"),
|
||||
many1(verify(
|
||||
@@ -59,7 +65,10 @@ pub fn citation_reference_key<'r, 's>(
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn key_prefix<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Vec<Object<'s>>> {
|
||||
fn key_prefix<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Vec<Object<'s>>> {
|
||||
// TODO: I could insert CitationBracket entries in the context after each matched object to reduce the scanning done for counting brackets which should be more efficient.
|
||||
let parser_context = context
|
||||
.with_additional_node(ContextElement::CitationBracket(CitationBracket {
|
||||
@@ -67,7 +76,7 @@ fn key_prefix<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str,
|
||||
depth: 0,
|
||||
}))
|
||||
.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
|
||||
class: ExitClass::Beta,
|
||||
class: ExitClass::Gamma,
|
||||
exit_matcher: &key_prefix_end,
|
||||
}));
|
||||
let (remaining, (children, _exit_contents)) = verify(
|
||||
@@ -81,7 +90,10 @@ fn key_prefix<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn key_suffix<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Vec<Object<'s>>> {
|
||||
fn key_suffix<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Vec<Object<'s>>> {
|
||||
// TODO: I could insert CitationBracket entries in the context after each matched object to reduce the scanning done for counting brackets which should be more efficient.
|
||||
let parser_context = context
|
||||
.with_additional_node(ContextElement::CitationBracket(CitationBracket {
|
||||
@@ -89,7 +101,7 @@ fn key_suffix<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str,
|
||||
depth: 0,
|
||||
}))
|
||||
.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
|
||||
class: ExitClass::Beta,
|
||||
class: ExitClass::Gamma,
|
||||
exit_matcher: &key_suffix_end,
|
||||
}));
|
||||
let (remaining, (children, _exit_contents)) = verify(
|
||||
@@ -114,12 +126,15 @@ pub fn get_bracket_depth<'r, 's>(context: Context<'r, 's>) -> Option<&'r Citatio
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn key_prefix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn key_prefix_end<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
let context_depth = get_bracket_depth(context)
|
||||
.expect("This function should only be called from inside a citation reference.");
|
||||
let text_since_context_entry = get_consumed(context_depth.position, input);
|
||||
let mut current_depth = context_depth.depth;
|
||||
for c in text_since_context_entry.chars() {
|
||||
for c in Into::<&str>::into(text_since_context_entry).chars() {
|
||||
match c {
|
||||
'[' => {
|
||||
current_depth += 1;
|
||||
@@ -134,7 +149,7 @@ fn key_prefix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s s
|
||||
}
|
||||
}
|
||||
if current_depth == 0 {
|
||||
let close_bracket = tag::<&str, &str, CustomError<&str>>("]")(input);
|
||||
let close_bracket = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("]")(input);
|
||||
if close_bracket.is_ok() {
|
||||
return close_bracket;
|
||||
}
|
||||
@@ -146,12 +161,15 @@ fn key_prefix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s s
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn key_suffix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn key_suffix_end<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
let context_depth = get_bracket_depth(context)
|
||||
.expect("This function should only be called from inside a citation reference.");
|
||||
let text_since_context_entry = get_consumed(context_depth.position, input);
|
||||
let mut current_depth = context_depth.depth;
|
||||
for c in text_since_context_entry.chars() {
|
||||
for c in Into::<&str>::into(text_since_context_entry).chars() {
|
||||
match c {
|
||||
'[' => {
|
||||
current_depth += 1;
|
||||
@@ -166,7 +184,7 @@ fn key_suffix_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s s
|
||||
}
|
||||
}
|
||||
if current_depth == 0 {
|
||||
let close_bracket = tag::<&str, &str, CustomError<&str>>("]")(input);
|
||||
let close_bracket = tag::<&str, OrgSource<'_>, CustomError<OrgSource<'_>>>("]")(input);
|
||||
if close_bracket.is_ok() {
|
||||
return close_bracket;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use nom::combinator::recognize;
|
||||
use nom::combinator::verify;
|
||||
use nom::sequence::tuple;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::Context;
|
||||
use crate::error::Res;
|
||||
use crate::parser::parser_with_context::parser_with_context;
|
||||
@@ -19,8 +20,11 @@ use crate::parser::util::start_of_line;
|
||||
use crate::parser::Clock;
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn clock<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Clock<'s>> {
|
||||
start_of_line(context, input)?;
|
||||
pub fn clock<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Clock<'s>> {
|
||||
start_of_line(input)?;
|
||||
let (remaining, _leading_whitespace) = space0(input)?;
|
||||
let (remaining, _clock) = tag_no_case("clock:")(remaining)?;
|
||||
let (remaining, _gap_whitespace) = space1(remaining)?;
|
||||
@@ -31,14 +35,19 @@ pub fn clock<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, C
|
||||
))(remaining)?;
|
||||
|
||||
let source = get_consumed(input, remaining);
|
||||
Ok((remaining, Clock { source }))
|
||||
Ok((
|
||||
remaining,
|
||||
Clock {
|
||||
source: source.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn inactive_timestamp_range_duration<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: &'s str,
|
||||
) -> Res<&'s str, &'s str> {
|
||||
_context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
recognize(tuple((
|
||||
tag("["),
|
||||
is_not("\r\n]"),
|
||||
@@ -50,14 +59,17 @@ fn inactive_timestamp_range_duration<'r, 's>(
|
||||
space1,
|
||||
digit1,
|
||||
tag(":"),
|
||||
verify(digit1, |mm: &str| mm.len() == 2),
|
||||
verify(digit1, |mm: &OrgSource<'_>| mm.len() == 2),
|
||||
space0,
|
||||
alt((line_ending, eof)),
|
||||
)))(input)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn inactive_timestamp<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn inactive_timestamp<'r, 's>(
|
||||
_context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
recognize(tuple((
|
||||
tag("["),
|
||||
is_not("\r\n]"),
|
||||
|
||||
@@ -11,6 +11,7 @@ use nom::multi::many0;
|
||||
use nom::sequence::preceded;
|
||||
use nom::sequence::tuple;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::util::get_consumed;
|
||||
use super::Context;
|
||||
use crate::error::CustomError;
|
||||
@@ -24,10 +25,13 @@ use crate::parser::util::start_of_line;
|
||||
use crate::parser::Comment;
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn comment<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Comment<'s>> {
|
||||
pub fn comment<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Comment<'s>> {
|
||||
if immediate_in_section(context, "comment") {
|
||||
return Err(nom::Err::Error(CustomError::MyError(MyError(
|
||||
"Cannot nest objects of the same element",
|
||||
"Cannot nest objects of the same element".into(),
|
||||
))));
|
||||
}
|
||||
let parser_context = context.with_additional_node(ContextElement::Context("comment"));
|
||||
@@ -38,12 +42,20 @@ pub fn comment<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str,
|
||||
many0(preceded(not(exit_matcher), comment_line_matcher))(remaining)?;
|
||||
|
||||
let source = get_consumed(input, remaining);
|
||||
Ok((remaining, Comment { source }))
|
||||
Ok((
|
||||
remaining,
|
||||
Comment {
|
||||
source: source.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn comment_line<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
start_of_line(context, input)?;
|
||||
fn comment_line<'r, 's>(
|
||||
_context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
start_of_line(input)?;
|
||||
let (remaining, _indent) = space0(input)?;
|
||||
let (remaining, (_hash, _leading_whitespace_and_content, _line_ending)) = tuple((
|
||||
tag("#"),
|
||||
@@ -57,22 +69,21 @@ fn comment_line<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::parser::parser_context::ContextElement;
|
||||
use crate::parser::parser_context::ContextTree;
|
||||
use crate::parser::parser_with_context::parser_with_context;
|
||||
|
||||
#[test]
|
||||
fn require_space_after_hash() {
|
||||
let input = "# Comment line
|
||||
let input = OrgSource::new(
|
||||
"# Comment line
|
||||
#not a comment
|
||||
# Comment again";
|
||||
# Comment again",
|
||||
);
|
||||
let initial_context: ContextTree<'_, '_> = ContextTree::new();
|
||||
let document_context =
|
||||
initial_context.with_additional_node(ContextElement::DocumentRoot(input));
|
||||
let comment_matcher = parser_with_context!(comment)(&document_context);
|
||||
let comment_matcher = parser_with_context!(comment)(&initial_context);
|
||||
let (remaining, first_comment) = comment_matcher(input).expect("Parse first comment");
|
||||
assert_eq!(
|
||||
remaining,
|
||||
Into::<&str>::into(remaining),
|
||||
r#"#not a comment
|
||||
# Comment again"#
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ use nom::combinator::eof;
|
||||
use nom::combinator::recognize;
|
||||
use nom::sequence::tuple;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::sexp::sexp;
|
||||
use super::Context;
|
||||
use crate::error::Res;
|
||||
@@ -14,8 +15,11 @@ use crate::parser::util::start_of_line;
|
||||
use crate::parser::DiarySexp;
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn diary_sexp<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, DiarySexp<'s>> {
|
||||
start_of_line(context, input)?;
|
||||
pub fn diary_sexp<'r, 's>(
|
||||
_context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, DiarySexp<'s>> {
|
||||
start_of_line(input)?;
|
||||
let (remaining, _leading_whitespace) = space0(input)?;
|
||||
let (remaining, _clock) = tag("%%")(remaining)?;
|
||||
let (remaining, _gap_whitespace) = space0(remaining)?;
|
||||
@@ -24,5 +28,10 @@ pub fn diary_sexp<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s s
|
||||
recognize(tuple((space0, alt((line_ending, eof)))))(remaining)?;
|
||||
|
||||
let source = get_consumed(input, remaining);
|
||||
Ok((remaining, DiarySexp { source }))
|
||||
Ok((
|
||||
remaining,
|
||||
DiarySexp {
|
||||
source: source.into(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ use nom::sequence::tuple;
|
||||
|
||||
use super::element::Element;
|
||||
use super::object::Object;
|
||||
use super::org_source::convert_error;
|
||||
use super::org_source::OrgSource;
|
||||
use super::parser_with_context::parser_with_context;
|
||||
use super::source::Source;
|
||||
use super::token::AllTokensIterator;
|
||||
@@ -95,9 +97,10 @@ impl<'s> Source<'s> for Heading<'s> {
|
||||
#[allow(dead_code)]
|
||||
pub fn document(input: &str) -> Res<&str, Document> {
|
||||
let initial_context: ContextTree<'_, '_> = ContextTree::new();
|
||||
let document_context =
|
||||
initial_context.with_additional_node(ContextElement::DocumentRoot(input));
|
||||
let (remaining, document) = _document(&document_context, input)?;
|
||||
let wrapped_input = OrgSource::new(input);
|
||||
let (remaining, document) = _document(&initial_context, wrapped_input)
|
||||
.map(|(rem, out)| (Into::<&str>::into(rem), out))
|
||||
.map_err(convert_error)?;
|
||||
{
|
||||
// If there are radio targets in this document then we need to parse the entire document again with the knowledge of the radio targets.
|
||||
let all_radio_targets: Vec<&Vec<Object<'_>>> = document
|
||||
@@ -113,17 +116,22 @@ pub fn document(input: &str) -> Res<&str, Document> {
|
||||
.map(|rt| &rt.children)
|
||||
.collect();
|
||||
if !all_radio_targets.is_empty() {
|
||||
let document_context = document_context
|
||||
let initial_context = initial_context
|
||||
.with_additional_node(ContextElement::RadioTarget(all_radio_targets));
|
||||
let (remaining, document) = _document(&document_context, input)?;
|
||||
return Ok((remaining, document));
|
||||
let (remaining, document) = _document(&initial_context, wrapped_input)
|
||||
.map(|(rem, out)| (Into::<&str>::into(rem), out))
|
||||
.map_err(convert_error)?;
|
||||
return Ok((remaining.into(), document));
|
||||
}
|
||||
}
|
||||
Ok((remaining, document))
|
||||
Ok((remaining.into(), document))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn _document<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Document<'s>> {
|
||||
fn _document<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Document<'s>> {
|
||||
let zeroth_section_matcher = parser_with_context!(zeroth_section)(context);
|
||||
let heading_matcher = parser_with_context!(heading)(context);
|
||||
let (remaining, _blank_lines) = many0(blank_line)(input)?;
|
||||
@@ -133,7 +141,7 @@ pub fn _document<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s st
|
||||
Ok((
|
||||
remaining,
|
||||
Document {
|
||||
source,
|
||||
source: source.into(),
|
||||
zeroth_section,
|
||||
children,
|
||||
},
|
||||
@@ -141,7 +149,10 @@ pub fn _document<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s st
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn zeroth_section<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Section<'s>> {
|
||||
fn zeroth_section<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Section<'s>> {
|
||||
// TODO: The zeroth section is specialized so it probably needs its own parser
|
||||
let parser_context = context
|
||||
.with_additional_node(ContextElement::ConsumeTrailingWhitespace(true))
|
||||
@@ -160,7 +171,7 @@ fn zeroth_section<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s s
|
||||
opt(parser_with_context!(comment)(
|
||||
&without_consuming_whitespace_context,
|
||||
)),
|
||||
parser_with_context!(property_drawer)(&without_consuming_whitespace_context),
|
||||
parser_with_context!(property_drawer)(context),
|
||||
many0(blank_line),
|
||||
)))(input)?;
|
||||
|
||||
@@ -182,11 +193,20 @@ fn zeroth_section<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s s
|
||||
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
|
||||
|
||||
let source = get_consumed(input, remaining);
|
||||
Ok((remaining, Section { source, children }))
|
||||
Ok((
|
||||
remaining,
|
||||
Section {
|
||||
source: source.into(),
|
||||
children,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn section<'r, 's>(context: Context<'r, 's>, mut input: &'s str) -> Res<&'s str, Section<'s>> {
|
||||
fn section<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
mut input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Section<'s>> {
|
||||
// TODO: The zeroth section is specialized so it probably needs its own parser
|
||||
let parser_context = context
|
||||
.with_additional_node(ContextElement::ConsumeTrailingWhitespace(true))
|
||||
@@ -223,17 +243,29 @@ fn section<'r, 's>(context: Context<'r, 's>, mut input: &'s str) -> Res<&'s str,
|
||||
maybe_consume_trailing_whitespace_if_not_exiting(context, remaining)?;
|
||||
|
||||
let source = get_consumed(input, remaining);
|
||||
Ok((remaining, Section { source, children }))
|
||||
Ok((
|
||||
remaining,
|
||||
Section {
|
||||
source: source.into(),
|
||||
children,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn section_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn section_end<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
let headline_matcher = parser_with_context!(headline)(context);
|
||||
recognize(headline_matcher)(input)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn heading<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Heading<'s>> {
|
||||
fn heading<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Heading<'s>> {
|
||||
not(|i| context.check_exit_matcher(i))(input)?;
|
||||
let (remaining, (star_count, _ws, title)) = headline(context, input)?;
|
||||
let section_matcher = parser_with_context!(section)(context);
|
||||
@@ -249,7 +281,7 @@ fn heading<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Hea
|
||||
Ok((
|
||||
remaining,
|
||||
Heading {
|
||||
source,
|
||||
source: source.into(),
|
||||
stars: star_count,
|
||||
title,
|
||||
children,
|
||||
@@ -260,18 +292,17 @@ fn heading<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Hea
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn headline<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: &'s str,
|
||||
) -> Res<&'s str, (usize, &'s str, Vec<Object<'s>>)> {
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, (usize, OrgSource<'s>, Vec<Object<'s>>)> {
|
||||
let parser_context =
|
||||
context.with_additional_node(ContextElement::ExitMatcherNode(ExitMatcherNode {
|
||||
class: ExitClass::Document,
|
||||
exit_matcher: &headline_end,
|
||||
}));
|
||||
let standard_set_object_matcher = parser_with_context!(standard_set_object)(&parser_context);
|
||||
let start_of_line_matcher = parser_with_context!(start_of_line)(&parser_context);
|
||||
|
||||
let (remaining, (_sol, star_count, ws, title, _line_ending)) = tuple((
|
||||
start_of_line_matcher,
|
||||
start_of_line,
|
||||
many1_count(tag("*")),
|
||||
space1,
|
||||
many1(standard_set_object_matcher),
|
||||
@@ -281,7 +312,10 @@ fn headline<'r, 's>(
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn headline_end<'r, 's>(_context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn headline_end<'r, 's>(
|
||||
_context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
line_ending(input)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use nom::combinator::recognize;
|
||||
use nom::multi::many_till;
|
||||
use nom::sequence::tuple;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::Context;
|
||||
use crate::error::CustomError;
|
||||
use crate::error::MyError;
|
||||
@@ -31,13 +32,16 @@ use crate::parser::Element;
|
||||
use crate::parser::Paragraph;
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn drawer<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, Drawer<'s>> {
|
||||
pub fn drawer<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, Drawer<'s>> {
|
||||
if immediate_in_section(context, "drawer") {
|
||||
return Err(nom::Err::Error(CustomError::MyError(MyError(
|
||||
"Cannot nest objects of the same element",
|
||||
"Cannot nest objects of the same element".into(),
|
||||
))));
|
||||
}
|
||||
start_of_line(context, input)?;
|
||||
start_of_line(input)?;
|
||||
let (remaining, _leading_whitespace) = space0(input)?;
|
||||
let (remaining, (_open_colon, drawer_name, _close_colon, _new_line)) = tuple((
|
||||
tag(":"),
|
||||
@@ -63,9 +67,9 @@ pub fn drawer<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str,
|
||||
))(remaining)
|
||||
{
|
||||
Ok((remain, (_not_immediate_exit, first_line, (_trailing_whitespace, _exit_contents)))) => {
|
||||
let mut element = Element::Paragraph(Paragraph::of_text(first_line));
|
||||
let mut element = Element::Paragraph(Paragraph::of_text(first_line.into()));
|
||||
let source = get_consumed(remaining, remain);
|
||||
element.set_source(source);
|
||||
element.set_source(source.into());
|
||||
(remain, vec![element])
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -81,21 +85,24 @@ pub fn drawer<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str,
|
||||
Ok((
|
||||
remaining,
|
||||
Drawer {
|
||||
source,
|
||||
name: drawer_name,
|
||||
source: source.into(),
|
||||
name: drawer_name.into(),
|
||||
children,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn name<'s>(input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn name<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
take_while(|c| WORD_CONSTITUENT_CHARACTERS.contains(c) || "-_".contains(c))(input)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn drawer_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
start_of_line(context, input)?;
|
||||
fn drawer_end<'r, 's>(
|
||||
_context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
start_of_line(input)?;
|
||||
recognize(tuple((
|
||||
space0,
|
||||
tag_no_case(":end:"),
|
||||
|
||||
@@ -11,6 +11,7 @@ use nom::combinator::recognize;
|
||||
use nom::multi::many_till;
|
||||
use nom::sequence::tuple;
|
||||
|
||||
use super::org_source::OrgSource;
|
||||
use super::Context;
|
||||
use crate::error::CustomError;
|
||||
use crate::error::MyError;
|
||||
@@ -33,15 +34,15 @@ use crate::parser::Element;
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
pub fn dynamic_block<'r, 's>(
|
||||
context: Context<'r, 's>,
|
||||
input: &'s str,
|
||||
) -> Res<&'s str, DynamicBlock<'s>> {
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, DynamicBlock<'s>> {
|
||||
// TODO: Do I need to differentiate between different dynamic block types.
|
||||
if immediate_in_section(context, "dynamic block") {
|
||||
return Err(nom::Err::Error(CustomError::MyError(MyError(
|
||||
"Cannot nest objects of the same element",
|
||||
"Cannot nest objects of the same element".into(),
|
||||
))));
|
||||
}
|
||||
start_of_line(context, input)?;
|
||||
start_of_line(input)?;
|
||||
let (remaining, _leading_whitespace) = space0(input)?;
|
||||
let (remaining, (_begin, name, parameters, _ws)) = tuple((
|
||||
recognize(tuple((tag_no_case("#+begin:"), space1))),
|
||||
@@ -69,9 +70,9 @@ pub fn dynamic_block<'r, 's>(
|
||||
))(remaining)
|
||||
{
|
||||
Ok((remain, (_not_immediate_exit, first_line, (_trailing_whitespace, _exit_contents)))) => {
|
||||
let mut element = Element::Paragraph(Paragraph::of_text(first_line));
|
||||
let mut element = Element::Paragraph(Paragraph::of_text(first_line.into()));
|
||||
let source = get_consumed(remaining, remain);
|
||||
element.set_source(source);
|
||||
element.set_source(source.into());
|
||||
(remain, vec![element])
|
||||
}
|
||||
Err(_) => {
|
||||
@@ -86,27 +87,30 @@ pub fn dynamic_block<'r, 's>(
|
||||
Ok((
|
||||
remaining,
|
||||
DynamicBlock {
|
||||
source,
|
||||
name,
|
||||
parameters,
|
||||
source: source.into(),
|
||||
name: name.into(),
|
||||
parameters: parameters.map(|val| val.into()),
|
||||
children,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn name<'s>(input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn name<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
is_not(" \t\r\n")(input)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn parameters<'s>(input: &'s str) -> Res<&'s str, &'s str> {
|
||||
fn parameters<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
is_not("\r\n")(input)
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
|
||||
fn dynamic_block_end<'r, 's>(context: Context<'r, 's>, input: &'s str) -> Res<&'s str, &'s str> {
|
||||
start_of_line(context, input)?;
|
||||
fn dynamic_block_end<'r, 's>(
|
||||
_context: Context<'r, 's>,
|
||||
input: OrgSource<'s>,
|
||||
) -> Res<OrgSource<'s>, OrgSource<'s>> {
|
||||
start_of_line(input)?;
|
||||
let (remaining, source) = recognize(tuple((
|
||||
space0,
|
||||
tag_no_case("#+end:"),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user