72 个版本
新 0.12.0-alpha.1 | 2024 年 8 月 18 日 |
---|---|
0.11.6 | 2023 年 6 月 3 日 |
0.11.5 | 2023 年 1 月 22 日 |
0.11.4 | 2022 年 1 月 5 日 |
0.3.0 | 2018 年 7 月 16 日 |
#101 在 解析器实现 中
460 每月下载量
在 14 个crate(5 个直接)中使用
585KB
7.5K SLoC
RESS
Rusty EcmaScript 扫描器
用 Rust 编写的 JS 扫描器/标记化器
使用方法
与 ress 交互的主要方式是通过实现 Scanner
结构体,该结构体对 Item
结构体进行迭代。 Item
有三个字段:用于找到的 Token
的 token
,表示原始字符串中字节位置开始和结束的 span
,以及表示行和列的起始和结束字符位置的 location
。其定义如下。
Item {
token: Token::Punct(Punct::Bang),
span: Span {
start: 0,
end: 1,
},
location: SourceLocation {
start: Position {
line: 1,
column: 1,
},
end: Position {
line: 1,
column: 2,
}
}
}
注意:EcmaScript 规范允许有 4 个换行符,但现代文本编辑器通常只渲染其中的两个,位置行号将计算这些未渲染的行。
以下是一个示例,它将检查一些 JS 文本是否存在分号,如果找到则引发 panic。
use ress::Scanner;
static JS: &str = include_str!("index.js");
fn main() {
let s = Scanner::new(JS);
for item in s {
let token = item.unwrap().token;
if token.matches_punct_str(";") {
panic!("A semi-colon!? Heathen!");
}
}
println!("Good show! Why use something that's optional?")
}
Item
的最重要的部分是 Token
枚举,它将代表由 ECMAScript 规范 支持的 11 种不同类型的标记。
在 JavaScript 中 很难判断一个正斜杠是除法还是正则表达式的开始。上面的 Scanner
将通过跟踪先前解析的标记来自动检测正则表达式,这使得事情变得非常方便,但是如果您正在将 JavaScript 解析为 AST,您可能已经需要跟踪相同的信息。在这种情况下,您可能不想承担自动正则表达式检测的性能成本,您会想要使用 ManualScanner
。它不是暴露基本 Iterator
接口,而是暴露了两个主要的驱动扫描器的方法:next_token
和 next_regex
。第一个方法在遇到正则表达式时将始终返回 /
或 /=
,后者如果下一个标记不是正则表达式将失败。
use ress::{ManualScanner, prelude::*};
fn main() {
let mut s = ManualScanner::new("let x = /[a-z]+/g");
while let Some(Ok(item)) = s.next_token() {
if item.token.matches_punct(Punct::ForwardSlash)
|| item.token.matches_punct(Punct::ForwardSlashEqual) {
// it could be a 1 or 2 length prefix
let regex = s.next_regex(1).unwrap().unwrap();
println!("{:?}", regex);
} else {
println!("{:?}", item);
}
}
}
ES 标记
- 布尔文字
- 文件结束
- 标识符
- 关键字
- 空文字
- 数值文字
- 标点符号
- 字符串文字
- 正则表达式文字
- 模板字符串
- 注释
请注意,关键字在JavaScript中从ES3到ES2019之间有很多变化,所以在ES2019环境中可能会发现一些在ES3环境中不存在的解析为关键字的项目,这应该在更高层次上处理。一个很好的例子是yield
,它有时是关键字,有时是标识符,这个包总是将其解析为关键字。截至本README的编写,ress
支持所有第二阶段和第三阶段ECMAScript提议中的标记,除了#!
注释和数字分隔符。
对于每个标记情况,都有一个结构或枚举来提供额外的信息,除了NullLiteral
和EoF
,这两个应该很容易理解。更复杂的项目实现了ToString
,它应该能将您带回到该标记的原始JavaScript文本。此外,Token
枚举还提供了一些辅助函数,用于构建该图片而无需从枚举中拉取内部数据。以Punct
情况为例,辅助函数看起来像这样。
fn is_punct(&self) -> bool;
fn matches_punct(&self, p: Punct) -> bool;
fn matches_punct_str(&self, s: &str) -> bool;
每个情况都有类似的一组函数。
类似于所有Iterators
,Scanner
有一个next
方法,它还有一个look_ahead
方法,允许您在不前进的情况下解析下一个值。使用这个方法可以是一个方便的方式来获取下一个标记,而无需执行可变借用,但是您将承担解析该标记两次的成本。所有Iterators
都可以通过一个peek
方法转换成一个Peekable
迭代器,这允许您只支付一次成本就向前看,但是peek
执行可变借用,这意味着它需要在与next
调用不同的作用域中。
// look_ahead
let js = "function() { return; }";
let mut s = Scanner::new(js);
let current = s.next();
let next = s.look_ahead();
let new_current = s.next();
assert_eq!(next, new_current);
// peekable (fails to compile)
let p = Scanner::new(js).peekable();
let current = s.next(); // <-- first mutable borrow
let next = p.peek(); // <-- second mutable borrow
对于更复杂的向前看场景,Scanner
提供了get_state
和set_state
方法。这些方法允许您捕获当前位置和任何上下文的快照,然后稍后重置到该位置和上下文。
let js = "function() {
return 0;
};";
let mut s = Scanner::new(js);
let start = s.get_state();
assert_eq!(s.next().unwrap().unwrap().token, Token::Keyword(Keyword::Function));
assert_eq!(s.next().unwrap().unwrap().token, Token::Punct(Punct::OpenParen));
assert_eq!(s.next().unwrap().unwrap().token, Token::Punct(Punct::CloseParen));
s.set_state(start);
assert_eq!(s.next().unwrap().unwrap().token, Token::Keyword(Keyword::Function));
为什么?
编写新的JavaScript开发工具不是很好吗?clear-comments示例证明了您如何使用这个crate来完成这项工作。这个示例将接受一个JavaScript文件,并输出一个带有所有注释都已被删除的版本。下面是如何看到它在行动的示例(假设在项目根目录中有一个名为in.js的文件)。
cargo run --example clear-comments -- ./in.js ./out.js
性能
以下统计数据是在运行cargo +nightly bench
在MBP(2.9 GHz i9-8850H & 16bg RAM)上得到的。
库 | 大小 | 时间 | +/- |
---|---|---|---|
Angular 1.5 | 1.16mb | 18.991 ms | 4.393 ms |
jQuery | 271.75kb | 7.218 ms | 577.236 μs |
React | 59.09kb | 1.976 ms | 116.139 μs |
ReactDOM | 641.51kb | 16.880 ms | 3.614 ms |
Vue | 289.30kb | 9.675 ms | 1.402 ms |
如果您对性能感兴趣,而无需等待cargo bench
完成,您可以运行以下命令。
cargo run --example major_libs