Skip to content

Commit f8ac776

Browse files
committed
feat: add tools/std_i18n
1 parent b47a523 commit f8ac776

File tree

10 files changed

+1860
-0
lines changed

10 files changed

+1860
-0
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/std_i18n/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "std_i18n"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
# local
8+
emmylua_parser.workspace = true
9+
10+
# external
11+
serde.workspace = true
12+
serde_yml.workspace = true
13+
walkdir.workspace = true
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
use std::collections::HashMap;
2+
3+
#[derive(Debug, Clone, Copy)]
4+
pub struct LineInfo {
5+
pub start: usize,
6+
pub end: usize, // 不含换行(也不含 CR)
7+
}
8+
9+
impl LineInfo {
10+
pub fn text<'a>(&self, raw: &'a str) -> &'a str {
11+
raw.get(self.start..self.end).unwrap_or("")
12+
}
13+
14+
pub fn indent(&self, raw: &str) -> String {
15+
self.text(raw)
16+
.chars()
17+
.take_while(|c| c.is_whitespace())
18+
.collect()
19+
}
20+
21+
pub fn trim_start_text<'a>(&self, raw: &'a str) -> &'a str {
22+
self.text(raw).trim_start()
23+
}
24+
}
25+
26+
pub fn split_lines_with_offsets(s: &str) -> Vec<LineInfo> {
27+
let bytes = s.as_bytes();
28+
let mut out: Vec<LineInfo> = Vec::new();
29+
30+
let mut line_start = 0usize;
31+
let mut i = 0usize;
32+
while i < bytes.len() {
33+
if bytes[i] == b'\n' {
34+
let mut line_end = i;
35+
if line_end > line_start && bytes[line_end - 1] == b'\r' {
36+
line_end -= 1;
37+
}
38+
out.push(LineInfo {
39+
start: line_start,
40+
end: line_end,
41+
});
42+
line_start = i + 1;
43+
}
44+
i += 1;
45+
}
46+
47+
if line_start <= bytes.len() {
48+
out.push(LineInfo {
49+
start: line_start,
50+
end: bytes.len(),
51+
});
52+
}
53+
54+
out
55+
}
56+
57+
pub fn normalize_optional_name(s: &str) -> String {
58+
s.trim()
59+
.trim_end_matches('?')
60+
.trim_end_matches(',')
61+
.to_string()
62+
}
63+
64+
pub fn normalize_field_key_token(token: &str) -> String {
65+
let t = token.trim();
66+
if let Some(inner) = t.strip_prefix("[\"").and_then(|s| s.strip_suffix("\"]")) {
67+
return inner.to_string();
68+
}
69+
if let Some(inner) = t.strip_prefix("['").and_then(|s| s.strip_suffix("']")) {
70+
return inner.to_string();
71+
}
72+
if (t.starts_with('"') && t.ends_with('"')) || (t.starts_with('\'') && t.ends_with('\'')) {
73+
return t[1..t.len() - 1].to_string();
74+
}
75+
t.to_string()
76+
}
77+
78+
pub fn parse_param_name_from_line(trimmed: &str) -> Option<String> {
79+
let after = doc_tag_payload(trimmed, "@param")?;
80+
let name = after.split_whitespace().next()?;
81+
Some(normalize_optional_name(name))
82+
}
83+
84+
pub fn parse_field_name_from_line(trimmed: &str) -> Option<String> {
85+
let after = doc_tag_payload(trimmed, "@field")?;
86+
let token = after.split_whitespace().next()?;
87+
Some(normalize_optional_name(&normalize_field_key_token(token)))
88+
}
89+
90+
pub fn find_desc_block_line_range(raw: &str, lines: &[LineInfo]) -> Option<(usize, usize)> {
91+
let mut start_idx: Option<usize> = None;
92+
for (i, li) in lines.iter().enumerate() {
93+
let t = li.trim_start_text(raw);
94+
if t.starts_with("---@") || t.starts_with("---|") {
95+
continue;
96+
}
97+
if t.starts_with("---") {
98+
start_idx = Some(i);
99+
break;
100+
}
101+
}
102+
let start = start_idx?;
103+
104+
let mut end = start;
105+
while end < lines.len() {
106+
let t = lines[end].trim_start_text(raw);
107+
if t.starts_with("---@") || t.starts_with("---|") {
108+
break;
109+
}
110+
if t.starts_with("---") {
111+
end += 1;
112+
continue;
113+
}
114+
break;
115+
}
116+
Some((start, end))
117+
}
118+
119+
/// 解析 union item 行的 value 部分。
120+
///
121+
/// 支持:
122+
/// - `---| "n" # ...`
123+
/// - `---|>"collect" # ...`
124+
/// - `---|+"n" # ...`
125+
/// - `---|>+"n" # ...`
126+
pub fn parse_union_item_value_from_line_trim(line_trim_start: &str) -> Option<String> {
127+
let after = line_trim_start.strip_prefix("---|")?.trim_start();
128+
let after = after.strip_prefix('>').unwrap_or(after).trim_start();
129+
let after = after.strip_prefix('+').unwrap_or(after).trim_start();
130+
131+
if let Some(rest) = after.strip_prefix('"') {
132+
let end = rest.find('"')?;
133+
return Some(rest[..end].to_string());
134+
}
135+
if let Some(rest) = after.strip_prefix('\'') {
136+
let end = rest.find('\'')?;
137+
return Some(rest[..end].to_string());
138+
}
139+
140+
let end = after
141+
.find(|c: char| c.is_whitespace() || c == '#')
142+
.unwrap_or(after.len());
143+
if end == 0 {
144+
None
145+
} else {
146+
Some(after[..end].to_string())
147+
}
148+
}
149+
150+
pub fn build_tag_line_indexes(raw: &str, lines: &[LineInfo]) -> TagLineIndexes {
151+
let default_indent = lines.first().map(|l| l.indent(raw)).unwrap_or_default();
152+
153+
let desc_block = find_desc_block_line_range(raw, lines);
154+
155+
let mut param_line: HashMap<String, usize> = HashMap::new();
156+
let mut field_line: HashMap<String, usize> = HashMap::new();
157+
let mut return_lines: Vec<usize> = Vec::new();
158+
let mut union_line: HashMap<String, usize> = HashMap::new();
159+
160+
for (i, li) in lines.iter().enumerate() {
161+
let t = li.trim_start_text(raw);
162+
163+
if let Some(name) = parse_param_name_from_line(t) {
164+
param_line.entry(name).or_insert(i);
165+
continue;
166+
}
167+
if let Some(name) = parse_field_name_from_line(t) {
168+
field_line.entry(name).or_insert(i);
169+
continue;
170+
}
171+
if doc_tag_payload(t, "@return").is_some() {
172+
return_lines.push(i);
173+
continue;
174+
}
175+
if t.starts_with("---|") {
176+
if let Some(value) = parse_union_item_value_from_line_trim(t) {
177+
union_line.entry(value).or_insert(i);
178+
}
179+
}
180+
}
181+
182+
TagLineIndexes {
183+
default_indent,
184+
desc_block,
185+
param_line,
186+
field_line,
187+
return_lines,
188+
union_line,
189+
}
190+
}
191+
192+
pub struct TagLineIndexes {
193+
pub default_indent: String,
194+
pub desc_block: Option<(usize, usize)>,
195+
pub param_line: HashMap<String, usize>,
196+
pub field_line: HashMap<String, usize>,
197+
pub return_lines: Vec<usize>,
198+
pub union_line: HashMap<String, usize>,
199+
}
200+
201+
fn doc_tag_payload<'a>(line_trim_start: &'a str, tag: &str) -> Option<&'a str> {
202+
// 支持 `---@param ...` 以及 `--- @param ...`(中间允许空格)。
203+
let t = line_trim_start.trim_start();
204+
let after = t.strip_prefix("---")?.trim_start();
205+
let after = after.strip_prefix(tag)?;
206+
Some(after.trim_start())
207+
}

0 commit comments

Comments
 (0)