Skip to content

Commit 15c9f4c

Browse files
Fix parsing of non-numeric page numbers and serial numbers
Resolves #170 Resolves #440 (which is a part of #312)
1 parent a137441 commit 15c9f4c

4 files changed

Lines changed: 71 additions & 9 deletions

File tree

docs/file-format.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,9 +537,14 @@ ampersands, and hyphens. Numeric variables can express a single number or a
537537
range and contain only integers, but may contain negative numbers. Numeric variables can have a non-numeric prefix and suffix.
538538

539539
```yaml
540-
page-range: S10-15
540+
page-range: S10-15 # Page S10 to 15
541541
```
542542

543+
Note that the prefix and suffix for a numeric variable should be non-numeric. If
544+
you specify a number with leading zeros or numeric suffix, then the whole variable
545+
will be interpreted as a [string](#string) instead. This improves the style for
546+
atypical page numbers like `011` and `11E201`.
547+
543548
#### Unicode Language Identifier
544549

545550
A [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#unicode_language_id) identifies a language or its variants. At the simplest, you can specify an all-lowercase [two-letter ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) like `en` or `es` as a language. It is possible to specify regions, scripts, or variants to more precisely identify a variety of a language, especially in cases where the ISO 639-1 code is considered a "macrolanguage" (`zh` includes both Cantonese and Mandarin). In such cases, specify values like `en-US` for American English or `zh-Hans-CN` for Mandarin written in simplified script in mainland China. The region tags have to be written in all-caps and are mostly corresponding to [ISO 3166-1 alpha_2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements) codes.

src/types/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,33 @@ mod tests {
606606
assert!(Numeric::from_str("2nd edition").is_err());
607607
}
608608

609+
#[test]
610+
fn test_preserve_space_separator() {
611+
// https://github.com/typst/hayagriva/issues/312
612+
// https://github.com/typst/hayagriva/issues/440
613+
let serial_numbers = &["ISO/IEC 23009-1:2022(E)", "GB/T 7714—2025", "GB/T 7714"];
614+
for s in serial_numbers {
615+
let val: MaybeTyped<Numeric> = MaybeTyped::infallible_from_str(s);
616+
// It can be either typed or string, as long as whitespaces between
617+
// prefixes and numbers are preserved.
618+
assert_eq!(val.to_string(), *s);
619+
}
620+
621+
// For GB standards, em dash is the recommended separator, but
622+
// hyphen-minus and en dash should also be supported.
623+
let dashes = ["-", "–", "—"];
624+
for dash in dashes {
625+
let s = format!("GB/T 7714{dash}2015");
626+
let val: MaybeTyped<Numeric> = MaybeTyped::infallible_from_str(&s);
627+
assert_eq!(
628+
val.to_string()
629+
.replace(dashes[0], dashes[2])
630+
.replace(dashes[1], dashes[2]),
631+
format!("GB/T 7714{}2015", dashes[2])
632+
);
633+
}
634+
}
635+
609636
#[test]
610637
#[cfg(feature = "biblatex")]
611638
fn test_issue_227() {

src/types/numeric.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,8 @@ impl FromStr for Numeric {
246246

247247
fn from_str(value: &str) -> Result<Self, Self::Err> {
248248
let mut s = Scanner::new(value);
249-
let prefix =
250-
s.eat_while(|c: char| !c.is_numeric() && !c.is_whitespace() && c != '-');
249+
s.eat_whitespace();
250+
let prefix = s.eat_while(|c: char| !c.is_numeric() && c != '-');
251251

252252
let value = number(&mut s).ok_or(NumericError::NoNumber)?;
253253
s.eat_whitespace();
@@ -258,6 +258,7 @@ impl FromStr for Numeric {
258258
s.eat_until(|c: char| !is_delimiter(c));
259259
let mut items = vec![(value, Some(NumericDelimiter::try_from(c)?))];
260260
loop {
261+
s.eat_whitespace();
261262
let num = number(&mut s).ok_or(NumericError::NoNumber)?;
262263
s.eat_whitespace();
263264
match NumericDelimiter::from_str(s.eat_while(is_delimiter)) {
@@ -276,7 +277,7 @@ impl FromStr for Numeric {
276277
_ => NumericValue::Number(value),
277278
};
278279
s.eat_whitespace();
279-
let post = s.eat_while(|c: char| !c.is_whitespace());
280+
let post = s.eat_while(|c: char| !c.is_numeric() && !c.is_whitespace());
280281

281282
if !s.after().is_empty() {
282283
return Err(NumericError::UnexpectedCharactersAfterPostfix);
@@ -324,15 +325,23 @@ pub enum NumericError {
324325
MissingDelimiter,
325326
}
326327

328+
/// Eat a number from the scanner.
329+
/// The number can be positive, negative, or zero, but can't have leading zeros.
327330
fn number(s: &mut Scanner) -> Option<i32> {
328-
s.eat_whitespace();
331+
let start = s.cursor();
332+
329333
let negative = s.eat_if('-');
334+
let leading_zero = s.eat_if('0');
330335
let num = s.eat_while(|c: char| c.is_numeric());
331-
if num.is_empty() {
332-
return None;
333-
}
334336

335-
num.parse::<i32>().ok().map(|n| if negative { -n } else { n })
337+
match (leading_zero, num.is_empty()) {
338+
(true, true) => Some(0),
339+
(false, false) => num.parse::<i32>().ok().map(|n| if negative { -n } else { n }),
340+
_ => {
341+
s.jump(start);
342+
None
343+
}
344+
}
336345
}
337346

338347
impl Display for Numeric {

src/types/page.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ where
392392

393393
#[cfg(test)]
394394
mod test {
395+
use super::*;
396+
395397
#[test]
396398
fn group_by() {
397399
fn group(s: &str) -> Vec<&'_ str> {
@@ -410,4 +412,23 @@ mod test {
410412
assert_eq!(["–a", ","], group("–a,").as_slice());
411413
assert_eq!(["a–", ",", "–b"], group("a–,–b").as_slice());
412414
}
415+
416+
#[test]
417+
fn nonnumeric_page() {
418+
// https://github.com/typst/hayagriva/issues/170
419+
for s in &["11E201", "072711"] {
420+
let n: MaybeTyped<PageRanges> = MaybeTyped::infallible_from_str(s);
421+
assert_eq!(n, MaybeTyped::String(s.to_string()));
422+
}
423+
424+
// Page ranges should still be parsed as numeric values.
425+
let n: MaybeTyped<PageRanges> = MaybeTyped::infallible_from_str("S10-15");
426+
assert_eq!(
427+
n,
428+
MaybeTyped::Typed(PageRanges::new(vec![PageRangesPart::Range(
429+
Numeric::from_str("S10").unwrap(),
430+
Numeric::from_str("15").unwrap(),
431+
)]))
432+
);
433+
}
413434
}

0 commit comments

Comments
 (0)