summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/typst-syntax/src/lexer.rs142
-rw-r--r--tests/ref/double-percent.pngbin496 -> 0 bytes
-rw-r--r--tests/suite/foundations/float.typ8
-rw-r--r--tests/suite/layout/length.typ36
-rw-r--r--tests/suite/layout/relative.typ7
5 files changed, 112 insertions, 81 deletions
diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs
index ac69eb61..7d363d7b 100644
--- a/crates/typst-syntax/src/lexer.rs
+++ b/crates/typst-syntax/src/lexer.rs
@@ -807,86 +807,96 @@ impl Lexer<'_> {
}
}
- fn number(&mut self, mut start: usize, c: char) -> SyntaxKind {
+ fn number(&mut self, start: usize, first_c: char) -> SyntaxKind {
// Handle alternative integer bases.
- let mut base = 10;
- if c == '0' {
- if self.s.eat_if('b') {
- base = 2;
- } else if self.s.eat_if('o') {
- base = 8;
- } else if self.s.eat_if('x') {
- base = 16;
- }
- if base != 10 {
- start = self.s.cursor();
- }
- }
+ let base = match first_c {
+ '0' if self.s.eat_if('b') => 2,
+ '0' if self.s.eat_if('o') => 8,
+ '0' if self.s.eat_if('x') => 16,
+ _ => 10,
+ };
- // Read the first part (integer or fractional depending on `first`).
- self.s.eat_while(if base == 16 {
- char::is_ascii_alphanumeric
+ // Read the initial digits.
+ if base == 16 {
+ self.s.eat_while(char::is_ascii_alphanumeric);
} else {
- char::is_ascii_digit
- });
-
- // Read the fractional part if not already done.
- // Make sure not to confuse a range for the decimal separator.
- if c != '.'
- && !self.s.at("..")
- && !self.s.scout(1).is_some_and(is_id_start)
- && self.s.eat_if('.')
- && base == 10
- {
self.s.eat_while(char::is_ascii_digit);
}
- // Read the exponent.
- if !self.s.at("em") && self.s.eat_if(['e', 'E']) && base == 10 {
- self.s.eat_if(['+', '-']);
- self.s.eat_while(char::is_ascii_digit);
- }
+ // Read floating point digits and exponents.
+ let mut is_float = false;
+ if base == 10 {
+ // Read digits following a dot. Make sure not to confuse a spread
+ // operator or a method call for the decimal separator.
+ if first_c == '.' {
+ is_float = true; // We already ate the trailing digits above.
+ } else if !self.s.at("..")
+ && !self.s.scout(1).is_some_and(is_id_start)
+ && self.s.eat_if('.')
+ {
+ is_float = true;
+ self.s.eat_while(char::is_ascii_digit);
+ }
- // Read the suffix.
- let suffix_start = self.s.cursor();
- if !self.s.eat_if('%') {
- self.s.eat_while(char::is_ascii_alphanumeric);
+ // Read the exponent.
+ if !self.s.at("em") && self.s.eat_if(['e', 'E']) {
+ is_float = true;
+ self.s.eat_if(['+', '-']);
+ self.s.eat_while(char::is_ascii_digit);
+ }
}
- let number = self.s.get(start..suffix_start);
- let suffix = self.s.from(suffix_start);
+ let number = self.s.from(start);
+ let suffix = self.s.eat_while(|c: char| c.is_ascii_alphanumeric() || c == '%');
- let kind = if i64::from_str_radix(number, base).is_ok() {
- SyntaxKind::Int
- } else if base == 10 && number.parse::<f64>().is_ok() {
- SyntaxKind::Float
- } else {
- return self.error(match base {
- 2 => eco_format!("invalid binary number: 0b{}", number),
- 8 => eco_format!("invalid octal number: 0o{}", number),
- 16 => eco_format!("invalid hexadecimal number: 0x{}", number),
- _ => eco_format!("invalid number: {}", number),
- });
+ let mut suffix_result = match suffix {
+ "" => Ok(None),
+ "pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%" => Ok(Some(())),
+ _ => Err(eco_format!("invalid number suffix: {suffix}")),
};
- if suffix.is_empty() {
- return kind;
- }
-
- if !matches!(
- suffix,
- "pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%"
- ) {
- return self.error(eco_format!("invalid number suffix: {}", suffix));
- }
+ let number_result = if is_float && number.parse::<f64>().is_err() {
+ // The only invalid case should be when a float lacks digits after
+ // the exponent: e.g. `1.2e`, `2.3E-`, or `1EM`.
+ Err(eco_format!("invalid floating point number: {number}"))
+ } else if base == 10 {
+ Ok(())
+ } else {
+ let name = match base {
+ 2 => "binary",
+ 8 => "octal",
+ 16 => "hexadecimal",
+ _ => unreachable!(),
+ };
+ // The index `[2..]` skips the leading `0b`/`0o`/`0x`.
+ match i64::from_str_radix(&number[2..], base) {
+ Ok(_) if suffix.is_empty() => Ok(()),
+ Ok(value) => {
+ if suffix_result.is_ok() {
+ suffix_result = Err(eco_format!(
+ "try using a decimal number: {value}{suffix}"
+ ));
+ }
+ Err(eco_format!("{name} numbers cannot have a suffix"))
+ }
+ Err(_) => Err(eco_format!("invalid {name} number: {number}")),
+ }
+ };
- if base != 10 {
- let kind = self.error(eco_format!("invalid base-{base} prefix"));
- self.hint("numbers with a unit cannot have a base prefix");
- return kind;
+ // Return our number or write an error with helpful hints.
+ match (number_result, suffix_result) {
+ // Valid numbers :D
+ (Ok(()), Ok(None)) if is_float => SyntaxKind::Float,
+ (Ok(()), Ok(None)) => SyntaxKind::Int,
+ (Ok(()), Ok(Some(()))) => SyntaxKind::Numeric,
+ // Invalid numbers :(
+ (Err(number_err), Err(suffix_err)) => {
+ let err = self.error(number_err);
+ self.hint(suffix_err);
+ err
+ }
+ (Ok(()), Err(msg)) | (Err(msg), Ok(_)) => self.error(msg),
}
-
- SyntaxKind::Numeric
}
fn string(&mut self) -> SyntaxKind {
diff --git a/tests/ref/double-percent.png b/tests/ref/double-percent.png
deleted file mode 100644
index 61a0d614..00000000
--- a/tests/ref/double-percent.png
+++ /dev/null
Binary files differ
diff --git a/tests/suite/foundations/float.typ b/tests/suite/foundations/float.typ
index a18e9f09..0579acaa 100644
--- a/tests/suite/foundations/float.typ
+++ b/tests/suite/foundations/float.typ
@@ -107,11 +107,11 @@
#123.E // this is a field access, so is fine syntactically
#0.e
#1.E+020
-// Error: 2-10 invalid number: 123.456e
+// Error: 2-10 invalid floating point number: 123.456e
#123.456e
-// Error: 2-11 invalid number: 123.456e+
+// Error: 2-11 invalid floating point number: 123.456e+
#123.456e+
-// Error: 2-6 invalid number: .1E-
+// Error: 2-6 invalid floating point number: .1E-
#.1E-
-// Error: 2-4 invalid number: 0e
+// Error: 2-4 invalid floating point number: 0e
#0e
diff --git a/tests/suite/layout/length.typ b/tests/suite/layout/length.typ
index 3409614f..22b01694 100644
--- a/tests/suite/layout/length.typ
+++ b/tests/suite/layout/length.typ
@@ -75,11 +75,25 @@
// Hint: 2-24 or use `length.abs.inches()` instead to ignore its em component
#(4.5em + 6in).inches()
---- issue-5519-length-base ---
-// Error: 2-9 invalid base-2 prefix
-// Hint: 2-9 numbers with a unit cannot have a base prefix
+--- issue-5519-nondecimal-suffix ---
+// Error: 2-9 binary numbers cannot have a suffix
+// Hint: 2-9 try using a decimal number: 4pt
#0b100pt
+--- nondecimal-suffix-edge-cases ---
+// Error: 2-7 octal numbers cannot have a suffix
+// Hint: 2-7 try using a decimal number: 50%
+#0o62%
+// Error: 2-8 hexadecimal numbers cannot have a suffix
+// Hint: 2-8 try using a decimal number: 2748%
+#0xabc%
+// Error: 2-9 invalid hexadecimal number: 0xabcem
+#0xabcem
+// Error: 2-11 binary numbers cannot have a suffix
+// Hint: 2-11 invalid number suffix: dag
+#0b0101dag
+
+
--- number-syntax-edge-cases ---
// Test numeric syntax edge cases with suffixes and which spans of text are
// highlighted. Valid items are those not annotated with an error comment since
@@ -92,17 +106,23 @@
#1.2E+0%
#1.2e-0%
#0.0e0deg
-#5in%
#0.%
+// Error: 2-6 invalid number suffix: in%
+#5in%
+// Error: 2-6 invalid number suffix: %in
+#5%in
// Error: 2-8 invalid number suffix: hello
#1hello
// Error: 2-7 invalid number suffix: infr
#1infr
-// Error: 2-5 invalid number: 2E
+// Error: 2-5 invalid floating point number: 2E
+// Hint: 2-5 invalid number suffix: M
#2EM
-// Error: 2-8 invalid number: .1E-
+// Error: 2-8 invalid floating point number: .1E-
#.1E-fr
-// Error: 2-16 invalid number: 0.1E+
+// Error: 2-16 invalid floating point number: 0.1E+
+// Hint: 2-16 invalid number suffix: fr123e456
#0.1E+fr123e456
-// Error: 2-11 invalid number: .1e-
+// Error: 2-11 invalid floating point number: .1e-
+// Hint: 2-11 invalid number suffix: fr123
#.1e-fr123.456
diff --git a/tests/suite/layout/relative.typ b/tests/suite/layout/relative.typ
index 5a590892..4b267cf4 100644
--- a/tests/suite/layout/relative.typ
+++ b/tests/suite/layout/relative.typ
@@ -6,10 +6,11 @@
#test((100% + 2pt - 2pt).length, 0pt)
#test((56% + 2pt - 56%).ratio, 0%)
---- double-percent ---
+--- double-percent-embedded ---
// Test for two percent signs in a row.
+// Error: 2-7 invalid number suffix: %%
#3.1%%
---- double-percent-error ---
-// Error: 7-8 the character `%` is not valid in code
+--- double-percent-parens ---
+// Error: 3-8 invalid number suffix: %%
#(3.1%%)