summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--CONTRIBUTING.md14
-rw-r--r--Cargo.lock1209
-rw-r--r--Cargo.toml25
-rw-r--r--DCO-1-1.txt34
-rw-r--r--DECISIONS.md139
-rw-r--r--NEWS.md60
-rw-r--r--README.md6
-rw-r--r--RELEASE.md7
-rwxr-xr-xbuild-docs48
-rw-r--r--build.rs12
-rwxr-xr-xcheck113
-rw-r--r--debian/changelog24
-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--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/seq-extras.rs6
-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.yaml58
-rw-r--r--share/common/lib/runcmd.yaml65
-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.tera2
-rw-r--r--share/rust/template/template.rs.tera2
-rw-r--r--share/subplot.css40
-rw-r--r--src/ast.rs483
-rw-r--r--src/bin/cli/mod.rs20
-rw-r--r--src/bin/subplot.rs200
-rw-r--r--src/bindings.rs279
-rw-r--r--src/codegen.rs33
-rw-r--r--src/diagrams.rs2
-rw-r--r--src/doc.rs749
-rw-r--r--src/embedded.rs10
-rw-r--r--src/error.rs84
-rw-r--r--src/html.rs918
-rw-r--r--src/lib.rs23
-rw-r--r--src/matches.rs42
-rw-r--r--src/md.rs801
-rw-r--r--src/metadata.rs432
-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/embedded.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.toml7
-rw-r--r--subplot-build/src/lib.rs7
-rw-r--r--subplot.md789
-rw-r--r--subplot.py5
-rw-r--r--subplot.yaml28
-rw-r--r--subplotlib-derive/Cargo.toml9
-rw-r--r--subplotlib-derive/src/lib.rs21
-rw-r--r--subplotlib/Cargo.toml15
-rw-r--r--subplotlib/build.rs8
-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.rs20
-rw-r--r--subplotlib/src/steplibrary/datadir.rs3
-rw-r--r--subplotlib/src/steplibrary/files.rs8
-rw-r--r--subplotlib/src/steplibrary/runcmd.rs26
-rw-r--r--subplotlib/src/types.rs2
-rw-r--r--subplotlib/src/utils.rs4
-rw-r--r--subplotlib/subplot-rust-support.rs40
-rw-r--r--tests/bindings-ubm.rs22
92 files changed, 5149 insertions, 3105 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index de9baa2..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.60
- - ./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 befad05..2de1d69 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,40 +3,84 @@
version = 3
[[package]]
-name = "ahash"
-version = "0.7.6"
+name = "aho-corasick"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
dependencies = [
- "getrandom",
- "once_cell",
- "version_check",
+ "memchr",
]
[[package]]
-name = "aho-corasick"
-version = "0.7.19"
+name = "aligned"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+checksum = "80a21b9440a626c7fc8573a9e3d3a06b75c7c97754c2949bc7857b90353ca655"
dependencies = [
- "memchr",
+ "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.66"
+version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
+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 0.1.19",
- "libc",
- "winapi",
+ "stable_deref_trait",
]
[[package]]
@@ -47,9 +91,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
-version = "0.13.1"
+version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "bitflags"
@@ -58,28 +102,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
+name = "bitflags"
+version = "2.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+
+[[package]]
name = "block-buffer"
-version = "0.10.3"
+version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"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 = "bumpalo"
+version = "3.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
+
+[[package]]
name = "cc"
-version = "1.0.76"
+version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f"
+checksum = "3286b845d0fccbdd15af433f61c5970e711987036cb468f437ff6badd70f4e24"
[[package]]
name = "cfg-if"
@@ -89,57 +146,33 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
-version = "3.2.23"
+version = "4.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5"
+checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642"
dependencies = [
- "atty",
- "bitflags",
- "clap_derive 3.2.18",
- "clap_lex 0.2.4",
- "indexmap",
- "once_cell",
- "strsim",
- "termcolor",
- "textwrap 0.16.0",
+ "clap_builder",
+ "clap_derive",
]
[[package]]
-name = "clap"
-version = "4.0.29"
+name = "clap_builder"
+version = "4.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d"
+checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9"
dependencies = [
- "bitflags",
- "clap_derive 4.0.21",
- "clap_lex 0.3.0",
- "is-terminal",
- "once_cell",
+ "anstream",
+ "anstyle",
+ "clap_lex",
"strsim",
- "termcolor",
-]
-
-[[package]]
-name = "clap_derive"
-version = "3.2.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
-dependencies = [
- "heck",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
]
[[package]]
name = "clap_derive"
-version = "4.0.21"
+version = "4.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
+checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
dependencies = [
"heck",
- "proc-macro-error",
"proc-macro2",
"quote",
"syn",
@@ -147,73 +180,49 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.2.4"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
-dependencies = [
- "os_str_bytes",
-]
+checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
[[package]]
-name = "clap_lex"
-version = "0.3.0"
+name = "colorchoice"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
-dependencies = [
- "os_str_bytes",
-]
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "cpufeatures"
-version = "0.2.5"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
dependencies = [
"libc",
]
[[package]]
-name = "crossbeam-channel"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
-dependencies = [
- "cfg-if",
- "crossbeam-utils",
-]
-
-[[package]]
name = "crossbeam-deque"
-version = "0.8.2"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
dependencies = [
- "cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
-version = "0.9.11"
+version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
- "autocfg",
- "cfg-if",
"crossbeam-utils",
- "memoffset",
- "scopeguard",
]
[[package]]
name = "crossbeam-utils"
-version = "0.8.12"
+version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
-dependencies = [
- "cfg-if",
-]
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crypto-common"
@@ -226,91 +235,95 @@ dependencies = [
]
[[package]]
-name = "deunicode"
-version = "0.4.3"
+name = "culpa"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
+checksum = "5ae0bfe9317b1cb4ff5a56d766ee4b157b3e1f47f11979253570e88d10fd1fd3"
+dependencies = [
+ "culpa-macros",
+]
[[package]]
-name = "digest"
-version = "0.10.5"
+name = "culpa-macros"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
+checksum = "1234e1717066d3c71dcf89b75e7b586299e41204d361db56ec51e6ded5014279"
dependencies = [
- "block-buffer",
- "crypto-common",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "either"
-version = "1.8.0"
+name = "cvt"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+checksum = "d2ae9bf77fbf2d39ef573205d554d87e86c12f1994e9ea335b0651b9b278bcf1"
+dependencies = [
+ "cfg-if",
+]
[[package]]
-name = "env_logger"
-version = "0.9.3"
+name = "deranged"
+version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
- "atty",
- "humantime",
- "log",
- "regex",
- "termcolor",
+ "powerfmt",
]
[[package]]
-name = "errno"
-version = "0.2.8"
+name = "deunicode"
+version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
-dependencies = [
- "errno-dragonfly",
- "libc",
- "winapi",
-]
+checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94"
[[package]]
-name = "errno-dragonfly"
-version = "0.1.2"
+name = "digest"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
- "cc",
- "libc",
+ "block-buffer",
+ "crypto-common",
]
[[package]]
-name = "fastrand"
-version = "1.8.0"
+name = "env_logger"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
- "instant",
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
]
[[package]]
-name = "fehler"
-version = "1.0.0"
+name = "equivalent"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5729fe49ba028cd550747b6e62cd3d841beccab5390aa398538c31a2d983635"
-dependencies = [
- "fehler-macros",
-]
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
-name = "fehler-macros"
-version = "1.0.0"
+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"
@@ -318,23 +331,17 @@ checksum = "31a7a908b8f32538a2143e59a6e4e2508988832d5d4d6f7c156b3cbc762643a5"
[[package]]
name = "filetime"
-version = "0.2.18"
+version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3"
+checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
- "windows-sys",
+ "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"
@@ -345,10 +352,24 @@ 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.1"
+version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7"
+checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
@@ -359,9 +380,9 @@ dependencies = [
[[package]]
name = "generic-array"
-version = "0.14.6"
+version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
@@ -378,9 +399,9 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.8"
+version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if",
"libc",
@@ -389,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",
@@ -412,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.9"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
+checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
dependencies = [
"aho-corasick",
"bstr",
- "fnv",
"log",
- "regex",
+ "regex-automata 0.4.5",
+ "regex-syntax 0.8.2",
]
[[package]]
@@ -435,7 +455,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"ignore",
"walkdir",
]
@@ -445,39 +465,42 @@ name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
-dependencies = [
- "ahash",
-]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+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 = "hermit-abi"
-version = "0.2.6"
+name = "html-escape"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
- "libc",
+ "utf8-width",
]
[[package]]
name = "humansize"
-version = "1.1.1"
+version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
[[package]]
name = "humantime"
@@ -487,89 +510,80 @@ 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.9.1"
+version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+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 = "io-lifetimes"
-version = "1.0.3"
+name = "is-terminal"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
dependencies = [
+ "hermit-abi",
"libc",
- "windows-sys",
+ "windows-sys 0.52.0",
]
[[package]]
-name = "is-terminal"
-version = "0.4.1"
+name = "itoa"
+version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330"
-dependencies = [
- "hermit-abi 0.2.6",
- "io-lifetimes",
- "rustix",
- "windows-sys",
-]
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
-name = "itertools"
-version = "0.8.2"
+name = "lazy_static"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
-dependencies = [
- "either",
-]
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
-name = "itoa"
-version = "1.0.4"
+name = "libc"
+version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
-name = "lazy_static"
-version = "1.4.0"
+name = "libm"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
-name = "libc"
-version = "0.2.137"
+name = "line-col"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+checksum = "9e69cdf6b85b5c8dce514f694089a2cf8b1a702f6cd28607bcb3cf296c9778db"
[[package]]
name = "linked-hash-map"
@@ -579,18 +593,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
-version = "0.1.3"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "log"
-version = "0.4.17"
+version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
-dependencies = [
- "cfg-if",
-]
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "loom"
@@ -613,29 +624,34 @@ 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.5.0"
+version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+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"
+name = "normpath"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
+checksum = "ec60c60a693226186f5d6edf073232bfb6464ed97eb22cf3b01c1e8198fd97f5"
+dependencies = [
+ "windows-sys 0.48.0",
+]
[[package]]
name = "nu-ansi-term"
@@ -648,26 +664,16 @@ dependencies = [
]
[[package]]
-name = "num_cpus"
-version = "1.14.0"
+name = "num-conv"
+version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
-dependencies = [
- "hermit-abi 0.1.19",
- "libc",
-]
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "once_cell"
-version = "1.16.0"
+version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
-
-[[package]]
-name = "os_str_bytes"
-version = "6.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "overload"
@@ -676,57 +682,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
-name = "pandoc"
-version = "0.8.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2eb8469d27ed9fd7925629076a3675fea964c3f44c49662bdf549a8b7ddf0820"
-dependencies = [
- "itertools",
-]
-
-[[package]]
-name = "pandoc_ast"
-version = "0.7.3"
-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.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3f66e753ae68f272480f3eaedf07798d758cdfab3903724c119a827a0e5536e"
-dependencies = [
- "serde",
- "serde_derive",
- "serde_json",
-]
-
-[[package]]
name = "percent-encoding"
-version = "2.2.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
-version = "2.4.1"
+version = "2.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8"
+checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546"
dependencies = [
+ "memchr",
"thiserror",
"ucd-trie",
]
[[package]]
name = "pest_derive"
-version = "2.4.1"
+version = "2.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5fd9bc6500181952d34bd0b2b0163a54d794227b498be0b7afa7698d0a7b18f"
+checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809"
dependencies = [
"pest",
"pest_generator",
@@ -734,9 +710,9 @@ dependencies = [
[[package]]
name = "pest_generator"
-version = "2.4.1"
+version = "2.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2610d5ac5156217b4ff8e46ddcef7cdf44b273da2ac5bca2ecbfa86a330e7c4"
+checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e"
dependencies = [
"pest",
"pest_meta",
@@ -747,20 +723,20 @@ dependencies = [
[[package]]
name = "pest_meta"
-version = "2.4.1"
+version = "2.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "824749bf7e21dd66b36fbe26b3f45c713879cccd4a009a917ab8e045ca8246fe"
+checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a"
dependencies = [
"once_cell",
"pest",
- "sha1",
+ "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",
@@ -768,56 +744,38 @@ dependencies = [
[[package]]
name = "pin-project-lite"
-version = "0.2.9"
+version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
-name = "ppv-lite86"
-version = "0.2.17"
+name = "powerfmt"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
-
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-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.47"
+version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pulldown-cmark"
-version = "0.9.2"
+version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d9cc634bc78768157b5cbfe988ffcd1dcba95cd2b2f03a88316c08c6d00ed63"
+checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
dependencies = [
- "bitflags",
+ "bitflags 2.4.2",
"getopts",
"memchr",
"unicase",
@@ -825,9 +783,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.21"
+version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@@ -863,47 +821,24 @@ dependencies = [
]
[[package]]
-name = "rayon"
-version = "1.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d"
-dependencies = [
- "autocfg",
- "crossbeam-deque",
- "either",
- "rayon-core",
-]
-
-[[package]]
-name = "rayon-core"
-version = "1.9.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f"
-dependencies = [
- "crossbeam-channel",
- "crossbeam-deque",
- "crossbeam-utils",
- "num_cpus",
-]
-
-[[package]]
name = "redox_syscall"
-version = "0.2.16"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
]
[[package]]
name = "regex"
-version = "1.7.0"
+version = "1.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax",
+ "regex-automata 0.4.5",
+ "regex-syntax 0.8.2",
]
[[package]]
@@ -912,76 +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.28"
+version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+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.5"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f0c08002dd427499194cef0e292cfd515281777d5b9cc4c638028d2d3aebda4"
+checksum = "a129e44a647b309ed394a092e21eabcb58537802c6912920ef4ea76239421234"
dependencies = [
"anyhow",
- "clap 3.2.23",
"serde",
- "serde_yaml",
- "textwrap 0.15.2",
+ "serde_yaml 0.8.26",
+ "textwrap",
"thiserror",
]
[[package]]
name = "rustix"
-version = "0.36.4"
+version = "0.38.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23"
+checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
dependencies = [
- "bitflags",
+ "bitflags 2.4.2",
"errno",
- "io-lifetimes",
"libc",
"linux-raw-sys",
- "windows-sys",
+ "windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
-version = "1.0.9"
+version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "ryu"
-version = "1.0.11"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "same-file"
@@ -999,25 +943,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
-name = "scopeguard"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
-
-[[package]]
name = "serde"
-version = "1.0.147"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-aux"
-version = "4.1.0"
+version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb6a3148cb21f1afb585b9ce6aeea9e58bd02c37ddb336277af10396ca3574fd"
+checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95"
dependencies = [
"serde",
"serde_json",
@@ -1025,9 +963,9 @@ dependencies = [
[[package]]
name = "serde_derive"
-version = "1.0.147"
+version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
@@ -1036,9 +974,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.87"
+version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45"
+checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
@@ -1051,17 +989,30 @@ version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
dependencies = [
- "indexmap",
+ "indexmap 1.9.3",
"ryu",
"serde",
"yaml-rust",
]
[[package]]
-name = "sha1"
-version = "0.10.5"
+name = "serde_yaml"
+version = "0.9.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f"
+dependencies = [
+ "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 = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
@@ -1070,9 +1021,9 @@ dependencies = [
[[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",
]
@@ -1085,24 +1036,31 @@ 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.10.0"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smawk"
-version = "0.3.1"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "state"
@@ -1121,19 +1079,18 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "subplot"
-version = "0.6.0"
+version = "0.9.0"
dependencies = [
"anyhow",
"base64",
- "clap 4.0.29",
+ "clap",
"env_logger",
"file_diff",
"git-testament",
+ "html-escape",
"lazy_static",
+ "line-col",
"log",
- "pandoc",
- "pandoc_ast 0.7.3",
- "pandoc_ast 0.8.2",
"pikchr",
"pulldown-cmark",
"regex",
@@ -1141,7 +1098,7 @@ dependencies = [
"serde",
"serde-aux",
"serde_json",
- "serde_yaml",
+ "serde_yaml 0.9.32",
"tempfile",
"tempfile-fast",
"tera",
@@ -1152,7 +1109,7 @@ dependencies = [
[[package]]
name = "subplot-build"
-version = "0.6.0"
+version = "0.9.0"
dependencies = [
"subplot",
"tempfile",
@@ -1163,23 +1120,23 @@ dependencies = [
name = "subplot-seq-example"
version = "0.1.0"
dependencies = [
- "fehler",
+ "culpa",
"subplot-build",
"subplotlib",
]
[[package]]
name = "subplotlib"
-version = "0.6.0"
+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",
@@ -1192,9 +1149,9 @@ dependencies = [
[[package]]
name = "subplotlib-derive"
-version = "0.6.0"
+version = "0.9.0"
dependencies = [
- "fehler",
+ "culpa",
"proc-macro2",
"quote",
"syn",
@@ -1202,9 +1159,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "1.0.103"
+version = "2.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
dependencies = [
"proc-macro2",
"quote",
@@ -1213,16 +1170,14 @@ dependencies = [
[[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]]
@@ -1238,9 +1193,9 @@ dependencies = [
[[package]]
name = "tera"
-version = "1.17.1"
+version = "1.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3df578c295f9ec044ff1c829daf31bb7581d5b3c2a7a3d87419afe1f2531438c"
+checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8"
dependencies = [
"globwalk",
"humansize",
@@ -1258,9 +1213,9 @@ 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",
]
@@ -1277,25 +1232,19 @@ dependencies = [
]
[[package]]
-name = "textwrap"
-version = "0.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
-
-[[package]]
name = "thiserror"
-version = "1.0.37"
+version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.37"
+version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
@@ -1304,20 +1253,24 @@ 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.17"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
+checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
dependencies = [
+ "deranged",
"itoa",
+ "num-conv",
+ "powerfmt",
"serde",
"time-core",
"time-macros",
@@ -1325,26 +1278,26 @@ dependencies = [
[[package]]
name = "time-core"
-version = "0.1.0"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
-version = "0.2.6"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
+checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
dependencies = [
+ "num-conv",
"time-core",
]
[[package]]
name = "tracing"
-version = "0.1.37"
+version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
- "cfg-if",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -1352,9 +1305,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.23"
+version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
@@ -1363,9 +1316,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.30"
+version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
@@ -1373,20 +1326,20 @@ dependencies = [
[[package]]
name = "tracing-log"
-version = "0.1.3"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
- "lazy_static",
"log",
+ "once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
-version = "0.3.16"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70"
+checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -1402,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.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
[[package]]
name = "unescape"
@@ -1470,34 +1423,48 @@ 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-ident"
-version = "1.0.5"
+version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-linebreak"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
-dependencies = [
- "hashbrown",
- "regex",
-]
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-width"
-version = "0.1.10"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b"
+
+[[package]]
+name = "utf8-width"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "valuable"
@@ -1513,12 +1480,11 @@ 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",
]
@@ -1529,6 +1495,60 @@ 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 = "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"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1546,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",
]
@@ -1561,103 +1581,210 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
-version = "0.32.0"
+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 = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
- "windows_aarch64_msvc 0.32.0",
- "windows_i686_gnu 0.32.0",
- "windows_i686_msvc 0.32.0",
- "windows_x86_64_gnu 0.32.0",
- "windows_x86_64_msvc 0.32.0",
+ "windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
-version = "0.42.0"
+version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc 0.42.0",
- "windows_i686_gnu 0.42.0",
- "windows_i686_msvc 0.42.0",
- "windows_x86_64_gnu 0.42.0",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc 0.42.0",
+ "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.42.0"
+version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.32.0"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+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.32.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.0"
+version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb"
[[package]]
name = "windows_i686_msvc"
-version = "0.32.0"
+version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.0"
+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 = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.32.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.0"
+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 = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.0"
+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 = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.32.0"
+version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.0"
+version = "0.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6"
[[package]]
name = "yaml-rust"
diff --git a/Cargo.toml b/Cargo.toml
index 6aa3727..658733e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,11 +1,11 @@
[package]
name = "subplot"
-version = "0.6.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,
@@ -13,42 +13,37 @@ and implementing automated acceptance tests for systems and software'''
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"
+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"
pulldown-cmark = "0.9.0"
regex = "1"
-roadmap = "0.4.5"
+roadmap = "0.5.0"
serde = { version = "1.0.101", features = ["derive"] }
serde-aux = { version = "4.0", default-features = false }
serde_json = "1.0"
-serde_yaml = "0.8.26"
+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 bc6b766..b192fdc 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -13,26 +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.
-## Threshold for refactoring
+## Adopting a Developer Certificate of Origin
-Date: 2022-10-23
+Date: 2023-10-07
-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.
+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 e6785bf..61a8537 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -4,23 +4,65 @@ 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
+- 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
+- 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.
+- 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
+- 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
+- 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
@@ -28,16 +70,16 @@ the user of the Subplot software.
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
+- 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
+- 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
+- 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
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 845c41b..98d6ef7 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -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
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 755a483..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,10 +275,11 @@ 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)
@@ -271,6 +288,9 @@ def check_subplots(r):
"**/*.subplot",
lambda f: f == f.lower() and "subplotlib" not in f and "test-outputs" not in f,
)
+ 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}")
@@ -280,7 +300,7 @@ def check_subplots(r):
doc_template = None
- for template in r.get_templates(subplot0):
+ for template in r.get_templates(subplot0, strict):
if doc_template is None:
doc_template = template
if template == "python":
@@ -320,8 +340,9 @@ def check_subplots(r):
base = os.path.basename(subplot)
base, _ = os.path.splitext(subplot)
base = os.path.join(output, base)
- r.docgen(subplot, doc_template, base + ".pdf", cwd=dirname)
- r.docgen(subplot, 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 67e3321..3f38e7f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,27 @@
+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.
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/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/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/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 6a43b44..6e546c6 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,12 +1,15 @@
{
"nodes": {
"flake-utils": {
+ "inputs": {
+ "systems": "systems"
+ },
"locked": {
- "lastModified": 1656928814,
- "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
+ "lastModified": 1694529238,
+ "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
+ "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
@@ -17,10 +20,10 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1657802959,
- "narHash": "sha256-9+JWARSdlL8KiH3ymnKDXltE1vM+/WEJ78F5B1kjXys=",
- "path": "/nix/store/v3s2diqbiykxvlfzcgjcpgk9fhbhy9mg-source",
- "rev": "4a01ca36d6bfc133bc617e661916a81327c9bbc8",
+ "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 cb39b96..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:
@@ -20,6 +23,9 @@
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 "{text}" to file {filename}
impl:
@@ -30,6 +36,8 @@
types:
filename: path
text: text
+ doc: |
+ Create a file on disk with the given content.
# Manage directories (distinct from files).
@@ -41,6 +49,8 @@
function: files_make_directory
types:
path: path
+ doc: |
+ Create a directory on disk.
- when: I create directory {path}
impl:
@@ -50,6 +60,8 @@
function: files_make_directory
types:
path: path
+ doc: |
+ Create a directory on disk.
- when: I remove directory {path}
impl:
@@ -59,6 +71,8 @@
function: files_remove_directory
types:
path: path
+ doc: |
+ Remove a directory on disk.
- then: directory {path} exists
impl:
@@ -68,6 +82,8 @@
function: files_directory_exists
types:
path: path
+ doc: |
+ Check that a directory exists.
- then: directory {path} does not exist
impl:
@@ -77,6 +93,8 @@
function: files_directory_does_not_exist
types:
path: path
+ doc: |
+ Check that a directory does not exist.
- then: directory {path} is empty
impl:
@@ -86,6 +104,8 @@
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:
@@ -95,6 +115,8 @@
function: files_directory_is_not_empty
types:
path: path
+ doc: |
+ Check that a directory exists and contains something.
# File metadata management and testing.
@@ -108,6 +130,8 @@
types:
filename: path
mtime: text
+ doc: |
+ Create a file with specific modification time.
- when: I remember metadata for file {filename}
impl:
@@ -117,6 +141,8 @@
function: files_remember_metadata
types:
filename: path
+ doc: |
+ Remember the metadata of a file.
- when: I touch file {filename}
impl:
@@ -126,6 +152,8 @@
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:
@@ -135,6 +163,9 @@
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:
@@ -144,6 +175,8 @@
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:
@@ -153,6 +186,8 @@
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:
@@ -162,6 +197,8 @@
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:
@@ -171,6 +208,8 @@
function: files_mtime_is_ancient
types:
filename: path
+ doc: |
+ Check that file modification is far in the past.
# Testing file existence.
@@ -182,6 +221,8 @@
function: files_file_exists
types:
filename: path
+ doc: |
+ Check that a file exist.
- then: file {filename} does not exist
impl:
@@ -191,6 +232,8 @@
function: files_file_does_not_exist
types:
filename: path
+ doc: |
+ Check that a file does not exist.
- then: only files (?P<filenames>.+) exist
impl:
@@ -199,6 +242,10 @@
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.
@@ -211,6 +258,8 @@
types:
filename: path
data: text
+ doc: |
+ Check that a file contains a string.
- then: file {filename} doesn't contain "{data}"
impl:
@@ -221,6 +270,8 @@
types:
filename: path
data: text
+ doc: |
+ Check that a file does not contain a string.
- then: file {filename} matches regex /{regex}/
impl:
@@ -231,6 +282,8 @@
types:
filename: path
regex: text
+ doc: |
+ Check that file content matches a regular expression.
- then: file {filename} matches regex "{regex}"
impl:
@@ -241,6 +294,8 @@
types:
filename: path
regex: text
+ doc: |
+ Check that file content matches a regular expression.
- then: files {filename1} and {filename2} match
impl:
@@ -251,4 +306,5 @@
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 7be2c05..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,6 +28,8 @@
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}
impl:
@@ -33,6 +41,9 @@
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:
@@ -40,6 +51,9 @@
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}
impl:
@@ -51,6 +65,9 @@
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.
@@ -62,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:
@@ -71,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:
@@ -78,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:
@@ -85,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.
@@ -94,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:
@@ -101,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:
@@ -108,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:
@@ -115,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.
@@ -124,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:
@@ -131,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:
@@ -138,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:
@@ -145,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.
@@ -154,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:
@@ -161,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:
@@ -168,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:
@@ -175,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 104eb23..cc1d9c4 100644
--- a/share/rust/template/macros.rs.tera
+++ b/share/rust/template/macros.rs.tera
@@ -27,5 +27,5 @@
)
{% 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 14a57be..0000000
--- a/src/ast.rs
+++ /dev/null
@@ -1,483 +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 serde_yaml::{Mapping, Value};
-use std::collections::{BTreeMap, HashMap};
-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: YamlMetadata,
-}
-
-impl AbstractSyntaxTree {
- /// Create a new AST.
- pub fn new(meta: YamlMetadata, markdown: &str) -> Self {
- let blocks = parse_blocks(markdown);
- Self { blocks, meta }
- }
-
- /// Return a Pandoc-compatible AST.
- pub fn to_pandoc(&self) -> Pandoc {
- Pandoc {
- meta: self.meta.to_map(),
- blocks: self.blocks.clone(),
- pandoc_api_version: vec![1, 20],
- }
- }
-}
-
-/// Extract YAML metadata from a Markdown document.
-pub fn extract_metadata(markdown: &str) -> Result<(YamlMetadata, &str), Error> {
- trace!("Extracting YAML from Markdown");
- let (yaml, md) = if let Some((yaml, markdown)) = get_yaml(&LEADING_YAML_PATTERN, markdown) {
- trace!("Found leading YAML: {:?}", yaml);
- (yaml, markdown)
- } else if let Some((yaml, _markdown)) = get_yaml(&TRAILING_YAML_PATTERN, markdown) {
- trace!("Found trailing YAML: {:?}", yaml);
- (yaml, markdown)
- } else {
- trace!("No YAML to be found");
- return Err(Error::NoMetadata);
- };
- let meta = YamlMetadata::new(yaml)?;
- trace!("Parsing markdown: OK");
- Ok((meta, md))
-}
-
-// 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("Markdown doesn't contain a YAML block for document metadata")]
- NoMetadata,
-
- #[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>>,
- bibliography: Option<Vec<PathBuf>>,
- markdowns: Vec<PathBuf>,
- bindings: Option<Vec<PathBuf>>,
- documentclass: Option<String>,
- #[serde(default)]
- impls: BTreeMap<String, Vec<PathBuf>>,
- pandoc: Option<HashMap<String, Value>>,
-}
-
-impl YamlMetadata {
- fn new(yaml_text: &str) -> Result<Self, Error> {
- trace!("Parsing YAML");
- let meta: Self = serde_yaml::from_str(yaml_text)?;
- Ok(meta)
- }
-
- /// Name of file with the Markdown for the subplot document.
- pub fn markdown(&self) -> &Path {
- &self.markdowns[0]
- }
-
- /// Convert into a pandoc_ast::Map.
- pub fn to_map(&self) -> Map<String, MetaValue> {
- trace!("Creating metadata map from parsed YAML");
- let mut map: Map<String, MetaValue> = Map::new();
-
- map.insert("title".into(), meta_string(&self.title));
-
- if let Some(v) = &self.subtitle {
- map.insert("subtitle".into(), meta_string(v));
- }
-
- if let Some(authors) = &self.authors {
- let authors: Vec<MetaValue> = authors
- .iter()
- .map(|s| MetaValue::MetaString(s.into()))
- .collect();
- map.insert("author".into(), MetaValue::MetaList(authors));
- }
-
- if let Some(v) = &self.date {
- map.insert("date".into(), meta_string(v));
- }
-
- if let Some(v) = &self.classes {
- map.insert("classes".into(), 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("impls".into(), MetaValue::MetaMap(impls));
- }
-
- if let Some(v) = &self.bibliography {
- map.insert("bibliography".into(), meta_path_bufs(v));
- }
-
- if let Some(v) = &self.bindings {
- map.insert("bindings".into(), meta_path_bufs(v));
- }
-
- if let Some(v) = &self.documentclass {
- map.insert("documentclass".into(), meta_string(v));
- }
-
- if let Some(pandoc) = &self.pandoc {
- for (key, value) in pandoc.iter() {
- map.insert(key.to_string(), value_to_pandoc(value));
- }
- }
-
- trace!("Created metadata map from parsed YAML");
- map
- }
-}
-
-fn mapping_to_pandoc(mapping: &Mapping) -> MetaValue {
- let mut map = Map::new();
- for (key, value) in mapping.iter() {
- let key = if let MetaValue::MetaString(s) = value_to_pandoc(key) {
- s
- } else {
- panic!("key not a string: {:?}", key);
- };
- map.insert(key, Box::new(value_to_pandoc(value)));
- }
-
- MetaValue::MetaMap(map)
-}
-
-fn value_to_pandoc(data: &Value) -> MetaValue {
- match data {
- Value::Null => unreachable!("null not OK"),
- Value::Number(_) => unreachable!("number not OK"),
- Value::Sequence(_) => unreachable!("sequence not OK"),
-
- Value::Bool(b) => MetaValue::MetaBool(*b),
- Value::String(s) => MetaValue::MetaString(s.clone()),
- Value::Mapping(mapping) => mapping_to_pandoc(mapping),
- }
-}
-
-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, YamlMetadata};
- use std::path::{Path, PathBuf};
-
- #[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 full_meta() {
- let meta = YamlMetadata::new(
- "\
-title: Foo Bar
-date: today
-classes: [json, text]
-impls:
- python:
- - foo.py
- - bar.py
-bibliography:
-- foo.bib
-- bar.bib
-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.bibliography.unwrap(),
- &[path("foo.bib"), path("bar.bib")]
- );
- 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)
- }
-}
diff --git a/src/bin/cli/mod.rs b/src/bin/cli/mod.rs
index 4e15efb..30ad5f6 100644
--- a/src/bin/cli/mod.rs
+++ b/src/bin/cli/mod.rs
@@ -13,7 +13,7 @@ use std::{collections::HashMap, convert::TryFrom};
use subplot::{Document, EmbeddedFile, Style, SubplotError};
pub fn extract_file<'a>(doc: &'a Document, filename: &str) -> Result<&'a EmbeddedFile> {
- for file in doc.files() {
+ for file in doc.embedded_files() {
if file.filename() == filename {
return Ok(file);
}
@@ -27,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>,
}
@@ -64,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()
@@ -78,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();
@@ -88,7 +80,6 @@ impl TryFrom<&mut Document> for Metadata {
title,
binding_files,
impls,
- bibliographies,
scenarios,
files,
})
@@ -97,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) {
@@ -107,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");
}
@@ -139,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.rs b/src/bin/subplot.rs
index b86c613..8dc8973 100644
--- a/src/bin/subplot.rs
+++ b/src/bin/subplot.rs
@@ -6,13 +6,13 @@ use anyhow::Result;
use env_logger::fmt::Color;
use log::{debug, error, info, trace, warn};
use subplot::{
- codegen, load_document, resource, Document, EmbeddedFile, 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};
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -58,6 +58,8 @@ enum Cmd {
Codegen(Codegen),
#[clap(hide = true)]
Resources(Resources),
+ #[clap(hide = true)]
+ Libdocgen(Libdocgen),
}
impl Cmd {
@@ -68,6 +70,7 @@ impl Cmd {
Cmd::Docgen(d) => d.run(),
Cmd::Codegen(c) => c.run(),
Cmd::Resources(r) => r.run(),
+ Cmd::Libdocgen(r) => r.run(),
}
}
@@ -78,6 +81,7 @@ impl Cmd {
Cmd::Docgen(d) => d.doc_path(),
Cmd::Codegen(c) => c.doc_path(),
Cmd::Resources(r) => r.doc_path(),
+ Cmd::Libdocgen(r) => r.doc_path(),
}
}
}
@@ -88,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.")?;
}
@@ -159,7 +163,7 @@ impl Extract {
let doc = load_linted_doc(&self.filename, Style::default(), None, self.merciful)?;
let files: Vec<&EmbeddedFile> = if self.embedded.is_empty() {
- doc.files()
+ doc.embedded_files()
.iter()
.map(Result::Ok)
.collect::<Result<Vec<_>>>()
@@ -221,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)]
@@ -251,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
@@ -269,32 +268,29 @@ impl Docgen {
} else if let Some(date) = doc.meta().date() {
date.to_string()
} else {
- let filename = doc.meta().basedir().join(doc.meta().markdown_filename());
- Self::mtime_formatted(Self::mtime(&filename)?)
- };
- 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().unwrap_or(127);
- 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(())
}
@@ -316,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)]
@@ -397,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");
@@ -422,16 +492,24 @@ 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)
diff --git a/src/bindings.rs b/src/bindings.rs
index 98379c9..825b895 100644
--- a/src/bindings.rs
+++ b/src/bindings.rs
@@ -3,6 +3,7 @@ use super::MatchedSteps;
use super::PartialStep;
use super::ScenarioStep;
use super::StepKind;
+use crate::Warning;
use crate::{resource, SubplotError};
use serde::{Deserialize, Serialize};
@@ -166,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 {
@@ -174,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,
@@ -193,6 +192,8 @@ impl Binding {
regex,
impls: HashMap::new(),
types,
+ doc,
+ filename,
})
}
@@ -214,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()
@@ -242,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);
@@ -275,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)
}
};
@@ -298,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 {
@@ -310,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"));
@@ -327,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);
@@ -347,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();
@@ -373,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());
}
}
@@ -407,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,
@@ -428,6 +536,7 @@ impl Deref for ParsedImplWrapper {
}
#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
struct ParsedBinding {
given: Option<String>,
when: Option<String>,
@@ -439,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,
@@ -470,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(())
}
@@ -516,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(())
}
@@ -533,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));
}
@@ -557,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));
};
@@ -572,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());
@@ -583,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;
@@ -591,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() {
@@ -605,6 +739,8 @@ mod test_bindings {
r"I am (?P<name>\S+)",
false,
HashMap::new(),
+ None,
+ path(),
)
.unwrap();
let mut bindings = Bindings::new();
@@ -640,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"));
@@ -660,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),
@@ -677,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),
@@ -686,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!(
@@ -698,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();
@@ -716,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,
@@ -726,6 +882,8 @@ mod test_bindings {
.unwrap(),
false,
HashMap::new(),
+ None,
+ path(),
)
.unwrap(),
);
@@ -737,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();
@@ -754,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();
@@ -774,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),
}
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 a62553f..5d91c2e 100644
--- a/src/diagrams.rs
+++ b/src/diagrams.rs
@@ -217,8 +217,6 @@ impl PlantumlMarkup {
}
env::join_paths(Some(java_bin).iter().chain(cur_path.iter())).ok()
}
-
- // Acquire path to JAR for pandoc
}
impl DiagramMarkup for PlantumlMarkup {
diff --git a/src/doc.rs b/src/doc.rs
index cc6a616..ff52b79 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -1,21 +1,22 @@
-use crate::ast;
+use crate::bindings::CaptureType;
use crate::generate_test_program;
use crate::get_basedir_from;
-use crate::visitor;
+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::LintingVisitor;
use crate::MatchedScenario;
-use crate::Metadata;
use crate::PartialStep;
use crate::Scenario;
-use crate::ScenarioStep;
use crate::Style;
use crate::SubplotError;
-use crate::YamlMetadata;
-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;
@@ -23,10 +24,11 @@ use std::fs::read;
use std::ops::Deref;
use std::path::{Path, PathBuf};
-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] = &[
@@ -37,26 +39,17 @@ 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;
@@ -83,70 +76,42 @@ static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines", "noNumberLines"];
#[derive(Debug)]
pub struct Document {
subplot: PathBuf,
- markdowns: Vec<PathBuf>,
- ast: Pandoc,
+ markdowns: Vec<Markdown>,
meta: Metadata,
files: EmbeddedFiles,
style: Style,
- warnings: Warnings,
}
impl Document {
fn new(
subplot: PathBuf,
- markdowns: Vec<PathBuf>,
- ast: Pandoc,
+ markdowns: Vec<Markdown>,
meta: Metadata,
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()
+ };
+ trace!("Document::new -> {:#?}", doc);
+ doc
}
- fn from_ast<P>(
- basedir: P,
- subplot: PathBuf,
- markdowns: Vec<PathBuf>,
- yamlmeta: &ast::YamlMetadata,
- mut ast: Pandoc,
- style: Style,
- template: Option<&str>,
- ) -> Result<Document, SubplotError>
- where
- P: AsRef<Path> + Debug,
- {
- let meta = Metadata::new(basedir, yamlmeta, 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 = EmbeddedFiles::new(&mut ast);
- let doc = Document::new(subplot, markdowns, ast, meta, files, style);
- trace!("Loaded from JSON OK");
- Ok(doc)
+ 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,
@@ -160,88 +125,216 @@ impl Document {
);
let meta = load_metadata_from_yaml_file(filename)?;
+ trace!("metadata from YAML file: {:#?}", meta);
- let mdfile = meta.markdown();
- let mdfile = basedir.join(mdfile);
- let markdowns = vec![mdfile.clone()];
-
- let mut pandoc = pandoc::new();
- pandoc.add_input(&mdfile);
- 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 mut markdowns = vec![];
+ for filename in meta.markdowns() {
+ let filename = basedir.join(filename);
+ markdowns.push(Markdown::load_file(&filename)?);
+ }
- // Add external Pandoc filters.
- crate::policy::add_citeproc(&mut pandoc);
-
- trace!(
- "Invoking Pandoc to parse document {:?} into AST as JSON",
- mdfile,
- );
- let json = match pandoc.execute().map_err(SubplotError::Pandoc)? {
- pandoc::PandocOutput::ToBuffer(o) => o,
- _ => return Err(SubplotError::NotJson),
- };
- trace!("Pandoc was happy");
-
- trace!("Parsing document AST as JSON...");
- let mut ast: Pandoc = serde_json::from_str(&json).map_err(SubplotError::AstJson)?;
- ast.meta = meta.to_map();
- let doc = Self::from_ast(
- basedir,
- filename.into(),
- markdowns,
- &meta,
- ast,
- style,
- template,
- )?;
+ 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!("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 meta = load_metadata_from_yaml_file(filename)?;
- let mdfile = meta.markdown();
- let mdfile = basedir.join(mdfile);
- let markdown = std::fs::read_to_string(&mdfile)
- .map_err(|err| SubplotError::ReadFile(mdfile.clone(), err))?;
- let ast = ast::AbstractSyntaxTree::new(meta.clone(), &markdown);
-
- trace!("Parsed document OK");
- Self::from_ast(
- basedir,
- filename.into(),
- vec![mdfile],
- &meta,
- ast.to_pandoc(),
- style,
- template,
- )
+ /// 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));
+ }
+
+ 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.
@@ -249,11 +342,16 @@ impl 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> {
+ pub fn sources(&self, template: Option<&str>) -> Vec<PathBuf> {
let mut names = vec![self.subplot.clone()];
for x in self.meta().bindings_filenames() {
@@ -276,25 +374,20 @@ impl 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) -> &[EmbeddedFile] {
+ pub fn embedded_files(&self) -> &[EmbeddedFile] {
self.files.files()
}
@@ -302,16 +395,33 @@ impl 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));
}
@@ -331,27 +441,20 @@ impl 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() {
@@ -361,11 +464,61 @@ impl 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();
@@ -378,11 +531,16 @@ impl 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(),
));
@@ -396,9 +554,13 @@ impl 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();
@@ -410,7 +572,12 @@ impl 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());
}
@@ -419,8 +586,7 @@ impl 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
@@ -431,7 +597,7 @@ impl 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) {
@@ -444,7 +610,7 @@ impl 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(),
));
@@ -456,36 +622,28 @@ impl 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",
@@ -514,14 +672,12 @@ impl 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: ast::YamlMetadata = serde_yaml::from_slice(&yaml)
+ 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,
@@ -564,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)
}
@@ -591,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);
@@ -618,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/embedded.rs b/src/embedded.rs
index c868054..e71fa54 100644
--- a/src/embedded.rs
+++ b/src/embedded.rs
@@ -1,4 +1,3 @@
-use pandoc_ast::{MutVisitor, Pandoc};
use serde::{Deserialize, Serialize};
/// A data file embedded in the document.
@@ -26,19 +25,12 @@ impl EmbeddedFile {
}
/// A collection of data files embedded in document.
-#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
+#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct EmbeddedFiles {
files: Vec<EmbeddedFile>,
}
impl EmbeddedFiles {
- /// Create new set of data files.
- pub fn new(ast: &mut Pandoc) -> EmbeddedFiles {
- let mut files = EmbeddedFiles { files: vec![] };
- files.walk_pandoc(ast);
- files
- }
-
/// Return slice of all data files.
pub fn files(&self) -> &[EmbeddedFile] {
&self.files
diff --git a/src/error.rs b/src/error.rs
index a729bf0..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,10 +10,18 @@ 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}")]
BindingsFileNotFound(PathBuf, #[source] std::io::Error),
@@ -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,
@@ -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,10 +306,6 @@ 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),
@@ -306,10 +314,6 @@ pub enum SubplotError {
#[error("Failed to parse YAML metadata in {0}")]
MetadataFile(PathBuf, #[source] serde_yaml::Error),
- /// Abstract syntax tree error.
- #[error(transparent)]
- Ast(#[from] crate::ast::Error),
-
/// UTF8 conversion error.
#[error("failed to parse UTF8 in file {0}")]
FileUtf8(PathBuf, #[source] std::string::FromUtf8Error),
@@ -318,6 +322,10 @@ pub enum SubplotError {
#[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),
@@ -329,6 +337,10 @@ pub enum SubplotError {
/// 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 {
@@ -378,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.
@@ -404,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 725e49c..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;
@@ -27,19 +20,15 @@ mod embedded;
pub use embedded::EmbeddedFile;
pub use embedded::EmbeddedFiles;
-mod panhelper;
-mod typeset;
-
-mod visitor;
-use visitor::LintingVisitor;
-
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::{extract_metadata, AbstractSyntaxTree, YamlMetadata};
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 261017a..e382840 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -1,13 +1,160 @@
-use crate::{Bindings, SubplotError, TemplateSpec, YamlMetadata};
+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};
+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)]
@@ -15,13 +162,15 @@ pub struct Metadata {
basedir: PathBuf,
title: String,
date: Option<String>,
- markdown_filename: PathBuf,
+ 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)]
@@ -31,60 +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,
- meta: &YamlMetadata,
+ yaml: &YamlMetadata,
template: Option<&str>,
- ) -> Result<Metadata, SubplotError>
+ ) -> Result<Self, SubplotError>
where
P: AsRef<Path> + Debug,
{
- let map = meta.to_map();
- let title = get_title(&map);
- let date = get_date(&map);
- let bindings_filenames = get_bindings_filenames(&map);
- let bibliographies = get_bibliographies(basedir.as_ref(), &map);
- let classes = get_classes(&map);
- 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) = map.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)?;
+ 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);
+ }
+ }
- trace!("Loaded all metadata successfully");
+ let css_urls = if let Some(urls) = &yaml.css_urls {
+ urls.clone()
+ } else {
+ vec![]
+ };
- Ok(Metadata {
+ let meta = Self {
basedir: basedir.as_ref().to_path_buf(),
- title,
- date,
- markdown_filename: meta.markdown().into(),
+ 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.
@@ -97,14 +257,24 @@ 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 filename of the markdown file.
- pub fn markdown_filename(&self) -> &Path {
- &self.markdown_filename
+ /// Return filenames of the markdown files.
+ pub fn markdown_filenames(&self) -> &[PathBuf] {
+ &self.markdown_filenames
}
/// Return filename where bindings are specified.
@@ -127,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 {
@@ -152,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");
@@ -177,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>
-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>)
+fn pathbufs<P>(basedir: P, v: &[PathBuf]) -> 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 f63206a..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, Svg};
-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: Svg) -> 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: Svg) -> String {
- let svg = base64::encode(svg.data());
- 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/embedded.rs b/src/visitor/embedded.rs
deleted file mode 100644
index 891240b..0000000
--- a/src/visitor/embedded.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-use crate::panhelper;
-use crate::EmbeddedFile;
-use crate::EmbeddedFiles;
-
-use pandoc_ast::{Block, MutVisitor};
-
-impl MutVisitor for EmbeddedFiles {
- 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(EmbeddedFile::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 1c095ac..0000000
--- a/src/visitor/mod.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-mod block_class;
-pub use block_class::BlockClassVisitor;
-
-mod embedded;
-
-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 a732db3..66a0627 100644
--- a/subplot-build/Cargo.toml
+++ b/subplot-build/Cargo.toml
@@ -1,19 +1,20 @@
[package]
name = "subplot-build"
-version = "0.6.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.tech/"
repository = "https://gitlab.com/subplot/subplot"
+rust-version = "1.70"
[dependencies]
-subplot = { version = "0.6.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 7f194c5..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,7 +55,9 @@ where
// re-running.
let base_path = get_basedir_from(filename);
let meta = output.doc.meta();
- buildrs_deps(&base_path, Some(meta.markdown_filename()));
+ 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 0901133..76e36f2 100644
--- a/subplot.md
+++ b/subplot.md
@@ -20,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
@@ -63,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
@@ -85,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];
@@ -100,20 +97,13 @@ 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
@@ -140,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];
@@ -157,7 +144,6 @@ bindings -> docgen;
md -> codegen;
bindings -> codegen;
impl -> codegen;
-docgen -> pdf;
docgen -> html;
codegen -> testprog;
testprog -> report;
@@ -315,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.
@@ -372,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,
@@ -387,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
@@ -521,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
@@ -550,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:
@@ -561,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
@@ -621,31 +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
+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.
-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.
-
-~~~{.yaml .numberLines}
----
+~~~{.file .yaml .numberLines}
title: "Subplot"
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
@@ -716,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
@@ -772,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.
@@ -802,8 +791,6 @@ given file badfilename.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen --merciful badfilename.subplot -o foo.pdf
-then file foo.pdf exists
when I try to run subplot codegen --run badfilename.md -o test.py
then command fails
```
@@ -825,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
@@ -1074,7 +1129,7 @@ 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.
@@ -1085,8 +1140,6 @@ given file simple.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen simple.subplot -o simple.pdf
-then file simple.pdf exists
when I run subplot docgen simple.subplot -o simple.html
then file simple.html exists
when I run subplot codegen --run simple.subplot -o test.py
@@ -1097,6 +1150,92 @@ 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
@@ -1174,8 +1313,6 @@ given file allkeywords.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen allkeywords.subplot -o foo.pdf
-then file foo.pdf exists
when I run subplot codegen --run allkeywords.subplot -o test.py
then scenario "All keywords" was run
and step "given precondition foo" was run
@@ -1207,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
@@ -1248,6 +1386,7 @@ then bar was done
then foobar was done
```
~~~
+-->
### Misuse of continuation keywords
@@ -1262,8 +1401,6 @@ given file continuationmisuse.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen continuationmisuse.subplot -o foo.pdf
-then file foo.pdf exists
when I try to run subplot codegen --run continuationmisuse.subplot -o test.py
then command fails
~~~
@@ -1300,8 +1437,8 @@ section. This scenario verifies that all markup works.
given file title-markup.subplot
given file title-markup.md
given an installed subplot
-when I run subplot docgen title-markup.subplot -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.subplot .file .yaml .numberLines}
@@ -1315,6 +1452,82 @@ impls: { python: [] }
# 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 OK.
@@ -1325,8 +1538,6 @@ given file emptylines.md
and file b.yaml
and file f.py
and an installed subplot
-when I run subplot docgen emptylines.subplot -o emptylines.pdf
-then file emptylines.pdf exists
when I run subplot docgen emptylines.subplot -o emptylines.html
then file emptylines.html exists
when I run subplot codegen --run emptylines.subplot -o test.py
@@ -1942,98 +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.subplot
-given file simple.md
-and file b.yaml
-and file f.py
-and an installed subplot
-when I run subplot docgen simple.subplot -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.subplot -o simple.pdf
-then file simple.pdf has same metadata as before
-and only files simple.subplot, simple.md, b.yaml, f.py, simple.pdf exist
-~~~
-
-### Do typeset if output is older than subplot
-
-~~~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.subplot -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.subplot
-and I run subplot docgen simple.subplot -o simple.pdf
-then file simple.pdf has changed from before
-~~~
-
-### Do typeset if output is older than markdown
-
-~~~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.subplot -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.subplot -o simple.pdf
-then file simple.pdf has changed from before
-~~~
-
-### Do typeset if output is older than functions
-
-~~~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.subplot -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.subplot -o simple.pdf
-then file simple.pdf has changed from before
-~~~
-
-### Do typeset if output is older than bindings
-
-~~~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.subplot -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.subplot -o simple.pdf
-then file simple.pdf has changed from before
-~~~
-
## Document structure
Subplot uses chapters and sections to keep together scenario snippets
@@ -2192,9 +2311,7 @@ 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
@@ -2277,6 +2394,7 @@ 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
@@ -2513,32 +2631,6 @@ and file mtime.html contains "Geoffrey Butler"
and file mtime.html contains "2020-02-26 07:53"
~~~
-### Pandoc metadata
-
-~~~scenario
-given file pandoc.subplot
-given file pandoc.md
-and an installed subplot
-when I run subplot docgen pandoc.subplot -o pandoc.html
-when I run cat pandoc.html
-then file pandoc.html exists
-and file pandoc.html contains "<title>The Fabulous Title</title>"
-and file pandoc.html contains "Superlative Subtitle"
-~~~
-
-~~~{#pandoc.subplot .file .yaml .numberLines}
-title: The Fabulous Title
-markdowns:
-- pandoc.md
-pandoc:
- subtitle: Superlative Subtitle
-~~~
-
-~~~{#pandoc.md .file .markdown .numberLines}
-# Introduction
-This is a test document. That's all.
-~~~
-
### Missing bindings file
If a bindings file is missing, the error message should name the
@@ -2620,8 +2712,6 @@ 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.subplot
@@ -2630,8 +2720,6 @@ 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"
@@ -2652,7 +2740,6 @@ impls:
python:
- f.py
- other.py
-bibliography: [foo.bib, bar.bib]
~~~
~~~{#images.md .file .markdown .numberLines}
@@ -2666,34 +2753,12 @@ 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",
@@ -2710,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
@@ -3187,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
~~~
@@ -3203,7 +3301,7 @@ 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}
---
@@ -3250,7 +3348,7 @@ 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.
@@ -3299,7 +3397,7 @@ 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
@@ -3387,7 +3485,7 @@ 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.
@@ -3447,7 +3545,7 @@ markdowns:
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
@@ -3522,3 +3620,192 @@ This is a test file.
~~~{#expected.txt .file}
This is a test file.
~~~
+## Mistakes in markdown
+
+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.
+
+### Scenario before the first heading
+
+_Requirement: A scenario must follow a heading._
+
+Justification: the heading can be used as the title for the scenario.
+
+~~~scenario
+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"
+~~~
+
+~~~{#scenario-before-heading.subplot .file .yaml}
+title: Foo
+markdowns:
+ - scenario-before-heading.md
+~~~
+
+~~~~~~{#scenario-before-heading.md .file .markdown}
+~~~scenario
+~~~
+~~~~~~
+
+### Attempt to use definition list
+
+_Requirement: Attempt to use definition lists is reported._
+
+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 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"
+~~~
+
+~~~{#dl.subplot .file .yaml}
+title: Foo
+markdowns:
+ - dl.md
+~~~
+
+~~~~~~{#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}
+~~~
+~~~~~~
+
+## 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.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 36337d4..7d5b0c4 100644
--- a/subplotlib-derive/Cargo.toml
+++ b/subplotlib-derive/Cargo.toml
@@ -1,23 +1,24 @@
[package]
name = "subplotlib-derive"
-version = "0.6.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.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 7aef0dc..8e18c98 100644
--- a/subplotlib-derive/src/lib.rs
+++ b/subplotlib-derive/src/lib.rs
@@ -8,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 {
@@ -209,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
@@ -350,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,
)
}
}
@@ -403,10 +404,7 @@ fn process_step(mut input: ItemFn) -> proc_macro2::TokenStream {
for context in outer_ctx.into_iter().chain(contexts.iter()) {
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#"
@@ -424,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 c347886..bb328c6 100644
--- a/subplotlib/Cargo.toml
+++ b/subplotlib/Cargo.toml
@@ -1,24 +1,24 @@
[package]
name = "subplotlib"
-version = "0.6.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 = '''
Utility functions and types for `subplot codegen` generated Rust based
test suites. Relies on `subplotlib-derive` for associated macros.'''
homepage = "https://subplot.tech/"
repository = "https://gitlab.com/subplot/subplot"
+rust-version = "1.70"
[dependencies]
-fehler = "1"
-subplotlib-derive = { version = "0.6.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.6.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 77e493a..977f432 100644
--- a/subplotlib/build.rs
+++ b/subplotlib/build.rs
@@ -14,7 +14,7 @@
use glob::glob;
use std::{fs, path::Path};
-fn gen_tests() {
+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.subplot".into())));
@@ -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.subplot which is a good indicator.
if fs::metadata("../subplot.subplot").is_ok() {
- gen_tests();
+ 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 7a70e23..76b9193 100644
--- a/subplotlib/src/step.rs
+++ b/subplotlib/src/step.rs
@@ -25,11 +25,12 @@ type StepFunc = dyn Fn(&ScenarioContext, bool) -> 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,
+ location: &'static str,
func: Box<StepFunc>,
reg: Box<dyn Fn(&Scenario)>,
}
@@ -40,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),
}
@@ -55,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")
}
}
@@ -73,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
@@ -85,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 8abe546..9b00e17 100644
--- a/subplotlib/src/steplibrary/files.rs
+++ b/subplotlib/src/steplibrary/files.rs
@@ -90,7 +90,7 @@ 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);
@@ -130,7 +130,7 @@ pub fn remember_metadata(context: &ScenarioContext, filename: &Path) {
|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);
@@ -320,7 +320,7 @@ pub fn has_remembered_metadata(context: &ScenarioContext, filename: &Path) {
|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,
@@ -359,7 +359,7 @@ pub fn has_different_metadata(context: &ScenarioContext, filename: &Path) {
|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,
diff --git a/subplotlib/src/steplibrary/runcmd.rs b/subplotlib/src/steplibrary/runcmd.rs
index 6692441..99605a3 100644
--- a/subplotlib/src/steplibrary/runcmd.rs
+++ b/subplotlib/src/steplibrary/runcmd.rs
@@ -353,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}"));
}
}
@@ -422,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:?}"));
}
}
@@ -435,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:?}"));
}
}
@@ -448,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:?}"));
}
}
@@ -461,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:?}"));
}
}
@@ -474,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:?}"));
}
}
@@ -487,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:?}"));
}
}
@@ -500,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:?}"));
}
}
@@ -513,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:?}"));
}
}
@@ -527,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:?}"));
}
}
@@ -541,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:?}"));
}
}
@@ -555,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:?}"));
}
}
@@ -569,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 4763eff..b502e7a 100644
--- a/subplotlib/subplot-rust-support.rs
+++ b/subplotlib/subplot-rust-support.rs
@@ -61,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,
),
)?;
{
@@ -92,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)?;
}
@@ -119,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)?;
}
@@ -135,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)?;
}
@@ -162,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"));
}
}
@@ -171,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",
));
}
}
@@ -182,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) {
@@ -209,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/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();