Skip to content

Commit cfcf3fc

Browse files
committed
fix(parser): accept the range of unicode in xref/image/inline etc
This is probably too permissive of a set but let's see if anyone complains. If I need to tighten this up I'll use a specialised function, rather than just pushing a range. Fixes #438.
1 parent ed757ad commit cfcf3fc

4 files changed

Lines changed: 110 additions & 6 deletions

File tree

acdc-parser/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
167167

168168
### Fixed
169169

170+
- Path-based macros now recognize non-ASCII local targets, including cross-references,
171+
links, icons, images, audio, and video, matching Asciidoctor.
170172
- Table cell specifiers now accept the Asciidoctor order with spans or repeats
171173
before alignment and style. Column styles after spans, and across repeated
172174
cells, now follow source-cell order.

acdc-parser/src/grammar/document.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5602,9 +5602,11 @@ peg::parser! {
56025602
format!("#{fragment}")
56035603
}
56045604

5605-
/// Filesystem path - conservative character set for cross-platform compatibility
5606-
/// Includes '{' and '}' for `AsciiDoc` attribute substitution
5607-
pub rule path() -> String = path:$(['A'..='Z' | 'a'..='z' | '0'..='9' | '{' | '}' | '_' | '-' | '.' | '/' | '\\' ]+)
5605+
/// Filesystem path accepted by block macros.
5606+
///
5607+
/// ASCII input uses a conservative filename set. Non-ASCII Unicode characters
5608+
/// are accepted unchanged, and `{`/`}` permit `AsciiDoc` attribute substitution.
5609+
pub rule path() -> String = path:$(['A'..='Z' | 'a'..='z' | '0'..='9' | '{' | '}' | '_' | '-' | '.' | '/' | '\\' | '\u{80}'..='\u{10FFFF}' ]+)
56085610
{?
56095611
let inline_state = InlinePreprocessorParserState::new_all_enabled(
56105612
path,

acdc-parser/src/grammar/inlines.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2304,9 +2304,11 @@ peg::parser! {
23042304
Cow::Owned(format!("#{fragment}"))
23052305
}
23062306

2307-
/// Filesystem path - conservative character set for cross-platform compatibility
2308-
/// Includes '{' and '}' for `AsciiDoc` attribute substitution
2309-
pub rule path() -> Cow<'input, str> = path:$(['A'..='Z' | 'a'..='z' | '0'..='9' | '{' | '}' | '_' | '-' | '.' | '/' | '\\' ]+)
2307+
/// Filesystem path accepted by inline macros.
2308+
///
2309+
/// ASCII input uses a conservative filename set. Non-ASCII Unicode characters
2310+
/// are accepted unchanged, and `{`/`}` permit `AsciiDoc` attribute substitution.
2311+
pub rule path() -> Cow<'input, str> = path:$(['A'..='Z' | 'a'..='z' | '0'..='9' | '{' | '}' | '_' | '-' | '.' | '/' | '\\' | '\u{80}'..='\u{10FFFF}' ]+)
23102312
{?
23112313
let inline_state = InlinePreprocessorParserState::new_all_enabled(
23122314
path,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use acdc_parser::{Block, InlineMacro, InlineNode, Options, parse, parse_inline};
2+
3+
type TestResult = Result<(), Box<dyn std::error::Error>>;
4+
5+
fn unexpected(message: &str, actual: impl std::fmt::Debug) -> Box<dyn std::error::Error> {
6+
std::io::Error::other(format!("{message}, got {actual:?}")).into()
7+
}
8+
9+
fn assert_plain_text(text: &[InlineNode<'_>], expected: &str) -> TestResult {
10+
let [InlineNode::PlainText(text)] = text else {
11+
return Err(unexpected("expected plain macro text", text));
12+
};
13+
assert_eq!(text.content, expected);
14+
Ok(())
15+
}
16+
17+
#[test]
18+
fn xref_accepts_non_ascii_path_characters() -> TestResult {
19+
for (input, target, text) in [
20+
(
21+
"xref:die_straße.adoc[my street]",
22+
"die_straße.adoc",
23+
"my street",
24+
),
25+
(
26+
"xref:die_straße.adoc#section[street section]",
27+
"die_straße.adoc#section",
28+
"street section",
29+
),
30+
] {
31+
let parsed = parse_inline(input, &Options::default())?;
32+
let [InlineNode::Macro(InlineMacro::CrossReference(xref))] = parsed.inlines() else {
33+
return Err(unexpected(
34+
"expected one cross-reference macro",
35+
parsed.inlines(),
36+
));
37+
};
38+
39+
assert_eq!(xref.target, target);
40+
assert_plain_text(&xref.text, text)?;
41+
}
42+
43+
Ok(())
44+
}
45+
46+
#[test]
47+
fn other_inline_macros_accept_non_ascii_path_characters() -> TestResult {
48+
let parsed = parse_inline("link:die_straße.adoc[street]", &Options::default())?;
49+
let [InlineNode::Macro(InlineMacro::Link(link))] = parsed.inlines() else {
50+
return Err(unexpected("expected one link macro", parsed.inlines()));
51+
};
52+
assert_eq!(link.target.to_string(), "die_straße.adoc");
53+
54+
let parsed = parse_inline("image:straße.png[street]", &Options::default())?;
55+
let [InlineNode::Macro(InlineMacro::Image(image))] = parsed.inlines() else {
56+
return Err(unexpected(
57+
"expected one inline image macro",
58+
parsed.inlines(),
59+
));
60+
};
61+
assert_eq!(image.source.to_string(), "straße.png");
62+
63+
let parsed = parse_inline("icon:straße[]", &Options::default())?;
64+
let [InlineNode::Macro(InlineMacro::Icon(icon))] = parsed.inlines() else {
65+
return Err(unexpected("expected one icon macro", parsed.inlines()));
66+
};
67+
assert_eq!(icon.target.to_string(), "straße");
68+
69+
Ok(())
70+
}
71+
72+
#[test]
73+
fn block_media_macros_accept_non_ascii_path_characters() -> TestResult {
74+
let parsed = parse(
75+
"image::straße.png[]\n\naudio::straße.mp3[]\n\nvideo::straße.mp4[]\n",
76+
&Options::default(),
77+
)?;
78+
let [
79+
Block::Image(image),
80+
Block::Audio(audio),
81+
Block::Video(video),
82+
] = parsed.document().blocks.as_slice()
83+
else {
84+
return Err(unexpected(
85+
"expected image, audio, and video blocks",
86+
&parsed.document().blocks,
87+
));
88+
};
89+
90+
assert_eq!(image.source.to_string(), "straße.png");
91+
assert_eq!(audio.source.to_string(), "straße.mp3");
92+
let [video_source] = video.sources.as_slice() else {
93+
return Err(unexpected("expected one video source", &video.sources));
94+
};
95+
assert_eq!(video_source.to_string(), "straße.mp4");
96+
97+
Ok(())
98+
}

0 commit comments

Comments
 (0)