diff --git a/.lighthouse/pipeline-rust-build.yaml b/.lighthouse/pipeline-rust-build.yaml index d38280b9..b21ce7e7 100644 --- a/.lighthouse/pipeline-rust-build.yaml +++ b/.lighthouse/pipeline-rust-build.yaml @@ -192,6 +192,54 @@ spec: ] - name: docker-image value: "$(params.image-name):$(tasks.fetch-repository.results.commit)" + - name: run-image-wasm + taskRef: + name: run-docker-image + workspaces: + - name: source + workspace: git-source + - name: cargo-cache + workspace: cargo-cache + runAfter: + - run-image-all + params: + - name: args + value: + [ + "--target", + "wasm32-unknown-unknown", + "--profile", + "wasm", + "--bin", + "wasm", + "--no-default-features", + "--features", + "wasm", + ] + - name: docker-image + value: "$(params.image-name):$(tasks.fetch-repository.results.commit)" + - name: run-image-wasm-test + taskRef: + name: run-docker-image + workspaces: + - name: source + workspace: git-source + - name: cargo-cache + workspace: cargo-cache + runAfter: + - run-image-wasm + params: + - name: args + value: + [ + "--bin", + "wasm_test", + "--no-default-features", + "--features", + "wasm_test", + ] + - name: docker-image + value: "$(params.image-name):$(tasks.fetch-repository.results.commit)" finally: - name: report-success when: diff --git a/.lighthouse/pipeline-rust-test.yaml b/.lighthouse/pipeline-rust-test.yaml index e5b05bfa..25d1ed3f 100644 --- a/.lighthouse/pipeline-rust-test.yaml +++ b/.lighthouse/pipeline-rust-test.yaml @@ -115,7 +115,7 @@ spec: [ --no-default-features, --features, - compare, + "compare,wasm_test", --no-fail-fast, --lib, --test, diff --git a/.lighthouse/pipeline-rustfmt.yaml b/.lighthouse/pipeline-rustfmt.yaml index 9e6d6564..aa290fbf 100644 --- a/.lighthouse/pipeline-rustfmt.yaml +++ b/.lighthouse/pipeline-rustfmt.yaml @@ -127,7 +127,7 @@ spec: - name: command value: ["cargo", "fix"] - name: args - value: ["--allow-dirty"] + value: ["--all-targets", "--all-features", "--allow-dirty"] - name: docker-image value: "$(params.image-name):$(tasks.fetch-repository.results.commit)" - name: commit-changes diff --git a/Cargo.toml b/Cargo.toml index e2b10524..8e4be935 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,17 +39,32 @@ path = "src/lib.rs" path = "src/bin_foreign_document_test.rs" required-features = ["foreign_document_test"] +[[bin]] + name = "wasm" + path = "src/bin_wasm.rs" + required-features = ["wasm"] + +[[bin]] + # This bin exists for development purposes only. The real target of this crate is the library. + name = "wasm_test" + path = "src/bin_wasm_test.rs" + required-features = ["wasm_test"] + [dependencies] futures = { version = "0.3.28", optional = true } nom = "7.1.1" opentelemetry = { version = "0.20.0", optional = true, default-features = false, features = ["trace", "rt-tokio"] } opentelemetry-otlp = { version = "0.13.0", optional = true } opentelemetry-semantic-conventions = { version = "0.12.0", optional = true } +serde = { version = "1.0.193", optional = true, features = ["derive"] } +serde-wasm-bindgen = { version = "0.6.3", optional = true } +serde_json = { version = "1.0.108", optional = true } tokio = { version = "1.30.0", optional = true, default-features = false, features = ["rt", "rt-multi-thread"] } tracing = { version = "0.1.37", optional = true } tracing-opentelemetry = { version = "0.20.0", optional = true } tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] } walkdir = { version = "2.3.3", optional = true } +wasm-bindgen = { version = "0.2.89", optional = true } [build-dependencies] walkdir = "2.3.3" @@ -60,6 +75,8 @@ compare = ["tokio/process", "tokio/macros"] foreign_document_test = ["compare", "dep:futures", "tokio/sync", "dep:walkdir", "tokio/process"] tracing = ["dep:opentelemetry", "dep:opentelemetry-otlp", "dep:opentelemetry-semantic-conventions", "dep:tokio", "dep:tracing", "dep:tracing-opentelemetry", "dep:tracing-subscriber"] event_count = [] +wasm = ["dep:serde", "dep:wasm-bindgen", "dep:serde-wasm-bindgen"] +wasm_test = ["wasm", "dep:serde_json", "tokio/process", "tokio/macros"] # Optimized build for any sort of release. [profile.release-lto] @@ -79,3 +96,8 @@ strip = "symbols" inherits = "release" lto = true debug = true + +[profile.wasm] +inherits = "release" +lto = true +strip = true diff --git a/Makefile b/Makefile index 67a8fc9a..9738c8e4 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,11 @@ build: release: > cargo build --release $(RELEASEFLAGS) +.PHONY: wasm +wasm: +> cargo build --target=wasm32-unknown-unknown --profile wasm --bin wasm --features wasm +> wasm-bindgen --target web --out-dir target/wasm32-unknown-unknown/js target/wasm32-unknown-unknown/wasm/wasm.wasm + .PHONY: clean clean: > cargo clean @@ -49,11 +54,20 @@ clippy: test: > cargo test --no-default-features --features compare --no-fail-fast --lib --test test_loader -- --test-threads $(TESTJOBS) +.PHONY: doc +doc: +> cargo doc --no-deps --open --lib --release --all-features + .PHONY: dockertest dockertest: > $(MAKE) -C docker/organic_test > docker run --init --rm -i -t --read-only -v "$$(readlink -f ./):/source:ro" --mount type=tmpfs,destination=/tmp --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source organic-test --no-default-features --features compare --no-fail-fast --lib --test test_loader -- --test-threads $(TESTJOBS) +.PHONY: dockerwasmtest +dockerwasmtest: +> $(MAKE) -C docker/organic_test +> docker run --init --rm -i -t --read-only -v "$$(readlink -f ./):/source:ro" --mount type=tmpfs,destination=/tmp --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source organic-test --no-default-features --features compare,wasm_test --no-fail-fast --lib --test test_loader autogen_wasm_ -- --test-threads $(TESTJOBS) + .PHONY: buildtest buildtest: > cargo build --no-default-features @@ -62,6 +76,8 @@ buildtest: > cargo build --no-default-features --features compare,tracing > cargo build --no-default-features --features compare,foreign_document_test > cargo build --no-default-features --features compare,tracing,foreign_document_test +> cargo build --target wasm32-unknown-unknown --profile wasm --bin wasm --no-default-features --features wasm +> cargo build --bin wasm_test --no-default-features --features wasm_test .PHONY: foreign_document_test foreign_document_test: diff --git a/docker/organic_build/Dockerfile b/docker/organic_build/Dockerfile index fdf38f43..39a74ccb 100644 --- a/docker/organic_build/Dockerfile +++ b/docker/organic_build/Dockerfile @@ -2,5 +2,6 @@ FROM rustlang/rust:nightly-alpine3.17 RUN apk add --no-cache musl-dev RUN cargo install --locked --no-default-features --features ci-autoclean cargo-cache +RUN rustup target add wasm32-unknown-unknown ENTRYPOINT ["cargo", "build"] diff --git a/docker/organic_clippy/Makefile b/docker/organic_clippy/Makefile index 6e9fb21d..8553fb24 100644 --- a/docker/organic_clippy/Makefile +++ b/docker/organic_clippy/Makefile @@ -30,7 +30,7 @@ endif # NOTE: This target will write to folders underneath the git-root .PHONY: run run: build - docker run --rm --init --read-only --mount type=tmpfs,destination=/tmp -v "$$(readlink -f ../../):/source:ro" --workdir=/source --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target $(IMAGE_NAME) + docker run --rm --init -t --read-only --mount type=tmpfs,destination=/tmp -v "$$(readlink -f ../../):/source:ro" --workdir=/source --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target $(IMAGE_NAME) .PHONY: shell shell: build diff --git a/scripts/build_all_feature_flag_combinations.bash b/scripts/build_all_feature_flag_combinations.bash new file mode 100755 index 00000000..b8d90841 --- /dev/null +++ b/scripts/build_all_feature_flag_combinations.bash @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# +# Time running a single parse without invoking a compare with emacs. +set -euo pipefail +IFS=$'\n\t' +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +: ${PROFILE:="debug"} + +############## Setup ######################### + +function cleanup { + for f in "${folders[@]}"; do + log "Deleting $f" + rm -rf "$f" + done +} +folders=() +for sig in EXIT INT QUIT HUP TERM; do + trap "set +e; cleanup" "$sig" +done + +function die { + local status_code="$1" + shift + (>&2 echo "${@}") + exit "$status_code" +} + +function log { + (>&2 echo "${@}") +} + +############## Program ######################### + +function main { + if [ "$#" -gt 0 ]; then + export CARGO_TARGET_DIR="$1" + else + local work_directory=$(mktemp -d -t 'organic.XXXXXX') + folders+=("$work_directory") + export CARGO_TARGET_DIR="$work_directory" + fi + local features=(compare foreign_document_test tracing event_count wasm wasm_test) + ENABLED_FEATURES= for_each_combination "${features[@]}" +} + +function for_each_combination { + local additional_flags=() + if [ "$PROFILE" = "dev" ] || [ "$PROFILE" = "debug" ]; then + PROFILE="debug" + else + additional_flags+=(--profile "$PROFILE") + fi + + + local flag=$1 + shift + + if [ "$#" -gt 0 ]; then + ENABLED_FEATURES="$ENABLED_FEATURES" for_each_combination "${@}" + elif [ -z "$ENABLED_FEATURES" ]; then + (cd "$DIR/../" && printf "\n\n\n========== no features ==========\n\n\n" && set -x && cargo build "${additional_flags[@]}" --no-default-features) + else + (cd "$DIR/../" && printf "\n\n\n========== %s ==========\n\n\n" "${ENABLED_FEATURES:1}" && set -x && cargo build "${additional_flags[@]}" --no-default-features --features "${ENABLED_FEATURES:1}") + fi + + ENABLED_FEATURES="$ENABLED_FEATURES,$flag" + if [ "$#" -gt 0 ]; then + ENABLED_FEATURES="$ENABLED_FEATURES" for_each_combination "${@}" + else + (cd "$DIR/../" && printf "\n\n\n========== %s ==========\n\n\n" "${ENABLED_FEATURES:1}" && set -x && cargo build "${additional_flags[@]}" --no-default-features --features "${ENABLED_FEATURES:1}") + fi +} + +main "${@}" diff --git a/scripts/run_docker_wasm_compare.bash b/scripts/run_docker_wasm_compare.bash new file mode 100755 index 00000000..500b25c1 --- /dev/null +++ b/scripts/run_docker_wasm_compare.bash @@ -0,0 +1,111 @@ +#!/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 +: ${NO_COLOR:=""} # Set to anything to disable color output +: ${PROFILE:="debug"} + +REALPATH=$(command -v uu-realpath || command -v realpath) +MAKE=$(command -v gmake || command -v make) + +############## Setup ######################### + +function die { + local status_code="$1" + shift + (>&2 echo "${@}") + exit "$status_code" +} + +function log { + (>&2 echo "${@}") +} + +############## Program ######################### + +function main { + build_container + launch_container "${@}" +} + +function build_container { + $MAKE -C "$DIR/../docker/organic_test" +} + +function launch_container { + local additional_flags=() + local features=(wasm_test) + + if [ "$NO_COLOR" != "" ]; then + additional_flags+=(--env "NO_COLOR=$NO_COLOR") + 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) + features+=(tracing) + fi + + if [ "$SHELL" != "YES" ]; then + additional_flags+=(--read-only) + else + additional_flags+=(-t) + fi + + if [ "$BACKTRACE" = "YES" ]; then + additional_flags+=(--env RUST_BACKTRACE=full) + fi + + if [ "$SHELL" = "YES" ]; then + exec docker run "${additional_flags[@]}" --init --rm -i --mount type=tmpfs,destination=/tmp -v "/:/input:ro" -v "$($REALPATH "$DIR/../"):/source:ro" --mount source=cargo-cache,target=/usr/local/cargo/registry --mount source=rust-cache,target=/target --env CARGO_TARGET_DIR=/target -w /source --entrypoint "" organic-test /bin/sh + fi + + local features_joined + features_joined=$(IFS=","; echo "${features[*]}") + + local build_flags=() + if [ "$PROFILE" = "dev" ] || [ "$PROFILE" = "debug" ]; then + PROFILE="debug" + else + build_flags+=(--profile "$PROFILE") + fi + + + if [ $# -gt 0 ]; then + # If we passed in args, we need to forward them along + for path in "${@}"; do + local full_path + full_path=$($REALPATH "$path") + init_script=$(cat < wasm_bindgen::JsValue { + organic::wasm_cli::parse_org(org_contents) +} + +fn main() -> Result<(), Box> { + Ok(()) +} diff --git a/src/bin_wasm_test.rs b/src/bin_wasm_test.rs new file mode 100644 index 00000000..5bc41f32 --- /dev/null +++ b/src/bin_wasm_test.rs @@ -0,0 +1,62 @@ +#![feature(exact_size_is_empty)] +#![feature(exit_status_error)] +use std::io::Read; + +use organic::wasm_test::wasm_run_anonymous_compare; +use organic::wasm_test::wasm_run_compare_on_file; + +#[cfg(feature = "tracing")] +use crate::init_tracing::init_telemetry; +#[cfg(feature = "tracing")] +use crate::init_tracing::shutdown_telemetry; +#[cfg(feature = "tracing")] +mod init_tracing; + +#[cfg(not(feature = "tracing"))] +fn main() -> Result<(), Box> { + let rt = tokio::runtime::Runtime::new()?; + rt.block_on(async { + let main_body_result = main_body().await; + main_body_result + }) +} + +#[cfg(feature = "tracing")] +fn main() -> Result<(), Box> { + let rt = tokio::runtime::Runtime::new()?; + rt.block_on(async { + init_telemetry()?; + let main_body_result = main_body().await; + shutdown_telemetry()?; + main_body_result + }) +} + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +async fn main_body() -> Result<(), Box> { + let args = std::env::args().skip(1); + if args.is_empty() { + let org_contents = read_stdin_to_string()?; + if wasm_run_anonymous_compare(org_contents).await? { + } else { + Err("Diff results do not match.")?; + } + Ok(()) + } else { + for arg in args { + if wasm_run_compare_on_file(arg).await? { + } else { + Err("Diff results do not match.")?; + } + } + Ok(()) + } +} + +fn read_stdin_to_string() -> Result> { + let mut stdin_contents = String::new(); + std::io::stdin() + .lock() + .read_to_string(&mut stdin_contents)?; + Ok(stdin_contents) +} diff --git a/src/compare/compare.rs b/src/compare/compare.rs index 756dcb7b..5f16abb0 100644 --- a/src/compare/compare.rs +++ b/src/compare/compare.rs @@ -1,16 +1,16 @@ use std::path::Path; use crate::compare::diff::compare_document; -use crate::compare::diff::DiffResult; -use crate::compare::parse::emacs_parse_anonymous_org_document; -use crate::compare::parse::emacs_parse_file_org_document; -use crate::compare::parse::get_emacs_version; -use crate::compare::parse::get_org_mode_version; -use crate::compare::sexp::sexp; use crate::context::GlobalSettings; use crate::context::LocalFileAccessInterface; use crate::parser::parse_file_with_settings; use crate::parser::parse_with_settings; +use crate::util::cli::emacs_parse_anonymous_org_document; +use crate::util::cli::emacs_parse_file_org_document; +use crate::util::cli::print_versions; +use crate::util::elisp::sexp; +use crate::util::terminal::foreground_color; +use crate::util::terminal::reset_color; pub async fn run_anonymous_compare>( org_contents: P, @@ -68,8 +68,8 @@ pub async fn run_anonymous_compare_with_settings<'g, 's, P: AsRef>( } else if !silent { println!( "{color}Entire document passes.{reset}", - color = DiffResult::foreground_color(0, 255, 0), - reset = DiffResult::reset_color(), + color = foreground_color(0, 255, 0), + reset = reset_color(), ); } @@ -121,19 +121,10 @@ pub async fn run_compare_on_file_with_settings<'g, 's, P: AsRef>( } else if !silent { println!( "{color}Entire document passes.{reset}", - color = DiffResult::foreground_color(0, 255, 0), - reset = DiffResult::reset_color(), + color = foreground_color(0, 255, 0), + reset = reset_color(), ); } Ok(true) } - -async fn print_versions() -> Result<(), Box> { - eprintln!("Using emacs version: {}", get_emacs_version().await?.trim()); - eprintln!( - "Using org-mode version: {}", - get_org_mode_version().await?.trim() - ); - Ok(()) -} diff --git a/src/compare/compare_field.rs b/src/compare/compare_field.rs index 9ab611f9..a67ca35d 100644 --- a/src/compare/compare_field.rs +++ b/src/compare/compare_field.rs @@ -9,8 +9,6 @@ use super::diff::artificial_owned_diff_scope; use super::diff::compare_ast_node; use super::diff::DiffEntry; use super::diff::DiffStatus; -use super::sexp::unquote; -use super::sexp::Token; use super::util::get_property; use super::util::get_property_numeric; use super::util::get_property_quoted_string; @@ -20,6 +18,8 @@ use crate::types::CharOffsetInLine; use crate::types::LineNumber; use crate::types::RetainLabels; use crate::types::SwitchNumberLines; +use crate::util::elisp::unquote; +use crate::util::elisp::Token; #[derive(Debug)] pub(crate) enum EmacsField<'s> { diff --git a/src/compare/diff.rs b/src/compare/diff.rs index 6475a052..e9a6e569 100644 --- a/src/compare/diff.rs +++ b/src/compare/diff.rs @@ -16,10 +16,6 @@ use super::compare_field::compare_property_retain_labels; use super::compare_field::compare_property_set_of_quoted_string; use super::compare_field::compare_property_single_ast_node; use super::compare_field::compare_property_unquoted_atom; -use super::elisp_fact::ElispFact; -use super::elisp_fact::GetElispFact; -use super::sexp::unquote; -use super::sexp::Token; use super::util::affiliated_keywords_names; use super::util::assert_no_children; use super::util::compare_additional_properties; @@ -109,6 +105,12 @@ use crate::types::Verbatim; use crate::types::VerseBlock; use crate::types::WarningDelayType; use crate::types::Year; +use crate::util::elisp::unquote; +use crate::util::elisp::Token; +use crate::util::elisp_fact::ElispFact; +use crate::util::elisp_fact::GetElispFact; +use crate::util::terminal::foreground_color; +use crate::util::terminal::reset_color; #[derive(Debug)] pub enum DiffEntry<'b, 's> { @@ -200,21 +202,21 @@ impl<'b, 's> DiffResult<'b, 's> { if self.has_bad_children() { format!( "{color}BADCHILD{reset}", - color = DiffResult::foreground_color(255, 255, 0), - reset = DiffResult::reset_color(), + color = foreground_color(255, 255, 0), + reset = reset_color(), ) } else { format!( "{color}GOOD{reset}", - color = DiffResult::foreground_color(0, 255, 0), - reset = DiffResult::reset_color(), + color = foreground_color(0, 255, 0), + reset = reset_color(), ) } } DiffStatus::Bad => format!( "{color}BAD{reset}", - color = DiffResult::foreground_color(255, 0, 0), - reset = DiffResult::reset_color(), + color = foreground_color(255, 0, 0), + reset = reset_color(), ), } }; @@ -239,45 +241,6 @@ impl<'b, 's> DiffResult<'b, 's> { .iter() .any(|child| child.is_immediately_bad() || child.has_bad_children()) } - - pub(crate) fn foreground_color(red: u8, green: u8, blue: u8) -> String { - if DiffResult::should_use_color() { - format!( - "\x1b[38;2;{red};{green};{blue}m", - red = red, - green = green, - blue = blue - ) - } else { - String::new() - } - } - - #[allow(dead_code)] - pub(crate) fn background_color(red: u8, green: u8, blue: u8) -> String { - if DiffResult::should_use_color() { - format!( - "\x1b[48;2;{red};{green};{blue}m", - red = red, - green = green, - blue = blue - ) - } else { - String::new() - } - } - - pub(crate) fn reset_color() -> &'static str { - if DiffResult::should_use_color() { - "\x1b[0m" - } else { - "" - } - } - - fn should_use_color() -> bool { - !std::env::var("NO_COLOR").is_ok_and(|val| !val.is_empty()) - } } impl<'b, 's> DiffLayer<'b, 's> { @@ -295,14 +258,14 @@ impl<'b, 's> DiffLayer<'b, 's> { let status_text = if self.has_bad_children() { format!( "{color}BADCHILD{reset}", - color = DiffResult::foreground_color(255, 255, 0), - reset = DiffResult::reset_color(), + color = foreground_color(255, 255, 0), + reset = reset_color(), ) } else { format!( "{color}GOOD{reset}", - color = DiffResult::foreground_color(0, 255, 0), - reset = DiffResult::reset_color(), + color = foreground_color(0, 255, 0), + reset = reset_color(), ) }; println!( diff --git a/src/compare/mod.rs b/src/compare/mod.rs index 5a2c30ef..f5f4ee56 100644 --- a/src/compare/mod.rs +++ b/src/compare/mod.rs @@ -2,10 +2,7 @@ mod compare; mod compare_field; mod diff; -mod elisp_fact; mod macros; -mod parse; -mod sexp; mod util; pub use compare::run_anonymous_compare; pub use compare::run_anonymous_compare_with_settings; diff --git a/src/compare/util.rs b/src/compare/util.rs index 9029b243..32e8de6c 100644 --- a/src/compare/util.rs +++ b/src/compare/util.rs @@ -8,14 +8,15 @@ use super::compare_field::compare_property_quoted_string; use super::compare_field::ComparePropertiesResult; use super::diff::DiffEntry; use super::diff::DiffStatus; -use super::elisp_fact::GetElispFact; -use super::sexp::Token; use crate::compare::diff::compare_ast_node; -use crate::compare::sexp::unquote; use crate::types::AffiliatedKeywordValue; use crate::types::AstNode; use crate::types::GetAffiliatedKeywords; use crate::types::StandardProperties; +use crate::util::elisp::get_emacs_standard_properties; +use crate::util::elisp::unquote; +use crate::util::elisp::Token; +use crate::util::elisp_fact::GetElispFact; /// Check if the child string slice is a slice of the parent string slice. fn is_slice_of(parent: &str, child: &str) -> bool { @@ -145,80 +146,6 @@ fn assert_post_blank<'b, 's, S: StandardProperties<'s> + ?Sized>( Ok(()) } -struct EmacsStandardProperties { - begin: Option, - #[allow(dead_code)] - post_affiliated: Option, - #[allow(dead_code)] - contents_begin: Option, - #[allow(dead_code)] - contents_end: Option, - end: Option, - #[allow(dead_code)] - post_blank: Option, -} - -fn get_emacs_standard_properties( - emacs: &Token<'_>, -) -> Result> { - let children = emacs.as_list()?; - let attributes_child = children.get(1).ok_or("Should have an attributes child.")?; - let attributes_map = attributes_child.as_map()?; - let standard_properties = attributes_map.get(":standard-properties"); - Ok(if standard_properties.is_some() { - let mut std_props = standard_properties - .expect("if statement proves its Some") - .as_vector()? - .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())?; - EmacsStandardProperties { - begin, - post_affiliated, - contents_begin, - contents_end, - end, - post_blank, - } - } else { - let begin = maybe_token_to_usize(attributes_map.get(":begin").copied())?; - let end = maybe_token_to_usize(attributes_map.get(":end").copied())?; - let contents_begin = maybe_token_to_usize(attributes_map.get(":contents-begin").copied())?; - let contents_end = maybe_token_to_usize(attributes_map.get(":contents-end").copied())?; - let post_blank = maybe_token_to_usize(attributes_map.get(":post-blank").copied())?; - let post_affiliated = - maybe_token_to_usize(attributes_map.get(":post-affiliated").copied())?; - EmacsStandardProperties { - begin, - post_affiliated, - contents_begin, - contents_end, - end, - post_blank, - } - }) -} - -fn maybe_token_to_usize( - token: Option<&Token<'_>>, -) -> Result, Box> { - Ok(token - .map(|token| token.as_atom()) - .map_or(Ok(None), |r| r.map(Some))? - .and_then(|val| { - if val == "nil" { - None - } else { - Some(val.parse::()) - } - }) - .map_or(Ok(None), |r| r.map(Some))?) -} - /// Get a named property from the emacs token. /// /// Returns Ok(None) if value is nil or absent. diff --git a/src/event_count/database.rs b/src/event_count/database.rs index c9adbcfc..3925260d 100644 --- a/src/event_count/database.rs +++ b/src/event_count/database.rs @@ -24,7 +24,7 @@ pub(crate) fn record_event(event_type: EventType, input: OrgSource<'_>) { *db.entry(key).or_insert(0) += 1; } -pub fn report(original_document: &str) { +pub(crate) fn report(original_document: &str) { let mut db = GLOBAL_DATA.lock().unwrap(); let db = db.get_or_insert_with(HashMap::new); let mut results: Vec<_> = db.iter().collect(); diff --git a/src/event_count/mod.rs b/src/event_count/mod.rs index 8987f8ba..687fef33 100644 --- a/src/event_count/mod.rs +++ b/src/event_count/mod.rs @@ -2,5 +2,5 @@ mod database; mod event_type; pub(crate) use database::record_event; -pub use database::report; +pub(crate) use database::report; pub(crate) use event_type::EventType; diff --git a/src/lib.rs b/src/lib.rs index 6795061f..44ed0763 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ #![feature(path_file_prefix)] #![feature(is_sorted)] #![feature(test)] +#![feature(iter_intersperse)] +#![feature(exact_size_is_empty)] // TODO: #![warn(missing_docs)] #![allow(clippy::bool_assert_comparison)] // Sometimes you want the long form because its easier to see at a glance. @@ -10,11 +12,20 @@ extern crate test; #[cfg(feature = "compare")] pub mod compare; +pub mod parse_cli; +#[cfg(any(feature = "compare", feature = "wasm", feature = "wasm_test"))] +mod util; +#[cfg(any(feature = "wasm", feature = "wasm_test"))] +mod wasm; +#[cfg(any(feature = "wasm", feature = "wasm_test"))] +pub mod wasm_cli; +#[cfg(feature = "wasm_test")] +pub mod wasm_test; mod context; mod error; #[cfg(feature = "event_count")] -pub mod event_count; +mod event_count; mod iter; pub mod parser; pub mod types; diff --git a/src/main.rs b/src/main.rs index ecf67c1a..116136f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,4 @@ -#![feature(round_char_boundary)] -#![feature(exact_size_is_empty)] -use std::io::Read; -use std::path::Path; - -use ::organic::parser::parse; -use organic::parser::parse_with_settings; -use organic::settings::GlobalSettings; -use organic::settings::LocalFileAccessInterface; +use organic::parse_cli::main_body; #[cfg(feature = "tracing")] use crate::init_tracing::init_telemetry; @@ -30,55 +22,3 @@ fn main() -> Result<(), Box> { main_body_result }) } - -#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] -fn main_body() -> Result<(), Box> { - let args = std::env::args().skip(1); - if args.is_empty() { - let org_contents = read_stdin_to_string()?; - run_anonymous_parse(org_contents) - } else { - for arg in args { - run_parse_on_file(arg)? - } - Ok(()) - } -} - -fn read_stdin_to_string() -> Result> { - let mut stdin_contents = String::new(); - std::io::stdin() - .lock() - .read_to_string(&mut stdin_contents)?; - Ok(stdin_contents) -} - -fn run_anonymous_parse>(org_contents: P) -> Result<(), Box> { - let org_contents = org_contents.as_ref(); - let rust_parsed = parse(org_contents)?; - println!("{:#?}", rust_parsed); - #[cfg(feature = "event_count")] - organic::event_count::report(org_contents); - Ok(()) -} - -fn run_parse_on_file>(org_path: P) -> Result<(), Box> { - let org_path = org_path.as_ref(); - let parent_directory = org_path - .parent() - .ok_or("Should be contained inside a directory.")?; - let org_contents = std::fs::read_to_string(org_path)?; - let org_contents = org_contents.as_str(); - let file_access_interface = LocalFileAccessInterface { - working_directory: Some(parent_directory.to_path_buf()), - }; - let global_settings = GlobalSettings { - file_access: &file_access_interface, - ..Default::default() - }; - let rust_parsed = parse_with_settings(org_contents, &global_settings)?; - println!("{:#?}", rust_parsed); - #[cfg(feature = "event_count")] - organic::event_count::report(org_contents); - Ok(()) -} diff --git a/src/parse_cli/mod.rs b/src/parse_cli/mod.rs new file mode 100644 index 00000000..c3bc9744 --- /dev/null +++ b/src/parse_cli/mod.rs @@ -0,0 +1,59 @@ +use std::io::Read; +use std::path::Path; + +use crate::parser::parse; +use crate::parser::parse_with_settings; +use crate::settings::GlobalSettings; +use crate::settings::LocalFileAccessInterface; + +#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))] +pub fn main_body() -> Result<(), Box> { + let args = std::env::args().skip(1); + if args.is_empty() { + let org_contents = read_stdin_to_string()?; + run_anonymous_parse(org_contents) + } else { + for arg in args { + run_parse_on_file(arg)? + } + Ok(()) + } +} + +fn read_stdin_to_string() -> Result> { + let mut stdin_contents = String::new(); + std::io::stdin() + .lock() + .read_to_string(&mut stdin_contents)?; + Ok(stdin_contents) +} + +fn run_anonymous_parse>(org_contents: P) -> Result<(), Box> { + let org_contents = org_contents.as_ref(); + let rust_parsed = parse(org_contents)?; + println!("{:#?}", rust_parsed); + #[cfg(feature = "event_count")] + crate::event_count::report(org_contents); + Ok(()) +} + +fn run_parse_on_file>(org_path: P) -> Result<(), Box> { + let org_path = org_path.as_ref(); + let parent_directory = org_path + .parent() + .ok_or("Should be contained inside a directory.")?; + let org_contents = std::fs::read_to_string(org_path)?; + let org_contents = org_contents.as_str(); + let file_access_interface = LocalFileAccessInterface { + working_directory: Some(parent_directory.to_path_buf()), + }; + let global_settings = GlobalSettings { + file_access: &file_access_interface, + ..Default::default() + }; + let rust_parsed = parse_with_settings(org_contents, &global_settings)?; + println!("{:#?}", rust_parsed); + #[cfg(feature = "event_count")] + crate::event_count::report(org_contents); + Ok(()) +} diff --git a/src/types/object.rs b/src/types/object.rs index b2f5f33c..f8afa55c 100644 --- a/src/types/object.rs +++ b/src/types/object.rs @@ -332,19 +332,19 @@ pub type HourInner = u8; pub type MinuteInner = u8; #[derive(Debug, Clone)] -pub struct Year(YearInner); +pub struct Year(pub YearInner); #[derive(Debug, Clone)] -pub struct Month(MonthInner); +pub struct Month(pub MonthInner); #[derive(Debug, Clone)] -pub struct DayOfMonth(DayOfMonthInner); +pub struct DayOfMonth(pub DayOfMonthInner); #[derive(Debug, Clone)] -pub struct Hour(HourInner); +pub struct Hour(pub HourInner); #[derive(Debug, Clone)] -pub struct Minute(MinuteInner); +pub struct Minute(pub MinuteInner); impl Year { // TODO: Make a real error type instead of a boxed any error. diff --git a/src/compare/parse.rs b/src/util/cli.rs similarity index 91% rename from src/compare/parse.rs rename to src/util/cli.rs index 8b28fc3e..3591dff3 100644 --- a/src/compare/parse.rs +++ b/src/util/cli.rs @@ -2,28 +2,53 @@ use std::path::Path; use tokio::process::Command; -use crate::context::HeadlineLevelFilter; use crate::settings::GlobalSettings; +use crate::settings::HeadlineLevelFilter; -/// Generate elisp to configure org-mode parsing settings -/// -/// Currently only org-list-allow-alphabetical is supported. -fn global_settings_elisp(global_settings: &GlobalSettings) -> String { - // This string concatenation is wildly inefficient but its only called in tests 🤷. - let mut ret = "".to_owned(); - if global_settings.list_allow_alphabetical { - ret += "(setq org-list-allow-alphabetical t)\n" - } - if global_settings.tab_width != crate::settings::DEFAULT_TAB_WIDTH { - ret += format!("(setq-default tab-width {})", global_settings.tab_width).as_str(); - } - if global_settings.odd_levels_only != HeadlineLevelFilter::default() { - ret += match global_settings.odd_levels_only { - HeadlineLevelFilter::Odd => "(setq org-odd-levels-only t)\n", - HeadlineLevelFilter::OddEven => "(setq org-odd-levels-only nil)\n", - }; - } - ret +pub async fn print_versions() -> Result<(), Box> { + eprintln!("Using emacs version: {}", get_emacs_version().await?.trim()); + eprintln!( + "Using org-mode version: {}", + get_org_mode_version().await?.trim() + ); + Ok(()) +} + +pub(crate) async fn get_emacs_version() -> Result> { + let elisp_script = r#"(progn + (message "%s" (version)) +)"#; + let mut cmd = Command::new("emacs"); + let cmd = cmd + .arg("-q") + .arg("--no-site-file") + .arg("--no-splash") + .arg("--batch") + .arg("--eval") + .arg(elisp_script); + + let out = cmd.output().await?; + out.status.exit_ok()?; + Ok(String::from_utf8(out.stderr)?) +} + +pub(crate) async fn get_org_mode_version() -> Result> { + let elisp_script = r#"(progn + (org-mode) + (message "%s" (org-version nil t nil)) +)"#; + let mut cmd = Command::new("emacs"); + let cmd = cmd + .arg("-q") + .arg("--no-site-file") + .arg("--no-splash") + .arg("--batch") + .arg("--eval") + .arg(elisp_script); + + let out = cmd.output().await?; + out.status.exit_ok()?; + Ok(String::from_utf8(out.stderr)?) } pub(crate) async fn emacs_parse_anonymous_org_document<'g, 's, C>( @@ -144,39 +169,23 @@ where output } -pub async fn get_emacs_version() -> Result> { - let elisp_script = r#"(progn - (message "%s" (version)) -)"#; - let mut cmd = Command::new("emacs"); - let cmd = cmd - .arg("-q") - .arg("--no-site-file") - .arg("--no-splash") - .arg("--batch") - .arg("--eval") - .arg(elisp_script); - - let out = cmd.output().await?; - out.status.exit_ok()?; - Ok(String::from_utf8(out.stderr)?) -} - -pub async fn get_org_mode_version() -> Result> { - let elisp_script = r#"(progn - (org-mode) - (message "%s" (org-version nil t nil)) -)"#; - let mut cmd = Command::new("emacs"); - let cmd = cmd - .arg("-q") - .arg("--no-site-file") - .arg("--no-splash") - .arg("--batch") - .arg("--eval") - .arg(elisp_script); - - let out = cmd.output().await?; - out.status.exit_ok()?; - Ok(String::from_utf8(out.stderr)?) +/// Generate elisp to configure org-mode parsing settings +/// +/// Currently only org-list-allow-alphabetical is supported. +fn global_settings_elisp(global_settings: &GlobalSettings) -> String { + // This string concatenation is wildly inefficient but its only called in tests 🤷. + let mut ret = "".to_owned(); + if global_settings.list_allow_alphabetical { + ret += "(setq org-list-allow-alphabetical t)\n" + } + if global_settings.tab_width != crate::settings::DEFAULT_TAB_WIDTH { + ret += format!("(setq-default tab-width {})", global_settings.tab_width).as_str(); + } + if global_settings.odd_levels_only != HeadlineLevelFilter::default() { + ret += match global_settings.odd_levels_only { + HeadlineLevelFilter::Odd => "(setq org-odd-levels-only t)\n", + HeadlineLevelFilter::OddEven => "(setq org-odd-levels-only nil)\n", + }; + } + ret } diff --git a/src/util/elisp/mod.rs b/src/util/elisp/mod.rs new file mode 100644 index 00000000..1fac0cb5 --- /dev/null +++ b/src/util/elisp/mod.rs @@ -0,0 +1,14 @@ +mod sexp; +mod util; + +pub use sexp::sexp; +pub(crate) use sexp::unquote; +#[cfg(feature = "wasm_test")] +pub(crate) use sexp::TextWithProperties; +pub use sexp::Token; +#[cfg(feature = "compare")] +pub(crate) use util::get_emacs_standard_properties; +#[cfg(feature = "wasm_test")] +pub(crate) use util::maybe_token_to_usize; +#[cfg(feature = "wasm_test")] +pub(crate) use util::EmacsStandardProperties; diff --git a/src/compare/sexp.rs b/src/util/elisp/sexp.rs similarity index 99% rename from src/compare/sexp.rs rename to src/util/elisp/sexp.rs index a45816a4..cd72fd33 100644 --- a/src/compare/sexp.rs +++ b/src/util/elisp/sexp.rs @@ -61,6 +61,7 @@ impl<'s> Token<'s> { }?) } + #[cfg(feature = "compare")] pub(crate) fn as_text<'p>( &'p self, ) -> Result<&'p TextWithProperties<'s>, Box> { diff --git a/src/util/elisp/util.rs b/src/util/elisp/util.rs new file mode 100644 index 00000000..2978d0fb --- /dev/null +++ b/src/util/elisp/util.rs @@ -0,0 +1,76 @@ +use super::Token; + +pub(crate) fn maybe_token_to_usize( + token: Option<&Token<'_>>, +) -> Result, Box> { + Ok(token + .map(|token| token.as_atom()) + .map_or(Ok(None), |r| r.map(Some))? + .and_then(|val| { + if val == "nil" { + None + } else { + Some(val.parse::()) + } + }) + .map_or(Ok(None), |r| r.map(Some))?) +} + +pub(crate) struct EmacsStandardProperties { + pub(crate) begin: Option, + #[allow(dead_code)] + pub(crate) post_affiliated: Option, + #[allow(dead_code)] + pub(crate) contents_begin: Option, + #[allow(dead_code)] + pub(crate) contents_end: Option, + pub(crate) end: Option, + #[allow(dead_code)] + pub(crate) post_blank: Option, +} + +#[cfg(feature = "compare")] +pub(crate) fn get_emacs_standard_properties( + emacs: &Token<'_>, +) -> Result> { + let children = emacs.as_list()?; + let attributes_child = children.get(1).ok_or("Should have an attributes child.")?; + let attributes_map = attributes_child.as_map()?; + let standard_properties = attributes_map.get(":standard-properties"); + Ok(if standard_properties.is_some() { + let mut std_props = standard_properties + .expect("if statement proves its Some") + .as_vector()? + .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())?; + EmacsStandardProperties { + begin, + post_affiliated, + contents_begin, + contents_end, + end, + post_blank, + } + } else { + let begin = maybe_token_to_usize(attributes_map.get(":begin").copied())?; + let end = maybe_token_to_usize(attributes_map.get(":end").copied())?; + let contents_begin = maybe_token_to_usize(attributes_map.get(":contents-begin").copied())?; + let contents_end = maybe_token_to_usize(attributes_map.get(":contents-end").copied())?; + let post_blank = maybe_token_to_usize(attributes_map.get(":post-blank").copied())?; + let post_affiliated = + maybe_token_to_usize(attributes_map.get(":post-affiliated").copied())?; + EmacsStandardProperties { + begin, + post_affiliated, + contents_begin, + contents_end, + end, + post_blank, + } + }) +} diff --git a/src/compare/elisp_fact.rs b/src/util/elisp_fact.rs similarity index 100% rename from src/compare/elisp_fact.rs rename to src/util/elisp_fact.rs diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 00000000..34f1fea5 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,8 @@ +#[cfg(any(feature = "compare", feature = "wasm_test"))] +pub mod cli; +#[cfg(any(feature = "compare", feature = "wasm_test"))] +pub mod elisp; +#[cfg(any(feature = "compare", feature = "wasm", feature = "wasm_test"))] +pub mod elisp_fact; +#[cfg(any(feature = "compare", feature = "wasm_test"))] +pub mod terminal; diff --git a/src/util/terminal.rs b/src/util/terminal.rs new file mode 100644 index 00000000..836c7870 --- /dev/null +++ b/src/util/terminal.rs @@ -0,0 +1,42 @@ +use std::borrow::Cow; + +fn should_use_color() -> bool { + !std::env::var("NO_COLOR").is_ok_and(|val| !val.is_empty()) +} + +pub(crate) fn foreground_color(red: u8, green: u8, blue: u8) -> Cow<'static, str> { + if should_use_color() { + format!( + "\x1b[38;2;{red};{green};{blue}m", + red = red, + green = green, + blue = blue + ) + .into() + } else { + Cow::from("") + } +} + +#[allow(dead_code)] +pub(crate) fn background_color(red: u8, green: u8, blue: u8) -> Cow<'static, str> { + if should_use_color() { + format!( + "\x1b[48;2;{red};{green};{blue}m", + red = red, + green = green, + blue = blue + ) + .into() + } else { + Cow::from("") + } +} + +pub(crate) fn reset_color() -> &'static str { + if should_use_color() { + "\x1b[0m" + } else { + "" + } +} diff --git a/src/wasm/additional_property.rs b/src/wasm/additional_property.rs new file mode 100644 index 00000000..051d7160 --- /dev/null +++ b/src/wasm/additional_property.rs @@ -0,0 +1,95 @@ +use std::collections::HashMap; + +use serde::Deserialize; +use serde::Serialize; + +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::WasmAstNode; +use crate::types::AffiliatedKeywordValue; +use crate::types::AffiliatedKeywords; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AdditionalPropertyValue { + SingleString(String), + ListOfStrings(Vec), + OptionalPair { + optval: Option, + val: String, + }, + ObjectTree { + #[serde(rename = "object-tree")] + object_tree: Vec<(Option>, Vec)>, + }, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct AdditionalProperties { + #[serde(flatten)] + pub(crate) properties: HashMap, +} + +to_wasm!( + AdditionalProperties, + AffiliatedKeywords<'s>, + original, + wasm_context, + { + let mut additional_properties = AdditionalProperties::default(); + for (name, val) in original.keywords.iter() { + let converted_val = match val { + AffiliatedKeywordValue::SingleString(val) => { + AdditionalPropertyValue::SingleString((*val).to_owned()) + } + AffiliatedKeywordValue::ListOfStrings(val) => { + AdditionalPropertyValue::ListOfStrings( + val.iter().map(|s| (*s).to_owned()).collect(), + ) + } + AffiliatedKeywordValue::OptionalPair { optval, val } => { + AdditionalPropertyValue::OptionalPair { + optval: optval.map(|s| (*s).to_owned()), + val: (*val).to_owned(), + } + } + AffiliatedKeywordValue::ObjectTree(val) => { + let mut ret = Vec::with_capacity(val.len()); + + for (optval, value) in val { + let converted_optval = if let Some(optval) = optval { + Some( + optval + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?, + ) + } else { + None + }; + let converted_value = value + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + ret.push((converted_optval, converted_value)); + } + + AdditionalPropertyValue::ObjectTree { object_tree: ret } + } + }; + additional_properties + .properties + .insert(name.clone(), converted_val); + } + Ok(additional_properties) + } +); diff --git a/src/wasm/angle_link.rs b/src/wasm/angle_link.rs new file mode 100644 index 00000000..31da1e78 --- /dev/null +++ b/src/wasm/angle_link.rs @@ -0,0 +1,54 @@ +use std::borrow::Cow; + +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::AngleLink; +use crate::types::LinkType; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "format")] +#[serde(rename = "angle")] +pub struct WasmAngleLink { + #[serde(rename = "type")] + pub(crate) link_type: String, + pub(crate) path: String, + #[serde(rename = "raw-link")] + pub(crate) raw_link: String, + pub(crate) application: Option, + #[serde(rename = "search-option")] + pub(crate) search_option: Option, +} + +to_wasm!( + WasmAngleLink, + AngleLink<'s>, + original, + wasm_context, + { WasmAstNode::AngleLink(original) }, + { "link".into() }, + { + Ok(( + Vec::new(), + WasmAngleLink { + link_type: match &original.link_type { + LinkType::File => "file".to_owned(), + LinkType::Protocol(protocol) => protocol.clone().into_owned(), + LinkType::Id => "id".to_owned(), + LinkType::CustomId => "custom-id".to_owned(), + LinkType::CodeRef => "coderef".to_owned(), + LinkType::Fuzzy => "fuzzy".to_owned(), + }, + path: original.get_path().into_owned(), + raw_link: original.raw_link.to_owned(), + application: original.application.map(|c| c.to_owned()), + search_option: original.get_search_option().map(Cow::into_owned), + }, + )) + } +); diff --git a/src/wasm/ast_node.rs b/src/wasm/ast_node.rs new file mode 100644 index 00000000..76b7f35b --- /dev/null +++ b/src/wasm/ast_node.rs @@ -0,0 +1,142 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::angle_link::WasmAngleLink; +use super::babel_call::WasmBabelCall; +use super::bold::WasmBold; +use super::center_block::WasmCenterBlock; +use super::citation::WasmCitation; +use super::citation_reference::WasmCitationReference; +use super::clock::WasmClock; +use super::code::WasmCode; +use super::comment::WasmComment; +use super::comment_block::WasmCommentBlock; +use super::diary_sexp::WasmDiarySexp; +use super::document::WasmDocument; +use super::drawer::WasmDrawer; +use super::dynamic_block::WasmDynamicBlock; +use super::entity::WasmEntity; +use super::example_block::WasmExampleBlock; +use super::export_block::WasmExportBlock; +use super::export_snippet::WasmExportSnippet; +use super::fixed_width_area::WasmFixedWidthArea; +use super::footnote_definition::WasmFootnoteDefinition; +use super::footnote_reference::WasmFootnoteReference; +use super::headline::WasmHeadline; +use super::horizontal_rule::WasmHorizontalRule; +use super::inline_babel_call::WasmInlineBabelCall; +use super::inline_source_block::WasmInlineSourceBlock; +use super::italic::WasmItalic; +use super::keyword::WasmKeyword; +use super::latex_environment::WasmLatexEnvironment; +use super::latex_fragment::WasmLatexFragment; +use super::line_break::WasmLineBreak; +use super::node_property::WasmNodeProperty; +use super::org_macro::WasmOrgMacro; +use super::paragraph::WasmParagraph; +use super::plain_link::WasmPlainLink; +use super::plain_list::WasmPlainList; +use super::plain_list_item::WasmPlainListItem; +use super::plain_text::WasmPlainText; +use super::planning::WasmPlanning; +use super::property_drawer::WasmPropertyDrawer; +use super::quote_block::WasmQuoteBlock; +use super::radio_link::WasmRadioLink; +use super::radio_target::WasmRadioTarget; +use super::regular_link::WasmRegularLink; +use super::section::WasmSection; +use super::special_block::WasmSpecialBlock; +use super::src_block::WasmSrcBlock; +use super::statistics_cookie::WasmStatisticsCookie; +use super::strike_through::WasmStrikeThrough; +use super::subscript::WasmSubscript; +use super::superscript::WasmSuperscript; +use super::table::WasmTable; +use super::table_cell::WasmTableCell; +use super::table_row::WasmTableRow; +use super::target::WasmTarget; +use super::timestamp::WasmTimestamp; +use super::underline::WasmUnderline; +use super::verbatim::WasmVerbatim; +use super::verse_block::WasmVerseBlock; +use super::WasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmAstNodeWrapper { + #[serde(rename = "ast-node")] + pub(crate) ast_node: String, + #[serde(rename = "standard-properties")] + pub(crate) standard_properties: WasmStandardProperties, + #[serde(rename = "children")] + pub(crate) children: Vec, + #[serde(rename = "properties")] + pub(crate) properties: I, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum WasmAstNode { + // Document Nodes + Document(WasmAstNodeWrapper), + Headline(WasmAstNodeWrapper), + Section(WasmAstNodeWrapper), + // Elements + Paragraph(WasmAstNodeWrapper), + PlainList(WasmAstNodeWrapper), + PlainListItem(WasmAstNodeWrapper), + CenterBlock(WasmAstNodeWrapper), + QuoteBlock(WasmAstNodeWrapper), + SpecialBlock(WasmAstNodeWrapper), + DynamicBlock(WasmAstNodeWrapper), + FootnoteDefinition(WasmAstNodeWrapper), + Comment(WasmAstNodeWrapper), + Drawer(WasmAstNodeWrapper), + PropertyDrawer(WasmAstNodeWrapper), + NodeProperty(WasmAstNodeWrapper), + Table(WasmAstNodeWrapper), + TableRow(WasmAstNodeWrapper), + VerseBlock(WasmAstNodeWrapper), + CommentBlock(WasmAstNodeWrapper), + ExampleBlock(WasmAstNodeWrapper), + ExportBlock(WasmAstNodeWrapper), + SrcBlock(WasmAstNodeWrapper), + Clock(WasmAstNodeWrapper), + DiarySexp(WasmAstNodeWrapper), + Planning(WasmAstNodeWrapper), + FixedWidthArea(WasmAstNodeWrapper), + HorizontalRule(WasmAstNodeWrapper), + Keyword(WasmAstNodeWrapper), + BabelCall(WasmAstNodeWrapper), + LatexEnvironment(WasmAstNodeWrapper), + // Objects + Bold(WasmAstNodeWrapper), + Italic(WasmAstNodeWrapper), + Underline(WasmAstNodeWrapper), + StrikeThrough(WasmAstNodeWrapper), + Code(WasmAstNodeWrapper), + Verbatim(WasmAstNodeWrapper), + PlainText(WasmAstNodeWrapper), + RegularLink(WasmAstNodeWrapper), + RadioLink(WasmAstNodeWrapper), + RadioTarget(WasmAstNodeWrapper), + PlainLink(WasmAstNodeWrapper), + AngleLink(WasmAstNodeWrapper), + OrgMacro(WasmAstNodeWrapper), + Entity(WasmAstNodeWrapper), + LatexFragment(WasmAstNodeWrapper), + ExportSnippet(WasmAstNodeWrapper), + FootnoteReference(WasmAstNodeWrapper), + Citation(WasmAstNodeWrapper), + CitationReference(WasmAstNodeWrapper), + InlineBabelCall(WasmAstNodeWrapper), + InlineSourceBlock(WasmAstNodeWrapper), + LineBreak(WasmAstNodeWrapper), + Target(WasmAstNodeWrapper), + StatisticsCookie(WasmAstNodeWrapper), + Subscript(WasmAstNodeWrapper), + Superscript(WasmAstNodeWrapper), + TableCell(WasmAstNodeWrapper), + Timestamp(WasmAstNodeWrapper), +} + +impl WasmAstNode {} diff --git a/src/wasm/babel_call.rs b/src/wasm/babel_call.rs new file mode 100644 index 00000000..cfe51a77 --- /dev/null +++ b/src/wasm/babel_call.rs @@ -0,0 +1,50 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::BabelCall; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmBabelCall { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) call: Option, + #[serde(rename = "inside-header")] + pub(crate) inside_header: Option, + pub(crate) arguments: Option, + #[serde(rename = "end-header")] + pub(crate) end_header: Option, + pub(crate) value: String, +} + +to_wasm!( + WasmBabelCall, + BabelCall<'s>, + original, + wasm_context, + { WasmAstNode::BabelCall(original) }, + { "babel-call".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmBabelCall { + additional_properties, + call: original.call.map(|s| s.to_owned()), + inside_header: original.inside_header.map(|s| s.to_owned()), + arguments: original.arguments.map(|s| s.to_owned()), + end_header: original.end_header.map(|s| s.to_owned()), + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/bold.rs b/src/wasm/bold.rs new file mode 100644 index 00000000..7af4a129 --- /dev/null +++ b/src/wasm/bold.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Bold; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmBold {} + +to_wasm!( + WasmBold, + Bold<'s>, + original, + wasm_context, + { WasmAstNode::Bold(original) }, + { "bold".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok((children, WasmBold {})) + } +); diff --git a/src/wasm/center_block.rs b/src/wasm/center_block.rs new file mode 100644 index 00000000..1418abf6 --- /dev/null +++ b/src/wasm/center_block.rs @@ -0,0 +1,48 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::CenterBlock; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmCenterBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, +} + +to_wasm!( + WasmCenterBlock, + CenterBlock<'s>, + original, + wasm_context, + { WasmAstNode::CenterBlock(original) }, + { "center-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmCenterBlock { + additional_properties, + }, + )) + } +); diff --git a/src/wasm/citation.rs b/src/wasm/citation.rs new file mode 100644 index 00000000..0a61d4d0 --- /dev/null +++ b/src/wasm/citation.rs @@ -0,0 +1,71 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Citation; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmCitation { + pub(crate) style: Option, + pub(crate) prefix: Option>, + pub(crate) suffix: Option>, +} + +to_wasm!( + WasmCitation, + Citation<'s>, + original, + wasm_context, + { WasmAstNode::Citation(original) }, + { "citation".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + let prefix = original + .prefix + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + let suffix = original + .suffix + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmCitation { + style: original.style.map(|s| s.to_owned()), + prefix: if prefix.is_empty() { + None + } else { + Some(prefix) + }, + suffix: if suffix.is_empty() { + None + } else { + Some(suffix) + }, + }, + )) + } +); diff --git a/src/wasm/citation_reference.rs b/src/wasm/citation_reference.rs new file mode 100644 index 00000000..7a83f82c --- /dev/null +++ b/src/wasm/citation_reference.rs @@ -0,0 +1,62 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::CitationReference; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmCitationReference { + pub(crate) key: String, + pub(crate) prefix: Option>, + pub(crate) suffix: Option>, +} + +to_wasm!( + WasmCitationReference, + CitationReference<'s>, + original, + wasm_context, + { WasmAstNode::CitationReference(original) }, + { "citation-reference".into() }, + { + let prefix = original + .prefix + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + let suffix = original + .suffix + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + Vec::new(), + WasmCitationReference { + key: original.key.to_owned(), + prefix: if prefix.is_empty() { + None + } else { + Some(prefix) + }, + suffix: if suffix.is_empty() { + None + } else { + Some(suffix) + }, + }, + )) + } +); diff --git a/src/wasm/clock.rs b/src/wasm/clock.rs new file mode 100644 index 00000000..f0badfd0 --- /dev/null +++ b/src/wasm/clock.rs @@ -0,0 +1,43 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Clock; +use crate::types::ClockStatus; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmClock { + #[serde(rename = "value")] + pub(crate) timestamp: Box, + pub(crate) duration: Option, + pub(crate) status: String, +} + +to_wasm!( + WasmClock, + Clock<'s>, + original, + wasm_context, + { WasmAstNode::Clock(original) }, + { "clock".into() }, + { + Ok(( + Vec::new(), + WasmClock { + timestamp: Box::new(Into::::into( + original.timestamp.to_wasm(wasm_context.clone())?, + )), + duration: original.duration.map(|s| s.to_owned()), + status: match original.status { + ClockStatus::Running => "running", + ClockStatus::Closed => "closed", + } + .to_owned(), + }, + )) + } +); diff --git a/src/wasm/code.rs b/src/wasm/code.rs new file mode 100644 index 00000000..58b110eb --- /dev/null +++ b/src/wasm/code.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Code; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmCode { + pub(crate) value: String, +} + +to_wasm!( + WasmCode, + Code<'s>, + original, + wasm_context, + { WasmAstNode::Code(original) }, + { "code".into() }, + { + Ok(( + Vec::new(), + WasmCode { + value: original.contents.to_owned(), + }, + )) + } +); diff --git a/src/wasm/comment.rs b/src/wasm/comment.rs new file mode 100644 index 00000000..7336f821 --- /dev/null +++ b/src/wasm/comment.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Comment; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmComment { + pub(crate) value: String, +} + +to_wasm!( + WasmComment, + Comment<'s>, + original, + wasm_context, + { WasmAstNode::Comment(original) }, + { "comment".into() }, + { + Ok(( + Vec::new(), + WasmComment { + value: original.get_value(), + }, + )) + } +); diff --git a/src/wasm/comment_block.rs b/src/wasm/comment_block.rs new file mode 100644 index 00000000..37d00a26 --- /dev/null +++ b/src/wasm/comment_block.rs @@ -0,0 +1,40 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::CommentBlock; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmCommentBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) value: String, +} + +to_wasm!( + WasmCommentBlock, + CommentBlock<'s>, + original, + wasm_context, + { WasmAstNode::CommentBlock(original) }, + { "comment-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmCommentBlock { + additional_properties, + value: original.contents.to_owned(), + }, + )) + } +); diff --git a/src/wasm/diary_sexp.rs b/src/wasm/diary_sexp.rs new file mode 100644 index 00000000..7fcca85a --- /dev/null +++ b/src/wasm/diary_sexp.rs @@ -0,0 +1,40 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::DiarySexp; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmDiarySexp { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) value: String, +} + +to_wasm!( + WasmDiarySexp, + DiarySexp<'s>, + original, + wasm_context, + { WasmAstNode::DiarySexp(original) }, + { "diary-sexp".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmDiarySexp { + additional_properties, + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/document.rs b/src/wasm/document.rs new file mode 100644 index 00000000..62864066 --- /dev/null +++ b/src/wasm/document.rs @@ -0,0 +1,69 @@ +use std::path::PathBuf; + +use serde::Deserialize; +use serde::Serialize; + +use super::additional_property::AdditionalProperties; +use super::additional_property::AdditionalPropertyValue; +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Document; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmDocument { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + #[serde(rename = "CATEGORY")] + pub(crate) category: Option, + pub(crate) path: Option, +} + +to_wasm!( + WasmDocument, + Document<'s>, + original, + wasm_context, + { WasmAstNode::Document(original) }, + { "org-data".into() }, + { + let category = original.category.as_deref(); + let path = original.path.clone(); + + let mut additional_properties = AdditionalProperties::default(); + for (name, val) in original.get_additional_properties().map(|node_property| { + ( + node_property.property_name.to_uppercase(), + AdditionalPropertyValue::SingleString(node_property.value.unwrap_or("").to_owned()), + ) + }) { + additional_properties.properties.insert(name, val); + } + + let children = original + .zeroth_section + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .chain(original.children.iter().map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + })) + .collect::, _>>()?; + + Ok(( + children, + WasmDocument { + additional_properties, + category: category.map(str::to_owned), + path, + }, + )) + } +); diff --git a/src/wasm/drawer.rs b/src/wasm/drawer.rs new file mode 100644 index 00000000..63a68418 --- /dev/null +++ b/src/wasm/drawer.rs @@ -0,0 +1,51 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::Drawer; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmDrawer { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + #[serde(rename = "drawer-name")] + pub(crate) drawer_name: String, +} + +to_wasm!( + WasmDrawer, + Drawer<'s>, + original, + wasm_context, + { WasmAstNode::Drawer(original) }, + { "drawer".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmDrawer { + additional_properties, + drawer_name: original.drawer_name.to_owned(), + }, + )) + } +); diff --git a/src/wasm/dynamic_block.rs b/src/wasm/dynamic_block.rs new file mode 100644 index 00000000..3cb50340 --- /dev/null +++ b/src/wasm/dynamic_block.rs @@ -0,0 +1,53 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::DynamicBlock; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmDynamicBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + #[serde(rename = "block-name")] + pub(crate) block_name: String, + pub(crate) arguments: Option, +} + +to_wasm!( + WasmDynamicBlock, + DynamicBlock<'s>, + original, + wasm_context, + { WasmAstNode::DynamicBlock(original) }, + { "dynamic-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmDynamicBlock { + additional_properties, + block_name: original.block_name.to_owned(), + arguments: original.parameters.map(|s| s.to_owned()), + }, + )) + } +); diff --git a/src/wasm/entity.rs b/src/wasm/entity.rs new file mode 100644 index 00000000..05197fef --- /dev/null +++ b/src/wasm/entity.rs @@ -0,0 +1,49 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::headline::Noop; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Entity; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmEntity { + pub(crate) name: String, + pub(crate) latex: String, + #[serde(rename = "latex-math-p")] + pub(crate) latex_math_mode: bool, + pub(crate) html: String, + pub(crate) ascii: String, + pub(crate) latin1: Noop, + #[serde(rename = "utf-8")] + pub(crate) utf8: String, + #[serde(rename = "use-brackets-p")] + pub(crate) use_brackets: bool, +} + +to_wasm!( + WasmEntity, + Entity<'s>, + original, + wasm_context, + { WasmAstNode::Entity(original) }, + { "entity".into() }, + { + Ok(( + Vec::new(), + WasmEntity { + name: original.name.to_owned(), + latex: original.latex.to_owned(), + latex_math_mode: original.latex_math_mode, + html: original.html.to_owned(), + ascii: original.ascii.to_owned(), + latin1: Noop {}, + utf8: original.utf8.to_owned(), + use_brackets: original.use_brackets, + }, + )) + } +); diff --git a/src/wasm/example_block.rs b/src/wasm/example_block.rs new file mode 100644 index 00000000..e7945ff7 --- /dev/null +++ b/src/wasm/example_block.rs @@ -0,0 +1,75 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::src_block::WasmNumberLines; +use super::src_block::WasmNumberLinesWrapper; +use super::src_block::WasmRetainLabels; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::CharOffsetInLine; +use crate::types::ExampleBlock; +use crate::types::GetAffiliatedKeywords; +use crate::types::RetainLabels; +use crate::types::SwitchNumberLines; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmExampleBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) value: String, + pub(crate) switches: Option, + #[serde(rename = "number-lines")] + pub(crate) number_lines: Option, + #[serde(rename = "preserve-indent")] + pub(crate) preserve_indent: Option, + #[serde(rename = "retain-labels")] + pub(crate) retain_labels: WasmRetainLabels, + #[serde(rename = "use-labels")] + pub(crate) use_labels: bool, + #[serde(rename = "label-fmt")] + pub(crate) label_format: Option, +} + +to_wasm!( + WasmExampleBlock, + ExampleBlock<'s>, + original, + wasm_context, + { WasmAstNode::ExampleBlock(original) }, + { "example-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmExampleBlock { + additional_properties, + value: original.get_value().into_owned(), + switches: original.switches.map(|s| s.to_owned()), + number_lines: match original.number_lines { + None => None, + Some(SwitchNumberLines::New(n)) => Some(WasmNumberLinesWrapper { + inner: WasmNumberLines::New(n), + }), + Some(SwitchNumberLines::Continued(n)) => Some(WasmNumberLinesWrapper { + inner: WasmNumberLines::Continued(n), + }), + }, + preserve_indent: original.preserve_indent, + retain_labels: match original.retain_labels { + RetainLabels::No => WasmRetainLabels::YesNo(false), + RetainLabels::Yes => WasmRetainLabels::YesNo(true), + RetainLabels::Keep(n) => WasmRetainLabels::Keep(n), + }, + use_labels: original.use_labels, + label_format: original.label_format.map(|s| s.to_owned()), + }, + )) + } +); diff --git a/src/wasm/export_block.rs b/src/wasm/export_block.rs new file mode 100644 index 00000000..9ad0d144 --- /dev/null +++ b/src/wasm/export_block.rs @@ -0,0 +1,43 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::ExportBlock; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmExportBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + #[serde(rename = "type")] + pub(crate) export_type: Option, + pub(crate) value: String, +} + +to_wasm!( + WasmExportBlock, + ExportBlock<'s>, + original, + wasm_context, + { WasmAstNode::ExportBlock(original) }, + { "export-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmExportBlock { + additional_properties, + export_type: original.get_export_type(), + value: original.get_value().into_owned(), + }, + )) + } +); diff --git a/src/wasm/export_snippet.rs b/src/wasm/export_snippet.rs new file mode 100644 index 00000000..01652d32 --- /dev/null +++ b/src/wasm/export_snippet.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::ExportSnippet; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmExportSnippet { + #[serde(rename = "back-end")] + pub(crate) backend: String, + pub(crate) value: Option, +} + +to_wasm!( + WasmExportSnippet, + ExportSnippet<'s>, + original, + wasm_context, + { WasmAstNode::ExportSnippet(original) }, + { "export-snippet".into() }, + { + Ok(( + Vec::new(), + WasmExportSnippet { + backend: original.backend.to_owned(), + value: original.contents.map(|s| s.to_owned()), + }, + )) + } +); diff --git a/src/wasm/fixed_width_area.rs b/src/wasm/fixed_width_area.rs new file mode 100644 index 00000000..7d50c1fd --- /dev/null +++ b/src/wasm/fixed_width_area.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::FixedWidthArea; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmFixedWidthArea { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) value: String, +} + +to_wasm!( + WasmFixedWidthArea, + FixedWidthArea<'s>, + original, + wasm_context, + { WasmAstNode::FixedWidthArea(original) }, + { "fixed-width".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let value = original.get_value(); + + Ok(( + Vec::new(), + WasmFixedWidthArea { + additional_properties, + value, + }, + )) + } +); diff --git a/src/wasm/footnote_definition.rs b/src/wasm/footnote_definition.rs new file mode 100644 index 00000000..a77c6c1a --- /dev/null +++ b/src/wasm/footnote_definition.rs @@ -0,0 +1,54 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::headline::Noop; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::FootnoteDefinition; +use crate::types::GetAffiliatedKeywords; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmFootnoteDefinition { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) label: String, + #[serde(rename = "pre-blank")] + pub(crate) pre_blank: Noop, +} + +to_wasm!( + WasmFootnoteDefinition, + FootnoteDefinition<'s>, + original, + wasm_context, + { WasmAstNode::FootnoteDefinition(original) }, + { "footnote-definition".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmFootnoteDefinition { + additional_properties, + label: original.label.to_owned(), + pre_blank: Noop {}, + }, + )) + } +); diff --git a/src/wasm/footnote_reference.rs b/src/wasm/footnote_reference.rs new file mode 100644 index 00000000..47c37d8a --- /dev/null +++ b/src/wasm/footnote_reference.rs @@ -0,0 +1,49 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::FootnoteReference; +use crate::types::FootnoteReferenceType; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmFootnoteReference { + pub(crate) label: Option, + #[serde(rename = "type")] + pub(crate) footnote_reference_type: String, +} + +to_wasm!( + WasmFootnoteReference, + FootnoteReference<'s>, + original, + wasm_context, + { WasmAstNode::FootnoteReference(original) }, + { "footnote-reference".into() }, + { + let children = original + .definition + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmFootnoteReference { + label: original.label.map(|s| s.to_owned()), + footnote_reference_type: match original.get_type() { + FootnoteReferenceType::Standard => "standard", + FootnoteReferenceType::Inline => "inline", + } + .to_owned(), + }, + )) + } +); diff --git a/src/wasm/headline.rs b/src/wasm/headline.rs new file mode 100644 index 00000000..137ab34e --- /dev/null +++ b/src/wasm/headline.rs @@ -0,0 +1,140 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use super::AdditionalPropertyValue; +use crate::types::Heading; +use crate::types::HeadlineLevel; +use crate::types::PriorityCookie; +use crate::types::TodoKeywordType; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmHeadline { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) level: HeadlineLevel, + pub(crate) tags: Vec, + #[serde(rename = "todo-keyword")] + pub(crate) todo_keyword: Option, + #[serde(rename = "todo-type")] + pub(crate) todo_type: Option, + pub(crate) title: Vec, + pub(crate) priority: Option, + #[serde(rename = "archivedp")] + pub(crate) is_archived: bool, + #[serde(rename = "commentedp")] + pub(crate) is_comment: bool, + #[serde(rename = "raw-value")] + pub(crate) raw_value: String, + #[serde(rename = "footnote-section-p")] + pub(crate) is_footnote_section: bool, + pub(crate) scheduled: Option>, + pub(crate) deadline: Option>, + pub(crate) closed: Option>, + #[serde(rename = "pre-blank")] + pub(crate) pre_blank: Noop, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "noop")] +pub struct Noop {} + +to_wasm!( + WasmHeadline, + Heading<'s>, + original, + wasm_context, + { WasmAstNode::Headline(original) }, + { "headline".into() }, + { + let mut additional_properties = AdditionalProperties::default(); + for (name, val) in original.get_additional_properties().map(|node_property| { + ( + node_property.property_name.to_uppercase(), + AdditionalPropertyValue::SingleString(node_property.value.unwrap_or("").to_owned()), + ) + }) { + additional_properties.properties.insert(name, val); + } + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmHeadline { + additional_properties, + level: original.level, + tags: original.tags.iter().map(|tag| (*tag).to_owned()).collect(), + todo_keyword: original + .todo_keyword + .as_ref() + .map(|(_, keyword)| (*keyword).to_owned()), + todo_type: original + .todo_keyword + .as_ref() + .map(|(keyword, _)| match keyword { + TodoKeywordType::Done => "done".to_owned(), + TodoKeywordType::Todo => "todo".to_owned(), + }), + title: original + .title + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?, + priority: original.priority_cookie, + is_archived: original.is_archived, + is_comment: original.is_comment, + raw_value: original.get_raw_value(), + is_footnote_section: original.is_footnote_section, + scheduled: original + .scheduled + .as_ref() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .map_or(Ok(None), |r| r.map(Some))? + .map(Box::new), + deadline: original + .deadline + .as_ref() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .map_or(Ok(None), |r| r.map(Some))? + .map(Box::new), + closed: original + .closed + .as_ref() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .map_or(Ok(None), |r| r.map(Some))? + .map(Box::new), + pre_blank: Noop {}, + }, + )) + } +); diff --git a/src/wasm/horizontal_rule.rs b/src/wasm/horizontal_rule.rs new file mode 100644 index 00000000..152a366c --- /dev/null +++ b/src/wasm/horizontal_rule.rs @@ -0,0 +1,38 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::HorizontalRule; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmHorizontalRule { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, +} + +to_wasm!( + WasmHorizontalRule, + HorizontalRule<'s>, + original, + wasm_context, + { WasmAstNode::HorizontalRule(original) }, + { "horizontal-rule".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmHorizontalRule { + additional_properties, + }, + )) + } +); diff --git a/src/wasm/inline_babel_call.rs b/src/wasm/inline_babel_call.rs new file mode 100644 index 00000000..4c495b49 --- /dev/null +++ b/src/wasm/inline_babel_call.rs @@ -0,0 +1,41 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::InlineBabelCall; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmInlineBabelCall { + pub(crate) call: String, + #[serde(rename = "inside-header")] + pub(crate) inside_header: Option, + pub(crate) arguments: Option, + #[serde(rename = "end-header")] + pub(crate) end_header: Option, + pub(crate) value: String, +} + +to_wasm!( + WasmInlineBabelCall, + InlineBabelCall<'s>, + original, + wasm_context, + { WasmAstNode::InlineBabelCall(original) }, + { "inline-babel-call".into() }, + { + Ok(( + Vec::new(), + WasmInlineBabelCall { + call: original.call.to_owned(), + inside_header: original.inside_header.map(|s| s.to_owned()), + arguments: original.arguments.map(|s| s.to_owned()), + end_header: original.end_header.map(|s| s.to_owned()), + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/inline_source_block.rs b/src/wasm/inline_source_block.rs new file mode 100644 index 00000000..1d8ce6d8 --- /dev/null +++ b/src/wasm/inline_source_block.rs @@ -0,0 +1,35 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::InlineSourceBlock; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmInlineSourceBlock { + pub(crate) language: String, + pub(crate) value: String, + pub(crate) parameters: Option, +} + +to_wasm!( + WasmInlineSourceBlock, + InlineSourceBlock<'s>, + original, + wasm_context, + { WasmAstNode::InlineSourceBlock(original) }, + { "inline-src-block".into() }, + { + Ok(( + Vec::new(), + WasmInlineSourceBlock { + language: original.language.to_owned(), + value: original.value.to_owned(), + parameters: original.parameters.map(str::to_owned), + }, + )) + } +); diff --git a/src/wasm/italic.rs b/src/wasm/italic.rs new file mode 100644 index 00000000..4e86e65b --- /dev/null +++ b/src/wasm/italic.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Italic; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmItalic {} + +to_wasm!( + WasmItalic, + Italic<'s>, + original, + wasm_context, + { WasmAstNode::Italic(original) }, + { "italic".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok((children, WasmItalic {})) + } +); diff --git a/src/wasm/keyword.rs b/src/wasm/keyword.rs new file mode 100644 index 00000000..3f789782 --- /dev/null +++ b/src/wasm/keyword.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::Keyword; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmKeyword { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) key: String, + pub(crate) value: String, +} + +to_wasm!( + WasmKeyword, + Keyword<'s>, + original, + wasm_context, + { WasmAstNode::Keyword(original) }, + { "keyword".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmKeyword { + additional_properties, + key: original.key.to_uppercase(), + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/latex_environment.rs b/src/wasm/latex_environment.rs new file mode 100644 index 00000000..2f2638e9 --- /dev/null +++ b/src/wasm/latex_environment.rs @@ -0,0 +1,40 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::LatexEnvironment; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmLatexEnvironment { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) value: String, +} + +to_wasm!( + WasmLatexEnvironment, + LatexEnvironment<'s>, + original, + wasm_context, + { WasmAstNode::LatexEnvironment(original) }, + { "latex-environment".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmLatexEnvironment { + additional_properties, + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/latex_fragment.rs b/src/wasm/latex_fragment.rs new file mode 100644 index 00000000..bc816392 --- /dev/null +++ b/src/wasm/latex_fragment.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::LatexFragment; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmLatexFragment { + pub(crate) value: String, +} + +to_wasm!( + WasmLatexFragment, + LatexFragment<'s>, + original, + wasm_context, + { WasmAstNode::LatexFragment(original) }, + { "latex-fragment".into() }, + { + Ok(( + Vec::new(), + WasmLatexFragment { + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/line_break.rs b/src/wasm/line_break.rs new file mode 100644 index 00000000..e8447def --- /dev/null +++ b/src/wasm/line_break.rs @@ -0,0 +1,22 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::LineBreak; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmLineBreak {} + +to_wasm!( + WasmLineBreak, + LineBreak<'s>, + original, + wasm_context, + { WasmAstNode::LineBreak(original) }, + { "line-break".into() }, + { Ok((Vec::new(), WasmLineBreak {},)) } +); diff --git a/src/wasm/macros.rs b/src/wasm/macros.rs new file mode 100644 index 00000000..590d4d34 --- /dev/null +++ b/src/wasm/macros.rs @@ -0,0 +1,58 @@ +/// Write the implementation for the intermediate ast node. +/// +/// This exists to make changing the type signature easier. +macro_rules! to_wasm { + ($ostruct:ty, $istruct:ty, $original:ident, $wasm_context:ident, $fnbody:tt) => { + impl<'s> ToWasm for $istruct { + type Output = $ostruct; + + fn to_wasm( + &self, + $wasm_context: crate::wasm::to_wasm::ToWasmContext<'_>, + ) -> Result { + let $original = self; + $fnbody + } + } + }; + ($ostruct:ty, $istruct:ty, $original:ident, $wasm_context:ident, $toastnodebody:tt, $elispnamebody:tt, $fnbody:tt) => { + impl<'s> ToWasm for $istruct { + type Output = crate::wasm::ast_node::WasmAstNodeWrapper<$ostruct>; + + fn to_wasm( + &self, + $wasm_context: crate::wasm::to_wasm::ToWasmContext<'_>, + ) -> Result { + #[allow(unused_variables)] + let $original = self; + let standard_properties = + self.to_wasm_standard_properties($wasm_context.clone())?; + + $fnbody.map( + |(children, inner)| crate::wasm::ast_node::WasmAstNodeWrapper { + ast_node: inner.get_elisp_name().into_owned(), + standard_properties, + children, + properties: inner, + }, + ) + } + } + + impl From> for WasmAstNode { + fn from($original: crate::wasm::ast_node::WasmAstNodeWrapper<$ostruct>) -> Self { + let ret = $toastnodebody; + ret + } + } + + impl<'s> crate::util::elisp_fact::ElispFact<'s> for $ostruct { + fn get_elisp_name<'b>(&'b self) -> std::borrow::Cow<'s, str> { + let ret = $elispnamebody; + ret + } + } + }; +} + +pub(crate) use to_wasm; diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs new file mode 100644 index 00000000..20e4b571 --- /dev/null +++ b/src/wasm/mod.rs @@ -0,0 +1,76 @@ +mod additional_property; +mod angle_link; +mod ast_node; +mod babel_call; +mod bold; +mod center_block; +mod citation; +mod citation_reference; +mod clock; +mod code; +mod comment; +mod comment_block; +mod diary_sexp; +mod document; +mod drawer; +mod dynamic_block; +mod entity; +mod example_block; +mod export_block; +mod export_snippet; +mod fixed_width_area; +mod footnote_definition; +mod footnote_reference; +mod headline; +mod horizontal_rule; +mod inline_babel_call; +mod inline_source_block; +mod italic; +mod keyword; +mod latex_environment; +mod latex_fragment; +mod line_break; +mod macros; +mod node_property; +mod org_macro; +mod paragraph; +mod parse_result; +mod plain_link; +mod plain_list; +mod plain_list_item; +mod plain_text; +mod planning; +mod property_drawer; +mod quote_block; +mod radio_link; +mod radio_target; +mod regular_link; +mod section; +mod special_block; +mod src_block; +mod standard_properties; +mod statistics_cookie; +mod strike_through; +mod subscript; +mod superscript; +mod table; +mod table_cell; +mod table_row; +mod target; +mod timestamp; +mod to_wasm; +mod underline; +mod verbatim; +mod verse_block; + +pub use additional_property::AdditionalProperties; +pub use additional_property::AdditionalPropertyValue; +pub use ast_node::WasmAstNode; +#[cfg(feature = "wasm_test")] +pub use ast_node::WasmAstNodeWrapper; +#[cfg(feature = "wasm_test")] +pub use document::WasmDocument; +pub use parse_result::ParseResult; +pub(crate) use standard_properties::WasmStandardProperties; +pub use to_wasm::ToWasm; +pub use to_wasm::ToWasmContext; diff --git a/src/wasm/node_property.rs b/src/wasm/node_property.rs new file mode 100644 index 00000000..8d698d2b --- /dev/null +++ b/src/wasm/node_property.rs @@ -0,0 +1,33 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::NodeProperty; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmNodeProperty { + pub(crate) key: String, + pub(crate) value: Option, +} + +to_wasm!( + WasmNodeProperty, + NodeProperty<'s>, + original, + wasm_context, + { WasmAstNode::NodeProperty(original) }, + { "node-property".into() }, + { + Ok(( + Vec::new(), + WasmNodeProperty { + key: original.property_name.to_owned(), + value: original.value.map(|s| s.to_owned()), + }, + )) + } +); diff --git a/src/wasm/org_macro.rs b/src/wasm/org_macro.rs new file mode 100644 index 00000000..9c417884 --- /dev/null +++ b/src/wasm/org_macro.rs @@ -0,0 +1,35 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::OrgMacro; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmOrgMacro { + pub(crate) key: String, + pub(crate) value: String, + pub(crate) args: Vec, +} + +to_wasm!( + WasmOrgMacro, + OrgMacro<'s>, + original, + wasm_context, + { WasmAstNode::OrgMacro(original) }, + { "macro".into() }, + { + Ok(( + Vec::new(), + WasmOrgMacro { + key: original.key.to_lowercase(), + value: original.value.to_owned(), + args: original.get_args().map(|s| s.into_owned()).collect(), + }, + )) + } +); diff --git a/src/wasm/paragraph.rs b/src/wasm/paragraph.rs new file mode 100644 index 00000000..62534f0f --- /dev/null +++ b/src/wasm/paragraph.rs @@ -0,0 +1,48 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::Paragraph; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmParagraph { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, +} + +to_wasm!( + WasmParagraph, + Paragraph<'s>, + original, + wasm_context, + { WasmAstNode::Paragraph(original) }, + { "paragraph".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmParagraph { + additional_properties, + }, + )) + } +); diff --git a/src/wasm/parse_result.rs b/src/wasm/parse_result.rs new file mode 100644 index 00000000..33ab251a --- /dev/null +++ b/src/wasm/parse_result.rs @@ -0,0 +1,15 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNodeWrapper; +use super::document::WasmDocument; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "status", content = "content")] +pub enum ParseResult { + #[serde(rename = "success")] + Success(WasmAstNodeWrapper), + + #[serde(rename = "error")] + Error(String), +} diff --git a/src/wasm/plain_link.rs b/src/wasm/plain_link.rs new file mode 100644 index 00000000..c7ebf165 --- /dev/null +++ b/src/wasm/plain_link.rs @@ -0,0 +1,52 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::LinkType; +use crate::types::PlainLink; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "format")] +#[serde(rename = "plain")] +pub struct WasmPlainLink { + #[serde(rename = "type")] + pub(crate) link_type: String, + pub(crate) path: String, + #[serde(rename = "raw-link")] + pub(crate) raw_link: String, + pub(crate) application: Option, + #[serde(rename = "search-option")] + pub(crate) search_option: Option, +} + +to_wasm!( + WasmPlainLink, + PlainLink<'s>, + original, + wasm_context, + { WasmAstNode::PlainLink(original) }, + { "link".into() }, + { + Ok(( + Vec::new(), + WasmPlainLink { + link_type: match &original.link_type { + LinkType::File => "file".to_owned(), + LinkType::Protocol(protocol) => protocol.clone().into_owned(), + LinkType::Id => "id".to_owned(), + LinkType::CustomId => "custom-id".to_owned(), + LinkType::CodeRef => "coderef".to_owned(), + LinkType::Fuzzy => "fuzzy".to_owned(), + }, + path: original.path.to_owned(), + raw_link: original.raw_link.to_owned(), + application: original.application.map(str::to_owned), + search_option: original.search_option.map(str::to_owned), + }, + )) + } +); diff --git a/src/wasm/plain_list.rs b/src/wasm/plain_list.rs new file mode 100644 index 00000000..51a9b25c --- /dev/null +++ b/src/wasm/plain_list.rs @@ -0,0 +1,57 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::PlainList; +use crate::types::PlainListType; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmPlainList { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + #[serde(rename = "type")] + pub(crate) list_type: String, +} + +to_wasm!( + WasmPlainList, + PlainList<'s>, + original, + wasm_context, + { WasmAstNode::PlainList(original) }, + { "plain-list".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmPlainList { + additional_properties, + list_type: match original.list_type { + PlainListType::Unordered => "unordered", + PlainListType::Ordered => "ordered", + PlainListType::Descriptive => "descriptive", + } + .to_owned(), + }, + )) + } +); diff --git a/src/wasm/plain_list_item.rs b/src/wasm/plain_list_item.rs new file mode 100644 index 00000000..8081b6e1 --- /dev/null +++ b/src/wasm/plain_list_item.rs @@ -0,0 +1,68 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::CheckboxType; +use crate::types::PlainListItem; +use crate::types::PlainListItemCounter; +use crate::types::PlainListItemPreBlank; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmPlainListItem { + pub(crate) tag: Vec, + pub(crate) bullet: String, + pub(crate) counter: Option, + pub(crate) checkbox: Option, + #[serde(rename = "pre-blank")] + pub(crate) pre_blank: PlainListItemPreBlank, +} + +to_wasm!( + WasmPlainListItem, + PlainListItem<'s>, + original, + wasm_context, + { WasmAstNode::PlainListItem(original) }, + { "item".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmPlainListItem { + tag: original + .tag + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?, + bullet: original.bullet.to_owned(), + counter: original.counter, + checkbox: original.checkbox.as_ref().map(|(checkbox_type, _)| { + match checkbox_type { + CheckboxType::On => "on", + CheckboxType::Trans => "trans", + CheckboxType::Off => "off", + } + .to_owned() + }), + pre_blank: original.pre_blank, + }, + )) + } +); diff --git a/src/wasm/plain_text.rs b/src/wasm/plain_text.rs new file mode 100644 index 00000000..e339c71d --- /dev/null +++ b/src/wasm/plain_text.rs @@ -0,0 +1,22 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::PlainText; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmPlainText {} + +to_wasm!( + WasmPlainText, + PlainText<'s>, + original, + wasm_context, + { WasmAstNode::PlainText(original) }, + { "plain-text".into() }, + { Ok((Vec::new(), WasmPlainText {},)) } +); diff --git a/src/wasm/planning.rs b/src/wasm/planning.rs new file mode 100644 index 00000000..1091f6e5 --- /dev/null +++ b/src/wasm/planning.rs @@ -0,0 +1,62 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Planning; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmPlanning { + pub(crate) scheduled: Option>, + pub(crate) deadline: Option>, + pub(crate) closed: Option>, +} + +to_wasm!( + WasmPlanning, + Planning<'s>, + original, + wasm_context, + { WasmAstNode::Planning(original) }, + { "planning".into() }, + { + Ok(( + Vec::new(), + WasmPlanning { + scheduled: original + .scheduled + .as_ref() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .map_or(Ok(None), |r| r.map(Some))? + .map(Box::new), + deadline: original + .deadline + .as_ref() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .map_or(Ok(None), |r| r.map(Some))? + .map(Box::new), + closed: original + .closed + .as_ref() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .map_or(Ok(None), |r| r.map(Some))? + .map(Box::new), + }, + )) + } +); diff --git a/src/wasm/property_drawer.rs b/src/wasm/property_drawer.rs new file mode 100644 index 00000000..4a1053bb --- /dev/null +++ b/src/wasm/property_drawer.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::PropertyDrawer; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmPropertyDrawer {} + +to_wasm!( + WasmPropertyDrawer, + PropertyDrawer<'s>, + original, + wasm_context, + { WasmAstNode::PropertyDrawer(original) }, + { "property-drawer".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok((children, WasmPropertyDrawer {})) + } +); diff --git a/src/wasm/quote_block.rs b/src/wasm/quote_block.rs new file mode 100644 index 00000000..fa1ff938 --- /dev/null +++ b/src/wasm/quote_block.rs @@ -0,0 +1,48 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::QuoteBlock; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmQuoteBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, +} + +to_wasm!( + WasmQuoteBlock, + QuoteBlock<'s>, + original, + wasm_context, + { WasmAstNode::QuoteBlock(original) }, + { "quote-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmQuoteBlock { + additional_properties, + }, + )) + } +); diff --git a/src/wasm/radio_link.rs b/src/wasm/radio_link.rs new file mode 100644 index 00000000..8a77ae2d --- /dev/null +++ b/src/wasm/radio_link.rs @@ -0,0 +1,54 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::RadioLink; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "format")] +#[serde(rename = "plain")] +pub struct WasmRadioLink { + #[serde(rename = "type")] + pub(crate) link_type: String, + pub(crate) path: String, + #[serde(rename = "raw-link")] + pub(crate) raw_link: String, + pub(crate) application: Option, // Always None + #[serde(rename = "search-option")] + pub(crate) search_option: Option, // Always None +} + +to_wasm!( + WasmRadioLink, + RadioLink<'s>, + original, + wasm_context, + { WasmAstNode::RadioLink(original) }, + { "link".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmRadioLink { + link_type: "radio".to_owned(), + path: original.path.to_owned(), + raw_link: original.get_raw_link().to_owned(), + application: None, + search_option: None, + }, + )) + } +); diff --git a/src/wasm/radio_target.rs b/src/wasm/radio_target.rs new file mode 100644 index 00000000..d1f288a0 --- /dev/null +++ b/src/wasm/radio_target.rs @@ -0,0 +1,41 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::RadioTarget; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmRadioTarget { + pub(crate) value: String, +} + +to_wasm!( + WasmRadioTarget, + RadioTarget<'s>, + original, + wasm_context, + { WasmAstNode::RadioTarget(original) }, + { "radio-target".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmRadioTarget { + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/regular_link.rs b/src/wasm/regular_link.rs new file mode 100644 index 00000000..c34569eb --- /dev/null +++ b/src/wasm/regular_link.rs @@ -0,0 +1,67 @@ +use std::borrow::Cow; + +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::LinkType; +use crate::types::RegularLink; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "format")] +#[serde(rename = "bracket")] +pub struct WasmRegularLink { + #[serde(rename = "type")] + pub(crate) link_type: String, + pub(crate) path: String, + #[serde(rename = "raw-link")] + pub(crate) raw_link: String, + pub(crate) application: Option, + #[serde(rename = "search-option")] + pub(crate) search_option: Option, +} + +to_wasm!( + WasmRegularLink, + RegularLink<'s>, + original, + wasm_context, + { WasmAstNode::RegularLink(original) }, + { "link".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmRegularLink { + link_type: match &original.link_type { + LinkType::File => "file".to_owned(), + LinkType::Protocol(protocol) => protocol.clone().into_owned(), + LinkType::Id => "id".to_owned(), + LinkType::CustomId => "custom-id".to_owned(), + LinkType::CodeRef => "coderef".to_owned(), + LinkType::Fuzzy => "fuzzy".to_owned(), + }, + path: original.get_path().into_owned(), + raw_link: original.get_raw_link().into_owned(), + application: original + .application + .as_ref() + .map(|c| c.clone().into_owned()), + search_option: original.get_search_option().map(Cow::into_owned), + }, + )) + } +); diff --git a/src/wasm/section.rs b/src/wasm/section.rs new file mode 100644 index 00000000..ded0b26f --- /dev/null +++ b/src/wasm/section.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Section; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmSection {} + +to_wasm!( + WasmSection, + Section<'s>, + original, + wasm_context, + { WasmAstNode::Section(original) }, + { "section".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok((children, WasmSection {})) + } +); diff --git a/src/wasm/special_block.rs b/src/wasm/special_block.rs new file mode 100644 index 00000000..e49c9b56 --- /dev/null +++ b/src/wasm/special_block.rs @@ -0,0 +1,56 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::headline::Noop; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::SpecialBlock; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmSpecialBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + #[serde(rename = "type")] + pub(crate) block_type: String, + pub(crate) parameters: Option, + pub(crate) results: Noop, +} + +to_wasm!( + WasmSpecialBlock, + SpecialBlock<'s>, + original, + wasm_context, + { WasmAstNode::SpecialBlock(original) }, + { "special-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmSpecialBlock { + additional_properties, + block_type: original.block_type.to_owned(), + parameters: original.parameters.map(|s| s.to_owned()), + results: Noop {}, + }, + )) + } +); diff --git a/src/wasm/src_block.rs b/src/wasm/src_block.rs new file mode 100644 index 00000000..7633c545 --- /dev/null +++ b/src/wasm/src_block.rs @@ -0,0 +1,100 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::CharOffsetInLine; +use crate::types::GetAffiliatedKeywords; +use crate::types::LineNumber; +use crate::types::RetainLabels; +use crate::types::SrcBlock; +use crate::types::SwitchNumberLines; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmSrcBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + pub(crate) language: Option, + pub(crate) value: String, + pub(crate) switches: Option, + pub(crate) parameters: Option, + #[serde(rename = "number-lines")] + pub(crate) number_lines: Option, + #[serde(rename = "preserve-indent")] + pub(crate) preserve_indent: Option, + #[serde(rename = "retain-labels")] + pub(crate) retain_labels: WasmRetainLabels, + #[serde(rename = "use-labels")] + pub(crate) use_labels: bool, + #[serde(rename = "label-fmt")] + pub(crate) label_format: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub(crate) enum WasmRetainLabels { + YesNo(bool), + Keep(CharOffsetInLine), +} + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) enum WasmNumberLines { + #[serde(rename = "new")] + New(LineNumber), + #[serde(rename = "continued")] + Continued(LineNumber), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "number-lines")] +#[serde(tag = "number-lines")] +pub(crate) struct WasmNumberLinesWrapper { + #[serde(flatten)] + pub(crate) inner: WasmNumberLines, +} + +to_wasm!( + WasmSrcBlock, + SrcBlock<'s>, + original, + wasm_context, + { WasmAstNode::SrcBlock(original) }, + { "src-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + Ok(( + Vec::new(), + WasmSrcBlock { + additional_properties, + language: original.language.map(|s| s.to_owned()), + value: original.get_value().into_owned(), + switches: original.switches.map(|s| s.to_owned()), + parameters: original.parameters.map(|s| s.to_owned()), + number_lines: match original.number_lines { + None => None, + Some(SwitchNumberLines::New(n)) => Some(WasmNumberLinesWrapper { + inner: WasmNumberLines::New(n), + }), + Some(SwitchNumberLines::Continued(n)) => Some(WasmNumberLinesWrapper { + inner: WasmNumberLines::Continued(n), + }), + }, + preserve_indent: original.preserve_indent, + retain_labels: match original.retain_labels { + RetainLabels::No => WasmRetainLabels::YesNo(false), + RetainLabels::Yes => WasmRetainLabels::YesNo(true), + RetainLabels::Keep(n) => WasmRetainLabels::Keep(n), + }, + use_labels: original.use_labels, + label_format: original.label_format.map(|s| s.to_owned()), + }, + )) + } +); diff --git a/src/wasm/standard_properties.rs b/src/wasm/standard_properties.rs new file mode 100644 index 00000000..de46b165 --- /dev/null +++ b/src/wasm/standard_properties.rs @@ -0,0 +1,74 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::to_wasm::ToWasmContext; +use super::to_wasm::ToWasmStandardProperties; +use crate::types::PostBlank; +use crate::types::StandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct WasmStandardProperties { + pub(crate) begin: usize, + pub(crate) end: usize, + #[serde(rename = "contents-begin")] + pub(crate) contents_begin: Option, + #[serde(rename = "contents-end")] + pub(crate) contents_end: Option, + #[serde(rename = "post-blank")] + pub(crate) post_blank: PostBlank, +} + +impl<'s, SP: StandardProperties<'s>> ToWasmStandardProperties for SP { + type Output = WasmStandardProperties; + + fn to_wasm_standard_properties( + &self, + wasm_context: ToWasmContext<'_>, + ) -> Result { + let (begin, end) = get_char_indices(wasm_context.full_document, self.get_source()); + let (contents_begin, contents_end) = match self.get_contents() { + Some(contents) => { + let (begin, end) = get_char_indices(wasm_context.full_document, contents); + (Some(begin), Some(end)) + } + None => (None, None), + }; + + let post_blank = self.get_post_blank(); + Ok(WasmStandardProperties { + begin, + end, + contents_begin, + contents_end, + post_blank, + }) + } +} + +fn get_char_indices(original_document: &str, subset: &str) -> (usize, usize) { + let (begin_byte, end_byte) = get_rust_byte_offsets(original_document, subset); + // Add 1 to make this 1-based to match emacs. + let begin_char = original_document[..begin_byte].chars().count() + 1; + // Also 1-based: + let end_char = begin_char + original_document[begin_byte..end_byte].chars().count(); + (begin_char, end_char) +} + +/// Get the byte offset into source that the string slice exists at. +/// +/// These offsets are zero-based. +fn get_rust_byte_offsets(original_document: &str, subset: &str) -> (usize, usize) { + debug_assert!(is_slice_of(original_document, subset)); + let offset = subset.as_ptr() as usize - original_document.as_ptr() as usize; + let end = offset + subset.len(); + (offset, end) +} + +/// Check if the child string slice is a slice of the parent string slice. +fn is_slice_of(parent: &str, child: &str) -> bool { + let parent_start = parent.as_ptr() as usize; + let parent_end = parent_start + parent.len(); + let child_start = child.as_ptr() as usize; + let child_end = child_start + child.len(); + child_start >= parent_start && child_end <= parent_end +} diff --git a/src/wasm/statistics_cookie.rs b/src/wasm/statistics_cookie.rs new file mode 100644 index 00000000..85e075af --- /dev/null +++ b/src/wasm/statistics_cookie.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::StatisticsCookie; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmStatisticsCookie { + pub(crate) value: String, +} + +to_wasm!( + WasmStatisticsCookie, + StatisticsCookie<'s>, + original, + wasm_context, + { WasmAstNode::StatisticsCookie(original) }, + { "statistics-cookie".into() }, + { + Ok(( + Vec::new(), + WasmStatisticsCookie { + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/strike_through.rs b/src/wasm/strike_through.rs new file mode 100644 index 00000000..18407baf --- /dev/null +++ b/src/wasm/strike_through.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::StrikeThrough; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmStrikeThrough {} + +to_wasm!( + WasmStrikeThrough, + StrikeThrough<'s>, + original, + wasm_context, + { WasmAstNode::StrikeThrough(original) }, + { "strike-through".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok((children, WasmStrikeThrough {})) + } +); diff --git a/src/wasm/subscript.rs b/src/wasm/subscript.rs new file mode 100644 index 00000000..151665c9 --- /dev/null +++ b/src/wasm/subscript.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Subscript; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmSubscript { + #[serde(rename = "use-brackets-p")] + pub(crate) use_brackets: bool, +} + +to_wasm!( + WasmSubscript, + Subscript<'s>, + original, + wasm_context, + { WasmAstNode::Subscript(original) }, + { "subscript".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmSubscript { + use_brackets: original.use_brackets, + }, + )) + } +); diff --git a/src/wasm/superscript.rs b/src/wasm/superscript.rs new file mode 100644 index 00000000..c6c3500d --- /dev/null +++ b/src/wasm/superscript.rs @@ -0,0 +1,42 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Superscript; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmSuperscript { + #[serde(rename = "use-brackets-p")] + pub(crate) use_brackets: bool, +} + +to_wasm!( + WasmSuperscript, + Superscript<'s>, + original, + wasm_context, + { WasmAstNode::Superscript(original) }, + { "superscript".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmSuperscript { + use_brackets: original.use_brackets, + }, + )) + } +); diff --git a/src/wasm/table.rs b/src/wasm/table.rs new file mode 100644 index 00000000..e0340695 --- /dev/null +++ b/src/wasm/table.rs @@ -0,0 +1,75 @@ +use std::collections::BTreeSet; + +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::Table; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmTable { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, + #[serde(rename = "tblfm")] + pub(crate) formulas: Option, + #[serde(rename = "type")] + pub(crate) table_type: String, + pub(crate) value: Option, // Always None +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "string-set")] +#[serde(rename = "string-set")] +pub(crate) struct WasmStringSet { + value: BTreeSet, +} + +to_wasm!( + WasmTable, + Table<'s>, + original, + wasm_context, + { WasmAstNode::Table(original) }, + { "table".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmTable { + additional_properties, + formulas: if original.formulas.is_empty() { + None + } else { + Some(WasmStringSet { + value: original + .formulas + .iter() + .map(|kw| kw.value.to_owned()) + .collect(), + }) + }, + table_type: "org".to_owned(), + value: None, + }, + )) + } +); diff --git a/src/wasm/table_cell.rs b/src/wasm/table_cell.rs new file mode 100644 index 00000000..b6a61b69 --- /dev/null +++ b/src/wasm/table_cell.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::TableCell; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmTableCell {} + +to_wasm!( + WasmTableCell, + TableCell<'s>, + original, + wasm_context, + { WasmAstNode::TableCell(original) }, + { "table-cell".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok((children, WasmTableCell {})) + } +); diff --git a/src/wasm/table_row.rs b/src/wasm/table_row.rs new file mode 100644 index 00000000..e4587dee --- /dev/null +++ b/src/wasm/table_row.rs @@ -0,0 +1,47 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::TableRow; +use crate::types::TableRowType; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmTableRow { + #[serde(rename = "type")] + pub(crate) row_type: String, +} + +to_wasm!( + WasmTableRow, + TableRow<'s>, + original, + wasm_context, + { WasmAstNode::TableRow(original) }, + { "table-row".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmTableRow { + row_type: match original.get_type() { + TableRowType::Standard => "standard", + TableRowType::Rule => "rule", + } + .to_owned(), + }, + )) + } +); diff --git a/src/wasm/target.rs b/src/wasm/target.rs new file mode 100644 index 00000000..125821de --- /dev/null +++ b/src/wasm/target.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Target; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmTarget { + pub(crate) value: String, +} + +to_wasm!( + WasmTarget, + Target<'s>, + original, + wasm_context, + { WasmAstNode::Target(original) }, + { "target".into() }, + { + Ok(( + Vec::new(), + WasmTarget { + value: original.value.to_owned(), + }, + )) + } +); diff --git a/src/wasm/timestamp.rs b/src/wasm/timestamp.rs new file mode 100644 index 00000000..607fc5fa --- /dev/null +++ b/src/wasm/timestamp.rs @@ -0,0 +1,146 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::DayOfMonthInner; +use crate::types::HourInner; +use crate::types::MinuteInner; +use crate::types::MonthInner; +use crate::types::RepeaterType; +use crate::types::RepeaterWarningDelayValueType; +use crate::types::TimeUnit; +use crate::types::Timestamp; +use crate::types::TimestampRangeType; +use crate::types::TimestampType; +use crate::types::WarningDelayType; +use crate::types::YearInner; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmTimestamp { + #[serde(rename = "type")] + pub(crate) timestamp_type: String, + #[serde(rename = "range-type")] + pub(crate) range_type: Option, + #[serde(rename = "raw-value")] + pub(crate) raw_value: String, + #[serde(rename = "year-start")] + pub(crate) year_start: Option, + #[serde(rename = "month-start")] + pub(crate) month_start: Option, + #[serde(rename = "day-start")] + pub(crate) day_of_month_start: Option, + #[serde(rename = "hour-start")] + pub(crate) hour_start: Option, + #[serde(rename = "minute-start")] + pub(crate) minute_start: Option, + #[serde(rename = "year-end")] + pub(crate) year_end: Option, + #[serde(rename = "month-end")] + pub(crate) month_end: Option, + #[serde(rename = "day-end")] + pub(crate) day_of_month_end: Option, + #[serde(rename = "hour-end")] + pub(crate) hour_end: Option, + #[serde(rename = "minute-end")] + pub(crate) minute_end: Option, + #[serde(rename = "repeater-type")] + pub(crate) repeater_type: Option, + #[serde(rename = "repeater-unit")] + pub(crate) repeater_unit: Option, + #[serde(rename = "repeater-value")] + pub(crate) repeater_value: Option, + #[serde(rename = "warning-type")] + pub(crate) warning_type: Option, + #[serde(rename = "warning-unit")] + pub(crate) warning_unit: Option, + #[serde(rename = "warning-value")] + pub(crate) warning_value: Option, +} + +to_wasm!( + WasmTimestamp, + Timestamp<'s>, + original, + wasm_context, + { WasmAstNode::Timestamp(original) }, + { "timestamp".into() }, + { + Ok(( + Vec::new(), + WasmTimestamp { + timestamp_type: match original.timestamp_type { + TimestampType::Diary => "diary", + TimestampType::Active => "active", + TimestampType::Inactive => "inactive", + TimestampType::ActiveRange => "active-range", + TimestampType::InactiveRange => "inactive-range", + } + .to_owned(), + range_type: match original.range_type { + TimestampRangeType::DateRange => Some("daterange".to_owned()), + TimestampRangeType::TimeRange => Some("timerange".to_owned()), + TimestampRangeType::None => None, + }, + raw_value: original.get_raw_value().to_owned(), + year_start: original.start.as_ref().map(|date| date.get_year().0), + month_start: original.start.as_ref().map(|date| date.get_month().0), + day_of_month_start: original + .start + .as_ref() + .map(|date| date.get_day_of_month().0), + hour_start: original.start_time.as_ref().map(|time| time.get_hour().0), + minute_start: original.start_time.as_ref().map(|time| time.get_minute().0), + year_end: original.end.as_ref().map(|date| date.get_year().0), + month_end: original.end.as_ref().map(|date| date.get_month().0), + day_of_month_end: original.end.as_ref().map(|date| date.get_day_of_month().0), + hour_end: original.end_time.as_ref().map(|time| time.get_hour().0), + minute_end: original.end_time.as_ref().map(|time| time.get_minute().0), + repeater_type: original + .repeater + .as_ref() + .map(|repeater| match repeater.repeater_type { + RepeaterType::Cumulative => "cumulate", + RepeaterType::CatchUp => "catch-up", + RepeaterType::Restart => "restart", + }) + .map(|text| text.to_owned()), + repeater_unit: original + .repeater + .as_ref() + .map(|repeater| match repeater.unit { + TimeUnit::Hour => "hour", + TimeUnit::Day => "day", + TimeUnit::Week => "week", + TimeUnit::Month => "month", + TimeUnit::Year => "year", + }) + .map(|text| text.to_owned()), + repeater_value: original.repeater.as_ref().map(|repeater| repeater.value), + warning_type: original + .warning_delay + .as_ref() + .map(|warning| match warning.warning_delay_type { + WarningDelayType::All => "all", + WarningDelayType::First => "first", + }) + .map(|text| text.to_owned()), + warning_unit: original + .warning_delay + .as_ref() + .map(|warning| match warning.unit { + TimeUnit::Hour => "hour", + TimeUnit::Day => "day", + TimeUnit::Week => "week", + TimeUnit::Month => "month", + TimeUnit::Year => "year", + }) + .map(|text| text.to_owned()), + warning_value: original.warning_delay.as_ref().map(|warning| warning.value), + }, + )) + } +); diff --git a/src/wasm/to_wasm.rs b/src/wasm/to_wasm.rs new file mode 100644 index 00000000..7b8211fd --- /dev/null +++ b/src/wasm/to_wasm.rs @@ -0,0 +1,124 @@ +use super::macros::to_wasm; +use super::WasmAstNode; +use crate::error::CustomError; +use crate::types::DocumentElement; +use crate::types::Element; +use crate::types::Object; + +pub trait ToWasm { + type Output; + + fn to_wasm(&self, full_document: ToWasmContext<'_>) -> Result; +} + +pub(crate) trait ToWasmStandardProperties { + type Output; + + fn to_wasm_standard_properties( + &self, + wasm_context: ToWasmContext<'_>, + ) -> Result; +} + +#[derive(Debug, Clone)] +pub struct ToWasmContext<'s> { + pub(crate) full_document: &'s str, +} + +impl<'s> ToWasmContext<'s> { + pub fn new(full_document: &'s str) -> ToWasmContext<'s> { + ToWasmContext { full_document } + } +} + +to_wasm!(WasmAstNode, DocumentElement<'s>, original, wasm_context, { + match original { + DocumentElement::Heading(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + DocumentElement::Section(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + } +}); + +to_wasm!(WasmAstNode, Element<'s>, original, wasm_context, { + match original { + Element::Paragraph(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::PlainList(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::CenterBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::QuoteBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::SpecialBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::DynamicBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::FootnoteDefinition(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Element::Comment(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::Drawer(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::PropertyDrawer(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Element::Table(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::VerseBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::CommentBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::ExampleBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::ExportBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::SrcBlock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::Clock(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::DiarySexp(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::Planning(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::FixedWidthArea(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Element::HorizontalRule(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Element::Keyword(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::BabelCall(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Element::LatexEnvironment(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + } +}); + +to_wasm!(WasmAstNode, Object<'s>, original, wasm_context, { + match original { + Object::Bold(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::Italic(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::Underline(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::StrikeThrough(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::Code(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::Verbatim(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::PlainText(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::RegularLink(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::RadioLink(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::RadioTarget(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::PlainLink(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::AngleLink(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::OrgMacro(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::Entity(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::LatexFragment(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::ExportSnippet(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::FootnoteReference(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Object::Citation(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::CitationReference(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Object::InlineBabelCall(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Object::InlineSourceBlock(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Object::LineBreak(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::Target(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::StatisticsCookie(inner) => { + inner.to_wasm(wasm_context).map(Into::::into) + } + Object::Subscript(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::Superscript(inner) => inner.to_wasm(wasm_context).map(Into::::into), + Object::Timestamp(inner) => inner.to_wasm(wasm_context).map(Into::::into), + } +}); diff --git a/src/wasm/underline.rs b/src/wasm/underline.rs new file mode 100644 index 00000000..7539ae6e --- /dev/null +++ b/src/wasm/underline.rs @@ -0,0 +1,34 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Underline; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmUnderline {} + +to_wasm!( + WasmUnderline, + Underline<'s>, + original, + wasm_context, + { WasmAstNode::Underline(original) }, + { "underline".into() }, + { + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok((children, WasmUnderline {})) + } +); diff --git a/src/wasm/verbatim.rs b/src/wasm/verbatim.rs new file mode 100644 index 00000000..18088099 --- /dev/null +++ b/src/wasm/verbatim.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use crate::types::Verbatim; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmVerbatim { + pub(crate) value: String, +} + +to_wasm!( + WasmVerbatim, + Verbatim<'s>, + original, + wasm_context, + { WasmAstNode::Verbatim(original) }, + { "verbatim".into() }, + { + Ok(( + Vec::new(), + WasmVerbatim { + value: original.contents.to_owned(), + }, + )) + } +); diff --git a/src/wasm/verse_block.rs b/src/wasm/verse_block.rs new file mode 100644 index 00000000..5028223d --- /dev/null +++ b/src/wasm/verse_block.rs @@ -0,0 +1,48 @@ +use serde::Deserialize; +use serde::Serialize; + +use super::ast_node::WasmAstNode; +use super::macros::to_wasm; +use super::to_wasm::ToWasm; +use super::AdditionalProperties; +use crate::types::GetAffiliatedKeywords; +use crate::types::VerseBlock; +use crate::util::elisp_fact::ElispFact; +use crate::wasm::to_wasm::ToWasmStandardProperties; + +#[derive(Debug, Serialize, Deserialize)] +pub struct WasmVerseBlock { + #[serde(flatten)] + pub(crate) additional_properties: AdditionalProperties, +} + +to_wasm!( + WasmVerseBlock, + VerseBlock<'s>, + original, + wasm_context, + { WasmAstNode::VerseBlock(original) }, + { "verse-block".into() }, + { + let additional_properties = original + .get_affiliated_keywords() + .to_wasm(wasm_context.clone())?; + + let children = original + .children + .iter() + .map(|child| { + child + .to_wasm(wasm_context.clone()) + .map(Into::::into) + }) + .collect::, _>>()?; + + Ok(( + children, + WasmVerseBlock { + additional_properties, + }, + )) + } +); diff --git a/src/wasm_cli/mod.rs b/src/wasm_cli/mod.rs new file mode 100644 index 00000000..54cf0c58 --- /dev/null +++ b/src/wasm_cli/mod.rs @@ -0,0 +1,24 @@ +use crate::parser::parse_with_settings; +use crate::settings::GlobalSettings; +use crate::wasm::ParseResult; +use crate::wasm::ToWasm; +use crate::wasm::ToWasmContext; + +pub fn parse_org(org_contents: &str) -> wasm_bindgen::JsValue { + let rust_parsed = match parse_with_settings(org_contents, &GlobalSettings::default()) { + Ok(document) => document, + Err(err) => { + return serde_wasm_bindgen::to_value(&ParseResult::Error(format!("{:?}", err))) + .unwrap(); + } + }; + let to_wasm_context = ToWasmContext::new(org_contents); + let wasm_document = match rust_parsed.to_wasm(to_wasm_context) { + Ok(document) => document, + Err(err) => { + return serde_wasm_bindgen::to_value(&ParseResult::Error(format!("{:?}", err))) + .unwrap(); + } + }; + serde_wasm_bindgen::to_value(&ParseResult::Success(wasm_document)).unwrap() +} diff --git a/src/wasm_test/compare.rs b/src/wasm_test/compare.rs new file mode 100644 index 00000000..ec923fe5 --- /dev/null +++ b/src/wasm_test/compare.rs @@ -0,0 +1,912 @@ +use std::borrow::Borrow; +use std::borrow::Cow; +use std::collections::BTreeSet; +use std::collections::HashMap; + +use super::diff::WasmDiffResult; +use super::diff::WasmDiffStatus; +use crate::util::elisp::maybe_token_to_usize; +use crate::util::elisp::unquote; +use crate::util::elisp::EmacsStandardProperties; +use crate::util::elisp::TextWithProperties; +use crate::util::elisp::Token; +use crate::wasm::WasmAstNodeWrapper; +use crate::wasm::WasmDocument; + +pub fn wasm_compare_document<'s>( + source: &'s str, + emacs: &Token<'s>, + wasm: &WasmAstNodeWrapper, +) -> Result, Box> { + let wasm_json = serde_json::to_string(&wasm)?; + let wasm_json_parsed = serde_json::from_str(&wasm_json)?; + compare_json_value(source, emacs, &wasm_json_parsed) +} + +fn compare_json_value<'s>( + source: &'s str, + emacs: &Token<'s>, + wasm: &serde_json::Value, +) -> Result, Box> { + // println!("XXXXXXXXXXXXXX compare_json_value XXXXXXXXXXXXXX"); + // println!("{:?}", emacs); + // println!("{:?}", wasm); + // println!("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"); + match (wasm, emacs) { + (serde_json::Value::Object(wasm), Token::List(el)) if wasm.contains_key("ast-node") => { + // We hit a regular ast node. + compare_ast_node(source, el, wasm) + } + (serde_json::Value::String(w), Token::Atom(e)) + if e.starts_with('"') && e.ends_with('"') => + { + // We hit a string compared against a quoted string from elisp (as opposed to an unquoted literal). + compare_quoted_string(source, e, w) + } + (serde_json::Value::Array(w), Token::List(e)) => { + // TODO: This is creating children with no names. + wasm_compare_list(source, e.iter(), w.iter()) + } + (serde_json::Value::Object(wasm), Token::List(e)) + if wasm.contains_key("optval") && wasm.contains_key("val") => + { + compare_optional_pair(source, e, wasm) + } + (serde_json::Value::Object(wasm), Token::List(el)) if wasm.contains_key("object-tree") => { + // We hit an object tree additional property. + compare_object_tree(source, el, wasm) + } + (serde_json::Value::Object(wasm), Token::List(el)) if wasm.contains_key("number-lines") => { + // We hit an object tree additional property. + compare_number_lines(source, el, wasm) + } + (serde_json::Value::Object(wasm), Token::List(el)) if wasm.contains_key("string-set") => { + // We hit an object tree additional property. + compare_string_set(source, el, wasm) + } + (serde_json::Value::Object(w), Token::TextWithProperties(e)) if is_plain_text(w) => { + compare_plain_text(source, e, w) + } + (serde_json::Value::Null, Token::Atom("nil")) => Ok(WasmDiffResult::default()), + (serde_json::Value::Bool(false), Token::Atom("nil")) => Ok(WasmDiffResult::default()), + (serde_json::Value::Bool(true), Token::Atom(e)) if (*e) != "nil" => { + Ok(WasmDiffResult::default()) + } + (serde_json::Value::Bool(w), Token::Atom(e)) => { + let mut result = WasmDiffResult::default(); + result.status.push(WasmDiffStatus::Bad( + format!( + "Value mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = e, + wasm = w, + ) + .into(), + )); + Ok(result) + } + (serde_json::Value::Number(w), Token::Atom(e)) if w.to_string().as_str() == (*e) => { + Ok(WasmDiffResult::default()) + } + (serde_json::Value::Number(w), Token::Atom(e)) => { + let mut result = WasmDiffResult::default(); + result.status.push(WasmDiffStatus::Bad( + format!( + "Value mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = e, + wasm = w, + ) + .into(), + )); + Ok(result) + } + (serde_json::Value::Array(w), Token::Atom("nil")) if w.is_empty() => { + Ok(WasmDiffResult::default()) + } + (serde_json::Value::String(w), Token::Atom(e)) if w.as_str() == *e => { + Ok(WasmDiffResult::default()) + } + (serde_json::Value::Object(w), _) if w.contains_key("noop") => { + Ok(WasmDiffResult::default()) + } + (serde_json::Value::Null, Token::Atom(_)) => todo!(), + (serde_json::Value::Null, Token::List(_)) => todo!(), + (serde_json::Value::Null, Token::TextWithProperties(_)) => todo!(), + (serde_json::Value::Null, Token::Vector(_)) => todo!(), + // (serde_json::Value::Bool(_), Token::Atom(_)) => todo!(), + (serde_json::Value::Bool(_), Token::List(_)) => todo!(), + (serde_json::Value::Bool(_), Token::TextWithProperties(_)) => todo!(), + (serde_json::Value::Bool(_), Token::Vector(_)) => todo!(), + // (serde_json::Value::Number(_), Token::Atom(_)) => todo!(), + (serde_json::Value::Number(_), Token::List(_)) => todo!(), + (serde_json::Value::Number(_), Token::TextWithProperties(_)) => todo!(), + (serde_json::Value::Number(_), Token::Vector(_)) => todo!(), + // (serde_json::Value::String(_), Token::Atom(_)) => todo!(), + (serde_json::Value::String(w), Token::Atom(e)) => { + let mut result = WasmDiffResult::default(); + result.status.push(WasmDiffStatus::Bad( + format!( + "Value mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = e, + wasm = w, + ) + .into(), + )); + Ok(result) + } + (serde_json::Value::String(_), Token::List(_)) => todo!(), + (serde_json::Value::String(_), Token::TextWithProperties(_)) => todo!(), + (serde_json::Value::String(_), Token::Vector(_)) => todo!(), + (serde_json::Value::Array(_), Token::Atom(_)) => todo!(), + // (serde_json::Value::Array(_), Token::List(_)) => todo!(), + (serde_json::Value::Array(_), Token::TextWithProperties(_)) => todo!(), + (serde_json::Value::Array(_), Token::Vector(_)) => todo!(), + (serde_json::Value::Object(_), Token::Atom(_)) => todo!(), + (serde_json::Value::Object(_), Token::List(_)) => todo!(), + (serde_json::Value::Object(_), Token::TextWithProperties(_)) => todo!(), + (serde_json::Value::Object(_), Token::Vector(_)) => todo!(), + } +} + +fn compare_optional_json_value<'s>( + source: &'s str, + emacs: Option<&Token<'s>>, + wasm: Option<&serde_json::Value>, +) -> Result, Box> { + match (emacs, wasm) { + (None, None) | (None, Some(serde_json::Value::Null)) | (Some(Token::Atom("nil")), None) => { + Ok(WasmDiffResult::default()) + } + (None, Some(serde_json::Value::Object(w))) if w.contains_key("noop") => { + Ok(WasmDiffResult::default()) + } + (Some(e), Some(w)) => compare_json_value(source, e, w), + _ => Ok(WasmDiffResult { + status: vec![WasmDiffStatus::Bad( + format!( + "Nullness mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs, + wasm = wasm + ) + .into(), + )], + children: Vec::new(), + name: "".into(), + }), + } +} + +fn compare_ast_node<'s>( + source: &'s str, + emacs: &[Token<'s>], + wasm: &serde_json::Map, +) -> Result, Box> { + let mut result = WasmDiffResult::default(); + let mut emacs_list_iter = emacs.iter(); + + { + // Compare ast node type. + let emacs_name = emacs_list_iter + .next() + .ok_or("Should have a name as the first child.")? + .as_atom()?; + let wasm_name = wasm + .get("ast-node") + .ok_or("Should have a ast node type.")? + .as_str() + .ok_or("Ast node type should be a string.")?; + result.name = emacs_name.into(); + if emacs_name != wasm_name { + result.status.push(WasmDiffStatus::Bad( + format!( + "AST node name mismatch. Emacs=({emacs}) Wasm=({wasm}).", + emacs = emacs_name, + wasm = wasm_name, + ) + .into(), + )); + } + } + + if result.is_bad() { + return Ok(result); + } + + let emacs_attributes_map = emacs_list_iter + .next() + .ok_or("Should have an attributes child.")? + .as_map()?; + let wasm_attributes_map = wasm + .get("properties") + .ok_or(r#"Wasm ast node should have a "properties" attribute."#)? + .as_object() + .ok_or(r#"Wasm ast node "properties" attribute should be an object."#)?; + + { + // Compare attribute names. + let emacs_keys: std::collections::BTreeSet = emacs_attributes_map + .keys() + .map(|s| (*s).to_owned()) + .collect(); + // wasm_attributes_map.iter().filter_map(|(k,v)| if matches!(v, serde_json::Value::Null) {None} else {Some(k)}).map(wasm_key_to_emacs_key) + let wasm_keys: std::collections::BTreeSet = + std::iter::once(":standard-properties".to_owned()) + .chain(wasm_attributes_map.keys().map(wasm_key_to_emacs_key)) + .collect(); + let emacs_only_attributes: Vec<&String> = emacs_keys.difference(&wasm_keys).collect(); + let wasm_only_attributes: Vec<&String> = wasm_keys + .difference(&emacs_keys) + .filter(|attribute| { + emacs_attributes_map + .get(attribute.as_str()) + .map(|token| !matches!(token, Token::Atom("nil"))) + .unwrap_or(false) + }) + .collect(); + if !emacs_only_attributes.is_empty() { + result.status.push(WasmDiffStatus::Bad( + format!( + "Wasm node lacked field present in elisp node ({name}).", + name = emacs_only_attributes + .iter() + .map(|s| s.as_str()) + .intersperse(", ") + .collect::(), + ) + .into(), + )); + } + if !wasm_only_attributes.is_empty() { + result.status.push(WasmDiffStatus::Bad( + format!( + "Elisp node lacked field present in wasm node ({name}).", + name = wasm_only_attributes + .iter() + .map(|s| s.as_str()) + .intersperse(", ") + .collect::(), + ) + .into(), + )); + } + } + + if result.is_bad() { + return Ok(result); + } + + { + // Compare attributes. + for attribute_name in wasm_attributes_map.keys() { + let mut layer = WasmDiffResult::<'_> { + name: Cow::Owned(attribute_name.clone()), + ..Default::default() + }; + let wasm_attribute_value = wasm_attributes_map.get(attribute_name); + let emacs_key = wasm_key_to_emacs_key(attribute_name); + let emacs_attribute_value = emacs_attributes_map.get(emacs_key.as_str()).copied(); + let inner_layer = + compare_optional_json_value(source, emacs_attribute_value, wasm_attribute_value)?; + if !inner_layer.name.is_empty() { + layer.children.push(inner_layer); + } else { + layer.extend(inner_layer)?; + } + result.children.push(layer); + } + } + + { + // Compare standard-properties. + let mut layer = WasmDiffResult::<'_> { + name: "standard-properties".into(), + ..Default::default() + }; + let emacs_standard_properties = wasm_get_emacs_standard_properties(&emacs_attributes_map)?; + let wasm_standard_properties = wasm + .get("standard-properties") + .ok_or(r#"Wasm AST nodes should have a "standard-properties" attribute."#)? + .as_object() + .ok_or(r#"Wasm ast node "standard-properties" attribute should be an object."#)?; + for (emacs_value, wasm_name) in [ + (emacs_standard_properties.begin, "begin"), + (emacs_standard_properties.end, "end"), + (emacs_standard_properties.contents_begin, "contents-begin"), + (emacs_standard_properties.contents_end, "contents-end"), + (emacs_standard_properties.post_blank, "post-blank"), + ] { + match (emacs_value, wasm_standard_properties.get(wasm_name)) { + (None, None) | (None, Some(serde_json::Value::Null)) => {} + (None, Some(_)) => { + layer.status.push(WasmDiffStatus::Bad( + format!( + "Elisp node lacked field present in wasm node. Name=({name}).", + name = wasm_name, + ) + .into(), + )); + } + (Some(_), None) => { + layer.status.push(WasmDiffStatus::Bad( + format!( + "Wasm node lacked field present in elisp node. Name=({name}).", + name = wasm_name, + ) + .into(), + )); + } + (Some(e), Some(serde_json::Value::Number(w))) + if w.as_u64().map(|w| w as usize) == Some(e) => {} + (Some(e), Some(w)) => { + layer.status.push(WasmDiffStatus::Bad( + format!( + "Property value mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = e, + wasm = w, + ) + .into(), + )); + } + } + } + result.children.push(layer); + } + + { + // Compare children. + let mut layer = WasmDiffResult::<'_> { + name: "children".into(), + ..Default::default() + }; + if let Some(wasm_iter) = wasm + .get("children") + .and_then(|children| children.as_array()) + .map(|children| children.iter()) + { + layer.extend(wasm_compare_list(source, emacs_list_iter, wasm_iter)?)?; + } else { + layer.extend(wasm_compare_list( + source, + emacs_list_iter, + std::iter::empty::<&serde_json::Value>(), + )?)?; + } + result.children.push(layer); + } + + Ok(result) +} + +fn wasm_key_to_emacs_key(wasm_key: WK) -> String { + format!(":{key}", key = wasm_key) +} + +fn compare_quoted_string<'s>( + _source: &'s str, + emacs: &str, + wasm: &String, +) -> Result, Box> { + let mut result = WasmDiffResult::default(); + let emacs_text = unquote(emacs)?; + if wasm.as_str() != emacs_text { + result.status.push(WasmDiffStatus::Bad( + format!( + "Text mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_text, + wasm = wasm, + ) + .into(), + )); + } + Ok(result) +} + +pub(crate) fn wasm_get_emacs_standard_properties( + attributes_map: &HashMap<&str, &Token<'_>>, +) -> Result> { + 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()? + .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())?; + EmacsStandardProperties { + begin, + post_affiliated, + contents_begin, + contents_end, + end, + post_blank, + } + } else { + let begin = maybe_token_to_usize(attributes_map.get(":begin").copied())?; + let end = maybe_token_to_usize(attributes_map.get(":end").copied())?; + let contents_begin = maybe_token_to_usize(attributes_map.get(":contents-begin").copied())?; + let contents_end = maybe_token_to_usize(attributes_map.get(":contents-end").copied())?; + let post_blank = maybe_token_to_usize(attributes_map.get(":post-blank").copied())?; + let post_affiliated = + maybe_token_to_usize(attributes_map.get(":post-affiliated").copied())?; + EmacsStandardProperties { + begin, + post_affiliated, + contents_begin, + contents_end, + end, + post_blank, + } + }) +} + +fn wasm_compare_list<'e, 's: 'e, 'w, EI, WI>( + source: &'s str, + mut emacs: EI, + wasm: WI, +) -> Result, Box> +where + EI: Iterator> + ExactSizeIterator, + WI: Iterator + ExactSizeIterator, +{ + let emacs_length = emacs.len(); + let wasm_length = wasm.len(); + if emacs_length == 1 && wasm_length == 0 && emacs.all(|t| matches!(t.as_atom(), Ok(r#""""#))) { + return Ok(WasmDiffResult::default()); + } + if emacs_length != wasm_length { + return Ok(WasmDiffResult { + status: vec![WasmDiffStatus::Bad( + format!( + "Child length mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_length, + wasm = wasm_length + ) + .into(), + )], + children: Vec::new(), + name: "".into(), + }); + } + + let mut child_status = Vec::with_capacity(emacs_length); + for (emacs_child, wasm_child) in emacs.zip(wasm) { + child_status.push(compare_json_value(source, emacs_child, wasm_child)?); + } + Ok(WasmDiffResult { + status: Vec::new(), + children: child_status, + name: "".into(), + }) +} + +fn compare_optional_pair<'s>( + source: &'s str, + emacs: &Vec>, + wasm: &serde_json::Map, +) -> Result, Box> { + let mut result = WasmDiffResult::default(); + let wasm_optval = wasm + .get("optval") + .ok_or(r#"Wasm optional pair should have an "optval" attribute."#)?; + let wasm_val = wasm + .get("val") + .ok_or(r#"Wasm optional pair should have an "optval" attribute."#)?; + if let serde_json::Value::Null = wasm_optval { + // If the optval is null, then the elisp should have just a single value of a quoted string. + if emacs.len() != 1 { + return Ok(WasmDiffResult { + status: vec![WasmDiffStatus::Bad( + format!( + "Optional pair with null optval should have 1 element. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs, + wasm = wasm + ) + .into(), + )], + children: Vec::new(), + name: "".into(), + }); + } + + let emacs_val = emacs + .first() + .expect("If-statement proves this will be Some."); + result.extend(compare_json_value(source, emacs_val, wasm_val)?)?; + } else { + // If the optval is not null, then the elisp should have 3 values, the optval, a dot, and the val. + if emacs.len() != 3 { + return Ok(WasmDiffResult { + status: vec![WasmDiffStatus::Bad( + format!( + "Optional pair with non-null optval should have 3 elements. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs, + wasm = wasm + ) + .into(), + )], + children: Vec::new(), + name: "".into(), + }); + } + let emacs_optval = emacs + .get(2) + .expect("If-statement proves this will be Some."); + let emacs_val = emacs + .first() + .expect("If-statement proves this will be Some."); + result.extend(compare_json_value(source, emacs_optval, wasm_optval)?)?; + result.extend(compare_json_value(source, emacs_val, wasm_val)?)?; + } + Ok(result) +} + +fn compare_object_tree<'s>( + source: &'s str, + emacs: &[Token<'s>], + wasm: &serde_json::Map, +) -> Result, Box> { + let mut result = WasmDiffResult::default(); + let wasm_attributes = wasm + .get("object-tree") + .ok_or(r#"Wasm object tree should have an "object-tree" attribute."#)? + .as_array() + .ok_or(r#"Wasm "object-tree" attribute should be a list."#)?; + let emacs_outer_length = emacs.len(); + let wasm_outer_length = wasm_attributes.len(); + if emacs_outer_length != wasm_outer_length { + result.status.push(WasmDiffStatus::Bad( + format!( + "Child length mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_outer_length, + wasm = wasm_outer_length + ) + .into(), + )); + return Ok(result); + } + + for (emacs_attribute, wasm_attribute) in emacs.iter().zip(wasm_attributes.iter()) { + let wasm_attribute = wasm_attribute + .as_array() + .ok_or("Wasm middle layer in object tree should be a list.")?; + if wasm_attribute.len() != 2 { + result.status.push(WasmDiffStatus::Bad( + format!( + "Wasm middle layer in object tree should have a length of 2. Wasm=({wasm:?}).", + wasm = wasm_attribute.len() + ) + .into(), + )); + return Ok(result); + } + if let Ok("nil") = emacs_attribute.as_atom() { + if let Some(serde_json::Value::Null) = wasm_attribute.first() { + if let Some(serde_json::Value::Array(w)) = wasm_attribute.get(1) { + if w.is_empty() { + continue; + } + } + } + } + let emacs_attribute = emacs_attribute.as_list()?; + if let Some(serde_json::Value::Null) = wasm_attribute.first() { + // If optval is null then the emacs array should only contain 1 value. + if emacs_attribute.len() != 1 { + result.status.push(WasmDiffStatus::Bad( + format!( + "Emacs middle layer in object tree should have a length of 1. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_attribute, + wasm = wasm_attribute + ) + .into(), + )); + return Ok(result); + } + + let emacs_val = emacs_attribute + .first() + .expect("If-statement proves this will be Some."); + let wasm_val = wasm_attribute + .get(1) + .expect("If-statement proves this will be Some."); + result.extend(compare_json_value(source, emacs_val, wasm_val)?)?; + } else { + // If optval is not null, then the emacs array should contain a list, the first child of which is a list for optval, and all other entries to the outer list are the val. + if emacs_attribute.len() < 2 { + result.status.push(WasmDiffStatus::Bad( + format!( + "Emacs middle layer in object tree should have a length of at least 2. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_attribute, + wasm = wasm_attribute + ) + .into(), + )); + return Ok(result); + } + let emacs_optval = emacs_attribute.iter().skip(1); + let wasm_optval = wasm_attribute + .first() + .expect("If-statement proves this will be Some.") + .as_array() + .ok_or("first value in wasm object tree should be a list.")?; + let emacs_val = emacs_attribute + .first() + .ok_or("If-statement proves this will be Some.")?; + let wasm_val = wasm_attribute + .get(1) + .expect("If-statement proves this will be Some.") + .as_array() + .ok_or("2nd value in wasm object tree should be a list.")?; + result.extend(wasm_compare_list(source, emacs_optval, wasm_optval.iter())?)?; + if let Ok("nil") = emacs_val.as_atom() { + result.extend(wasm_compare_list( + source, + std::iter::empty(), + wasm_val.iter(), + )?)?; + } else { + result.extend(wasm_compare_list( + source, + emacs_val.as_list()?.iter(), + wasm_val.iter(), + )?)?; + } + } + } + + Ok(result) +} + +fn compare_number_lines<'s>( + _source: &'s str, + emacs: &[Token<'s>], + wasm: &serde_json::Map, +) -> Result, Box> { + let mut result = WasmDiffResult::default(); + let mut emacs_iter = emacs.iter(); + let emacs_directive = emacs_iter + .next() + .ok_or("Emacs number lines should have 3 children.")? + .as_atom()?; + let emacs_number: i64 = emacs_iter + .nth(1) + .ok_or("Emacs number lines should have 3 children.")? + .as_atom()? + .parse()?; + if wasm.contains_key("new") { + let wasm_number = wasm + .get("new") + .ok_or(r#"Wasm number lines should have a "new" attribute."#)? + .as_i64() + .ok_or(r#"Wasm number lines "new" attribute should be a number."#)?; + if emacs_directive != "new" { + result.status.push(WasmDiffStatus::Bad( + format!( + "Number lines directive mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_directive, + wasm = "new" + ) + .into(), + )); + return Ok(result); + } + if emacs_number != wasm_number { + result.status.push(WasmDiffStatus::Bad( + format!( + "Number lines number mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_number, + wasm = wasm_number + ) + .into(), + )); + return Ok(result); + } + } else if wasm.contains_key("continued") { + let wasm_number = wasm + .get("continued") + .ok_or(r#"Wasm number lines should have a "continued" attribute."#)? + .as_i64() + .ok_or(r#"Wasm number lines "continued" attribute should be a number."#)?; + if emacs_directive != "continued" { + result.status.push(WasmDiffStatus::Bad( + format!( + "Number lines directive mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_directive, + wasm = "continued" + ) + .into(), + )); + return Ok(result); + } + if emacs_number != wasm_number { + result.status.push(WasmDiffStatus::Bad( + format!( + "Number lines number mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_number, + wasm = wasm_number + ) + .into(), + )); + return Ok(result); + } + } else { + result.status.push(WasmDiffStatus::Bad( + format!( + "Unrecognized number lines directive. Wasm=({wasm:?}).", + wasm = wasm + ) + .into(), + )); + return Ok(result); + } + + Ok(result) +} + +fn compare_string_set<'s>( + _source: &'s str, + emacs: &[Token<'s>], + wasm: &serde_json::Map, +) -> Result, Box> { + let mut result = WasmDiffResult::default(); + let wasm_list = wasm + .get("value") + .ok_or(r#"Wasm string set should have a "value" attribute."#)? + .as_array() + .ok_or(r#"Wasm string set "value" attribute should be a list."#)?; + let wasm_strings = wasm_list + .iter() + .map(|v| v.as_str().ok_or("Non-string in wasm string set.")) + .collect::, &'static str>>()?; + let emacs_strings = emacs + .iter() + .map(|v| v.as_atom()) + .collect::, Box>>()? + .into_iter() + .map(unquote) + .collect::, Box>>()?; + let emacs_strings = emacs_strings + .iter() + .map(|s| s.borrow()) + .collect::>(); + + let mismatched: Vec<_> = emacs_strings + .symmetric_difference(&wasm_strings) + .copied() + .collect(); + if !mismatched.is_empty() { + result.status.push(WasmDiffStatus::Bad( + format!( + "String set mismatch. MismatchedValues=({values:?}).", + values = mismatched.join(", ") + ) + .into(), + )); + } + + Ok(result) +} + +fn is_plain_text(wasm: &serde_json::Map) -> bool { + if let Some(serde_json::Value::String(node_type)) = wasm.get("ast-node") { + node_type == "plain-text" + } else { + false + } +} + +fn compare_plain_text<'s>( + source: &'s str, + emacs: &TextWithProperties<'s>, + wasm: &serde_json::Map, +) -> Result, Box> { + let mut result = WasmDiffResult::<'_> { + name: "plain-text".into(), + ..Default::default() + }; + if !is_plain_text(wasm) { + result.status.push(WasmDiffStatus::Bad( + format!( + "AST node type mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs, + wasm = wasm, + ) + .into(), + )); + return Ok(result); + } + + let emacs_text = unquote(emacs.text)?; + let wasm_standard_properties = wasm + .get("standard-properties") + .ok_or(r#"Wasm AST nodes should have a "standard-properties" attribute."#)? + .as_object() + .ok_or(r#"Wasm ast node "standard-properties" attribute should be an object."#)?; + let wasm_begin = { + if let Some(serde_json::Value::Number(begin)) = wasm_standard_properties.get("begin") { + begin + .as_u64() + .map(|w| w as usize) + .ok_or("Begin should be a number.")? + } else { + result.status.push(WasmDiffStatus::Bad( + format!( + "AST node type mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs, + wasm = wasm, + ) + .into(), + )); + return Ok(result); + } + }; + let wasm_end = { + if let Some(serde_json::Value::Number(end)) = wasm_standard_properties.get("end") { + end.as_u64() + .map(|w| w as usize) + .ok_or("End should be a number.")? + } else { + result.status.push(WasmDiffStatus::Bad( + format!( + "AST node type mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs, + wasm = wasm, + ) + .into(), + )); + return Ok(result); + } + }; + let wasm_text: String = source + .chars() + .skip(wasm_begin - 1) + .take(wasm_end - wasm_begin) + .collect(); + if wasm_text != emacs_text { + result.status.push(WasmDiffStatus::Bad( + format!( + "Text mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_text, + wasm = wasm_text, + ) + .into(), + )); + return Ok(result); + } + + let emacs_start = emacs + .properties + .first() + .map(|t| t.as_atom()) + .map_or(Ok(None), |r| r.map(Some))?; + if emacs_start != Some("0") { + result.status.push(WasmDiffStatus::Bad( + format!( + "Text should start at offset 0. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs, + wasm = wasm, + ) + .into(), + )); + } + let emacs_end = emacs + .properties + .get(1) + .map(|t| t.as_atom()) + .map_or(Ok(None), |r| r.map(Some))?; + if emacs_end != Some((wasm_end - wasm_begin).to_string().as_str()) { + result.status.push(WasmDiffStatus::Bad( + format!( + "Text end mismatch. Emacs=({emacs:?}) Wasm=({wasm:?}).", + emacs = emacs_end, + wasm = wasm_end - wasm_begin, + ) + .into(), + )); + } + + Ok(result) +} diff --git a/src/wasm_test/diff.rs b/src/wasm_test/diff.rs new file mode 100644 index 00000000..815e115d --- /dev/null +++ b/src/wasm_test/diff.rs @@ -0,0 +1,109 @@ +use std::borrow::Cow; + +use crate::util::terminal::foreground_color; +use crate::util::terminal::reset_color; + +#[derive(Debug)] +pub struct WasmDiffResult<'s> { + pub(crate) status: Vec, + pub(crate) name: Cow<'s, str>, + pub(crate) children: Vec>, +} + +#[derive(Debug)] +pub(crate) enum WasmDiffStatus { + #[allow(dead_code)] + Good, + Bad(Cow<'static, str>), +} + +impl<'s> WasmDiffResult<'s> { + pub(crate) fn extend( + &mut self, + other: WasmDiffResult<'s>, + ) -> Result<&mut WasmDiffResult<'s>, Box> { + if self.name.is_empty() { + self.name = other.name; + } + self.status.extend(other.status); + self.children.extend(other.children); + Ok(self) + } + + pub fn is_bad(&self) -> bool { + self.is_self_bad() || self.has_bad_children() + } + + pub fn is_self_bad(&self) -> bool { + self.status + .iter() + .any(|status| matches!(status, WasmDiffStatus::Bad(_))) + } + + pub fn has_bad_children(&self) -> bool { + self.children.iter().any(WasmDiffResult::is_bad) + } + + pub fn print(&self, original_document: &str) -> Result<(), Box> { + self.print_indented(0, original_document) + } + + fn print_indented( + &self, + indentation: usize, + _original_document: &str, + ) -> Result<(), Box> { + let status_text = { + if self.is_self_bad() { + format!( + "{color}BAD{reset}", + color = foreground_color(255, 0, 0), + reset = reset_color(), + ) + } else if self.has_bad_children() { + format!( + "{color}BADCHILD{reset}", + color = foreground_color(255, 255, 0), + reset = reset_color(), + ) + } else { + format!( + "{color}GOOD{reset}", + color = foreground_color(0, 255, 0), + reset = reset_color(), + ) + } + }; + let message = self + .status + .iter() + .filter_map(|status| match status { + WasmDiffStatus::Good => None, + WasmDiffStatus::Bad(message) => Some(message), + }) + .next(); + println!( + "{indentation}{status_text} {name} {message}", + indentation = " ".repeat(indentation), + status_text = status_text, + name = self.name, + message = message.unwrap_or(&Cow::Borrowed("")) + ); + + for child in self.children.iter() { + child.print_indented(indentation + 1, _original_document)?; + } + + Ok(()) + } +} + +impl<'s> Default for WasmDiffResult<'s> { + fn default() -> Self { + WasmDiffResult { + status: Vec::new(), + name: "".into(), + children: Vec::new(), + } + } +} diff --git a/src/wasm_test/mod.rs b/src/wasm_test/mod.rs new file mode 100644 index 00000000..f2432786 --- /dev/null +++ b/src/wasm_test/mod.rs @@ -0,0 +1,8 @@ +mod compare; +mod diff; +mod runner; + +pub use runner::wasm_run_anonymous_compare; +pub use runner::wasm_run_anonymous_compare_with_settings; +pub use runner::wasm_run_compare_on_file; +pub use runner::wasm_run_compare_on_file_with_settings; diff --git a/src/wasm_test/runner.rs b/src/wasm_test/runner.rs new file mode 100644 index 00000000..5921d75e --- /dev/null +++ b/src/wasm_test/runner.rs @@ -0,0 +1,131 @@ +use std::path::Path; + +use super::compare::wasm_compare_document; +use crate::context::GlobalSettings; +use crate::parser::parse_file_with_settings; +use crate::parser::parse_with_settings; +use crate::settings::LocalFileAccessInterface; +use crate::util::cli::emacs_parse_anonymous_org_document; +use crate::util::cli::emacs_parse_file_org_document; +use crate::util::cli::print_versions; +use crate::util::elisp::sexp; +use crate::util::terminal::foreground_color; +use crate::util::terminal::reset_color; +use crate::wasm::ToWasm; +use crate::wasm::ToWasmContext; + +pub async fn wasm_run_anonymous_compare>( + org_contents: P, +) -> Result> { + wasm_run_anonymous_compare_with_settings(org_contents, &GlobalSettings::default(), false).await +} + +pub async fn wasm_run_anonymous_compare_with_settings<'g, 's, P: AsRef>( + org_contents: P, + global_settings: &GlobalSettings<'g, 's>, + silent: bool, +) -> Result> { + // TODO: This is a work-around to pretend that dos line endings do not exist. It would be better to handle the difference in line endings. + let org_contents = org_contents.as_ref().replace("\r\n", "\n"); + let org_contents = org_contents.as_str(); + if !silent { + print_versions().await?; + } + let rust_parsed = parse_with_settings(org_contents, global_settings)?; + let to_wasm_context = ToWasmContext::new(org_contents); + let wasm_parsed = rust_parsed + .to_wasm(to_wasm_context) + .map_err(|_e| "Failed to convert to wasm.")?; + let org_sexp = emacs_parse_anonymous_org_document(org_contents, global_settings).await?; + let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?; + + if !silent { + println!("{}\n\n\n", org_contents); + println!("{}", org_sexp); + println!("{:#?}", rust_parsed); + println!("{}", serde_json::to_string(&wasm_parsed)?); + } + + // We do the diffing after printing out both parsed forms in case the diffing panics + let diff_result = wasm_compare_document(org_contents, &parsed_sexp, &wasm_parsed)?; + if !silent { + diff_result.print(org_contents)?; + } + + if diff_result.is_bad() { + return Ok(false); + } else if !silent { + println!( + "{color}Entire document passes.{reset}", + color = foreground_color(0, 255, 0), + reset = reset_color(), + ); + } + + Ok(true) +} + +//wasm_run_compare_on_file +pub async fn wasm_run_compare_on_file>( + org_path: P, +) -> Result> { + wasm_run_compare_on_file_with_settings(org_path, &GlobalSettings::default(), false).await +} + +pub async fn wasm_run_compare_on_file_with_settings<'g, 's, P: AsRef>( + org_path: P, + global_settings: &GlobalSettings<'g, 's>, + silent: bool, +) -> Result> { + let org_path = org_path.as_ref(); + if !silent { + print_versions().await?; + } + let parent_directory = org_path + .parent() + .ok_or("Should be contained inside a directory.")?; + let org_contents = std::fs::read_to_string(org_path)?; + // TODO: This is a work-around to pretend that dos line endings do not exist. It would be better to handle the difference in line endings. + let org_contents = org_contents.replace("\r\n", "\n"); + let org_contents = org_contents.as_str(); + let file_access_interface = LocalFileAccessInterface { + working_directory: Some(parent_directory.to_path_buf()), + }; + let global_settings = { + let mut global_settings = global_settings.clone(); + global_settings.file_access = &file_access_interface; + global_settings + }; + let rust_parsed = parse_file_with_settings(org_contents, &global_settings, Some(org_path))?; + let to_wasm_context = ToWasmContext::new(org_contents); + let wasm_parsed = rust_parsed + .to_wasm(to_wasm_context) + .map_err(|_e| "Failed to convert to wasm.")?; + let org_sexp = emacs_parse_file_org_document(org_path, &global_settings).await?; + let (_remaining, parsed_sexp) = sexp(org_sexp.as_str()).map_err(|e| e.to_string())?; + + if !silent { + println!("{}\n\n\n", org_contents); + println!("{}", org_sexp); + println!("{:#?}", rust_parsed); + println!("{}", serde_json::to_string(&wasm_parsed)?); + } + + // We do the diffing after printing out both parsed forms in case the diffing panics + let diff_result = wasm_compare_document(org_contents, &parsed_sexp, &wasm_parsed)?; + if !silent { + diff_result.print(org_contents)?; + } + + if diff_result.is_bad() { + return Ok(false); + } else if !silent { + println!( + "{color}Entire document passes.{reset}", + color = foreground_color(0, 255, 0), + reset = reset_color(), + ); + } + + Ok(true) +} diff --git a/tests/test_template b/tests/test_template index e3d7375b..033f0085 100644 --- a/tests/test_template +++ b/tests/test_template @@ -60,3 +60,13 @@ async fn autogen_odd_{name}() -> Result<(), Box> {{ assert!(organic::compare::run_anonymous_compare_with_settings(org_contents.as_str(), &global_settings, false).await?); Ok(()) }} + +#[cfg(feature = "wasm_test")] +{expect_fail} +#[tokio::test] +async fn autogen_wasm_{name}() -> Result<(), Box> {{ + let org_path = "{path}"; + let org_contents = std::fs::read_to_string(org_path).expect("Read org file."); + assert!(organic::wasm_test::wasm_run_anonymous_compare(org_contents.as_str()).await?); + Ok(()) +}}