mirror of
https://github.com/tommilligan/mdbook-admonish.git
synced 2025-12-28 10:15:34 -05:00
Compare commits
25 Commits
prep-1.16.
...
readme-exa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7482415a7 | ||
|
|
08397fbffa | ||
|
|
b78fc39031 | ||
|
|
fe7b475753 | ||
|
|
a56976d085 | ||
|
|
08967d550d | ||
|
|
8fa1411095 | ||
|
|
a1e5cfa48d | ||
|
|
33fd522d68 | ||
|
|
4805398359 | ||
|
|
c681ff922d | ||
|
|
cd0726aaf2 | ||
|
|
a08967e073 | ||
|
|
f7e6970fa3 | ||
|
|
5d2124b319 | ||
|
|
d79ebb4fad | ||
|
|
9df896cd77 | ||
|
|
ffb819c315 | ||
|
|
f278374c88 | ||
|
|
2c18292401 | ||
|
|
294af2478c | ||
|
|
278d17792b | ||
|
|
9f6c73091a | ||
|
|
9f221abc12 | ||
|
|
82c7bd4fd9 |
20
.github/workflows/check.yml
vendored
20
.github/workflows/check.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -19,10 +19,9 @@ jobs:
|
||||
target
|
||||
key: fast-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Install more toolchain
|
||||
run: rustup component add rustfmt clippy
|
||||
- name: Run tests
|
||||
@@ -37,20 +36,20 @@ jobs:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
cargo_target
|
||||
/tmp/cargo-install-target-dir
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Install node toolchain
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
@@ -59,7 +58,7 @@ jobs:
|
||||
cache-dependency-path: compile_assets/yarn.lock
|
||||
- name: Install additional test dependencies
|
||||
env:
|
||||
CARGO_TARGET_DIR: cargo_target
|
||||
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
|
||||
run: ./scripts/install
|
||||
- name: Run check script
|
||||
run: ./scripts/check
|
||||
@@ -105,7 +104,7 @@ jobs:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache build files
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
@@ -113,9 +112,8 @@ jobs:
|
||||
target
|
||||
key: test-${{ matrix.os }}-${{ matrix.rust }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
override: true
|
||||
- name: Run tests
|
||||
run: cargo test
|
||||
|
||||
18
.github/workflows/deploy.yml
vendored
18
.github/workflows/deploy.yml
vendored
@@ -43,19 +43,19 @@ jobs:
|
||||
|
||||
# Cache files between builds
|
||||
- name: Setup | Cache Cargo
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
# Note that we don't cache the `target` directory here
|
||||
# so we do a completely clean rebuild for artefacts
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Setup | Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: Setup | cross
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
|
||||
- name: Post Setup | Extract tag name
|
||||
shell: bash
|
||||
run: echo "##[set-output name=tag;]$(echo ${GITHUB_REF#refs/tags/})"
|
||||
run: echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
id: extract_tag
|
||||
|
||||
- name: Post Setup | Prepare artifacts [Windows]
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Setup | Extract version
|
||||
shell: bash
|
||||
run: echo "##[set-output name=version;]$(echo ${GITHUB_REF#refs/tags/v})"
|
||||
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||
id: extract_version
|
||||
|
||||
- name: Setup | Release notes
|
||||
@@ -134,21 +134,19 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
cargo_target
|
||||
# We reuse the cache from our detailed test environment, if available
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Publish crate
|
||||
env:
|
||||
CARGO_LOGIN_TOKEN: ${{ secrets.CARGO_LOGIN_TOKEN }}
|
||||
|
||||
11
.github/workflows/docs.yml
vendored
11
.github/workflows/docs.yml
vendored
@@ -14,24 +14,27 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
~/.cargo/bin
|
||||
cargo_target
|
||||
/tmp/cargo-install-target-dir
|
||||
# We reuse the cache from our detailed test environment, if available
|
||||
key: detailed-test-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Install toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Install mdbook
|
||||
env:
|
||||
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
|
||||
run: ./scripts/install-mdbook
|
||||
- name: Install mdbook extras
|
||||
env:
|
||||
CARGO_TARGET_DIR: "/tmp/cargo-install-target-dir"
|
||||
run: ./book/scripts/install-mdbook-extras
|
||||
- name: Build book
|
||||
run: ./scripts/build-book
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,6 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## v1.17.1
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed a stray debug statement ([#186](https://github.com/tommilligan/mdbook-admonish/pull/186))
|
||||
|
||||
## v1.17.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Blocks should have key-value options separated by commas. Existing syntax remains is supported for back-compatibility. See [the documentation on Additional Options](https://tommilligan.github.io/mdbook-admonish/#additional-options) for more details ([#181](https://github.com/tommilligan/mdbook-admonish/pull/181))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Titles contining `=` will now render correctly. Thanks to [@s00500](https://github.com/s00500) for the bug report! ([#181](https://github.com/tommilligan/mdbook-admonish/pull/181))
|
||||
|
||||
## v1.16.0
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -967,7 +967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mdbook-admonish"
|
||||
version = "1.16.0"
|
||||
version = "1.17.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook-admonish"
|
||||
version = "1.16.0"
|
||||
version = "1.17.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.74.0"
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@ into this:
|
||||
|
||||
Read the documentation [here](https://tommilligan.github.io/mdbook-admonish/), to see the actual examples in action. You can see the source in the [`./book`](./book) subdirectory.
|
||||
|
||||
Other projects using mdbook-admonish:
|
||||
Projects using mdbook-admonish include:
|
||||
|
||||
- [The Rhai Book](https://rhai.rs/book/)
|
||||
- [The Rhai Book](https://rhai.rs/book/) ([source](https://github.com/rhaiscript/book))
|
||||
- [The Trunk Guide](https://trunkrs.dev/guide/) ([source](https://github.com/trunk-rs/trunk))
|
||||
- [PRQL language book](https://prql-lang.org/book/) ([source](https://github.com/PRQL/prql))
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
- [Overview](./overview.md)
|
||||
- [Reference](./reference.md)
|
||||
- [Examples](./examples.md)
|
||||
|
||||
15
book/src/examples.md
Normal file
15
book/src/examples.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Examples
|
||||
|
||||
## Combining multiple custom properties
|
||||
|
||||
Note that the comma `,` is used to seperate custom options.
|
||||
|
||||
````
|
||||
```admonish quote collapsible=true, title='A title that really <span style="color: #e70073">pops</span>'
|
||||
To really <b><span style="color: #e70073">grab</span></b> your reader's attention.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish quote collapsible=true, title='A title that really <span style="color: #e70073">pops</span>'
|
||||
To really <b><span style="color: #e70073">grab</span></b> your reader's attention.
|
||||
```
|
||||
@@ -78,14 +78,19 @@ You can also configure the build to fail loudly, by setting `on_failure = "bail"
|
||||
|
||||
### Additional Options
|
||||
|
||||
You can pass additional options to each block. The options are structured as TOML key-value pairs.
|
||||
You can pass additional options to each block. Options are given like a [TOML Inline Table](https://toml.io/en/v1.0.0#inline-table), as key-value pairs separated by commas.
|
||||
|
||||
`mdbook-admonish` parses options by wrapping your options in an inline table before parsing them, so please consult [The TOML Reference](https://toml.io) if you run into any syntax errors. Be aware that:
|
||||
|
||||
- Key-value pairs must be separated with a comma `,`
|
||||
- TOML escapes must be escaped again - for instance, write `\"` as `\\"`.
|
||||
- For complex strings such as HTML, you may want to use a [literal string](https://toml.io/en/v1.0.0#string) to avoid complex escape sequences
|
||||
|
||||
Note that some options can be passed globally, through the `default` section in `book.toml`. See the [configuration reference](./reference.md#booktoml-configuration) for more details.
|
||||
|
||||
#### Custom title
|
||||
|
||||
A custom title can be provided, contained in a double quoted TOML string.
|
||||
Note that TOML escapes must be escaped again - for instance, write `\"` as `\\"`.
|
||||
A custom title can be provided:
|
||||
|
||||
````
|
||||
```admonish warning title="Data loss"
|
||||
@@ -114,13 +119,13 @@ This will take a while, go and grab a drink of water.
|
||||
Markdown and HTML can be used in the inner content, as you'd expect:
|
||||
|
||||
````
|
||||
```admonish tip title="_Referencing_ and <i>dereferencing</i>"
|
||||
```admonish tip title='_Referencing_ and <i>dereferencing</i>'
|
||||
The opposite of *referencing* by using `&` is *dereferencing*, which is
|
||||
accomplished with the <span style="color: hotpink">dereference operator</span>, `*`.
|
||||
```
|
||||
````
|
||||
|
||||
```admonish tip title="_Referencing_ and <i>dereferencing</i>"
|
||||
```admonish tip title='_Referencing_ and <i>dereferencing</i>'
|
||||
The opposite of *referencing* by using `&` is *dereferencing*, which is
|
||||
accomplished with the <span style="color: hotpink">dereference operator</span>, `*`.
|
||||
```
|
||||
@@ -148,7 +153,7 @@ print "Hello, world!"
|
||||
If you want to provide custom styling to a specific admonition, you can attach one or more custom classnames:
|
||||
|
||||
````
|
||||
```admonish note class="custom-0 custom-1"
|
||||
```admonish note title="Stylish", class="custom-0 custom-1"
|
||||
Styled with my custom CSS class.
|
||||
```
|
||||
````
|
||||
@@ -173,7 +178,7 @@ with an appended number if multiple blocks would have the same id.
|
||||
Setting the `id` field will _ignore_ all other ids and the duplicate counter.
|
||||
|
||||
````
|
||||
```admonish info title="My Info" id="my-special-info"
|
||||
```admonish info title="My Info", id="my-special-info"
|
||||
Link to this block with `#my-special-info` instead of the default `#admonition-my-info`.
|
||||
```
|
||||
````
|
||||
@@ -183,14 +188,14 @@ Link to this block with `#my-special-info` instead of the default `#admonition-m
|
||||
For a block to be initially collapsible, and then be openable, set `collapsible=true`:
|
||||
|
||||
````
|
||||
```admonish collapsible=true
|
||||
```admonish title="Sneaky", collapsible=true
|
||||
Content will be hidden initially.
|
||||
```
|
||||
````
|
||||
|
||||
Will yield something like the following HTML, which you can then apply styles to:
|
||||
|
||||
```admonish collapsible=true
|
||||
```admonish title="Sneaky", collapsible=true
|
||||
Content will be hidden initially.
|
||||
```
|
||||
|
||||
|
||||
@@ -39,10 +39,12 @@
|
||||
</div>
|
||||
<div>
|
||||
<p>Failed with:</p>
|
||||
<pre><code class="language-log">TOML parsing error: TOML parse error at line 1, column 8
|
||||
<pre><code class="language-log">'title="' is not a valid directive or TOML key-value pair.
|
||||
|
||||
TOML parsing error: TOML parse error at line 1, column 21
|
||||
|
|
||||
1 | title="
|
||||
| ^
|
||||
1 | config = { title=" }
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
</code></pre>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
mod toml_wrangling;
|
||||
mod v1;
|
||||
mod v2;
|
||||
mod v3;
|
||||
|
||||
/// Configuration as described by the instance of an admonition in markdown.
|
||||
///
|
||||
/// This structure represents the configuration the user must provide in each
|
||||
/// instance.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub(crate) struct InstanceConfig {
|
||||
pub(crate) directive: String,
|
||||
pub(crate) title: Option<String>,
|
||||
@@ -35,20 +37,29 @@ impl InstanceConfig {
|
||||
/// - `Some(InstanceConfig)` if this is an `admonish` block
|
||||
pub fn from_info_string(info_string: &str) -> Option<Result<Self, String>> {
|
||||
let config_string = admonition_config_string(info_string)?;
|
||||
Some(Self::from_admonish_config_string(config_string))
|
||||
}
|
||||
|
||||
// If we succeed at parsing v2, return that. Otherwise hold onto the error
|
||||
let config_v2_error = match v2::from_config_string(config_string) {
|
||||
Ok(config) => return Some(Ok(config)),
|
||||
Err(config) => config,
|
||||
/// Parse an info string that is known to be for `admonish`.
|
||||
fn from_admonish_config_string(config_string: &str) -> Result<Self, String> {
|
||||
// If we succeed at parsing v3, return that. Otherwise hold onto the error
|
||||
let config_v3_error = match v3::from_config_string(config_string) {
|
||||
Ok(config) => return Ok(config),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
Some(if let Ok(config) = v1::from_config_string(config_string) {
|
||||
// If we succeed at parsing v1, return that.
|
||||
Ok(config)
|
||||
} else {
|
||||
// Otherwise return our v2 error.
|
||||
Err(config_v2_error)
|
||||
})
|
||||
// If we succeed at parsing v2, return that
|
||||
if let Ok(config) = v2::from_config_string(config_string) {
|
||||
return Ok(config);
|
||||
};
|
||||
|
||||
// If we succeed at parsing v1, return that.
|
||||
if let Ok(config) = v1::from_config_string(config_string) {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
// Otherwise return our v3 error.
|
||||
Err(config_v3_error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,5 +101,20 @@ mod test {
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
// v3 syntax is supported
|
||||
assert_eq!(
|
||||
InstanceConfig::from_info_string(
|
||||
r#"admonish title="Custom Title", type="question", id="my-id""#
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
InstanceConfig {
|
||||
directive: "question".to_owned(),
|
||||
title: Some("Custom Title".to_owned()),
|
||||
id: Some("my-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
44
src/config/toml_wrangling.rs
Normal file
44
src/config/toml_wrangling.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub(crate) struct UserInput {
|
||||
#[serde(default)]
|
||||
pub r#type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub class: Option<String>,
|
||||
#[serde(default)]
|
||||
pub collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
impl UserInput {
|
||||
pub fn classnames(&self) -> Vec<String> {
|
||||
self.class
|
||||
.as_ref()
|
||||
.map(|class| {
|
||||
class
|
||||
.split(' ')
|
||||
.filter(|classname| !classname.is_empty())
|
||||
.map(|classname| classname.to_owned())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) static RX_DIRECTIVE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^[A-Za-z0-9_-]+$"#).expect("directive regex"));
|
||||
|
||||
pub(crate) fn format_toml_parsing_error(error: impl Display) -> String {
|
||||
format!("TOML parsing error: {error}")
|
||||
}
|
||||
|
||||
pub(crate) fn format_invalid_directive(directive: &str, original_error: impl Display) -> String {
|
||||
format!("'{directive}' is not a valid directive or TOML key-value pair.\n\n{original_error}")
|
||||
}
|
||||
139
src/config/v2.rs
139
src/config/v2.rs
@@ -1,30 +1,16 @@
|
||||
use super::toml_wrangling::{
|
||||
format_invalid_directive, format_toml_parsing_error, UserInput, RX_DIRECTIVE,
|
||||
};
|
||||
use super::InstanceConfig;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct UserInput {
|
||||
#[serde(default)]
|
||||
r#type: Option<String>,
|
||||
#[serde(default)]
|
||||
title: Option<String>,
|
||||
#[serde(default)]
|
||||
id: Option<String>,
|
||||
#[serde(default)]
|
||||
class: Option<String>,
|
||||
#[serde(default)]
|
||||
collapsible: Option<bool>,
|
||||
}
|
||||
|
||||
/// Transform our config string into valid toml
|
||||
fn bare_key_value_pairs_to_toml(pairs: &str) -> String {
|
||||
use regex::Captures;
|
||||
|
||||
static RX_BARE_KEY_ASSIGNMENT: Lazy<Regex> = Lazy::new(|| {
|
||||
let bare_key = r#"[A-Za-z0-9_-]+"#;
|
||||
Regex::new(&format!("(?:{bare_key}) *=")).expect("bare key assignment regex")
|
||||
});
|
||||
static RX_BARE_KEY_ASSIGNMENT: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"(?:[A-Za-z0-9_-]+) *="#).expect("bare key assignment regex"));
|
||||
|
||||
fn prefix_with_newline(captures: &Captures) -> String {
|
||||
format!(
|
||||
@@ -41,10 +27,18 @@ fn bare_key_value_pairs_to_toml(pairs: &str) -> String {
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn user_input_from_config_toml(config_toml: &str) -> Result<UserInput, String> {
|
||||
toml::from_str(config_toml).map_err(format_toml_parsing_error)
|
||||
}
|
||||
|
||||
/// Parse and return the config assuming v2 format.
|
||||
///
|
||||
/// Note that if an error occurs, a parsed struct that can be returned to
|
||||
/// show the error message will be returned.
|
||||
///
|
||||
/// The basic idea here is to accept space separated key-value pairs, break them
|
||||
/// onto separate lines, and then parse them as a TOML document.
|
||||
/// This breaks when values contain a literal '=' sign, for which v3 syntax should be used.
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, String> {
|
||||
let config_toml = bare_key_value_pairs_to_toml(config_string);
|
||||
let config_toml = config_toml.trim();
|
||||
@@ -52,7 +46,7 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig,
|
||||
let config: UserInput = match toml::from_str(config_toml) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
let original_error = Err(format!("TOML parsing error: {error}"));
|
||||
let original_error = format_toml_parsing_error(error);
|
||||
|
||||
// For ergonomic reasons, we allow users to specify the directive without
|
||||
// a key. So if parsing fails initially, take the first line,
|
||||
@@ -62,17 +56,11 @@ pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig,
|
||||
None => (config_toml, ""),
|
||||
};
|
||||
|
||||
static RX_DIRECTIVE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^[A-Za-z0-9_-]+$"#).expect("directive regex"));
|
||||
|
||||
if !RX_DIRECTIVE.is_match(directive) {
|
||||
return original_error;
|
||||
return Err(format_invalid_directive(directive, original_error));
|
||||
}
|
||||
|
||||
let mut config: UserInput = match toml::from_str(config_toml) {
|
||||
Ok(config) => config,
|
||||
Err(_) => return original_error,
|
||||
};
|
||||
let mut config = user_input_from_config_toml(config_toml)?;
|
||||
config.r#type = Some(directive.to_owned());
|
||||
config
|
||||
}
|
||||
@@ -102,97 +90,120 @@ mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_v2() {
|
||||
assert_eq!(
|
||||
from_config_string("").unwrap(),
|
||||
fn test_from_config_string_v2() -> Result<(), ()> {
|
||||
fn check(config_string: &str, expected: InstanceConfig) -> Result<(), ()> {
|
||||
let actual = match from_config_string(config_string) {
|
||||
Ok(config) => config,
|
||||
Err(error) => panic!("Expected config to be valid, got error:\n\n{}", error),
|
||||
};
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
check(
|
||||
"",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(" ").unwrap(),
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
" ",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
from_config_string(
|
||||
r#"type="note" class="additional classname" title="Никита" collapsible=true"#
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
r#"type="note" class="additional classname" title="Никита" collapsible=true"#,
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: Some("Никита".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: vec!["additional".to_owned(), "classname".to_owned()],
|
||||
collapsible: Some(true),
|
||||
}
|
||||
);
|
||||
},
|
||||
)?;
|
||||
// Specifying unknown keys is okay, as long as they're valid
|
||||
assert_eq!(
|
||||
from_config_string(r#"unkonwn="but valid toml""#).unwrap(),
|
||||
check(
|
||||
r#"unkonwn="but valid toml""#,
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
},
|
||||
)?;
|
||||
// Just directive is fine
|
||||
assert_eq!(
|
||||
from_config_string(r#"info"#).unwrap(),
|
||||
check(
|
||||
r#"info"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
},
|
||||
)?;
|
||||
// Directive plus toml config
|
||||
assert_eq!(
|
||||
from_config_string(r#"info title="Information" collapsible=false"#).unwrap(),
|
||||
check(
|
||||
r#"info title="Information" collapsible=false"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("Information".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: Some(false),
|
||||
}
|
||||
);
|
||||
},
|
||||
)?;
|
||||
// Test custom id
|
||||
assert_eq!(
|
||||
from_config_string(r#"info title="My Info" id="my-info-custom-id""#).unwrap(),
|
||||
check(
|
||||
r#"info title="My Info" id="my-info-custom-id""#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("My Info".to_owned()),
|
||||
id: Some("my-info-custom-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
}
|
||||
);
|
||||
},
|
||||
)?;
|
||||
// Directive after toml config is an error
|
||||
assert!(from_config_string(r#"title="Information" info"#).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_invalid_directive() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"oh!wow titlel=""#).unwrap_err(),
|
||||
r#"'oh!wow' is not a valid directive or TOML key-value pair.
|
||||
|
||||
TOML parsing error: TOML parse error at line 1, column 3
|
||||
|
|
||||
1 | oh!wow
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_invalid_toml_value() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"note titlel=""#).unwrap_err(),
|
||||
r#"TOML parsing error: TOML parse error at line 1, column 6
|
||||
r#"TOML parsing error: TOML parse error at line 1, column 9
|
||||
|
|
||||
1 | note
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
1 | titlel="
|
||||
| ^
|
||||
invalid basic string
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
202
src/config/v3.rs
Normal file
202
src/config/v3.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use super::toml_wrangling::{
|
||||
format_invalid_directive, format_toml_parsing_error, UserInput, RX_DIRECTIVE,
|
||||
};
|
||||
use super::InstanceConfig;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
struct Wrapper<T> {
|
||||
config: T,
|
||||
}
|
||||
|
||||
/// Transform our config string into valid toml
|
||||
fn bare_inline_table_to_toml(pairs: &str) -> String {
|
||||
format!("config = {{ {pairs} }}")
|
||||
}
|
||||
|
||||
fn user_input_from_config_string(config_string: &str) -> Result<UserInput, String> {
|
||||
match toml::from_str::<Wrapper<_>>(&bare_inline_table_to_toml(config_string)) {
|
||||
Ok(wrapper) => Ok(wrapper.config),
|
||||
Err(error) => Err(format_toml_parsing_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and return the config assuming v3 format.
|
||||
///
|
||||
/// Note that if an error occurs, a parsed struct that can be returned to
|
||||
/// show the error message will be returned.
|
||||
///
|
||||
/// The basic idea here is to accept the inside of an inline table, wrap it,
|
||||
/// parse it, and then use the toml values.
|
||||
pub(crate) fn from_config_string(config_string: &str) -> Result<InstanceConfig, String> {
|
||||
let config_string = config_string.trim();
|
||||
|
||||
let config = match user_input_from_config_string(config_string) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
// For ergonomic reasons, we allow users to specify the directive without
|
||||
// a key. So if parsing fails initially, take the first word,
|
||||
// use that as the directive, and reparse.
|
||||
let (directive, config_string) = match config_string.split_once(' ') {
|
||||
Some((directive, config_string)) => (directive.trim(), config_string.trim()),
|
||||
None => (config_string, ""),
|
||||
};
|
||||
|
||||
if !RX_DIRECTIVE.is_match(directive) {
|
||||
return Err(format_invalid_directive(directive, error));
|
||||
}
|
||||
|
||||
let mut config = user_input_from_config_string(config_string)?;
|
||||
config.r#type = Some(directive.to_owned());
|
||||
config
|
||||
}
|
||||
};
|
||||
|
||||
let additional_classnames = config.classnames();
|
||||
Ok(InstanceConfig {
|
||||
directive: config.r#type.unwrap_or_default(),
|
||||
title: config.title,
|
||||
id: config.id,
|
||||
additional_classnames,
|
||||
collapsible: config.collapsible,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_v3() -> Result<(), ()> {
|
||||
fn check(config_string: &str, expected: InstanceConfig) -> Result<(), ()> {
|
||||
let actual = match from_config_string(config_string) {
|
||||
Ok(config) => config,
|
||||
Err(error) => {
|
||||
panic!("Expected config '{config_string}' to be valid, got error:\n\n{error}")
|
||||
}
|
||||
};
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
check(
|
||||
"",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
" ",
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
check(
|
||||
r#"type="note", class="additional classname", title="Никита", collapsible=true"#,
|
||||
InstanceConfig {
|
||||
directive: "note".to_owned(),
|
||||
title: Some("Никита".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: vec!["additional".to_owned(), "classname".to_owned()],
|
||||
collapsible: Some(true),
|
||||
},
|
||||
)?;
|
||||
// Specifying unknown keys is okay, as long as they're valid
|
||||
check(
|
||||
r#"unkonwn="but valid toml""#,
|
||||
InstanceConfig {
|
||||
directive: "".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Just directive is fine
|
||||
check(
|
||||
r#"info"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: None,
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Directive plus toml config
|
||||
check(
|
||||
r#"info title="Information", collapsible=false"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("Information".to_owned()),
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: Some(false),
|
||||
},
|
||||
)?;
|
||||
// Test custom id
|
||||
check(
|
||||
r#"info title="My Info", id="my-info-custom-id""#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some("My Info".to_owned()),
|
||||
id: Some("my-info-custom-id".to_owned()),
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
// Directive after toml config is an error
|
||||
assert!(from_config_string(r#"title="Information" info"#).is_err());
|
||||
// HTML with quotes inside content
|
||||
// Note that we use toml literal (single quoted) strings here
|
||||
check(
|
||||
r#"info title='My <span class="emphasis">Title</span>'"#,
|
||||
InstanceConfig {
|
||||
directive: "info".to_owned(),
|
||||
title: Some(r#"My <span class="emphasis">Title</span>"#.to_owned()),
|
||||
id: None,
|
||||
additional_classnames: Vec::new(),
|
||||
collapsible: None,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_invalid_directive() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"oh!wow titlel=""#).unwrap_err(),
|
||||
r#"'oh!wow' is not a valid directive or TOML key-value pair.
|
||||
|
||||
TOML parsing error: TOML parse error at line 1, column 14
|
||||
|
|
||||
1 | config = { oh!wow titlel=" }
|
||||
| ^
|
||||
expected `.`, `=`
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_config_string_invalid_toml_value() {
|
||||
assert_eq!(
|
||||
from_config_string(r#"note titlel=""#).unwrap_err(),
|
||||
r#"TOML parsing error: TOML parse error at line 1, column 22
|
||||
|
|
||||
1 | config = { titlel=" }
|
||||
| ^
|
||||
invalid basic string
|
||||
"#
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -596,10 +596,12 @@ Error rendering admonishment
|
||||
Failed with:
|
||||
|
||||
```log
|
||||
TOML parsing error: TOML parse error at line 1, column 8
|
||||
'title="' is not a valid directive or TOML key-value pair.
|
||||
|
||||
TOML parsing error: TOML parse error at line 1, column 21
|
||||
|
|
||||
1 | title="
|
||||
| ^
|
||||
1 | config = { title=" }
|
||||
| ^
|
||||
invalid basic string
|
||||
|
||||
```
|
||||
@@ -890,6 +892,39 @@ Check Mark
|
||||
|
||||
A simple admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
"##;
|
||||
|
||||
assert_eq!(expected, prep(content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_and_content_with_html() {
|
||||
// Note that we use toml literal (single quoted) strings here
|
||||
// and the fact we have an equals sign in the value does not cause
|
||||
// us to break (because we're using v3 syntax, not v2)
|
||||
let content = r#"# Chapter
|
||||
```admonish success title='Check <span class="emphasis">Mark</span>'
|
||||
A <span class="emphasis">simple</span> admonition.
|
||||
```
|
||||
Text
|
||||
"#;
|
||||
|
||||
let expected = r##"# Chapter
|
||||
|
||||
<div id="admonition-check-mark" class="admonition admonish-success">
|
||||
<div class="admonition-title">
|
||||
|
||||
Check <span class="emphasis">Mark</span>
|
||||
|
||||
<a class="admonition-anchor-link" href="#admonition-check-mark"></a>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
A <span class="emphasis">simple</span> admonition.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Text
|
||||
|
||||
Reference in New Issue
Block a user