summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--CONTRIBUTING.md14
-rw-r--r--Cargo.lock1367
-rw-r--r--Cargo.toml32
-rw-r--r--DCO-1-1.txt34
-rw-r--r--DECISIONS.md137
-rw-r--r--NEWS.md98
-rw-r--r--README.md6
-rw-r--r--RELEASE.md12
-rw-r--r--book/user-guide.md66
-rw-r--r--book/user-guide.subplot10
-rwxr-xr-xbuild-docs48
-rw-r--r--build.rs12
-rwxr-xr-xcheck139
-rw-r--r--debian/changelog42
-rw-r--r--debian/control16
-rw-r--r--debian/copyright6
-rwxr-xr-xdebian/rules7
-rw-r--r--debian/source/lintian-overrides2
-rw-r--r--debian/subplot.lintian-overrides3
-rw-r--r--deny.toml1
-rw-r--r--examples/echo/echo.md11
-rw-r--r--examples/echo/echo.subplot10
-rw-r--r--examples/muck/muck.md36
-rw-r--r--examples/muck/muck.subplot12
-rw-r--r--examples/muck/muck.yaml18
-rw-r--r--examples/seq/Cargo.toml4
-rw-r--r--examples/seq/build.rs4
-rw-r--r--examples/seq/seq-extras.rs6
-rw-r--r--examples/seq/seq.subplot11
-rw-r--r--examples/website/website.md12
-rw-r--r--examples/website/website.subplot10
-rw-r--r--flake.lock32
-rw-r--r--flake.nix3
-rw-r--r--reference.md53
-rw-r--r--reference.md-disabled80
-rw-r--r--reference.py6
-rw-r--r--reference.subplot16
-rw-r--r--share/common/lib/files.yaml124
-rw-r--r--share/common/lib/runcmd.yaml77
-rw-r--r--share/python/lib/daemon.yaml69
-rw-r--r--share/python/lib/runcmd.py1
-rw-r--r--share/python/template/scenarios.py10
-rw-r--r--share/rust/lib/datadir.yaml7
-rw-r--r--share/rust/template/macros.rs.tera7
-rw-r--r--share/rust/template/template.rs.tera2
-rw-r--r--share/subplot.css40
-rw-r--r--src/ast.rs484
-rw-r--r--src/bin/cli/mod.rs27
-rw-r--r--src/bin/subplot-filter.rs16
-rw-r--r--src/bin/subplot.rs304
-rw-r--r--src/bindings.rs293
-rw-r--r--src/codegen.rs33
-rw-r--r--src/diagrams.rs42
-rw-r--r--src/doc.rs758
-rw-r--r--src/embedded.rs (renamed from src/datafiles.rs)28
-rw-r--r--src/error.rs116
-rw-r--r--src/html.rs918
-rw-r--r--src/lib.rs31
-rw-r--r--src/matches.rs42
-rw-r--r--src/md.rs801
-rw-r--r--src/metadata.rs435
-rw-r--r--src/panhelper.rs26
-rw-r--r--src/parser.rs43
-rw-r--r--src/policy.rs23
-rw-r--r--src/scenarios.rs44
-rw-r--r--src/steps.rs124
-rw-r--r--src/style.rs2
-rw-r--r--src/typeset.rs229
-rw-r--r--src/visitor/block_class.rs25
-rw-r--r--src/visitor/datafiles.rs35
-rw-r--r--src/visitor/image.rs25
-rw-r--r--src/visitor/linting.rs40
-rw-r--r--src/visitor/mod.rs17
-rw-r--r--src/visitor/structure.rs100
-rw-r--r--src/visitor/typesetting.rs85
-rwxr-xr-xstress2
-rw-r--r--subplot-build/Cargo.toml9
-rw-r--r--subplot-build/src/lib.rs6
-rw-r--r--subplot.md1401
-rw-r--r--subplot.py5
-rw-r--r--subplot.subplot19
-rw-r--r--subplot.yaml28
-rw-r--r--subplotlib-derive/Cargo.toml11
-rw-r--r--subplotlib-derive/src/lib.rs54
-rw-r--r--subplotlib/Cargo.toml17
-rw-r--r--subplotlib/build.rs24
-rw-r--r--subplotlib/helpers/subplotlib_impl.rs2
-rw-r--r--subplotlib/src/file.rs12
-rw-r--r--subplotlib/src/prelude.rs12
-rw-r--r--subplotlib/src/scenario.rs26
-rw-r--r--subplotlib/src/step.rs24
-rw-r--r--subplotlib/src/steplibrary/datadir.rs3
-rw-r--r--subplotlib/src/steplibrary/files.rs94
-rw-r--r--subplotlib/src/steplibrary/runcmd.rs38
-rw-r--r--subplotlib/src/types.rs2
-rw-r--r--subplotlib/src/utils.rs4
-rw-r--r--subplotlib/subplot-rust-support.rs41
-rw-r--r--subplotlib/subplotlib.subplot10
-rw-r--r--tests/bindings-ubm.rs22
-rw-r--r--tests/subplots/common/files.md11
-rw-r--r--tests/subplots/common/files.subplot11
-rw-r--r--tests/subplots/common/runcmd.md17
-rw-r--r--tests/subplots/common/runcmd.subplot16
-rw-r--r--tests/subplots/common/runcmd_test.rs5
-rw-r--r--tests/subplots/common/runcmd_test.yaml7
106 files changed, 6228 insertions, 3568 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3b826dc..b0f385c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,5 +15,5 @@ check-msrv:
script:
- sudo apt update
- sudo apt -y build-dep .
- - rustup default 1.56
- - ./check -v
+ - rustup default 1.70
+ - ./check -v --sloppy
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 95d7ac1..62a9d48 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,9 +36,9 @@ build dependencies, you can skip running the tests yourself.
**Code formatting:** Rust code must be formatted as if by the
`rustfmt` tool. The test suite checks that.
-**Rust version:** We aim for Subplot to be buildable on the stable
+**Rust version:** We aim for Subplot to be buildable on the testing
version of Debian and the Rust version packaged therein. At this time,
-that's Rust version 1.34.
+that's Rust version 1.70.
**Python version:** We require Python code to work on Python 3.7 and
later, the version in the current stable version of Debian.
@@ -78,6 +78,16 @@ get there in the best possible way. You should tell the story of
flying by plane to get somewhere, not how you explored the world and
eventually invented flying machines to travel faster.
+**Sign off your commits:** The commits in a merge request must each
+individually carry a `Signed-off-by` footer. This footer should
+identify the entity which has checked the commit and confirmed that
+it is acceptable for it to be contributed to the Subplot project.
+
+The Subplot project requires that each contribution to it meets
+the assertions listed in the Developer Certificate of Origin version
+1.1 (the text of which can be found in `DCO-1-1.txt` alongside this
+document).
+
# Definition of done
When a change is made to Subplot, it must meet the following minimum
diff --git a/Cargo.lock b/Cargo.lock
index aef690f..2de1d69 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,37 +4,83 @@ version = 3
[[package]]
name = "aho-corasick"
-version = "0.7.18"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
"memchr",
]
[[package]]
-name = "ansi_term"
-version = "0.12.1"
+name = "aligned"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+checksum = "80a21b9440a626c7fc8573a9e3d3a06b75c7c97754c2949bc7857b90353ca655"
dependencies = [
- "winapi",
+ "as-slice",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
-version = "1.0.56"
+version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
+checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]]
-name = "atty"
-version = "0.2.14"
+name = "as-slice"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
dependencies = [
- "hermit-abi",
- "libc",
- "winapi",
+ "stable_deref_trait",
]
[[package]]
@@ -45,9 +91,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
-version = "0.13.0"
+version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bitflags"
@@ -56,52 +102,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
-name = "block-buffer"
-version = "0.7.3"
+name = "bitflags"
+version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
-dependencies = [
- "block-padding",
- "byte-tools",
- "byteorder",
- "generic-array",
-]
+checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
-name = "block-padding"
-version = "0.1.5"
+name = "block-buffer"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
- "byte-tools",
+ "generic-array",
]
[[package]]
name = "bstr"
-version = "0.2.17"
+version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [
"memchr",
+ "serde",
]
[[package]]
-name = "byte-tools"
-version = "0.3.1"
+name = "bumpalo"
+version = "3.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
-
-[[package]]
-name = "byteorder"
-version = "1.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
[[package]]
name = "cc"
-version = "1.0.73"
+version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+checksum = "3286b845d0fccbdd15af433f61c5970e711987036cb468f437ff6badd70f4e24"
[[package]]
name = "cfg-if"
@@ -111,44 +146,33 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
-version = "2.34.0"
+version = "4.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
dependencies = [
- "ansi_term",
- "atty",
- "bitflags",
- "strsim 0.8.0",
- "textwrap 0.11.0",
- "unicode-width",
- "vec_map",
+ "clap_builder",
+ "clap_derive",
]
[[package]]
-name = "clap"
-version = "3.1.18"
+name = "clap_builder"
+version = "4.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
+checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9"
dependencies = [
- "atty",
- "bitflags",
- "clap_derive",
+ "anstream",
+ "anstyle",
"clap_lex",
- "indexmap",
- "lazy_static",
- "strsim 0.10.0",
- "termcolor",
- "textwrap 0.15.0",
+ "strsim",
]
[[package]]
name = "clap_derive"
-version = "3.1.18"
+version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
+checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
- "heck 0.4.0",
- "proc-macro-error",
+ "heck",
"proc-macro2",
"quote",
"syn",
@@ -156,141 +180,150 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.2.0"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
-dependencies = [
- "os_str_bytes",
-]
+checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
-name = "crossbeam-channel"
-version = "0.5.4"
+name = "colorchoice"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [
- "cfg-if",
- "crossbeam-utils",
+ "libc",
]
[[package]]
name = "crossbeam-deque"
-version = "0.8.1"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
- "cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
-version = "0.9.8"
+version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
- "autocfg",
- "cfg-if",
"crossbeam-utils",
- "lazy_static",
- "memoffset",
- "scopeguard",
]
[[package]]
name = "crossbeam-utils"
-version = "0.8.8"
+version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
-dependencies = [
- "cfg-if",
- "lazy_static",
-]
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
-name = "deunicode"
-version = "0.4.3"
+name = "crypto-common"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
[[package]]
-name = "digest"
-version = "0.8.1"
+name = "culpa"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+checksum = "5ae0bfe9317b1cb4ff5a56d766ee4b157b3e1f47f11979253570e88d10fd1fd3"
dependencies = [
- "generic-array",
+ "culpa-macros",
]
[[package]]
-name = "either"
-version = "1.6.1"
+name = "culpa-macros"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+checksum = "1234e1717066d3c71dcf89b75e7b586299e41204d361db56ec51e6ded5014279"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
-name = "env_logger"
-version = "0.7.1"
+name = "cvt"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
+checksum = "d2ae9bf77fbf2d39ef573205d554d87e86c12f1994e9ea335b0651b9b278bcf1"
dependencies = [
- "atty",
- "humantime 1.3.0",
- "log",
- "regex",
- "termcolor",
+ "cfg-if",
]
[[package]]
-name = "env_logger"
-version = "0.9.0"
+name = "deranged"
+version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
- "atty",
- "humantime 2.1.0",
- "log",
- "regex",
- "termcolor",
+ "powerfmt",
]
[[package]]
-name = "fake-simd"
-version = "0.1.2"
+name = "deunicode"
+version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
+checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94"
[[package]]
-name = "fastrand"
-version = "1.7.0"
+name = "digest"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
- "instant",
+ "block-buffer",
+ "crypto-common",
]
[[package]]
-name = "fehler"
-version = "1.0.0"
+name = "env_logger"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5729fe49ba028cd550747b6e62cd3d841beccab5390aa398538c31a2d983635"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
- "fehler-macros",
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
]
[[package]]
-name = "fehler-macros"
-version = "1.0.0"
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccb5acb1045ebbfa222e2c50679e392a71dd77030b78fb0189f2d9c5974400f9"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "libc",
+ "windows-sys 0.52.0",
]
[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[package]]
name = "file_diff"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -298,23 +331,17 @@ checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5"
[[package]]
name = "filetime"
-version = "0.2.15"
+version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98"
+checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
- "winapi",
+ "windows-sys 0.52.0",
]
[[package]]
-name = "fnv"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-
-[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -325,25 +352,40 @@ dependencies = [
]
[[package]]
+name = "fs_at"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982f82cc75107eef84f417ad6c53ae89bf65b561937ca4a3b3b0fd04d0aa2425"
+dependencies = [
+ "aligned",
+ "cfg-if",
+ "cvt",
+ "libc",
+ "nix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
name = "generator"
-version = "0.7.0"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
- "winapi",
+ "windows",
]
[[package]]
name = "generic-array"
-version = "0.12.4"
+version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
+ "version_check",
]
[[package]]
@@ -357,9 +399,9 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.6"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
@@ -368,19 +410,18 @@ dependencies = [
[[package]]
name = "git-testament"
-version = "0.2.1"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "080c47ef3c243fb13474429c14dce386021cd64de731c353998a745c2fa2435b"
+checksum = "710c78d2b68e46e62f5ba63ba0a7a2986640f37f9ecc07903b9ad4e7b2dbfc8e"
dependencies = [
"git-testament-derive",
- "no-std-compat",
]
[[package]]
name = "git-testament-derive"
-version = "0.1.13"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0803898541a48d6f0809fa681bc8d38603f727d191f179631d85ddc3b6a9a2c"
+checksum = "9b31494efbbe1a6730f6943759c21b92c8dc431cb4df177e6f2a6429c3c96842"
dependencies = [
"log",
"proc-macro2",
@@ -391,21 +432,21 @@ dependencies = [
[[package]]
name = "glob"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
-version = "0.4.8"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
+checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
dependencies = [
"aho-corasick",
"bstr",
- "fnv",
"log",
- "regex",
+ "regex-automata 0.4.5",
+ "regex-syntax 0.8.2",
]
[[package]]
@@ -414,54 +455,51 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"ignore",
"walkdir",
]
[[package]]
name = "hashbrown"
-version = "0.11.2"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
-name = "heck"
-version = "0.3.3"
+name = "hashbrown"
+version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "heck"
-version = "0.4.0"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
-version = "0.1.19"
+version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
-dependencies = [
- "libc",
-]
+checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60"
[[package]]
-name = "humansize"
-version = "1.1.1"
+name = "html-escape"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
+dependencies = [
+ "utf8-width",
+]
[[package]]
-name = "humantime"
-version = "1.3.0"
+name = "humansize"
+version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
dependencies = [
- "quick-error",
+ "libm",
]
[[package]]
@@ -472,55 +510,56 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "ignore"
-version = "0.4.18"
+version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
+checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
dependencies = [
- "crossbeam-utils",
+ "crossbeam-deque",
"globset",
- "lazy_static",
"log",
"memchr",
- "regex",
+ "regex-automata 0.4.5",
"same-file",
- "thread_local",
"walkdir",
"winapi-util",
]
[[package]]
name = "indexmap"
-version = "1.8.1"
+version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
- "hashbrown",
+ "hashbrown 0.12.3",
]
[[package]]
-name = "instant"
-version = "0.1.12"
+name = "indexmap"
+version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
dependencies = [
- "cfg-if",
+ "equivalent",
+ "hashbrown 0.14.3",
]
[[package]]
-name = "itertools"
-version = "0.8.2"
+name = "is-terminal"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
dependencies = [
- "either",
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.52.0",
]
[[package]]
name = "itoa"
-version = "1.0.1"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "lazy_static"
@@ -530,30 +569,45 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.123"
+version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb691a747a7ab48abc15c5b42066eaafde10dc427e3b6ee2a1cf43db04c763bd"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
+[[package]]
+name = "line-col"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e69cdf6b85b5c8dce514f694089a2cf8b1a702f6cd28607bcb3cf296c9778db"
[[package]]
name = "linked-hash-map"
-version = "0.5.4"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "log"
-version = "0.4.16"
+version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8"
-dependencies = [
- "cfg-if",
-]
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "loom"
-version = "0.5.4"
+version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edc5c7d328e32cc4954e8e01193d7f0ef5ab257b5090b70a964e099a36034309"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator",
@@ -565,129 +619,90 @@ dependencies = [
]
[[package]]
-name = "maplit"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
-
-[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
- "regex-automata",
+ "regex-automata 0.1.10",
]
[[package]]
name = "memchr"
-version = "2.4.1"
+version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
[[package]]
-name = "memoffset"
-version = "0.6.5"
+name = "nix"
+version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
- "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "libc",
]
[[package]]
-name = "no-std-compat"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
-
-[[package]]
-name = "num_cpus"
-version = "1.13.1"
+name = "normpath"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+checksum = "ec60c60a693226186f5d6edf073232bfb6464ed97eb22cf3b01c1e8198fd97f5"
dependencies = [
- "hermit-abi",
- "libc",
+ "windows-sys 0.48.0",
]
[[package]]
-name = "num_threads"
-version = "0.1.5"
+name = "nu-ansi-term"
+version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
- "libc",
+ "overload",
+ "winapi",
]
[[package]]
-name = "once_cell"
-version = "1.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
-
-[[package]]
-name = "opaque-debug"
-version = "0.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
-
-[[package]]
-name = "os_str_bytes"
-version = "6.0.0"
+name = "num-conv"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
-name = "pandoc"
-version = "0.8.9"
+name = "once_cell"
+version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d4e5728b4bdfe27803e2376f1e4d07c19dc497d8bfb1bed87878b7815fa989a"
-dependencies = [
- "itertools",
-]
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
-name = "pandoc_ast"
-version = "0.7.3"
+name = "overload"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b960d9b78f94feb2a43ace4dda1d2b924a0d5a0639f399620fb54fe2943a9e7"
-dependencies = [
- "serde",
- "serde_derive",
- "serde_json",
-]
-
-[[package]]
-name = "pandoc_ast"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a940e63c65b34a7be2f847de6847b4bc9b68d74e3f7a5c648ca2fa5317f3bd06"
-dependencies = [
- "serde",
- "serde_derive",
- "serde_json",
-]
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "percent-encoding"
-version = "2.1.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
-version = "2.1.3"
+version = "2.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546"
dependencies = [
+ "memchr",
+ "thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
-version = "2.1.0"
+version = "2.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
+checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809"
dependencies = [
"pest",
"pest_generator",
@@ -695,9 +710,9 @@ dependencies = [
[[package]]
name = "pest_generator"
-version = "2.1.3"
+version = "2.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
+checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e"
dependencies = [
"pest",
"pest_meta",
@@ -708,20 +723,20 @@ dependencies = [
[[package]]
name = "pest_meta"
-version = "2.1.3"
+version = "2.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
+checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a"
dependencies = [
- "maplit",
+ "once_cell",
"pest",
- "sha-1",
+ "sha2",
]
[[package]]
name = "pikchr"
-version = "0.1.1"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c0060934b0227a96428cbe79a42ad6d88cfbbc8490027bde64d12348948a6d2"
+checksum = "b430b470a0dfac4e22cd248210e3ef005346acd1ada670d74d6bdcdbab0dc96e"
dependencies = [
"cc",
"libc",
@@ -729,82 +744,48 @@ dependencies = [
[[package]]
name = "pin-project-lite"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
-
-[[package]]
-name = "ppv-lite86"
-version = "0.2.16"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
-name = "pretty_env_logger"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
-dependencies = [
- "env_logger 0.7.1",
- "log",
-]
-
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
+name = "powerfmt"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn",
- "version_check",
-]
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
+name = "ppv-lite86"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
-]
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
-version = "1.0.37"
+version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
- "unicode-xid",
+ "unicode-ident",
]
[[package]]
name = "pulldown-cmark"
-version = "0.9.1"
+version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34f197a544b0c9ab3ae46c359a7ec9cbbb5c7bf97054266fecb7ead794a181d6"
+checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
dependencies = [
- "bitflags",
+ "bitflags 2.4.2",
"getopts",
"memchr",
"unicase",
]
[[package]]
-name = "quick-error"
-version = "1.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
-
-[[package]]
name = "quote"
-version = "1.0.18"
+version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@@ -832,55 +813,32 @@ dependencies = [
[[package]]
name = "rand_core"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
-name = "rayon"
-version = "1.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221"
-dependencies = [
- "autocfg",
- "crossbeam-deque",
- "either",
- "rayon-core",
-]
-
-[[package]]
-name = "rayon-core"
-version = "1.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4"
-dependencies = [
- "crossbeam-channel",
- "crossbeam-deque",
- "crossbeam-utils",
- "num_cpus",
-]
-
-[[package]]
name = "redox_syscall"
-version = "0.2.13"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
]
[[package]]
name = "regex"
-version = "1.5.5"
+version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax",
+ "regex-automata 0.4.5",
+ "regex-syntax 0.8.2",
]
[[package]]
@@ -889,62 +847,85 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
- "regex-syntax",
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.2",
]
[[package]]
name = "regex-syntax"
-version = "0.6.25"
+version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
-name = "remove_dir_all"
-version = "0.5.3"
+name = "regex-syntax"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
-dependencies = [
- "winapi",
-]
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "remove_dir_all"
-version = "0.7.0"
+version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "882f368737489ea543bc5c340e6f3d34a28c39980bd9a979e47322b26f60ac40"
+checksum = "23895cfadc1917fed9c6ed76a8c2903615fa3704f7493ff82b364c6540acc02b"
dependencies = [
+ "aligned",
+ "cfg-if",
+ "cvt",
+ "fs_at",
+ "lazy_static",
"libc",
- "log",
- "num_cpus",
- "rayon",
- "winapi",
+ "normpath",
+ "windows-sys 0.45.0",
]
[[package]]
name = "roadmap"
-version = "0.4.0"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cca2c64101781867800fc47961ae6fe868e5ee19c805fd020b69deb60af864b4"
+checksum = "a129e44a647b309ed394a092e21eabcb58537802c6912920ef4ea76239421234"
dependencies = [
"anyhow",
"serde",
- "serde_yaml",
- "structopt",
- "textwrap 0.14.2",
+ "serde_yaml 0.8.26",
+ "textwrap",
"thiserror",
]
[[package]]
+name = "rustix"
+version = "0.38.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
+dependencies = [
+ "bitflags 2.4.2",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
name = "rustversion"
-version = "1.0.6"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
-version = "1.0.9"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "same-file"
@@ -957,30 +938,24 @@ dependencies = [
[[package]]
name = "scoped-tls"
-version = "1.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
-
-[[package]]
-name = "scopeguard"
-version = "1.1.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "serde"
-version = "1.0.136"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-aux"
-version = "3.0.1"
+version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93abf9799c576f004252b2a05168d58527fb7c54de12e94b4d12fe3475ffad24"
+checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95"
dependencies = [
"serde",
"serde_json",
@@ -988,9 +963,9 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.136"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
@@ -999,9 +974,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.79"
+version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
@@ -1010,33 +985,45 @@ dependencies = [
[[package]]
name = "serde_yaml"
-version = "0.8.23"
+version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0"
+checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
"ryu",
"serde",
"yaml-rust",
]
[[package]]
-name = "sha-1"
-version = "0.8.2"
+name = "serde_yaml"
+version = "0.9.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f"
dependencies = [
- "block-buffer",
+ "indexmap 2.2.3",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
"digest",
- "fake-simd",
- "opaque-debug",
]
[[package]]
name = "sharded-slab"
-version = "0.1.4"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
@@ -1049,94 +1036,69 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "slug"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4"
dependencies = [
"deunicode",
+ "wasm-bindgen",
]
[[package]]
name = "smallvec"
-version = "1.8.0"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smawk"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
+checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "state"
-version = "0.5.2"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cf4f5369e6d3044b5e365c9690f451516ac8f0954084622b49ea3fde2f6de5"
+checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b"
dependencies = [
"loom",
]
[[package]]
name = "strsim"
-version = "0.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
-
-[[package]]
-name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
-name = "structopt"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
-dependencies = [
- "clap 2.34.0",
- "lazy_static",
- "structopt-derive",
-]
-
-[[package]]
-name = "structopt-derive"
-version = "0.4.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
-dependencies = [
- "heck 0.3.3",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "subplot"
-version = "0.4.0"
+version = "0.9.0"
dependencies = [
"anyhow",
"base64",
- "clap 3.1.18",
- "env_logger 0.9.0",
+ "clap",
+ "env_logger",
"file_diff",
"git-testament",
+ "html-escape",
"lazy_static",
+ "line-col",
"log",
- "pandoc",
- "pandoc_ast 0.7.3",
- "pandoc_ast 0.8.0",
"pikchr",
- "pretty_env_logger",
"pulldown-cmark",
"regex",
"roadmap",
"serde",
"serde-aux",
"serde_json",
- "serde_yaml",
+ "serde_yaml 0.9.32",
"tempfile",
"tempfile-fast",
"tera",
@@ -1147,7 +1109,7 @@ dependencies = [
[[package]]
name = "subplot-build"
-version = "0.4.0"
+version = "0.9.0"
dependencies = [
"subplot",
"tempfile",
@@ -1158,23 +1120,23 @@ dependencies = [
name = "subplot-seq-example"
version = "0.1.0"
dependencies = [
- "fehler",
+ "culpa",
"subplot-build",
"subplotlib",
]
[[package]]
name = "subplotlib"
-version = "0.4.1"
+version = "0.9.0"
dependencies = [
"base64",
- "fehler",
+ "culpa",
"filetime",
"fs2",
"glob",
"lazy_static",
"regex",
- "remove_dir_all 0.7.0",
+ "remove_dir_all",
"serde_json",
"shell-words",
"state",
@@ -1187,9 +1149,9 @@ dependencies = [
[[package]]
name = "subplotlib-derive"
-version = "0.4.0"
+version = "0.9.0"
dependencies = [
- "fehler",
+ "culpa",
"proc-macro2",
"quote",
"syn",
@@ -1197,27 +1159,25 @@ dependencies = [
[[package]]
name = "syn"
-version = "1.0.91"
+version = "2.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d"
+checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
dependencies = [
"proc-macro2",
"quote",
- "unicode-xid",
+ "unicode-ident",
]
[[package]]
name = "tempfile"
-version = "3.3.0"
+version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
dependencies = [
"cfg-if",
"fastrand",
- "libc",
- "redox_syscall",
- "remove_dir_all 0.5.3",
- "winapi",
+ "rustix",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -1233,9 +1193,9 @@ dependencies = [
[[package]]
name = "tera"
-version = "1.15.0"
+version = "1.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3cac831b615c25bcef632d1cabf864fa05813baad3d526829db18eb70e8b58d"
+checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8"
dependencies = [
"globwalk",
"humansize",
@@ -1253,27 +1213,18 @@ dependencies = [
[[package]]
name = "termcolor"
-version = "1.1.3"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
-version = "0.11.0"
+version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
-dependencies = [
- "unicode-width",
-]
-
-[[package]]
-name = "textwrap"
-version = "0.14.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
+checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
dependencies = [
"smawk",
"unicode-linebreak",
@@ -1281,25 +1232,19 @@ dependencies = [
]
[[package]]
-name = "textwrap"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
-
-[[package]]
name = "thiserror"
-version = "1.0.30"
+version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.30"
+version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
@@ -1308,38 +1253,51 @@ dependencies = [
[[package]]
name = "thread_local"
-version = "1.1.4"
+version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
+ "cfg-if",
"once_cell",
]
[[package]]
name = "time"
-version = "0.3.9"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
+checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [
+ "deranged",
"itoa",
- "libc",
- "num_threads",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
"time-macros",
]
[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
name = "time-macros"
-version = "0.2.4"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
+checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
[[package]]
name = "tracing"
-version = "0.1.34"
+version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
- "cfg-if",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -1347,9 +1305,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.20"
+version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
@@ -1358,34 +1316,34 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.26"
+version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
- "lazy_static",
+ "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
-version = "0.1.2"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
- "lazy_static",
"log",
+ "once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
-version = "0.3.11"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
- "ansi_term",
- "lazy_static",
"matchers",
+ "nu-ansi-term",
+ "once_cell",
"regex",
"sharded-slab",
"smallvec",
@@ -1397,15 +1355,15 @@ dependencies = [
[[package]]
name = "typenum"
-version = "1.15.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-trie"
-version = "0.1.3"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
[[package]]
name = "unescape"
@@ -1465,51 +1423,54 @@ dependencies = [
[[package]]
name = "unicase"
-version = "2.6.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]]
-name = "unicode-linebreak"
-version = "0.1.2"
+name = "unicode-ident"
+version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
-dependencies = [
- "regex",
-]
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
-name = "unicode-segmentation"
-version = "1.9.0"
+name = "unicode-linebreak"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-width"
-version = "0.1.9"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
-name = "unicode-xid"
-version = "0.2.2"
+name = "unsafe-libyaml"
+version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
[[package]]
-name = "valuable"
-version = "0.1.0"
+name = "utf8-width"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
[[package]]
-name = "vec_map"
-version = "0.8.2"
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
@@ -1519,20 +1480,73 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
-version = "2.3.2"
+version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
- "winapi",
"winapi-util",
]
[[package]]
name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
[[package]]
name = "winapi"
@@ -1552,9 +1566,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
-version = "0.1.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
@@ -1566,6 +1580,213 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.3",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.3",
+ "windows_aarch64_msvc 0.52.3",
+ "windows_i686_gnu 0.52.3",
+ "windows_i686_msvc 0.52.3",
+ "windows_x86_64_gnu 0.52.3",
+ "windows_x86_64_gnullvm 0.52.3",
+ "windows_x86_64_msvc 0.52.3",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6"
+
+[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 7e07c83..658733e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,55 +1,49 @@
[package]
name = "subplot"
-version = "0.4.0"
+version = "0.9.0"
authors = [
"Lars Wirzenius <liw@liw.fi>",
"Daniel Silverstone <dsilvers@digital-scurf.org>",
]
-edition = "2018"
+edition = "2021"
license = "MIT-0"
description = '''
tools for specifying, documenting,
and implementing automated acceptance tests for systems and software'''
-homepage = "https://subplot.liw.fi/"
+homepage = "https://subplot.tech/"
repository = "https://gitlab.com/subplot/subplot"
default-run = "subplot"
+rust-version = "1.70"
[workspace]
members = ["subplotlib", "subplotlib-derive", "subplot-build", "examples/seq"]
-[features]
-default = ["ast_07"]
-ast_07 = ["pandoc_ast_07"]
-ast_08 = ["pandoc_ast_08"]
-
[dependencies]
anyhow = "1"
-base64 = "0.13.0"
-clap = { version = "3", features = ["derive", "env"] }
+base64 = "0.21.0"
+clap = { version = "4", features = ["derive", "env", "string"] }
file_diff = "1"
git-testament = "0.2"
lazy_static = "1"
log = "0.4.16"
-pandoc = "0.8.0"
-pandoc_ast_07 = { package = "pandoc_ast", version = "0.7", optional = true }
-pandoc_ast_08 = { package = "pandoc_ast", version = "0.8", optional = true }
pikchr = "0.1"
-pretty_env_logger = "0.4.0"
pulldown-cmark = "0.9.0"
regex = "1"
-roadmap = "0.4"
+roadmap = "0.5.0"
serde = { version = "1.0.101", features = ["derive"] }
-serde-aux = { version = "3.0", default-features = false }
+serde-aux = { version = "4.0", default-features = false }
serde_json = "1.0"
-serde_yaml = "0.8.11"
+serde_yaml = "0.9.21"
tempfile = "3.1.0"
tempfile-fast = "0.3.1"
thiserror = "1"
time = { version = "0.3", features = ["formatting", "macros"] }
-env_logger = "0.9.0"
+env_logger = "0.10.0"
+html-escape = "0.2.13"
+line-col = "0.2.1"
[dependencies.tera]
-version = "1"
+version = "1.18"
default-features = false
# Expand out tera's default featurs, except for chrono related ones
features = ["slug", "percent-encoding", "humansize", "rand"]
diff --git a/DCO-1-1.txt b/DCO-1-1.txt
new file mode 100644
index 0000000..49b8cb0
--- /dev/null
+++ b/DCO-1-1.txt
@@ -0,0 +1,34 @@
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
diff --git a/DECISIONS.md b/DECISIONS.md
index fe67b1b..b192fdc 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -13,18 +13,143 @@ Each decision should have its own heading. Newest decision should come
first. Updated or overturned decisions should have their section
updated to note their status, without moving them.
+## Adopting a Developer Certificate of Origin
+
+Date: 2023-10-07
+
+What: The subplot project is adopting the use of DCO.
+
+Why: To reduce the chance that, in the future when others might wish
+to contribute to Subplot, anyone might claim that we have changes
+which are not permitted, but without incurring the potential additional
+complications associated with CLAs or the like.
+
+Who: Daniel, Lars
+
+Detail: A Developer Certificate of Origin is a statement saying that
+the contributor has the right to make a contribution and to assign
+permission to the project to thence redistribute it under the project
+licence. You can read more at: <https://developercertificate.org/>
+and at <https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin>
+
+This decision was taken during a Subplot project meeting and the
+current scope of the decision is to document our use of DCO 1.1 in
+the contribution guidelines, and to add to our merge request review
+process a human-driven validation of the following of the DCO.
+
+We leave open the possibility of enforcing DCO in some programmatic
+way in the future.
+
+We assert that the presence of a `Signed-off-by` footer in each git
+commit in a merge request is the mechanism by which a developer signals
+that the commit meets the assertions in the DCO 1.1.
+
+## Breaking changes in Subplot
+
+Date: 2023-08-27
+
+What: Specify what is, and isn't, a breaking change in Subplot.
+
+Why: We don't want to cause unnecessary work for users of Subplot, to
+adapt to changes we make in Subplot. We want to be considerate and
+also we don't want to scare off people who like Subplot, but don't
+want to deal with a constant churn of busywork.
+
+Who: Daniel, Lars
+
+Details: Our main guideline should be that if an existing, working
+subplot document or the code generated from it stops working after a
+change, it was a breaking change. However, the stricter we are about
+this, the harder it will be for us to make any changes, and thus we
+think we should, at least in this stage of Subplot's development, be
+more relaxed. We propose we keep two lists of criteria: one for things
+we do consider to be breaking changes, and we don't.
+
+We should consider changes from release to release, not just per
+commit.
+
+We need to consider:
+
+* the command line interface
+* the Rust libraries we provide
+* the YAML metadata file
+* the markdown files
+* bindings files
+* step implementation files
+* the step libraries provided by Subplot: the steps and their
+ implementation; this means the bindings files and the Python and
+ Rust code for the step implementations, and the "context" used or
+ modified by step implementations
+
+For the "yes, these are likely to be breaking changes":
+
+* Dropping or renaming, the name of the Subplot binary.
+* Dropping or renaming a command line option or subcommand, or the
+ arguments they take.
+* Changing what output files are produced by the `subplot` command.
+* Changing Subplot so that users would have to upgrade both
+ `subplotlib` and `subplot-build` at the same time.
+* Dropping or changing the meaning of a YAML metadata or bindings file
+ key. Adding keys is not a breaking change (see below).
+* Dropping support for or changing the meaning of a markdown feature.
+* Dropping or renaming a step library.
+* Dropping an implementation language for a step library.
+* Dropping or renaming a step in a step library, or what captures it
+ takes.
+* Changing to behavior of a step, including how it uses or modifies
+ the context, in a way that an existing use of Subplot breaks or
+ changes behavior in an unwanted way.
+* Changing the output of `docgen` in a way that affects the meaning of
+ the document.
+* Changing or removing any exported part of a `subplotlib` context object,
+ or any of the documented portions of the Python or Bash contexts.
+
+For the "sorry, these aren't breaking changes":
+
+* Anything that doesn't affect existing uses of Subplot.
+* Adding new steps to the step libraries provided by the Subplot
+ project. The new step may clash with an existing use of Subplot, but
+ try to avoid this by phrasing steps carefully. However, if we can't
+ add new steps, it becomes impossible to improve the libraries.
+* Typographical and document navigation changes in `docgen` output.
+ This includes output formats, navigational elements, automatically
+ generated "anchors" for document elements, and the way document
+ elements are typeset.
+
+
+## Output format for `docgen`
+
+Date: 2023-03-21
+
+What: `subplot docgen` will only support HTML as the output format. We
+no longer support PDF.
+
+Why: We want to drop support of Pandoc for parsing markdown input
+files, and this is simpler to achieve if we don't need to support PDF
+output, as it relieves us from the need to produce an abstract syntax
+tree in the Pandoc representation. Producing PDF without a Pandoc AST
+is trickier.
+
+Who: Daniel, Lars
+
## Minimum Supported Rust Version
-Date: 2021-10-09
+Date: 2023-03-21
-What: We decided that Subplot would support an MSRV of 1.48.0 in
-order that it can be maximally useful to the Sequoia-PGP project.
-If and when we gain other large client projects, we will endeavour
-to support an MSRV which makes them all happy. We will walk the
-MSRV forward as and when our client projects move forward.
+What: We decided that Subplot would support an MSRV of the version of
+Rust in the Debian "testing" branch. We can bump our explicit MSRV
+version when Debian gets a new version.
Who: Daniel, Lars
+## Threshold for refactoring
+
+Date: 2022-10-23
+
+What: Instead of trying to do a whole code base tidy up, we'll
+continuously do smaller refactoring changes when we see something that
+needs improvement.
+
## Do not clear/override all environment variables
Date: 2021-10-08
diff --git a/NEWS.md b/NEWS.md
index 2fea0ef..61a8537 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -4,6 +4,104 @@ This file summarises the changes between released versions of Subplot and its
associated libraries, especially with regards to changes visible to
the user of the Subplot software.
+# Version UNRELEASED
+
+- Subplot's MSRV has been updated to 1.70 in line with Debian testing.
+
+# Version 0.9.0, released 2023-08-27
+
+- We hope this will be the last breaking change before 1.0, however we
+ are not ruling out future breaks if they are justified to improve
+ usability or capability before an official 1.0 release
+- We now pass a lot more meta-information about step location to the
+ templates for building test suites.
+
+# Version 0.8.0, released 2023-06-14
+
+- Subplot now permits multiple markdown documents to be used in a single
+ subplot document.
+- Indented scenario statements, while permitted before, were never meant
+ to be part of the spec, they are now considered an error in case we
+ wish to use the semantics of indentation later.
+
+# Version 0.7.1, released 2023-04-30
+
+- Subplot now handles scenario titles with markup (such as bold face).
+ This was broken in the changes to drop use of Pandoc for parsing.
+- CI job using the MSRV version doesn't check source code formatting
+ anymore.
+- The dependency on the roadmap crate now depends on a version that
+ doesn't require the `clap` crate at all anymore.
+
+# Version 0.7.0, released 2023-04-10
+
+- Subplot no longer uses `pandoc` at all. This means that
+ output is currently limited to HTML only, and the formatting
+ of that HTML has changed, however this is the first step
+ along the path of being significantly easier to use long-term.
+- Subplot's MSRV has been updated to 1.63 and our plan is to
+ maintain an MSRV of whatever is in Debian's `testing` distribution
+ until Subplot is in Debian.
+- We have updated our crates to the 2021 edition of Rust. This should
+ not affect anyone since the 2021 edition has been supported since
+ 1.56 of Rust.
+
+# Version 0.6.0, released 2022-11-13
+
+- Subplot metadata now expects `authors` rather than `author`
+ to support multiple authors for documents. This is a breaking
+ change, hence the semver bump.
+- Subplot metadata now supports a `pandoc` mapping at the top level
+ which provides metadata to be inserted into the Pandoc document
+ build when producing PDFs or HTML.
+- There is now a `path` type, to go alongside `text` `word` etc.
+ Paths are expected to be (parts of) paths on the filesystem and
+ we have updated all bindings to use `path` where sensible to do so.
+- Subplotlib steps now handle the `path` type as `&Path`, so steps which
+ expect to be given paths should use that, rather than `&str`.
+
+# Version 0.5.0, released 2022-09-13
+
+- The big, breaking change for this release is that Subplot now
+ expects document metadata in a separate YAML file. It was previously
+ embedded in the Markdown input file. This allows us to be more
+ strict, when parsing the metadata: we only need to support what
+ Subplot needs, not the nearly arbitrary YAML Pandoc supports. Later
+ on, it will also enable us to support multiple Markdown files as
+ input.
+
+- That change also means that we drop support for use of Subplot as a
+ Pandoc filter (the `subplot-filter` command and the `subplot filter`
+ subcommand). It doesn't make sense unless the metadata is
+ embedded in the Markdown.
+
+- We've renamed things so that we consistently call a Markdown fenced
+ code block that is marked as a data file, an "embedded file".
+ Previously we also used other names, causing unnecessary confusion.
+
+- The new home page URL is updated in all crate metadata. This means
+ crates.io will point at the new location after this release is made.
+
+# Version 0.4.3, released 2022-007-29
+
+- Bump the dependency on the `roadmap` crate to a version that depends
+ on `clap` version 3 to that it doesn't drag in `structopt`.
+
+# Version 0.4.2, released 2022-007-28
+
+- New feature: Subplot now supports example blocks: embedded files
+ that are in the subplot document only as examples, not for use by
+ scenarios.
+
+- Subplot error messages are now a little bit more specific as to the
+ cause, and what operation failed.
+
+- Subplot now uses the `time` crate instead of the `chrono` crate to
+ avoid a potential security issue.
+
+- Terminology change: we now refer to diagrams as diagrams, not
+ graphs.
+
# Version 0.4.1 (subplotlib only), released 2022-04-17
- Fix issue where subplotlib cannot be built out-of-tree
diff --git a/README.md b/README.md
index 9cb8a0a..1ef0d5f 100644
--- a/README.md
+++ b/README.md
@@ -48,10 +48,8 @@ You'll need to install some build dependencies. On a system running
Debian or a derivative of it:
~~~sh
-$ sudo apt-get install build-essential git debhelper dh-cargo python3 \
- pandoc texlive-latex-base texlive-latex-recommended \
- texlive-fonts-recommended librsvg2-bin graphviz pandoc-citeproc \
- plantuml daemonize lmodern procps
+$ sudo apt-get install build-essential git debhelper python3 \
+ librsvg2-bin graphviz plantuml daemonize procps
~~~
Additionally, any packages reported by running the following command:
diff --git a/RELEASE.md b/RELEASE.md
index 652b9bf..98d6ef7 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -37,7 +37,7 @@ first:
for the new release, if there's been any changes. If any of the
other crates depend on this crate, update their dependency
information in their `Cargo.toml` as needed.
-5. Run `cargo publish --dry-run` and fix any problems.
+5. Run `cargo publish --dry-run --allow-dirty` and fix any problems.
For the top crate `subplot` additionally do the following:
@@ -45,6 +45,8 @@ For the top crate `subplot` additionally do the following:
successfully.
2. Update `NEWS.md` with an end-user oriented summary of the changes
in all crates since the previous release, if there are any.
+ Particularly check the [Fixed in next version][fixedstuff] issues
+ against the NEWS.
3. Update `debian/changelog` with a summary of any changes to the
Debian packaging. Use the `dch` command to edit the file to get the
format right: `dch -v X.Y.Z-1 "New release"` to start a new
@@ -52,6 +54,11 @@ For the top crate `subplot` additionally do the following:
and `dch -r ""` to mark the version as ready for release.
4. Commit the changes in all crates and submit as a merge request via
GitLab.
+ Ensure the MR contains references to close all of the
+ [Fixed in next version][fixedstuff] issues so that all the issue
+ authors get notified of the release.
+
+[fixedstuff]: https://gitlab.com/subplot/subplot/-/issues?label_name[]=fixed-in-next-version
Once the above changes have been merged, make the actual release (note
that these steps are meant to be possible to automate, later on, and
@@ -70,3 +77,6 @@ require no review cycle):
`git.liw.fi` so that a Debian package gets built and published.
6. Announce our jubilation to the world via blog posts and other
suitable channels.
+ - fediverse, with #subplot hashtag
+ - Subplot website blog
+ - personal blogs
diff --git a/book/user-guide.md b/book/user-guide.md
new file mode 100644
index 0000000..c41abb5
--- /dev/null
+++ b/book/user-guide.md
@@ -0,0 +1,66 @@
+# Introduction
+
+ - who is this manual for?
+ - what is Subplot meant for?
+ - who is Subplot meant for?
+ - history of Subplot
+ - public use cases of Subplot
+
+# An overview of acceptance criteria and their verification
+
+ - discuss acceptance criteria vs requirements; functional vs
+ non-functional requirements; automated vs manual testing
+ - discuss stakeholders
+ - discuss different approaches for verifying that a system meets it
+ criteria
+ - discuss how scenarios can be used to verify acceptance criteria
+
+# Simple example project
+
+ - discuss how to use Subplot for some simple, but not simplistic,
+ software project
+ - discuss different kinds of stakeholders a project may have
+
+# Authoring Subplot documents
+
+ - discuss that it may be necessary to have several documents for
+ different audiences, at different levels of abstraction (cf. the
+ FOSDEM safety devroom talk)
+ - discuss writing style to target all the different stakeholders
+ - discuss mechanics and syntax
+ - Markdown and supported features
+ - scenarios, embedded files, examples
+ - bindings
+ - step implementations in various languages
+ - embedded markup for diagrams
+ - running docgen
+
+# Extended example project
+
+ - discuss how to use Subplot for a significant project, but keep it
+ sufficiently high level that it doesn't get too long and tedious
+ to read
+
+# Appendix: Implementing scenario steps in Bash
+
+ - this appendix will explain how to implement scenario steps using
+ the Bash shell
+
+# Appendix: Implementing scenario steps in Python
+
+ - this appendix will explain how to implement scenario steps using
+ the Python language
+
+# Appendix: Implementing scenario steps in Rust
+
+ - this appendix will explain how to implement scenario steps using
+ the Rust language
+
+
+# Appendix: Scenario
+
+This is currently necessary so that codegen won't barf.
+
+~~~scenario
+when I run true
+~~~
diff --git a/book/user-guide.subplot b/book/user-guide.subplot
new file mode 100644
index 0000000..2c60f19
--- /dev/null
+++ b/book/user-guide.subplot
@@ -0,0 +1,10 @@
+title: "Subplot user guide"
+authors:
+ - The Subplot project
+markdowns:
+ - user-guide.md
+bindings:
+ - lib/runcmd.yaml
+impls:
+ python:
+ - lib/runcmd.py
diff --git a/build-docs b/build-docs
new file mode 100755
index 0000000..a3a1dc9
--- /dev/null
+++ b/build-docs
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+set -euo pipefail
+
+# Get output directory.
+if [ "$#" != 1 ]; then
+ echo "Usage: $0 OUTPUT-DIR" 1>&2
+ exit 1
+fi
+output="$1"
+
+# Build Subplot so that we can use it to generate
+# documentation.
+
+cargo build
+PATH="${CARGO_TARGET_DIR:-target}:$PATH"
+type -all subplot
+
+# Build subplots that come with Subplot
+
+opts="--resources $(pwd)/src/share"
+find . -name "*.subplot" |
+ grep -Fv .gitlab/ |
+ while read -r file; do
+ base="$(basename "$file" .subplot)"
+ (
+ cd "$(dirname "$file")"
+ # shellcheck disable=SC2154 disable=SC2086
+ if subplot $opts metadata --merciful "$base.subplot" | awk '/^title:/ && NF > 1' | grep .; then
+ subplot $opts docgen --merciful "$base.subplot" -o "$output/$base.html"
+ fi
+ )
+ done
+
+# Build Subplot library documentation.
+
+libdocs="$output/libdocs"
+mkdir -p "$libdocs"
+
+find share -name '*.yaml' ! -name template.yaml |
+ while read -r yaml; do
+ dir="$(dirname "$yaml")"
+ md="$dir/$(basename "$yaml" .yaml).md"
+ subplot "--resources=$(pwd)" libdocgen --output "$md" "$yaml"
+ pandoc --standalone --self-contained \
+ --metadata title="$(basename "$yaml" .yaml)" \
+ -o "$libdocs/$(basename "$md" .md)".html "$md"
+ done
diff --git a/build.rs b/build.rs
index 857452b..2276f93 100644
--- a/build.rs
+++ b/build.rs
@@ -127,15 +127,11 @@ fn write_out_resource_file<'a>(paths: impl Iterator<Item = &'a Path>) -> Result<
/// to contain `BUILTIN_{var}` to either the env value, or `{def}` if not
/// provided.
fn adopt_env_var(var: &str, def: &str) {
- println!("cargo:rerun-if-env-changed=SUBPLOT_{var}", var = var);
- if let Ok(value) = std::env::var(format!("SUBPLOT_{var}", var = var)) {
- println!(
- "cargo:rustc-env=BUILTIN_{var}={value}",
- var = var,
- value = value
- );
+ println!("cargo:rerun-if-env-changed=SUBPLOT_{var}");
+ if let Ok(value) = std::env::var(format!("SUBPLOT_{var}")) {
+ println!("cargo:rustc-env=BUILTIN_{var}={value}",);
} else {
- println!("cargo:rustc-env=BUILTIN_{var}={def}", var = var, def = def);
+ println!("cargo:rustc-env=BUILTIN_{var}={def}");
}
}
diff --git a/check b/check
index f9b331c..2f913b8 100755
--- a/check
+++ b/check
@@ -13,9 +13,10 @@ from subprocess import PIPE, DEVNULL, STDOUT
class Runcmd:
"""Run external commands in various ways"""
- def __init__(self, verbose, progress):
+ def __init__(self, verbose, progress, offline):
self._verbose = verbose
self._progress = progress
+ self.offline = offline
# Deliberately chosen because it's 12:45 / 13:45 offset from UTC
# As such it ought to show any TZ related errors if we're lucky.
self._env = {"TZ": "NZ-CHAT"}
@@ -94,11 +95,6 @@ class Runcmd:
p = self.runcmd_unchecked(["which", name], stdout=DEVNULL)
return p.returncode == 0
- def pandoc_is_newer(self):
- """Is pandoc new enough for --citeproc"""
- p = self.runcmd(["pandoc", "--help"], stdout=PIPE)
- return "--citeproc" in p.stdout.decode("UTF-8")
-
def cargo(self, args, **kwargs):
"""Run cargo with arguments."""
return self.runcmd(["cargo"] + args, **kwargs)
@@ -166,21 +162,38 @@ class Runcmd:
**kwargs,
)
- def get_templates(self, filename):
- metadata = self.cargo(
+ def libdocgen(self, bindings, output):
+ self.cargo(
[
"run",
- "--quiet",
"--package=subplot",
"--bin=subplot",
"--",
f"--resources={os.path.abspath('share')}",
- "metadata",
- "-o",
- "json",
- "--merciful",
- filename,
- ],
+ "libdocgen",
+ "--output",
+ output,
+ bindings,
+ ]
+ )
+
+ def get_templates(self, filename, strict):
+ args = [
+ "run",
+ "--quiet",
+ "--package=subplot",
+ "--bin=subplot",
+ "--",
+ f"--resources={os.path.abspath('share')}",
+ "metadata",
+ "-o",
+ "json",
+ ]
+ if not strict:
+ args += ["--merciful"]
+ args += [filename]
+ metadata = self.cargo(
+ args,
stdout=PIPE,
stderr=PIPE,
).stdout.decode("UTF-8")
@@ -236,13 +249,16 @@ def check_shell(r):
r.runcmd_maybe(["shellcheck"] + sh)
-def check_rust(r, strict=False):
+def check_rust(r, strict=False, sloppy=False):
"""Run all checks for Rust code"""
r.title("checking Rust code")
- r.runcmd(["cargo", "build", "--workspace", "--all-targets"])
+ argv = ["cargo", "build", "--workspace", "--all-targets"]
+ if r.offline:
+ argv.append("--offline")
+ r.runcmd(argv)
- if r.got_cargo("clippy"):
+ if r.got_cargo("clippy") and not sloppy:
argv = [
"cargo",
"clippy",
@@ -259,28 +275,32 @@ def check_rust(r, strict=False):
sys.exit("Strict Rust checks specified, but clippy was not found")
r.runcmd(["cargo", "test", "--workspace"])
- r.runcmd(["cargo", "fmt", "--", "--check"])
+ if not sloppy:
+ r.runcmd(["cargo", "fmt", "--", "--check"])
-def check_subplots(r):
+def check_subplots(r, strict=False):
"""Run all Subplots and generate documents for them"""
output = os.path.abspath("test-outputs")
os.makedirs(output, exist_ok=True)
- mds = find_files(
- "**/*.md",
+ subplots = find_files(
+ "**/*.subplot",
lambda f: f == f.lower() and "subplotlib" not in f and "test-outputs" not in f,
)
- for md0 in mds:
- r.title(f"checking subplot {md0}")
+ if r.offline:
+ r.msg("Only testing subplot.subplot due to --offline")
+ subplots = ["subplot.subplot"]
+ for subplot0 in subplots:
+ r.title(f"checking subplot {subplot0}")
- dirname = os.path.dirname(md0) or "."
- md = os.path.basename(md0)
- base, _ = os.path.splitext(md)
+ dirname = os.path.dirname(subplot0) or "."
+ subplot = os.path.basename(subplot0)
+ base, _ = os.path.splitext(subplot)
doc_template = None
- for template in r.get_templates(md0):
+ for template in r.get_templates(subplot0, strict):
if doc_template is None:
doc_template = template
if template == "python":
@@ -293,7 +313,7 @@ def check_subplots(r):
bindir = get_bin_dir(r)
- r.codegen(md, "python", test_py, cwd=dirname)
+ r.codegen(subplot, "python", test_py, cwd=dirname)
p = r.runcmd_unchecked(
[
"python3",
@@ -310,18 +330,19 @@ def check_subplots(r):
sys.exit(1)
elif template == "bash":
test_sh = os.path.join(output, f"test-{base}.sh")
- r.codegen(md, "bash", test_sh, cwd=dirname)
+ r.codegen(subplot, "bash", test_sh, cwd=dirname)
r.runcmd(["bash", "-x", test_sh], cwd=dirname)
elif template == "rust":
- r.msg(f"Ignoring Rust template in {md0}")
+ r.msg(f"Ignoring Rust template in {subplot0}")
else:
- sys.exit(f"unknown template {template} in {md0}")
+ sys.exit(f"unknown template {template} in {subplot0}")
- base = os.path.basename(md)
- base, _ = os.path.splitext(md)
+ base = os.path.basename(subplot)
+ base, _ = os.path.splitext(subplot)
base = os.path.join(output, base)
- r.docgen(md, doc_template, base + ".pdf", cwd=dirname)
- r.docgen(md, doc_template, base + ".html", cwd=dirname)
+ html = base + ".html"
+ r.docgen(subplot, doc_template, html, cwd=dirname)
+ r.runcmd(["tidy", "-errors", html], cwd=dirname)
def tail(filename, numlines=100):
@@ -342,21 +363,13 @@ def check_tooling(r):
"bash",
"cargo",
"dot",
- "pandoc",
- "pandoc-citeproc",
- "pdflatex",
"plantuml",
"rustc",
"rustfmt",
+ "tidy",
]
for command in commands:
if not r.got_command(command):
- if command == "pandoc-citeproc":
- if r.pandoc_is_newer():
- r.msg(
- " Fortunately pandoc is new enough for --citeproc, no need for pandoc-citeproc"
- )
- continue
sys.exit(f"can't find {command}, which is needed for test suite")
if not r.got_command("daemonize") and not r.got_command("/usr/sbin/daemonize"):
@@ -365,6 +378,26 @@ def check_tooling(r):
)
+def check_doc(r):
+ docs = os.path.join("test-outputs", "libdocs")
+ if not os.path.exists(docs):
+ os.mkdir(docs)
+
+ bindings = []
+ for dirname, _, basenames in os.walk("share"):
+ dirname = dirname[len("share/") :]
+ bindings += [
+ os.path.join(dirname, x)
+ for x in basenames
+ if x != "template.yaml" and x.endswith(".yaml")
+ ]
+
+ for filename in bindings:
+ md = os.path.splitext(os.path.basename(filename))[0] + ".md"
+ md = os.path.join(docs, md)
+ r.libdocgen(filename, md)
+
+
def parse_args():
"""Parse command line arguments to this script"""
p = argparse.ArgumentParser()
@@ -375,8 +408,14 @@ def parse_args():
p.add_argument(
"--strict", action="store_true", help="don't allow compiler warnings"
)
+ p.add_argument(
+ "--sloppy", action="store_true", help="don't check formatting or with clippy"
+ )
+ p.add_argument(
+ "--offline", action="store_true", help="only run tests that can be run offline"
+ )
- all_whats = ["tooling", "python", "shell", "rust", "subplots"]
+ all_whats = ["tooling", "python", "shell", "rust", "subplots", "doc"]
p.add_argument(
"what", nargs="*", default=all_whats, help=f"what to test: {all_whats}"
)
@@ -397,7 +436,7 @@ def main():
"""Main program"""
args = parse_args()
- r = Runcmd(args.verbose, args.progress)
+ r = Runcmd(args.verbose, args.progress, args.offline)
r.setenv("PYTHONDONTWRITEBYTECODE", "1")
for what in args.what:
@@ -406,11 +445,13 @@ def main():
elif what == "shell":
check_shell(r)
elif what == "rust":
- check_rust(r, strict=args.strict)
+ check_rust(r, strict=args.strict, sloppy=args.sloppy)
elif what == "subplots":
- check_subplots(r)
+ check_subplots(r, strict=args.strict)
elif what == "tooling":
check_tooling(r)
+ elif what == "doc":
+ check_doc(r)
else:
sys.exit(f"Unknown test {what}")
diff --git a/debian/changelog b/debian/changelog
index 86a6299..3f38e7f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,45 @@
+subplot (0.9.0-1) unstable; urgency=medium
+
+ * New release.
+
+ -- Daniel Silverstone <dsilvers@digital-scurf.org> Sun, 27 Aug 2023 11:30:00 +0100
+
+subplot (0.8.0-1) unstable; urgency=medium
+
+ * New release.
+
+ -- Daniel Silverstone <dsilvers@digital-scurf.org> Wed, 14 Jun 2023 19:07:32 +0100
+
+subplot (0.7.1-1) unstable; urgency=medium
+
+ * New release.
+
+ -- Lars Wirzenius <liw@liw.fi> Sun, 30 Apr 2023 11:51:05 +0300
+
+subplot (0.7.0-1) unstable; urgency=medium
+
+ * New release.
+
+ -- Daniel Silverstone <dsilvers@digital-scurf.org> Mon, 10 Apr 2023 12:56:14 +0100
+
+subplot (0.6.0-1) unstable; urgency=medium
+
+ * New release.
+
+ -- Daniel Silverstone <dsilvers@digital-scurf.org> Sun, 13 Nov 2022 19:41:24 +0000
+
+subplot (0.5.0-1) unstable; urgency=medium
+
+ * New release.
+
+ -- Lars Wirzenius <liw@liw.fi> Tue, 13 Sep 2022 10:47:07 +0300
+
+subplot (0.4.2) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Lars Wirzenius <liw@liw.fi> Thu, 28 Jul 2022 16:35:39 +0300
+
subplot (0.4.0) unstable; urgency=medium
* New upstream release.
diff --git a/debian/control b/debian/control
index c44f536..fb9fd6a 100644
--- a/debian/control
+++ b/debian/control
@@ -3,21 +3,15 @@ Maintainer: Lars Wirzenius <liw@liw.fi>
Section: utils
Priority: optional
Standards-Version: 4.2.0
-Build-Depends: debhelper (>= 10~), dh-cargo, python3, python3-requests, pandoc, texlive-latex-base,
- texlive-latex-recommended, texlive-fonts-recommended, texlive-plain-generic, librsvg2-bin, graphviz,
- pandoc-citeproc, plantuml, daemonize, lmodern, procps
+Build-Depends: debhelper (>= 10~), python3, python3-requests,
+ librsvg2-bin, graphviz, plantuml, daemonize, procps, tidy
Homepage: https://subplot.liw.fi
Package: subplot
Architecture: any
-Depends: ${misc:Depends}, ${shlibs:Depends}, pandoc, pandoc-citeproc, lmodern
-Recommends: librsvg2-bin,
- graphviz
-Suggests: texlive-latex-base,
- texlive-latex-recommended,
- texlive-fonts-recommended,
- texlive-plain-generic,
- plantuml
+Depends: ${misc:Depends}, ${shlibs:Depends}
+Recommends: librsvg2-bin, graphviz
+Suggests: plantuml
Built-Using: ${cargo:Built-Using}
Description: automatic tool for acceptance testing
Capture and communicate acceptance criteria for software and systems,
diff --git a/debian/copyright b/debian/copyright
index fe3a242..d3493fa 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -13,10 +13,10 @@ License: MIT
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
-
+ .
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
-
+ .
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@@ -24,6 +24,6 @@ License: MIT
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
+ .
Fork this project to create your own MIT license that you can always
link to.
diff --git a/debian/rules b/debian/rules
index 7360a53..f1632f8 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,15 +1,16 @@
#!/usr/bin/make -f
%:
- dh $@ --buildsystem cargo
+ dh $@
override_dh_auto_build:
true
override_dh_auto_install:
- cargo install --path=. --root=debian/subplot
+ cargo install --path=. --root=debian/subplot --offline
rm -f debian/subplot/.crates.toml
rm -f debian/subplot/.crates2.json
+ dh_lintian
override_dh_auto_test:
- ./check
+ echo disabled: ./check
diff --git a/debian/source/lintian-overrides b/debian/source/lintian-overrides
new file mode 100644
index 0000000..511a470
--- /dev/null
+++ b/debian/source/lintian-overrides
@@ -0,0 +1,2 @@
+subplot source: source-nmu-has-incorrect-version-number
+subplot source: no-nmu-in-changelog
diff --git a/debian/subplot.lintian-overrides b/debian/subplot.lintian-overrides
new file mode 100644
index 0000000..ad79ed0
--- /dev/null
+++ b/debian/subplot.lintian-overrides
@@ -0,0 +1,3 @@
+subplot binary: no-manual-page
+subplot: shell-script-fails-syntax-check
+subplot: script-not-executable
diff --git a/deny.toml b/deny.toml
index 6af795f..2b7e8ff 100644
--- a/deny.toml
+++ b/deny.toml
@@ -13,6 +13,7 @@ allow = [
"BSD-3-Clause",
"MIT",
"MIT-0",
+ "Unicode-DFS-2016",
]
copyleft = "warn"
allow-osi-fsf-free = "neither"
diff --git a/examples/echo/echo.md b/examples/echo/echo.md
index 495a102..8849b4b 100644
--- a/examples/echo/echo.md
+++ b/examples/echo/echo.md
@@ -1,14 +1,3 @@
----
-title: "**echo**(1) acceptance tests"
-author: The Subplot project
-bindings: [echo.yaml]
-impls:
- bash: [echo.sh]
-...
-
-FIXME: This needs to move back into YAML: bibliography: [echo.bib]
-
-
Introduction
=============================================================================
diff --git a/examples/echo/echo.subplot b/examples/echo/echo.subplot
new file mode 100644
index 0000000..e8ae606
--- /dev/null
+++ b/examples/echo/echo.subplot
@@ -0,0 +1,10 @@
+title: "**echo**(1) acceptance tests"
+authors:
+ - The Subplot project
+markdowns:
+ - echo.md
+bindings:
+ - echo.yaml
+impls:
+ bash:
+ - echo.sh
diff --git a/examples/muck/muck.md b/examples/muck/muck.md
index b074424..0a28020 100644
--- a/examples/muck/muck.md
+++ b/examples/muck/muck.md
@@ -1,12 +1,3 @@
----
-title: Muck JSON storage server and API
-author: Lars Wirzenius
-date: work in progress
-bindings: [muck.yaml]
-impls:
- python: [muck.py]
-...
-
Introduction
=============================================================================
@@ -95,24 +86,19 @@ This chapter lists high level requirements for Muck.
Each requirement here is given a unique mnemonic id for easier
reference in discussions.
-**SimpleOps**
-
-: Muck must be simple to install and operate. Installation should be
- installing a .deb package, configuration by setting the public key
- for token signing of the authentication server.
-
-**Fast**
-
-: Muck must be fast. The speed requirement is that Muck must be able
- to handle at least 100 concurrent clients, creating 1000 objects
- each, and then retrieving each object, and then deleting each
- object, and all of this must happen in no more than ten minutes
- (600 seconds). Muck and the clients should run on different
- virtual machines.
+**SimpleOps** --- Muck must be simple to install and operate.
+ Installation should be installing a .deb package, configuration by
+ setting the public key for token signing of the authentication
+ server.
-**Secure**
+**Fast** --- Muck must be fast. The speed requirement is that Muck
+ must be able to handle at least 100 concurrent clients, creating
+ 1000 objects each, and then retrieving each object, and then
+ deleting each object, and all of this must happen in no more than
+ ten minutes (600 seconds). Muck and the clients should run on
+ different virtual machines.
-: Muck must allow access only by an authenticated client
+**Secure** --- Muck must allow access only by an authenticated client
representing a data subject, and must only allow that client to
access objects owned by the data subject, unless the client has
super privileges. The data subject specifies, via the access
diff --git a/examples/muck/muck.subplot b/examples/muck/muck.subplot
new file mode 100644
index 0000000..f6feb2a
--- /dev/null
+++ b/examples/muck/muck.subplot
@@ -0,0 +1,12 @@
+---
+title: Muck JSON storage server and API
+authors:
+ - Lars Wirzenius
+date: work in progress
+markdowns:
+ - muck.md
+bindings:
+ - muck.yaml
+impls:
+ python:
+ - muck.py
diff --git a/examples/muck/muck.yaml b/examples/muck/muck.yaml
index b22e088..967ab56 100644
--- a/examples/muck/muck.yaml
+++ b/examples/muck/muck.yaml
@@ -18,24 +18,34 @@
python:
function: fixme
regex: true
+ types:
+ json: text
- when: "I do PUT /res with Muck-Id: \\{(?P<id>\\S+)\\}, Muck-Revision: \\{(?P<rev>\\S+)\\}, and body (?P<json>\\{.*\\})"
impl:
python:
function: fixme
regex: true
+ types:
+ id: word
+ rev: word
+ json: text
- when: "I do GET /res with Muck-Id: \\{(?P<id>\\S+)\\}"
impl:
python:
function: fixme
regex: true
+ types:
+ id: word
- when: "I do DELETE /res with Muck-Id: \\{(?P<id>\\S+)\\}"
impl:
python:
function: fixme
regex: true
+ types:
+ id: word
- when: "I restart Muck"
impl:
@@ -58,15 +68,23 @@
python:
function: fixme
regex: true
+ types:
+ header: word
+ name: word
- then: "body matches (?P<json>\\{.*\\})"
impl:
python:
function: fixme
regex: true
+ types:
+ json: text
- then: "revisions \\{(?P<rev1>\\S+)\\} and \\{(?P<rev2>\\S+)\\} are different"
impl:
python:
function: fixme
regex: true
+ types:
+ rev1: word
+ rev2: word
diff --git a/examples/seq/Cargo.toml b/examples/seq/Cargo.toml
index 2d4696a..9915f94 100644
--- a/examples/seq/Cargo.toml
+++ b/examples/seq/Cargo.toml
@@ -8,7 +8,9 @@ license = "MIT-0"
[dev-dependencies]
subplotlib = { path = "../../subplotlib" }
-fehler = "1"
[build-dependencies]
subplot-build = { path = "../../subplot-build" }
+
+[dependencies]
+culpa = "1.0.1"
diff --git a/examples/seq/build.rs b/examples/seq/build.rs
index 398ec90..d730645 100644
--- a/examples/seq/build.rs
+++ b/examples/seq/build.rs
@@ -1,6 +1,6 @@
//! Subplot seq example build script.
fn main() {
- println!("cargo:rerun-if-changed=seq.md");
- subplot_build::codegen("seq.md").expect("failed to generate code with Subplot");
+ println!("cargo:rerun-if-changed=seq.subplot");
+ subplot_build::codegen("seq.subplot").expect("failed to generate code with Subplot");
}
diff --git a/examples/seq/seq-extras.rs b/examples/seq/seq-extras.rs
index 79863fc..b2185fb 100644
--- a/examples/seq/seq-extras.rs
+++ b/examples/seq/seq-extras.rs
@@ -26,8 +26,7 @@ fn count_lines_in_stdout(context: &Runcmd, count: usize) {
// step error. This will be reported as the reason the
// scenario fails.
throw!(format!(
- "Incorrect number of lines, got {} expected {}",
- stdout_count, count
+ "Incorrect number of lines, got {stdout_count} expected {count}",
));
}
}
@@ -57,8 +56,7 @@ fn stderr_contains_two_things(context: &ScenarioContext, what: &str, other: &str
if !stderr_has_both {
throw!(format!(
- "Stderr does not contain both of {:?} and {:?}",
- what, other
+ "Stderr does not contain both of {what:?} and {other:?}",
))
}
}
diff --git a/examples/seq/seq.subplot b/examples/seq/seq.subplot
new file mode 100644
index 0000000..3bea683
--- /dev/null
+++ b/examples/seq/seq.subplot
@@ -0,0 +1,11 @@
+title: "**seq**(1) acceptance tests"
+authors:
+ - The Subplot project
+markdowns:
+ - seq.md
+bindings:
+ - lib/runcmd.yaml
+ - seq-extras.yaml
+impls:
+ rust:
+ - seq-extras.rs
diff --git a/examples/website/website.md b/examples/website/website.md
index e85249a..b72862d 100644
--- a/examples/website/website.md
+++ b/examples/website/website.md
@@ -68,11 +68,10 @@ download [website.md][], [website.yaml][], and [website.py][].
## Generate typeset documents
To generate typeset versions of this document, run the following
-commands:
+command:
~~~
$ subplot docgen website.md -o website.html
-$ subplot docgen website.md -o website.pdf
~~~
Open up the files to see what they look like.
@@ -174,12 +173,3 @@ achieve this, you should make the following changes:
[website.md]: https://gitlab.com/subplot/subplot/-/tree/main/examples/website/website.md
[website.yaml]: https://gitlab.com/subplot/subplot/-/tree/main/examples/website/website.yaml
[website.py]: https://gitlab.com/subplot/subplot/-/tree/main/examples/website/website.py
-
-
----
-title: Subplot website tutorial
-author: The Subplot project
-bindings: [website.yaml]
-impls:
- python: [website.py]
-...
diff --git a/examples/website/website.subplot b/examples/website/website.subplot
new file mode 100644
index 0000000..a39d4e3
--- /dev/null
+++ b/examples/website/website.subplot
@@ -0,0 +1,10 @@
+title: Subplot website tutorial
+authors:
+ - The Subplot project
+markdowns:
+ - website.md
+bindings:
+ - website.yaml
+impls:
+ python:
+ - website.py
diff --git a/flake.lock b/flake.lock
index 38de242..6e546c6 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
"locked": {
- "lastModified": 1631561581,
- "narHash": "sha256-3VQMV5zvxaVLvqqUrNz3iJelLw30mIVSfZmAaauM3dA=",
+ "lastModified": 1694529238,
+ "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "7e5bf3925f6fbdfaf50a2a7ca0be2879c4261d19",
+ "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
@@ -17,10 +20,10 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1632639184,
- "narHash": "sha256-fRLxre+gPxIkjFVj17O68pyAWU1cxT20XFOiulIWzRw=",
- "path": "/nix/store/mz8vpyg587llb4c802w96m956icc39vm-source",
- "rev": "fd8a7fd07da0f3fc0e27575891f45c2f88e5dd44",
+ "lastModified": 1694767346,
+ "narHash": "sha256-5uH27SiVFUwsTsqC5rs3kS7pBoNhtoy9QfTP9BmknGk=",
+ "path": "/nix/store/6s86padm2iikrwhlq8nwfv0lw9d1sbvq-source",
+ "rev": "ace5093e36ab1e95cb9463863491bee90d5a4183",
"type": "path"
},
"original": {
@@ -33,6 +36,21 @@
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
+ },
+ "systems": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
}
},
"root": "root",
diff --git a/flake.nix b/flake.nix
index 9f1aafe..b8c244f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -23,12 +23,11 @@
stdenv
graphviz
plantuml
- pandoc
- texlive.combined.scheme-medium
daemonize
librsvg
(python3.withPackages test-python-packages)
black
+ html-tidy
];
SUBPLOT_DOT_PATH = "${pkgs.graphviz}/bin/dot";
SUBPLOT_JAVA_PATH = "${pkgs.jre}/bin/java";
diff --git a/reference.md b/reference.md
new file mode 100644
index 0000000..9999636
--- /dev/null
+++ b/reference.md
@@ -0,0 +1,53 @@
+# Introduction
+
+This document describes how we guard against accidental breaking
+changes in Subplot by running the current version against a curated
+set of subplot documents.
+
+# Subplot version 0.9.0
+
+## Produce HTML page
+
+~~~scenario
+given an installed subplot
+given a clone of https://gitlab.com/subplot/subplot.git in src at 5168420454b92205c13224a6801d3341d7f0c3d3
+when I docgen subplot.subplot to test.html, in src
+when I run, in src, subplot docgen subplot.subplot --merciful -o subplot.html -t python
+then file src/test.html exists
+~~~
+
+## Generate and run test program
+
+~~~scenario
+given an installed subplot
+given file run_test.sh
+given a clone of https://gitlab.com/subplot/subplot.git in src at 5168420454b92205c13224a6801d3341d7f0c3d3
+when I run, in src, subplot codegen subplot.subplot -o test-inner.py -t python
+when I run bash run_test.sh
+then command is successful
+~~~
+
+~~~{#run_test.sh .file .sh}
+#!/bin/bash
+
+set -euo pipefail
+
+N=100
+
+if python3 src/test-inner.py --log test-inner.log --env "PATH=$PATH" --env SUBPLOT_DIR=/
+then
+ exit=0
+else
+ exit="$?"
+fi
+
+if [ "$exit" != 0 ]
+then
+ # Failure. Show end of inner log file.
+
+ echo "last $N lines of test-inner.log:"
+ tail "-n$N" test-inner.log | sed 's/^/ /'
+fi
+
+exit "$exit"
+~~~
diff --git a/reference.md-disabled b/reference.md-disabled
deleted file mode 100644
index 25e4189..0000000
--- a/reference.md-disabled
+++ /dev/null
@@ -1,80 +0,0 @@
-
-# Introduction
-
-This document describes how we guard against accidental breaking
-changes in Subplot by running it against a curated set of subplot
-documents.
-
-# Subplot
-
-## Produce a PDF
-
-~~~scenario
-given an installed subplot
-given a clone of https://gitlab.com/subplot/subplot.git in src at 96371571338767e776f7de583ddbea4512ceeba1
-when I docgen subplot.md to test.pdf, in src
-then file src/test.pdf exists
-~~~
-
-## Produce HTML page
-
-~~~scenario
-given an installed subplot
-given a clone of https://gitlab.com/subplot/subplot.git in src at 96371571338767e776f7de583ddbea4512ceeba1
-when I docgen subplot.md to test.html, in src
-when I run, in src, subplot docgen subplot.md -o subplot.html
-then file src/test.html exists
-~~~
-
-## Generate and run test program
-
-~~~scenario
-given an installed subplot
-given file run_test.sh
-given a clone of https://gitlab.com/subplot/subplot.git in src at 96371571338767e776f7de583ddbea4512ceeba1
-when I run, in src, subplot codegen subplot.md -o test-inner.py
-when I run bash run_test.sh
-then command is successful
-~~~
-
-~~~{#run_test.sh .file .sh}
-#!/bin/bash
-
-set -euo pipefail
-
-N=100
-
-if python3 src/test-inner.py --log test-inner.log --env "PATH=$PATH" --env SUBPLOT_DIR=/
-then
- exit=0
-else
- exit="$?"
-fi
-
-if [ "$exit" != 0 ]
-then
- # Failure. Show end of inner log file.
-
- echo "last $N lines of test-inner.log:"
- tail "-n$N" test-inner.log | sed 's/^/ /'
-fi
-
-exit "$exit"
-~~~
-
-
----
-title: Test Subplot against reference subplots
-author: The Subplot project
-bindings:
-- reference.yaml
-- subplot.yaml
-- lib/runcmd.yaml
-- lib/files.yaml
-impls:
- python:
- - reference.py
- - subplot.py
- - lib/files.py
- - lib/runcmd.py
-...
diff --git a/reference.py b/reference.py
index 69ad466..803716a 100644
--- a/reference.py
+++ b/reference.py
@@ -13,5 +13,9 @@ def docgen(ctx, filename=None, output=None, dirname=None):
runcmd_run = globals()["runcmd_run"]
runcmd_exit_code_is_zero = globals()["runcmd_exit_code_is_zero"]
- runcmd_run(ctx, ["subplot", "docgen", filename, "--output", output], cwd=dirname)
+ runcmd_run(
+ ctx,
+ ["subplot", "docgen", filename, "--merciful", "--output", output, "-t", "python"],
+ cwd=dirname,
+ )
runcmd_exit_code_is_zero(ctx)
diff --git a/reference.subplot b/reference.subplot
new file mode 100644
index 0000000..4e3311c
--- /dev/null
+++ b/reference.subplot
@@ -0,0 +1,16 @@
+title: Test Subplot against reference subplots
+authors:
+- The Subplot project
+markdowns:
+- reference.md
+bindings:
+- reference.yaml
+- subplot.yaml
+- lib/runcmd.yaml
+- lib/files.yaml
+impls:
+ python:
+ - reference.py
+ - subplot.py
+ - lib/files.py
+ - lib/runcmd.py
diff --git a/share/common/lib/files.yaml b/share/common/lib/files.yaml
index e4d9b6b..689a5f3 100644
--- a/share/common/lib/files.yaml
+++ b/share/common/lib/files.yaml
@@ -10,6 +10,9 @@
function: files_create_from_embedded
types:
embedded_file: file
+ doc: |
+ Create a file on disk from an embedded file in the subplot
+ document. The created file has the same name as the embedded file.
- given: file {filename_on_disk} from {embedded_file}
impl:
@@ -18,15 +21,23 @@
python:
function: files_create_from_embedded_with_other_name
types:
+ filename_on_disk: path
embedded_file: file
+ doc: |
+ Create a file on disk from an embedded file in the subplot
+ document. Set the name of the created file.
-- when: I write "(?P<text>.*)" to file (?P<filename>\S+)
- regex: true
+- when: I write "{text}" to file {filename}
impl:
rust:
function: subplotlib::steplibrary::files::create_from_text
python:
function: files_create_from_text
+ types:
+ filename: path
+ text: text
+ doc: |
+ Create a file on disk with the given content.
# Manage directories (distinct from files).
@@ -36,6 +47,10 @@
function: subplotlib::steplibrary::files::make_directory
python:
function: files_make_directory
+ types:
+ path: path
+ doc: |
+ Create a directory on disk.
- when: I create directory {path}
impl:
@@ -43,6 +58,10 @@
function: subplotlib::steplibrary::files::make_directory
python:
function: files_make_directory
+ types:
+ path: path
+ doc: |
+ Create a directory on disk.
- when: I remove directory {path}
impl:
@@ -50,6 +69,10 @@
function: subplotlib::steplibrary::files::remove_directory
python:
function: files_remove_directory
+ types:
+ path: path
+ doc: |
+ Remove a directory on disk.
- then: directory {path} exists
impl:
@@ -57,6 +80,10 @@
function: subplotlib::steplibrary::files::path_exists
python:
function: files_directory_exists
+ types:
+ path: path
+ doc: |
+ Check that a directory exists.
- then: directory {path} does not exist
impl:
@@ -64,6 +91,10 @@
function: subplotlib::steplibrary::files::path_does_not_exist
python:
function: files_directory_does_not_exist
+ types:
+ path: path
+ doc: |
+ Check that a directory does not exist.
- then: directory {path} is empty
impl:
@@ -71,6 +102,10 @@
function: subplotlib::steplibrary::files::path_is_empty
python:
function: files_directory_is_empty
+ types:
+ path: path
+ doc: |
+ Check that a directory exists and does not contain anything.
- then: directory {path} is not empty
impl:
@@ -78,6 +113,10 @@
function: subplotlib::steplibrary::files::path_is_not_empty
python:
function: files_directory_is_not_empty
+ types:
+ path: path
+ doc: |
+ Check that a directory exists and contains something.
# File metadata management and testing.
@@ -89,7 +128,10 @@
python:
function: files_touch_with_timestamp
types:
+ filename: path
mtime: text
+ doc: |
+ Create a file with specific modification time.
- when: I remember metadata for file {filename}
impl:
@@ -97,6 +139,10 @@
function: subplotlib::steplibrary::files::remember_metadata
python:
function: files_remember_metadata
+ types:
+ filename: path
+ doc: |
+ Remember the metadata of a file.
- when: I touch file {filename}
impl:
@@ -104,6 +150,10 @@
function: subplotlib::steplibrary::files::touch
python:
function: files_touch
+ types:
+ filename: path
+ doc: |
+ Update the modification time of a file to be current time.
- then: file {filename} has same metadata as before
impl:
@@ -111,6 +161,11 @@
function: subplotlib::steplibrary::files::has_remembered_metadata
python:
function: files_has_remembered_metadata
+ types:
+ filename: path
+ doc: |
+ Check that a file has the same metadata as remembered from
+ earlier.
- then: file {filename} has different metadata from before
impl:
@@ -118,6 +173,10 @@
function: subplotlib::steplibrary::files::has_different_metadata
python:
function: files_has_different_metadata
+ types:
+ filename: path
+ doc: |
+ Check that a file metadata has changed from earlier.
- then: file {filename} has changed from before
impl:
@@ -125,6 +184,10 @@
function: subplotlib::steplibrary::files::has_different_metadata
python:
function: files_has_different_metadata
+ types:
+ filename: path
+ doc: |
+ Check that file metadata has changed from before.
- then: file {filename} has a very recent modification time
impl:
@@ -132,6 +195,10 @@
function: subplotlib::steplibrary::files::mtime_is_recent
python:
function: files_mtime_is_recent
+ types:
+ filename: path
+ doc: |
+ Check that file modification time is recent.
- then: file {filename} has a very old modification time
impl:
@@ -139,6 +206,10 @@
function: subplotlib::steplibrary::files::mtime_is_ancient
python:
function: files_mtime_is_ancient
+ types:
+ filename: path
+ doc: |
+ Check that file modification is far in the past.
# Testing file existence.
@@ -148,6 +219,10 @@
function: subplotlib::steplibrary::files::file_exists
python:
function: files_file_exists
+ types:
+ filename: path
+ doc: |
+ Check that a file exist.
- then: file {filename} does not exist
impl:
@@ -155,6 +230,10 @@
function: subplotlib::steplibrary::files::file_does_not_exist
python:
function: files_file_does_not_exist
+ types:
+ filename: path
+ doc: |
+ Check that a file does not exist.
- then: only files (?P<filenames>.+) exist
impl:
@@ -163,40 +242,60 @@
python:
function: files_only_these_exist
regex: true
+ types:
+ filenames: text
+ doc: |
+ Check that the test directory only contains specific files.
# Tests on file content.
-- then: file (?P<filename>\S+) contains "(?P<data>.*)"
- regex: true
+- then: file {filename} contains "{data}"
impl:
rust:
function: subplotlib::steplibrary::files::file_contains
python:
function: files_file_contains
+ types:
+ filename: path
+ data: text
+ doc: |
+ Check that a file contains a string.
-- then: file (?P<filename>\S+) doesn't contain "(?P<data>.*)"
- regex: true
+- then: file {filename} doesn't contain "{data}"
impl:
rust:
function: subplotlib::steplibrary::files::file_doesnt_contain
python:
function: files_file_doesnt_contain
+ types:
+ filename: path
+ data: text
+ doc: |
+ Check that a file does not contain a string.
-- then: file (?P<filename>\S+) matches regex /(?P<regex>.*)/
- regex: true
+- then: file {filename} matches regex /{regex}/
impl:
rust:
function: subplotlib::steplibrary::files::file_matches_regex
python:
function: files_file_matches_regex
+ types:
+ filename: path
+ regex: text
+ doc: |
+ Check that file content matches a regular expression.
-- then: file (?P<filename>\S+) matches regex "(?P<regex>.*)"
- regex: true
+- then: file {filename} matches regex "{regex}"
impl:
rust:
function: subplotlib::steplibrary::files::file_matches_regex
python:
function: files_file_matches_regex
+ types:
+ filename: path
+ regex: text
+ doc: |
+ Check that file content matches a regular expression.
- then: files {filename1} and {filename2} match
impl:
@@ -204,3 +303,8 @@
function: subplotlib::steplibrary::files::file_match
python:
function: files_match
+ types:
+ filename1: path
+ filename2: path
+ doc: |
+ Check that two files have the same content.
diff --git a/share/common/lib/runcmd.yaml b/share/common/lib/runcmd.yaml
index 5847613..b20eac8 100644
--- a/share/common/lib/runcmd.yaml
+++ b/share/common/lib/runcmd.yaml
@@ -8,6 +8,8 @@
function: subplotlib::steplibrary::runcmd::helper_script
types:
script: file
+ doc: |
+ Install a helper script from an embedded file.
- given: srcdir is in the PATH
impl:
@@ -15,6 +17,10 @@
function: runcmd_helper_srcdir_path
rust:
function: subplotlib::steplibrary::runcmd::helper_srcdir_path
+ doc: |
+ Make sure the source directory of the project being testes is on
+ the shell PATH. This makes it easy for tests to invoke programs
+ from the source tree.
- when: I run {argv0}{args:text}
impl:
@@ -22,13 +28,22 @@
function: runcmd_step
rust:
function: subplotlib::steplibrary::runcmd::run
+ doc: |
+ Run a program, and make sure it succeeds.
-- when: I run, in {dirname}, {argv0}{args:text}
+- when: I run, in {dirname}, {argv0}{args}
impl:
python:
function: runcmd_step_in
rust:
function: subplotlib::steplibrary::runcmd::run_in
+ types:
+ dirname: path
+ argv0: word
+ args: text
+ doc: |
+ Change to a different directory and run a program, and make sure
+ it succeeds;
- when: I try to run {argv0}{args:text}
impl:
@@ -36,13 +51,23 @@
function: runcmd_try_to_run
rust:
function: subplotlib::steplibrary::runcmd::try_to_run
+ doc: |
+ Run a program, but allow it to fail. Other steps can check if it
+ succeeded.
-- when: I try to run, in {dirname}, {argv0}{args:text}
+- when: I try to run, in {dirname}, {argv0}{args}
impl:
python:
function: runcmd_try_to_run_in
rust:
function: subplotlib::steplibrary::runcmd::try_to_run_in
+ types:
+ dirname: path
+ argv0: word
+ args: text
+ doc: |
+ Change to a different directory and run a program, but allow it to
+ fail. Other steps can check if it succeeded.
# Steps to examine exit code of latest command.
@@ -54,6 +79,9 @@
function: subplotlib::steplibrary::runcmd::exit_code_is
types:
exit: int
+ doc: |
+ Make sure the latest command run by `lib/runcmd` had a specific
+ exit code.
- then: exit code is not {exit}
impl:
@@ -63,6 +91,9 @@
function: subplotlib::steplibrary::runcmd::exit_code_is_not
types:
exit: int
+ doc: |
+ Make sure the latest command run by `lib/runcmd` did not have a
+ specific exit code.
- then: command is successful
impl:
@@ -70,6 +101,9 @@
function: runcmd_exit_code_is_zero
rust:
function: subplotlib::steplibrary::runcmd::exit_code_is_zero
+ doc: |
+ Make sure the latest command run by `lib/runcmd` indicated the
+ command succeeded.
- then: command fails
impl:
@@ -77,6 +111,9 @@
function: runcmd_exit_code_is_nonzero
rust:
function: subplotlib::steplibrary::runcmd::exit_code_is_nonzero
+ doc: |
+ Make sure the latest command run by `lib/runcmd` indicated the
+ command failed.
# Steps to examine stdout/stderr for exact content.
@@ -86,6 +123,9 @@
function: runcmd_stdout_is
rust:
function: subplotlib::steplibrary::runcmd::stdout_is
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` is exactly as desired.
- then: 'stdout isn''t exactly "{text:text}"'
impl:
@@ -93,6 +133,9 @@
function: runcmd_stdout_isnt
rust:
function: subplotlib::steplibrary::runcmd::stdout_isnt
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` is different from what is not wanted.
- then: stderr is exactly "{text:text}"
impl:
@@ -100,6 +143,9 @@
function: runcmd_stderr_is
rust:
function: subplotlib::steplibrary::runcmd::stderr_is
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` is exactly as desired.
- then: 'stderr isn''t exactly "{text:text}"'
impl:
@@ -107,6 +153,9 @@
function: runcmd_stderr_isnt
rust:
function: subplotlib::steplibrary::runcmd::stderr_isnt
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` is different from what is not wanted.
# Steps to examine stdout/stderr for sub-strings.
@@ -116,6 +165,9 @@
function: runcmd_stdout_contains
rust:
function: subplotlib::steplibrary::runcmd::stdout_contains
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` contains the desired sub-string.
- then: 'stdout doesn''t contain "{text:text}"'
impl:
@@ -123,6 +175,9 @@
function: runcmd_stdout_doesnt_contain
rust:
function: subplotlib::steplibrary::runcmd::stdout_doesnt_contain
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` does not contain the sub-string.
- then: stderr contains "{text:text}"
impl:
@@ -130,6 +185,9 @@
function: runcmd_stderr_contains
rust:
function: subplotlib::steplibrary::runcmd::stderr_contains
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` contains the desired sub-string.
- then: 'stderr doesn''t contain "{text:text}"'
impl:
@@ -137,6 +195,9 @@
function: runcmd_stderr_doesnt_contain
rust:
function: subplotlib::steplibrary::runcmd::stderr_doesnt_contain
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` does not contain the sub-string.
# Steps to match stdout/stderr against regular expressions.
@@ -146,6 +207,9 @@
function: runcmd_stdout_matches_regex
rust:
function: subplotlib::steplibrary::runcmd::stdout_matches_regex
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` matches the desired regular expression.
- then: stdout doesn't match regex {regex:text}
impl:
@@ -153,6 +217,9 @@
function: runcmd_stdout_doesnt_match_regex
rust:
function: subplotlib::steplibrary::runcmd::stdout_doesnt_match_regex
+ doc: |
+ Make sure the standard output of the latest command run by
+ `lib/runcmd` does not match a regular expression.
- then: stderr matches regex {regex:text}
impl:
@@ -160,6 +227,9 @@
function: runcmd_stderr_matches_regex
rust:
function: subplotlib::steplibrary::runcmd::stderr_matches_regex
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` matches the desired regular expression.
- then: stderr doesn't match regex {regex:text}
impl:
@@ -167,3 +237,6 @@
function: runcmd_stderr_doesnt_match_regex
rust:
function: subplotlib::steplibrary::runcmd::stderr_doesnt_match_regex
+ doc: |
+ Make sure the standard error output of the latest command run by
+ `lib/runcmd` does not match a regular expression.
diff --git a/share/python/lib/daemon.yaml b/share/python/lib/daemon.yaml
index acca151..e385880 100644
--- a/share/python/lib/daemon.yaml
+++ b/share/python/lib/daemon.yaml
@@ -2,6 +2,8 @@
impl:
python:
function: daemon_no_such_process
+ doc: |
+ Ensure a given process is not running.
- given: a daemon helper shell script {filename}
impl:
@@ -9,88 +11,155 @@
function: _daemon_shell_script
types:
filename: file
+ doc: |
+ Install a helper script from an embedded file.
- when: I start "{path}{args:text}" as a background process as {name}, on port {port}
impl:
python:
function: daemon_start_on_port
+ doc: |
+ Start a process in the background (as a daemon) and wait until it
+ listens on its assigned port.
- when: I start "(?P<path>[^ "]+)(?P<args>[^"]*)" as a background process as (?P<name>[^,]+), on port (?P<port>\d+), with environment (?P<env>.*)
regex: true
+ types:
+ args: text
+ path: path
+ name: text
+ port: uint
+ env: text
impl:
python:
function: daemon_start_on_port
+ doc: |
+ Start a process in the background (as a daemon) and wait until it
+ listens on its assigned port. Remember the process under the given
+ name.
- when: I try to start "{path}{args:text}" as {name}, on port {port}
impl:
python:
function: _daemon_start_soonish
cleanup: _daemon_stop_soonish
+ doc: |
+ Try to start a background process (as a daemon), but don't fail if
+ starting it fails.
- when: I try to start "(?P<path>[^ "]+)(?P<args>[^"]*)" as (?P<name>[^,]+), on port (?P<port>\d+), with environment (?P<env>.*)
regex: true
+ types:
+ path: path
+ args: text
+ name: text
+ port: uint
+ env: text
impl:
python:
function: _daemon_start_soonish
cleanup: _daemon_stop_soonish
+ doc: |
+ Start a process in the background (as a daemon) and wait until it
+ listens on its assigned port. Remember the process under the given
+ name. Don't fail if this fails.
- when: I start "{path}{args:text}" as a background process as {name}
impl:
python:
function: _daemon_start
+ doc: |
+ Start a process in the background (as a daemon). Remember the
+ process under the given name. Don't fail if this fails.
- when: I start "(?P<path>[^ "]+)(?P<args>[^"]*)" as a background process as (?P<name>[^,]+), with environment (?P<env>.*)
regex: true
+ types:
+ path: path
+ args: text
+ name: text
+ env: text
impl:
python:
function: _daemon_start
+ doc: |
+ Start a process in the background (as a daemon), with specific
+ environment variables set. Remember the process under the given
+ name. Don't fail if this fails.
- when: I stop background process {name}
impl:
python:
function: daemon_stop
+ doc: |
+ Stop a background process that was started earlier with the given
+ name.
- when: daemon {name} has produced output
impl:
python:
function: daemon_has_produced_output
+ doc: |
+ Wait until the named daemon has produced output to its stdout or
+ stderr.
- then: a process "{args:text}" is running
impl:
python:
function: daemon_process_exists
+ doc: |
+ Check that a given process is running.
- then: there is no "{args:text}" process
impl:
python:
function: daemon_no_such_process
+ doc: |
+ Check that a given process is not running.
- then: starting daemon fails with "{message:text}"
impl:
python:
function: daemon_start_fails_with
+ doc: |
+ Check that starting a daemon previously failed, and the error
+ message contains the given text.
- then: starting the daemon succeeds
impl:
python:
function: daemon_start_succeeds
+ doc: |
+ Check that staring a daemon previous succeeded.
- then: daemon {name} stdout is "{text:text}"
impl:
python:
function: daemon_stdout_is
+ doc: |
+ Check that the named daemon has written exactly the given text to
+ its stdout.
- then: daemon {name} stdout contains "{text:text}"
impl:
python:
function: daemon_stdout_contains
+ doc: |
+ Check that the named daemon has written the given text to its
+ stdout, possibly among other text.
- then: daemon {name} stdout doesn't contain "{text:text}"
impl:
python:
function: daemon_stdout_doesnt_contain
+ doc: |
+ Check that the named daemon has not written the given text to its
+ stdout.
- then: daemon {name} stderr is "{text:text}"
impl:
python:
function: daemon_stderr_is
+ doc: |
+ Check that the named daemon has written exactly the given text to
+ its stderr.
diff --git a/share/python/lib/runcmd.py b/share/python/lib/runcmd.py
index c4a6a12..6a4965f 100644
--- a/share/python/lib/runcmd.py
+++ b/share/python/lib/runcmd.py
@@ -70,7 +70,6 @@ def runcmd_run(ctx, argv, **kwargs):
logging.debug("runcmd_run: running command")
log_value("argv", 1, dict(enumerate(argv)))
- log_value("env", 1, env)
log_value("kwargs:", 1, kwargs)
p = subprocess.Popen(argv, env=env, **kwargs)
diff --git a/share/python/template/scenarios.py b/share/python/template/scenarios.py
index b215133..a6aa9f4 100644
--- a/share/python/template/scenarios.py
+++ b/share/python/template/scenarios.py
@@ -42,12 +42,14 @@ class Step:
self._cleanup(ctx, **self._args)
+_logged_env = False
+
+
class Scenario:
def __init__(self, ctx):
self._title = None
self._steps = []
self._ctx = ctx
- self._logged_env = False
def get_title(self):
return self._title
@@ -91,7 +93,9 @@ class Scenario:
os.environ.update(overrides)
os.environ.update(extra_env)
- if not self._logged_env:
- self._logged_env = True
+ global _logged_env
+ if not _logged_env:
+ _logged_env = True
log_value("extra_env", 0, dict(extra_env))
+ log_value("overrides", 0, dict(overrides))
log_value("os.environ", 0, dict(os.environ))
diff --git a/share/rust/lib/datadir.yaml b/share/rust/lib/datadir.yaml
index f4c313b..b77e5ef 100644
--- a/share/rust/lib/datadir.yaml
+++ b/share/rust/lib/datadir.yaml
@@ -9,9 +9,16 @@
function: subplotlib::steplibrary::datadir::datadir_has_enough_space
types:
bytes: uint
+ doc: |
+ Check the test data directory has at least the given amount of
+ free space expressed as bytes.
+
- given: datadir has at least {megabytes}M of space
impl:
rust:
function: subplotlib::steplibrary::datadir::datadir_has_enough_space_megabytes
types:
megabytes: uint
+ doc: |
+ Check the test data directory has at least the given amount of
+ free space expressed as megabytes.
diff --git a/share/rust/template/macros.rs.tera b/share/rust/template/macros.rs.tera
index 7a65e0f..cc1d9c4 100644
--- a/share/rust/template/macros.rs.tera
+++ b/share/rust/template/macros.rs.tera
@@ -8,8 +8,7 @@
{% if type in ['number', 'int', 'uint'] %}{{text}}
{%- elif type in ['text', 'word']%}
// "{{text | commentsafe }}"
- &base64_decode("{{text | base64}}"
- )
+ &base64_decode("{{text | base64}}")
{%- elif type in ['file'] %}
{
use std::path::PathBuf;
@@ -21,10 +20,12 @@
.expect("Unable to find file at runtime")
.clone()
}
+ {%- elif type in ['path'] %}
+ std::path::PathBuf::from(base64_decode("{{ text | base64 }}"))
{%- else %} /* WOAH unknown type {{step.types[name]}} */ {{text}}
{%- endif %}
)
{% endif -%}
{% endfor -%}
- .build(format!("{} {}", "{{step.kind | lower}}", base64_decode("{{step.text | base64}}")))
+ .build(format!("{} {}", "{{step.kind | lower}}", base64_decode("{{step.text | base64}}")), {{ step.origin | location }})
{%- endmacro builder -%}
diff --git a/share/rust/template/template.rs.tera b/share/rust/template/template.rs.tera
index 65fb755..447129c 100644
--- a/share/rust/template/template.rs.tera
+++ b/share/rust/template/template.rs.tera
@@ -30,7 +30,7 @@ lazy_static! {
#[test]
#[allow(non_snake_case)]
fn {{ scenario.title | nameslug }}() {
- let mut scenario = Scenario::new(&base64_decode("{{scenario.title | base64}}"));
+ let mut scenario = Scenario::new(&base64_decode("{{scenario.title | base64}}"), {{ scenario.origin | location }});
{% for step in scenario.steps %}
let step = {{ macros::builder(stepfn=step.function, step=step) }};
{%- if step.cleanup %}
diff --git a/share/subplot.css b/share/subplot.css
new file mode 100644
index 0000000..292a5f7
--- /dev/null
+++ b/share/subplot.css
@@ -0,0 +1,40 @@
+div.toc ol {
+ list-style-type: none;
+ padding: 0;
+ padding-inline-start: 2ch;
+}
+
+pre.file {
+ background: yellow;
+ border: 10px black;
+ padding: 1em;
+}
+
+div.scenario {
+ background: yellow;
+ padding: 1em;
+}
+
+span.capture-word {
+ font-family: monospace;
+}
+span.capture-text {
+ font-family: monospace;
+}
+span.capture-int {
+ font-weight: bold;
+}
+span.capture-uint {
+ font-weight: bold;
+}
+span.capture-number {
+ font-weight: bold;
+}
+span.capture-file {
+ font-family: monospace;
+}
+
+span.capture-path {
+ font-family: monospace;
+ font-weight: bold;
+}
diff --git a/src/ast.rs b/src/ast.rs
deleted file mode 100644
index eb10efc..0000000
--- a/src/ast.rs
+++ /dev/null
@@ -1,484 +0,0 @@
-use lazy_static::lazy_static;
-use log::trace;
-use pandoc_ast::{Attr, Block, Inline, Map, MetaValue, Pandoc};
-use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
-use regex::Regex;
-use serde::Deserialize;
-use std::collections::BTreeMap;
-use std::path::{Path, PathBuf};
-
-lazy_static! {
- // Pattern that recognises a YAML block at the beginning of a file.
- static ref LEADING_YAML_PATTERN: Regex = Regex::new(r"^(?:\S*\n)*(?P<yaml>-{3,}\n([^.].*\n)*\.{3,}\n)(?P<text>(.*\n)*)$").unwrap();
-
-
- // Pattern that recognises a YAML block at the end of a file.
- static ref TRAILING_YAML_PATTERN: Regex = Regex::new(r"(?P<text>(.*\n)*)\n*(?P<yaml>-{3,}\n([^.].*\n)*\.{3,}\n)(?:\S*\n)*$").unwrap();
-}
-
-/// An abstract syntax tree representation of a Markdown file.
-///
-/// This represents a Markdown file as an abstract syntax tree
-/// compatible with Pandoc's AST. The document YAML metadata MUST be
-/// at the top or bottom of the file, excluding leading or trailing
-/// empty lines.
-#[derive(Debug)]
-pub struct AbstractSyntaxTree {
- blocks: Vec<Block>,
- meta: Map<String, MetaValue>,
-}
-
-impl AbstractSyntaxTree {
- // Create a new AST.
- //
- // Note that this is not public.
- fn new(meta: Map<String, MetaValue>, blocks: Vec<Block>) -> Self {
- Self { blocks, meta }
- }
-
- /// Return a Pandoc-compatible AST.
- pub fn to_pandoc(&self) -> Pandoc {
- Pandoc {
- meta: self.meta.clone(),
- blocks: self.blocks.clone(),
- pandoc_api_version: vec![1, 20],
- }
- }
-}
-
-impl std::str::FromStr for AbstractSyntaxTree {
- type Err = Error;
-
- /// Create an abstract syntax tree from a string.
- fn from_str(markdown: &str) -> Result<Self, Self::Err> {
- trace!("Parsing markdown");
- let ast = if let Some((yaml, markdown)) = get_yaml(&LEADING_YAML_PATTERN, markdown) {
- trace!("Found leading YAML: {:?}", yaml);
- let meta = Metadata::new(yaml)?.to_map();
- let blocks = parse_blocks(markdown);
- AbstractSyntaxTree::new(meta, blocks)
- } else if let Some((yaml, _markdown)) = get_yaml(&TRAILING_YAML_PATTERN, markdown) {
- trace!("Found trailing YAML: {:?}", yaml);
- let meta = Metadata::new(yaml)?.to_map();
- let blocks = parse_blocks(markdown);
- AbstractSyntaxTree::new(meta, blocks)
- } else {
- trace!("No YAML to be found");
- let blocks = parse_blocks(markdown);
- AbstractSyntaxTree::new(Map::new(), blocks)
- };
- trace!("Parsing markdown: OK");
- Ok(ast)
- }
-}
-
-// Extract a YAML metadata block using a given regex.
-fn get_yaml<'a>(pat: &Regex, markdown: &'a str) -> Option<(&'a str, &'a str)> {
- trace!("Markdown: {:?}", markdown);
- if let Some(c) = pat.captures(markdown) {
- trace!("YAML regex matches: {:?}", c);
- let yaml = c.name("yaml");
- let text = c.name("text");
- trace!("YAML metadata: {:?}", yaml);
- trace!("markdown: {:?}", text);
- if yaml.is_some() && text.is_some() {
- trace!("YAML regex captures YAML and text");
- let yaml = yaml?;
- let text = text?;
- let yaml = &markdown[yaml.start()..yaml.end()];
- let text = &markdown[text.start()..text.end()];
- assert!(yaml.starts_with("---"));
- assert!(yaml.ends_with("...\n"));
- return Some((yaml, text));
- } else {
- trace!("YAML regex fails to capture YAML");
- }
- } else {
- trace!("YAML regex does not match");
- }
- None
-}
-
-// Parse Markdown into a sequence of Blocks.
-fn parse_blocks(markdown: &str) -> Vec<Block> {
- trace!("Parsing blocks");
-
- // Define the Markdown parser.
- let mut options = Options::empty();
- options.insert(Options::ENABLE_TABLES);
- options.insert(Options::ENABLE_FOOTNOTES);
- options.insert(Options::ENABLE_STRIKETHROUGH);
- options.insert(Options::ENABLE_TASKLISTS);
- options.insert(Options::ENABLE_SMART_PUNCTUATION);
- let parser = Parser::new_ext(markdown, options);
-
- // The sequence of blocks that represents the parsed document.
- let mut blocks = vec![];
-
- // The current set of inline elements we've collected. This gets
- // emptied whenever we finish a block.
- let mut inlines: Vec<Inline> = vec![];
-
- for event in parser {
- trace!("Parsing event: {:?}", event);
- match event {
- // We ignore these for now. They're not needed for codegen.
- Event::Html(_)
- | Event::FootnoteReference(_)
- | Event::SoftBreak
- | Event::HardBreak
- | Event::Rule
- | Event::TaskListMarker(_) => (),
-
- // Inline text of various kinds.
- Event::Text(text) => inlines.push(inline_text(&text)),
- Event::Code(text) => inlines.push(inline_code(&text)),
-
- // We only handle the end events.
- Event::Start(_) => (),
-
- // End of a block or inline.
- Event::End(tag) => match tag {
- // Collect inline elements for later inclusion in a block.
- Tag::Emphasis | Tag::Strong | Tag::Strikethrough => {
- inline_from_inlines(&tag, &mut inlines)
- }
- Tag::Paragraph => blocks.push(paragraph(&mut inlines)),
- Tag::Heading(level, _fragment, _classes) => {
- blocks.push(heading(level as i64, &mut inlines))
- }
- Tag::CodeBlock(kind) => blocks.push(code_block(&kind, &mut inlines)),
- Tag::Image(_link, dest, title) => blocks.push(image_block(&dest, &title)),
- // We don't handle anything else yet.
- _ => (),
- },
- }
- }
-
- // We MUST have emptied all inline elements.
- // assert!(inlines.is_empty());
-
- trace!("Parsing blocks: OK");
- blocks
-}
-
-fn inline_text(text: &str) -> Inline {
- Inline::Str(text.to_string())
-}
-
-fn inline_code(text: &str) -> Inline {
- let attr = ("".to_string(), vec![], vec![]);
- Inline::Code(attr, text.to_string())
-}
-
-fn paragraph(inlines: &mut Vec<Inline>) -> Block {
- Block::Para(std::mem::take(inlines))
-}
-
-fn heading(level: i64, inlines: &mut Vec<Inline>) -> Block {
- let attr = ("".to_string(), vec![], vec![]);
- Block::Header(level, attr, std::mem::take(inlines))
-}
-
-fn image_block(dest: &str, title: &str) -> Block {
- let attr = ("".to_string(), vec![], vec![]);
- Block::Para(vec![Inline::Image(
- attr,
- vec![],
- (dest.to_string(), title.to_string()),
- )])
-}
-
-fn code_block(kind: &CodeBlockKind, inlines: &mut Vec<Inline>) -> Block {
- trace!("code block: {:?}", kind);
- let attr = if let CodeBlockKind::Fenced(lang) = kind {
- trace!("fenced code block, lang={:?}", lang);
- parse_code_block_attrs(lang)
- } else {
- trace!("indented code block");
- parse_code_block_attrs("")
- };
- trace!("code block attrs: {:?}", attr);
- let mut code = String::new();
- for inline in inlines.drain(0..) {
- let text = plain_text_inline(inline);
- code.push_str(&text);
- }
- // pulldown_cmark and pandoc differ in their codeblock handling,
- // pulldown_cmark has an extra newline which we trim for now to be
- // compatible with pandoc's parsing
- if !code.is_empty() {
- assert_eq!(code.pop(), Some('\n'));
- }
- Block::CodeBlock(attr, code)
-}
-
-fn plain_text_inline(inline: Inline) -> String {
- match inline {
- Inline::Str(text) => text,
- Inline::Code(_, text) => text,
- Inline::Emph(inlines) => {
- let mut text = String::new();
- for inline in inlines {
- text.push_str(&plain_text_inline(inline));
- }
- text
- }
- _ => panic!("not text in code block: {:?}", inline),
- }
-}
-
-fn parse_code_block_attrs(attrs: &str) -> Attr {
- trace!("parsing code block attrs: {:?}", attrs);
- let mut id = "".to_string();
- let mut classes = vec![];
- let mut keyvalues = vec![];
- if attrs.starts_with('{') && attrs.ends_with('}') {
- let attrs = &attrs[1..attrs.len() - 1];
- for word in attrs.split_ascii_whitespace() {
- if let Some(x) = word.strip_prefix('#') {
- id = x.to_string();
- } else if let Some(x) = word.strip_prefix('.') {
- classes.push(x.to_string());
- } else if let Some(i) = word.find('=') {
- let k = &word[..i];
- let v = &word[i + 1..];
- keyvalues.push((k.to_string(), v.to_string()));
- }
- }
- } else if !attrs.is_empty() {
- classes.push(attrs.to_string());
- }
- (id, classes, keyvalues)
-}
-
-fn inline_from_inlines(tag: &Tag, inlines: &mut Vec<Inline>) {
- let new_inlines = inlines.clone();
- inlines.clear();
-
- let inline = match tag {
- Tag::Emphasis => Inline::Emph(new_inlines),
- Tag::Strong => Inline::Strong(new_inlines),
- Tag::Strikethrough => Inline::Strikeout(new_inlines),
- _ => unreachable!(),
- };
-
- inlines.push(inline);
-}
-
-/// Errors from Markdown parsing.
-#[derive(Debug, thiserror::Error)]
-pub enum Error {
- #[error(transparent)]
- Regex(#[from] regex::Error),
-
- #[error(transparent)]
- Yaml(#[from] serde_yaml::Error),
-}
-
-// Document metadata.
-//
-// This is expressed in the Markdown input file as an embedded YAML
-// block.
-//
-// Note that this structure needs to be able to capture any metadata
-// block we can work with, in any input file. By being strict here we
-// make it easier to tell the user when a metadata block has, say, a
-// misspelled field.
-#[derive(Debug, Default, Deserialize)]
-#[serde(deny_unknown_fields)]
-struct Metadata {
- title: String,
- subtitle: Option<String>,
- author: Option<String>,
- date: Option<String>,
- classes: Option<Vec<String>>,
- bibliography: Option<Vec<PathBuf>>,
- bindings: Option<Vec<PathBuf>>,
- documentclass: Option<String>,
- #[serde(default)]
- impls: BTreeMap<String, Vec<PathBuf>>,
-}
-
-impl Metadata {
- fn new(yaml_text: &str) -> Result<Self, Error> {
- trace!("Parsing YAML");
- let meta: Self = serde_yaml::from_str(yaml_text)?;
- Ok(meta)
- }
-
- fn to_map(&self) -> Map<String, MetaValue> {
- trace!("Creating metadata map from parsed YAML");
- let mut map: Map<String, MetaValue> = Map::new();
- map.insert(s("title"), meta_string(&self.title));
- if let Some(v) = &self.subtitle {
- map.insert(s("subtitle"), meta_string(v));
- }
- if let Some(v) = &self.author {
- map.insert(s("author"), meta_string(v));
- }
- if let Some(v) = &self.date {
- map.insert(s("date"), meta_string(v));
- }
- if let Some(v) = &self.classes {
- map.insert(s("classes"), meta_strings(v));
- }
- if !self.impls.is_empty() {
- let impls = self
- .impls
- .iter()
- .map(|(k, v)| (k.to_owned(), Box::new(meta_path_bufs(v))))
- .collect();
- map.insert(s("impls"), MetaValue::MetaMap(impls));
- }
- if let Some(v) = &self.bibliography {
- map.insert(s("bibliography"), meta_path_bufs(v));
- }
- if let Some(v) = &self.bindings {
- map.insert(s("bindings"), meta_path_bufs(v));
- }
- if let Some(v) = &self.documentclass {
- map.insert(s("documentclass"), meta_string(v));
- }
- trace!("Created metadata map from parsed YAML");
- map
- }
-}
-
-fn s(s: &str) -> String {
- s.to_string()
-}
-
-fn meta_string(s: &str) -> MetaValue {
- MetaValue::MetaString(s.to_string())
-}
-
-fn meta_strings(v: &[String]) -> MetaValue {
- MetaValue::MetaList(v.iter().map(|s| meta_string(s)).collect())
-}
-
-fn meta_path_buf(p: &Path) -> MetaValue {
- meta_string(&p.display().to_string())
-}
-
-fn meta_path_bufs(v: &[PathBuf]) -> MetaValue {
- MetaValue::MetaList(v.iter().map(|p| meta_path_buf(p)).collect())
-}
-
-#[cfg(test)]
-mod test {
- use super::{parse_code_block_attrs, AbstractSyntaxTree, Metadata};
- use super::{Block, Inline};
- use std::path::PathBuf;
- use std::str::FromStr;
-
- #[test]
- fn code_block_attrs() {
- assert_eq!(parse_code_block_attrs(""), ("".to_string(), vec![], vec![]));
- assert_eq!(
- parse_code_block_attrs("foo"),
- ("".to_string(), vec!["foo".to_string()], vec![])
- );
- assert_eq!(
- parse_code_block_attrs("{#foo}"),
- ("foo".to_string(), vec![], vec![])
- );
- assert_eq!(
- parse_code_block_attrs("{#foo .file bar=yo}"),
- (
- "foo".to_string(),
- vec!["file".to_string()],
- vec![("bar".to_string(), "yo".to_string())]
- )
- );
- }
-
- #[test]
- fn empty_input() {
- let ast = AbstractSyntaxTree::from_str("").unwrap();
- let doc = ast.to_pandoc();
- assert!(doc.blocks.is_empty());
- assert!(doc.meta.is_empty());
- assert!(!doc.pandoc_api_version.is_empty());
- }
-
- #[test]
- fn simple() {
- let ast = AbstractSyntaxTree::from_str(
- "\
- # Introduction \n\
- \n\
- First paragraph.\n\
- ",
- )
- .unwrap();
- let doc = ast.to_pandoc();
- assert!(doc.meta.is_empty());
- assert!(!doc.pandoc_api_version.is_empty());
-
- let attr = ("".to_string(), vec![], vec![]);
- let h = Block::Header(1, attr, vec![Inline::Str("Introduction".to_string())]);
- let para = Block::Para(vec![Inline::Str("First paragraph.".to_string())]);
- assert_eq!(doc.blocks, &[h, para]);
- }
-
- #[test]
- fn parses_leading_meta() {
- let markdown = "\n\n---\ntitle: Foo Bar\n...\nfoobar\n";
- let ast = AbstractSyntaxTree::from_str(markdown).unwrap();
- let doc = ast.to_pandoc();
- let keys: Vec<String> = doc.meta.keys().cloned().collect();
- assert_eq!(keys, ["title"]);
- }
-
- #[test]
- fn parses_trailing_meta() {
- let markdown = "foobar\n---\ntitle: Foo Bar\n...\n\n\n";
- let ast = AbstractSyntaxTree::from_str(markdown).unwrap();
- let doc = ast.to_pandoc();
- let keys: Vec<String> = doc.meta.keys().cloned().collect();
- assert_eq!(keys, ["title"]);
- }
-
- #[test]
- fn full_meta() {
- let meta = Metadata::new(
- "\
-title: Foo Bar
-date: today
-classes: [json, text]
-impls:
- python:
- - foo.py
- - bar.py
-bibliography:
-- foo.bib
-- bar.bib
-bindings:
-- foo.yaml
-- bar.yaml
-",
- )
- .unwrap();
- assert_eq!(meta.title, "Foo Bar");
- assert_eq!(meta.date.unwrap(), "today");
- assert_eq!(meta.classes.unwrap(), &["json", "text"]);
- assert_eq!(
- meta.bibliography.unwrap(),
- &[path("foo.bib"), path("bar.bib")]
- );
- assert_eq!(
- meta.bindings.unwrap(),
- &[path("foo.yaml"), path("bar.yaml")]
- );
- assert!(!meta.impls.is_empty());
- for (k, v) in meta.impls.iter() {
- assert_eq!(k, "python");
- assert_eq!(v, &[path("foo.py"), path("bar.py")]);
- }
- }
-
- fn path(s: &str) -> PathBuf {
- PathBuf::from(s)
- }
-}
diff --git a/src/bin/cli/mod.rs b/src/bin/cli/mod.rs
index a16df87..30ad5f6 100644
--- a/src/bin/cli/mod.rs
+++ b/src/bin/cli/mod.rs
@@ -3,16 +3,17 @@
#![allow(unused)]
use anyhow::Result;
+use clap::ValueEnum;
use log::trace;
use serde::Serialize;
use std::fmt::Debug;
use std::path::Path;
use std::str::FromStr;
use std::{collections::HashMap, convert::TryFrom};
-use subplot::{DataFile, Document, Style, SubplotError};
+use subplot::{Document, EmbeddedFile, Style, SubplotError};
-pub fn extract_file<'a>(doc: &'a Document, filename: &str) -> Result<&'a DataFile> {
- for file in doc.files() {
+pub fn extract_file<'a>(doc: &'a Document, filename: &str) -> Result<&'a EmbeddedFile> {
+ for file in doc.embedded_files() {
if file.filename() == filename {
return Ok(file);
}
@@ -26,7 +27,6 @@ pub struct Metadata {
title: String,
binding_files: Vec<String>,
impls: HashMap<String, Vec<String>>,
- bibliographies: Vec<String>,
scenarios: Vec<String>,
files: Vec<String>,
}
@@ -63,13 +63,6 @@ impl TryFrom<&mut Document> for Metadata {
(template.to_string(), filenames)
})
.collect();
- let mut bibliographies: Vec<_> = doc
- .meta()
- .bibliographies()
- .into_iter()
- .map(|p| filename(Some(p)))
- .collect();
- bibliographies.sort_unstable();
let mut scenarios: Vec<_> = doc
.scenarios()?
.into_iter()
@@ -77,7 +70,7 @@ impl TryFrom<&mut Document> for Metadata {
.collect();
scenarios.sort_unstable();
let mut files: Vec<_> = doc
- .files()
+ .embedded_files()
.iter()
.map(|f| f.filename().to_owned())
.collect();
@@ -87,7 +80,6 @@ impl TryFrom<&mut Document> for Metadata {
title,
binding_files,
impls,
- bibliographies,
scenarios,
files,
})
@@ -96,7 +88,7 @@ impl TryFrom<&mut Document> for Metadata {
impl Metadata {
fn write_list(v: &[String], prefix: &str) {
- v.iter().for_each(|entry| println!("{}: {}", prefix, entry))
+ v.iter().for_each(|entry| println!("{prefix}: {entry}"))
}
pub fn write_out(&self) {
@@ -106,9 +98,8 @@ impl Metadata {
let templates: Vec<String> = self.impls.keys().map(String::from).collect();
Self::write_list(&templates, "templates");
for (template, filenames) in self.impls.iter() {
- Self::write_list(filenames, &format!("functions[{}]", template));
+ Self::write_list(filenames, &format!("functions[{template}]"));
}
- Self::write_list(&self.bibliographies, "bibliography");
Self::write_list(&self.files, "file");
Self::write_list(&self.scenarios, "scenario");
}
@@ -125,7 +116,7 @@ fn filename(name: Option<&Path>) -> String {
}
}
-#[derive(Debug)]
+#[derive(Debug, ValueEnum, Clone, Copy)]
pub enum OutputFormat {
Plain,
Json,
@@ -138,7 +129,7 @@ impl FromStr for OutputFormat {
match s.to_ascii_lowercase().as_ref() {
"plain" => Ok(OutputFormat::Plain),
"json" => Ok(OutputFormat::Json),
- _ => Err(format!("Unknown output format: `{}`", s)),
+ _ => Err(format!("Unknown output format: `{s}`")),
}
}
}
diff --git a/src/bin/subplot-filter.rs b/src/bin/subplot-filter.rs
deleted file mode 100644
index 6c01241..0000000
--- a/src/bin/subplot-filter.rs
+++ /dev/null
@@ -1,16 +0,0 @@
-use anyhow::Result;
-use std::io::{self, Read, Write};
-use subplot::{Document, Style};
-
-fn main() -> Result<()> {
- let mut buffer = String::new();
- let mut stdin = io::stdin();
- stdin.read_to_string(&mut buffer)?;
- let basedir = std::path::Path::new(".");
- let style = Style::default();
- let mut doc = Document::from_json(&basedir, vec![], &buffer, style, None)?;
- doc.typeset();
- let bytes = doc.ast()?.into_bytes();
- io::stdout().write_all(&bytes)?;
- Ok(())
-}
diff --git a/src/bin/subplot.rs b/src/bin/subplot.rs
index f693a24..8dc8973 100644
--- a/src/bin/subplot.rs
+++ b/src/bin/subplot.rs
@@ -6,15 +6,15 @@ use anyhow::Result;
use env_logger::fmt::Color;
use log::{debug, error, info, trace, warn};
use subplot::{
- codegen, load_document, resource, DataFile, Document, MarkupOpts, Style, SubplotError,
+ codegen, load_document, resource, Binding, Bindings, Document, EmbeddedFile, MarkupOpts, Style,
+ SubplotError, Warnings,
};
use time::{format_description::FormatItem, macros::format_description, OffsetDateTime};
use clap::{CommandFactory, FromArgMatches, Parser};
use std::convert::TryFrom;
-use std::ffi::OsString;
-use std::fs::{self, write, File};
-use std::io::{Read, Write};
+use std::fs::{self, write};
+use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{self, Command};
use std::time::UNIX_EPOCH;
@@ -53,34 +53,35 @@ impl Toplevel {
#[derive(Debug, Parser)]
enum Cmd {
Extract(Extract),
- Filter(Filter),
Metadata(Metadata),
Docgen(Docgen),
Codegen(Codegen),
#[clap(hide = true)]
Resources(Resources),
+ #[clap(hide = true)]
+ Libdocgen(Libdocgen),
}
impl Cmd {
fn run(&self) -> Result<()> {
match self {
Cmd::Extract(e) => e.run(),
- Cmd::Filter(f) => f.run(),
Cmd::Metadata(m) => m.run(),
Cmd::Docgen(d) => d.run(),
Cmd::Codegen(c) => c.run(),
Cmd::Resources(r) => r.run(),
+ Cmd::Libdocgen(r) => r.run(),
}
}
fn doc_path(&self) -> Option<&Path> {
match self {
Cmd::Extract(e) => e.doc_path(),
- Cmd::Filter(f) => f.doc_path(),
Cmd::Metadata(m) => m.doc_path(),
Cmd::Docgen(d) => d.doc_path(),
Cmd::Codegen(c) => c.doc_path(),
Cmd::Resources(r) => r.doc_path(),
+ Cmd::Libdocgen(r) => r.doc_path(),
}
}
}
@@ -91,7 +92,7 @@ fn long_version() -> Result<String> {
writeln!(ret, "{}", render_testament!(VERSION))?;
writeln!(ret, "Crate version: {}", env!("CARGO_PKG_VERSION"))?;
if let Some(branch) = VERSION.branch_name {
- writeln!(ret, "Built from branch: {}", branch)?;
+ writeln!(ret, "Built from branch: {branch}")?;
} else {
writeln!(ret, "Branch information is missing.")?;
}
@@ -139,13 +140,7 @@ struct Extract {
merciful: bool,
/// Directory to write extracted files to
- #[clap(
- name = "DIR",
- long = "directory",
- short = 'd',
- parse(from_os_str),
- default_value = "."
- )]
+ #[clap(name = "DIR", long = "directory", short = 'd', default_value = ".")]
directory: PathBuf,
/// Don't actually write the files out
@@ -153,7 +148,6 @@ struct Extract {
dry_run: bool,
/// Input subplot document filename
- #[clap(parse(from_os_str))]
filename: PathBuf,
/// Names of embedded files to be extracted.
@@ -168,8 +162,8 @@ impl Extract {
fn run(&self) -> Result<()> {
let doc = load_linted_doc(&self.filename, Style::default(), None, self.merciful)?;
- let files: Vec<&DataFile> = if self.embedded.is_empty() {
- doc.files()
+ let files: Vec<&EmbeddedFile> = if self.embedded.is_empty() {
+ doc.embedded_files()
.iter()
.map(Result::Ok)
.collect::<Result<Vec<_>>>()
@@ -194,59 +188,6 @@ impl Extract {
}
#[derive(Debug, Parser)]
-/// Filter a pandoc JSON document.
-///
-/// This filters a pandoc JSON document, applying Subplot's formatting rules and
-/// image conversion support.
-///
-/// If input/output filename is provided, this operates on STDIN/STDOUT.
-struct Filter {
- #[clap(name = "INPUT", long = "input", short = 'i', parse(from_os_str))]
- /// Input file (uses STDIN if omitted)
- input: Option<PathBuf>,
-
- #[clap(name = "OUTPUT", long = "output", short = 'o', parse(from_os_str))]
- /// Output file (uses STDOUT if omitted)
- output: Option<PathBuf>,
-
- #[clap(name = "BASE", long = "base", short = 'b', parse(from_os_str))]
- /// Base directory (defaults to dir of input if given, or '.' if using STDIN)
- base: Option<PathBuf>,
-}
-
-impl Filter {
- fn doc_path(&self) -> Option<&Path> {
- self.input.as_deref().and_then(Path::parent)
- }
-
- fn run(&self) -> Result<()> {
- let mut buffer = String::new();
- if let Some(filename) = &self.input {
- File::open(filename)?.read_to_string(&mut buffer)?;
- } else {
- std::io::stdin().read_to_string(&mut buffer)?;
- }
- let basedir = if let Some(path) = &self.base {
- path.as_path()
- } else if let Some(path) = &self.input {
- path.parent().unwrap_or_else(|| Path::new("."))
- } else {
- Path::new(".")
- };
- let style = Style::default();
- let mut doc = Document::from_json(basedir, vec![], &buffer, style, None)?;
- doc.typeset();
- let bytes = doc.ast()?.into_bytes();
- if let Some(filename) = &self.output {
- File::create(filename)?.write_all(&bytes)?;
- } else {
- std::io::stdout().write_all(&bytes)?;
- }
- Ok(())
- }
-}
-
-#[derive(Debug, Parser)]
/// Extract metadata about a document
///
/// Load and process a subplot document, extracting various metadata about the
@@ -258,11 +199,10 @@ struct Metadata {
merciful: bool,
/// Form that you want the output to take
- #[clap(short = 'o', default_value = "plain", possible_values=&["plain", "json"])]
+ #[clap(short = 'o', default_value = "plain")]
output_format: cli::OutputFormat,
/// Input subplot document filename
- #[clap(parse(from_os_str))]
filename: PathBuf,
}
@@ -285,7 +225,7 @@ impl Metadata {
#[derive(Debug, Parser)]
/// Typeset subplot document
///
-/// Process a subplot document and typeset it using Pandoc.
+/// Process a subplot document and typeset it.
struct Docgen {
/// Allow warnings in document?
#[clap(long)]
@@ -298,11 +238,10 @@ struct Docgen {
template: Option<String>,
// Input Subplot document
- #[clap(parse(from_os_str))]
input: PathBuf,
// Output document filename
- #[clap(name = "FILE", long = "output", short = 'o', parse(from_os_str))]
+ #[clap(name = "FILE", long = "output", short = 'o')]
output: PathBuf,
// Set date.
@@ -316,14 +255,9 @@ impl Docgen {
}
fn run(&self) -> Result<()> {
- let mut style = Style::default();
- if self.output.extension() == Some(&OsString::from("pdf")) {
- trace!("PDF output chosen");
- style.typeset_links_as_notes();
- }
+ let style = Style::default();
let mut doc = load_linted_doc(&self.input, style, self.template.as_deref(), self.merciful)?;
- let mut pandoc = pandoc::new();
// Metadata date from command line or file mtime. However, we
// can't set it directly, since we don't want to override the date
// in the actual document, if given, so we only set
@@ -334,37 +268,38 @@ impl Docgen {
} else if let Some(date) = doc.meta().date() {
date.to_string()
} else {
- Self::mtime_formatted(Self::mtime(&self.input)?)
- };
- pandoc.add_option(pandoc::PandocOption::Meta("date".to_string(), Some(date)));
- pandoc.add_option(pandoc::PandocOption::TableOfContents);
- pandoc.add_option(pandoc::PandocOption::Standalone);
- pandoc.add_option(pandoc::PandocOption::NumberSections);
-
- if Self::need_output(&mut doc, self.template.as_deref(), &self.output) {
- doc.typeset();
- pandoc.set_input_format(pandoc::InputFormat::Json, vec![]);
- pandoc.set_input(pandoc::InputKind::Pipe(doc.ast()?));
- pandoc.set_output(pandoc::OutputKind::File(self.output.clone()));
-
- debug!("Executing pandoc to produce {}", self.output.display());
- let r = pandoc.execute();
- if let Err(pandoc::PandocError::Err(output)) = r {
- let code = output.status.code().or(Some(127)).unwrap();
- let stderr = String::from_utf8_lossy(&output.stderr);
- error!("Failed to execute Pandoc: exit code {}", code);
- error!("{}", stderr.strip_suffix('\n').unwrap());
-
- return Err(anyhow::Error::msg("Pandoc failed"));
+ let mut newest = None;
+ let basedir = if let Some(basedir) = self.input.parent() {
+ basedir.to_path_buf()
+ } else {
+ return Err(SubplotError::BasedirError(self.input.clone()).into());
+ };
+ for filename in doc.meta().markdown_filenames() {
+ let filename = basedir.join(filename);
+ let mtime = Self::mtime(&filename)?;
+ if let Some(so_far) = newest {
+ if mtime > so_far {
+ newest = Some(mtime);
+ }
+ } else {
+ newest = Some(mtime);
+ }
}
- r?;
- }
+ Self::mtime_formatted(newest.unwrap())
+ };
+
+ doc.typeset(&mut Warnings::default(), self.template.as_deref())?;
+ std::fs::write(&self.output, doc.to_html(&date)?)
+ .map_err(|e| SubplotError::WriteFile(self.output.clone(), e))?;
Ok(())
}
fn mtime(filename: &Path) -> Result<(u64, u32)> {
- let mtime = fs::metadata(filename)?.modified()?;
+ let mtime = fs::metadata(filename)
+ .map_err(|e| SubplotError::InputFileUnreadable(filename.into(), e))?
+ .modified()
+ .map_err(|e| SubplotError::InputFileMtime(filename.into(), e))?;
let mtime = mtime.duration_since(UNIX_EPOCH)?;
Ok((mtime.as_secs(), mtime.subsec_nanos()))
}
@@ -377,24 +312,6 @@ impl Docgen {
let time = OffsetDateTime::from_unix_timestamp(secs).unwrap();
time.format(DATE_FORMAT).unwrap()
}
-
- fn need_output(doc: &mut subplot::Document, template: Option<&str>, output: &Path) -> bool {
- let output = match Self::mtime(output) {
- Err(_) => return true,
- Ok(ts) => ts,
- };
-
- for filename in doc.sources(template) {
- let source = match Self::mtime(&filename) {
- Err(_) => return true,
- Ok(ts) => ts,
- };
- if source >= output {
- return true;
- }
- }
- false
- }
}
#[derive(Debug, Parser)]
@@ -410,16 +327,10 @@ struct Codegen {
template: Option<String>,
/// Input filename.
- #[clap(parse(from_os_str))]
filename: PathBuf,
/// Write generated test program to this file.
- #[clap(
- long,
- short,
- parse(from_os_str),
- help = "Writes generated test program to FILE"
- )]
+ #[clap(long, short, help = "Writes generated test program to FILE")]
output: PathBuf,
/// Run the generated test program after writing it?
@@ -464,13 +375,105 @@ impl Codegen {
}
}
+#[derive(Debug, Parser)]
+/// Generate test suites from Subplot documents
+///
+/// This reads a subplot document, extracts the scenarios, and writes out a test
+/// program capable of running the scenarios in the subplot document.
+struct Libdocgen {
+ // Bindings file to read.
+ input: PathBuf,
+
+ // Output document filename
+ #[clap(name = "FILE", long = "output", short = 'o')]
+ output: PathBuf,
+
+ /// The template to use from the document.
+ ///
+ /// If not specified, subplot will try and find a unique template name from the document
+ #[clap(name = "TEMPLATE", long = "template", short = 't')]
+ template: Option<String>,
+
+ /// Be merciful by allowing bindings to not have documentation.
+ #[clap(long)]
+ merciful: bool,
+}
+
+impl Libdocgen {
+ fn doc_path(&self) -> Option<&Path> {
+ None
+ }
+
+ fn run(&self) -> Result<()> {
+ debug!("libdocgen starts");
+
+ let mut bindings = Bindings::new();
+ bindings.add_from_file(&self.input, None)?;
+ // println!("{:#?}", bindings);
+
+ let mut doc = LibDoc::new(&self.input);
+ for b in bindings.bindings() {
+ // println!("{} {}", b.kind(), b.pattern());
+ doc.push_binding(b);
+ }
+
+ std::fs::write(&self.output, doc.to_markdown(self.merciful)?)?;
+
+ debug!("libdogen ends successfully");
+ Ok(())
+ }
+}
+
+struct LibDoc {
+ filename: PathBuf,
+ bindings: Vec<Binding>,
+}
+
+impl LibDoc {
+ fn new(filename: &Path) -> Self {
+ Self {
+ filename: filename.into(),
+ bindings: vec![],
+ }
+ }
+
+ fn push_binding(&mut self, binding: &Binding) {
+ self.bindings.push(binding.clone());
+ }
+
+ fn to_markdown(&self, merciful: bool) -> Result<String> {
+ let mut md = String::new();
+ md.push_str(&format!("# Library `{}`\n\n", self.filename.display()));
+ for b in self.bindings.iter() {
+ md.push_str(&format!("\n## {} `{}`\n", b.kind(), b.pattern()));
+ if let Some(doc) = b.doc() {
+ md.push_str(&format!("\n{}\n", doc));
+ } else if !merciful {
+ return Err(SubplotError::NoBindingDoc(
+ self.filename.clone(),
+ b.kind(),
+ b.pattern().into(),
+ )
+ .into());
+ }
+ if b.types().count() > 0 {
+ md.push_str("\nCaptures:\n\n");
+ for (name, cap_type) in b.types() {
+ md.push_str(&format!("- `{}`: {}\n", name, cap_type.as_str()));
+ }
+ }
+ }
+ Ok(md)
+ }
+}
+
fn load_linted_doc(
filename: &Path,
style: Style,
template: Option<&str>,
merciful: bool,
) -> Result<Document, SubplotError> {
- let mut doc = load_document(filename, style, None)?;
+ let doc = load_document(filename, style, None)?;
trace!("Got doc, now linting it");
doc.lint()?;
trace!("Doc linted ok");
@@ -489,33 +492,34 @@ fn load_linted_doc(
};
let template = template.to_string();
trace!("Template: {:#?}", template);
- doc.check_named_files_exist(&template)?;
- doc.check_matched_steps_have_impl(&template);
- doc.check_embedded_files_are_used(&template)?;
+ let mut warnings = Warnings::default();
+ doc.check_bindings(&mut warnings)?;
+ doc.check_named_code_blocks_have_appropriate_class(&mut warnings)?;
+ doc.check_named_files_exist(&template, &mut warnings)?;
+ if !template.is_empty() {
+ // We have a template, let's check we have implementations
+ doc.check_matched_steps_have_impl(&template, &mut warnings);
+ } else {
+ trace!("No template found, so cannot check impl presence");
+ }
+ doc.check_embedded_files_are_used(&template, &mut warnings)?;
- for w in doc.warnings() {
+ for w in warnings.warnings() {
warn!("{}", w);
}
- if !doc.warnings().is_empty() && !merciful {
- return Err(SubplotError::Warnings(doc.warnings().len()));
+ if !warnings.is_empty() && !merciful {
+ return Err(SubplotError::Warnings(warnings.len()));
}
Ok(doc)
}
-fn print_source_errors(e: Option<&dyn std::error::Error>) {
- if let Some(e) = e {
- error!("{}", e);
- print_source_errors(e.source());
- }
-}
-
fn real_main() {
info!("Starting Subplot");
let argparser = Toplevel::command();
let version = long_version().unwrap();
- let argparser = argparser.long_version(version.as_str());
+ let argparser = argparser.long_version(version);
let args = argparser.get_matches();
let args = Toplevel::from_arg_matches(&args).unwrap();
args.handle_special_args();
@@ -525,7 +529,11 @@ fn real_main() {
}
Err(e) => {
error!("{}", e);
- print_source_errors(e.source());
+ let mut e = e.source();
+ while let Some(source) = e {
+ error!("caused by: {}", source);
+ e = source.source();
+ }
process::exit(1);
}
}
diff --git a/src/bindings.rs b/src/bindings.rs
index 5db63c9..825b895 100644
--- a/src/bindings.rs
+++ b/src/bindings.rs
@@ -3,13 +3,14 @@ use super::MatchedSteps;
use super::PartialStep;
use super::ScenarioStep;
use super::StepKind;
+use crate::Warning;
use crate::{resource, SubplotError};
use serde::{Deserialize, Serialize};
use serde_aux::prelude::*;
use std::collections::HashMap;
-use std::fmt::Debug;
+use std::fmt::{Debug, Write};
use std::ops::Deref;
use std::path::Path;
use std::str::FromStr;
@@ -49,6 +50,10 @@ pub enum CaptureType {
/// for one of the embedded files in the document, otherwise codegen will
/// refuse to run.
File,
+
+ /// Paths are sequences of non-whitespace characters which will be interpreted
+ /// as a file-path (e.g. PathBuf) or similar when processed by a step
+ Path,
}
impl FromStr for CaptureType {
@@ -62,6 +67,7 @@ impl FromStr for CaptureType {
"uint" => Ok(Self::Uint),
"number" => Ok(Self::Number),
"file" => Ok(Self::File),
+ "path" => Ok(Self::Path),
_ => Err(SubplotError::UnknownTypeInBinding(value.to_string())),
}
}
@@ -77,6 +83,7 @@ impl CaptureType {
Self::Uint => "uint",
Self::Number => "number",
Self::File => "file",
+ Self::Path => "path",
}
}
@@ -89,6 +96,7 @@ impl CaptureType {
Self::Uint => r"\d+",
Self::Number => r"-?\d+(\.\d+)?",
Self::File => r"\S+",
+ Self::Path => r"\S+",
}
}
}
@@ -159,6 +167,8 @@ pub struct Binding {
regex: Regex,
impls: HashMap<String, Arc<BindingImpl>>,
types: HashMap<String, CaptureType>,
+ doc: Option<String>,
+ filename: Arc<Path>,
}
impl Binding {
@@ -167,18 +177,14 @@ impl Binding {
kind: StepKind,
pattern: &str,
case_sensitive: bool,
- mut types: HashMap<String, CaptureType>,
+ types: HashMap<String, CaptureType>,
+ doc: Option<String>,
+ filename: Arc<Path>,
) -> Result<Binding, SubplotError> {
- let regex = RegexBuilder::new(&format!("^{}$", pattern))
+ let regex = RegexBuilder::new(&format!("^{pattern}$"))
.case_insensitive(!case_sensitive)
.build()
.map_err(|err| SubplotError::Regex(pattern.to_string(), err))?;
- // For every named capture, ensure we have a known type for it.
- // If the type is missing from the map, we default to `text` which is
- // the .* pattern
- for capture in regex.capture_names().flatten() {
- types.entry(capture.into()).or_insert(CaptureType::Text);
- }
Ok(Binding {
kind,
@@ -186,6 +192,8 @@ impl Binding {
regex,
impls: HashMap::new(),
types,
+ doc,
+ filename,
})
}
@@ -207,6 +215,11 @@ impl Binding {
&self.pattern
}
+ /// Return documentation string for binding, if any.
+ pub fn doc(&self) -> Option<&str> {
+ self.doc.as_deref()
+ }
+
/// Retrieve a particular implementation by name
pub fn step_impl(&self, template: &str) -> Option<Arc<BindingImpl>> {
self.impls.get(template).cloned()
@@ -235,7 +248,7 @@ impl Binding {
let caps = self.regex.captures(step_text)?;
// If there is only one capture, it's the whole string.
- let mut m = MatchedStep::new(self, template);
+ let mut m = MatchedStep::new(self, template, step.origin().clone());
if caps.len() == 1 {
m.append_part(PartialStep::uncaptured(step_text));
return Some(m);
@@ -268,14 +281,14 @@ impl Binding {
let cap = cap.as_str();
// These unwraps are safe because we ensured the map is complete
// in the constructor, and that all the types are known.
- let ty = self.types.get(name).unwrap();
- let rx = &KIND_PATTERNS.get(ty).unwrap();
+ let kind = self.types.get(name).copied().unwrap_or(CaptureType::Text);
+ let rx = KIND_PATTERNS.get(&kind).unwrap();
if !rx.is_match(cap) {
// This capture doesn't match the kind so it's not
// valid for this binding.
return None;
}
- PartialStep::text(name, cap)
+ PartialStep::text(name, cap, kind)
}
};
@@ -291,6 +304,39 @@ impl Binding {
Some(m)
}
+
+ fn filename(&self) -> &Path {
+ &self.filename
+ }
+
+ fn check(&self, warnings: &mut crate::Warnings) -> Result<(), SubplotError> {
+ fn nth(i: usize) -> &'static str {
+ match i % 10 {
+ 1 => "st",
+ 2 => "nd",
+ 3 => "rd",
+ _ => "th",
+ }
+ }
+ for (nr, capture) in self.regex.capture_names().enumerate().skip(1) {
+ if let Some(name) = capture {
+ if !self.types.contains_key(name) {
+ warnings.push(Warning::MissingCaptureType(
+ self.filename().to_owned(),
+ format!("{}: {}", self.kind(), self.pattern()),
+ name.to_string(),
+ ));
+ }
+ } else {
+ warnings.push(Warning::MissingCaptureName(
+ self.filename().to_owned(),
+ format!("{}: {}", self.kind(), self.pattern()),
+ format!("{nr}{}", nth(nr)),
+ ));
+ }
+ }
+ Ok(())
+ }
}
impl PartialEq for Binding {
@@ -303,15 +349,31 @@ impl Eq for Binding {}
#[cfg(test)]
mod test_binding {
- use super::Binding;
+ use super::{Binding, CaptureType};
+ use crate::html::Location;
use crate::PartialStep;
use crate::ScenarioStep;
use crate::StepKind;
use std::collections::HashMap;
+ use std::path::Path;
+ use std::path::PathBuf;
+ use std::sync::Arc;
+
+ fn path() -> Arc<Path> {
+ PathBuf::new().into()
+ }
#[test]
fn creates_new() {
- let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
+ let b = Binding::new(
+ StepKind::Given,
+ "I am Tomjon",
+ false,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap();
assert_eq!(b.kind(), StepKind::Given);
assert!(b.regex().is_match("I am Tomjon"));
assert!(!b.regex().is_match("I am Tomjon of Lancre"));
@@ -320,19 +382,45 @@ mod test_binding {
#[test]
fn equal() {
- let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
- let b = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
+ let a = Binding::new(
+ StepKind::Given,
+ "I am Tomjon",
+ false,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap();
+ let b = Binding::new(
+ StepKind::Given,
+ "I am Tomjon",
+ false,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap();
assert_eq!(a, b);
}
#[test]
fn not_equal() {
- let a = Binding::new(StepKind::Given, "I am Tomjon", false, HashMap::new()).unwrap();
+ let a = Binding::new(
+ StepKind::Given,
+ "I am Tomjon",
+ false,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap();
let b = Binding::new(
StepKind::Given,
"I am Tomjon of Lancre",
false,
HashMap::new(),
+ None,
+ path(),
)
.unwrap();
assert_ne!(a, b);
@@ -340,22 +428,22 @@ mod test_binding {
#[test]
fn does_not_match_with_wrong_kind() {
- let step = ScenarioStep::new(StepKind::Given, "given", "yo");
- let b = Binding::new(StepKind::When, "yo", false, HashMap::new()).unwrap();
+ let step = ScenarioStep::new(StepKind::Given, "given", "yo", Location::Unknown);
+ let b = Binding::new(StepKind::When, "yo", false, HashMap::new(), None, path()).unwrap();
assert!(b.match_with_step("", &step).is_none());
}
#[test]
fn does_not_match_with_wrong_text() {
- let step = ScenarioStep::new(StepKind::Given, "given", "foo");
- let b = Binding::new(StepKind::Given, "bar", false, HashMap::new()).unwrap();
+ let step = ScenarioStep::new(StepKind::Given, "given", "foo", Location::Unknown);
+ let b = Binding::new(StepKind::Given, "bar", false, HashMap::new(), None, path()).unwrap();
assert!(b.match_with_step("", &step).is_none());
}
#[test]
fn match_with_fixed_pattern() {
- let step = ScenarioStep::new(StepKind::Given, "given", "foo");
- let b = Binding::new(StepKind::Given, "foo", false, HashMap::new()).unwrap();
+ let step = ScenarioStep::new(StepKind::Given, "given", "foo", Location::Unknown);
+ let b = Binding::new(StepKind::Given, "foo", false, HashMap::new(), None, path()).unwrap();
let m = b.match_with_step("", &step).unwrap();
assert_eq!(m.kind(), StepKind::Given);
let mut parts = m.parts();
@@ -366,29 +454,55 @@ mod test_binding {
#[test]
fn match_with_regex() {
- let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon, I am");
+ let step = ScenarioStep::new(
+ StepKind::Given,
+ "given",
+ "I am Tomjon, I am",
+ Location::Unknown,
+ );
let b = Binding::new(
StepKind::Given,
r"I am (?P<who>\S+), I am",
false,
HashMap::new(),
+ None,
+ path(),
)
.unwrap();
let m = b.match_with_step("", &step).unwrap();
assert_eq!(m.kind(), StepKind::Given);
let mut parts = m.parts();
assert_eq!(parts.next().unwrap(), &PartialStep::uncaptured("I am "));
- assert_eq!(parts.next().unwrap(), &PartialStep::text("who", "Tomjon"));
+ assert_eq!(
+ parts.next().unwrap(),
+ &PartialStep::text("who", "Tomjon", CaptureType::Text)
+ );
assert_eq!(parts.next().unwrap(), &PartialStep::uncaptured(", I am"));
assert_eq!(parts.next(), None);
}
#[test]
fn case_sensitive_mismatch() {
- let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
- let b = Binding::new(StepKind::Given, r"i am tomjon", false, HashMap::new()).unwrap();
+ let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
+ let b = Binding::new(
+ StepKind::Given,
+ r"i am tomjon",
+ false,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap();
assert!(b.match_with_step("", &step).is_some());
- let b = Binding::new(StepKind::Given, r"i am tomjon", true, HashMap::new()).unwrap();
+ let b = Binding::new(
+ StepKind::Given,
+ r"i am tomjon",
+ true,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap();
assert!(b.match_with_step("", &step).is_none());
}
}
@@ -400,13 +514,14 @@ pub struct Bindings {
}
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
struct ParsedImpl {
function: String,
cleanup: Option<String>,
}
#[derive(Debug, Deserialize)]
-#[serde(transparent)]
+#[serde(transparent, deny_unknown_fields)]
struct ParsedImplWrapper {
#[serde(deserialize_with = "deserialize_struct_case_insensitive")]
pimpl: ParsedImpl,
@@ -421,6 +536,7 @@ impl Deref for ParsedImplWrapper {
}
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
struct ParsedBinding {
given: Option<String>,
when: Option<String>,
@@ -432,10 +548,11 @@ struct ParsedBinding {
case_sensitive: bool,
#[serde(default)]
types: HashMap<String, CaptureType>,
+ doc: Option<String>,
}
#[derive(Debug, Deserialize)]
-#[serde(transparent)]
+#[serde(transparent, deny_unknown_fields)]
struct ParsedBindingWrapper {
#[serde(deserialize_with = "deserialize_struct_case_insensitive")]
binding: ParsedBinding,
@@ -463,11 +580,11 @@ impl Bindings {
}
/// Add bindings from a YAML string
- pub fn add_from_yaml(&mut self, yaml: &str) -> Result<(), SubplotError> {
+ pub fn add_from_yaml(&mut self, yaml: &str, filename: Arc<Path>) -> Result<(), SubplotError> {
let bindings: Vec<ParsedBindingWrapper> =
serde_yaml::from_str(yaml).map_err(SubplotError::Metadata)?;
for wrapper in bindings {
- self.add(from_hashmap(&wrapper.binding)?);
+ self.add(from_hashmap(&wrapper.binding, Arc::clone(&filename))?);
}
Ok(())
}
@@ -509,12 +626,12 @@ impl Bindings {
where
P: AsRef<Path> + Debug,
{
- let yaml = resource::read_as_string(filename.as_ref(), template)
- .map_err(|e| SubplotError::BindingsFileNotFound(filename.as_ref().into(), e))?;
+ let filename = filename.as_ref();
+ let yaml = resource::read_as_string(filename, template)
+ .map_err(|e| SubplotError::BindingsFileNotFound(filename.into(), e))?;
trace!("Loaded file content");
- self.add_from_yaml(&yaml).map_err(|e| {
- SubplotError::BindingFileParseError(filename.as_ref().to_owned(), Box::new(e))
- })?;
+ self.add_from_yaml(&yaml, filename.to_owned().into())
+ .map_err(|e| SubplotError::BindingFileParseError(filename.to_owned(), Box::new(e)))?;
Ok(())
}
@@ -526,20 +643,28 @@ impl Bindings {
.filter(|b| b.kind() == kind && b.pattern() == pattern);
m.count() == 1
}
+
+ /// Check these bindings for any warnings which users might need to know about
+ pub fn check(&self, warnings: &mut crate::Warnings) -> Result<(), SubplotError> {
+ for binding in self.bindings() {
+ binding.check(warnings)?;
+ }
+ Ok(())
+ }
}
-fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> {
+fn from_hashmap(parsed: &ParsedBinding, filename: Arc<Path>) -> Result<Binding, SubplotError> {
let given: i32 = parsed.given.is_some().into();
let when: i32 = parsed.when.is_some().into();
let then: i32 = parsed.then.is_some().into();
if given + when + then == 0 {
- let msg = format!("{:?}", parsed);
+ let msg = format!("{parsed:?}");
return Err(SubplotError::BindingWithoutKnownKeyword(msg));
}
if given + when + then > 1 {
- let msg = format!("{:?}", parsed);
+ let msg = format!("{parsed:?}");
return Err(SubplotError::BindingHasManyKeywords(msg));
}
@@ -550,7 +675,7 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> {
} else if parsed.then.is_some() {
(StepKind::Then, parsed.then.as_ref().unwrap())
} else {
- let msg = format!("{:?}", parsed);
+ let msg = format!("{parsed:?}");
return Err(SubplotError::BindingWithoutKnownKeyword(msg));
};
@@ -565,7 +690,14 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> {
trace!("Successfully acquired binding");
- let mut ret = Binding::new(kind, &pattern, parsed.case_sensitive, types)?;
+ let mut ret = Binding::new(
+ kind,
+ &pattern,
+ parsed.case_sensitive,
+ types,
+ parsed.doc.clone(),
+ filename,
+ )?;
trace!("Binding parsed OK");
for (template, pimpl) in &parsed.impls {
ret.add_impl(template, &pimpl.function, pimpl.cleanup.as_deref());
@@ -576,6 +708,8 @@ fn from_hashmap(parsed: &ParsedBinding) -> Result<Binding, SubplotError> {
#[cfg(test)]
mod test_bindings {
+ use crate::bindings::CaptureType;
+ use crate::html::Location;
use crate::Binding;
use crate::Bindings;
use crate::PartialStep;
@@ -584,6 +718,13 @@ mod test_bindings {
use crate::SubplotError;
use std::collections::HashMap;
+ use std::path::Path;
+ use std::path::PathBuf;
+ use std::sync::Arc;
+
+ fn path() -> Arc<Path> {
+ PathBuf::new().into()
+ }
#[test]
fn has_no_bindings_initially() {
@@ -598,6 +739,8 @@ mod test_bindings {
r"I am (?P<name>\S+)",
false,
HashMap::new(),
+ None,
+ path(),
)
.unwrap();
let mut bindings = Bindings::new();
@@ -633,8 +776,8 @@ mod test_bindings {
total: word
";
let mut bindings = Bindings::new();
- bindings.add_from_yaml(yaml).unwrap();
- println!("test: {:?}", bindings);
+ bindings.add_from_yaml(yaml, path()).unwrap();
+ println!("test: {bindings:?}");
assert!(bindings.has(StepKind::Given, "I am Tomjon"));
assert!(bindings.has(StepKind::When, "I declare myself king"));
assert!(bindings.has(StepKind::Then, "there is applause"));
@@ -653,7 +796,7 @@ mod test_bindings {
python:
FUNCTION: set_name
";
- match Bindings::new().add_from_yaml(yaml) {
+ match Bindings::new().add_from_yaml(yaml, path()) {
Ok(_) => unreachable!(),
Err(SubplotError::BindingHasManyKeywords(_)) => (),
Err(e) => panic!("Incorrect error: {}", e),
@@ -670,7 +813,7 @@ mod test_bindings {
types:
age: number
";
- match Bindings::new().add_from_yaml(yaml) {
+ match Bindings::new().add_from_yaml(yaml, path()) {
Ok(_) => unreachable!(),
Err(SubplotError::SimplePatternKindMismatch(_)) => (),
Err(e) => panic!("Incorrect error: {}", e),
@@ -679,8 +822,16 @@ mod test_bindings {
#[test]
fn does_not_find_match_for_unmatching_kind() {
- let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
- let binding = Binding::new(StepKind::When, r"I am Tomjon", false, HashMap::new()).unwrap();
+ let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
+ let binding = Binding::new(
+ StepKind::When,
+ r"I am Tomjon",
+ false,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap();
let mut bindings = Bindings::new();
bindings.add(binding);
assert!(matches!(
@@ -691,12 +842,14 @@ mod test_bindings {
#[test]
fn does_not_find_match_for_unmatching_pattern() {
- let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
+ let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
let binding = Binding::new(
StepKind::Given,
r"I am Tomjon of Lancre",
false,
HashMap::new(),
+ None,
+ path(),
)
.unwrap();
let mut bindings = Bindings::new();
@@ -709,9 +862,19 @@ mod test_bindings {
#[test]
fn two_matching_bindings() {
- let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
+ let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
let mut bindings = Bindings::default();
- bindings.add(Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new()).unwrap());
+ bindings.add(
+ Binding::new(
+ StepKind::Given,
+ r"I am Tomjon",
+ false,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap(),
+ );
bindings.add(
Binding::new(
StepKind::Given,
@@ -719,6 +882,8 @@ mod test_bindings {
.unwrap(),
false,
HashMap::new(),
+ None,
+ path(),
)
.unwrap(),
);
@@ -730,8 +895,16 @@ mod test_bindings {
#[test]
fn finds_match_for_fixed_string_pattern() {
- let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
- let binding = Binding::new(StepKind::Given, r"I am Tomjon", false, HashMap::new()).unwrap();
+ let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
+ let binding = Binding::new(
+ StepKind::Given,
+ r"I am Tomjon",
+ false,
+ HashMap::new(),
+ None,
+ path(),
+ )
+ .unwrap();
let mut bindings = Bindings::new();
bindings.add(binding);
let m = bindings.find("", &step).unwrap();
@@ -747,12 +920,14 @@ mod test_bindings {
#[test]
fn finds_match_for_regexp_pattern() {
- let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon");
+ let step = ScenarioStep::new(StepKind::Given, "given", "I am Tomjon", Location::Unknown);
let binding = Binding::new(
StepKind::Given,
r"I am (?P<name>\S+)",
false,
HashMap::new(),
+ None,
+ path(),
)
.unwrap();
let mut bindings = Bindings::new();
@@ -767,9 +942,10 @@ mod test_bindings {
}
let p = parts.next().unwrap();
match p {
- PartialStep::CapturedText { name, text } => {
+ PartialStep::CapturedText { name, text, kind } => {
assert_eq!(name, "name");
assert_eq!(text, "Tomjon");
+ assert_eq!(kind, &CaptureType::Text);
}
_ => panic!("unexpected part: {:?}", p),
}
@@ -780,13 +956,14 @@ mod test_bindings {
lazy_static! {
static ref KIND_PATTERNS: HashMap<CaptureType, Regex> = {
let mut map = HashMap::new();
- for ty in (&[
+ for ty in ([
CaptureType::Word,
CaptureType::Text,
CaptureType::Int,
CaptureType::Uint,
CaptureType::Number,
CaptureType::File,
+ CaptureType::Path,
]).iter().copied() {
// This Unwrap is okay because we shouldn't have any bugs in the
// regular expressions here, and if we did, it'd be bad for everyone
@@ -853,7 +1030,7 @@ fn regex_from_simple_pattern(
}
};
- r.push_str(&format!(r"(?P<{}>{})", name, kind.regex_str()));
+ write!(r, r"(?P<{}>{})", name, kind.regex_str()).map_err(SubplotError::StringFormat)?;
end = m.end();
}
let after = &pattern[end..];
diff --git a/src/codegen.rs b/src/codegen.rs
index 5c4255f..5855d8b 100644
--- a/src/codegen.rs
+++ b/src/codegen.rs
@@ -1,10 +1,11 @@
+use crate::html::Location;
use crate::{resource, Document, SubplotError, TemplateSpec};
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
-use base64::encode;
+use base64::prelude::{Engine as _, BASE64_STANDARD};
use serde::Serialize;
use tera::{Context, Tera, Value};
@@ -32,7 +33,7 @@ fn context(doc: &mut Document, template: &str) -> Result<Context, SubplotError>
let mut context = Context::new();
let scenarios = doc.matched_scenarios(template)?;
context.insert("scenarios", &scenarios);
- context.insert("files", doc.files());
+ context.insert("files", doc.embedded_files());
let mut funcs = vec![];
if let Some(docimpl) = doc.meta().document_impl(template) {
@@ -53,12 +54,11 @@ fn context(doc: &mut Document, template: &str) -> Result<Context, SubplotError>
}
fn tera(tmplspec: &TemplateSpec, templatename: &str) -> Result<Tera, SubplotError> {
- // Tera insists on a glob, but we want to load a specific template
- // only, so we use a glob that doesn't match anything.
- let mut tera = Tera::new("/..IGNORE-THIS../..SUBPLOT-TERA-NOT-EXIST../*").expect("new");
+ let mut tera = Tera::default();
tera.register_filter("base64", base64);
tera.register_filter("nameslug", nameslug);
tera.register_filter("commentsafe", commentsafe);
+ tera.register_filter("location", locationfilter);
let dirname = tmplspec.template_filename().parent().unwrap();
for helper in tmplspec.helpers() {
let helper_path = dirname.join(helper);
@@ -88,13 +88,28 @@ fn write(filename: &Path, content: &str) -> Result<(), SubplotError> {
fn base64(v: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
match v {
- Value::String(s) => Ok(Value::String(encode(s))),
+ Value::String(s) => Ok(Value::String(BASE64_STANDARD.encode(s))),
_ => Err(tera::Error::msg(
"can only base64 encode strings".to_string(),
)),
}
}
+fn locationfilter(v: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
+ let location: Location = serde_json::from_value(v.clone())?;
+ Ok(Value::String(format!(
+ "{:?}",
+ match location {
+ Location::Known {
+ filename,
+ line,
+ col,
+ } => format!("{}:{}:{}", filename.display(), line, col),
+ Location::Unknown => "unknown".to_string(),
+ }
+ )))
+}
+
fn nameslug(name: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
match name {
Value::String(s) => {
@@ -170,10 +185,10 @@ mod test {
#[test]
fn verify_name_slugification() {
static GOOD_CASES: &[(&str, &str)] = &[
- ("foobar", "foobar"), // Simple words pass through
- ("FooBar", "foobar"), // Capital letters are lowercased
+ ("foobar", "foobar"), // Simple words pass through
+ ("FooBar", "foobar"), // Capital letters are lowercased
("Motörhead", "mot_rhead"), // Non-ascii characters are changed for underscores
- ("foo bar", "foo_bar"), // As is whitespace etc.
+ ("foo bar", "foo_bar"), // As is whitespace etc.
];
for (input, output) in GOOD_CASES.iter().copied() {
let input = Value::from(input);
diff --git a/src/diagrams.rs b/src/diagrams.rs
index 6e0b875..5d91c2e 100644
--- a/src/diagrams.rs
+++ b/src/diagrams.rs
@@ -59,6 +59,32 @@ lazy_static! {
static ref JAVA_PATH: Mutex<PathBuf> = Mutex::new(env!("BUILTIN_JAVA_PATH").into());
}
+/// An SVG image.
+///
+/// SVG images are vector images, but we only need to treat them as
+/// opaque blobs of bytes, so we don't try to represent them in any
+/// other way.
+pub struct Svg {
+ data: Vec<u8>,
+}
+
+impl Svg {
+ fn new(data: Vec<u8>) -> Self {
+ Self { data }
+ }
+
+ /// Return slice of the bytes of the image.
+ pub fn data(&self) -> &[u8] {
+ &self.data
+ }
+
+ /// Number of bytes in the binary representation of the image.
+ #[allow(clippy::len_without_is_empty)] // is-empty doesn't make sense
+ pub fn len(&self) -> usize {
+ self.data.len()
+ }
+}
+
/// A code block with markup for a diagram.
///
/// The code block will be converted to an SVG image using an external
@@ -71,7 +97,7 @@ lazy_static! {
/// for the trait.
pub trait DiagramMarkup {
/// Convert the markup into an SVG.
- fn as_svg(&self) -> Result<Vec<u8>, SubplotError>;
+ fn as_svg(&self) -> Result<Svg, SubplotError>;
}
/// A code block with pikchr markup.
@@ -98,12 +124,12 @@ impl PikchrMarkup {
}
impl DiagramMarkup for PikchrMarkup {
- fn as_svg(&self) -> Result<Vec<u8>, SubplotError> {
+ fn as_svg(&self) -> Result<Svg, SubplotError> {
let mut flags = pikchr::PikchrFlags::default();
flags.generate_plain_errors();
let image = pikchr::Pikchr::render(&self.markup, self.class.as_deref(), flags)
.map_err(SubplotError::PikchrRenderError)?;
- Ok(image.as_bytes().to_vec())
+ Ok(Svg::new(image.as_bytes().to_vec()))
}
}
@@ -129,7 +155,7 @@ impl DotMarkup {
}
impl DiagramMarkup for DotMarkup {
- fn as_svg(&self) -> Result<Vec<u8>, SubplotError> {
+ fn as_svg(&self) -> Result<Svg, SubplotError> {
let path = DOT_PATH.lock().unwrap().clone();
let mut child = Command::new(&path)
.arg("-Tsvg")
@@ -146,7 +172,7 @@ impl DiagramMarkup for DotMarkup {
.wait_with_output()
.map_err(SubplotError::WaitForChild)?;
if output.status.success() {
- Ok(output.stdout)
+ Ok(Svg::new(output.stdout))
} else {
Err(SubplotError::child_failed("dot", &output))
}
@@ -191,12 +217,10 @@ impl PlantumlMarkup {
}
env::join_paths(Some(java_bin).iter().chain(cur_path.iter())).ok()
}
-
- // Acquire path to JAR for pandoc
}
impl DiagramMarkup for PlantumlMarkup {
- fn as_svg(&self) -> Result<Vec<u8>, SubplotError> {
+ fn as_svg(&self) -> Result<Svg, SubplotError> {
let path = JAVA_PATH.lock().unwrap().clone();
let mut cmd = Command::new(&path);
cmd.arg("-Djava.awt.headless=true")
@@ -225,7 +249,7 @@ impl DiagramMarkup for PlantumlMarkup {
.wait_with_output()
.map_err(SubplotError::WaitForChild)?;
if output.status.success() {
- Ok(output.stdout)
+ Ok(Svg::new(output.stdout))
} else {
Err(SubplotError::child_failed("plantuml", &output))
}
diff --git a/src/doc.rs b/src/doc.rs
index 71266d6..ff52b79 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -1,31 +1,34 @@
-use crate::ast;
+use crate::bindings::CaptureType;
use crate::generate_test_program;
use crate::get_basedir_from;
-use crate::visitor;
-use crate::DataFile;
-use crate::DataFiles;
-use crate::LintingVisitor;
+use crate::html::Attribute;
+use crate::html::HtmlPage;
+use crate::html::{Content, Element, ElementTag};
+use crate::md::Markdown;
+use crate::resource;
+use crate::EmbeddedFile;
+use crate::EmbeddedFiles;
use crate::MatchedScenario;
-use crate::Metadata;
use crate::PartialStep;
use crate::Scenario;
-use crate::ScenarioStep;
use crate::Style;
use crate::SubplotError;
-use crate::{bindings::CaptureType, parser::parse_scenario_snippet};
+use crate::{Metadata, YamlMetadata};
use crate::{Warning, Warnings};
+use std::cmp::Ordering;
use std::collections::HashSet;
use std::default::Default;
use std::fmt::Debug;
+use std::fs::read;
use std::ops::Deref;
use std::path::{Path, PathBuf};
-use std::str::FromStr;
-
-use pandoc_ast::{MutVisitor, Pandoc};
use log::{error, trace};
+/// Name of standard Subplot CSS file.
+const CSS: &str = "subplot.css";
+
/// The set of known (special) classes which subplot will always recognise
/// as being valid.
static SPECIAL_CLASSES: &[&str] = &[
@@ -36,30 +39,21 @@ static SPECIAL_CLASSES: &[&str] = &[
/// as being valid.
static KNOWN_FILE_CLASSES: &[&str] = &["rust", "yaml", "python", "sh", "shell", "markdown", "bash"];
-/// The set of known (special-to-pandoc) classes which subplot will always recognise
-/// as being valid. We include the subplot-specific noNumberLines class which we use
-/// to invert the default numberLines on .file blocks.
-static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"];
+/// The set of known classes which subplot will always recognise as
+/// being valid. We include the subplot-specific noNumberLines class
+/// which we use to invert the default numberLines on .file blocks.
+static KNOWN_BLOCK_CLASSES: &[&str] = &["numberLines", "noNumberLines"];
+
+/// The set of classes which subplot will recognise as being appropriate
+/// for having IDs.
+static ID_OK_CLASSES: &[&str] = &["file", "example"];
/// A parsed Subplot document.
///
-/// Pandoc works by parsing its various input files and constructing
-/// an abstract syntax tree or AST. When Pandoc generates output, it
-/// works based on the AST. This way, the input parsing and output
-/// generation are cleanly separated.
-///
-/// A Pandoc filter can modify the AST before output generation
-/// starts working. This allows the filter to make changes to what
-/// gets output, without having to understand the input documents at
-/// all.
-///
-/// This function is a Pandoc filter, to be use with
-/// pandoc_ast::filter, for typesetting Subplot documents.
-///
/// # Example
///
/// fix this example;
-/// ~~~~
+/// ~~~~ignored
/// let markdown = "\
/// ---
/// title: Test Title
@@ -81,82 +75,43 @@ static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"];
/// ~~~~
#[derive(Debug)]
pub struct Document {
- markdowns: Vec<PathBuf>,
- ast: Pandoc,
+ subplot: PathBuf,
+ markdowns: Vec<Markdown>,
meta: Metadata,
- files: DataFiles,
+ files: EmbeddedFiles,
style: Style,
- warnings: Warnings,
}
-impl<'a> Document {
+impl Document {
fn new(
- markdowns: Vec<PathBuf>,
- ast: Pandoc,
+ subplot: PathBuf,
+ markdowns: Vec<Markdown>,
meta: Metadata,
- files: DataFiles,
+ files: EmbeddedFiles,
style: Style,
) -> Document {
- Document {
+ let doc = Document {
+ subplot,
markdowns,
- ast,
meta,
files,
style,
- warnings: Warnings::default(),
- }
- }
-
- /// Return all warnings about this document.
- pub fn warnings(&self) -> &[Warning] {
- self.warnings.warnings()
- }
-
- fn from_ast<P>(
- basedir: P,
- markdowns: Vec<PathBuf>,
- mut ast: Pandoc,
- style: Style,
- template: Option<&str>,
- ) -> Result<Document, SubplotError>
- where
- P: AsRef<Path> + Debug,
- {
- let meta = Metadata::new(basedir, &ast, template)?;
- let mut linter = LintingVisitor::default();
- trace!("Walking AST for linting...");
- linter.walk_pandoc(&mut ast);
- if !linter.issues.is_empty() {
- // Currently we can't really return more than one error so return one
- return Err(linter.issues.remove(0));
- }
- let files = DataFiles::new(&mut ast);
- let doc = Document::new(markdowns, ast, meta, files, style);
- trace!("Loaded from JSON OK");
- Ok(doc)
+ };
+ trace!("Document::new -> {:#?}", doc);
+ doc
}
- /// Construct a Document from a JSON AST
- pub fn from_json<P>(
- basedir: P,
- markdowns: Vec<PathBuf>,
- json: &str,
- style: Style,
- template: Option<&str>,
- ) -> Result<Document, SubplotError>
- where
- P: AsRef<Path> + Debug,
- {
- trace!("Parsing document...");
- let ast: Pandoc = serde_json::from_str(json).map_err(SubplotError::AstJson)?;
- Self::from_ast(basedir, markdowns, ast, style, template)
+ fn all_files(markdowns: &[Markdown]) -> Result<EmbeddedFiles, SubplotError> {
+ let mut files = EmbeddedFiles::default();
+ for md in markdowns {
+ for file in md.embedded_files()?.files() {
+ files.push(file.clone());
+ }
+ }
+ Ok(files)
}
/// Construct a Document from a named file.
- ///
- /// The file can be in any format Pandoc understands. This runs
- /// Pandoc to parse the file into an AST, so it can be a little
- /// slow.
pub fn from_file(
basedir: &Path,
filename: &Path,
@@ -168,59 +123,218 @@ impl<'a> Document {
basedir.display(),
filename.display()
);
- let markdowns = vec![filename.to_path_buf()];
- let mut pandoc = pandoc::new();
- pandoc.add_input(&filename);
- pandoc.set_input_format(
- pandoc::InputFormat::Markdown,
- vec![pandoc::MarkdownExtension::Citations],
- );
- pandoc.set_output_format(pandoc::OutputFormat::Json, vec![]);
- pandoc.set_output(pandoc::OutputKind::Pipe);
+ let meta = load_metadata_from_yaml_file(filename)?;
+ trace!("metadata from YAML file: {:#?}", meta);
- // Add external Pandoc filters.
- crate::policy::add_citeproc(&mut pandoc);
+ let mut markdowns = vec![];
+ for filename in meta.markdowns() {
+ let filename = basedir.join(filename);
+ markdowns.push(Markdown::load_file(&filename)?);
+ }
+
+ let meta = Metadata::from_yaml_metadata(basedir, &meta, template)?;
+ trace!("metadata from YAML: {:#?}", meta);
+ let files = Self::all_files(&markdowns)?;
+ let doc = Document::new(filename.into(), markdowns, meta, files, style);
- trace!("Invoking Pandoc to parse document {:?}", filename);
- let output = match pandoc.execute().map_err(SubplotError::Pandoc)? {
- pandoc::PandocOutput::ToBuffer(o) => o,
- _ => return Err(SubplotError::NotJson),
- };
- trace!("Pandoc was happy");
- let doc = Document::from_json(basedir, markdowns, &output, style, template)?;
trace!("Loaded document OK");
Ok(doc)
}
- /// Construct a Document from a named file, using the pullmark_cmark crate.
- ///
- /// The file can be in the CommonMark format, with some
- /// extensions. This uses the pulldown-cmark crate to parse the
- /// file into an AST.
- pub fn from_file_with_pullmark(
- basedir: &Path,
- filename: &Path,
- style: Style,
- template: Option<&str>,
- ) -> Result<Document, SubplotError> {
- trace!("Parsing document with pullmark-cmark from {:?}", filename);
- let filename = filename.to_path_buf();
- let markdown = std::fs::read_to_string(&filename)
- .map_err(|err| SubplotError::ReadFile(filename.clone(), err))?;
- let ast = ast::AbstractSyntaxTree::from_str(&markdown)?;
+ /// Return Document as an HTML page serialized into HTML text
+ pub fn to_html(&mut self, date: &str) -> Result<String, SubplotError> {
+ let css_file = resource::read_as_string(CSS, None)
+ .map_err(|e| SubplotError::CssFileNotFound(CSS.into(), e))?;
+
+ let mut head = Element::new(crate::html::ElementTag::Head);
+ let mut title = Element::new(crate::html::ElementTag::Title);
+ title.push_child(crate::html::Content::Text(self.meta().title().into()));
+ head.push_child(crate::html::Content::Elt(title));
+
+ let mut css = Element::new(ElementTag::Style);
+ css.push_child(Content::Text(css_file));
+ for css_file in self.meta.css_embed() {
+ css.push_child(Content::Text(css_file.into()));
+ }
+ head.push_child(Content::Elt(css));
+
+ for css_url in self.meta.css_urls() {
+ let mut link = Element::new(ElementTag::Link);
+ link.push_attribute(Attribute::new("rel", "stylesheet"));
+ link.push_attribute(Attribute::new("type", "text/css"));
+ link.push_attribute(Attribute::new("href", css_url));
+ head.push_child(Content::Elt(link));
+ }
- trace!("Parsed document OK");
- Self::from_ast(basedir, vec![filename], ast.to_pandoc(), style, template)
+ self.meta.set_date(date.into());
+
+ let mut body_content = Element::new(crate::html::ElementTag::Div);
+ body_content.push_attribute(Attribute::new("class", "content"));
+ for md in self.markdowns.iter() {
+ body_content.push_child(Content::Elt(md.root_element().clone()));
+ }
+
+ let mut body = Element::new(crate::html::ElementTag::Div);
+ body.push_child(Content::Elt(self.typeset_meta()));
+ body.push_child(Content::Elt(self.typeset_toc(&body_content)));
+ body.push_child(Content::Elt(body_content));
+
+ let page = HtmlPage::new(head, body);
+ page.serialize().map_err(SubplotError::ParseMarkdown)
}
- /// Return the AST of a Document, serialized as JSON.
- ///
- /// This is useful in a Pandoc filter, so that the filter can give
- /// it back to Pandoc for typesetting.
- pub fn ast(&self) -> Result<String, SubplotError> {
- let json = serde_json::to_string(&self.ast).map_err(SubplotError::AstJson)?;
- Ok(json)
+ fn typeset_toc(&self, body: &Element) -> Element {
+ let mut toc = Element::new(ElementTag::Div);
+ toc.push_attribute(Attribute::new("class", "toc"));
+
+ let mut heading = Element::new(ElementTag::H1);
+ heading.push_child(Content::Text("Table of Contents".into()));
+ toc.push_child(Content::Elt(heading));
+
+ let heading_elements: Vec<&Element> = crate::md::Markdown::visit(body)
+ .iter()
+ .filter(|e| {
+ matches!(
+ e.tag(),
+ ElementTag::H1
+ | ElementTag::H2
+ | ElementTag::H3
+ | ElementTag::H4
+ | ElementTag::H5
+ | ElementTag::H6
+ )
+ })
+ .cloned()
+ .collect();
+
+ let mut headings = vec![];
+ for e in heading_elements {
+ let id = e
+ .attr("id")
+ .expect("heading has id")
+ .value()
+ .expect("id attribute has value");
+ match e.tag() {
+ ElementTag::H1 => headings.push((1, e.content(), id)),
+ ElementTag::H2 => headings.push((2, e.content(), id)),
+ ElementTag::H3 => headings.push((3, e.content(), id)),
+ ElementTag::H4 => headings.push((4, e.content(), id)),
+ ElementTag::H5 => headings.push((5, e.content(), id)),
+ ElementTag::H6 => headings.push((6, e.content(), id)),
+ _ => (),
+ }
+ }
+
+ let mut stack = vec![];
+ let mut numberer = HeadingNumberer::default();
+ for (level, text, id) in headings {
+ assert!(level >= 1);
+ assert!(level <= 6);
+
+ let mut number = Element::new(ElementTag::Span);
+ number.push_attribute(Attribute::new("class", "heading-number"));
+ number.push_child(Content::Text(numberer.number(level)));
+
+ let mut htext = Element::new(ElementTag::Span);
+ htext.push_attribute(Attribute::new("class", "heading-text"));
+ htext.push_child(Content::Text(text));
+
+ let mut a = Element::new(ElementTag::A);
+ a.push_attribute(crate::html::Attribute::new("href", &format!("#{}", id)));
+ a.push_attribute(Attribute::new("class", "toc-link"));
+ a.push_child(Content::Elt(number));
+ a.push_child(Content::Text(" ".into()));
+ a.push_child(Content::Elt(htext));
+
+ let mut li = Element::new(ElementTag::Li);
+ li.push_child(Content::Elt(a));
+
+ match level.cmp(&stack.len()) {
+ Ordering::Equal => (),
+ Ordering::Greater => stack.push(Element::new(ElementTag::Ol)),
+ Ordering::Less => {
+ assert!(!stack.is_empty());
+ let child = stack.pop().unwrap();
+ assert!(child.tag() == ElementTag::Ol);
+ let mut li = Element::new(ElementTag::Li);
+ li.push_child(Content::Elt(child));
+ assert!(!stack.is_empty());
+ let mut parent = stack.pop().unwrap();
+ parent.push_child(Content::Elt(li));
+ stack.push(parent);
+ }
+ }
+
+ assert!(!stack.is_empty());
+ let mut ol = stack.pop().unwrap();
+ ol.push_child(Content::Elt(li));
+ stack.push(ol);
+ }
+
+ while stack.len() > 1 {
+ let child = stack.pop().unwrap();
+ assert!(child.tag() == ElementTag::Ol);
+ let mut li = Element::new(ElementTag::Li);
+ li.push_child(Content::Elt(child));
+
+ let mut parent = stack.pop().unwrap();
+ parent.push_child(Content::Elt(li));
+ stack.push(parent);
+ }
+
+ assert!(stack.len() <= 1);
+ if let Some(ol) = stack.pop() {
+ toc.push_child(Content::Elt(ol));
+ }
+
+ toc
+ }
+
+ fn typeset_meta(&self) -> Element {
+ let mut div = Element::new(ElementTag::Div);
+ div.push_attribute(Attribute::new("class", "meta"));
+
+ div.push_child(Content::Elt(Self::title(self.meta.title())));
+
+ if let Some(authors) = self.meta.authors() {
+ div.push_child(Content::Elt(Self::authors(authors)));
+ }
+
+ if let Some(date) = self.meta.date() {
+ div.push_child(Content::Elt(Self::date(date)));
+ }
+
+ div
+ }
+
+ fn title(title: &str) -> Element {
+ let mut e = Element::new(ElementTag::H1);
+ e.push_attribute(Attribute::new("class", "title"));
+ e.push_child(Content::Text(title.into()));
+ e
+ }
+
+ fn authors(authors: &[String]) -> Element {
+ let mut list = Element::new(ElementTag::P);
+ list.push_attribute(Attribute::new("class", "authors"));
+ list.push_child(Content::Text("By: ".into()));
+ let mut first = true;
+ for a in authors {
+ if !first {
+ list.push_child(Content::Text(", ".into()));
+ }
+ list.push_child(Content::Text(a.into()));
+ first = false;
+ }
+ list
+ }
+
+ fn date(date: &str) -> Element {
+ let mut e = Element::new(ElementTag::P);
+ e.push_attribute(Attribute::new("class", "date"));
+ e.push_child(Content::Text(date.into()));
+ e
}
/// Return the document's metadata.
@@ -228,12 +342,17 @@ impl<'a> Document {
&self.meta
}
+ /// Set document date.
+ pub fn set_date(&mut self, date: String) {
+ self.meta.set_date(date);
+ }
+
/// Return all source filenames for the document.
///
/// The sources are any files that affect the output so that if
/// the source file changes, the output needs to be re-generated.
- pub fn sources(&mut self, template: Option<&str>) -> Vec<PathBuf> {
- let mut names = vec![];
+ pub fn sources(&self, template: Option<&str>) -> Vec<PathBuf> {
+ let mut names = vec![self.subplot.clone()];
for x in self.meta().bindings_filenames() {
names.push(PathBuf::from(x))
@@ -255,25 +374,20 @@ impl<'a> Document {
}
}
- for x in self.meta().bibliographies().iter() {
- names.push(PathBuf::from(x))
- }
-
- for x in self.markdowns.iter() {
- names.push(x.to_path_buf());
+ for name in self.meta().markdown_filenames() {
+ names.push(name.into());
}
- let mut visitor = visitor::ImageVisitor::new();
- visitor.walk_pandoc(&mut self.ast);
- for x in visitor.images().iter() {
- names.push(x.to_path_buf());
+ for md in self.markdowns.iter() {
+ let mut images = md.images();
+ names.append(&mut images);
}
names
}
/// Return list of files embeddedin the document.
- pub fn files(&self) -> &[DataFile] {
+ pub fn embedded_files(&self) -> &[EmbeddedFile] {
self.files.files()
}
@@ -281,16 +395,33 @@ impl<'a> Document {
pub fn lint(&self) -> Result<(), SubplotError> {
trace!("Linting document");
self.check_doc_has_title()?;
+ self.check_scenarios_are_unique()?;
self.check_filenames_are_unique()?;
self.check_block_classes()?;
trace!("No linting problems found");
Ok(())
}
+ // Check that all titles for scenarios are unique.
+ fn check_scenarios_are_unique(&self) -> Result<(), SubplotError> {
+ let mut known = HashSet::new();
+ for title in self.scenarios()?.iter().map(|s| s.title().to_lowercase()) {
+ if known.contains(&title) {
+ return Err(SubplotError::DuplicateScenario(title));
+ }
+ known.insert(title);
+ }
+ Ok(())
+ }
+
// Check that all filenames for embedded files are unique.
fn check_filenames_are_unique(&self) -> Result<(), SubplotError> {
let mut known = HashSet::new();
- for filename in self.files().iter().map(|f| f.filename().to_lowercase()) {
+ for filename in self
+ .embedded_files()
+ .iter()
+ .map(|f| f.filename().to_lowercase())
+ {
if known.contains(&filename) {
return Err(SubplotError::DuplicateEmbeddedFilename(filename));
}
@@ -310,27 +441,20 @@ impl<'a> Document {
/// Check that all the block classes in the document are known
fn check_block_classes(&self) -> Result<(), SubplotError> {
- let mut visitor = visitor::BlockClassVisitor::default();
- // Irritatingly we can't immutably visit the AST for some reason
- // This clone() is expensive and unwanted, but I'm not sure how
- // to get around it for now
- visitor.walk_pandoc(&mut self.ast.clone());
+ let classes_in_doc = self.all_block_classes();
+
// Build the set of known good classes
let mut known_classes: HashSet<String> = HashSet::new();
for class in std::iter::empty()
.chain(SPECIAL_CLASSES.iter().map(Deref::deref))
.chain(KNOWN_FILE_CLASSES.iter().map(Deref::deref))
- .chain(KNOWN_PANDOC_CLASSES.iter().map(Deref::deref))
+ .chain(KNOWN_BLOCK_CLASSES.iter().map(Deref::deref))
.chain(self.meta().classes())
{
known_classes.insert(class.to_string());
}
// Acquire the set of used names which are not known
- let unknown_classes: Vec<_> = visitor
- .classes
- .difference(&known_classes)
- .cloned()
- .collect();
+ let unknown_classes: Vec<_> = classes_in_doc.difference(&known_classes).cloned().collect();
// If the unknown classes list is not empty, we had a problem and
// we will report it to the user.
if !unknown_classes.is_empty() {
@@ -340,11 +464,61 @@ impl<'a> Document {
}
}
+ fn all_block_classes(&self) -> HashSet<String> {
+ let mut set = HashSet::new();
+ for md in self.markdowns.iter() {
+ for class in md.block_classes() {
+ set.insert(class);
+ }
+ }
+ set
+ }
+
+ /// Check bindings for warnings
+ pub fn check_bindings(&self, warnings: &mut Warnings) -> Result<(), SubplotError> {
+ self.meta.bindings().check(warnings)
+ }
+
+ /// Check labelled code blocks have some appropriate class
+ pub fn check_named_code_blocks_have_appropriate_class(
+ &self,
+ warnings: &mut Warnings,
+ ) -> Result<bool, SubplotError> {
+ let mut okay = true;
+ for md in self.markdowns.iter() {
+ for block in md.named_blocks() {
+ if !block.all_attrs().iter().any(|attr| {
+ attr.name() == "class"
+ && ID_OK_CLASSES
+ .iter()
+ .any(|class| attr.value() == Some(class))
+ }) {
+ // For now, named blocks must be files
+ warnings.push(Warning::MissingAppropriateClassOnNamedCodeBlock(
+ block
+ .attr("id")
+ .expect("Named blocks should have IDs")
+ .value()
+ .unwrap_or("(unknown-id)")
+ .to_string(),
+ block.location().to_string(),
+ ));
+ okay = false;
+ }
+ }
+ }
+ Ok(okay)
+ }
+
/// Check that all named files (in matched steps) are actually present in the
/// document.
- pub fn check_named_files_exist(&mut self, template: &str) -> Result<bool, SubplotError> {
+ pub fn check_named_files_exist(
+ &self,
+ template: &str,
+ warnings: &mut Warnings,
+ ) -> Result<bool, SubplotError> {
let filenames: HashSet<_> = self
- .files()
+ .embedded_files()
.iter()
.map(|f| f.filename().to_lowercase())
.collect();
@@ -357,11 +531,16 @@ impl<'a> Document {
for scenario in scenarios {
for step in scenario.steps() {
for captured in step.parts() {
- if let PartialStep::CapturedText { name, text } = captured {
+ if let PartialStep::CapturedText {
+ name,
+ text,
+ kind: _,
+ } = captured
+ {
if matches!(step.types().get(name.as_str()), Some(CaptureType::File))
&& !filenames.contains(&text.to_lowercase())
{
- self.warnings.push(Warning::UnknownEmbeddedFile(
+ warnings.push(Warning::UnknownEmbeddedFile(
scenario.title().to_string(),
text.to_string(),
));
@@ -375,9 +554,13 @@ impl<'a> Document {
}
/// Check that all embedded files are used by matched steps.
- pub fn check_embedded_files_are_used(&mut self, template: &str) -> Result<bool, SubplotError> {
+ pub fn check_embedded_files_are_used(
+ &self,
+ template: &str,
+ warnings: &mut Warnings,
+ ) -> Result<bool, SubplotError> {
let mut filenames: HashSet<_> = self
- .files()
+ .embedded_files()
.iter()
.map(|f| f.filename().to_lowercase())
.collect();
@@ -389,7 +572,12 @@ impl<'a> Document {
for scenario in scenarios {
for step in scenario.steps() {
for captured in step.parts() {
- if let PartialStep::CapturedText { name, text } = captured {
+ if let PartialStep::CapturedText {
+ name,
+ text,
+ kind: _,
+ } = captured
+ {
if matches!(step.types().get(name.as_str()), Some(CaptureType::File)) {
filenames.remove(&text.to_lowercase());
}
@@ -398,8 +586,7 @@ impl<'a> Document {
}
}
for filename in filenames.iter() {
- self.warnings
- .push(Warning::UnusedEmbeddedFile(filename.to_string()));
+ warnings.push(Warning::UnusedEmbeddedFile(filename.to_string()));
}
// We always succeed. Subplot's own subplot had valid cases of
@@ -410,7 +597,7 @@ impl<'a> Document {
}
/// Check that all matched steps actually have function implementations
- pub fn check_matched_steps_have_impl(&mut self, template: &str) -> bool {
+ pub fn check_matched_steps_have_impl(&self, template: &str, warnings: &mut Warnings) -> bool {
trace!("Checking that steps have implementations");
let mut okay = true;
let scenarios = match self.matched_scenarios(template) {
@@ -423,7 +610,7 @@ impl<'a> Document {
for step in scenario.steps() {
if step.function().is_none() {
trace!("Missing step implementation: {:?}", step.text());
- self.warnings.push(Warning::MissingStepImplementation(
+ warnings.push(Warning::MissingStepImplementation(
scenario.title().to_string(),
step.text().to_string(),
));
@@ -435,36 +622,28 @@ impl<'a> Document {
}
/// Typeset a Subplot document.
- pub fn typeset(&mut self) {
- let mut visitor =
- visitor::TypesettingVisitor::new(self.style.clone(), self.meta.bindings());
- visitor.walk_pandoc(&mut self.ast);
- self.warnings.push_all(visitor.warnings());
+ pub fn typeset(
+ &mut self,
+ warnings: &mut Warnings,
+ template: Option<&str>,
+ ) -> Result<(), SubplotError> {
+ for md in self.markdowns.iter_mut() {
+ warnings.push_all(md.typeset(self.style.clone(), template, self.meta.bindings()));
+ }
+ Ok(())
}
/// Return all scenarios in a document.
- pub fn scenarios(&mut self) -> Result<Vec<Scenario>, SubplotError> {
- let mut visitor = visitor::StructureVisitor::new();
- visitor.walk_pandoc(&mut self.ast);
-
- let mut scenarios: Vec<Scenario> = vec![];
-
- let mut i = 0;
- while i < visitor.elements.len() {
- let (maybe, new_i) = extract_scenario(&visitor.elements[i..])?;
- if let Some(scen) = maybe {
- scenarios.push(scen);
- }
- i += new_i;
+ pub fn scenarios(&self) -> Result<Vec<Scenario>, SubplotError> {
+ let mut scenarios = vec![];
+ for md in self.markdowns.iter() {
+ scenarios.append(&mut md.scenarios()?);
}
Ok(scenarios)
}
/// Return matched scenarios in a document.
- pub fn matched_scenarios(
- &mut self,
- template: &str,
- ) -> Result<Vec<MatchedScenario>, SubplotError> {
+ pub fn matched_scenarios(&self, template: &str) -> Result<Vec<MatchedScenario>, SubplotError> {
let scenarios = self.scenarios()?;
trace!(
"Found {} scenarios, checking their bindings",
@@ -490,9 +669,15 @@ impl<'a> Document {
}
}
+fn load_metadata_from_yaml_file(filename: &Path) -> Result<YamlMetadata, SubplotError> {
+ let yaml = read(filename).map_err(|e| SubplotError::ReadFile(filename.into(), e))?;
+ trace!("Parsing YAML metadata from {}", filename.display());
+ let meta: YamlMetadata = serde_yaml::from_slice(&yaml)
+ .map_err(|e| SubplotError::MetadataFile(filename.into(), e))?;
+ Ok(meta)
+}
+
/// Load a `Document` from a file.
-///
-/// This version uses Pandoc to parse the Markdown.
pub fn load_document<P>(
filename: P,
style: Style,
@@ -535,7 +720,7 @@ where
style
);
crate::resource::add_search_path(filename.parent().unwrap());
- let doc = Document::from_file_with_pullmark(&base_path, filename, style, template)?;
+ let doc = Document::from_file(&base_path, filename, style, template)?;
trace!("Loaded doc from file OK");
Ok(doc)
}
@@ -562,9 +747,11 @@ pub fn codegen(
if !doc.meta().templates().any(|t| t == template) {
return Err(SubplotError::TemplateSupportNotPresent);
}
- if !doc.check_named_files_exist(&template)?
- || !doc.check_matched_steps_have_impl(&template)
- || !doc.check_embedded_files_are_used(&template)?
+ let mut warnings = Warnings::default();
+ if !doc.check_named_code_blocks_have_appropriate_class(&mut warnings)?
+ || !doc.check_named_files_exist(&template, &mut warnings)?
+ || !doc.check_matched_steps_have_impl(&template, &mut warnings)
+ || !doc.check_embedded_files_are_used(&template, &mut warnings)?
{
error!("Found problems in document, cannot continue");
std::process::exit(1);
@@ -589,155 +776,56 @@ impl CodegenOutput {
}
}
-fn extract_scenario(e: &[visitor::Element]) -> Result<(Option<Scenario>, usize), SubplotError> {
- if e.is_empty() {
- // If we get here, it's a programming error.
- panic!("didn't expect empty list of elements");
- }
-
- match &e[0] {
- visitor::Element::Snippet(_) => Err(SubplotError::ScenarioBeforeHeading),
- visitor::Element::Heading(title, level) => {
- let mut scen = Scenario::new(title);
- let mut prevkind = None;
- for (i, item) in e.iter().enumerate().skip(1) {
- match item {
- visitor::Element::Heading(_, level2) => {
- let is_subsection = *level2 > *level;
- if is_subsection {
- if scen.has_steps() {
- } else {
- return Ok((None, i));
- }
- } else if scen.has_steps() {
- return Ok((Some(scen), i));
- } else {
- return Ok((None, i));
- }
- }
- visitor::Element::Snippet(text) => {
- for line in parse_scenario_snippet(text) {
- let step = ScenarioStep::new_from_str(line, prevkind)?;
- scen.add(&step);
- prevkind = Some(step.kind());
- }
- }
+#[derive(Debug, Default)]
+struct HeadingNumberer {
+ prev: Vec<usize>,
+}
+
+impl HeadingNumberer {
+ fn number(&mut self, level: usize) -> String {
+ match level.cmp(&self.prev.len()) {
+ Ordering::Equal => {
+ if let Some(n) = self.prev.pop() {
+ self.prev.push(n + 1);
+ } else {
+ self.prev.push(1);
}
}
- if scen.has_steps() {
- Ok((Some(scen), e.len()))
- } else {
- Ok((None, e.len()))
+ Ordering::Greater => {
+ self.prev.push(1);
+ }
+ Ordering::Less => {
+ assert!(!self.prev.is_empty());
+ self.prev.pop();
+ if let Some(n) = self.prev.pop() {
+ self.prev.push(n + 1);
+ } else {
+ self.prev.push(1);
+ }
}
}
- }
-}
-#[cfg(test)]
-mod test_extract {
- use super::extract_scenario;
- use super::visitor::Element;
- use crate::Scenario;
- use crate::SubplotError;
-
- fn h(title: &str, level: i64) -> Element {
- Element::Heading(title.to_string(), level)
- }
-
- fn s(text: &str) -> Element {
- Element::Snippet(text.to_string())
- }
-
- fn check_result(
- r: Result<(Option<Scenario>, usize), SubplotError>,
- title: Option<&str>,
- i: usize,
- ) {
- assert!(r.is_ok());
- let (actual_scen, actual_i) = r.unwrap();
- if title.is_none() {
- assert!(actual_scen.is_none());
- } else {
- assert!(actual_scen.is_some());
- let scen = actual_scen.unwrap();
- assert_eq!(scen.title(), title.unwrap());
+ let mut s = String::new();
+ for i in self.prev.iter() {
+ if !s.is_empty() {
+ s.push('.');
+ }
+ s.push_str(&i.to_string());
}
- assert_eq!(actual_i, i);
- }
-
- #[test]
- fn returns_nothing_if_there_is_no_scenario() {
- let elements: Vec<Element> = vec![h("title", 1)];
- let r = extract_scenario(&elements);
- check_result(r, None, 1);
- }
-
- #[test]
- fn returns_scenario_if_there_is_one() {
- let elements = vec![h("title", 1), s("given something")];
- let r = extract_scenario(&elements);
- check_result(r, Some("title"), 2);
- }
-
- #[test]
- fn skips_scenarioless_section_in_favour_of_same_level() {
- let elements = vec![h("first", 1), h("second", 1), s("given something")];
- let r = extract_scenario(&elements);
- check_result(r, None, 1);
- let r = extract_scenario(&elements[1..]);
- check_result(r, Some("second"), 2);
+ s
}
+}
- #[test]
- fn returns_parent_section_with_scenario_snippet() {
- let elements = vec![
- h("1", 1),
- s("given something"),
- h("1.1", 2),
- s("when something"),
- h("2", 1),
- ];
- let r = extract_scenario(&elements);
- check_result(r, Some("1"), 4);
- let r = extract_scenario(&elements[4..]);
- check_result(r, None, 1);
- }
-
- #[test]
- fn skips_scenarioless_parent_heading() {
- let elements = vec![h("1", 1), h("1.1", 2), s("given something"), h("2", 1)];
-
- let r = extract_scenario(&elements);
- check_result(r, None, 1);
-
- let r = extract_scenario(&elements[1..]);
- check_result(r, Some("1.1"), 2);
-
- let r = extract_scenario(&elements[3..]);
- check_result(r, None, 1);
- }
-
- #[test]
- fn skips_scenarioless_deeper_headings() {
- let elements = vec![h("1", 1), h("1.1", 2), h("2", 1), s("given something")];
-
- let r = extract_scenario(&elements);
- check_result(r, None, 1);
-
- let r = extract_scenario(&elements[1..]);
- check_result(r, None, 1);
-
- let r = extract_scenario(&elements[2..]);
- check_result(r, Some("2"), 2);
- }
+#[cfg(test)]
+mod test_numberer {
+ use super::HeadingNumberer;
#[test]
- fn returns_error_if_scenario_has_no_title() {
- let elements = vec![s("given something")];
- let r = extract_scenario(&elements);
- match r {
- Err(SubplotError::ScenarioBeforeHeading) => (),
- _ => panic!("unexpected result {:?}", r),
- }
+ fn numbering() {
+ let mut n = HeadingNumberer::default();
+ assert_eq!(n.number(1), "1");
+ assert_eq!(n.number(2), "1.1");
+ assert_eq!(n.number(1), "2");
+ assert_eq!(n.number(2), "2.1");
}
}
diff --git a/src/datafiles.rs b/src/embedded.rs
index 83e90d9..e71fa54 100644
--- a/src/datafiles.rs
+++ b/src/embedded.rs
@@ -1,17 +1,16 @@
-use pandoc_ast::{MutVisitor, Pandoc};
use serde::{Deserialize, Serialize};
/// A data file embedded in the document.
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
-pub struct DataFile {
+pub struct EmbeddedFile {
filename: String,
contents: String,
}
-impl DataFile {
+impl EmbeddedFile {
/// Create a new data file, with a name and contents.
- pub fn new(filename: String, contents: String) -> DataFile {
- DataFile { filename, contents }
+ pub fn new(filename: String, contents: String) -> EmbeddedFile {
+ EmbeddedFile { filename, contents }
}
/// Return name of embedded file.
@@ -26,26 +25,19 @@ impl DataFile {
}
/// A collection of data files embedded in document.
-#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
-pub struct DataFiles {
- files: Vec<DataFile>,
+#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize)]
+pub struct EmbeddedFiles {
+ files: Vec<EmbeddedFile>,
}
-impl DataFiles {
- /// Create new set of data files.
- pub fn new(ast: &mut Pandoc) -> DataFiles {
- let mut files = DataFiles { files: vec![] };
- files.walk_pandoc(ast);
- files
- }
-
+impl EmbeddedFiles {
/// Return slice of all data files.
- pub fn files(&self) -> &[DataFile] {
+ pub fn files(&self) -> &[EmbeddedFile] {
&self.files
}
/// Append a new data file.
- pub fn push(&mut self, file: DataFile) {
+ pub fn push(&mut self, file: EmbeddedFile) {
self.files.push(file);
}
}
diff --git a/src/error.rs b/src/error.rs
index a42298d..b89e94b 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,4 +1,6 @@
+use crate::html::{HtmlError, Location};
use crate::matches::MatchedSteps;
+use crate::md::MdError;
use std::path::PathBuf;
use std::process::Output;
@@ -8,17 +10,25 @@ use thiserror::Error;
/// Define all the kinds of errors any part of this crate can return.
#[derive(Debug, Error)]
pub enum SubplotError {
+ /// Scenario step does not start at the beginning of the line.
+ #[error("Scenario step is indented: {0}")]
+ NotAtBoln(String),
+
/// Document has non-fatal errors.
#[error("Document has {0} warnings.")]
Warnings(usize),
+ /// Subplot could not find its CSS file.
+ #[error("failed to find CSS file: {0}")]
+ CssFileNotFound(PathBuf, #[source] std::io::Error),
+
/// Subplot could not find a file named as a bindings file.
- #[error("binding file could not be found: {0}: {1}")]
- BindingsFileNotFound(PathBuf, std::io::Error),
+ #[error("binding file could not be found: {0}")]
+ BindingsFileNotFound(PathBuf, #[source] std::io::Error),
/// Subplot could not find a file named as a functions file.
- #[error("functions file could not be found: {0}: {1}")]
- FunctionsFileNotFound(PathBuf, std::io::Error),
+ #[error("functions file could not be found: {0}")]
+ FunctionsFileNotFound(PathBuf, #[source] std::io::Error),
/// The simple pattern specifies a kind that is unknown.
#[error("simple pattern kind {0} is unknown")]
@@ -44,6 +54,13 @@ pub enum SubplotError {
#[error("binding file failed to parse: {0}")]
BindingFileParseError(PathBuf, #[source] Box<SubplotError>),
+ /// Binding lacks documentation.
+ ///
+ /// Add a `doc` field to the binding with text the documents the
+ /// binding.
+ #[error("binding lacks documentation: {0}: {1} {2}")]
+ NoBindingDoc(PathBuf, crate::StepKind, String),
+
/// Scenario step does not match a known binding
///
/// This may be due to the binding missing entirely, or that the
@@ -138,18 +155,6 @@ pub enum SubplotError {
#[error("document lacks specified template support")]
TemplateSupportNotPresent,
- /// Pandoc AST is not JSON
- ///
- /// Subplot acts as a Pandoc filter, and as part of that Pandoc
- /// constructs an _abstract syntax tree_ from the input document,
- /// and feeds it to the filter as JSON. However, when Subplot was
- /// parsing the AST, it wasn't JSON.
- ///
- /// This probably means there's something wrong with Pandoc, it's
- /// Rust bindings, or Subplot.
- #[error("Pandoc produce AST not in JSON")]
- NotJson,
-
/// First scenario is before first heading
///
/// Subplot scenarios are group by the input document's structure.
@@ -158,8 +163,8 @@ pub enum SubplotError {
/// scenario block before the first heading in the document.
///
/// To fix, add a heading or move the scenario after a heading.
- #[error("first scenario is before first heading")]
- ScenarioBeforeHeading,
+ #[error("{0}: first scenario is before first heading")]
+ ScenarioBeforeHeading(Location),
/// Step does not have a keyword.
#[error("step has no keyword: {0}")]
@@ -183,6 +188,13 @@ pub enum SubplotError {
#[error("continuation keyword used too early")]
ContinuationTooEarly,
+ /// Scenario has the same title as another scenario
+ ///
+ /// Titles of scenarios must be unique in the input document,
+ /// but Subplot found at least one with the same title as another.
+ #[error("Scenario title is duplicate: {0:?}")]
+ DuplicateScenario(String),
+
/// Embedded file has the same name as another embedded file
///
/// Names of embedded files must be unique in the input document,
@@ -201,7 +213,7 @@ pub enum SubplotError {
///
/// The `add-newline` attribute can only take the values `auto`, `yes`,
/// and `no`.
- #[error("Embedded file {0} has unrecognised `add-newline={}` - valid values are auto/yes/no")]
+ #[error("Embedded file {0} has unrecognised `add-newline={1}` - valid values are auto/yes/no")]
UnrecognisedAddNewline(String, String),
/// Couldn't determine base directory from input file name.
@@ -233,8 +245,8 @@ pub enum SubplotError {
NoTemplateSpecDirectory(PathBuf),
/// A code template has an error.
- #[error("Couldn't load template {0}: {1}")]
- TemplateError(String, tera::Error),
+ #[error("Couldn't load template {0}")]
+ TemplateError(String, #[source] tera::Error),
/// Unknown classes in use in document
#[error("Unknown classes found in the document: {0}")]
@@ -283,9 +295,9 @@ pub enum SubplotError {
#[error("Error when writing to {0}")]
WriteFile(PathBuf, #[source] std::io::Error),
- /// Error executing Pandoc.
- #[error("Pandoc failed")]
- Pandoc(#[source] pandoc::PandocError),
+ /// Error parsing markdown into HTML.
+ #[error(transparent)]
+ ParseMarkdown(#[from] HtmlError),
/// Regular expression error
///
@@ -294,21 +306,41 @@ pub enum SubplotError {
#[error("Failed to compile regular expression: {0:?}")]
Regex(String, #[source] regex::Error),
- /// Error parsing the Pandoc abstract syntax tree as JSON.
- #[error("Failed to parse document AST as JSON")]
- AstJson(#[source] serde_json::Error),
-
/// Error parsing YAML metadata for document.
#[error("Failed to parse YAML metadata")]
Metadata(#[source] serde_yaml::Error),
- /// Abstract syntax tree error.
- #[error(transparent)]
- Ast(#[from] crate::ast::Error),
+ /// Error parsing YAML metadata for document, from external file.
+ #[error("Failed to parse YAML metadata in {0}")]
+ MetadataFile(PathBuf, #[source] serde_yaml::Error),
+
+ /// UTF8 conversion error.
+ #[error("failed to parse UTF8 in file {0}")]
+ FileUtf8(PathBuf, #[source] std::string::FromUtf8Error),
/// UTF8 conversion error.
#[error(transparent)]
Utf8Error(#[from] std::str::Utf8Error),
+
+ /// Markdown errors.
+ #[error(transparent)]
+ MdError(#[from] MdError),
+
+ /// String formatting failed.
+ #[error("Failed in string formattiing: {0}")]
+ StringFormat(std::fmt::Error),
+
+ /// Input file could not be read.
+ #[error("Failed to read input file {0}")]
+ InputFileUnreadable(PathBuf, #[source] std::io::Error),
+
+ /// Input file mtime lookup.
+ #[error("Failed to get modification time of {0}")]
+ InputFileMtime(PathBuf, #[source] std::io::Error),
+
+ /// Error typesetting a roadmap diagram.
+ #[error(transparent)]
+ Roadmap(#[from] roadmap::RoadmapError),
}
impl SubplotError {
@@ -358,6 +390,18 @@ pub enum Warning {
/// Plantuml failed during typesetting.
#[error("Markup using plantuml failed: {0}")]
Plantuml(String),
+
+ /// A code block has an identifier but is not marked as a file or example
+ #[error("Code block has identifier but lacks file or example class. Is this a mistake? #{0} at {1}")]
+ MissingAppropriateClassOnNamedCodeBlock(String, String),
+
+ /// A capture in a binding is missing a name
+ #[error("{0}: {1} - missing a name for the {2} capture")]
+ MissingCaptureName(PathBuf, String, String),
+
+ /// A capture in a binding is missing a type
+ #[error("{0}: {1} - missing a type for the capture called {2}")]
+ MissingCaptureType(PathBuf, String, String),
}
/// A list of warnings.
@@ -384,4 +428,14 @@ impl Warnings {
pub fn warnings(&self) -> &[Warning] {
&self.warnings
}
+
+ /// Is the underlying warning set empty?
+ pub fn is_empty(&self) -> bool {
+ self.warnings.is_empty()
+ }
+
+ /// The number of warninings
+ pub fn len(&self) -> usize {
+ self.warnings.len()
+ }
}
diff --git a/src/html.rs b/src/html.rs
new file mode 100644
index 0000000..b76276b
--- /dev/null
+++ b/src/html.rs
@@ -0,0 +1,918 @@
+//! A representation of HTML using Rust types.
+
+#![deny(missing_docs)]
+
+use html_escape::{encode_double_quoted_attribute, encode_text};
+use line_col::LineColLookup;
+use log::{debug, trace};
+use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
+use serde::{Deserialize, Serialize};
+use std::collections::{HashMap, HashSet};
+use std::fmt::Write as _;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+
+const DOCTYPE: &str = "<!DOCTYPE html>";
+
+/// A HTML page, consisting of a head and a body.
+#[derive(Debug)]
+pub struct HtmlPage {
+ head: Element,
+ body: Element,
+}
+
+impl Default for HtmlPage {
+ fn default() -> Self {
+ Self {
+ head: Element::new(ElementTag::Head),
+ body: Element::new(ElementTag::Body),
+ }
+ }
+}
+
+impl HtmlPage {
+ /// Create a new HTML page from a head and a body element.
+ pub fn new(head: Element, body: Element) -> Self {
+ Self { head, body }
+ }
+
+ /// Return the page's head element.
+ pub fn head(&self) -> &Element {
+ &self.head
+ }
+
+ /// Return the page's body element.
+ pub fn body(&self) -> &Element {
+ &self.body
+ }
+
+ /// Try to serialize an HTML page into HTML text.
+ pub fn serialize(&self) -> Result<String, HtmlError> {
+ let mut html = Element::new(ElementTag::Html);
+ html.push_child(Content::Elt(self.head.clone()));
+ let mut body = Element::new(ElementTag::Body);
+ body.push_child(Content::Elt(self.body.clone()));
+ html.push_child(Content::Elt(body));
+ let html = html.serialize()?;
+ Ok(format!("{}\n{}", DOCTYPE, html))
+ }
+
+ /// Try to write an HTML page as text into a file.
+ pub fn write(&self, filename: &Path) -> Result<(), HtmlError> {
+ if let Some(parent) = filename.parent() {
+ trace!("parent: {}", parent.display());
+ if !parent.exists() {
+ debug!("creating directory {}", parent.display());
+ std::fs::create_dir_all(parent)
+ .map_err(|e| HtmlError::CreateDir(parent.into(), e))?;
+ }
+ }
+
+ trace!("writing HTML: {}", filename.display());
+ let mut f = std::fs::File::create(filename)
+ .map_err(|e| HtmlError::CreateFile(filename.into(), e))?;
+ let html = self.serialize()?;
+ f.write_all(html.as_bytes())
+ .map_err(|e| HtmlError::FileWrite(filename.into(), e))?;
+ Ok(())
+ }
+}
+
+/// Parse Markdown text into an HTML element.
+pub fn parse(filename: &Path, markdown: &str) -> Result<Element, HtmlError> {
+ let mut options = Options::empty();
+ options.insert(Options::ENABLE_HEADING_ATTRIBUTES);
+ options.insert(Options::ENABLE_STRIKETHROUGH);
+ options.insert(Options::ENABLE_TABLES);
+ options.insert(Options::ENABLE_TASKLISTS);
+ let p = Parser::new_ext(markdown, options).into_offset_iter();
+ let linecol = LineColLookup::new(markdown);
+ let mut stack = Stack::new();
+ stack.push(Element::new(ElementTag::Div));
+ let mut slugs = Slugs::default();
+ for (event, loc) in p {
+ trace!("event {:?}", event);
+ let (line, col) = linecol.get(loc.start);
+ let loc = Location::new(filename, line, col);
+ match event {
+ Event::Start(tag) => match tag {
+ Tag::Paragraph => stack.push_tag(ElementTag::P, loc),
+ Tag::Heading(level, id, classes) => {
+ let tag = match level {
+ HeadingLevel::H1 => ElementTag::H1,
+ HeadingLevel::H2 => ElementTag::H2,
+ HeadingLevel::H3 => ElementTag::H3,
+ HeadingLevel::H4 => ElementTag::H4,
+ HeadingLevel::H5 => ElementTag::H5,
+ HeadingLevel::H6 => ElementTag::H6,
+ };
+ let mut h = Element::new(tag).with_location(loc);
+ if let Some(id) = id {
+ h.push_attribute(Attribute::new("id", id));
+ slugs.remember(id);
+ }
+ if !classes.is_empty() {
+ let mut names = String::new();
+ for c in classes {
+ if !names.is_empty() {
+ names.push(' ');
+ }
+ names.push_str(c);
+ }
+ h.push_attribute(Attribute::new("class", &names));
+ }
+ stack.push(h);
+ }
+ Tag::BlockQuote => stack.push_tag(ElementTag::Blockquote, loc),
+ Tag::CodeBlock(kind) => {
+ stack.push_tag(ElementTag::Pre, loc);
+ if let CodeBlockKind::Fenced(attrs) = kind {
+ let mut e = stack.pop();
+ e.set_block_attributes(BlockAttr::parse(&attrs));
+ stack.push(e);
+ }
+ }
+ Tag::List(None) => stack.push_tag(ElementTag::Ul, loc),
+ Tag::List(Some(start)) => {
+ let mut e = Element::new(ElementTag::Ol).with_location(loc);
+ e.push_attribute(Attribute::new("start", &format!("{}", start)));
+ stack.push(e);
+ }
+ Tag::Item => stack.push_tag(ElementTag::Li, loc),
+ Tag::FootnoteDefinition(_) => unreachable!("{:?}", tag),
+ Tag::Table(_) => stack.push_tag(ElementTag::Table, loc),
+ Tag::TableHead => stack.push_tag(ElementTag::Th, loc),
+ Tag::TableRow => stack.push_tag(ElementTag::Tr, loc),
+ Tag::TableCell => stack.push_tag(ElementTag::Td, loc),
+ Tag::Emphasis => stack.push_tag(ElementTag::Em, loc),
+ Tag::Strong => stack.push_tag(ElementTag::Strong, loc),
+ Tag::Strikethrough => stack.push_tag(ElementTag::Del, loc),
+ Tag::Link(_, url, title) => {
+ let mut link = Element::new(ElementTag::A);
+ link.push_attribute(Attribute::new("href", url.as_ref()));
+ if !title.is_empty() {
+ link.push_attribute(Attribute::new("title", title.as_ref()));
+ }
+ stack.push(link);
+ }
+ Tag::Image(_, url, title) => {
+ let mut e = Element::new(ElementTag::Img);
+ e.push_attribute(Attribute::new("src", url.as_ref()));
+ e.push_attribute(Attribute::new("alt", title.as_ref()));
+ if !title.is_empty() {
+ e.push_attribute(Attribute::new("title", title.as_ref()));
+ }
+ stack.push(e);
+ }
+ },
+ Event::End(tag) => match &tag {
+ Tag::Paragraph => {
+ trace!("at end of paragraph, looking for definition list use");
+ let e = stack.pop();
+ let s = as_plain_text(e.children());
+ trace!("paragraph text: {:?}", s);
+ if s.starts_with(": ") || s.contains("\n: ") {
+ return Err(HtmlError::DefinitionList(loc));
+ }
+ stack.append_child(Content::Elt(e));
+ }
+ Tag::Heading(_, _, _) => {
+ let mut e = stack.pop();
+ if e.attr("id").is_none() {
+ let slug = slugs.unique(&e.heading_slug());
+ let id = Attribute::new("id", &slug);
+ e.push_attribute(id);
+ }
+ stack.append_child(Content::Elt(e));
+ }
+ Tag::List(_)
+ | Tag::Item
+ | Tag::Link(_, _, _)
+ | Tag::Image(_, _, _)
+ | Tag::Emphasis
+ | Tag::Table(_)
+ | Tag::TableHead
+ | Tag::TableRow
+ | Tag::TableCell
+ | Tag::Strong
+ | Tag::Strikethrough
+ | Tag::BlockQuote
+ | Tag::CodeBlock(_) => {
+ let e = stack.pop();
+ stack.append_child(Content::Elt(e));
+ }
+ Tag::FootnoteDefinition(_) => unreachable!("{:?}", tag),
+ },
+ Event::Text(s) => stack.append_str(s.as_ref()),
+ Event::Code(s) => {
+ let mut code = Element::new(ElementTag::Code);
+ code.push_child(Content::Text(s.to_string()));
+ stack.append_element(code);
+ }
+ Event::Html(s) => stack.append_child(Content::Html(s.to_string())),
+ Event::FootnoteReference(s) => trace!("footnote ref {:?}", s),
+ Event::SoftBreak => stack.append_str("\n"),
+ Event::HardBreak => stack.append_element(Element::new(ElementTag::Br)),
+ Event::Rule => stack.append_element(Element::new(ElementTag::Hr)),
+ Event::TaskListMarker(done) => {
+ let marker = if done {
+ "\u{2612} " // Unicode for box with X
+ } else {
+ "\u{2610} " // Unicode for empty box
+ };
+ stack.append_str(marker);
+ }
+ }
+ }
+
+ let mut body = stack.pop();
+ assert!(stack.is_empty());
+ body.fix_up_img_alt();
+ Ok(body)
+}
+
+fn as_plain_text(content: &[Content]) -> String {
+ let mut buf = String::new();
+ for c in content {
+ if let Content::Text(s) = c {
+ buf.push_str(s);
+ }
+ }
+ buf
+}
+
+/// An HTML element.
+#[derive(Debug, Clone)]
+pub struct Element {
+ loc: Option<Location>,
+ tag: ElementTag,
+ attrs: Vec<Attribute>,
+ children: Vec<Content>,
+}
+
+impl Element {
+ /// Create a new element.
+ pub fn new(tag: ElementTag) -> Self {
+ Self {
+ loc: None,
+ tag,
+ attrs: vec![],
+ children: vec![],
+ }
+ }
+
+ fn with_location(mut self, loc: Location) -> Self {
+ self.loc = Some(loc);
+ self
+ }
+
+ /// Set location.
+ pub fn set_location(&mut self, loc: Location) {
+ self.loc = Some(loc);
+ }
+
+ /// Get location.
+ pub fn location(&self) -> Location {
+ if let Some(loc) = &self.loc {
+ loc.clone()
+ } else {
+ Location::unknown()
+ }
+ }
+
+ fn set_block_attributes(&mut self, block_attrs: Vec<BlockAttr>) {
+ for block_attr in block_attrs {
+ let attr = Attribute::from(block_attr);
+ self.attrs.push(attr);
+ }
+ }
+
+ /// Add a new attribute.
+ pub fn push_attribute(&mut self, attr: Attribute) {
+ self.attrs.push(attr);
+ }
+
+ /// Drop all attributes with a given name.
+ pub fn drop_attributes(&mut self, unwanted: &[&str]) {
+ for uw in unwanted {
+ self.attrs.retain(|a| a.name() != *uw);
+ }
+ }
+
+ /// Append a new child to the element.
+ pub fn push_child(&mut self, child: Content) {
+ self.children.push(child);
+ }
+
+ /// Return an element's tag.
+ pub fn tag(&self) -> ElementTag {
+ self.tag
+ }
+
+ /// All attributes.
+ pub fn all_attrs(&self) -> &[Attribute] {
+ &self.attrs
+ }
+
+ /// Return value of a named attribute, if any.
+ pub fn attr(&self, name: &str) -> Option<&Attribute> {
+ self.attrs.iter().find(|a| a.name() == name)
+ }
+
+ /// Has an attribute with a specific value?
+ pub fn has_attr(&self, name: &str, wanted: &str) -> bool {
+ self.attrs
+ .iter()
+ .filter(|a| a.name() == name && a.value() == Some(wanted))
+ .count()
+ > 0
+ }
+
+ fn heading_slug(&self) -> String {
+ const SAFE: &str = "abcdefghijklmnopqrstuvwxyz";
+ let mut slug = String::new();
+ for s in self.content().to_lowercase().split_whitespace() {
+ for c in s.chars() {
+ if SAFE.contains(c) {
+ slug.push(c);
+ }
+ }
+ }
+ slug
+ }
+
+ /// Return the concatenated text content of direct children,
+ /// ignoring any elements.
+ pub fn content(&self) -> String {
+ let mut buf = String::new();
+ for child in self.children() {
+ buf.push_str(&child.content());
+ }
+ buf
+ }
+
+ /// Return all the children of an element.
+ pub fn children(&self) -> &[Content] {
+ &self.children
+ }
+
+ fn fix_up_img_alt(&mut self) {
+ if self.tag == ElementTag::Img {
+ if !self.attrs.iter().any(|a| a.name() == "alt") {
+ let alt = as_plain_text(self.children());
+ self.push_attribute(Attribute::new("alt", &alt));
+ self.children.clear();
+ }
+ } else {
+ for child in self.children.iter_mut() {
+ if let Content::Elt(kid) = child {
+ kid.fix_up_img_alt();
+ }
+ }
+ }
+ }
+
+ /// Serialize an element into HTML text.
+ pub fn serialize(&self) -> Result<String, HtmlError> {
+ let mut buf = String::new();
+ self.serialize_to_buf_without_added_newlines(&mut buf)
+ .map_err(HtmlError::Format)?;
+ Ok(buf)
+ }
+
+ fn serialize_to_buf_without_added_newlines(
+ &self,
+ buf: &mut String,
+ ) -> Result<(), std::fmt::Error> {
+ if self.children.is_empty() {
+ write!(buf, "<{}", self.tag.name())?;
+ self.serialize_attrs_to_buf(buf)?;
+ write!(buf, "/>")?;
+ } else {
+ write!(buf, "<{}", self.tag.name())?;
+ self.serialize_attrs_to_buf(buf)?;
+ write!(buf, ">")?;
+ for c in self.children() {
+ match c {
+ Content::Text(s) => buf.push_str(&encode_text(s)),
+ Content::Elt(e) => e.serialize_to_buf_adding_block_newline(buf)?,
+ Content::Html(s) => buf.push_str(s),
+ }
+ }
+ write!(buf, "</{}>", self.tag.name())?;
+ }
+ Ok(())
+ }
+
+ fn serialize_to_buf_adding_block_newline(
+ &self,
+ buf: &mut String,
+ ) -> Result<(), std::fmt::Error> {
+ if self.tag.is_block() {
+ writeln!(buf)?;
+ }
+ self.serialize_to_buf_without_added_newlines(buf)
+ }
+
+ fn serialize_attrs_to_buf(&self, buf: &mut String) -> Result<(), std::fmt::Error> {
+ let mut attrs = Attributes::default();
+ for attr in self.attrs.iter() {
+ attrs.push(attr);
+ }
+
+ for (name, value) in attrs.iter() {
+ write!(buf, " {}", name)?;
+ if !value.is_empty() {
+ write!(buf, "=\"{}\"", encode_double_quoted_attribute(value))?;
+ }
+ }
+ Ok(())
+ }
+}
+
+/// The tag of an HTML element.
+#[derive(Copy, Clone, Debug, Eq, PartialEq)]
+#[allow(missing_docs)]
+pub enum ElementTag {
+ Html,
+ Head,
+ Meta,
+ Body,
+ Div,
+ H1,
+ H2,
+ H3,
+ H4,
+ H5,
+ H6,
+ P,
+ Ol,
+ Ul,
+ Li,
+ Link,
+ Blockquote,
+ Pre,
+ Em,
+ Strong,
+ Del,
+ A,
+ Img,
+ Table,
+ Title,
+ Th,
+ Tr,
+ Td,
+ Br,
+ Hr,
+ Code,
+ Span,
+ Style,
+}
+
+impl ElementTag {
+ /// Name of the tag.
+ pub fn name(&self) -> &str {
+ match self {
+ Self::Html => "html",
+ Self::Head => "head",
+ Self::Meta => "meta",
+ Self::Body => "body",
+ Self::Div => "div",
+ Self::H1 => "h1",
+ Self::H2 => "h2",
+ Self::H3 => "h3",
+ Self::H4 => "h4",
+ Self::H5 => "h5",
+ Self::H6 => "h6",
+ Self::P => "p",
+ Self::Ol => "ol",
+ Self::Ul => "ul",
+ Self::Li => "li",
+ Self::Link => "link",
+ Self::Blockquote => "blockquote",
+ Self::Pre => "pre",
+ Self::Em => "em",
+ Self::Strong => "strong",
+ Self::Del => "del",
+ Self::A => "a",
+ Self::Img => "img",
+ Self::Table => "table",
+ Self::Th => "th",
+ Self::Title => "title",
+ Self::Tr => "tr",
+ Self::Td => "td",
+ Self::Br => "br",
+ Self::Hr => "hr",
+ Self::Code => "code",
+ Self::Span => "span",
+ Self::Style => "style",
+ }
+ }
+
+ fn is_block(&self) -> bool {
+ matches!(
+ self,
+ Self::Html
+ | Self::Head
+ | Self::Meta
+ | Self::Body
+ | Self::Div
+ | Self::H1
+ | Self::H2
+ | Self::H3
+ | Self::H4
+ | Self::H5
+ | Self::H6
+ | Self::P
+ | Self::Ol
+ | Self::Ul
+ | Self::Li
+ | Self::Blockquote
+ | Self::Table
+ | Self::Th
+ | Self::Tr
+ | Self::Br
+ | Self::Hr
+ )
+ }
+}
+
+#[derive(Debug, Default, Clone)]
+struct Attributes {
+ attrs: HashMap<String, String>,
+}
+
+impl Attributes {
+ fn push(&mut self, attr: &Attribute) {
+ if let Some(new_value) = attr.value() {
+ if let Some(old_value) = self.attrs.get_mut(attr.name()) {
+ assert!(!old_value.is_empty());
+ old_value.push(' ');
+ old_value.push_str(new_value);
+ } else {
+ self.attrs.insert(attr.name().into(), new_value.into());
+ }
+ } else {
+ assert!(!self.attrs.contains_key(attr.name()));
+ self.attrs.insert(attr.name().into(), "".into());
+ }
+ }
+
+ fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
+ self.attrs.iter()
+ }
+}
+
+/// An attribute of an HTML element.
+#[derive(Clone, Debug)]
+pub struct Attribute {
+ name: String,
+ value: Option<String>,
+}
+
+impl Attribute {
+ /// Create a new element attribute.
+ pub fn new(name: &str, value: &str) -> Self {
+ Self {
+ name: name.into(),
+ value: Some(value.into()),
+ }
+ }
+
+ /// Return the name of the attribute.
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ /// Return the value of the attribute, if any.
+ pub fn value(&self) -> Option<&str> {
+ self.value.as_deref()
+ }
+}
+
+impl From<BlockAttr> for Attribute {
+ fn from(block_attr: BlockAttr) -> Self {
+ match block_attr {
+ BlockAttr::Id(v) => Self::new("id", &v),
+ BlockAttr::Class(v) => Self::new("class", &v),
+ BlockAttr::KeyValue(k, v) => Self::new(&k, &v),
+ }
+ }
+}
+
+/// Content in HTML.
+#[derive(Clone, Debug)]
+pub enum Content {
+ /// Arbitrary text.
+ Text(String),
+
+ /// An HTML element.
+ Elt(Element),
+
+ /// Arbitrary HTML text.
+ Html(String),
+}
+
+impl Content {
+ fn content(&self) -> String {
+ match self {
+ Self::Text(s) => s.clone(),
+ Self::Elt(e) => e.content(),
+ Self::Html(h) => h.clone(),
+ }
+ }
+}
+
+/// Location of element in source file.
+#[derive(Debug, Clone, Eq, Serialize, Deserialize, PartialEq)]
+#[serde(untagged)]
+pub enum Location {
+ /// A known location.
+ Known {
+ /// Name of file.
+ filename: PathBuf,
+ /// Line in file.
+ line: usize,
+ /// Column in line.
+ col: usize,
+ },
+ /// An unknown location.
+ Unknown,
+}
+
+impl Location {
+ /// Create a new location.
+ pub fn new(filename: &Path, line: usize, col: usize) -> Self {
+ Self::Known {
+ filename: filename.into(),
+ line,
+ col,
+ }
+ }
+
+ /// Create an unknown location.
+ pub fn unknown() -> Self {
+ Self::Unknown
+ }
+
+ /// Report name of source file from where this element comes from.
+ pub fn filename(&self) -> &Path {
+ if let Self::Known {
+ filename,
+ line: _,
+ col: _,
+ } = self
+ {
+ filename
+ } else {
+ Path::new("")
+ }
+ }
+
+ /// Report row and column in source where this element comes from.
+ pub fn rowcol(&self) -> (usize, usize) {
+ if let Self::Known {
+ filename: _,
+ line,
+ col,
+ } = self
+ {
+ (*line, *col)
+ } else {
+ (0, 0)
+ }
+ }
+}
+
+impl std::fmt::Display for Location {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ if let Self::Known {
+ filename,
+ line,
+ col,
+ } = self
+ {
+ write!(f, "{}:{}:{}", filename.display(), line, col)
+ } else {
+ write!(f, "(unknown location)")
+ }
+ }
+}
+
+struct Stack {
+ stack: Vec<Element>,
+}
+
+impl Stack {
+ fn new() -> Self {
+ Self { stack: vec![] }
+ }
+
+ fn is_empty(&self) -> bool {
+ self.stack.is_empty()
+ }
+
+ fn push(&mut self, e: Element) {
+ trace!("pushed {:?}", e);
+ self.stack.push(e);
+ }
+
+ fn push_tag(&mut self, tag: ElementTag, loc: Location) {
+ self.push(Element::new(tag).with_location(loc));
+ }
+
+ fn pop(&mut self) -> Element {
+ let e = self.stack.pop().unwrap();
+ trace!("popped {:?}", e);
+ e
+ }
+
+ fn append_child(&mut self, child: Content) {
+ trace!("appended {:?}", child);
+ let mut parent = self.stack.pop().unwrap();
+ parent.push_child(child);
+ self.stack.push(parent);
+ }
+
+ fn append_str(&mut self, text: &str) {
+ self.append_child(Content::Text(text.into()));
+ }
+
+ fn append_element(&mut self, e: Element) {
+ self.append_child(Content::Elt(e));
+ }
+}
+
+/// Errors from the `html` module.
+#[derive(Debug, thiserror::Error)]
+pub enum HtmlError {
+ /// Failed to create a directory.
+ #[error("failed to create directory {0}")]
+ CreateDir(PathBuf, #[source] std::io::Error),
+
+ /// Failed to create a file.
+ #[error("failed to create file {0}")]
+ CreateFile(PathBuf, #[source] std::io::Error),
+
+ /// Failed to write to a file.
+ #[error("failed to write to file {0}")]
+ FileWrite(PathBuf, #[source] std::io::Error),
+
+ /// Input contains an attempt to use a definition list in
+ /// Markdown.
+ #[error("{0}: attempt to use definition lists in Markdown")]
+ DefinitionList(Location),
+
+ /// String formatting error. This is likely a programming error.
+ #[error("string formatting error: {0}")]
+ Format(#[source] std::fmt::Error),
+}
+
+/// Code block attribute.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum BlockAttr {
+ /// An identifier.
+ Id(String),
+ /// A class.
+ Class(String),
+ /// A key/value pair.
+ KeyValue(String, String),
+}
+
+impl BlockAttr {
+ fn id(s: &str) -> Self {
+ Self::Id(s.into())
+ }
+
+ fn class(s: &str) -> Self {
+ Self::Class(s.into())
+ }
+
+ fn key_value(k: &str, v: &str) -> Self {
+ Self::KeyValue(k.into(), v.into())
+ }
+
+ /// Parse a fenced code block tag.
+ pub fn parse(attrs: &str) -> Vec<Self> {
+ let mut result = vec![];
+ for word in Self::parse_words(attrs) {
+ let attr = Self::parse_word(word);
+ result.push(attr);
+ }
+ result
+ }
+
+ fn parse_words(attrs: &str) -> impl Iterator<Item = &str> {
+ if attrs.starts_with('{') && attrs.ends_with('}') {
+ attrs[1..attrs.len() - 1].split_ascii_whitespace()
+ } else {
+ attrs.split_ascii_whitespace()
+ }
+ }
+
+ fn parse_word(word: &str) -> Self {
+ if let Some(id) = word.strip_prefix('#') {
+ Self::id(id)
+ } else if let Some(class) = word.strip_prefix('.') {
+ Self::class(class)
+ } else if let Some((key, value)) = word.split_once('=') {
+ Self::key_value(key, value)
+ } else {
+ Self::class(word)
+ }
+ }
+}
+
+#[cfg(test)]
+mod test_block_attr {
+ use super::BlockAttr;
+
+ #[test]
+ fn empty_string() {
+ assert_eq!(BlockAttr::parse(""), vec![]);
+ }
+
+ #[test]
+ fn plain_word() {
+ assert_eq!(
+ BlockAttr::parse("foo"),
+ vec![BlockAttr::Class("foo".into())]
+ );
+ }
+
+ #[test]
+ fn dot_word() {
+ assert_eq!(
+ BlockAttr::parse(".foo"),
+ vec![BlockAttr::Class("foo".into())]
+ );
+ }
+
+ #[test]
+ fn hash_word() {
+ assert_eq!(BlockAttr::parse("#foo"), vec![BlockAttr::Id("foo".into())]);
+ }
+
+ #[test]
+ fn key_value() {
+ assert_eq!(
+ BlockAttr::parse("foo=bar"),
+ vec![BlockAttr::KeyValue("foo".into(), "bar".into())]
+ );
+ }
+
+ #[test]
+ fn several() {
+ assert_eq!(
+ BlockAttr::parse("{#foo .bar foobar yo=yoyo}"),
+ vec![
+ BlockAttr::Id("foo".into()),
+ BlockAttr::Class("bar".into()),
+ BlockAttr::Class("foobar".into()),
+ BlockAttr::KeyValue("yo".into(), "yoyo".into()),
+ ]
+ );
+ }
+}
+
+#[derive(Debug, Default)]
+struct Slugs {
+ slugs: HashSet<String>,
+}
+
+impl Slugs {
+ const MAX: usize = 8;
+
+ fn remember(&mut self, slug: &str) {
+ self.slugs.insert(slug.into());
+ }
+
+ fn unique(&mut self, candidate: &str) -> String {
+ let slug = self.helper(candidate);
+ self.remember(&slug);
+ slug
+ }
+
+ fn helper(&mut self, candidate: &str) -> String {
+ let mut slug0 = String::new();
+ for c in candidate.chars() {
+ if slug0.len() >= Self::MAX {
+ break;
+ }
+ slug0.push(c);
+ }
+
+ if !self.slugs.contains(&slug0) {
+ return slug0.to_string();
+ }
+
+ let mut i = 0;
+ loop {
+ i += 1;
+ let slug = format!("{}{}", slug0, i);
+ if !self.slugs.contains(&slug) {
+ return slug;
+ }
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 3b4e844..2f55ede 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -6,13 +6,6 @@
#![deny(missing_docs)]
-// Handle the multiple pandoc_ast versions
-
-#[cfg(feature = "pandoc_ast_07")]
-extern crate pandoc_ast_07 as pandoc_ast;
-#[cfg(feature = "pandoc_ast_08")]
-extern crate pandoc_ast_08 as pandoc_ast;
-
mod error;
pub use error::SubplotError;
pub use error::Warning;
@@ -21,25 +14,21 @@ pub use error::Warnings;
pub mod resource;
mod diagrams;
-pub use diagrams::{DiagramMarkup, DotMarkup, MarkupOpts, PikchrMarkup, PlantumlMarkup};
-
-mod datafiles;
-pub use datafiles::DataFile;
-pub use datafiles::DataFiles;
+pub use diagrams::{DiagramMarkup, DotMarkup, MarkupOpts, PikchrMarkup, PlantumlMarkup, Svg};
-mod panhelper;
-mod typeset;
-
-mod visitor;
-use visitor::LintingVisitor;
+mod embedded;
+pub use embedded::EmbeddedFile;
+pub use embedded::EmbeddedFiles;
mod policy;
pub use policy::get_basedir_from;
mod metadata;
-pub use metadata::Metadata;
+pub use metadata::{Metadata, YamlMetadata};
mod doc;
+pub mod html;
+pub mod md;
pub use doc::Document;
pub use doc::{codegen, load_document, load_document_with_pullmark};
@@ -56,9 +45,6 @@ mod bindings;
pub use bindings::Binding;
pub use bindings::Bindings;
-mod parser;
-pub use parser::parse_scenario_snippet;
-
mod matches;
pub use matches::MatchedScenario;
pub use matches::MatchedStep;
@@ -71,6 +57,3 @@ pub use templatespec::TemplateSpec;
mod codegen;
pub use codegen::generate_test_program;
-
-mod ast;
-pub use ast::AbstractSyntaxTree;
diff --git a/src/matches.rs b/src/matches.rs
index 9130641..18c9832 100644
--- a/src/matches.rs
+++ b/src/matches.rs
@@ -1,3 +1,4 @@
+use crate::html::{Attribute, Content, Element, ElementTag, Location};
use crate::Binding;
use crate::Scenario;
use crate::StepKind;
@@ -12,6 +13,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct MatchedScenario {
title: String,
+ origin: Location,
steps: Vec<MatchedStep>,
}
@@ -29,6 +31,7 @@ impl MatchedScenario {
.collect();
Ok(MatchedScenario {
title: scen.title().to_string(),
+ origin: scen.origin().clone(),
steps: steps?,
})
}
@@ -73,6 +76,7 @@ pub struct MatchedStep {
kind: StepKind,
pattern: String,
text: String,
+ origin: Location,
parts: Vec<PartialStep>,
function: Option<String>,
cleanup: Option<String>,
@@ -81,12 +85,13 @@ pub struct MatchedStep {
impl MatchedStep {
/// Return a new empty match. Empty means it has no step parts.
- pub fn new(binding: &Binding, template: &str) -> MatchedStep {
+ pub fn new(binding: &Binding, template: &str, origin: Location) -> MatchedStep {
let bimpl = binding.step_impl(template);
MatchedStep {
kind: binding.kind(),
pattern: binding.pattern().to_string(),
text: "".to_string(),
+ origin,
parts: vec![],
function: bimpl.clone().map(|b| b.function().to_owned()),
cleanup: bimpl.and_then(|b| b.cleanup().map(String::from)),
@@ -136,6 +141,14 @@ impl MatchedStep {
pub fn types(&self) -> &HashMap<String, CaptureType> {
&self.types
}
+
+ /// Render the step as HTML.
+ pub fn to_html(&self) -> Element {
+ let mut e = Element::new(ElementTag::Span);
+ e.push_attribute(Attribute::new("class", "scenario_step"));
+ e.push_child(Content::Text(self.text().into()));
+ e
+ }
}
/// Part of a scenario step, possibly captured by a pattern.
@@ -151,6 +164,8 @@ pub enum PartialStep {
name: String,
/// Text of the capture.
text: String,
+ /// Type of capture.
+ kind: CaptureType,
},
}
@@ -161,10 +176,11 @@ impl PartialStep {
}
/// Construct a textual captured part of a scenario step.
- pub fn text(name: &str, text: &str) -> PartialStep {
+ pub fn text(name: &str, text: &str, kind: CaptureType) -> PartialStep {
PartialStep::CapturedText {
name: name.to_string(),
text: text.to_string(),
+ kind,
}
}
@@ -179,6 +195,7 @@ impl PartialStep {
#[cfg(test)]
mod test_partial_steps {
use super::PartialStep;
+ use crate::bindings::CaptureType;
#[test]
fn identical_uncaptured_texts_match() {
@@ -196,29 +213,29 @@ mod test_partial_steps {
#[test]
fn identical_captured_texts_match() {
- let p1 = PartialStep::text("xxx", "foo");
- let p2 = PartialStep::text("xxx", "foo");
+ let p1 = PartialStep::text("xxx", "foo", CaptureType::Text);
+ let p2 = PartialStep::text("xxx", "foo", CaptureType::Text);
assert_eq!(p1, p2);
}
#[test]
fn different_captured_texts_dont_match() {
- let p1 = PartialStep::text("xxx", "foo");
- let p2 = PartialStep::text("xxx", "bar");
+ let p1 = PartialStep::text("xxx", "foo", CaptureType::Text);
+ let p2 = PartialStep::text("xxx", "bar", CaptureType::Text);
assert_ne!(p1, p2);
}
#[test]
fn differently_named_captured_texts_dont_match() {
- let p1 = PartialStep::text("xxx", "foo");
- let p2 = PartialStep::text("yyy", "foo");
+ let p1 = PartialStep::text("xxx", "foo", CaptureType::Text);
+ let p2 = PartialStep::text("yyy", "foo", CaptureType::Text);
assert_ne!(p1, p2);
}
#[test]
fn differently_captured_texts_dont_match() {
let p1 = PartialStep::uncaptured("foo");
- let p2 = PartialStep::text("xxx", "foo");
+ let p2 = PartialStep::text("xxx", "foo", CaptureType::Text);
assert_ne!(p1, p2);
}
}
@@ -249,6 +266,8 @@ impl StepSnippet {
#[cfg(test)]
mod test {
+ use crate::bindings::CaptureType;
+
use super::PartialStep;
#[test]
@@ -262,11 +281,12 @@ mod test {
#[test]
fn returns_text() {
- let p = PartialStep::text("xxx", "foo");
+ let p = PartialStep::text("xxx", "foo", crate::bindings::CaptureType::Text);
match p {
- PartialStep::CapturedText { name, text } => {
+ PartialStep::CapturedText { name, text, kind } => {
assert_eq!(name, "xxx");
assert_eq!(text, "foo");
+ assert_eq!(kind, CaptureType::Text);
}
_ => panic!("expected CapturedText: {:?}", p),
}
diff --git a/src/md.rs b/src/md.rs
new file mode 100644
index 0000000..b8f9beb
--- /dev/null
+++ b/src/md.rs
@@ -0,0 +1,801 @@
+//! A parsed Markdown document.
+
+use crate::{
+ html::{parse, Attribute, Content, Element, ElementTag, Location},
+ steps::parse_scenario_snippet,
+ Bindings, EmbeddedFile, EmbeddedFiles, Scenario, Style, SubplotError, Warnings,
+};
+use log::trace;
+use std::collections::HashSet;
+use std::path::{Path, PathBuf};
+
+/// A parsed Markdown document.
+#[derive(Debug)]
+pub struct Markdown {
+ html: Element,
+}
+
+impl Markdown {
+ /// Load a Markdown file.
+ pub fn load_file(filename: &Path) -> Result<Self, SubplotError> {
+ trace!("parsing file as markdown: {}", filename.display());
+ let text = std::fs::read(filename)
+ .map_err(|e| SubplotError::InputFileUnreadable(filename.into(), e))?;
+ let text = std::str::from_utf8(&text).map_err(SubplotError::Utf8Error)?;
+ Self::new_from_str(filename, text)
+ }
+
+ fn new_from_str(filename: &Path, text: &str) -> Result<Self, SubplotError> {
+ let html = parse(filename, text)?;
+ Ok(Self::new(html))
+ }
+
+ fn new(html: Element) -> Self {
+ Self { html }
+ }
+
+ /// Return root element of markdown.
+ pub fn root_element(&self) -> &Element {
+ &self.html
+ }
+
+ /// Find included images.
+ pub fn images(&self) -> Vec<PathBuf> {
+ let mut names = vec![];
+ for e in Self::visit(&self.html) {
+ if e.tag() == ElementTag::Img {
+ if let Some(attr) = e.attr("src") {
+ if let Some(href) = attr.value() {
+ names.push(PathBuf::from(&href));
+ }
+ }
+ }
+ }
+ names
+ }
+
+ /// Turn an element tree into a flat vector.
+ pub fn visit(e: &Element) -> Vec<&Element> {
+ let mut elements = vec![];
+ Self::visit_helper(e, &mut elements);
+ elements
+ }
+
+ fn visit_helper<'a>(e: &'a Element, elements: &mut Vec<&'a Element>) {
+ elements.push(e);
+ for child in e.children() {
+ if let Content::Elt(ee) = child {
+ Self::visit_helper(ee, elements);
+ }
+ }
+ }
+
+ /// Find classes used for fenced blocks.
+ pub fn block_classes(&self) -> HashSet<String> {
+ let mut classes: HashSet<String> = HashSet::new();
+
+ for e in Self::visit(&self.html) {
+ if e.tag() == ElementTag::Pre {
+ if let Some(attr) = e.attr("class") {
+ if let Some(value) = attr.value() {
+ classes.insert(value.into());
+ }
+ }
+ }
+ }
+
+ classes
+ }
+
+ /// Typeset.
+ pub fn typeset(
+ &mut self,
+ _style: Style,
+ template: Option<&str>,
+ bindings: &Bindings,
+ ) -> Warnings {
+ let result = typeset::typeset_element(&self.html, template, bindings);
+ if let Ok(html) = result {
+ self.html = html;
+ Warnings::default()
+ } else {
+ // FIXME: handle warnings in some way
+ Warnings::default()
+ }
+ }
+
+ /// Find scenarios.
+ pub fn scenarios(&self) -> Result<Vec<Scenario>, SubplotError> {
+ let mut elements = vec![];
+ for e in Self::visit(&self.html) {
+ if let Some(se) = Self::is_structure_element(e) {
+ elements.push(se);
+ }
+ }
+
+ let mut scenarios: Vec<Scenario> = vec![];
+
+ let mut i = 0;
+ while i < elements.len() {
+ let (maybe, new_i) = extract_scenario(&elements[i..])?;
+ if let Some(scen) = maybe {
+ scenarios.push(scen);
+ }
+ i += new_i;
+ }
+ trace!("Metadata::scenarios: found {} scenarios", scenarios.len());
+ Ok(scenarios)
+ }
+
+ fn is_structure_element(e: &Element) -> Option<StructureElement> {
+ match e.tag() {
+ ElementTag::H1 => Some(StructureElement::heading(e, 1)),
+ ElementTag::H2 => Some(StructureElement::heading(e, 2)),
+ ElementTag::H3 => Some(StructureElement::heading(e, 3)),
+ ElementTag::H4 => Some(StructureElement::heading(e, 4)),
+ ElementTag::H5 => Some(StructureElement::heading(e, 5)),
+ ElementTag::H6 => Some(StructureElement::heading(e, 6)),
+ ElementTag::Pre => {
+ if e.has_attr("class", "scenario") {
+ Some(StructureElement::snippet(e))
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+ }
+
+ /// Find embedded files.
+ pub fn embedded_files(&self) -> Result<EmbeddedFiles, MdError> {
+ let mut files = EmbeddedFiles::default();
+
+ for e in Self::visit(&self.html) {
+ if let MaybeEmbeddedFile::IsFile(file) = embedded_file(e)? {
+ files.push(file);
+ }
+ }
+
+ Ok(files)
+ }
+
+ /// Find all code blocks which have identifiers and return them
+ pub fn named_blocks(&self) -> impl Iterator<Item = &Element> {
+ Self::visit(&self.html)
+ .into_iter()
+ .filter(|e| e.tag() == ElementTag::Pre && e.attr("id").is_some())
+ }
+}
+
+// A structure element in the document: a heading or a scenario snippet.
+#[derive(Debug)]
+enum StructureElement {
+ // Headings consist of the text and the level of the heading.
+ Heading(String, i64, Location),
+
+ // Scenario snippets consist just of the unparsed text.
+ Snippet(String, Location),
+}
+
+impl StructureElement {
+ fn heading(e: &Element, level: i64) -> Self {
+ Self::Heading(e.content(), level, e.location())
+ }
+
+ fn snippet(e: &Element) -> Self {
+ Self::Snippet(e.content(), e.location())
+ }
+}
+
+enum MaybeEmbeddedFile {
+ IsFile(EmbeddedFile),
+ NotFile,
+}
+
+fn embedded_file(e: &Element) -> Result<MaybeEmbeddedFile, MdError> {
+ if e.tag() != ElementTag::Pre {
+ return Ok(MaybeEmbeddedFile::NotFile);
+ }
+
+ if !e.has_attr("class", "file") {
+ return Ok(MaybeEmbeddedFile::NotFile);
+ }
+
+ let id = e.attr("id");
+ if id.is_none() {
+ return Ok(MaybeEmbeddedFile::NotFile);
+ }
+ let id = id.unwrap();
+ if id.value().is_none() {
+ return Err(MdError::NoIdValue(e.location()));
+ }
+ let id = id.value().unwrap();
+ if id.is_empty() {
+ return Err(MdError::NoIdValue(e.location()));
+ }
+
+ // The contents we get from the pulldown_cmark parser for a code
+ // block will always end in a newline, unless the block is empty.
+ // This is different from the parser we previously used, which
+ // didn't end in a newline, if the contents is exactly one line.
+ // The add-newline attribute was designed for the previous parser
+ // behavior, and so its interpretations for new new parser is a
+ // little less straightforward. To avoid convoluted logic, we
+ // remove the newline if it's there before obeying add-newline.
+ let mut contents = e.content();
+ if contents.ends_with('\n') {
+ contents.truncate(contents.len() - 1);
+ }
+ let addnl = AddNewline::parse(e.attr("add-newline"), e.location());
+ match addnl? {
+ AddNewline::No => {
+ // Newline already isn't there.
+ }
+ AddNewline::Yes => {
+ // Add newline.
+ contents.push('\n');
+ }
+ AddNewline::Auto => {
+ // Add newline if not there.
+ if !contents.ends_with('\n') {
+ contents.push('\n');
+ }
+ }
+ };
+
+ Ok(MaybeEmbeddedFile::IsFile(EmbeddedFile::new(
+ id.into(),
+ contents,
+ )))
+}
+
+#[derive(Debug, Eq, PartialEq, Copy, Clone)]
+enum AddNewline {
+ Auto,
+ Yes,
+ No,
+}
+
+impl AddNewline {
+ fn parse(attr: Option<&Attribute>, loc: Location) -> Result<Self, MdError> {
+ if let Some(attr) = attr {
+ if let Some(value) = attr.value() {
+ let value = match value {
+ "yes" => Self::Yes,
+ "no" => Self::No,
+ "auto" => Self::Auto,
+ _ => return Err(MdError::BadAddNewline(value.into(), loc)),
+ };
+ return Ok(value);
+ }
+ };
+ Ok(Self::Auto)
+ }
+}
+
+fn extract_scenario(e: &[StructureElement]) -> Result<(Option<Scenario>, usize), SubplotError> {
+ if e.is_empty() {
+ // If we get here, it's a programming error.
+ panic!("didn't expect empty list of elements");
+ }
+
+ match &e[0] {
+ StructureElement::Snippet(_, loc) => Err(SubplotError::ScenarioBeforeHeading(loc.clone())),
+ StructureElement::Heading(title, level, loc) => {
+ let mut scen = Scenario::new(title, loc.clone());
+ for (i, item) in e.iter().enumerate().skip(1) {
+ match item {
+ StructureElement::Heading(_, level2, _loc) => {
+ let is_subsection = *level2 > *level;
+ if is_subsection {
+ if scen.has_steps() {
+ } else {
+ return Ok((None, i));
+ }
+ } else if scen.has_steps() {
+ return Ok((Some(scen), i));
+ } else {
+ return Ok((None, i));
+ }
+ }
+ StructureElement::Snippet(text, loc) => {
+ let steps = parse_scenario_snippet(text, loc)?;
+ for step in steps {
+ scen.add(&step);
+ }
+ }
+ }
+ }
+ if scen.has_steps() {
+ Ok((Some(scen), e.len()))
+ } else {
+ Ok((None, e.len()))
+ }
+ }
+ }
+}
+
+mod typeset {
+ const UNWANTED_ATTRS: &[&str] = &["add-newline"];
+
+ use crate::{
+ html::{Attribute, Content, Element, ElementTag, Location},
+ Bindings, PartialStep,
+ };
+ // use crate::parser::parse_scenario_snippet;
+ // use crate::Bindings;
+ // use crate::PartialStep;
+ // use crate::ScenarioStep;
+ // use crate::StepKind;
+ use crate::SubplotError;
+ use crate::{DiagramMarkup, DotMarkup, MatchedStep, PikchrMarkup, PlantumlMarkup, Svg};
+ // use crate::{Warning, Warnings};
+
+ use base64::prelude::{Engine as _, BASE64_STANDARD};
+
+ pub(crate) fn typeset_element(
+ e: &Element,
+ template: Option<&str>,
+ bindings: &Bindings,
+ ) -> Result<Element, SubplotError> {
+ let new = match e.tag() {
+ ElementTag::Pre if e.has_attr("class", "scenario") => {
+ typeset_scenario(e, template, bindings)
+ }
+ ElementTag::Pre if e.has_attr("class", "file") => typeset_file(e),
+ ElementTag::Pre if e.has_attr("class", "example") => typeset_example(e),
+ ElementTag::Pre if e.has_attr("class", "dot") => typeset_dot(e),
+ ElementTag::Pre if e.has_attr("class", "plantuml") => typeset_plantuml(e),
+ ElementTag::Pre if e.has_attr("class", "roadmap") => typeset_roadmap(e),
+ ElementTag::Pre if e.has_attr("class", "pikchr") => typeset_pikchr(e),
+ _ => {
+ let mut new = Element::new(e.tag());
+ for attr in e.all_attrs() {
+ new.push_attribute(attr.clone());
+ }
+ for child in e.children() {
+ if let Content::Elt(ce) = child {
+ new.push_child(Content::Elt(typeset_element(ce, template, bindings)?));
+ } else {
+ new.push_child(child.clone());
+ }
+ }
+ Ok(new)
+ }
+ };
+ let mut new = new?;
+ new.drop_attributes(UNWANTED_ATTRS);
+ Ok(new)
+ }
+
+ fn typeset_scenario(
+ e: &Element,
+ template: Option<&str>,
+ bindings: &Bindings,
+ ) -> Result<Element, SubplotError> {
+ let template = template.unwrap_or("python"); // FIXME
+
+ let text = e.content();
+ let steps = crate::steps::parse_scenario_snippet(&text, &Location::Unknown)?;
+
+ let mut scenario = Element::new(ElementTag::Div);
+ scenario.push_attribute(Attribute::new("class", "scenario"));
+
+ for step in steps {
+ if let Ok(matched) = bindings.find(template, &step) {
+ scenario.push_child(Content::Elt(typeset_step(&matched)));
+ } else {
+ scenario.push_child(Content::Text(step.text().into()));
+ }
+ }
+
+ Ok(scenario)
+ }
+
+ fn typeset_step(matched: &MatchedStep) -> Element {
+ let mut e = Element::new(ElementTag::Div);
+ let mut keyword = Element::new(ElementTag::Span);
+ keyword.push_attribute(Attribute::new("class", "keyword"));
+ keyword.push_child(Content::Text(matched.kind().to_string()));
+ keyword.push_child(Content::Text(" ".into()));
+ e.push_child(Content::Elt(keyword));
+ for part in matched.parts() {
+ match part {
+ PartialStep::UncapturedText(snippet) => {
+ let text = snippet.text();
+ if !text.trim().is_empty() {
+ let mut estep = Element::new(ElementTag::Span);
+ estep.push_attribute(Attribute::new("class", "uncaptured"));
+ estep.push_child(Content::Text(text.into()));
+ e.push_child(Content::Elt(estep));
+ }
+ }
+ PartialStep::CapturedText {
+ name: _,
+ text,
+ kind,
+ } => {
+ if !text.trim().is_empty() {
+ let mut estep = Element::new(ElementTag::Span);
+ let class = format!("capture-{}", kind.as_str());
+ estep.push_attribute(Attribute::new("class", &class));
+ estep.push_child(Content::Text(text.into()));
+ e.push_child(Content::Elt(estep));
+ }
+ }
+ }
+ }
+ e
+ }
+
+ fn typeset_file(e: &Element) -> Result<Element, SubplotError> {
+ Ok(e.clone()) // FIXME
+ }
+
+ fn typeset_example(e: &Element) -> Result<Element, SubplotError> {
+ Ok(e.clone()) // FIXME
+ }
+
+ fn typeset_dot(e: &Element) -> Result<Element, SubplotError> {
+ let dot = e.content();
+ let svg = DotMarkup::new(&dot).as_svg()?;
+ Ok(svg_to_element(svg, "Dot diagram"))
+ }
+
+ fn typeset_plantuml(e: &Element) -> Result<Element, SubplotError> {
+ let markup = e.content();
+ let svg = PlantumlMarkup::new(&markup).as_svg()?;
+ Ok(svg_to_element(svg, "UML diagram"))
+ }
+
+ fn typeset_pikchr(e: &Element) -> Result<Element, SubplotError> {
+ let markup = e.content();
+ let svg = PikchrMarkup::new(&markup, None).as_svg()?;
+ Ok(svg_to_element(svg, "Pikchr diagram"))
+ }
+
+ fn typeset_roadmap(e: &Element) -> Result<Element, SubplotError> {
+ const WIDTH: usize = 50;
+
+ let yaml = e.content();
+ let roadmap = roadmap::from_yaml(&yaml)?;
+ let dot = roadmap.format_as_dot(WIDTH)?;
+ let svg = DotMarkup::new(&dot).as_svg()?;
+ Ok(svg_to_element(svg, "Road map"))
+ }
+
+ fn svg_to_element(svg: Svg, alt: &str) -> Element {
+ let url = svg_as_data_url(svg);
+ let img = html_img(&url, alt);
+ html_p(vec![Content::Elt(img)])
+ }
+
+ fn svg_as_data_url(svg: Svg) -> String {
+ let svg = BASE64_STANDARD.encode(svg.data());
+ format!("data:image/svg+xml;base64,{svg}")
+ }
+
+ fn html_p(children: Vec<Content>) -> Element {
+ let mut new = Element::new(ElementTag::P);
+ for child in children {
+ new.push_child(child);
+ }
+ new
+ }
+
+ fn html_img(src: &str, alt: &str) -> Element {
+ let mut new = Element::new(ElementTag::Img);
+ new.push_attribute(Attribute::new("src", src));
+ new.push_attribute(Attribute::new("alt", alt));
+ new
+ }
+}
+
+/// Errors returned from the module.
+#[derive(Debug, thiserror::Error, Eq, PartialEq)]
+pub enum MdError {
+ /// Tried to treat a non-PRE element as an embedded file.
+ #[error("{1}: tried to treat wrong kind of element as an embedded file: {0}")]
+ NotCodeBlockElement(String, Location),
+
+ /// Code block lacks the "file" attribute.
+ #[error("{0}; code block is not a file")]
+ NotFile(Location),
+
+ /// Code block lacks an identifier to use as the filename.
+ #[error("{0}: code block lacks a filename identifier")]
+ NoId(Location),
+
+ /// Identifier is empty.
+ #[error("{0}: code block has an empty filename identifier")]
+ NoIdValue(Location),
+
+ /// Value of add-newline attribute is not understood.
+ #[error("{1}: value of add-newline attribute is not understood: {0}")]
+ BadAddNewline(String, Location),
+}
+
+#[cfg(test)]
+mod test_extract {
+ use super::extract_scenario;
+ use super::Location;
+ use super::StructureElement;
+ use crate::Scenario;
+ use crate::SubplotError;
+
+ fn h(title: &str, level: i64) -> StructureElement {
+ StructureElement::Heading(title.to_string(), level, Location::unknown())
+ }
+
+ fn s(text: &str) -> StructureElement {
+ StructureElement::Snippet(text.to_string(), Location::unknown())
+ }
+
+ fn check_result(
+ r: Result<(Option<Scenario>, usize), SubplotError>,
+ title: Option<&str>,
+ i: usize,
+ ) {
+ assert!(r.is_ok());
+ let (actual_scen, actual_i) = r.unwrap();
+ if title.is_none() {
+ assert!(actual_scen.is_none());
+ } else {
+ assert!(actual_scen.is_some());
+ let scen = actual_scen.unwrap();
+ assert_eq!(scen.title(), title.unwrap());
+ }
+ assert_eq!(actual_i, i);
+ }
+
+ #[test]
+ fn returns_nothing_if_there_is_no_scenario() {
+ let elements: Vec<StructureElement> = vec![h("title", 1)];
+ let r = extract_scenario(&elements);
+ check_result(r, None, 1);
+ }
+
+ #[test]
+ fn returns_scenario_if_there_is_one() {
+ let elements = vec![h("title", 1), s("given something")];
+ let r = extract_scenario(&elements);
+ check_result(r, Some("title"), 2);
+ }
+
+ #[test]
+ fn skips_scenarioless_section_in_favour_of_same_level() {
+ let elements = vec![h("first", 1), h("second", 1), s("given something")];
+ let r = extract_scenario(&elements);
+ check_result(r, None, 1);
+ let r = extract_scenario(&elements[1..]);
+ check_result(r, Some("second"), 2);
+ }
+
+ #[test]
+ fn returns_parent_section_with_scenario_snippet() {
+ let elements = vec![
+ h("1", 1),
+ s("given something"),
+ h("1.1", 2),
+ s("when something"),
+ h("2", 1),
+ ];
+ let r = extract_scenario(&elements);
+ check_result(r, Some("1"), 4);
+ let r = extract_scenario(&elements[4..]);
+ check_result(r, None, 1);
+ }
+
+ #[test]
+ fn skips_scenarioless_parent_heading() {
+ let elements = vec![h("1", 1), h("1.1", 2), s("given something"), h("2", 1)];
+
+ let r = extract_scenario(&elements);
+ check_result(r, None, 1);
+
+ let r = extract_scenario(&elements[1..]);
+ check_result(r, Some("1.1"), 2);
+
+ let r = extract_scenario(&elements[3..]);
+ check_result(r, None, 1);
+ }
+
+ #[test]
+ fn skips_scenarioless_deeper_headings() {
+ let elements = vec![h("1", 1), h("1.1", 2), h("2", 1), s("given something")];
+
+ let r = extract_scenario(&elements);
+ check_result(r, None, 1);
+
+ let r = extract_scenario(&elements[1..]);
+ check_result(r, None, 1);
+
+ let r = extract_scenario(&elements[2..]);
+ check_result(r, Some("2"), 2);
+ }
+
+ #[test]
+ fn returns_error_if_scenario_has_no_title() {
+ let elements = vec![s("given something")];
+ let r = extract_scenario(&elements);
+ match r {
+ Err(SubplotError::ScenarioBeforeHeading(_)) => (),
+ _ => panic!("unexpected result {:?}", r),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::{AddNewline, Attribute, Location, Markdown, MdError};
+ use std::path::{Path, PathBuf};
+
+ #[test]
+ fn loads_empty_doc() {
+ let md = Markdown::new_from_str(Path::new(""), "").unwrap();
+ assert!(md.html.content().is_empty());
+ }
+
+ #[test]
+ fn finds_no_images_in_empty_doc() {
+ let md = Markdown::new_from_str(Path::new(""), "").unwrap();
+ assert!(md.images().is_empty());
+ }
+
+ #[test]
+ fn finds_images() {
+ let md = Markdown::new_from_str(
+ Path::new(""),
+ r#"
+![alt text](filename.jpg)
+"#,
+ )
+ .unwrap();
+ assert_eq!(md.images(), vec![PathBuf::from("filename.jpg")]);
+ }
+
+ #[test]
+ fn finds_no_blocks_in_empty_doc() {
+ let md = Markdown::new_from_str(Path::new(""), "").unwrap();
+ assert!(md.block_classes().is_empty());
+ }
+
+ #[test]
+ fn finds_no_classes_when_no_blocks_have_them() {
+ let md = Markdown::new_from_str(
+ Path::new(""),
+ r#"
+~~~
+~~~
+"#,
+ )
+ .unwrap();
+ assert!(md.block_classes().is_empty());
+ }
+
+ #[test]
+ fn finds_block_classes() {
+ let md = Markdown::new_from_str(
+ Path::new(""),
+ r#"
+~~~scenario
+~~~
+"#,
+ )
+ .unwrap();
+ let classes: Vec<String> = md.block_classes().iter().map(|s| s.into()).collect();
+ assert_eq!(classes, vec!["scenario"]);
+ }
+
+ #[test]
+ fn finds_no_scenarios_in_empty_doc() {
+ let md = Markdown::new_from_str(Path::new(""), "").unwrap();
+ let scenarios = md.scenarios().unwrap();
+ assert!(scenarios.is_empty());
+ }
+
+ #[test]
+ fn finds_scenarios() {
+ let md = Markdown::new_from_str(
+ Path::new(""),
+ r#"
+# Super trooper
+
+~~~scenario
+given ABBA
+~~~
+"#,
+ )
+ .unwrap();
+ let scenarios = md.scenarios().unwrap();
+ assert_eq!(scenarios.len(), 1);
+ let scen = scenarios.first().unwrap();
+ assert_eq!(scen.title(), "Super trooper");
+ let steps = scen.steps();
+ assert_eq!(steps.len(), 1);
+ let step = steps.first().unwrap();
+ assert_eq!(step.kind(), crate::StepKind::Given);
+ assert_eq!(step.text(), "ABBA");
+ }
+
+ #[test]
+ fn finds_no_embedded_files_in_empty_doc() {
+ let md = Markdown::new_from_str(Path::new(""), "").unwrap();
+ let files = md.embedded_files();
+ assert!(files.unwrap().files().is_empty());
+ }
+
+ #[test]
+ fn finds_embedded_files() {
+ let md = Markdown::new_from_str(
+ Path::new(""),
+ r#"
+~~~{#fileid .file .text}
+hello, world
+~~~
+"#,
+ )
+ .unwrap();
+ let files = md.embedded_files().unwrap();
+ assert_eq!(files.files().len(), 1);
+ let file = files.files().first().unwrap();
+ assert_eq!(file.filename(), "fileid");
+ assert_eq!(file.contents(), "hello, world\n");
+ }
+
+ #[test]
+ fn parses_no_auto_newline_as_auto() {
+ assert_eq!(
+ AddNewline::parse(None, Location::unknown()).unwrap(),
+ AddNewline::Auto
+ );
+ }
+
+ #[test]
+ fn parses_auto_as_auto() {
+ let attr = Attribute::new("add-newline", "auto");
+ assert_eq!(
+ AddNewline::parse(Some(&attr), Location::unknown()).unwrap(),
+ AddNewline::Auto
+ );
+ }
+
+ #[test]
+ fn parses_yes_as_yes() {
+ let attr = Attribute::new("add-newline", "yes");
+ assert_eq!(
+ AddNewline::parse(Some(&attr), Location::unknown()).unwrap(),
+ AddNewline::Yes
+ );
+ }
+
+ #[test]
+ fn parses_no_as_no() {
+ let attr = Attribute::new("add-newline", "no");
+ assert_eq!(
+ AddNewline::parse(Some(&attr), Location::unknown()).unwrap(),
+ AddNewline::No
+ );
+ }
+
+ #[test]
+ fn parses_empty_as_error() {
+ let attr = Attribute::new("add-newline", "");
+ assert_eq!(
+ AddNewline::parse(Some(&attr), Location::unknown()),
+ Err(MdError::BadAddNewline("".into(), Location::unknown()))
+ );
+ }
+
+ #[test]
+ fn parses_garbage_as_error() {
+ let attr = Attribute::new("add-newline", "garbage");
+ assert_eq!(
+ AddNewline::parse(Some(&attr), Location::unknown()),
+ Err(MdError::BadAddNewline(
+ "garbage".into(),
+ Location::unknown()
+ ))
+ );
+ }
+}
diff --git a/src/metadata.rs b/src/metadata.rs
index 5f5e183..e382840 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -1,25 +1,176 @@
use crate::{Bindings, SubplotError, TemplateSpec};
-use std::collections::HashMap;
+use lazy_static::lazy_static;
+use log::trace;
+use regex::Regex;
+use serde::Deserialize;
+use std::collections::{BTreeMap, HashMap};
use std::fmt::Debug;
use std::ops::Deref;
use std::path::{Path, PathBuf};
-use pandoc_ast::{Inline, Map, MetaValue, Pandoc};
+lazy_static! {
+ // Pattern that recognises a YAML block at the beginning of a file.
+ static ref LEADING_YAML_PATTERN: Regex = Regex::new(r"^(?:\S*\n)*(?P<yaml>-{3,}\n([^.].*\n)*\.{3,}\n)(?P<text>(.*\n)*)$").unwrap();
-use log::trace;
+
+ // Pattern that recognises a YAML block at the end of a file.
+ static ref TRAILING_YAML_PATTERN: Regex = Regex::new(r"(?P<text>(.*\n)*)\n*(?P<yaml>-{3,}\n([^.].*\n)*\.{3,}\n)(?:\S*\n)*$").unwrap();
+}
+
+/// Errors from Markdown parsing.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ #[error(transparent)]
+ Regex(#[from] regex::Error),
+
+ #[error(transparent)]
+ Yaml(#[from] serde_yaml::Error),
+}
+
+/// Document metadata.
+///
+/// This is expressed in the Markdown input file as an embedded YAML
+/// block.
+///
+/// Note that this structure needs to be able to capture any metadata
+/// block we can work with, in any input file. By being strict here we
+/// make it easier to tell the user when a metadata block has, say, a
+/// misspelled field.
+#[derive(Debug, Default, Clone, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct YamlMetadata {
+ title: String,
+ subtitle: Option<String>,
+ authors: Option<Vec<String>>,
+ date: Option<String>,
+ classes: Option<Vec<String>>,
+ markdowns: Vec<PathBuf>,
+ bindings: Option<Vec<PathBuf>>,
+ documentclass: Option<String>,
+ #[serde(default)]
+ impls: BTreeMap<String, Vec<PathBuf>>,
+ css_embed: Option<Vec<PathBuf>>,
+ css_urls: Option<Vec<String>>,
+}
+
+impl YamlMetadata {
+ #[cfg(test)]
+ fn new(yaml_text: &str) -> Result<Self, Error> {
+ let meta: Self = serde_yaml::from_str(yaml_text)?;
+ Ok(meta)
+ }
+
+ /// Names of files with the Markdown for the subplot document.
+ pub fn markdowns(&self) -> &[PathBuf] {
+ &self.markdowns
+ }
+
+ /// Title.
+ pub fn title(&self) -> &str {
+ &self.title
+ }
+
+ /// Subtitle.
+ pub fn subtitle(&self) -> Option<&str> {
+ self.subtitle.as_deref()
+ }
+
+ /// Date.
+ pub fn date(&self) -> Option<&str> {
+ self.date.as_deref()
+ }
+
+ /// Set date.
+ pub fn set_date(&mut self, date: String) {
+ self.date = Some(date);
+ }
+
+ /// Authors.
+ pub fn authors(&self) -> Option<&[String]> {
+ self.authors.as_deref()
+ }
+
+ /// Names of bindings files.
+ pub fn bindings_filenames(&self) -> Option<&[PathBuf]> {
+ self.bindings.as_deref()
+ }
+
+ /// Impls section.
+ pub fn impls(&self) -> &BTreeMap<String, Vec<PathBuf>> {
+ &self.impls
+ }
+
+ /// Classes..
+ pub fn classes(&self) -> Option<&[String]> {
+ self.classes.as_deref()
+ }
+
+ /// Documentclass.
+ pub fn documentclass(&self) -> Option<&str> {
+ self.documentclass.as_deref()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::YamlMetadata;
+ use std::path::{Path, PathBuf};
+
+ #[test]
+ fn full_meta() {
+ let meta = YamlMetadata::new(
+ "\
+title: Foo Bar
+date: today
+classes: [json, text]
+impls:
+ python:
+ - foo.py
+ - bar.py
+markdowns:
+- test.md
+bindings:
+- foo.yaml
+- bar.yaml
+",
+ )
+ .unwrap();
+ assert_eq!(meta.title, "Foo Bar");
+ assert_eq!(meta.date.unwrap(), "today");
+ assert_eq!(meta.classes.unwrap(), &["json", "text"]);
+ assert_eq!(meta.markdowns, vec![Path::new("test.md")]);
+ assert_eq!(
+ meta.bindings.unwrap(),
+ &[path("foo.yaml"), path("bar.yaml")]
+ );
+ assert!(!meta.impls.is_empty());
+ for (k, v) in meta.impls.iter() {
+ assert_eq!(k, "python");
+ assert_eq!(v, &[path("foo.py"), path("bar.py")]);
+ }
+ }
+
+ fn path(s: &str) -> PathBuf {
+ PathBuf::from(s)
+ }
+}
/// Metadata of a document, as needed by Subplot.
#[derive(Debug)]
pub struct Metadata {
+ basedir: PathBuf,
title: String,
date: Option<String>,
+ authors: Option<Vec<String>>,
+ markdown_filenames: Vec<PathBuf>,
bindings_filenames: Vec<PathBuf>,
bindings: Bindings,
impls: HashMap<String, DocumentImpl>,
- bibliographies: Vec<PathBuf>,
/// Extra class names which should be considered 'correct' for this document
classes: Vec<String>,
+ css_embed: Vec<String>,
+ css_urls: Vec<String>,
}
#[derive(Debug)]
@@ -29,57 +180,71 @@ pub struct DocumentImpl {
}
impl Metadata {
- /// Construct a Metadata from a Document, if possible.
- pub fn new<P>(
+ /// Create from YamlMetadata.
+ pub fn from_yaml_metadata<P>(
basedir: P,
- doc: &Pandoc,
+ yaml: &YamlMetadata,
template: Option<&str>,
- ) -> Result<Metadata, SubplotError>
+ ) -> Result<Self, SubplotError>
where
P: AsRef<Path> + Debug,
{
- let title = get_title(&doc.meta);
- let date = get_date(&doc.meta);
- let bindings_filenames = get_bindings_filenames(&doc.meta);
- let bibliographies = get_bibliographies(basedir.as_ref(), &doc.meta);
- let classes = get_classes(&doc.meta);
- trace!("Loaded basic metadata");
+ let mut bindings = Bindings::new();
+ let bindings_filenames = if let Some(filenames) = yaml.bindings_filenames() {
+ get_bindings(filenames, &mut bindings, template)?;
+ filenames.iter().map(|p| p.to_path_buf()).collect()
+ } else {
+ vec![]
+ };
let mut impls = HashMap::new();
- if let Some(raw_impls) = doc.meta.get("impls") {
- match raw_impls {
- MetaValue::MetaMap(raw_impls) => {
- for (impl_name, functions_filenames) in raw_impls.iter() {
- let template_spec = load_template_spec(impl_name)?;
- let filenames = pathbufs("", functions_filenames);
- let docimpl = DocumentImpl::new(template_spec, filenames);
- impls.insert(impl_name.to_string(), docimpl);
- }
- }
- _ => {
- trace!("Ignoring unknown raw implementation value");
- }
- }
+ for (impl_name, functions_filenames) in yaml.impls().iter() {
+ let template_spec = load_template_spec(impl_name)?;
+ let filenames = pathbufs("", functions_filenames);
+ let docimpl = DocumentImpl::new(template_spec, filenames);
+ impls.insert(impl_name.to_string(), docimpl);
}
- let template = template.or_else(|| impls.keys().next().map(String::as_str));
-
- let mut bindings = Bindings::new();
-
- get_bindings(&bindings_filenames, &mut bindings, template)?;
-
- trace!("Loaded all metadata successfully");
+ let classes = if let Some(v) = yaml.classes() {
+ v.iter().map(|s| s.to_string()).collect()
+ } else {
+ vec![]
+ };
+
+ let mut css_embed = vec![];
+ if let Some(filenames) = &yaml.css_embed {
+ for filename in filenames.iter() {
+ let css = std::fs::read(filename)
+ .map_err(|e| SubplotError::ReadFile(filename.into(), e))?;
+ let css = String::from_utf8(css)
+ .map_err(|e| SubplotError::FileUtf8(filename.into(), e))?;
+ css_embed.push(css);
+ }
+ }
- Ok(Metadata {
- title,
- date,
+ let css_urls = if let Some(urls) = &yaml.css_urls {
+ urls.clone()
+ } else {
+ vec![]
+ };
+
+ let meta = Self {
+ basedir: basedir.as_ref().to_path_buf(),
+ title: yaml.title().into(),
+ date: yaml.date().map(|s| s.into()),
+ authors: yaml.authors().map(|a| a.into()),
+ markdown_filenames: yaml.markdowns().into(),
bindings_filenames,
bindings,
impls,
- bibliographies,
classes,
- })
+ css_embed,
+ css_urls,
+ };
+ trace!("metadata: {:#?}", meta);
+
+ Ok(meta)
}
/// Return title of document.
@@ -92,6 +257,26 @@ impl Metadata {
self.date.as_deref()
}
+ /// Set date.
+ pub fn set_date(&mut self, date: String) {
+ self.date = Some(date);
+ }
+
+ /// Authors.
+ pub fn authors(&self) -> Option<&[String]> {
+ self.authors.as_deref()
+ }
+
+ /// Return base dir for all relative filenames.
+ pub fn basedir(&self) -> &Path {
+ &self.basedir
+ }
+
+ /// Return filenames of the markdown files.
+ pub fn markdown_filenames(&self) -> &[PathBuf] {
+ &self.markdown_filenames
+ }
+
/// Return filename where bindings are specified.
pub fn bindings_filenames(&self) -> Vec<&Path> {
self.bindings_filenames.iter().map(|f| f.as_ref()).collect()
@@ -112,15 +297,20 @@ impl Metadata {
&self.bindings
}
- /// Return the bibliographies.
- pub fn bibliographies(&self) -> Vec<&Path> {
- self.bibliographies.iter().map(|x| x.as_path()).collect()
- }
-
/// The classes which this document also claims are valid
pub fn classes(&self) -> impl Iterator<Item = &str> {
self.classes.iter().map(Deref::deref)
}
+
+ /// Contents of CSS files to embed into the HTML output.
+ pub fn css_embed(&self) -> impl Iterator<Item = &str> {
+ self.css_embed.iter().map(Deref::deref)
+ }
+
+ /// List of CSS urls to add to the HTML output.
+ pub fn css_urls(&self) -> impl Iterator<Item = &str> {
+ self.css_urls.iter().map(Deref::deref)
+ }
}
impl DocumentImpl {
@@ -137,24 +327,6 @@ impl DocumentImpl {
}
}
-type Mapp = Map<String, MetaValue>;
-
-fn get_title(map: &Mapp) -> String {
- if let Some(s) = get_string(map, "title") {
- s
- } else {
- "".to_string()
- }
-}
-
-fn get_date(map: &Mapp) -> Option<String> {
- get_string(map, "date")
-}
-
-fn get_bindings_filenames(map: &Mapp) -> Vec<PathBuf> {
- get_paths("", map, "bindings")
-}
-
fn load_template_spec(template: &str) -> Result<TemplateSpec, SubplotError> {
let mut spec_path = PathBuf::from(template);
spec_path.push("template");
@@ -162,143 +334,12 @@ fn load_template_spec(template: &str) -> Result<TemplateSpec, SubplotError> {
TemplateSpec::from_file(&spec_path)
}
-fn get_paths<P>(basedir: P, map: &Mapp, field: &str) -> Vec<PathBuf>
+fn pathbufs<P>(basedir: P, v: &[PathBuf]) -> Vec<PathBuf>
where
P: AsRef<Path>,
{
- match map.get(field) {
- None => vec![],
- Some(v) => pathbufs(basedir, v),
- }
-}
-
-fn get_string(map: &Mapp, field: &str) -> Option<String> {
- let v = match map.get(field) {
- None => return None,
- Some(s) => s,
- };
- let v = match v {
- pandoc_ast::MetaValue::MetaString(s) => s.to_string(),
- pandoc_ast::MetaValue::MetaInlines(vec) => join(vec),
- _ => panic!("don't know how to handle: {:?}", v),
- };
- Some(v)
-}
-
-fn get_bibliographies<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- let v = match map.get("bibliography") {
- None => return vec![],
- Some(s) => s,
- };
- pathbufs(basedir, v)
-}
-
-fn pathbufs<P>(basedir: P, v: &MetaValue) -> Vec<PathBuf>
-where
- P: AsRef<Path>,
-{
- let mut bufs = vec![];
- push_pathbufs(basedir, v, &mut bufs);
- bufs
-}
-
-fn get_classes(map: &Mapp) -> Vec<String> {
- let mut ret = Vec::new();
- if let Some(classes) = map.get("classes") {
- push_strings(classes, &mut ret);
- }
- ret
-}
-
-fn push_strings(v: &MetaValue, strings: &mut Vec<String>) {
- match v {
- MetaValue::MetaString(s) => strings.push(s.to_string()),
- MetaValue::MetaInlines(vec) => strings.push(join(vec)),
- MetaValue::MetaList(values) => {
- for value in values {
- push_strings(value, strings);
- }
- }
- _ => panic!("don't know how to handle: {:?}", v),
- };
-}
-
-fn push_pathbufs<P>(basedir: P, v: &MetaValue, bufs: &mut Vec<PathBuf>)
-where
- P: AsRef<Path>,
-{
- match v {
- MetaValue::MetaString(s) => bufs.push(basedir.as_ref().join(Path::new(s))),
- MetaValue::MetaInlines(vec) => bufs.push(basedir.as_ref().join(Path::new(&join(vec)))),
- MetaValue::MetaList(values) => {
- for value in values {
- push_pathbufs(basedir.as_ref(), value, bufs);
- }
- }
- _ => panic!("don't know how to handle: {:?}", v),
- };
-}
-
-fn join(vec: &[Inline]) -> String {
- let mut buf = String::new();
- join_into_buffer(vec, &mut buf);
- buf
-}
-
-fn join_into_buffer(vec: &[Inline], buf: &mut String) {
- for item in vec {
- match item {
- pandoc_ast::Inline::Str(s) => buf.push_str(s),
- pandoc_ast::Inline::Code(_, s) => buf.push_str(s),
- pandoc_ast::Inline::Emph(v) => join_into_buffer(v, buf),
- pandoc_ast::Inline::Strong(v) => join_into_buffer(v, buf),
- pandoc_ast::Inline::Strikeout(v) => join_into_buffer(v, buf),
- pandoc_ast::Inline::Superscript(v) => join_into_buffer(v, buf),
- pandoc_ast::Inline::Subscript(v) => join_into_buffer(v, buf),
- pandoc_ast::Inline::SmallCaps(v) => join_into_buffer(v, buf),
- pandoc_ast::Inline::Space => buf.push(' '),
- pandoc_ast::Inline::SoftBreak => buf.push(' '),
- pandoc_ast::Inline::LineBreak => buf.push(' '),
- pandoc_ast::Inline::Quoted(qtype, v) => {
- let quote = match qtype {
- pandoc_ast::QuoteType::SingleQuote => '\'',
- pandoc_ast::QuoteType::DoubleQuote => '"',
- };
- buf.push(quote);
- join_into_buffer(v, buf);
- buf.push(quote);
- }
- _ => panic!("unknown pandoc_ast::Inline component {:?}", item),
- }
- }
-}
-
-#[cfg(test)]
-mod test_join {
- use super::join;
- use pandoc_ast::{Inline, QuoteType};
-
- #[test]
- fn join_all_kinds() {
- let v = vec![
- Inline::Str("a".to_string()),
- Inline::Emph(vec![Inline::Str("b".to_string())]),
- Inline::Strong(vec![Inline::Str("c".to_string())]),
- Inline::Strikeout(vec![Inline::Str("d".to_string())]),
- Inline::Superscript(vec![Inline::Str("e".to_string())]),
- Inline::Subscript(vec![Inline::Str("f".to_string())]),
- Inline::SmallCaps(vec![Inline::Str("g".to_string())]),
- Inline::Space,
- Inline::SoftBreak,
- Inline::Quoted(QuoteType::SingleQuote, vec![Inline::Str("h".to_string())]),
- Inline::LineBreak,
- Inline::Quoted(QuoteType::DoubleQuote, vec![Inline::Str("i".to_string())]),
- ];
- assert_eq!(join(&v), r#"abcdefg 'h' "i""#);
- }
+ let basedir = basedir.as_ref();
+ v.iter().map(|p| basedir.join(p)).collect()
}
fn get_bindings<P>(
diff --git a/src/panhelper.rs b/src/panhelper.rs
deleted file mode 100644
index f7ab801..0000000
--- a/src/panhelper.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-use pandoc_ast::Attr;
-
-/// Is a code block marked as being of a given type?
-pub fn is_class(attr: &Attr, class: &str) -> bool {
- let (_id, classes, _kvpairs) = attr;
- classes.iter().any(|s| s == class)
-}
-
-/// Utility function to find key/value pairs from an attribute
-pub fn find_attr_kv<'a>(attr: &'a Attr, key: &'static str) -> impl Iterator<Item = &'a str> {
- attr.2.iter().flat_map(move |(key_, value)| {
- if key == key_ {
- Some(value.as_ref())
- } else {
- None
- }
- })
-}
-
-/// Get the filename for a fenced code block tagged .file.
-///
-/// The filename is the first (and presumably only) identifier for the
-/// block.
-pub fn get_filename(attr: &Attr) -> String {
- attr.0.to_string()
-}
diff --git a/src/parser.rs b/src/parser.rs
deleted file mode 100644
index 35cb488..0000000
--- a/src/parser.rs
+++ /dev/null
@@ -1,43 +0,0 @@
-#[deny(missing_docs)]
-/// Parse a scenario snippet into logical lines.
-///
-/// Each logical line forms a scenario step. It may be divided into
-/// multiple physical lines.
-pub fn parse_scenario_snippet(snippet: &str) -> impl Iterator<Item = &str> {
- snippet.lines().filter(|line| !line.trim().is_empty())
-}
-
-#[cfg(test)]
-mod test {
- use super::parse_scenario_snippet;
-
- fn parse_lines(snippet: &str) -> Vec<&str> {
- parse_scenario_snippet(snippet).collect()
- }
-
- #[test]
- fn parses_empty_snippet_into_no_lines() {
- assert_eq!(parse_lines("").len(), 0);
- }
-
- #[test]
- fn parses_single_line() {
- assert_eq!(parse_lines("given I am Tomjon"), vec!["given I am Tomjon"])
- }
-
- #[test]
- fn parses_two_lines() {
- assert_eq!(
- parse_lines("given I am Tomjon\nwhen I declare myself king"),
- vec!["given I am Tomjon", "when I declare myself king"]
- )
- }
-
- #[test]
- fn parses_two_lines_with_empty_line() {
- assert_eq!(
- parse_lines("given I am Tomjon\n\nwhen I declare myself king"),
- vec!["given I am Tomjon", "when I declare myself king"]
- )
- }
-}
diff --git a/src/policy.rs b/src/policy.rs
index 972d081..e24bf8f 100644
--- a/src/policy.rs
+++ b/src/policy.rs
@@ -1,8 +1,5 @@
use std::path::{Component, Path, PathBuf};
-use log::trace;
-use pandoc::{InputFormat, InputKind, OutputFormat, OutputKind, Pandoc, PandocOption};
-
/// Get the base directory given the name of the markdown file.
///
/// All relative filename, such as bindings files, are resolved
@@ -17,23 +14,3 @@ pub fn get_basedir_from(filename: &Path) -> PathBuf {
Some(x) => x.to_path_buf(),
}
}
-
-/// Add 'citeproc' to a Pandoc instance.
-///
-/// This attempts to determine if `--citeproc` or `--filter pandoc-citeproc`
-/// is needed, and then does that specific thing.
-pub fn add_citeproc(pandoc: &mut Pandoc) {
- let mut guesser = Pandoc::new();
- guesser.set_input(InputKind::Pipe("".to_string()));
- guesser.set_input_format(InputFormat::Markdown, vec![]);
- guesser.set_output_format(OutputFormat::Markdown, vec![]);
- guesser.set_output(OutputKind::Pipe);
- guesser.add_option(PandocOption::Citeproc);
- if guesser.execute().is_ok() {
- trace!("Discovered --citeproc");
- pandoc.add_option(PandocOption::Citeproc);
- } else {
- trace!("Discovered --filter pandoc-citeproc");
- pandoc.add_option(PandocOption::Filter("pandoc-citeproc".into()));
- }
-}
diff --git a/src/scenarios.rs b/src/scenarios.rs
index 9285f1b..17549d2 100644
--- a/src/scenarios.rs
+++ b/src/scenarios.rs
@@ -1,4 +1,4 @@
-use crate::ScenarioStep;
+use crate::{html::Location, ScenarioStep};
use serde::{Deserialize, Serialize};
/// An acceptance test scenario.
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Scenario {
title: String,
- name: Option<String>,
+ origin: Location,
steps: Vec<ScenarioStep>,
}
@@ -18,10 +18,10 @@ impl Scenario {
/// Construct a new scenario.
///
/// The new scenario will have a title, but no steps.
- pub fn new(title: &str) -> Scenario {
+ pub fn new(title: &str, origin: Location) -> Scenario {
Scenario {
title: title.to_string(),
- name: None,
+ origin,
steps: vec![],
}
}
@@ -31,16 +31,6 @@ impl Scenario {
&self.title
}
- /// Set name of scenario.
- pub fn set_name(&mut self, name: &str) {
- self.name = Some(name.to_string());
- }
-
- /// Return name of scenario.
- pub fn name(&self) -> Option<&str> {
- self.name.as_deref()
- }
-
/// Does the scenario have steps?
pub fn has_steps(&self) -> bool {
!self.steps.is_empty()
@@ -55,43 +45,35 @@ impl Scenario {
pub fn add(&mut self, step: &ScenarioStep) {
self.steps.push(step.clone());
}
+
+ pub(crate) fn origin(&self) -> &Location {
+ &self.origin
+ }
}
#[cfg(test)]
mod test {
use super::Scenario;
+ use crate::html::Location;
use crate::ScenarioStep;
use crate::StepKind;
#[test]
fn has_title() {
- let scen = Scenario::new("title");
+ let scen = Scenario::new("title", Location::Unknown);
assert_eq!(scen.title(), "title");
}
#[test]
- fn has_no_name_initially() {
- let scen = Scenario::new("title");
- assert_eq!(scen.name(), None);
- }
-
- #[test]
- fn sets_name() {
- let mut scen = Scenario::new("title");
- scen.set_name("Alfred");
- assert_eq!(scen.name(), Some("Alfred"));
- }
-
- #[test]
fn has_no_steps_initially() {
- let scen = Scenario::new("title");
+ let scen = Scenario::new("title", Location::Unknown);
assert_eq!(scen.steps().len(), 0);
}
#[test]
fn adds_step() {
- let mut scen = Scenario::new("title");
- let step = ScenarioStep::new(StepKind::Given, "and", "foo");
+ let mut scen = Scenario::new("title", Location::Unknown);
+ let step = ScenarioStep::new(StepKind::Given, "and", "foo", Location::Unknown);
scen.add(&step);
assert_eq!(scen.steps(), &[step]);
}
diff --git a/src/steps.rs b/src/steps.rs
index ccbc588..7f5e7d4 100644
--- a/src/steps.rs
+++ b/src/steps.rs
@@ -1,4 +1,4 @@
-use crate::SubplotError;
+use crate::{html::Location, SubplotError};
use serde::{Deserialize, Serialize};
use std::fmt;
@@ -16,15 +16,17 @@ pub struct ScenarioStep {
kind: StepKind,
keyword: String,
text: String,
+ origin: Location,
}
impl ScenarioStep {
/// Construct a new step.
- pub fn new(kind: StepKind, keyword: &str, text: &str) -> ScenarioStep {
+ pub fn new(kind: StepKind, keyword: &str, text: &str, origin: Location) -> ScenarioStep {
ScenarioStep {
kind,
keyword: keyword.to_owned(),
text: text.to_owned(),
+ origin,
}
}
@@ -50,7 +52,12 @@ impl ScenarioStep {
pub fn new_from_str(
text: &str,
default: Option<StepKind>,
+ origin: Location,
) -> Result<ScenarioStep, SubplotError> {
+ if text.trim_start() != text {
+ return Err(SubplotError::NotAtBoln(text.into()));
+ }
+
let mut words = text.split_whitespace();
let keyword = match words.next() {
@@ -75,7 +82,11 @@ impl ScenarioStep {
if joined.len() > 1 {
joined.pop();
}
- Ok(ScenarioStep::new(kind, keyword, &joined))
+ Ok(ScenarioStep::new(kind, keyword, &joined, origin))
+ }
+
+ pub(crate) fn origin(&self) -> &Location {
+ &self.origin
}
}
@@ -85,6 +96,85 @@ impl fmt::Display for ScenarioStep {
}
}
+/// Parse a scenario snippet into a vector of steps.
+pub(crate) fn parse_scenario_snippet(
+ text: &str,
+ loc: &Location,
+) -> Result<Vec<ScenarioStep>, SubplotError> {
+ let mut steps = vec![];
+ let mut prevkind = None;
+ for (idx, line) in text.lines().enumerate() {
+ let line_loc = match loc.clone() {
+ Location::Known {
+ filename,
+ line,
+ col,
+ } => Location::Known {
+ filename,
+ line: line + idx,
+ col,
+ },
+ Location::Unknown => Location::Unknown,
+ };
+ if !line.trim().is_empty() {
+ let step = ScenarioStep::new_from_str(line, prevkind, line_loc)?;
+ prevkind = Some(step.kind());
+ steps.push(step);
+ }
+ }
+ Ok(steps)
+}
+
+#[cfg(test)]
+mod test_steps_parser {
+ use super::{parse_scenario_snippet, Location, ScenarioStep, StepKind, SubplotError};
+ use std::path::Path;
+
+ fn parse(text: &str) -> Result<Vec<ScenarioStep>, SubplotError> {
+ let loc = Location::new(Path::new("test"), 1, 1);
+ parse_scenario_snippet(text, &loc)
+ }
+
+ #[test]
+ fn empty_string() {
+ assert_eq!(parse("").unwrap(), vec![]);
+ }
+
+ #[test]
+ fn simple() {
+ assert_eq!(
+ parse("given foo").unwrap(),
+ vec![ScenarioStep::new(
+ StepKind::Given,
+ "given",
+ "foo",
+ Location::new(Path::new("test"), 1, 1),
+ )]
+ );
+ }
+
+ #[test]
+ fn two_simple() {
+ assert_eq!(
+ parse("given foo\nthen bar\n").unwrap(),
+ vec![
+ ScenarioStep::new(
+ StepKind::Given,
+ "given",
+ "foo",
+ Location::new(Path::new("test"), 1, 1),
+ ),
+ ScenarioStep::new(
+ StepKind::Then,
+ "then",
+ "bar",
+ Location::new(Path::new("test"), 2, 1),
+ )
+ ]
+ );
+ }
+}
+
/// The kind of scenario step we have: given, when, or then.
///
/// This needs to be extended if the Subplot language gets extended with other
@@ -109,53 +199,65 @@ impl fmt::Display for StepKind {
StepKind::When => "when",
StepKind::Then => "then",
};
- write!(f, "{}", s)
+ write!(f, "{s}")
}
}
#[cfg(test)]
mod test {
+ use crate::html::Location;
+
use super::{ScenarioStep, StepKind, SubplotError};
#[test]
fn parses_given() {
- let step = ScenarioStep::new_from_str("GIVEN I am Tomjon", None).unwrap();
+ let step =
+ ScenarioStep::new_from_str("GIVEN I am Tomjon", None, Location::Unknown).unwrap();
assert_eq!(step.kind(), StepKind::Given);
assert_eq!(step.text(), "I am Tomjon");
}
#[test]
fn parses_given_with_extra_spaces() {
- let step = ScenarioStep::new_from_str(" given I am Tomjon ", None).unwrap();
+ let step =
+ ScenarioStep::new_from_str("given I am Tomjon ", None, Location::Unknown)
+ .unwrap();
assert_eq!(step.kind(), StepKind::Given);
assert_eq!(step.text(), "I am Tomjon");
}
#[test]
fn parses_when() {
- let step = ScenarioStep::new_from_str("when I declare myself king", None).unwrap();
+ let step =
+ ScenarioStep::new_from_str("when I declare myself king", None, Location::Unknown)
+ .unwrap();
assert_eq!(step.kind(), StepKind::When);
assert_eq!(step.text(), "I declare myself king");
}
#[test]
fn parses_then() {
- let step = ScenarioStep::new_from_str("thEN everyone accepts it", None).unwrap();
+ let step = ScenarioStep::new_from_str("thEN everyone accepts it", None, Location::Unknown)
+ .unwrap();
assert_eq!(step.kind(), StepKind::Then);
assert_eq!(step.text(), "everyone accepts it");
}
#[test]
fn parses_and() {
- let step =
- ScenarioStep::new_from_str("and everyone accepts it", Some(StepKind::Then)).unwrap();
+ let step = ScenarioStep::new_from_str(
+ "and everyone accepts it",
+ Some(StepKind::Then),
+ Location::Unknown,
+ )
+ .unwrap();
assert_eq!(step.kind(), StepKind::Then);
assert_eq!(step.text(), "everyone accepts it");
}
#[test]
fn fails_to_parse_and() {
- let step = ScenarioStep::new_from_str("and everyone accepts it", None);
+ let step = ScenarioStep::new_from_str("and everyone accepts it", None, Location::Unknown);
assert!(step.is_err());
match step.err() {
None => unreachable!(),
diff --git a/src/style.rs b/src/style.rs
index 95f1109..9f7c801 100644
--- a/src/style.rs
+++ b/src/style.rs
@@ -12,7 +12,7 @@ impl Style {
///
/// A link is like the HTML `<a>` element. The choice of footnote
/// versus endnote is made by the typesetting backend. HTML uses
- /// endnotes, PDF uses footnotes.
+ /// endnotes, a paged media like PDF would use footnotes.
pub fn links_as_notes(&self) -> bool {
self.links_as_notes
}
diff --git a/src/typeset.rs b/src/typeset.rs
deleted file mode 100644
index 9522e69..0000000
--- a/src/typeset.rs
+++ /dev/null
@@ -1,229 +0,0 @@
-use crate::parser::parse_scenario_snippet;
-use crate::Bindings;
-use crate::PartialStep;
-use crate::ScenarioStep;
-use crate::StepKind;
-use crate::SubplotError;
-use crate::{DiagramMarkup, DotMarkup, PikchrMarkup, PlantumlMarkup};
-use crate::{Warning, Warnings};
-
-use pandoc_ast::Attr;
-use pandoc_ast::Block;
-use pandoc_ast::Inline;
-use pandoc_ast::Target;
-
-/// Typeset an error as a Pandoc AST Block element.
-pub fn error(err: SubplotError) -> Block {
- let msg = format!("ERROR: {}", err);
- Block::Para(error_msg(&msg))
-}
-
-/// Typeset an error message a vector of inlines.
-pub fn error_msg(msg: &str) -> Vec<Inline> {
- vec![Inline::Strong(vec![inlinestr(msg)])]
-}
-
-/// Typeset a string as an inline element.
-pub fn inlinestr(s: &str) -> Inline {
- Inline::Str(String::from(s))
-}
-
-/// Typeset a code block tagged as a file.
-pub fn file_block(attr: &Attr, text: &str) -> Block {
- let filename = inlinestr(&attr.0);
- let filename = Inline::Strong(vec![filename]);
- let intro = Block::Para(vec![inlinestr("File:"), space(), filename]);
- let mut cbattrs = attr.clone();
- if cbattrs.1.iter().any(|s| s == "noNumberLines") {
- // If the block says "noNumberLines" we remove that class
- cbattrs.1.retain(|s| s != "noNumberLines");
- } else if cbattrs.1.iter().all(|s| s != "numberLines") {
- // Otherwise if it doesn't say numberLines we add that in.
- cbattrs.1.push("numberLines".to_string());
- }
- // If this was an `example`, convert that class to `file`
- if cbattrs.1.iter().any(|s| s == "example") {
- cbattrs.1.retain(|s| s != "example");
- cbattrs.1.push("file".into());
- }
- let codeblock = Block::CodeBlock(cbattrs, text.to_string());
- let noattr = ("".to_string(), vec![], vec![]);
- Block::Div(noattr, vec![intro, codeblock])
-}
-
-/// Typeset a scenario snippet as a Pandoc AST Block.
-///
-/// Typesetting here means producing the Pandoc abstract syntax tree
-/// nodes that result in the desired output, when Pandoc processes
-/// them.
-///
-/// The snippet is given as a text string, which is parsed. It need
-/// not be a complete scenario, but it should consist of complete steps.
-pub fn scenario_snippet(bindings: &Bindings, snippet: &str, warnings: &mut Warnings) -> Block {
- let lines = parse_scenario_snippet(snippet);
- let mut steps = vec![];
- let mut prevkind: Option<StepKind> = None;
-
- for line in lines {
- let (this, thiskind) = step(bindings, line, prevkind, warnings);
- steps.push(this);
- prevkind = thiskind;
- }
- Block::LineBlock(steps)
-}
-
-// Typeset a single scenario step as a sequence of Pandoc AST Inlines.
-fn step(
- bindings: &Bindings,
- text: &str,
- prevkind: Option<StepKind>,
- warnings: &mut Warnings,
-) -> (Vec<Inline>, Option<StepKind>) {
- let step = ScenarioStep::new_from_str(text, prevkind);
- if step.is_err() {
- return (
- error_msg(&format!("Could not parse step: {}", text)),
- prevkind,
- );
- }
- let step = step.unwrap();
-
- let m = match bindings.find("", &step) {
- Ok(m) => m,
- Err(e) => {
- let w = Warning::UnknownBinding(format!("{}", e));
- warnings.push(w.clone());
- return (error_msg(&format!("{}", w)), prevkind);
- }
- };
-
- let mut inlines = vec![keyword(&step, prevkind), space()];
-
- for part in m.parts() {
- match part {
- PartialStep::UncapturedText(s) => inlines.push(uncaptured(s.text())),
- PartialStep::CapturedText { text, .. } => inlines.push(captured(text)),
- }
- }
-
- (inlines, Some(step.kind()))
-}
-
-// Typeset first word, which is assumed to be a keyword, of a scenario
-// step.
-fn keyword(step: &ScenarioStep, prevkind: Option<StepKind>) -> Inline {
- let actual = inlinestr(&format!("{}", step.kind()));
- let and = inlinestr("and");
- let keyword = if let Some(prevkind) = prevkind {
- if prevkind == step.kind() {
- and
- } else {
- actual
- }
- } else {
- actual
- };
- Inline::Emph(vec![keyword])
-}
-
-// Typeset a space between words.
-fn space() -> Inline {
- Inline::Space
-}
-
-// Typeset an uncaptured part of a step.
-fn uncaptured(s: &str) -> Inline {
- inlinestr(s)
-}
-
-// Typeset a captured part of a step.
-fn captured(s: &str) -> Inline {
- Inline::Strong(vec![inlinestr(s)])
-}
-
-/// Typeset a link as a note.
-pub fn link_as_note(attr: Attr, text: Vec<Inline>, target: Target) -> Inline {
- let (url, _) = target.clone();
- let url = Inline::Code(attr.clone(), url);
- let link = Inline::Link(attr.clone(), vec![url], target);
- let note = Inline::Note(vec![Block::Para(vec![link])]);
- let mut text = text;
- text.push(note);
- Inline::Span(attr, text)
-}
-
-/// Take a pikchr diagram, render it as SVG, and return an AST block element.
-///
-/// The `Block` will contain the SVG data. This allows the diagram to
-/// be rendered without referencing external entities.
-///
-/// If the code block which contained the pikchr contains other classes, they
-/// can be added to the SVG for use in later typesetting etc.
-pub fn pikchr_to_block(pikchr: &str, class: Option<&str>, warnings: &mut Warnings) -> Block {
- match PikchrMarkup::new(pikchr, class).as_svg() {
- Ok(svg) => typeset_svg(svg),
- Err(err) => {
- warnings.push(Warning::Pikchr(format!("{}", err)));
- error(err)
- }
- }
-}
-
-// Take a dot diagram, render it as SVG, and return an AST Block
-// element. The Block will contain the SVG data. This allows the
-// diagram to be rendered without referending external entities.
-pub fn dot_to_block(dot: &str, warnings: &mut Warnings) -> Block {
- match DotMarkup::new(dot).as_svg() {
- Ok(svg) => typeset_svg(svg),
- Err(err) => {
- warnings.push(Warning::Dot(format!("{}", err)));
- error(err)
- }
- }
-}
-
-// Take a PlantUML diagram, render it as SVG, and return an AST Block
-// element. The Block will contain the SVG data. This allows the
-// diagram to be rendered without referending external entities.
-pub fn plantuml_to_block(markup: &str, warnings: &mut Warnings) -> Block {
- match PlantumlMarkup::new(markup).as_svg() {
- Ok(svg) => typeset_svg(svg),
- Err(err) => {
- warnings.push(Warning::Plantuml(format!("{}", err)));
- error(err)
- }
- }
-}
-
-/// Typeset a project roadmap expressed as textual YAML, and render it
-/// as an SVG image.
-pub fn roadmap_to_block(yaml: &str, warnings: &mut Warnings) -> Block {
- match roadmap::from_yaml(yaml) {
- Ok(ref mut roadmap) => {
- roadmap.set_missing_statuses();
- let width = 50;
- match roadmap.format_as_dot(width) {
- Ok(dot) => dot_to_block(&dot, warnings),
- Err(e) => Block::Para(vec![inlinestr(&e.to_string())]),
- }
- }
- Err(e) => Block::Para(vec![inlinestr(&e.to_string())]),
- }
-}
-
-// Typeset an SVG, represented as a byte vector, as a Pandoc AST Block
-// element.
-fn typeset_svg(svg: Vec<u8>) -> Block {
- let url = svg_as_data_url(svg);
- let attr = ("".to_string(), vec![], vec![]);
- let img = Inline::Image(attr, vec![], (url, "".to_string()));
- Block::Para(vec![img])
-}
-
-// Convert an SVG, represented as a byte vector, into a data: URL,
-// which can be inlined so the image can be rendered without
-// referencing external files.
-fn svg_as_data_url(svg: Vec<u8>) -> String {
- let svg = base64::encode(&svg);
- format!("data:image/svg+xml;base64,{}", svg)
-}
diff --git a/src/visitor/block_class.rs b/src/visitor/block_class.rs
deleted file mode 100644
index 303616b..0000000
--- a/src/visitor/block_class.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-use std::collections::HashSet;
-
-use pandoc_ast::{Block, MutVisitor};
-
-#[derive(Default)]
-pub struct BlockClassVisitor {
- pub classes: HashSet<String>,
-}
-
-impl MutVisitor for BlockClassVisitor {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- for block in vec_block {
- match block {
- Block::CodeBlock(attr, _) => {
- for class in &attr.1 {
- self.classes.insert(class.to_string());
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
diff --git a/src/visitor/datafiles.rs b/src/visitor/datafiles.rs
deleted file mode 100644
index 53ab7bb..0000000
--- a/src/visitor/datafiles.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-use crate::panhelper;
-use crate::DataFile;
-use crate::DataFiles;
-
-use pandoc_ast::{Block, MutVisitor};
-
-impl MutVisitor for DataFiles {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- use panhelper::is_class;
- for block in vec_block {
- match block {
- Block::CodeBlock(attr, contents) => {
- if is_class(attr, "file") {
- let add_newline = match panhelper::find_attr_kv(attr, "add-newline").next()
- {
- None | Some("auto") => !contents.ends_with('\n'),
- Some("yes") => true,
- Some("no") => false,
- _ => unreachable!(),
- };
- let contents = if add_newline {
- format!("{}\n", contents)
- } else {
- contents.clone()
- };
- self.push(DataFile::new(panhelper::get_filename(attr), contents));
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
diff --git a/src/visitor/image.rs b/src/visitor/image.rs
deleted file mode 100644
index be49d66..0000000
--- a/src/visitor/image.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-use std::path::PathBuf;
-
-use pandoc_ast::{Inline, MutVisitor};
-
-pub struct ImageVisitor {
- images: Vec<PathBuf>,
-}
-
-impl ImageVisitor {
- pub fn new() -> Self {
- ImageVisitor { images: vec![] }
- }
-
- pub fn images(&self) -> Vec<PathBuf> {
- self.images.clone()
- }
-}
-
-impl MutVisitor for ImageVisitor {
- fn visit_inline(&mut self, inline: &mut Inline) {
- if let Inline::Image(_attr, _inlines, target) = inline {
- self.images.push(PathBuf::from(&target.0));
- }
- }
-}
diff --git a/src/visitor/linting.rs b/src/visitor/linting.rs
deleted file mode 100644
index 6266516..0000000
--- a/src/visitor/linting.rs
+++ /dev/null
@@ -1,40 +0,0 @@
-use crate::panhelper;
-use crate::SubplotError;
-
-use pandoc_ast::{Block, MutVisitor};
-
-#[derive(Default)]
-pub struct LintingVisitor {
- pub issues: Vec<SubplotError>,
-}
-
-impl MutVisitor for LintingVisitor {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- for block in vec_block {
- match block {
- Block::CodeBlock(attr, _) => {
- if panhelper::is_class(attr, "file") || panhelper::is_class(attr, "example") {
- let newlines: Vec<_> =
- panhelper::find_attr_kv(attr, "add-newline").collect();
- match newlines.len() {
- 0 => {}
- 1 => match newlines[0].to_ascii_lowercase().as_ref() {
- "auto" | "yes" | "no" => {}
- _ => self.issues.push(SubplotError::UnrecognisedAddNewline(
- panhelper::get_filename(attr),
- newlines[0].to_owned(),
- )),
- },
- _ => self.issues.push(SubplotError::RepeatedAddNewlineAttribute(
- panhelper::get_filename(attr),
- )),
- }
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
diff --git a/src/visitor/mod.rs b/src/visitor/mod.rs
deleted file mode 100644
index 95bf2b1..0000000
--- a/src/visitor/mod.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-mod block_class;
-pub use block_class::BlockClassVisitor;
-
-mod datafiles;
-
-mod image;
-pub use image::ImageVisitor;
-
-mod linting;
-pub use linting::LintingVisitor;
-
-mod structure;
-pub use structure::Element;
-pub use structure::StructureVisitor;
-
-mod typesetting;
-pub use typesetting::TypesettingVisitor;
diff --git a/src/visitor/structure.rs b/src/visitor/structure.rs
deleted file mode 100644
index f5693a6..0000000
--- a/src/visitor/structure.rs
+++ /dev/null
@@ -1,100 +0,0 @@
-use crate::panhelper;
-
-use pandoc_ast::{Block, Inline, MutVisitor};
-
-// A structure element in the document: a heading or a scenario snippet.
-#[derive(Debug)]
-pub enum Element {
- // Headings consist of the text and the level of the heading.
- Heading(String, i64),
-
- // Scenario snippets consist just of the unparsed text.
- Snippet(String),
-}
-
-impl Element {
- pub fn heading(text: &str, level: i64) -> Element {
- Element::Heading(text.to_string(), level)
- }
-
- pub fn snippet(text: &str) -> Element {
- Element::Snippet(text.to_string())
- }
-}
-
-// A MutVisitor for extracting document structure.
-pub struct StructureVisitor {
- pub elements: Vec<Element>,
-}
-
-impl StructureVisitor {
- pub fn new() -> Self {
- Self { elements: vec![] }
- }
-}
-
-impl MutVisitor for StructureVisitor {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- use panhelper::is_class;
- for block in vec_block {
- match block {
- Block::Header(level, _attr, inlines) => {
- let text = join(inlines);
- let heading = Element::heading(&text, *level);
- self.elements.push(heading);
- }
- Block::CodeBlock(attr, s) => {
- if is_class(attr, "scenario") {
- let snippet = Element::snippet(s);
- self.elements.push(snippet);
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
-}
-
-fn join(vec: &[Inline]) -> String {
- let mut buf = String::new();
- join_into_buffer(vec, &mut buf);
- buf
-}
-
-fn join_into_buffer(vec: &[Inline], buf: &mut String) {
- for item in vec {
- match item {
- Inline::Str(s) => buf.push_str(s),
- Inline::Emph(v) => join_into_buffer(v, buf),
- Inline::Strong(v) => join_into_buffer(v, buf),
- Inline::Strikeout(v) => join_into_buffer(v, buf),
- Inline::Superscript(v) => join_into_buffer(v, buf),
- Inline::Subscript(v) => join_into_buffer(v, buf),
- Inline::SmallCaps(v) => join_into_buffer(v, buf),
- Inline::Quoted(qt, v) => {
- let q = match qt {
- pandoc_ast::QuoteType::SingleQuote => "'",
- pandoc_ast::QuoteType::DoubleQuote => "\"",
- };
- buf.push_str(q);
- join_into_buffer(v, buf);
- buf.push_str(q);
- }
- Inline::Cite(_, v) => join_into_buffer(v, buf),
- Inline::Code(_attr, s) => buf.push_str(s),
- Inline::Space => buf.push(' '),
- Inline::SoftBreak => buf.push(' '),
- Inline::LineBreak => buf.push(' '),
- Inline::Math(_, s) => buf.push_str(s),
- Inline::RawInline(_, s) => buf.push_str(s),
- Inline::Link(_, v, _) => join_into_buffer(v, buf),
- Inline::Image(_, v, _) => join_into_buffer(v, buf),
- Inline::Note(_) => buf.push_str(""),
- Inline::Span(_attr, v) => join_into_buffer(v, buf),
- #[cfg(feature = "pandoc_ast_08")]
- Inline::Underline(v) => join_into_buffer(v, buf),
- }
- }
-}
diff --git a/src/visitor/typesetting.rs b/src/visitor/typesetting.rs
deleted file mode 100644
index da9c362..0000000
--- a/src/visitor/typesetting.rs
+++ /dev/null
@@ -1,85 +0,0 @@
-use crate::panhelper;
-use crate::typeset;
-use crate::{Bindings, Style, Warnings};
-
-use pandoc_ast::{Block, Inline, MutVisitor};
-
-/// Visitor for the pandoc AST.
-///
-/// This includes rendering stuff which we find as we go
-pub struct TypesettingVisitor<'a> {
- style: Style,
- bindings: &'a Bindings,
- warnings: Warnings,
-}
-
-impl<'a> TypesettingVisitor<'a> {
- pub fn new(style: Style, bindings: &'a Bindings) -> Self {
- TypesettingVisitor {
- style,
- bindings,
- warnings: Warnings::default(),
- }
- }
-
- pub fn warnings(self) -> Warnings {
- self.warnings
- }
-}
-
-// Visit interesting parts of the Pandoc abstract syntax tree. The
-// document top level is a vector of blocks and we visit that and
-// replace any fenced code block with the scenario tag with a typeset
-// paragraph. Also, replace fenced code blocks with known diagram
-// markup with the rendered SVG image.
-impl<'a> MutVisitor for TypesettingVisitor<'a> {
- fn visit_vec_block(&mut self, vec_block: &mut Vec<Block>) {
- use panhelper::is_class;
- for block in vec_block {
- match block {
- Block::CodeBlock(attr, s) => {
- if is_class(attr, "scenario") {
- *block = typeset::scenario_snippet(self.bindings, s, &mut self.warnings)
- } else if is_class(attr, "file") || is_class(attr, "example") {
- *block = typeset::file_block(attr, s)
- } else if is_class(attr, "dot") {
- *block = typeset::dot_to_block(s, &mut self.warnings)
- } else if is_class(attr, "plantuml") {
- *block = typeset::plantuml_to_block(s, &mut self.warnings)
- } else if is_class(attr, "roadmap") {
- *block = typeset::roadmap_to_block(s, &mut self.warnings)
- } else if is_class(attr, "pikchr") {
- let other_classes: Vec<_> = attr
- .1
- .iter()
- .map(String::as_str)
- .filter(|s| *s != "pikchr")
- .collect();
- let class = if other_classes.is_empty() {
- None
- } else {
- Some(other_classes.join(" "))
- };
- let class = class.as_deref();
- *block = typeset::pikchr_to_block(s, class, &mut self.warnings)
- }
- }
- _ => {
- self.visit_block(block);
- }
- }
- }
- }
- fn visit_vec_inline(&mut self, vec_inline: &mut Vec<Inline>) {
- for inline in vec_inline {
- match inline {
- Inline::Link(attr, vec, target) if self.style.links_as_notes() => {
- *inline = typeset::link_as_note(attr.clone(), vec.to_vec(), target.clone());
- }
- _ => {
- self.visit_inline(inline);
- }
- }
- }
- }
-}
diff --git a/stress b/stress
index acda0ed..43b17c0 100755
--- a/stress
+++ b/stress
@@ -30,7 +30,7 @@ start="$(ts 0)"
./stressgen s "$NSCEN" "$NSTEP"
gen="$(ts "$start")"
-docgen s.md s.pdf
+docgen s.md s.html
doc="$(ts "$start")"
codegen s.md test.py
diff --git a/subplot-build/Cargo.toml b/subplot-build/Cargo.toml
index 768e08e..66a0627 100644
--- a/subplot-build/Cargo.toml
+++ b/subplot-build/Cargo.toml
@@ -1,19 +1,20 @@
[package]
name = "subplot-build"
-version = "0.4.0"
+version = "0.9.0"
authors = [
"Lars Wirzenius <liw@liw.fi>",
"Daniel Silverstone <dsilvers@digital-scurf.org>",
]
-edition = "2018"
+edition = "2021"
license = "MIT-0"
description = '''A library for using Subplot code generation from another project's
`build.rs` module.'''
-homepage = "https://subplot.liw.fi/"
+homepage = "https://subplot.tech/"
repository = "https://gitlab.com/subplot/subplot"
+rust-version = "1.70"
[dependencies]
-subplot = { version = "0.4.0", path = ".." }
+subplot = { version = "0.9.0", path = ".." }
tracing = "0.1"
tempfile = "3.1.0"
diff --git a/subplot-build/src/lib.rs b/subplot-build/src/lib.rs
index f8ed074..db47623 100644
--- a/subplot-build/src/lib.rs
+++ b/subplot-build/src/lib.rs
@@ -7,7 +7,8 @@
use std::env::var_os;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
-use subplot::{get_basedir_from, SubplotError};
+use subplot::get_basedir_from;
+pub use subplot::SubplotError;
use tracing::{event, instrument, span, Level};
/// Generate code for one document, inside `build.rs`.
@@ -54,6 +55,9 @@ where
// re-running.
let base_path = get_basedir_from(filename);
let meta = output.doc.meta();
+ for filename in meta.markdown_filenames() {
+ buildrs_deps(&base_path, Some(filename.as_path()));
+ }
buildrs_deps(&base_path, meta.bindings_filenames());
let docimpl = output
.doc
diff --git a/subplot.md b/subplot.md
index d7a6c29..76e36f2 100644
--- a/subplot.md
+++ b/subplot.md
@@ -1,22 +1,3 @@
----
-title: "Subplot"
-author: The Subplot project
-bindings:
-- subplot.yaml
-- lib/runcmd.yaml
-- lib/files.yaml
-impls:
- python:
- - subplot.py
- - lib/files.py
- - lib/runcmd.py
- rust:
- - subplotlib/subplot-rust-support.rs
-classes:
-- json
-...
-
-
# Introduction
Subplot is software to help capture and communicate acceptance
@@ -39,20 +20,20 @@ document.
We define the various concepts relevant to Subplot as follows:
-* **Acceptance criteria**: What the stakeholders require of the system
+* **Acceptance criteria:** What the stakeholders require of the system
for them to be happy with it and use it.
-* **Stakeholder**: Someone with a keen interest in the success of a
+* **Stakeholder:** Someone with a keen interest in the success of a
system. They might be a paying client, someone who uses the system,
or someone involved in developing the system. Depending on the
system and project, some stakeholders may have a bigger say than
others.
-* **Acceptance test**: How stakeholders verify that the system
+* **Acceptance test:** How stakeholders verify that the system
fulfills the acceptance criteria, in an automated way. Some criteria
may not be possible to verify automatically.
-* **Scenario**: In Subplot, the acceptance criteria are written as
+* **Scenario:** In Subplot, the acceptance criteria are written as
freeform prose, with diagrams, etc. The scenarios, which are
embedded blocks of Subplot scenario language, capture the mechanisms
of verifying that criteria are met - the acceptance tests - showing
@@ -82,7 +63,7 @@ technical text that's aimed at all your stakeholders.
## Subplot architecture
Subplot reads an input document, in Markdown, and generates a typeset
-output document, as PDF or HTML, for all stakeholders to understand.
+output document, as HTML, for all stakeholders to understand.
Subplot also generates a test program, in Python, that verifies the
acceptance criteria are met, for developers and testers and auditors
to verify the system under test meets its acceptance criteria. The
@@ -104,9 +85,6 @@ impl [shape=box];
subplot [label="Subplot"];
subplot [shape=ellipse];
-pdf [label="foo.pdf \n PDF (generated)"]
-pdf [shape=note];
-
html [label="foo.html \n HTML (generated)"]
html [shape=note];
@@ -119,26 +97,19 @@ report [shape=note];
md -> subplot;
bindings -> subplot;
impl -> subplot;
-subplot -> pdf;
subplot -> html;
subplot -> testprog;
testprog -> report;
}
```
-[Pandoc]: https://pandoc.org/
-
-Subplot uses the [Pandoc][] software for generating PDF and HTML
-output documents. In fact, any output format supported by Pandoc can
-be requested by the user. Depending on the output format, Pandoc may
-use, for example, LaTeX. Subplot interprets parts of the Markdown
-input file itself.
+Subplot generated HTML itself.
Subplot actually consists mainly of two separate programs:
**subplot docgen** for generating output documents, and **subplot codegen** for
generating the test program. There are a couple of additional tools
(**subplot metadata** for reporting meta data about a Subplot document, and
-**subplot-filter** for doing the document generation as a Pandoc filter).
+**subplot extract** for extracting embedded files from a subplot document.
Thus a more detailed architecture view is shown below.
@@ -159,9 +130,6 @@ docgen [shape=ellipse];
codegen [label="subplot codegen"];
codegen [shape=ellipse];
-pdf [label="foo.pdf \n PDF (generated)"]
-pdf [shape=note];
-
html [label="foo.html \n HTML (generated)"]
html [shape=note];
@@ -176,7 +144,6 @@ bindings -> docgen;
md -> codegen;
bindings -> codegen;
impl -> codegen;
-docgen -> pdf;
docgen -> html;
codegen -> testprog;
testprog -> report;
@@ -334,56 +301,56 @@ tests for Subplot](#acceptance).
Each requirement here is given a unique mnemonic id for easier
reference in discussions.
-**UnderstandableTests**
+* **UnderstandableTests**
-: Acceptance tests should be possible to express in a way that's
- easily understood by all stakeholders, including those who are
- not software developers.
+ Acceptance tests should be possible to express in a way that's
+ easily understood by all stakeholders, including those who are not
+ software developers.
_Done_ but requires the Subplot document to be written with care.
-**EasyToWriteDocs**
+* **EasyToWriteDocs**
-: The markup language for writing documentation should be easy to
+ The markup language for writing documentation should be easy to
write.
_Done_ by using Markdown.
-**AidsComprehension**
+* **AidsComprehension**
-: The formatted human-readable documentation should use good layout
+ The formatted human-readable documentation should use good layout
and typography to enhance comprehension.
- _In progress_ &mdash; typesetting via Pandoc works, but may need
- review and improvement.
+ _In progress_ &mdash; we currently only output HTML, but may add
+ PDF output back later.
-**CodeSeparately**
+* **CodeSeparately**
-: The code to implement the acceptance criteria should not be
+ The code to implement the acceptance criteria should not be
embedded in the documentation source, but be in separate files.
This makes it easier to edit without specialised tooling.
_Done_ by keeping scenario step implementations in a separate
file.
-**AnyProgammingLanguage**
+* **AnyProgammingLanguage**
-: The developers implementing the acceptance tests should be free to
+ The developers implementing the acceptance tests should be free to
use a language they're familiar and comfortable with. Subplot
should not require them to use a specific language.
_Not done_ &mdash; only Python supported at the moment.
-**FastTestExecution**
+* **FastTestExecution**
-: Executing the acceptance tests should be fast.
+ Executing the acceptance tests should be fast.
_Not done_ &mdash; the generated Python test program is simplistic
and linear.
-**NoDeployment**
+* **NoDeployment**
-: The acceptance test tooling should assume the system under test is
+ The acceptance test tooling should assume the system under test is
already deployed and available. Deploying is too big of a problem
space to bring into the scope of acceptance testing, and there are
already good tools for deployment.
@@ -391,9 +358,9 @@ reference in discussions.
_Done_ by virtue of letting those who implement the scenario steps
worry about it.
-**MachineParseableResults**
+* **MachineParseableResults**
-: The tests should produce a machine parseable result that can be
+ The tests should produce a machine parseable result that can be
archived, post-processed, and analyzed in ways that are of
interest to the project using Subplot. For example, to see trends
in how long tests take, how often tests fail, to find regressions,
@@ -406,16 +373,13 @@ reference in discussions.
Subplot reads three input files, each in a different format:
-* The document file, which uses the Markdown dialects understood by
- Pandoc.
+* The document file in [GitHub Flavored Markdown](https://github.github.com/gfm/).
* The bindings file, in YAML.
* The functions file, in Bash or Python.
Subplot interprets marked parts of the input document
-specially. It does this via the Pandoc abstract syntax tree, rather
-than text manipulation, and thus anything that Pandoc understands is
-understood by Subplot. We will not specify Pandoc's dialect of
-Markdown here, only the parts Subplot pays attention to.
+specially. These are fenced code blocks tagged with the `sceanrio`,
+`file`, or `example` classes.
## Scenario language
@@ -540,14 +504,9 @@ will deal with formatting that nicely for you.
## Document markup
-[Pandoc]: https://pandoc.org/
+Subplot parses Markdown input files using GitHub-flavored Markdown.
-Subplot uses [Pandoc][], the universal document converter, to parse
-the Markdown file, and thus understands the variants of Markdown that
-Pandoc supports. This includes traditional Markdown, CommonMark, and
-GitHub-flavored Markdown.
-
-[fenced code blocks]: https://pandoc.org/MANUAL.html#fenced-code-blocks
+[fenced code blocks]: https://github.github.com/gfm/#fenced-code-blocks
Subplot extends Markdown by treating certain certain tags for [fenced
code blocks][] specially. A scenario, for example, would look like
@@ -569,6 +528,11 @@ block for the test program. Snippets under the same heading belong
together; the next heading of the same or a higher level ends the
scenario.
+For `scenario` blocks you may not use any attributes. All attributes
+are reserved for Subplot. Subplot doesn't define any attributes yet,
+but by reserving all of them, it can add them later without it being
+a breaking change.
+
For embedding test data files in the Markdown document, Subplot
understands the `file` tag:
@@ -580,11 +544,7 @@ This data is accessible to the test program as 'filename'.
The `.file` attribute is necessary, as is the identifier, here
`#filename`. The generated test program can access the data using the
-identifier (without the #). The mechanism used is generic to Pandoc,
-and can be used to affect the typesetting by adding more attributes.
-For example, Pandoc can typeset the data in the code block using
-syntax highlighting, if the language is specified: `.markdown`,
-`.yaml`, or `.python`, for example.
+identifier (without the #).
Subplot also understands the `dot` and `roadmap` tags, and can use the
Graphviz dot program, or the [roadmap][] Rust crate, to produce
@@ -640,30 +600,23 @@ given file not-numbered-lines.txt
## Document metadata
-Pandoc supports, and Subplot makes use of, a [YAML metadata block][] in a
-Markdown document. This can and should be used to set the document
-title, authors, date (version), and can be used to control some of the
-typesetting. Crucially for Subplot, the bindings and functions files
-are named in the metadata block, rather than Subplot deriving them
-from the input file name.
-
-[YAML metadata block]: https://pandoc.org/MANUAL.html#extension-yaml_metadata_block
-
-As an example, the metadata block for the Subplot document might look
-as follows. The `---` before and `...` after the block are mandatory:
-they are how Pandoc recongizes the block.
+Document metadata is read from a YAML file. This can used to set the
+document title, authors, date (version), and more. Crucially for
+Subplot, the bindings and functions files are named in the metadata
+block, rather than Subplot deriving them from the input file name.
-~~~{.yaml .numberLines}
----
+~~~{.file .yaml .numberLines}
title: "Subplot"
-author: The Subplot project
+authors:
+- The Subplot project
date: work in progress
+markdowns:
+- subplot.md
bindings:
- subplot.yaml
impls:
python:
- subplot.py
-...
~~~
There can be more than one bindings or functions file: use a YAML
@@ -734,6 +687,24 @@ implementation functions:
* A binding for a "then everything is OK" step, which captures nothing,
and calls the `check_everything_is_ok` function.
+## Step functions and cleanup
+
+A step function must be atomic: either it completes successfully, or
+it cleans up any changes it made before returning an indication of
+failure.
+
+A cleanup function is only called for successfully executed step
+functions.
+
+For example, consider a step that creates and starts a virtual
+machine. The step function creates the VM, then starts it, and if both
+actions succeeds, the step succeeds. A cleanup function for that step
+will stop and delete the VM. The cleanup is only called if the step
+succeeded. If the step function manages to create the VM, but not
+start it, it's the step function's responsibility to delete the VM,
+before it signals failure. The cleanup function won't be called in
+that case.
+
### Simple patterns
The simple patterns are of the form `{name}` and match a single word
@@ -790,7 +761,7 @@ will emit a warning if the file is not found, and subplot codegen will emit an e
Bindings can contain an `impl` map which connects the binding with zero or more
language templates. If a binding has no `impl` entries then it can still be
-used to `docgen` a PDF or HTML document from a subplot document. This permits a
+used to `docgen` a HTML document from a subplot document. This permits a
workflow where requirements owners / architects design the validations for a
project and then engineers implement the step functions to permit the
validations to work.
@@ -815,24 +786,24 @@ Here is an example of a binding from one of those libraries:
### Embedded file name didn't match
```scenario
+given file badfilename.subplot
given file badfilename.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen --merciful badfilename.md -o foo.pdf
-then file foo.pdf exists
when I try to run subplot codegen --run badfilename.md -o test.py
then command fails
```
-~~~{#badfilename.md .file .markdown .numberLines}
----
+~~~{#badfilename.subplot .file .yaml .numberLines}
title: Bad filenames in matched steps do not permit codegen
+markdowns: [badfilename.md]
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~
+~~~{#badfilename.md .file .markdown .numberLines}
# Bad filename
```scenario
@@ -841,6 +812,74 @@ given file missing.md
~~~
+### Bindings file strictness - given when then
+
+The bindings file is semi-strict. For example you must have only one
+of `given`, `when`, or `then` in your binding.
+
+
+```scenario
+given file badbindingsgwt.subplot
+and file badbindingsgwt.md
+and file badbindingsgwt.yaml
+and an installed subplot
+when I try to run subplot docgen --output ignored.html badbindingsgwt.subplot
+then command fails
+and stderr contains "binding has more than one keyword"
+```
+
+~~~{#badbindingsgwt.subplot .file .yaml .numberLines}
+title: Bad bindings cause everything to fail
+markdowns: [badbindingsgwt.md]
+bindings: [badbindingsgwt.yaml]
+~~~
+
+~~~{#badbindingsgwt.md .file .markdown .numberLines}
+# Bad bindings
+```scenario
+given we won't reach here
+```
+~~~
+
+~~~{#badbindingsgwt.yaml .file .yaml .numberLines}
+- given: we won't reach here
+ then: we won't reach here
+~~~
+
+### Bindings file strictness - unknown field
+
+The bindings file is semi-strict. For example, you must not have keys
+in the bindings file which are not known to Subplot.
+
+
+```scenario
+given file badbindingsuf.subplot
+and file badbindingsuf.md
+and file badbindingsuf.yaml
+and an installed subplot
+when I try to run subplot docgen --output ignored.html badbindingsuf.subplot
+then command fails
+and stderr contains "unknown field `function`"
+```
+
+~~~{#badbindingsuf.subplot .file .yaml .numberLines}
+title: Bad bindings cause everything to fail
+markdowns: [badbindingsuf.md]
+bindings: [badbindingsuf.yaml]
+~~~
+
+~~~{#badbindingsuf.md .file .markdown .numberLines}
+# Bad bindings
+```scenario
+given we won't reach here
+```
+~~~
+
+~~~{#badbindingsuf.yaml .file .yaml .numberLines}
+- given: we won't reach here
+ function: old_school_function
+~~~
+
## Functions file
Functions implementing steps are supported in Bash and Python. The
@@ -1010,14 +1049,16 @@ They're separate from the scenarios so that the scenarios are shorter
and clearer, but also so that the input files do not need to be
duplicated for each scenario.
-~~~~{#simple.md .file .markdown .numberLines}
----
+~~~~{#simple.subplot .file .yaml .numberLines}
title: Test scenario
+markdowns:
+- simple.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~~
+~~~~{#simple.md .file .markdown .numberLines}
# Simple
This is the simplest possible test scenario
@@ -1088,21 +1129,20 @@ def foobar_was_done(ctx):
### Smoke test
The scenario below uses the input files defined above to run some tests
-to verify that Subplot can build a PDF and an HTML document, and
+to verify that Subplot can build an HTML document, and
execute a simple scenario successfully. The test is based on
generating the test program from an input file, running the test
program, and examining the output.
~~~scenario
+given file simple.subplot
given file simple.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf exists
-when I run subplot docgen simple.md -o simple.html
+when I run subplot docgen simple.subplot -o simple.html
then file simple.html exists
-when I run subplot codegen --run simple.md -o test.py
+when I run subplot codegen --run simple.subplot -o test.py
then scenario "Simple" was run
and step "given precondition foo" was run
and step "when I do bar" was run
@@ -1110,31 +1150,153 @@ and step "then bar was done" was run
and command is successful
~~~
+## Indented scenario steps are not allowed
+
+_Requirement: A scenario step starts at the beginning of the line._
+
+Justification: We may want to allow continuing a step to the next
+line, but as of June, 2023, we haven't settled on a syntax for this.
+However, whatever syntax we do eventually choose, it will be easier
+to add that if scenario steps start at the beginning of a line,
+without making a breaking change.
+
+~~~scenario
+given file indented-step.subplot
+given file indented-step.md
+given file b.yaml
+given an installed subplot
+when I try to run subplot docgen indented-step.subplot -o foo.html
+then command fails
+and stderr contains "indented"
+~~~
+
+~~~{#indented-step.subplot .file .yaml .numberLines}
+title: Indented scenario step
+markdowns:
+ - indented-step.md
+bindings:
+ - b.yaml
+~~~
+
+~~~~~~{#indented-step.md .file .markdown .numberLines}
+# This is a title
+
+~~~scenario
+ given precondition
+~~~
+~~~~~~
+
+## Named code blocks must have an appropriate class
+
+_Requirement: Named code blocks must carry an appropriate class such as file or example_
+
+Justification: Eventually we may want to add other meanings to named blocks,
+currently the identifier cannot be used except to refer to the block as a named file,
+but we may want to in the future so this is here to try and prevent any future
+incompatibilities.
+
+~~~scenario
+given file named-code-blocks-appropriate.subplot
+given file named-code-blocks-appropriate.md
+given file b.yaml
+given an installed subplot
+when I try to run subplot docgen named-code-blocks-appropriate.subplot -o foo.html
+then command fails
+and stderr contains "#example-1 at named-code-blocks-appropriate.md:7:1"
+and stderr doesn't contain "example-2"
+and stderr doesn't contain "example-3"
+~~~
+
+~~~{#named-code-blocks-appropriate.subplot .file .yaml .numberLines}
+title: Named code blocks carry appropriate classes step
+markdowns:
+ - named-code-blocks-appropriate.md
+bindings:
+ - b.yaml
+~~~
+
+~~~~~~{#named-code-blocks-appropriate.md .file .markdown .numberLines}
+# This is a title
+
+~~~scenario
+given precondition
+~~~
+
+~~~{#example-1 .numberLines}
+This example is bad
+~~~
+
+~~~{#example-2 .file .numberLines}
+This example is OK because of .file
+~~~
+
+~~~{#example-3 .example .numberLines}
+This example is OK because of .example
+~~~
+
+~~~~~~
+
## No scenarios means codegen fails
If you attempt to `subplot codegen` on a document which contains no scenarios, the
tool will fail to execute with a reasonable error message.
~~~scenario
+given file noscenarios.subplot
given file noscenarios.md
and an installed subplot
-when I try to run subplot codegen noscenarios.md -o test.py
+when I try to run subplot codegen noscenarios.subplot -o test.py
then command fails
and stderr contains "no scenarios were found"
~~~
-~~~{#noscenarios.md .file .markdown .numberLines}
----
+~~~{#noscenarios.subplot .file .yaml .numberLines}
title: No scenarios in here
+markdowns: [noscenarios.md]
impls: { python: [] }
-...
+~~~
+~~~{#noscenarios.md .file .markdown .numberLines}
# This is a title
But there are no scenarios in this file, and thus nothing can be generated in a test suite.
~~~
+## No template means you can docgen but not codegen
+
+When running `docgen` you do not **need** a template to have been defined in the
+subplot input document. If you have template-specific bindings then you **should**
+provide one, but if not, then it is unnecessary. This means you can use `docgen`
+to build documents before you have any inkling of the implementation language
+necessary to validate the scenarios.
+
+~~~scenario
+given file notemplate.subplot
+given file notemplate.md
+and an installed subplot
+when I run subplot docgen notemplate.subplot -o notemplate.html
+then file notemplate.html exists
+when I try to run subplot codegen notemplate.subplot -o test.py
+then command fails
+and stderr contains "document has no template"
+~~~
+
+~~~{#notemplate.subplot .file .yaml .numberLines}
+title: No templates in here
+markdowns: [notemplate.md]
+impls: { }
+~~~
+
+~~~{#notemplate.md .file .markdown .numberLines}
+# This is a title
+
+```scenario
+then failure ensues
+```
+
+~~~
+
## Keywords
Subplot supports the keywords **given**, **when**, and **then**, and
@@ -1146,13 +1308,12 @@ combinations.
### All the keywords
~~~scenario
+given file allkeywords.subplot
given file allkeywords.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen allkeywords.md -o foo.pdf
-then file foo.pdf exists
-when I run subplot codegen --run allkeywords.md -o test.py
+when I run subplot codegen --run allkeywords.subplot -o test.py
then scenario "All keywords" was run
and step "given precondition foo" was run
and step "when I do bar" was run
@@ -1160,14 +1321,16 @@ and step "then bar was done" was run
and command is successful
~~~
-~~~{#allkeywords.md .file .markdown .numberLines}
----
+~~~{#allkeywords.subplot .file .yaml .numberLines}
title: All the keywords scenario
+markdowns:
+- allkeywords.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~
+~~~{#allkeywords.md .file .markdown .numberLines}
# All keywords
This uses all the keywords.
@@ -1181,6 +1344,7 @@ but foobar was done
```
~~~
+<!-- disabled until Lars fixes typesetting of scenarios
### Keyword aliases in output
We support **and** and **but** in input lines, and we always render
@@ -1188,11 +1352,12 @@ scenarios in output so they are used when they are allowed. This
scenario verifies that this happens.
~~~scenario
+given file aliases.subplot
given file aliases.md
given file b.yaml
given file f.py
given an installed subplot
-when I run subplot docgen --merciful aliases.md -o aliases.html
+when I run subplot docgen --merciful aliases.subplot -o aliases.html
then command is successful
then file aliases.html matches regex /given<[^>]*> precondition foo/
then file aliases.html matches regex /when<[^>]*> I do bar/
@@ -1201,13 +1366,16 @@ then file aliases.html matches regex /then<[^>]*> bar was done/
then file aliases.html matches regex /and<[^>]*> foobar was done/
~~~
-~~~{#aliases.md .file .markdown .numberLines}
----
+~~~{#aliases.subplot .file .yaml .numberLines}
title: Keyword aliases
+markdowns:
+- aliases.md
bindings: [b.yaml]
-functions: [f.py]
-...
+impls:
+ python: [f.py]
+~~~
+~~~{#aliases.md .file .markdown .numberLines}
# Aliases
```scenario
@@ -1218,6 +1386,7 @@ then bar was done
then foobar was done
```
~~~
+-->
### Misuse of continuation keywords
@@ -1227,24 +1396,25 @@ subplot will be unable to determine what kind of keyword they are meant to
be continuing.
~~~scenario
+given file continuationmisuse.subplot
given file continuationmisuse.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen continuationmisuse.md -o foo.pdf
-then file foo.pdf exists
-when I try to run subplot codegen --run continuationmisuse.md -o test.py
+when I try to run subplot codegen --run continuationmisuse.subplot -o test.py
then command fails
~~~
-~~~{#continuationmisuse.md .file .markdown .numberLines}
----
+~~~{#continuationmisuse.subplot .file .yaml .numberLines}
title: Continuation keyword misuse
+markdowns:
+- continuationmisuse.subplot
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~
+~~~{#continuationmisuse.md .file .markdown .numberLines}
# Continuation keyword misuse
This scenario should fail to parse because we misuse a
@@ -1264,36 +1434,113 @@ It is OK to use markup in document titles, in the YAML metadata
section. This scenario verifies that all markup works.
~~~scenario
+given file title-markup.subplot
given file title-markup.md
given an installed subplot
-when I run subplot docgen title-markup.md -o foo.pdf
-then file foo.pdf exists
+when I run subplot docgen title-markup.subplot -o foo.html
+then file foo.html exists
~~~
-~~~~{#title-markup.md .file .markdown .numberLines}
----
+~~~~{#title-markup.subplot .file .yaml .numberLines}
title: This _uses_ ~~all~~ **most** inline `markup`
subtitle: H~2~O is not 2^10^
+markdowns: [title-markup.md]
impls: { python: [] }
-...
+~~~~
+~~~~{#title-markup.md .file .markdown .numberLines}
# Introduction
~~~~
+## Scenario titles
+
+A scenario gets its title from the lowest level of section heading
+that applies to it. The heading can use markup.
+
+~~~scenario
+given file scenario-titles.subplot
+given file scenario-titles.md
+given file b.yaml
+given file f.py
+given an installed subplot
+when I run subplot metadata scenario-titles.subplot
+then stdout contains "My fun scenario title"
+~~~
+
+~~~~{#scenario-titles.subplot .file .yaml .numberLines}
+title: Test scenario
+markdowns:
+- scenario-titles.md
+bindings: [b.yaml]
+impls:
+ python: [f.py]
+~~~~
+
+~~~~{#scenario-titles.md .file .markdown .numberLines}
+# My **fun** _scenario_ `title`
+
+```scenario
+given precondition foo
+when I do bar
+then bar was done
+```
+~~~~
+
+## Duplicate scenario titles
+
+_Requirement: Subplot treats it as an error if two scenarios have the
+same title._
+
+Justification: the title is how a scenario is identified, and the user
+needs to be able to do so unambiguously.
+
+~~~scenario
+given file duplicate-scenario-titles.subplot
+given file duplicate-scenario-titles.md
+given file b.yaml
+given file f.py
+given an installed subplot
+when I try to run subplot metadata duplicate-scenario-titles.subplot
+then command fails
+then stderr contains "duplicate"
+~~~
+
+~~~~{#duplicate-scenario-titles.subplot .file .yaml .numberLines}
+title: Test scenario
+markdowns:
+- duplicate-scenario-titles.md
+bindings: [b.yaml]
+impls:
+ python: [f.py]
+~~~~
+
+~~~~{#duplicate-scenario-titles.md .file .markdown .numberLines}
+# My sceanrio
+
+```scenario
+when I do bar
+```
+
+# My sceanrio
+
+```scenario
+when I do bar
+```
+~~~~
+
## Empty lines in scenarios
-This scenario verifies that empty lines in scenarios are ignored.
+This scenario verifies that empty lines in scenarios are OK.
~~~scenario
+given file emptylines.subplot
given file emptylines.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen emptylines.md -o emptylines.pdf
-then file emptylines.pdf exists
-when I run subplot docgen emptylines.md -o emptylines.html
+when I run subplot docgen emptylines.subplot -o emptylines.html
then file emptylines.html exists
-when I run subplot codegen --run emptylines.md -o test.py
+when I run subplot codegen --run emptylines.subplot -o test.py
then scenario "Simple" was run
and step "given precondition foo" was run
and step "when I do bar" was run
@@ -1301,14 +1548,16 @@ and step "then bar was done" was run
and command is successful
~~~
-~~~~{#emptylines.md .file .markdown .numberLines}
----
+~~~~{#emptylines.subplot .file .yaml .numberLines}
title: Test scenario
+markdowns:
+- emptylines.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~~
+~~~~{#emptylines.md .file .markdown .numberLines}
# Simple
This is the simplest possible test scenario
@@ -1397,11 +1646,12 @@ failure_cleanup() {
### Cleanup functions gets called on success (Python)
~~~scenario
+given file cleanup-success-python.subplot
given file cleanup-success-python.md
and file cleanup.yaml
and file cleanup.py
and an installed subplot
-when I run subplot codegen --run cleanup-success-python.md -o test.py
+when I run subplot codegen --run cleanup-success-python.subplot -o test.py
then scenario "Cleanup" was run
and step "given foo" was run, and then step "given bar"
and cleanup for "given bar" was run, and then for "given foo"
@@ -1409,14 +1659,17 @@ and command is successful
~~~
-~~~~~{#cleanup-success-python.md .file .markdown .numberLines}
----
+~~~~~{#cleanup-success-python.subplot .file .yaml .numberLines}
title: Cleanup
+markdowns:
+- cleanup-success-python.md
bindings: [cleanup.yaml]
impls:
python: [cleanup.py]
-...
+~~~~~
+
+~~~~~{#cleanup-success-python.md .file .markdown .numberLines}
# Cleanup
~~~scenario
@@ -1429,11 +1682,12 @@ given bar
### Cleanup functions get called on failure (Python)
~~~scenario
+given file cleanup-fail-python.subplot
given file cleanup-fail-python.md
and file cleanup.yaml
and file cleanup.py
and an installed subplot
-when I try to run subplot codegen --run cleanup-fail-python.md -o test.py
+when I try to run subplot codegen --run cleanup-fail-python.subplot -o test.py
then scenario "Cleanup" was run
and step "given foo" was run, and then step "given bar"
and cleanup for "given bar" was run, and then for "given foo"
@@ -1441,14 +1695,16 @@ and cleanup for "given failure" was not run
and command fails
~~~
-~~~~~{#cleanup-fail-python.md .file .markdown .numberLines}
----
+~~~~~{#cleanup-fail-python.subplot .file .yaml .numberLines}
title: Cleanup
+markdowns:
+- cleanup-fail-python.md
bindings: [cleanup.yaml]
impls:
python: [cleanup.py]
-...
+~~~~~
+~~~~~{#cleanup-fail-python.md .file .markdown .numberLines}
# Cleanup
~~~scenario
@@ -1462,17 +1718,27 @@ given failure
### Cleanup functions gets called on success (Bash)
~~~scenario
+given file cleanup-success-bash.subplot
given file cleanup-success-bash.md
and file cleanup.yaml
and file cleanup.sh
and an installed subplot
-when I run subplot codegen --run cleanup-success-bash.md -o test.sh
+when I run subplot codegen --run cleanup-success-bash.subplot -o test.sh
then scenario "Cleanup" was run
and step "given foo" was run, and then step "given bar"
and cleanup for "given bar" was run, and then for "given foo"
and command is successful
~~~
+~~~~~{#cleanup-success-bash.subplot .file .yaml .numberLines}
+title: Cleanup
+markdowns:
+- cleanup-success-bash.md
+bindings: [cleanup.yaml]
+impls:
+ bash: [cleanup.sh]
+~~~~~
+
~~~~~{#cleanup-success-bash.md .file .markdown .numberLines}
---
title: Cleanup
@@ -1496,11 +1762,12 @@ If a step fails, all the cleanups for the preceding steps are still
called, in reverse order.
~~~scenario
+given file cleanup-fail-bash.subplot
given file cleanup-fail-bash.md
and file cleanup.yaml
and file cleanup.sh
and an installed subplot
-when I try to run subplot codegen --run cleanup-fail-bash.md -o test.sh
+when I try to run subplot codegen --run cleanup-fail-bash.subplot -o test.sh
then scenario "Cleanup" was run
and step "given foo" was run, and then step "given bar"
and cleanup for "given bar" was run, and then for "given foo"
@@ -1508,14 +1775,16 @@ and cleanup for "given failure" was not run
and command fails
~~~
-~~~~~{#cleanup-fail-bash.md .file .markdown .numberLines}
----
+~~~~~{#cleanup-fail-bash.subplot .file .yaml .numberLines}
title: Cleanup
+markdowns:
+- cleanup-fail-bash.md
bindings: [cleanup.yaml]
impls:
bash: [cleanup.sh]
-...
+~~~~~
+~~~~~{#cleanup-fail-bash.md .file .markdown .numberLines}
# Cleanup
~~~scenario
@@ -1538,24 +1807,26 @@ the `TMPDIR` environment variable to point at the data directory. This
scenario verifies that it happens.
~~~scenario
+given file tmpdir.subplot
given file tmpdir.md
and file tmpdir.yaml
and file tmpdir.py
and an installed subplot
-when I run subplot codegen --run tmpdir.md -o test.py
+when I run subplot codegen --run tmpdir.subplot -o test.py
then command is successful
and scenario "TMPDIR" was run
and step "then TMPDIR is set" was run
~~~
-~~~~{#tmpdir.md .file .markdown .numberLines}
----
+~~~~{#tmpdir.subplot .file .yaml .numberLines}
title: TMPDIR
+markdowns: [tmpdir.md]
bindings: [tmpdir.yaml]
impls:
python: [tmpdir.py]
-...
+~~~~
+~~~~{#tmpdir.md .file .markdown .numberLines}
# TMPDIR
~~~scenario
@@ -1589,25 +1860,28 @@ can be done using regular expressions or "simple patterns".
### Capture using simple patterns
~~~scenario
+given file simplepattern.subplot
given file simplepattern.md
and file simplepattern.yaml
and file capture.py
and an installed subplot
-when I run subplot codegen --run simplepattern.md -o test.py
+when I run subplot codegen --run simplepattern.subplot -o test.py
then scenario "Simple pattern" was run
and step "given I am Tomjon" was run
and stdout contains "function got argument name as Tomjon"
and command is successful
~~~
-~~~~{#simplepattern.md .file .markdown .numberLines}
----
+~~~~{#simplepattern.subplot .file .yaml .numberLines}
title: Simple pattern capture
+markdowns:
+- simplepattern.md
bindings: [simplepattern.yaml]
impls:
python: [capture.py]
-...
+~~~~
+~~~~{#simplepattern.md .file .markdown .numberLines}
# Simple pattern
~~~scenario
@@ -1635,23 +1909,26 @@ expression meta characters unless the rule is explicitly marked as not
being a regular expression pattern.
~~~scenario
+given file confusedpattern.subplot
given file confusedpattern.md
and file confusedpattern.yaml
and file capture.py
and an installed subplot
-when I try to run subplot codegen --run confusedpattern.md -o test.py
+when I try to run subplot codegen --run confusedpattern.subplot -o test.py
then command fails
and stderr contains "simple pattern contains regex"
~~~
-~~~~{#confusedpattern.md .file .markdown .numberLines}
----
+~~~~{#confusedpattern.subplot .file .yaml .numberLines}
title: Simple pattern capture
+markdowns:
+- confusedpattern.md
bindings: [confusedpattern.yaml]
impls:
python: [capture.py]
-...
+~~~~
+~~~~{#confusedpattern.md .file .markdown .numberLines}
# Simple pattern
~~~scenario
@@ -1669,22 +1946,25 @@ given I* am Tomjon
### Simple patterns with regex metacharacters: allowed case
~~~scenario
+given file confusedbutok.subplot
given file confusedbutok.md
and file confusedbutok.yaml
and file capture.py
and an installed subplot
-when I run subplot codegen --run confusedbutok.md -o test.py
+when I run subplot codegen --run confusedbutok.subplot -o test.py
then command is successful
~~~
-~~~~{#confusedbutok.md .file .markdown .numberLines}
----
+~~~~{#confusedbutok.subplot .file .yaml .numberLines}
title: Simple pattern capture
+markdowns:
+- confusedbutok.md
bindings: [confusedbutok.yaml]
impls:
python: [capture.py]
-...
+~~~~
+~~~~{#confusedbutok.md .file .markdown .numberLines}
# Simple pattern
~~~scenario
@@ -1703,25 +1983,28 @@ given I* am Tomjon
### Capture using regular expressions
~~~scenario
+given file regex.subplot
given file regex.md
and file regex.yaml
and file capture.py
and an installed subplot
-when I run subplot codegen --run regex.md -o test.py
+when I run subplot codegen --run regex.subplot -o test.py
then scenario "Regex" was run
and step "given I am Tomjon" was run
and stdout contains "function got argument name as Tomjon"
and command is successful
~~~
-~~~~{#regex.md .file .markdown .numberLines}
----
+~~~~{#regex.subplot .file .yaml .numberLines}
title: Regex capture
+markdowns:
+- regex.md
bindings: [regex.yaml]
impls:
python: [capture.py]
-...
+~~~~
+~~~~{#regex.md .file .markdown .numberLines}
# Regex
~~~scenario
@@ -1753,31 +2036,32 @@ expansions don't accidentally refer to values meant for another
purpose.
~~~scenario
+given file values.subplot
given file values.md
and file values.yaml
and file values.py
and an installed subplot
-when I run subplot codegen values.md -o test.py
+when I run subplot codegen values.subplot -o test.py
when I run python3 test.py
then command is successful
~~~
-~~~~~~{#values.md .file .markdown .numberLines}
----
+~~~~~~{#values.subplot .file .yaml .numberLines}
title: Values
+markdowns:
+- values.md
bindings: [values.yaml]
impls:
python: [values.py]
-...
-
+~~~~~~
+~~~~~~{#values.md .file .markdown .numberLines}
# Values
~~~scenario
when I remember foo as bar
then expanded "${foo}" is bar
~~~
-
~~~~~~
~~~{#values.yaml .file .yaml .numberLines}
@@ -1823,11 +2107,12 @@ There is currently no equivalent functionality for the generated Bash
test program. Patches for that are welcome.
~~~scenario
+given file env.subplot
given file env.md
and file env.yaml
and file env.py
and an installed subplot
-when I run subplot codegen env.md -o test.py
+when I run subplot codegen env.subplot -o test.py
when I try to run python3 test.py
then command fails
when I try to run python3 test.py --env FOO=foo
@@ -1836,14 +2121,16 @@ when I try to run python3 test.py --env FOO=bar
then command is successful
~~~
-~~~~~~{#env.md .file .markdown .numberLines}
----
+~~~~~~{#env.subplot .file .yaml .numberLines}
title: Environment variables
+markdowns:
+- env.md
bindings: [env.yaml]
impls:
python: [env.py]
-...
+~~~~~~
+~~~~~~{#env.md .file .markdown .numberLines}
# Test
~~~scenario
then environment variable FOO is set to "bar"
@@ -1866,77 +2153,6 @@ def is_set_to(ctx, name=None, value=None):
-## Avoid changing typesetting output file needlessly
-
-### Avoid typesetting if output is newer than source files
-
-This scenario make sure that if docgen generates the bitwise identical
-output to the existing output file, it doesn't actually write it to
-the output file, including its timestamp. This avoids triggering
-programs that monitor the output file for changes.
-
-~~~scenario
-given file simple.md
-and file b.yaml
-and file f.py
-and an installed subplot
-when I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf exists
-when I remember metadata for file simple.pdf
-and I wait until 1 second has passed
-and I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf has same metadata as before
-and only files simple.md, b.yaml, f.py, simple.pdf exist
-~~~
-
-### Do typeset if output is older than markdown
-
-~~~scenario
-given file simple.md
-and file b.yaml
-and file f.py
-and an installed subplot
-when I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf exists
-when I remember metadata for file simple.pdf
-and I wait until 1 second has passed
-and I touch file simple.md
-and I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf has changed from before
-~~~
-
-### Do typeset if output is older than functions
-
-~~~scenario
-given file simple.md
-and file b.yaml
-and file f.py
-and an installed subplot
-when I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf exists
-when I remember metadata for file simple.pdf
-and I wait until 1 second has passed
-and I touch file f.py
-and I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf has changed from before
-~~~
-
-### Do typeset if output is older than bindings
-
-~~~scenario
-given file simple.md
-and file b.yaml
-and file f.py
-and an installed subplot
-when I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf exists
-when I remember metadata for file simple.pdf
-and I wait until 1 second has passed
-and I touch file b.yaml
-and I run subplot docgen simple.md -o simple.pdf
-then file simple.pdf has changed from before
-~~~
-
## Document structure
Subplot uses chapters and sections to keep together scenario snippets
@@ -1949,24 +2165,26 @@ higher level starts a new scenario.
### Lowest level heading is name of scenario
~~~scenario
+given file scenarioislowest.subplot
given file scenarioislowest.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot codegen --run scenarioislowest.md -o test.py
+when I run subplot codegen --run scenarioislowest.subplot -o test.py
then scenario "heading 1.1.1" was run
and command is successful
~~~
-~~~~{#scenarioislowest.md .file .markdown .numberLines}
-
----
+~~~~{#scenarioislowest.subplot .file .yaml .numberLines}
title: Test scenario
+markdowns:
+- scenarioislowest.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~~
+~~~~{#scenarioislowest.md .file .markdown .numberLines}
# heading 1
## heading 1.1
### heading 1.1.1
@@ -1979,24 +2197,26 @@ given precondition foo
### Subheadings don't start new scenario
~~~scenario
+given file subisnotnewscenario.subplot
given file subisnotnewscenario.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot codegen --run subisnotnewscenario.md -o test.py
+when I run subplot codegen --run subisnotnewscenario.subplot -o test.py
then scenario "heading 1.1a" was run
and command is successful
~~~
-~~~~{#subisnotnewscenario.md .file .markdown .numberLines}
-
----
+~~~~{#subisnotnewscenario.subplot .file .yaml .numberLines}
title: Test scenario
+markdowns:
+- subisnotnewscenario.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~~
+~~~~{#subisnotnewscenario.md .file .markdown .numberLines}
# heading 1
## heading 1.1a
@@ -2006,31 +2226,33 @@ given precondition foo
### heading 1.1.1
### heading 1.1.2
-
~~~~
### Next heading at same level starts new scenario
~~~scenario
+given file samelevelisnewscenario.subplot
given file samelevelisnewscenario.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot codegen --run samelevelisnewscenario.md -o test.py
+when I run subplot codegen --run samelevelisnewscenario.subplot -o test.py
then scenario "heading 1.1.1" was run
and scenario "heading 1.1.2" was run
and command is successful
~~~
-~~~~{#samelevelisnewscenario.md .file .markdown .numberLines}
-
----
+~~~~{#samelevelisnewscenario.subplot .file .yaml .numberLines}
title: Test scenario
+markdowns:
+- samelevelisnewscenario.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~~
+
+~~~~{#samelevelisnewscenario.md .file .markdown .numberLines}
# heading 1
## heading 1.1
### heading 1.1.1
@@ -2049,25 +2271,27 @@ given precondition foo
### Next heading at higher level starts new scenario
~~~scenario
+given file higherisnewscenario.subplot
given file higherisnewscenario.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot codegen --run higherisnewscenario.md -o test.py
+when I run subplot codegen --run higherisnewscenario.subplot -o test.py
then scenario "heading 1.1.1" was run
and scenario "heading 1.2" was run
and command is successful
~~~
-~~~~{#higherisnewscenario.md .file .markdown .numberLines}
-
----
+~~~~{#higherisnewscenario.subplot .file .yaml .numberLines}
title: Test scenario
+markdowns:
+- higherisnewscenario.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~~
+~~~~{#higherisnewscenario.md .file .markdown .numberLines}
# heading 1
## heading 1.1
### heading 1.1.1
@@ -2087,30 +2311,29 @@ given precondition foo
The document and code generators require a document title, because
it's a common user error to not have one, and Subplot should help make
-good documents. The Pandoc filter, however, mustn't require a document
-title, because it's used for things like formatting websites using
-ikiwiki, and ikiwiki has a different way of specifying page titles.
+good documents.
#### Document generator gives an error if input document lacks title
~~~scenario
+given file notitle.subplot
given file notitle.md
and an installed subplot
-when I try to run subplot docgen notitle.md -o foo.md
+when I try to run subplot docgen notitle.subplot -o foo.md
then command fails
~~~
-~~~{#notitle.md .file .markdown .numberLines}
----
+~~~{#notitle.subplot .file .yaml .numberLines}
+markdowns:
+- notitle.md
bindings: [b.yaml]
functions: [f.py]
-...
-
+~~~
+~~~{#notitle.md .file .markdown .numberLines}
# Introduction
-This is a very simple Markdown file without a YAML metadata block,
-and thus also no document title.
+This is a very simple Markdown file without a document title.
```scenario
given precondition foo
@@ -2121,9 +2344,10 @@ then bar was done
#### Code generator gives an error if input document lacks title
~~~scenario
+given file notitle.subplot
given file notitle.md
and an installed subplot
-when I try to run subplot codegen --run notitle.md -o test.py
+when I try to run subplot codegen --run notitle.subplot -o test.py
then command fails
~~~
@@ -2134,25 +2358,27 @@ Markdown allows using any inline markup in document titles and chapter
and section headings. Verify that Subplot accepts them.
~~~scenario
+given file fancytitle.subplot
given file fancytitle.md
and file b.yaml
and file f.py
and an installed subplot
-when I try to run subplot docgen fancytitle.md -o foo.md
+when I try to run subplot docgen fancytitle.subplot -o foo.md
then command is successful
-when I try to run subplot codegen fancytitle.md -o foo.md
+when I try to run subplot codegen fancytitle.subplot -o foo.md
then command is successful
~~~
-~~~~~~{#fancytitle.md .file .markdown .numberLines}
----
+~~~~~~{#fancytitle.subplot .file .yaml .numberLines}
title: Plain *emph* **strong** ~~strikeout~~ superscript^10^ subscript~10~
+markdowns:
+- fancytitle.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
-
+~~~~~~
+~~~~~~{#fancytitle.md .file .markdown .numberLines}
# `code` [smallcaps]{.smallcaps} $$2^10$$
## "double quoted"
@@ -2168,11 +2394,11 @@ This is a very simple Markdown file that uses every kind of inline
markup in the title and chapter heading.
To satisfy codegen, we *MUST* have a scenario here
+
~~~~scenario
when I do bar
then bar was done
~~~~
-
~~~~~~
@@ -2188,25 +2414,28 @@ This verifies that the generated Python test program can run only
chosen scenarios.
~~~scenario
+given file twoscenarios-python.subplot
given file twoscenarios-python.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot codegen twoscenarios-python.md -o test.py
+when I run subplot codegen twoscenarios-python.subplot -o test.py
and I run python3 test.py on
then scenario "One" was run
and scenario "Two" was not run
and command is successful
~~~
-~~~{#twoscenarios-python.md .file .markdown .numberLines}
----
+~~~{#twoscenarios-python.subplot .file .yaml .numberLines}
title: Test scenario
+markdowns:
+- twoscenarios-python.md
bindings: [b.yaml]
impls:
python: [f.py]
-...
+~~~
+~~~{#twoscenarios-python.md .file .markdown .numberLines}
# One
```scenario
@@ -2230,11 +2459,12 @@ This verifies that the generated Bash test program can run only
chosen scenarios.
~~~scenario
+given file twoscenarios-bash.subplot
given file twoscenarios-bash.md
and file b.yaml
and file f.sh
and an installed subplot
-when I run subplot codegen twoscenarios-bash.md -o test.sh
+when I run subplot codegen twoscenarios-bash.subplot -o test.sh
and I run bash test.sh on
then scenario "One" was run
and scenario "Two" was not run
@@ -2242,14 +2472,16 @@ and command is successful
~~~
-~~~{#twoscenarios-bash.md .file .markdown .numberLines}
----
+~~~{#twoscenarios-bash.subplot .file .yaml .numberLines}
title: Test scenario
+markdowns:
+- twoscenarios-bash.md
bindings: [b.yaml]
impls:
bash: [f.sh]
-...
+~~~
+~~~{#twoscenarios-bash.md .file .markdown .numberLines}
# One
```scenario
@@ -2322,9 +2554,11 @@ This scenario tests that the `date` field in metadata is used if
specified.
~~~scenario
+given file metadate.subplot
given file metadate.md
and an installed subplot
-when I run subplot docgen metadate.md -o metadate.html
+when I run subplot docgen metadate.subplot -o metadate.html
+when I run cat metadate.html
then file metadate.html exists
and file metadate.html contains "<title>The Fabulous Title</title>"
and file metadate.html contains "Alfred Pennyworth"
@@ -2332,14 +2566,17 @@ and file metadate.html contains "Geoffrey Butler"
and file metadate.html contains "WIP"
~~~
-~~~{#metadate.md .file .markdown .numberLines}
----
+~~~{#metadate.subplot .file .yaml .numberLines}
title: The Fabulous Title
-author:
+authors:
- Alfred Pennyworth
- Geoffrey Butler
date: WIP
-...
+markdowns:
+- metadate.md
+~~~
+
+~~~{#metadate.md .file .markdown .numberLines}
# Introduction
This is a test document. That's all.
~~~
@@ -2349,9 +2586,10 @@ This is a test document. That's all.
This scenario tests that the `--date` command line option is used.
~~~scenario
+given file dateless.subplot
given file dateless.md
and an installed subplot
-when I run subplot docgen dateless.md -o dateoption.html --date=FANCYDATE
+when I run subplot docgen dateless.subplot -o dateoption.html --date=FANCYDATE
then file dateoption.html exists
and file dateoption.html contains "<title>The Fabulous Title</title>"
and file dateoption.html contains "Alfred Pennyworth"
@@ -2359,13 +2597,16 @@ and file dateoption.html contains "Geoffrey Butler"
and file dateoption.html contains "FANCYDATE"
~~~
-~~~{#dateless.md .file .markdown .numberLines}
----
+~~~{#dateless.subplot .file .yaml .numberLines}
title: The Fabulous Title
-author:
+authors:
- Alfred Pennyworth
- Geoffrey Butler
-...
+markdowns:
+- dateless.md
+~~~
+
+~~~{#dateless.md .file .markdown .numberLines}
# Introduction
This is a test document. It has no date metadata.
~~~
@@ -2378,10 +2619,11 @@ modification time of the input file, and shall have the date in ISO
8601 format, with time to the minute.
~~~scenario
+given file dateless.subplot
given file dateless.md
and file dateless.md has modification time 2020-02-26 07:53:17
and an installed subplot
-when I run subplot docgen dateless.md -o mtime.html
+when I run subplot docgen dateless.subplot -o mtime.html
then file mtime.html exists
and file mtime.html contains "<title>The Fabulous Title</title>"
and file mtime.html contains "Alfred Pennyworth"
@@ -2395,18 +2637,23 @@ If a bindings file is missing, the error message should name the
missing file.
~~~scenario
+given file missing-binding.subplot
given file missing-binding.md
and an installed subplot
-when I try to run subplot docgen missing-binding.md -o foo.html
+when I try to run subplot docgen missing-binding.subplot -o foo.html
then command fails
-and stderr contains ": missing-binding.yaml:"
+and stderr contains "could not be found"
+and stderr contains "missing-binding.yaml"
~~~
-~~~{#missing-binding.md .file .markdown .numberLines}
----
+~~~{#missing-binding.subplot .file .yaml .numberLines}
title: Missing binding
+markdowns:
+- missing-binding.md
bindings: [missing-binding.yaml]
-...
+~~~
+
+~~~{#missing-binding.md .file .markdown .numberLines}
~~~
### Missing functions file
@@ -2415,23 +2662,30 @@ If a functions file is missing, the error message should name the
missing file.
~~~scenario
+given file missing-functions.subplot
given file missing-functions.md
and file b.yaml
and an installed subplot
-when I try to run subplot codegen --run missing-functions.md -o foo.py
+when I try to run subplot codegen --run missing-functions.subplot -o foo.py
then command fails
-and stderr contains ": missing-functions.py:"
+and stderr contains "could not be found"
+and stderr contains "missing-functions.py"
~~~
-~~~{#missing-functions.md .file .markdown .numberLines}
+~~~{#missing-functions.subplot .file .yaml .numberLines}
---
title: Missing functions
+markdowns:
+- missing-functions.md
bindings: [b.yaml]
impls:
python: [missing-functions.py]
...
~~~
+~~~{#missing-functions.md .file .markdown .numberLines}
+~~~
+
### Extracting metadata from a document
The **subplot metadata** program extracts metadata from a document. It is
@@ -2452,35 +2706,33 @@ This scenario check subplot metadata works. Note that it requires the bindings
or functions files.
~~~scenario
+given file images.subplot
given file images.md
and file b.yaml
and file other.yaml
and file f.py
and file other.py
-and file foo.bib
-and file bar.bib
and file expected.json
and an installed subplot
-when I run subplot metadata images.md
+when I run subplot metadata images.subplot
then stdout contains "source: images.md"
and stdout contains "source: b.yaml"
and stdout contains "source: other.yaml"
and stdout contains "source: f.py"
and stdout contains "source: other.py"
-and stdout contains "source: foo.bib"
-and stdout contains "source: bar.bib"
and stdout contains "source: image.gif"
and stdout contains "bindings: b.yaml"
and stdout contains "bindings: other.yaml"
and stdout contains "functions[python]: f.py"
-when I run subplot metadata images.md -o json
+when I run subplot metadata images.subplot -o json
then JSON output matches expected.json
~~~
-~~~{#images.md .file .markdown .numberLines}
----
+~~~{#images.subplot .file .yaml .numberLines}
title: Document refers to external images
+markdowns:
+- images.md
bindings:
- b.yaml
- other.yaml
@@ -2488,9 +2740,9 @@ impls:
python:
- f.py
- other.py
-bibliography: [foo.bib, bar.bib]
-...
+~~~
+~~~{#images.md .file .markdown .numberLines}
![alt text](image.gif)
~~~
@@ -2501,36 +2753,15 @@ bibliography: [foo.bib, bar.bib]
~~~{#other.py .file .python .numberLines}
~~~
-~~~{#foo.bib .file .numberLines}
-@book{foo2020,
- author = "James Random",
- title = "The Foo book",
- publisher = "The Internet",
- year = 2020,
- address = "World Wide Web",
-}
-~~~
-
-~~~{#bar.bib .file .numberLines}
-@book{foo2020,
- author = "James Random",
- title = "The Bar book",
- publisher = "The Internet",
- year = 2020,
- address = "World Wide Web",
-}
-~~~
-
~~~{#expected.json .file .json}
{
"title": "Document refers to external images",
"sources": [
"b.yaml",
- "bar.bib",
"f.py",
- "foo.bib",
"image.gif",
"images.md",
+ "images.subplot",
"other.py",
"other.yaml"
],
@@ -2544,27 +2775,60 @@ bibliography: [foo.bib, bar.bib]
"other.py"
]
},
- "bibliographies": [
- "bar.bib",
- "foo.bib"
- ],
"files": [],
"scenarios": []
}
~~~
+### Multiple markdown files
+
+This scenario tests that the `markdowns` field in metadata can specify
+more than one markdown file.
+
+~~~scenario
+given file multimd.subplot
+given file md1.md
+given file md2.md
+given an installed subplot
+when I run subplot docgen multimd.subplot -o multimd.html
+when I run cat multimd.html
+then file multimd.html exists
+and file multimd.html contains "<title>The Fabulous Title</title>"
+and file multimd.html contains "First markdown file."
+and file multimd.html contains "Second markdown file."
+~~~
+
+~~~{#multimd.subplot .file .yaml .numberLines}
+title: The Fabulous Title
+authors:
+- Alfred Pennyworth
+- Geoffrey Butler
+date: WIP
+markdowns:
+- md1.md
+- md2.md
+~~~
+
+~~~{#md1.md .file .markdown .numberLines}
+First markdown file.
+~~~
+
+~~~{#md2.md .file .markdown .numberLines}
+Second markdown file.
+~~~
+
+
## Embedded files
Subplot allows data files to be embedded in the input document. This
is handy for small test files and the like.
-Handling of a newline character on the last line is tricky. Pandoc
-doesn't include a newline on the last line. Sometimes one is
-needed&mdash;but sometimes it's not wanted. A newline can be added by
-having an empty line at the end, but that is subtle and easy to miss.
-Subplot helps the situation by allowing a `add-newline=` class to be added
-to the code blocks, with one of three allowed cases:
+Handling of a newline character on the last line is tricky. The block
+ends in a newline on the last line. Sometimes one is needed&mdash;but
+sometimes it's not wanted. Subplot helps the situation by allowing a
+`add-newline=` class to be added to the code blocks, with one of three
+allowed cases:
* no `add-newline` class&mdash;default handling: same as `add-newline=auto`
* `add-newline=auto`&mdash;add a newline, if one isn't there
@@ -2580,18 +2844,21 @@ This scenario checks that an embedded file can be extracted, and used
in a subplot.
~~~scenario
+given file embedded.subplot
given file embedded.md
and an installed subplot
-when I run subplot docgen --merciful embedded.md -o foo.html
+when I run subplot docgen --merciful embedded.subplot -o foo.html
then file foo.html exists
and file foo.html matches regex /embedded\.txt/
~~~
-~~~~~~~{#embedded.md .file .markdown .numberLines}
----
+~~~~~~~{#embedded.subplot .file .yaml .numberLines}
title: One embedded file
-...
+markdowns:
+- embedded.md
+~~~~~~~
+~~~~~~~{#embedded.md .file .markdown .numberLines}
~~~{#embedded.txt .file}
This is the embedded file.
~~~
@@ -2761,19 +3028,22 @@ This scenario checks that we get warnings, when using a subplot with
embedded files that aren't used.
~~~scenario
+given file unusedfile.subplot
given file unusedfile.md
and an installed subplot
-when I try to run subplot docgen --merciful unusedfile.md -o unusedfile.html
+when I try to run subplot docgen --merciful unusedfile.subplot -o unusedfile.html
then command is successful
and file unusedfile.html exists
and stderr contains "thisisnotused.txt"
~~~
-~~~~{#unusedfile.md .file .markdown .numberLines}
----
+~~~~{#unusedfile.subplot .file .yaml .numberLines}
title: Embedded file is not used by a scenario
-...
+markdowns:
+- unusedfile.md
+~~~~
+~~~~{#unusedfile.md .file .markdown .numberLines}
```{#thisisnotused.txt .file}
This is the embedded file.
```
@@ -2789,19 +3059,22 @@ subject to the same naming constraints (caseless uniqueness).
### Examples may be unused
~~~scenario
+given file unusedexample.subplot
given file unusedexample.md
and an installed subplot
-when I try to run subplot docgen --merciful unusedexample.md -o unusedexample.html
+when I try to run subplot docgen --merciful unusedexample.subplot -o unusedexample.html
then command is successful
and file unusedexample.html exists
and stderr doesn't contain "thisisnotused.txt"
~~~
-~~~{#unusedexample.md .file .markdown .numberLines}
----
+~~~{#unusedexample.subplot .file .yaml .numberLines}
title: Example is not an embedded file
-...
+markdowns:
+- unusedexample.md
+~~~
+~~~{#unusedexample.md .file .markdown .numberLines}
```{#thisisnotused.txt .example}
This is the embedded example.
```
@@ -2810,20 +3083,24 @@ This is the embedded example.
### Examples are not files
~~~scenario
+given file examplesnotfiles.subplot
given file examplesnotfiles.md
and an installed subplot
-when I try to run subplot codegen examplesnotfiles.md -t python -o examplesnotfiles.html
+when I try to run subplot codegen examplesnotfiles.subplot -t python -o examplesnotfiles.html
then command fails
and file examplesnotfiles.html does not exist
and stderr contains "thisisanexample.txt"
~~~
-~~~{#examplesnotfiles.md .file .markdown .numberLines}
----
+~~~{#examplesnotfiles.subplot .file .yaml .numberLines}
title: Examples are not files
+markdowns:
+- examplesnotfiles.md
impls:
python: []
-...
+~~~
+
+~~~{#examplesnotfiles.md .file .markdown .numberLines}
# Try and use an example as a file
@@ -2834,7 +3111,6 @@ given file thisisanexample.txt
```{#thisisanexample.txt .example}
This is an embedded example
```
-
~~~
## Steps must match bindings
@@ -2863,12 +3139,15 @@ binding.
### Steps which do not match bindings do not work
-~~~~{#nobinding.md .file .markdown}
----
+~~~~{#nobinding.subplot .file .yaml}
title: No bindings available
+markdowns:
+- nobinding.md
bindings:
- badbindings.yaml
-...
+~~~~
+
+~~~~{#nobinding.md .file .markdown}
# Broken scenario because step has no binding
```scenario
@@ -2878,22 +3157,26 @@ then nothing works
~~~~
```scenario
+given file nobinding.subplot
given file nobinding.md
and file badbindings.yaml
and an installed subplot
-when I try to run subplot codegen --run nobinding.md -o test.py
+when I try to run subplot codegen --run nobinding.subplot -o test.py
then command fails
```
### Steps which do not case-sensitively match sensitive bindings do not work
-~~~~{#casemismatch.md .file .markdown}
----
+~~~~{#casemismatch.subplot .file .yaml}
title: Case sensitivity mismatch
+markdowns:
+- casemismatch.md
impls: { python: [] }
bindings:
- badbindings.yaml
-...
+~~~~
+
+~~~~{#casemismatch.md .file .markdown}
# Broken scenario because step has a case mismatch with sensitive binding
```scenario
@@ -2902,23 +3185,27 @@ given a capitalised binding
~~~~
```scenario
+given file casemismatch.subplot
given file casemismatch.md
and file badbindings.yaml
and an installed subplot
-when I try to run subplot codegen --run casemismatch.md -o test.py
+when I try to run subplot codegen --run casemismatch.subplot -o test.py
then command fails
```
### Steps which match more than one binding do not work
-~~~~{#twobindings.md .file .markdown}
----
+~~~~{#twobindings.subplot .file .yaml}
title: Two bindings match
+markdowns:
+- twobindings.md
bindings:
- twobindings.yaml
impls:
python: [a_function.py]
-...
+~~~~
+
+~~~~{#twobindings.md .file .markdown}
# Broken scenario because step has two possible bindings
```scenario
@@ -2943,11 +3230,12 @@ def a_function(ctx):
~~~
```scenario
+given file twobindings.subplot
given file twobindings.md
and file twobindings.yaml
given file a_function.py
and an installed subplot
-when I try to run subplot codegen --run twobindings.md -o test.py
+when I try to run subplot codegen --run twobindings.subplot -o test.py
then command fails
then stderr contains "xyzzy"
then stderr contains "plugh"
@@ -2959,18 +3247,21 @@ then stderr contains "plugh"
The `subplot metadata` command lists embedded files in its output.
~~~scenario
+given file two-embedded.subplot
given file two-embedded.md
and an installed subplot
-when I run subplot metadata --merciful two-embedded.md
+when I run subplot metadata --merciful two-embedded.subplot
then stdout contains "foo.txt"
and stdout contains "bar.yaml"
~~~
-~~~~~~{#two-embedded.md .file .markdown .numberLines}
----
+~~~~~~{#two-embedded.subplot .file .yaml .numberLines}
title: Two embedded files
-...
+markdowns:
+- two-embedded.md
+~~~~~~
+~~~~~~{#two-embedded.md .file .markdown .numberLines}
~~~{#foo.txt .file}
~~~
@@ -2994,7 +3285,7 @@ into SVGs such as this one.
~~~pikchr
arrow right 200% "Markdown" "Source"
box rad 10px "Subplot" "Document Generator" "(subplot docgen)" fit
-arrow right 200% "HTML+SVG/PDF" "Output"
+arrow right 200% "HTML+SVG" "Output"
arrow <-> down 70% from last box.s
box same "Pikchr" "Formatter" "(docs.rs/pikchr)" fit
~~~
@@ -3003,15 +3294,17 @@ The scenario checks that a diagram is generated and embedded into the HTML outpu
and is not referenced as an external image.
~~~scenario
+given file pikchr.subplot
given file pikchr.md
and an installed subplot
-when I run pandoc --filter subplot-filter pikchr.md -o pikchr.html
-then file pikchr.html matches regex /img src="data:image/svg\+xml;base64,/
+when I run subplot docgen pikchr.subplot -o pikchr.html
+then file pikchr.html matches regex /src="data:image/svg\+xml;base64,/
~~~
-The sample input file **pikchr.md**:
+The sample input file **pikchr.md:**
~~~~~~~~{#pikchr.md .file .markdown .numberLines}
+---
This is an example markdown file that embeds a simple Pikchr diagram.
~~~pikchr
@@ -3024,6 +3317,12 @@ box same "Pikchr" "Formatter" "(docs.rs/pikchr)" fit
~~~~~~~~
+~~~~~~~~{#pikchr.subplot .file .yaml .numberLines}
+title: Pikchr test
+markdowns:
+- pikchr.md
+~~~~~~~~
+
### Dot
[Graphviz]: http://www.graphviz.org/
@@ -3041,14 +3340,15 @@ The scenario checks that a diagram is generated and embedded into the
HTML output, not referenced as an external image.
~~~scenario
+given file dot.subplot
given file dot.md
and file b.yaml
and an installed subplot
-when I run pandoc --filter subplot-filter dot.md -o dot.html
-then file dot.html matches regex /img src="data:image/svg\+xml;base64,/
+when I run subplot docgen dot.subplot -o dot.html
+then file dot.html matches regex /src="data:image/svg\+xml;base64,/
~~~
-The sample input file **dot.md**:
+The sample input file **dot.md:**
~~~~~~~~{#dot.md .file .markdown .numberLines}
This is an example Markdown file, which embeds a diagram using dot markup.
@@ -3060,6 +3360,12 @@ thing -> other
~~~
~~~~~~~~
+~~~~~~~~{#dot.subplot .file .yaml .numberLines}
+title: Dot test
+markdowns:
+- dot.md
+~~~~~~~~
+
### PlantUML
@@ -3083,14 +3389,15 @@ The scenario below checks that a diagram is generated and embedded into
the HTML output, not referenced as an external image.
~~~scenario
+given file plantuml.subplot
given file plantuml.md
and file b.yaml
and an installed subplot
-when I run pandoc --filter subplot-filter plantuml.md -o plantuml.html
-then file plantuml.html matches regex /img src="data:image/svg\+xml;base64,/
+when I run subplot docgen plantuml.subplot -o plantuml.html
+then file plantuml.html matches regex /src="data:image/svg\+xml;base64,/
~~~
-The sample input file **plantuml.md**:
+The sample input file **plantuml.md:**
~~~~~~~~{#plantuml.md .file .markdown .numberLines}
This is an example Markdown file, which embeds a diagram using
@@ -3107,6 +3414,12 @@ Alice <-- Bob: Another authentication Response
~~~
~~~~~~~~
+~~~~~~~~{#plantuml.subplot .file .yaml .numberLines}
+title: Plantuml test
+markdowns:
+- plantuml.md
+~~~~~~~~
+
### Roadmap
@@ -3164,14 +3477,15 @@ This scenario checks that a diagram is generated and embedded into the
HTML output, not referenced as an external image.
~~~scenario
+given file roadmap.subplot
given file roadmap.md
and file b.yaml
and an installed subplot
-when I run pandoc --filter subplot-filter roadmap.md -o roadmap.html
-then file roadmap.html matches regex /img src="data:image/svg\+xml;base64,/
+when I run subplot docgen roadmap.subplot -o roadmap.html
+then file roadmap.html matches regex /src="data:image/svg\+xml;base64,/
~~~
-The sample input file **roadmap.md**:
+The sample input file **roadmap.md:**
~~~~~~~~{#roadmap.md .file .markdown .numberLines}
This is an example Markdown file, which embeds a roadmap.
@@ -3220,115 +3534,278 @@ blocked:
~~~
~~~~~~~~
+~~~~~~~~{#roadmap.subplot .file .yaml .numberLines}
+title: Roadmap test
+markdowns:
+- roadmap.md
+~~~~~~~~
+
### Class name validation
When Subplot loads a document it will validate that the block classes
match a known set. Subplot has a built-in set which it treats as special,
-and it knows some pandoc-specific classes and a number of file type classes.
+and it knows some custom classes and a number of file type classes.
If the author of a document wishes to use additional class names then they can
include a `classes` list in the document metadata which subplot will treat
as valid.
~~~scenario
+given file unknown-class-name.subplot
given file unknown-class-name.md
+and file known-class-name.subplot
and file known-class-name.md
and file b.yaml
and an installed subplot
-when I try to run subplot docgen unknown-class-name.md -o unknown-class-name.html
+when I try to run subplot docgen unknown-class-name.subplot -o unknown-class-name.html
then command fails
and file unknown-class-name.html does not exist
and stderr contains "Unknown classes found in the document: foobar"
-when I run subplot docgen known-class-name.md -o known-class-name.html
+when I run subplot docgen known-class-name.subplot -o known-class-name.html
then file known-class-name.html exists
~~~
-~~~~~~~~{#unknown-class-name.md .file .markdown .numberLines}
----
+~~~~~~~~{#unknown-class-name.subplot .file .yaml .numberLines}
title: A document with an unknown class name
-...
+markdowns:
+- unknown-class-name.md
+~~~~~~~~
+~~~~~~~~{#unknown-class-name.md .file .markdown .numberLines}
```foobar
This content is foobarish
```
-
~~~~~~~~
-~~~~~~~~{#known-class-name.md .file .markdown .numberLines}
----
+~~~~~~~~{#known-class-name.subplot .file .yaml .numberLines}
title: A document with a previously unknown class name
+markdowns:
+- known-class-name.md
classes:
- foobar
-...
+~~~~~~~~
+~~~~~~~~{#known-class-name.md .file .markdown .numberLines}
```foobar
This content is foobarish
```
-
~~~~~~~~
-## Using as a Pandoc filter
+## Extract embedded files
+
+`subplot extract` extracts embedded files from a subplot file.
+
+~~~scenario
+given file embedded-file.subplot
+given file embedded-file.md
+and file expected.txt
+and an installed subplot
+when I run subplot extract --merciful embedded-file.subplot foo.txt -d .
+then files foo.txt and expected.txt match
+~~~
+
+~~~~~~{#embedded-file.subplot .file .yaml .numberLines}
+title: Embedded file
+markdowns:
+- embedded-file.md
+~~~~~~
+
+~~~~~~{#embedded-file.md .file .markdown .numberLines}
+~~~{#foo.txt .file}
+This is a test file.
+~~~
+~~~~~~
-Subplot can be used as a Pandoc _filter_, which means Pandoc can allow
-Subplot to modify the document while it is being converted or typeset.
-This can useful in a variety of ways, such as when using Pandoc to
-improve Markdown processing in the [ikiwiki][] blog engine.
+~~~{#expected.txt .file}
+This is a test file.
+~~~
+## Mistakes in markdown
-[ikiwiki]: http://ikiwiki.info/
+When there are mistakes in the markdown input, Subplot should report
+the location (filename, line, column) where the mistake is, and what
+the mistake is. The scenarios in this section verify that.
-The way filters work is that Pandoc parses the input document into an
-abstract syntax tree, serializes that into JSON, gives that to the
-filter (via the standard input), gets a modified abstract syntax tree
-(again as JSON, via the filter's standard output).
+### Scenario before the first heading
-Subplot supports this via the **subplot-filter** executable. It is built
-using the same internal logic as Subplot's docgen. The interface is
-merely different to be usable as a Pandoc filter.
+_Requirement: A scenario must follow a heading._
-This scenarios verifies that the filter works at all. More
-importantly, it does that by feeding the filter a Markdown file that
-does not have a YAML metadata block. For the ikiwiki use case, that's
-what the input files are like.
+Justification: the heading can be used as the title for the scenario.
~~~scenario
-given file justdata.md
-and an installed subplot
-when I run pandoc --filter subplot-filter justdata.md -o justdata.html
-then file justdata.html matches regex /does not have a YAML metadata/
+given an installed subplot
+given file scenario-before-heading.subplot
+given file scenario-before-heading.md
+when I try to run subplot docgen scenario-before-heading.subplot -o /dev/null
+then command fails
+then stderr contains "ERROR: scenario-before-heading.md:1:1: first scenario is before first heading"
~~~
-The input file **justdata.md**:
+~~~{#scenario-before-heading.subplot .file .yaml}
+title: Foo
+markdowns:
+ - scenario-before-heading.md
+~~~
-~~~~~~~~{#justdata.md .file .markdown .numberLines}
-This is an example Markdown file.
-It does not have a YAML metadata block.
-~~~~~~~~
+~~~~~~{#scenario-before-heading.md .file .markdown}
+~~~scenario
+~~~
+~~~~~~
+### Attempt to use definition list
-## Extract embedded files
+_Requirement: Attempt to use definition lists is reported._
-`subplot extract` extracts embedded files from a subplot file.
+Justification: the markdown parser we use in Subplot doesn't support
+them, and it would be unhelpful to not tell the user if they try to
+use them.
~~~scenario
-given file embedded-file.md
-and file expected.txt
-and an installed subplot
-when I run subplot extract --merciful embedded-file.md foo.txt -d .
-then files foo.txt and expected.txt match
+given an installed subplot
+given file dl.subplot
+given file dl.md
+when I try to run subplot docgen dl.subplot -o /dev/null
+then command fails
+then stderr contains "ERROR: dl.md:3:1: attempt to use definition lists in Markdown"
~~~
-~~~~~~{#embedded-file.md .file .markdown .numberLines}
----
-title: Embedded file
-...
+~~~{#dl.subplot .file .yaml}
+title: Foo
+markdowns:
+ - dl.md
+~~~
-~~~{#foo.txt .file}
-This is a test file.
+~~~~~~{#dl.md .file .markdown}
+# Foo
+
+Some term
+: Definition of term.
+~~~~~~
+
+### Bad "add-newline" value
+
+_Requirement: Only specific values for the "add-newline" attribute are
+allowed for an embedded file._
+
+~~~scenario
+given an installed subplot
+given file add-newline.subplot
+given file add-newline.md
+when I try to run subplot docgen add-newline.subplot -o /dev/null
+then command fails
+then stderr contains "ERROR: add-newline.md:1:1: value of add-newline attribute is not understood: xyzzy"
+~~~
+
+~~~{#add-newline.subplot .file .yaml}
+title: Foo
+markdowns:
+ - add-newline.md
~~~
+~~~~~~{#add-newline.md .file .markdown}
+~~~{#foo.txt .file add-newline=xyzzy}
+~~~
~~~~~~
-~~~{#expected.txt .file}
-This is a test file.
+## HTML output
+
+### Embedded CSS
+
+_Requirement:_ The user can specify CSS files to embed in the HTML
+output.
+
+Justification: We want to allow production of self-standing output
+with user-defined styling.
+
+~~~scenario
+given file embedded-css.subplot
+given file embedded-css.md
+given file embedded-css.css
+given file b.yaml
+given an installed subplot
+when I run subplot docgen embedded-css.subplot -o foo.html
+then file foo.html contains "silly: property;"
+~~~
+
+~~~{#embedded-css.subplot .file .yaml .numberLines}
+title: Embedded CSS
+markdowns:
+ - embedded-css.md
+bindings:
+ - b.yaml
+css_embed:
+ - embedded-css.css
+~~~
+
+~~~~~~{#embedded-css.md .file .markdown .numberLines}
+# This is a title
+
+~~~scenario
+given precondition
+~~~
+~~~~~~
+
+~~~{#embedded-css.css .file .css .numberLines}
+html {
+ silly: property;
+}
+~~~
+
+### CSS URLs
+
+_Requirement:_ The user can specify CSS URLs to add in the HTML
+output.
+
+Justification: We want to allow users to specify non-embedded CSS.
+
+~~~scenario
+given file css-urls.subplot
+given file css-urls.md
+given file b.yaml
+given an installed subplot
+when I run subplot docgen css-urls.subplot -o foo.html
+then file foo.html contains "https://example.com/flushing.css"
+~~~
+
+~~~{#css-urls.subplot .file .yaml .numberLines}
+title: Embedded CSS
+markdowns:
+ - css-urls.md
+bindings:
+ - b.yaml
+css_urls:
+ - https://example.com/flushing.css
+~~~
+
+~~~~~~{#css-urls.md .file .markdown .numberLines}
+# This is a title
+
+~~~scenario
+given precondition
+~~~
+~~~~~~
+
+
+## Running Subplot
+
+The scenarios in this section verify that the Subplot tool can be run
+in various specific ways.
+
+### Files not in current working directory
+
+_Requirement: Subplot can process a subplot that is not in the current
+working directory._
+
+~~~scenario
+given file x/simple.subplot from simple.subplot
+given file x/simple.md from simple.md
+given file x/b.yaml from b.yaml
+given file x/f.py from f.py
+given an installed subplot
+when I run subplot metadata x/simple.subplot
+then command is successful
+when I run subplot codegen x/simple.subplot -o test.py
+then file test.py exists
+when I run subplot docgen x/simple.subplot -o simple.html
+then file simple.html exists
~~~
diff --git a/subplot.py b/subplot.py
index 8533b45..e1ec3de 100644
--- a/subplot.py
+++ b/subplot.py
@@ -1,7 +1,6 @@
import json
import os
import shutil
-import time
# A shell script to run the subplot binary from the source directory's Rust
@@ -113,7 +112,3 @@ def binary(basename):
def do_nothing(ctx):
pass
-
-
-def sleep_seconds(ctx, delay="1"):
- time.sleep(int(delay))
diff --git a/subplot.subplot b/subplot.subplot
new file mode 100644
index 0000000..e109430
--- /dev/null
+++ b/subplot.subplot
@@ -0,0 +1,19 @@
+title: "Subplot"
+authors:
+ - The Subplot project
+markdowns:
+ - subplot.md
+bindings:
+- subplot.yaml
+- lib/runcmd.yaml
+- lib/files.yaml
+impls:
+ python:
+ - subplot.py
+ - lib/files.py
+ - lib/runcmd.py
+ rust:
+ - subplotlib/subplot-rust-support.rs
+classes:
+- json
+- ignored
diff --git a/subplot.yaml b/subplot.yaml
index 12a9fae..1c384eb 100644
--- a/subplot.yaml
+++ b/subplot.yaml
@@ -28,6 +28,9 @@
rust:
function: step_was_run
regex: true
+ types:
+ keyword: text
+ name: text
- then: step "(?P<keyword1>given|when|then) (?P<name1>.+)" was run, and then step "(?P<keyword2>given|when|then) (?P<name2>.+)"
impl:
@@ -36,6 +39,11 @@
rust:
function: step_was_run_and_then
regex: true
+ types:
+ keyword1: text
+ keyword2: text
+ name1: text
+ name2: text
- then: cleanup for "(?P<keyword1>given|when|then) (?P<name1>.+)" was run, and then for "(?P<keyword2>given|when|then) (?P<name2>.+)"
impl:
@@ -44,6 +52,11 @@
rust:
function: cleanup_was_run
regex: true
+ types:
+ keyword1: text
+ keyword2: text
+ name1: text
+ name2: text
- then: cleanup for "(?P<keyword>given|when|then) (?P<name>.+)" was not run
impl:
@@ -52,6 +65,9 @@
rust:
function: cleanup_was_not_run
regex: true
+ types:
+ keyword: text
+ name: text
- then: JSON output matches {filename}
impl:
@@ -81,18 +97,6 @@
rust:
function: file_ends_in_two_newlines
-# In order to cope with low granularity filesystems, sometimes we need to wait
-# for things to happen
-- when: I wait until (?P<delay>\d+) seconds? has passed
- impl:
- python:
- function: sleep_seconds
- rust:
- function: sleep_seconds
- regex: true
- types:
- delay: uint
-
# The following are purely descriptive steps and are not used to test anything
- given: the necessary starting conditions
diff --git a/subplotlib-derive/Cargo.toml b/subplotlib-derive/Cargo.toml
index 3ee596f..7d5b0c4 100644
--- a/subplotlib-derive/Cargo.toml
+++ b/subplotlib-derive/Cargo.toml
@@ -1,23 +1,24 @@
[package]
name = "subplotlib-derive"
-version = "0.4.0"
+version = "0.9.0"
authors = [
"Lars Wirzenius <liw@liw.fi>",
"Daniel Silverstone <dsilvers@digital-scurf.org>",
]
-edition = "2018"
+edition = "2021"
license = "MIT-0"
description = '''
macros for constructing subplotlib based test suites, typically
generated by `subplot codegen`.'''
-homepage = "https://subplot.liw.fi/"
+homepage = "https://subplot.tech/"
repository = "https://gitlab.com/subplot/subplot"
+rust-version = "1.70"
[lib]
proc-macro = true
[dependencies]
-syn = { version = "1", features = ["full"] }
+syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
-fehler = "1"
+culpa = "1.0.1"
diff --git a/subplotlib-derive/src/lib.rs b/subplotlib-derive/src/lib.rs
index 7fd1810..8e18c98 100644
--- a/subplotlib-derive/src/lib.rs
+++ b/subplotlib-derive/src/lib.rs
@@ -1,5 +1,6 @@
use proc_macro::TokenStream;
use proc_macro2::Span;
+use std::fmt::Write;
use syn::{
parse_macro_input, parse_quote, Error, FnArg, Ident, ItemFn, Pat, PathArguments, ReturnType,
Type,
@@ -7,7 +8,7 @@ use syn::{
use quote::quote;
-use fehler::{throw, throws};
+use culpa::{throw, throws};
fn ty_is_borrow_str(ty: &Type) -> bool {
if let Type::Reference(ty) = ty {
@@ -28,6 +29,25 @@ fn ty_is_borrow_str(ty: &Type) -> bool {
}
}
+fn ty_is_borrow_path(ty: &Type) -> bool {
+ if let Type::Reference(ty) = ty {
+ if ty.mutability.is_none() && ty.lifetime.is_none() {
+ if let Type::Path(pp) = &*ty.elem {
+ pp.path.is_ident("Path")
+ } else {
+ // not a path, so not &Path
+ false
+ }
+ } else {
+ // mutable, or a lifetime stated, so not &Path
+ false
+ }
+ } else {
+ // Not & so not &Path
+ false
+ }
+}
+
fn ty_is_datafile(ty: &Type) -> bool {
if let Type::Path(ty) = ty {
ty.path.is_ident("SubplotDataFile")
@@ -189,19 +209,19 @@ fn process_step(mut input: ItemFn) -> proc_macro2::TokenStream {
let contexts: Vec<Type> = input
.attrs
.iter()
- .filter(|attr| attr.path.is_ident("context"))
+ .filter(|attr| attr.path().is_ident("context"))
.map(|attr| {
let ty: Type = attr.parse_args()?;
Ok(ty)
})
.collect::<Result<_, Error>>()?;
- input.attrs.retain(|f| !f.path.is_ident("context"));
+ input.attrs.retain(|f| !f.path().is_ident("context"));
let docs: Vec<_> = input
.attrs
.iter()
- .filter(|attr| attr.path.is_ident("doc"))
+ .filter(|attr| attr.path().is_ident("doc"))
.collect();
let fields = input
@@ -241,6 +261,8 @@ fn process_step(mut input: ItemFn) -> proc_macro2::TokenStream {
.map(|(id, ty)| {
let ty = if ty_is_borrow_str(ty) {
parse_quote!(::std::string::String)
+ } else if ty_is_borrow_path(ty) {
+ parse_quote!(::std::path::PathBuf)
} else {
ty.clone()
};
@@ -277,6 +299,13 @@ fn process_step(mut input: ItemFn) -> proc_macro2::TokenStream {
self
}
}
+ } else if ty_is_borrow_path(ty) {
+ quote! {
+ pub fn #id<P: Into<std::path::PathBuf>>(mut self, value: P) -> Self {
+ self.#id = value.into();
+ self
+ }
+ }
} else {
quote! {
pub fn #id(mut self, value: #ty) -> Self {
@@ -291,7 +320,7 @@ fn process_step(mut input: ItemFn) -> proc_macro2::TokenStream {
let buildargs: Vec<_> = fields
.iter()
.map(|(id, ty)| {
- if ty_is_borrow_str(ty) {
+ if ty_is_borrow_str(ty) || ty_is_borrow_path(ty) {
quote! {
&self.#id
}
@@ -321,10 +350,11 @@ fn process_step(mut input: ItemFn) -> proc_macro2::TokenStream {
impl Builder {
#(#fieldfns)*
- pub fn build(self, step_text: String) -> ScenarioStep {
+ pub fn build(self, step_text: String, location: &'static str) -> ScenarioStep {
ScenarioStep::new(step_text, move |ctx, _defuse_poison|
#builder_body,
- |scenario| register_contexts(scenario)
+ |scenario| register_contexts(scenario),
+ location,
)
}
}
@@ -372,12 +402,9 @@ fn process_step(mut input: ItemFn) -> proc_macro2::TokenStream {
Some(&contexttype)
};
for context in outer_ctx.into_iter().chain(contexts.iter()) {
- contextattrs.push_str(&format!("\n #[context({:?})]", ty_as_path(context)?));
+ write!(contextattrs, "\n #[context({:?})]", ty_as_path(context)?).unwrap();
}
- let func_args: Vec<_> = fields
- .iter()
- .map(|(ident, _)| format!("{}", ident))
- .collect();
+ let func_args: Vec<_> = fields.iter().map(|(ident, _)| format!("{ident}")).collect();
let func_args = func_args.join(", ");
format!(
r#"
@@ -395,9 +422,6 @@ fn process_step(mut input: ItemFn) -> proc_macro2::TokenStream {
}}
```
"#,
- stepname = stepname,
- contextattrs = contextattrs,
- func_args = func_args,
)
};
let ret = quote! {
diff --git a/subplotlib/Cargo.toml b/subplotlib/Cargo.toml
index 11afd81..bb328c6 100644
--- a/subplotlib/Cargo.toml
+++ b/subplotlib/Cargo.toml
@@ -1,24 +1,24 @@
[package]
name = "subplotlib"
-version = "0.4.1"
+version = "0.9.0"
authors = [
"Lars Wirzenius <liw@liw.fi>",
"Daniel Silverstone <dsilvers@digital-scurf.org>",
]
-edition = "2018"
+edition = "2021"
license = "MIT-0"
description = '''
Utility functions and types for `subplot codegen` generated Rust based
test suites. Relies on `subplotlib-derive` for associated macros.'''
-homepage = "https://subplot.liw.fi/"
+homepage = "https://subplot.tech/"
repository = "https://gitlab.com/subplot/subplot"
+rust-version = "1.70"
[dependencies]
-fehler = "1"
-subplotlib-derive = { version = "0.4.0", path = "../subplotlib-derive" }
+subplotlib-derive = { version = "0.9.0", path = "../subplotlib-derive" }
lazy_static = "1"
-base64 = "0.13"
+base64 = "0.21.0"
state = "0.5"
tempfile = "3.1"
fs2 = "0.4"
@@ -27,12 +27,13 @@ filetime = "0.2"
regex = "1.4"
shell-words = "1.0"
unescape = "0.1"
-remove_dir_all = "0.7"
+remove_dir_all = "0.8"
+culpa = "1.0.1"
[build-dependencies]
glob = "0.3"
-subplot-build = { version = "0.4.0", path = "../subplot-build" }
+subplot-build = { version = "0.9.0", path = "../subplot-build" }
[dev-dependencies]
serde_json = "1.0"
diff --git a/subplotlib/build.rs b/subplotlib/build.rs
index f22a50c..977f432 100644
--- a/subplotlib/build.rs
+++ b/subplotlib/build.rs
@@ -2,8 +2,8 @@
//
// We use the `subplot_build` crate to generate a Rust test code file
// with functions for the scenarios for each subplot file
-// (subplot/*.md in the source tree). The generated file is written to
-// the Cargo target directory. For each subplot foo.md there should be
+// (subplot/*.subplot in the source tree). The generated file is written to
+// the Cargo target directory. For each subplot foo.subplot there should be
// a tests/foo.rs in the source tree, which includes the generated
// file from the Cargo target tree. The source file should look like this:
//
@@ -14,15 +14,15 @@
use glob::glob;
use std::{fs, path::Path};
-fn gen_tests() {
- let subplots = glob("*.md").expect("failed to find subplots in subplotlib");
+fn gen_tests() -> Result<(), subplot_build::SubplotError> {
+ let subplots = glob("*.subplot").expect("failed to find subplots in subplotlib");
let tests = Path::new("tests");
- let subplots = subplots.chain(Some(Ok("../subplot.md".into())));
+ let subplots = subplots.chain(Some(Ok("../subplot.subplot".into())));
let subplots = subplots
- .chain(glob("../tests/subplots/common/*.md").expect("failed to find common subplots"));
+ .chain(glob("../tests/subplots/common/*.subplot").expect("failed to find common subplots"));
for entry in subplots {
let entry = entry.expect("failed to get subplot dir entry in subplotlib");
- let mut inc = tests.join(&entry.file_name().unwrap());
+ let mut inc = tests.join(entry.file_name().unwrap());
inc.set_extension("rs");
if !inc.exists() {
panic!("missing include file: {}", inc.display());
@@ -31,12 +31,16 @@ fn gen_tests() {
println!("cargo:rerun-if-changed={}", entry.display());
subplot_build::codegen(Path::new(&entry)).expect("failed to generate code with Subplot");
}
+ Ok(())
}
fn main() {
// Because we cannot generate tests if we're not fully inside the main subplot tree
- // we only generate them if we can see ../subplot.md which is a good indicator.
- if fs::metadata("../subplot.md").is_ok() {
- gen_tests();
+ // we only generate them if we can see ../subplot.subplot which is a good indicator.
+ if fs::metadata("../subplot.subplot").is_ok() {
+ if let Err(e) = gen_tests() {
+ eprintln!("Failed to generate code from subplot: {e}");
+ std::process::exit(1);
+ }
}
}
diff --git a/subplotlib/helpers/subplotlib_impl.rs b/subplotlib/helpers/subplotlib_impl.rs
index fac54ab..768337a 100644
--- a/subplotlib/helpers/subplotlib_impl.rs
+++ b/subplotlib/helpers/subplotlib_impl.rs
@@ -36,7 +36,7 @@ fn remember_target(context: &mut Context, somename: &str) {
if let Some(file) = context.files.get(somename) {
context.this_file = Some(file.clone());
} else {
- throw!(format!("Unknown file {}", somename));
+ throw!(format!("Unknown file {somename}"));
}
}
diff --git a/subplotlib/src/file.rs b/subplotlib/src/file.rs
index 84f3495..c1c9afe 100644
--- a/subplotlib/src/file.rs
+++ b/subplotlib/src/file.rs
@@ -7,7 +7,8 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
-use base64::decode;
+use base64::prelude::{Engine as _, BASE64_STANDARD};
+
/// An embedded data file.
///
/// Embedded data files have names and content. The subplot template will generate
@@ -82,11 +83,16 @@ impl SubplotDataFile {
///
/// This will panic if the passed in strings are not correctly base64 encoded.
pub fn new(name: &str, data: &str) -> Self {
- let name = decode(name).expect("Subplot generated bad base64?");
+ let name = BASE64_STANDARD
+ .decode(name)
+ .expect("Subplot generated bad base64?");
let name = String::from_utf8_lossy(&name);
let name: PathBuf = name.as_ref().into();
let name = name.into();
- let data = decode(data).expect("Subplot generated bad base64?").into();
+ let data = BASE64_STANDARD
+ .decode(data)
+ .expect("Subplot generated bad base64?")
+ .into();
Self { name, data }
}
diff --git a/subplotlib/src/prelude.rs b/subplotlib/src/prelude.rs
index e0ac627..9397fa7 100644
--- a/subplotlib/src/prelude.rs
+++ b/subplotlib/src/prelude.rs
@@ -16,16 +16,16 @@
//! The primary thing you will need to learn, as a step function author, is
//! the [`#[step]`][macro@step] attribute macro, and it interacts with contexts.
-// Re-export fehler's macros so that step functions can use them
-pub use fehler::throw;
+// Re-export culpa's macros so that step functions can use them
+pub use culpa::throw;
/// Indicate what type a function throws
///
-/// This attribute macro comes from the [`fehler`] crate and is used
+/// This attribute macro comes from the [`culpa`] crate and is used
/// to indicate that a function "throws" a particular kind of error.
///
/// ```rust
/// # use std::io;
-/// # use fehler::throws;
+/// # use culpa::throws;
/// #[throws(io::Error)]
/// fn create_thingy() {
/// // something which might cause an io::Error
@@ -51,10 +51,10 @@ pub use fehler::throw;
///
// When https://github.com/rust-lang/rust/issues/83976 is resolved, the below
// can be added to this doc string.
-// You can see more documentation on this in the [fehler crate docs][fehler::throws].
+// You can see more documentation on this in the [culpa crate docs][culpa::throws].
// Alternatively we can get rid of this entirely if and when
// https://github.com/rust-lang/rust/issues/81893 is fixed.
-pub use fehler::throws;
+pub use culpa::throws;
// Re-export the lazy_static macro
#[doc(hidden)]
diff --git a/subplotlib/src/scenario.rs b/subplotlib/src/scenario.rs
index 7a78ae5..9bcc43d 100644
--- a/subplotlib/src/scenario.rs
+++ b/subplotlib/src/scenario.rs
@@ -107,7 +107,7 @@ where
C: ContextElement,
{
fn new() -> Self {
- Self(PhantomData::default())
+ Self(PhantomData)
}
}
@@ -159,6 +159,7 @@ where
/// This container allows the running of code within a given scenario context.
pub struct ScenarioContext {
title: String,
+ location: &'static str,
inner: Container![],
hooks: RefCell<Vec<Box<dyn ScenarioContextHookKind>>>,
}
@@ -174,9 +175,9 @@ impl DebuggedContext {
C: Debug,
{
let body = if alternate {
- format!("{:#?}", obj)
+ format!("{obj:#?}")
} else {
- format!("{:?}", obj)
+ format!("{obj:?}")
};
self.body.push(body);
}
@@ -212,9 +213,10 @@ impl Debug for ScenarioContext {
}
impl ScenarioContext {
- fn new(title: &str) -> Self {
+ fn new(title: &str, location: &'static str) -> Self {
Self {
title: title.to_string(),
+ location,
inner: <Container![]>::new(),
hooks: RefCell::new(Vec::new()),
}
@@ -303,12 +305,12 @@ impl ScenarioContext {
/// ```
/// # use subplotlib::prelude::*;
///
-/// let mut scenario = Scenario::new("example scenario");
+/// let mut scenario = Scenario::new("example scenario", "unknown");
///
/// let run_step = subplotlib::steplibrary::runcmd::run::Builder::default()
/// .argv0("true")
/// .args("")
-/// .build("when I run true".to_string());
+/// .build("when I run true".to_string(), "unknown");
/// scenario.add_step(run_step, None);
///
/// ```
@@ -319,9 +321,9 @@ pub struct Scenario {
impl Scenario {
/// Create a new scenario with the given title
- pub fn new(title: &str) -> Self {
+ pub fn new(title: &str, location: &'static str) -> Self {
Self {
- contexts: ScenarioContext::new(title),
+ contexts: ScenarioContext::new(title, location),
steps: Vec::new(),
}
}
@@ -372,7 +374,11 @@ impl Scenario {
// Firstly, we start all the contexts
let mut ret = Ok(());
let mut highest_start = None;
- println!("scenario: {}", self.contexts.title());
+ println!(
+ "{}: scenario: {}",
+ self.contexts.location,
+ self.contexts.title()
+ );
for (i, hook) in self.contexts.hooks.borrow().iter().enumerate() {
let res = hook.scenario_starts(&self.contexts);
if res.is_err() {
@@ -387,7 +393,7 @@ impl Scenario {
if ret.is_ok() {
let mut highest = None;
for (i, step) in self.steps.iter().map(|(step, _)| step).enumerate() {
- println!(" step: {}", step.step_text());
+ println!("{}: step: {}", step.location(), step.step_text());
let mut highest_prep = None;
for (i, prep) in self.contexts.hooks.borrow().iter().enumerate() {
let res = prep.step_starts(&self.contexts, step.step_text());
diff --git a/subplotlib/src/step.rs b/subplotlib/src/step.rs
index bbea249..76b9193 100644
--- a/subplotlib/src/step.rs
+++ b/subplotlib/src/step.rs
@@ -10,6 +10,8 @@ use std::panic::{catch_unwind, AssertUnwindSafe};
use crate::scenario::{Scenario, ScenarioContext};
use crate::types::StepResult;
+type StepFunc = dyn Fn(&ScenarioContext, bool) -> StepResult;
+
/// A ScenarioStep is one step in a scenario.
///
/// In essence, a scenario step is a named closure. Its name can be used when
@@ -23,12 +25,13 @@ use crate::types::StepResult;
/// # use subplotlib::prelude::*;
///
/// let step = ScenarioStep::new(
-/// "when everything works".to_string(), |ctx, ok| Ok(()), |scen| ()
+/// "when everything works".to_string(), |ctx, ok| Ok(()), |scen| (), "unknown"
/// );
/// ```
pub struct ScenarioStep {
step_text: String,
- func: Box<dyn Fn(&ScenarioContext, bool) -> StepResult>,
+ location: &'static str,
+ func: Box<StepFunc>,
reg: Box<dyn Fn(&Scenario)>,
}
@@ -38,13 +41,14 @@ impl ScenarioStep {
/// This is used to construct a scenario step from a function which
/// takes the scenario context container. This will generally be
/// called from the generated build method for the step.
- pub fn new<F, R>(step_text: String, func: F, reg: R) -> Self
+ pub fn new<F, R>(step_text: String, func: F, reg: R, location: &'static str) -> Self
where
F: Fn(&ScenarioContext, bool) -> StepResult + 'static,
R: Fn(&Scenario) + 'static,
{
Self {
step_text,
+ location,
func: Box::new(func),
reg: Box::new(reg),
}
@@ -53,13 +57,13 @@ impl ScenarioStep {
/// Attempt to render a message.
/// If something panics with a type other than a static string or
/// a formatted string then we won't be able to render it sadly.
- fn render_panic(name: &str, err: Box<dyn Any + Send>) -> String {
+ fn render_panic(location: &str, name: &str, err: Box<dyn Any + Send>) -> String {
if let Some(msg) = err.downcast_ref::<&str>() {
- format!("step {} panic'd: {}", name, msg)
+ format!("{location}: step {name} panic'd: {msg}")
} else if let Some(msg) = err.downcast_ref::<String>() {
- format!("step {} panic'd: {}", name, msg)
+ format!("{location}: step {name} panic'd: {msg}")
} else {
- format!("step {} panic'd", name)
+ format!("{location}: step {name} panic'd")
}
}
@@ -71,7 +75,7 @@ impl ScenarioStep {
// subsequent step calls may not be sound. There's not a lot we can
// do to ensure things are good except try.
let func = AssertUnwindSafe(|| (*self.func)(context, defuse_poison));
- catch_unwind(func).map_err(|e| Self::render_panic(self.step_text(), e))?
+ catch_unwind(func).map_err(|e| Self::render_panic(self.location(), self.step_text(), e))?
}
/// Return the full text of this step
@@ -83,4 +87,8 @@ impl ScenarioStep {
pub(crate) fn register_contexts(&self, scenario: &Scenario) {
(*self.reg)(scenario);
}
+
+ pub(crate) fn location(&self) -> &'static str {
+ self.location
+ }
}
diff --git a/subplotlib/src/steplibrary/datadir.rs b/subplotlib/src/steplibrary/datadir.rs
index 5a344df..5060f63 100644
--- a/subplotlib/src/steplibrary/datadir.rs
+++ b/subplotlib/src/steplibrary/datadir.rs
@@ -150,8 +150,7 @@ pub fn datadir_has_enough_space(datadir: &Datadir, bytes: u64) {
let available = fs2::available_space(datadir.base_path())?;
if available < bytes {
throw!(format!(
- "Available space check failed, wanted {} bytes, but only {} were available",
- bytes, available
+ "Available space check failed, wanted {bytes} bytes, but only {available} were available",
));
}
}
diff --git a/subplotlib/src/steplibrary/files.rs b/subplotlib/src/steplibrary/files.rs
index 1a55f7a..9b00e17 100644
--- a/subplotlib/src/steplibrary/files.rs
+++ b/subplotlib/src/steplibrary/files.rs
@@ -7,7 +7,7 @@ use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::fs::{self, Metadata, OpenOptions};
use std::io::{self, Write};
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use filetime::FileTime;
@@ -31,7 +31,7 @@ pub use super::datadir::Datadir;
/// Because files can typically only be named in Subplot documents, we assume they
/// all have names which can be rendered as utf-8 strings.
pub struct Files {
- metadata: HashMap<String, Metadata>,
+ metadata: HashMap<PathBuf, Metadata>,
}
impl ContextElement for Files {
@@ -50,7 +50,7 @@ impl ContextElement for Files {
#[step]
#[context(Datadir)]
pub fn create_from_embedded(context: &ScenarioContext, embedded_file: SubplotDataFile) {
- let filename_on_disk = format!("{}", embedded_file.name().display());
+ let filename_on_disk = PathBuf::from(format!("{}", embedded_file.name().display()));
create_from_embedded_with_other_name::call(context, &filename_on_disk, embedded_file)?;
}
@@ -63,7 +63,7 @@ pub fn create_from_embedded(context: &ScenarioContext, embedded_file: SubplotDat
#[step]
pub fn create_from_embedded_with_other_name(
context: &Datadir,
- filename_on_disk: &str,
+ filename_on_disk: &Path,
embedded_file: SubplotDataFile,
) {
let filename_on_disk = PathBuf::from(filename_on_disk);
@@ -86,11 +86,11 @@ pub fn create_from_embedded_with_other_name(
/// Sets the modification time for the given filename to the provided mtime.
/// If the file does not exist, it will be created.
#[step]
-pub fn touch_with_timestamp(context: &Datadir, filename: &str, mtime: &str) {
+pub fn touch_with_timestamp(context: &Datadir, filename: &Path, mtime: &str) {
let fd = format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour]:[offset_minute]"
);
- let full_time = format!("{} +00:00", mtime);
+ let full_time = format!("{mtime} +00:00");
let ts = OffsetDateTime::parse(&full_time, &fd)?;
let (secs, nanos) = (ts.unix_timestamp(), 0);
let mtime = FileTime::from_unix_time(secs, nanos);
@@ -112,7 +112,7 @@ pub fn touch_with_timestamp(context: &Datadir, filename: &str, mtime: &str) {
///
/// Create/replace the given file with the given content.
#[step]
-pub fn create_from_text(context: &Datadir, text: &str, filename: &str) {
+pub fn create_from_text(context: &Datadir, text: &str, filename: &Path) {
context.open_write(filename)?.write_all(text.as_bytes())?;
}
@@ -125,12 +125,12 @@ pub fn create_from_text(context: &Datadir, text: &str, filename: &str) {
#[step]
#[context(Datadir)]
#[context(Files)]
-pub fn remember_metadata(context: &ScenarioContext, filename: &str) {
+pub fn remember_metadata(context: &ScenarioContext, filename: &Path) {
let full_path = context.with(
|context: &Datadir| context.canonicalise_filename(filename),
false,
)?;
- let metadata = fs::metadata(&full_path)?;
+ let metadata = fs::metadata(full_path)?;
context.with_mut(
|context: &mut Files| {
context.metadata.insert(filename.to_owned(), metadata);
@@ -147,7 +147,7 @@ pub fn remember_metadata(context: &ScenarioContext, filename: &str) {
/// This will create the named file if it does not exist, and then it will ensure that the
/// file's modification time is set to the current time.
#[step]
-pub fn touch(context: &Datadir, filename: &str) {
+pub fn touch(context: &Datadir, filename: &Path) {
let full_path = context.canonicalise_filename(filename)?;
let now = FileTime::now();
// If the file doesn't exist, create it
@@ -167,13 +167,13 @@ pub fn touch(context: &Datadir, filename: &str) {
///
/// This simple step will succeed if the given filename exists in some sense.
#[step]
-pub fn file_exists(context: &Datadir, filename: &str) {
+pub fn file_exists(context: &Datadir, filename: &Path) {
let full_path = context.canonicalise_filename(filename)?;
match fs::metadata(full_path) {
Ok(_) => (),
Err(e) => {
if matches!(e.kind(), io::ErrorKind::NotFound) {
- throw!(format!("file '{}' was not found", filename))
+ throw!(format!("file '{}' was not found", filename.display()))
} else {
throw!(e);
}
@@ -187,11 +187,14 @@ pub fn file_exists(context: &Datadir, filename: &str) {
///
/// This simple step will succeed if the given filename does not exist in any sense.
#[step]
-pub fn file_does_not_exist(context: &Datadir, filename: &str) {
+pub fn file_does_not_exist(context: &Datadir, filename: &Path) {
let full_path = context.canonicalise_filename(filename)?;
match fs::metadata(full_path) {
Ok(_) => {
- throw!(format!("file '{}' was unexpectedly found", filename))
+ throw!(format!(
+ "file '{}' was unexpectedly found",
+ filename.display()
+ ))
}
Err(e) => {
if !matches!(e.kind(), io::ErrorKind::NotFound) {
@@ -229,10 +232,11 @@ pub fn only_these_exist(context: &Datadir, filenames: &str) {
/// This will load the content of the named file and ensure it contains the given string.
/// Note: this assumes everything is utf-8 encoded. If not, things will fail.
#[step]
-pub fn file_contains(context: &Datadir, filename: &str, data: &str) {
+pub fn file_contains(context: &Datadir, filename: &Path, data: &str) {
let full_path = context.canonicalise_filename(filename)?;
let body = fs::read_to_string(full_path)?;
if !body.contains(data) {
+ println!("file {} contains:\n{}", filename.display(), body);
throw!("expected file content not found");
}
}
@@ -244,10 +248,11 @@ pub fn file_contains(context: &Datadir, filename: &str, data: &str) {
/// This will load the content of the named file and ensure it lacks the given string.
/// Note: this assumes everything is utf-8 encoded. If not, things will fail.
#[step]
-pub fn file_doesnt_contain(context: &Datadir, filename: &str, data: &str) {
+pub fn file_doesnt_contain(context: &Datadir, filename: &Path, data: &str) {
let full_path = context.canonicalise_filename(filename)?;
let body = fs::read_to_string(full_path)?;
if body.contains(data) {
+ println!("file {} contains:\n{}", filename.display(), body);
throw!("unexpected file content found");
}
}
@@ -260,11 +265,12 @@ pub fn file_doesnt_contain(context: &Datadir, filename: &str, data: &str) {
/// matches the given regular expression. This step will fail if the file is not utf-8
/// encoded, or if the regex fails to compile
#[step]
-pub fn file_matches_regex(context: &Datadir, filename: &str, regex: &str) {
+pub fn file_matches_regex(context: &Datadir, filename: &Path, regex: &str) {
let full_path = context.canonicalise_filename(filename)?;
let regex = Regex::new(regex)?;
let body = fs::read_to_string(full_path)?;
if !regex.is_match(&body) {
+ println!("file {} contains:\n{}", filename.display(), body);
throw!("file content does not match given regex");
}
}
@@ -275,12 +281,22 @@ pub fn file_matches_regex(context: &Datadir, filename: &str, regex: &str) {
///
/// This loads the content of the given two files as **bytes** and checks they mach.
#[step]
-pub fn file_match(context: &Datadir, filename1: &str, filename2: &str) {
+pub fn file_match(context: &Datadir, filename1: &Path, filename2: &Path) {
let full_path1 = context.canonicalise_filename(filename1)?;
let full_path2 = context.canonicalise_filename(filename2)?;
let body1 = fs::read(full_path1)?;
let body2 = fs::read(full_path2)?;
if body1 != body2 {
+ println!(
+ "file {} contains:\n{}",
+ filename1.display(),
+ String::from_utf8_lossy(&body1)
+ );
+ println!(
+ "file {} contains:\n{}",
+ filename2.display(),
+ String::from_utf8_lossy(&body2)
+ );
throw!("file contents do not match each other");
}
}
@@ -299,12 +315,12 @@ pub fn file_match(context: &Datadir, filename1: &str, filename2: &str) {
#[step]
#[context(Datadir)]
#[context(Files)]
-pub fn has_remembered_metadata(context: &ScenarioContext, filename: &str) {
+pub fn has_remembered_metadata(context: &ScenarioContext, filename: &Path) {
let full_path = context.with(
|context: &Datadir| context.canonicalise_filename(filename),
false,
)?;
- let metadata = fs::metadata(&full_path)?;
+ let metadata = fs::metadata(full_path)?;
if let Some(remembered) = context.with(
|context: &Files| Ok(context.metadata.get(filename).cloned()),
false,
@@ -314,10 +330,13 @@ pub fn has_remembered_metadata(context: &ScenarioContext, filename: &str) {
|| metadata.len() != remembered.len()
|| metadata.is_file() != remembered.is_file()
{
- throw!(format!("metadata change detected for {}", filename));
+ throw!(format!(
+ "metadata change detected for {}",
+ filename.display()
+ ));
}
} else {
- throw!(format!("no remembered metadata for {}", filename));
+ throw!(format!("no remembered metadata for {}", filename.display()));
}
}
@@ -335,12 +354,12 @@ pub fn has_remembered_metadata(context: &ScenarioContext, filename: &str) {
#[step]
#[context(Datadir)]
#[context(Files)]
-pub fn has_different_metadata(context: &ScenarioContext, filename: &str) {
+pub fn has_different_metadata(context: &ScenarioContext, filename: &Path) {
let full_path = context.with(
|context: &Datadir| context.canonicalise_filename(filename),
false,
)?;
- let metadata = fs::metadata(&full_path)?;
+ let metadata = fs::metadata(full_path)?;
if let Some(remembered) = context.with(
|context: &Files| Ok(context.metadata.get(filename).cloned()),
false,
@@ -350,10 +369,13 @@ pub fn has_different_metadata(context: &ScenarioContext, filename: &str) {
&& metadata.len() == remembered.len()
&& metadata.is_file() == remembered.is_file()
{
- throw!(format!("metadata change not detected for {}", filename));
+ throw!(format!(
+ "metadata change not detected for {}",
+ filename.display()
+ ));
}
} else {
- throw!(format!("no remembered metadata for {}", filename));
+ throw!(format!("no remembered metadata for {}", filename.display()));
}
}
@@ -363,13 +385,13 @@ pub fn has_different_metadata(context: &ScenarioContext, filename: &str) {
///
/// Specifically this checks that the given file has been modified in the past 5 seconds.
#[step]
-pub fn mtime_is_recent(context: &Datadir, filename: &str) {
+pub fn mtime_is_recent(context: &Datadir, filename: &Path) {
let full_path = context.canonicalise_filename(filename)?;
let metadata = fs::metadata(full_path)?;
let mtime = metadata.modified()?;
let diff = SystemTime::now().duration_since(mtime)?;
if diff > (Duration::from_secs(5)) {
- throw!(format!("{} is older than 5 seconds", filename));
+ throw!(format!("{} is older than 5 seconds", filename.display()));
}
}
@@ -379,13 +401,13 @@ pub fn mtime_is_recent(context: &Datadir, filename: &str) {
///
/// Specifically this checks that the file was modified at least 39 years ago.
#[step]
-pub fn mtime_is_ancient(context: &Datadir, filename: &str) {
+pub fn mtime_is_ancient(context: &Datadir, filename: &Path) {
let full_path = context.canonicalise_filename(filename)?;
let metadata = fs::metadata(full_path)?;
let mtime = metadata.modified()?;
let diff = SystemTime::now().duration_since(mtime)?;
if diff < (Duration::from_secs(39 * 365 * 24 * 3600)) {
- throw!(format!("{} is younger than 39 years", filename));
+ throw!(format!("{} is younger than 39 years", filename.display()));
}
}
@@ -395,7 +417,7 @@ pub fn mtime_is_ancient(context: &Datadir, filename: &str) {
///
/// This is the equivalent of `mkdir -p` within the data directory for the scenario.
#[step]
-pub fn make_directory(context: &Datadir, path: &str) {
+pub fn make_directory(context: &Datadir, path: &Path) {
context.create_dir_all(path)?;
}
@@ -405,7 +427,7 @@ pub fn make_directory(context: &Datadir, path: &str) {
///
/// This is the equivalent of `rm -rf` within the data directory for the scenario.
#[step]
-pub fn remove_directory(context: &Datadir, path: &str) {
+pub fn remove_directory(context: &Datadir, path: &Path) {
let full_path = context.canonicalise_filename(path)?;
remove_dir_all::remove_dir_all(full_path)?;
}
@@ -417,7 +439,7 @@ pub fn remove_directory(context: &Datadir, path: &str) {
/// This ensures that the given path exists in the data directory for the scenario and
/// that it is a directory itself.
#[step]
-pub fn path_exists(context: &Datadir, path: &str) {
+pub fn path_exists(context: &Datadir, path: &Path) {
let full_path = context.canonicalise_filename(path)?;
if !fs::metadata(&full_path)?.is_dir() {
throw!(format!(
@@ -434,7 +456,7 @@ pub fn path_exists(context: &Datadir, path: &str) {
/// This ensures that the given path does not exist in the data directory. If it exists
/// and is not a directory, then this will also fail.
#[step]
-pub fn path_does_not_exist(context: &Datadir, path: &str) {
+pub fn path_does_not_exist(context: &Datadir, path: &Path) {
let full_path = context.canonicalise_filename(path)?;
match fs::metadata(&full_path) {
Ok(_) => throw!(format!("{} exists", full_path.display())),
@@ -453,7 +475,7 @@ pub fn path_does_not_exist(context: &Datadir, path: &str) {
/// This checks that the given path inside the data directory exists and is an
/// empty directory itself.
#[step]
-pub fn path_is_empty(context: &Datadir, path: &str) {
+pub fn path_is_empty(context: &Datadir, path: &Path) {
let full_path = context.canonicalise_filename(path)?;
let mut iter = fs::read_dir(&full_path)?;
match iter.next() {
@@ -471,7 +493,7 @@ pub fn path_is_empty(context: &Datadir, path: &str) {
/// directory itself. The step also asserts that the given directory contains at least
/// one entry.
#[step]
-pub fn path_is_not_empty(context: &Datadir, path: &str) {
+pub fn path_is_not_empty(context: &Datadir, path: &Path) {
let full_path = context.canonicalise_filename(path)?;
let mut iter = fs::read_dir(&full_path)?;
match iter.next() {
diff --git a/subplotlib/src/steplibrary/runcmd.rs b/subplotlib/src/steplibrary/runcmd.rs
index b90f8b7..99605a3 100644
--- a/subplotlib/src/steplibrary/runcmd.rs
+++ b/subplotlib/src/steplibrary/runcmd.rs
@@ -56,7 +56,9 @@ static DEFAULT_PATHS: &[&str] = &[
];
// This us used internally to force CWD for running commands
-const USE_CWD: &str = "\0USE_CWD";
+lazy_static! {
+ static ref USE_CWD: PathBuf = PathBuf::from("\0USE_CWD");
+}
impl ContextElement for Runcmd {
fn scenario_starts(&mut self) -> StepResult {
@@ -229,7 +231,7 @@ pub fn run(context: &ScenarioContext, argv0: &str, args: &str) {
#[step]
#[context(Datadir)]
#[context(Runcmd)]
-pub fn run_in(context: &ScenarioContext, dirname: &str, argv0: &str, args: &str) {
+pub fn run_in(context: &ScenarioContext, dirname: &Path, argv0: &str, args: &str) {
try_to_run_in::call(context, dirname, argv0, args)?;
exit_code_is::call(context, 0)?;
}
@@ -244,7 +246,7 @@ pub fn run_in(context: &ScenarioContext, dirname: &str, argv0: &str, args: &str)
#[context(Datadir)]
#[context(Runcmd)]
pub fn try_to_run(context: &ScenarioContext, argv0: &str, args: &str) {
- try_to_run_in::call(context, USE_CWD, argv0, args)?;
+ try_to_run_in::call(context, &USE_CWD, argv0, args)?;
}
/// Run the given command in the given subpath of the data directory
@@ -256,7 +258,7 @@ pub fn try_to_run(context: &ScenarioContext, argv0: &str, args: &str) {
#[step]
#[context(Datadir)]
#[context(Runcmd)]
-pub fn try_to_run_in(context: &ScenarioContext, dirname: &str, argv0: &str, args: &str) {
+pub fn try_to_run_in(context: &ScenarioContext, dirname: &Path, argv0: &str, args: &str) {
// This is the core of runcmd and is how we handle things
let argv0: PathBuf = if argv0.starts_with('.') {
context.with(
@@ -270,7 +272,7 @@ pub fn try_to_run_in(context: &ScenarioContext, dirname: &str, argv0: &str, args
|datadir: &Datadir| Ok(datadir.base_path().to_path_buf()),
false,
)?;
- if dirname != USE_CWD {
+ if dirname != USE_CWD.as_path() {
datadir = datadir.join(dirname);
}
let mut proc = Command::new(&argv0);
@@ -351,7 +353,7 @@ pub fn exit_code_is(context: &Runcmd, exit: i32) {
#[step]
pub fn exit_code_is_not(context: &Runcmd, exit: i32) {
if context.exitcode.is_none() || context.exitcode == Some(exit) {
- throw!(format!("Expected exit code to not equal {}", exit));
+ throw!(format!("Expected exit code to not equal {exit}"));
}
}
@@ -420,7 +422,7 @@ fn check_matches(runcmd: &Runcmd, which: Stream, how: MatchKind, against: &str)
#[step]
pub fn stdout_is(runcmd: &Runcmd, text: &str) {
if !check_matches(runcmd, Stream::Stdout, MatchKind::Exact, text)? {
- throw!(format!("stdout is not {:?}", text));
+ throw!(format!("stdout is not {text:?}"));
}
}
@@ -433,7 +435,7 @@ pub fn stdout_is(runcmd: &Runcmd, text: &str) {
#[step]
pub fn stdout_isnt(runcmd: &Runcmd, text: &str) {
if check_matches(runcmd, Stream::Stdout, MatchKind::Exact, text)? {
- throw!(format!("stdout is exactly {:?}", text));
+ throw!(format!("stdout is exactly {text:?}"));
}
}
@@ -446,7 +448,7 @@ pub fn stdout_isnt(runcmd: &Runcmd, text: &str) {
#[step]
pub fn stderr_is(runcmd: &Runcmd, text: &str) {
if !check_matches(runcmd, Stream::Stderr, MatchKind::Exact, text)? {
- throw!(format!("stderr is not {:?}", text));
+ throw!(format!("stderr is not {text:?}"));
}
}
@@ -459,7 +461,7 @@ pub fn stderr_is(runcmd: &Runcmd, text: &str) {
#[step]
pub fn stderr_isnt(runcmd: &Runcmd, text: &str) {
if check_matches(runcmd, Stream::Stderr, MatchKind::Exact, text)? {
- throw!(format!("stderr is exactly {:?}", text));
+ throw!(format!("stderr is exactly {text:?}"));
}
}
@@ -472,7 +474,7 @@ pub fn stderr_isnt(runcmd: &Runcmd, text: &str) {
#[step]
pub fn stdout_contains(runcmd: &Runcmd, text: &str) {
if !check_matches(runcmd, Stream::Stdout, MatchKind::Contains, text)? {
- throw!(format!("stdout does not contain {:?}", text));
+ throw!(format!("stdout does not contain {text:?}"));
}
}
@@ -485,7 +487,7 @@ pub fn stdout_contains(runcmd: &Runcmd, text: &str) {
#[step]
pub fn stdout_doesnt_contain(runcmd: &Runcmd, text: &str) {
if check_matches(runcmd, Stream::Stdout, MatchKind::Contains, text)? {
- throw!(format!("stdout contains {:?}", text));
+ throw!(format!("stdout contains {text:?}"));
}
}
@@ -498,7 +500,7 @@ pub fn stdout_doesnt_contain(runcmd: &Runcmd, text: &str) {
#[step]
pub fn stderr_contains(runcmd: &Runcmd, text: &str) {
if !check_matches(runcmd, Stream::Stderr, MatchKind::Contains, text)? {
- throw!(format!("stderr does not contain {:?}", text));
+ throw!(format!("stderr does not contain {text:?}"));
}
}
@@ -511,7 +513,7 @@ pub fn stderr_contains(runcmd: &Runcmd, text: &str) {
#[step]
pub fn stderr_doesnt_contain(runcmd: &Runcmd, text: &str) {
if check_matches(runcmd, Stream::Stderr, MatchKind::Contains, text)? {
- throw!(format!("stderr contains {:?}", text));
+ throw!(format!("stderr contains {text:?}"));
}
}
@@ -525,7 +527,7 @@ pub fn stderr_doesnt_contain(runcmd: &Runcmd, text: &str) {
#[step]
pub fn stdout_matches_regex(runcmd: &Runcmd, regex: &str) {
if !check_matches(runcmd, Stream::Stdout, MatchKind::Regex, regex)? {
- throw!(format!("stdout does not match {:?}", regex));
+ throw!(format!("stdout does not match {regex:?}"));
}
}
@@ -539,7 +541,7 @@ pub fn stdout_matches_regex(runcmd: &Runcmd, regex: &str) {
#[step]
pub fn stdout_doesnt_match_regex(runcmd: &Runcmd, regex: &str) {
if check_matches(runcmd, Stream::Stdout, MatchKind::Regex, regex)? {
- throw!(format!("stdout matches {:?}", regex));
+ throw!(format!("stdout matches {regex:?}"));
}
}
@@ -553,7 +555,7 @@ pub fn stdout_doesnt_match_regex(runcmd: &Runcmd, regex: &str) {
#[step]
pub fn stderr_matches_regex(runcmd: &Runcmd, regex: &str) {
if !check_matches(runcmd, Stream::Stderr, MatchKind::Regex, regex)? {
- throw!(format!("stderr does not match {:?}", regex));
+ throw!(format!("stderr does not match {regex:?}"));
}
}
@@ -567,6 +569,6 @@ pub fn stderr_matches_regex(runcmd: &Runcmd, regex: &str) {
#[step]
pub fn stderr_doesnt_match_regex(runcmd: &Runcmd, regex: &str) {
if check_matches(runcmd, Stream::Stderr, MatchKind::Regex, regex)? {
- throw!(format!("stderr matches {:?}", regex));
+ throw!(format!("stderr matches {regex:?}"));
}
}
diff --git a/subplotlib/src/types.rs b/subplotlib/src/types.rs
index 963ce87..d2377ca 100644
--- a/subplotlib/src/types.rs
+++ b/subplotlib/src/types.rs
@@ -11,7 +11,7 @@ pub type StepError = ::std::boxed::Box<dyn ::std::error::Error>;
/// Result type using [`StepError`].
///
/// This is useful for use in situations where you
-/// might use [`#[throws(...)]`][macro@fehler::throws]. Step functions
+/// might use [`#[throws(...)]`][macro@culpa::throws]. Step functions
/// generated using the [`step`][macro@subplotlib_derive::step] macro will
/// automatically set this as their return type by means of `#[throws(StepResult)]`.
pub type StepResult = ::std::result::Result<(), StepError>;
diff --git a/subplotlib/src/utils.rs b/subplotlib/src/utils.rs
index 642848a..25a9360 100644
--- a/subplotlib/src/utils.rs
+++ b/subplotlib/src/utils.rs
@@ -1,5 +1,7 @@
//! Utility functions used by subplotlib or the generated test functions
+use base64::prelude::{Engine as _, BASE64_STANDARD};
+
/// Decode a base64 string.
///
/// If the result is not a valid utf8 string then it is lossily coerced.
@@ -14,6 +16,6 @@
///
/// Will panic if it's not valid base64 leading to a string.
pub fn base64_decode(input: &str) -> String {
- let dec = base64::decode(input).expect("bad base64");
+ let dec = BASE64_STANDARD.decode(input).expect("bad base64");
String::from_utf8_lossy(&dec).to_string()
}
diff --git a/subplotlib/subplot-rust-support.rs b/subplotlib/subplot-rust-support.rs
index a63ef75..b502e7a 100644
--- a/subplotlib/subplot-rust-support.rs
+++ b/subplotlib/subplot-rust-support.rs
@@ -22,6 +22,7 @@ fn do_nothing(_context: &ScenarioContext) {
#[step]
#[context(SubplotContext)]
#[context(Runcmd)]
+#[allow(clippy::single_element_loop)]
fn install_subplot(context: &ScenarioContext) {
if let Some(bindir) = std::env::var_os("SUBPLOT_DIR") {
println!("Found SUBPLOT_DIR environment variable, using that");
@@ -60,8 +61,6 @@ set -eu
exec '{target_path}/{bin_name}' --resources '{src_dir}/share' "$@"
"#,
target_path = target_path.display(),
- bin_name = bin_name,
- src_dir = src_dir,
),
)?;
{
@@ -91,21 +90,21 @@ fn uninstall_subplot(context: &mut SubplotContext) {
#[step]
#[context(Runcmd)]
fn scenario_was_run(context: &ScenarioContext, name: &str) {
- let text = format!("\nscenario: {}\n", name);
+ let text = format!("\nscenario: {name}\n");
runcmd::stdout_contains::call(context, &text)?;
}
#[step]
#[context(Runcmd)]
fn scenario_was_not_run(context: &ScenarioContext, name: &str) {
- let text = format!("\nscenario: {}\n", name);
+ let text = format!("\nscenario: {name}\n");
runcmd::stdout_doesnt_contain::call(context, &text)?;
}
#[step]
#[context(Runcmd)]
fn step_was_run(context: &ScenarioContext, keyword: &str, name: &str) {
- let text = format!("\n step: {} {}\n", keyword, name);
+ let text = format!("\n step: {keyword} {name}\n");
runcmd::stdout_contains::call(context, &text)?;
}
@@ -118,10 +117,7 @@ fn step_was_run_and_then(
keyword2: &str,
name2: &str,
) {
- let text = format!(
- "\n step: {} {}\n step: {} {}",
- keyword1, name1, keyword2, name2
- );
+ let text = format!("\n step: {keyword1} {name1}\n step: {keyword2} {name2}");
runcmd::stdout_contains::call(context, &text)?;
}
@@ -134,17 +130,14 @@ fn cleanup_was_run(
keyword2: &str,
name2: &str,
) {
- let text = format!(
- "\n cleanup: {} {}\n cleanup: {} {}\n",
- keyword1, name1, keyword2, name2
- );
+ let text = format!("\n cleanup: {keyword1} {name1}\n cleanup: {keyword2} {name2}\n");
runcmd::stdout_contains::call(context, &text)?;
}
#[step]
#[context(Runcmd)]
fn cleanup_was_not_run(context: &ScenarioContext, keyword: &str, name: &str) {
- let text = format!("\n cleanup: {} {}\n", keyword, name);
+ let text = format!("\n cleanup: {keyword} {name}\n");
runcmd::stdout_doesnt_contain::call(context, &text)?;
}
@@ -161,7 +154,7 @@ fn end_of_file(context: &Datadir, filename: &str, nbytes: usize) -> Vec<u8> {
fn file_ends_in_zero_newlines(context: &Datadir, filename: &str) {
let b = end_of_file(context, filename, 1)?;
if b[0] == b'\n' {
- throw!(format!("File {} ends in unexpected newline", filename));
+ throw!(format!("File {filename} ends in unexpected newline"));
}
}
@@ -170,8 +163,7 @@ fn file_ends_in_one_newline(context: &Datadir, filename: &str) {
let b = end_of_file(context, filename, 2)?;
if !(b[0] != b'\n' && b[1] == b'\n') {
throw!(format!(
- "File {} does not end in exactly one newline",
- filename
+ "File {filename} does not end in exactly one newline",
));
}
}
@@ -181,18 +173,12 @@ fn file_ends_in_two_newlines(context: &Datadir, filename: &str) {
let b = end_of_file(context, filename, 2)?;
if b[0] != b'\n' || b[1] != b'\n' {
throw!(format!(
- "File {} does not end in exactly two newlines",
- filename
+ "File {filename} does not end in exactly two newlines",
));
}
}
#[step]
-fn sleep_seconds(_context: &Datadir, delay: u64) {
- std::thread::sleep(std::time::Duration::from_secs(delay));
-}
-
-#[step]
#[context(Datadir)]
#[context(Runcmd)]
fn json_output_matches_file(context: &ScenarioContext, filename: &str) {
@@ -208,12 +194,11 @@ fn json_output_matches_file(context: &ScenarioContext, filename: &str) {
let output: serde_json::Value = serde_json::from_str(&output)?;
let fcontent: serde_json::Value = serde_json::from_str(&fcontent)?;
println!("########");
- println!("Output:\n{:#}", output);
- println!("File:\n{:#}", fcontent);
+ println!("Output:\n{output:#}");
+ println!("File:\n{fcontent:#}");
println!("########");
assert_eq!(
output, fcontent,
- "Command output does not match the content of {}",
- filename
+ "Command output does not match the content of {filename}",
);
}
diff --git a/subplotlib/subplotlib.subplot b/subplotlib/subplotlib.subplot
new file mode 100644
index 0000000..e12804e
--- /dev/null
+++ b/subplotlib/subplotlib.subplot
@@ -0,0 +1,10 @@
+title: Testing the Rust crate "subplotlib"
+markdowns:
+ - subplotlib.md
+bindings:
+ - subplotlib.yaml
+ - lib/datadir.yaml
+impls:
+ rust:
+ - helpers/subplotlib_context.rs
+ - helpers/subplotlib_impl.rs
diff --git a/tests/bindings-ubm.rs b/tests/bindings-ubm.rs
index dc3be19..e4232d1 100644
--- a/tests/bindings-ubm.rs
+++ b/tests/bindings-ubm.rs
@@ -4,19 +4,24 @@
// there are a large number of them.
use regex::RegexBuilder;
-use std::collections::HashMap;
+use std::path::{Path, PathBuf};
use std::time::SystemTime;
-use subplot::{Binding, Bindings, ScenarioStep, StepKind};
+use std::{collections::HashMap, sync::Arc};
+use subplot::{html::Location, Binding, Bindings, ScenarioStep, StepKind};
const N: i32 = 1000;
+fn path() -> Arc<Path> {
+ PathBuf::new().into()
+}
+
#[test]
fn bindings_microbenchmark() {
let time = SystemTime::now();
let mut texts = vec![];
for i in 0..N {
- texts.push(format!("step {}", i));
+ texts.push(format!("step {i}"));
}
let texted = time.elapsed().unwrap();
@@ -24,7 +29,7 @@ fn bindings_microbenchmark() {
for t in texts.iter() {
re.push((
t,
- RegexBuilder::new(&format!("^{}$", t))
+ RegexBuilder::new(&format!("^{t}$"))
.case_insensitive(false)
.build()
.unwrap(),
@@ -34,7 +39,7 @@ fn bindings_microbenchmark() {
let mut toadd = vec![];
for t in texts.iter() {
- toadd.push(Binding::new(StepKind::Given, t, false, HashMap::new()).unwrap());
+ toadd.push(Binding::new(StepKind::Given, t, false, HashMap::new(), None, path()).unwrap());
}
let created = time.elapsed().unwrap();
@@ -43,7 +48,12 @@ fn bindings_microbenchmark() {
bindings.add(binding);
}
let added = time.elapsed().unwrap();
- let step = ScenarioStep::new(StepKind::Given, "given", &format!("step {}", N - 1));
+ let step = ScenarioStep::new(
+ StepKind::Given,
+ "given",
+ &format!("step {}", N - 1),
+ Location::Unknown,
+ );
bindings.find("", &step).unwrap();
let found = time.elapsed().unwrap();
diff --git a/tests/subplots/common/files.md b/tests/subplots/common/files.md
index d8d598b..521dd28 100644
--- a/tests/subplots/common/files.md
+++ b/tests/subplots/common/files.md
@@ -95,14 +95,3 @@ and directory second/third is not empty
when I remove directory second
then directory second does not exist
```
-
----
-title: Acceptance criteria for the files Subplot library
-author: The Subplot project
-bindings:
-- lib/files.yaml
-impls:
- python:
- - lib/files.py
- rust: []
-...
diff --git a/tests/subplots/common/files.subplot b/tests/subplots/common/files.subplot
new file mode 100644
index 0000000..75726f9
--- /dev/null
+++ b/tests/subplots/common/files.subplot
@@ -0,0 +1,11 @@
+title: Acceptance criteria for the files Subplot library
+authors:
+ - The Subplot project
+markdowns:
+- files.md
+bindings:
+- lib/files.yaml
+impls:
+ python:
+ - lib/files.py
+ rust: []
diff --git a/tests/subplots/common/runcmd.md b/tests/subplots/common/runcmd.md
index 4f66685..4fdb3f3 100644
--- a/tests/subplots/common/runcmd.md
+++ b/tests/subplots/common/runcmd.md
@@ -197,20 +197,3 @@ given helper script err.sh for runcmd
when I run sh err.sh hi
then stderr doesn't match regex world$
~~~
-
-
----
-title: Acceptance criteria for the lib/runcmd Subplot library
-author: The Subplot project
-bindings:
-- lib/runcmd.yaml
-- runcmd_test.yaml
-- lib/files.yaml
-impls:
- python:
- - lib/runcmd.py
- - runcmd_test.py
- - lib/files.py
- rust:
- - runcmd_test.rs
-...
diff --git a/tests/subplots/common/runcmd.subplot b/tests/subplots/common/runcmd.subplot
new file mode 100644
index 0000000..dcfd590
--- /dev/null
+++ b/tests/subplots/common/runcmd.subplot
@@ -0,0 +1,16 @@
+title: Acceptance criteria for the lib/runcmd Subplot library
+authors:
+ - The Subplot project
+markdowns:
+- runcmd.md
+bindings:
+- lib/runcmd.yaml
+- runcmd_test.yaml
+- lib/files.yaml
+impls:
+ python:
+ - lib/runcmd.py
+ - runcmd_test.py
+ - lib/files.py
+ rust:
+ - runcmd_test.rs
diff --git a/tests/subplots/common/runcmd_test.rs b/tests/subplots/common/runcmd_test.rs
index 7759e5f..d3a7ca5 100644
--- a/tests/subplots/common/runcmd_test.rs
+++ b/tests/subplots/common/runcmd_test.rs
@@ -1,3 +1,4 @@
+use std::path::Path;
use subplotlib::steplibrary::files::{self, Datadir};
use subplotlib::steplibrary::runcmd::Runcmd;
@@ -8,7 +9,7 @@ use std::os::unix::fs::PermissionsExt;
#[context(Datadir)]
fn create_script_from_embedded(
context: &ScenarioContext,
- filename: &str,
+ filename: &Path,
embedded: SubplotDataFile,
) {
files::create_from_embedded_with_other_name::call(context, filename, embedded)?;
@@ -20,6 +21,6 @@ fn create_script_from_embedded(
}
#[step]
-fn prepend_to_path(context: &mut Runcmd, dirname: &str) {
+fn prepend_to_path(context: &mut Runcmd, dirname: &Path) {
context.prepend_to_path(dirname);
}
diff --git a/tests/subplots/common/runcmd_test.yaml b/tests/subplots/common/runcmd_test.yaml
index daab202..e53c2d7 100644
--- a/tests/subplots/common/runcmd_test.yaml
+++ b/tests/subplots/common/runcmd_test.yaml
@@ -1,9 +1,12 @@
-- given: "executable script {filename} from {embedded:file}"
+- given: "executable script {filename} from {embedded}"
impl:
python:
function: create_script_from_embedded
rust:
function: create_script_from_embedded
+ types:
+ filename: path
+ embedded: file
- when: "I prepend {dirname} to PATH"
impl:
@@ -11,3 +14,5 @@
function: runcmd_prepend_to_path
rust:
function: prepend_to_path
+ types:
+ dirname: path