Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.14.2](https://github.com/oxc-project/oxc-toml/compare/v0.14.1...v0.14.2) - 2025-12-22

### Added

- add justfile and toml-test integration tests

### Fixed

- add semantic validation for numbers and dates after logos removal

### Other

- convert remaining char operations to byte-based for performance
- use bytes instead of chars for lexer performance
- fail when toml-test directory is missing
- replace logos with manual lexer implementation
- use sort_by_cached_key to avoid repeated to_string allocations
- setup dprint
- enhance justfile with comprehensive dev workflow tasks
- add .rustfmt.toml and apply formatting
- remove itertools dependency
- remove dead code after making modules private
- only pub what's necessary
- remove unused HashMap/HashSet type aliases
- *(formatter)* remove unused create_options macro
- disable crate unit test
- *(README)* add sponsors info
- add deny.yml
- *(deps)* update github-actions ([#4](https://github.com/oxc-project/oxc-toml/pull/4))
- fix cargo docs
- cargo fmt
- add CI
- Initial commit: oxc-toml formatter library
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "oxc-toml"
version = "0.14.1"
version = "0.14.2"
authors = ["Boshen"]
categories = ["parser-implementations", "parsing"]
edition = "2024"
Expand Down
2 changes: 1 addition & 1 deletion src/formatter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,7 +1048,7 @@ trait NewlineCount {

impl NewlineCount for &str {
fn newline_count(&self) -> usize {
self.chars().filter(|c| c == &'\n').count()
self.as_bytes().iter().filter(|&&b| b == b'\n').count()
}
}

Expand Down
17 changes: 14 additions & 3 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,20 @@ impl<'source, Token: LexerToken<'source>> Iterator for Lexer<'source, Token> {
self.current_span = start..self.pos;
Some(Ok(token))
} else {
// Error case - consume one char
let c = remainder.chars().next()?;
self.pos += c.len_utf8();
// Error case - consume one UTF-8 character
let bytes = remainder.as_bytes();
let first_byte = *bytes.first()?;
// Determine UTF-8 sequence length from first byte
let char_len = if first_byte < 0x80 {
1 // ASCII
} else if first_byte < 0xE0 {
2 // 2-byte sequence
} else if first_byte < 0xF0 {
3 // 3-byte sequence
} else {
4 // 4-byte sequence
};
self.pos += char_len;
self.current_span = start..self.pos;
Some(Err(()))
}
Expand Down
32 changes: 26 additions & 6 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,25 +889,45 @@ impl<'p> Parser<'p> {
}

fn check_underscores(s: &str, radix: u32) -> bool {
if s.starts_with('_') || s.ends_with('_') {
let bytes = s.as_bytes();
if bytes.first() == Some(&b'_') || bytes.last() == Some(&b'_') {
return false;
}

let mut last_char = 0 as char;
let mut last_byte = 0_u8;

for c in s.chars() {
if c == '_' && !last_char.is_digit(radix) {
for &b in bytes {
let is_digit = match radix {
2 => b == b'0' || b == b'1',
8 => (b'0'..=b'7').contains(&b),
10 => b.is_ascii_digit(),
16 => b.is_ascii_hexdigit(),
_ => unreachable!(),
};

if b == b'_' && !is_digit_byte(last_byte, radix) {
return false;
}
if !c.is_digit(radix) && last_char == '_' {
if !is_digit && last_byte == b'_' {
return false;
}
last_char = c;
last_byte = b;
}

true
}

#[inline]
fn is_digit_byte(b: u8, radix: u32) -> bool {
match radix {
2 => b == b'0' || b == b'1',
8 => (b'0'..=b'7').contains(&b),
10 => b.is_ascii_digit(),
16 => b.is_ascii_hexdigit(),
_ => unreachable!(),
}
}

/// The final results of a parsing.
/// It contains the green tree, and
/// the errors that occurred during parsing.
Expand Down
29 changes: 20 additions & 9 deletions src/util/escape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ impl<'source> LexerToken<'source> for Escape {

// Newline escape: backslash followed by optional whitespace and newline
if let Some(rest) = input.strip_prefix('\\') {
let ws_len: usize =
rest.chars().take_while(|&c| c == ' ' || c == '\t').map(|c| c.len_utf8()).sum();
let bytes = rest.as_bytes();
let ws_len = bytes.iter().take_while(|&&b| b == b' ' || b == b'\t').count();
let after_ws = &rest[ws_len..];
if after_ws.starts_with('\n') {
return Some((Newline, 1 + ws_len + 1));
Expand All @@ -67,16 +67,16 @@ impl<'source> LexerToken<'source> for Escape {

// Unicode escape \uXXXX
if input.starts_with("\\u") && input.len() >= 6 {
let hex_chars = &input[2..6];
if hex_chars.chars().all(|c| c.is_ascii_hexdigit() || c == '_') {
let hex_bytes = &input.as_bytes()[2..6];
if hex_bytes.iter().all(|&b| b.is_ascii_hexdigit() || b == b'_') {
return Some((Unicode, 6));
}
}

// Unicode escape \UXXXXXXXX
if input.starts_with("\\U") && input.len() >= 10 {
let hex_chars = &input[2..10];
if hex_chars.chars().all(|c| c.is_ascii_hexdigit() || c == '_') {
let hex_bytes = &input.as_bytes()[2..10];
if hex_bytes.iter().all(|&b| b.is_ascii_hexdigit() || b == b'_') {
return Some((UnicodeLarge, 10));
}
}
Expand All @@ -86,9 +86,20 @@ impl<'source> LexerToken<'source> for Escape {
return Some((Unknown, 2));
}

// Unescaped character
if let Some(c) = input.chars().next() {
return Some((UnEscaped, c.len_utf8()));
// Unescaped character - need to handle UTF-8 here
let bytes = input.as_bytes();
if let Some(&first_byte) = bytes.first() {
// Determine UTF-8 sequence length from first byte
let len = if first_byte < 0x80 {
1 // ASCII
} else if first_byte < 0xE0 {
2 // 2-byte sequence
} else if first_byte < 0xF0 {
3 // 3-byte sequence
} else {
4 // 4-byte sequence
};
return Some((UnEscaped, len));
}

None
Expand Down
45 changes: 26 additions & 19 deletions src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ pub use escape::check_escape;
pub(crate) mod allowed_chars {
pub(crate) fn comment(s: &str) -> Result<(), Vec<usize>> {
let mut err_indices = Vec::new();
let bytes = s.as_bytes();

for (i, c) in s.chars().enumerate() {
if c != '\t' && c.is_control() {
for (i, &b) in bytes.iter().enumerate() {
// Tab is 0x09, control chars are 0x00-0x1F and 0x7F (all ASCII)
if b != b'\t' && (b < 0x20 || b == 0x7F) {
err_indices.push(i);
}
}
Expand All @@ -19,12 +21,13 @@ pub(crate) mod allowed_chars {

pub(crate) fn string(s: &str) -> Result<(), Vec<usize>> {
let mut err_indices = Vec::new();
let bytes = s.as_bytes();

for (i, c) in s.chars().enumerate() {
if c != '\t'
&& (('\u{0000}'..='\u{0008}').contains(&c)
|| ('\u{000A}'..='\u{001F}').contains(&c)
|| c == '\u{007F}')
for (i, &b) in bytes.iter().enumerate() {
// Check for disallowed control chars: 0x00-0x08, 0x0A-0x1F, 0x7F
// Tab (0x09) is allowed
if b != b'\t'
&& ((0x00..=0x08).contains(&b) || (0x0A..=0x1F).contains(&b) || b == 0x7F)
{
err_indices.push(i);
}
Expand All @@ -35,14 +38,14 @@ pub(crate) mod allowed_chars {

pub(crate) fn multi_line_string(s: &str) -> Result<(), Vec<usize>> {
let mut err_indices = Vec::new();

for (i, c) in s.chars().enumerate() {
if c != '\t'
&& c != '\n'
&& c != '\r'
&& (('\u{0000}'..='\u{0008}').contains(&c)
|| ('\u{000A}'..='\u{001F}').contains(&c)
|| c == '\u{007F}')
let bytes = s.as_bytes();

for (i, &b) in bytes.iter().enumerate() {
// Tab (0x09), LF (0x0A), CR (0x0D) are allowed
if b != b'\t'
&& b != b'\n'
&& b != b'\r'
&& ((0x00..=0x08).contains(&b) || (0x0A..=0x1F).contains(&b) || b == 0x7F)
{
err_indices.push(i);
}
Expand All @@ -53,9 +56,11 @@ pub(crate) mod allowed_chars {

pub(crate) fn string_literal(s: &str) -> Result<(), Vec<usize>> {
let mut err_indices = Vec::new();
let bytes = s.as_bytes();

for (i, c) in s.chars().enumerate() {
if c != '\t' && c.is_control() {
for (i, &b) in bytes.iter().enumerate() {
// Tab is 0x09, control chars are 0x00-0x1F and 0x7F
if b != b'\t' && (b < 0x20 || b == 0x7F) {
err_indices.push(i);
}
}
Expand All @@ -65,9 +70,11 @@ pub(crate) mod allowed_chars {

pub(crate) fn multi_line_string_literal(s: &str) -> Result<(), Vec<usize>> {
let mut err_indices = Vec::new();
let bytes = s.as_bytes();

for (i, c) in s.chars().enumerate() {
if c != '\t' && c != '\n' && c != '\r' && c.is_control() {
for (i, &b) in bytes.iter().enumerate() {
// Tab (0x09), LF (0x0A), CR (0x0D) are allowed
if b != b'\t' && b != b'\n' && b != b'\r' && (b < 0x20 || b == 0x7F) {
err_indices.push(i);
}
}
Expand Down
Loading