@chaoswise/intl 2.1.10 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chaoswise-intl.js +20 -12
- package/bin/scripts/conf/default.js +49 -1
- package/bin/scripts/nozhcn.js +440 -0
- package/bin/scripts/util/babelOptions.js +49 -0
- package/bin/scripts/util/findZhCnInFile.js +377 -0
- package/bin/scripts/util/findZhCnInSvgFile.js +139 -0
- package/bin/scripts/util/fixI18nDefaultInFile.js +179 -0
- package/bin/scripts/util/fixZhCnInFile.js +217 -0
- package/bin/scripts/util/fixZhCnInSvgFile.js +206 -0
- package/bin/scripts/util/makeVisitorCollect.js +14 -6
- package/bin/scripts/util/transformAst.js +38 -20
- package/package.json +4 -2
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fixZhCnInFile.js
|
|
3
|
+
*
|
|
4
|
+
* Fixes Chinese characters found in COMMENT nodes by patching the raw source
|
|
5
|
+
* using byte-offset positions from AST tokens.
|
|
6
|
+
*
|
|
7
|
+
* Strategy options:
|
|
8
|
+
* 'clean' - Remove only Chinese characters from comment value.
|
|
9
|
+
* If the cleaned value is whitespace-only, remove the entire
|
|
10
|
+
* comment token (including delimiters // or /* ... *\/).
|
|
11
|
+
* 'remove' - Remove the entire comment token unconditionally.
|
|
12
|
+
*
|
|
13
|
+
* Important: This function operates on raw source bytes:
|
|
14
|
+
* - It sorts findings by `start` in descending order so that later
|
|
15
|
+
* replacements do not invalidate earlier offsets.
|
|
16
|
+
* - Only `comment` type findings (which carry `start` and `end` byte
|
|
17
|
+
* offsets) are accepted.
|
|
18
|
+
*
|
|
19
|
+
* Returns the number of comment tokens patched (0 if nothing was modified).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const log = require('./log');
|
|
24
|
+
|
|
25
|
+
// Chinese character range, same as primaryRegx default
|
|
26
|
+
const ZH_REGEX = /[\u4e00-\u9fa5]/g;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compute the replacement string for a single comment token.
|
|
30
|
+
*
|
|
31
|
+
* In Babel AST (with tokens:true):
|
|
32
|
+
* CommentLine : value = text after '//' (no newline)
|
|
33
|
+
* CommentBlock : value = text between '/*' and '*\/'
|
|
34
|
+
*
|
|
35
|
+
* source.slice(start, end) gives the full token including delimiters.
|
|
36
|
+
*
|
|
37
|
+
* @param {{ type: string, value: string, start: number, end: number }} comment
|
|
38
|
+
* @param {'clean'|'remove'} strategy
|
|
39
|
+
* @returns {string|null} null means "delete the token entirely"
|
|
40
|
+
*/
|
|
41
|
+
function computeReplacement(comment, strategy) {
|
|
42
|
+
const { type, value } = comment;
|
|
43
|
+
|
|
44
|
+
if (strategy === 'remove') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// strategy === 'clean': strip Chinese characters from value
|
|
49
|
+
const cleanedValue = value.replace(ZH_REGEX, '');
|
|
50
|
+
|
|
51
|
+
if (!cleanedValue.trim()) {
|
|
52
|
+
// Comment content is now empty → remove the token
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (type === 'CommentLine') {
|
|
57
|
+
return `//${cleanedValue}`;
|
|
58
|
+
}
|
|
59
|
+
// CommentBlock
|
|
60
|
+
return `/*${cleanedValue}*/`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* After removing a comment token, if the rest of the line is blank
|
|
65
|
+
* (only whitespace between the last newline and the token start),
|
|
66
|
+
* also remove the trailing newline so we don't leave empty lines.
|
|
67
|
+
*
|
|
68
|
+
* Special case – JSX expression-container comments { /* ... */ } (where
|
|
69
|
+
* the comment is the sole content of a JSX expression): the surrounding
|
|
70
|
+
* { } braces must also be removed, otherwise an orphan {} expression is left.
|
|
71
|
+
* We detect this by checking whether the characters directly flanking the
|
|
72
|
+
* comment token (past optional spaces) are { and }, and if so extend the
|
|
73
|
+
* deletion range to cover them as well.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} source
|
|
76
|
+
* @param {number} tokenStart - char index of the comment token start
|
|
77
|
+
* @param {number} tokenEnd - char index just past the comment token end
|
|
78
|
+
* @returns {{ patchStart: number, patchEnd: number }}
|
|
79
|
+
*/
|
|
80
|
+
function calcDeletionRange(source, tokenStart, tokenEnd) {
|
|
81
|
+
// ── Determine the effective deletion boundaries ───────────────────────────
|
|
82
|
+
// For JSX expression-container comments `{ /* ... */ }`, we want to delete
|
|
83
|
+
// the wrapping braces as well, otherwise we leave an orphan `{}` expression.
|
|
84
|
+
let effectiveStart = tokenStart;
|
|
85
|
+
let effectiveEnd = tokenEnd;
|
|
86
|
+
|
|
87
|
+
// Walk backwards past optional spaces to find a potential `{`
|
|
88
|
+
let braceOpen = tokenStart - 1;
|
|
89
|
+
while (braceOpen >= 0 && source[braceOpen] === ' ') braceOpen--;
|
|
90
|
+
|
|
91
|
+
// Walk forwards past optional spaces to find a potential `}`
|
|
92
|
+
let braceClose = tokenEnd;
|
|
93
|
+
while (braceClose < source.length && source[braceClose] === ' ') braceClose++;
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
braceOpen >= 0 && source[braceOpen] === '{' &&
|
|
97
|
+
braceClose < source.length && source[braceClose] === '}'
|
|
98
|
+
) {
|
|
99
|
+
// The comment is the sole content of a JSX `{...}` expression — include the braces.
|
|
100
|
+
effectiveStart = braceOpen;
|
|
101
|
+
effectiveEnd = braceClose + 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Guard: if tokenEnd somehow overshot a newline, clamp back ────────────
|
|
105
|
+
// Babel's comment.end should never include the trailing \n of a CommentLine,
|
|
106
|
+
// but be defensive: if the character just before effectiveEnd is a newline,
|
|
107
|
+
// we are already at the start of the next line — step back to the \n itself
|
|
108
|
+
// so the lineEnd walk below stays on the correct line.
|
|
109
|
+
let adjustedEnd = effectiveEnd;
|
|
110
|
+
if (adjustedEnd > effectiveStart && adjustedEnd > 0 &&
|
|
111
|
+
source[adjustedEnd - 1] === '\n' &&
|
|
112
|
+
(adjustedEnd >= source.length || source[adjustedEnd] !== '\n')) {
|
|
113
|
+
adjustedEnd--;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Decide whether to remove the whole line or just the token ────────────
|
|
117
|
+
let lineStart = effectiveStart;
|
|
118
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n') {
|
|
119
|
+
lineStart--;
|
|
120
|
+
}
|
|
121
|
+
const before = source.slice(lineStart, effectiveStart);
|
|
122
|
+
const onlyWhitespaceBefore = /^\s*$/.test(before);
|
|
123
|
+
|
|
124
|
+
let lineEnd = adjustedEnd;
|
|
125
|
+
while (lineEnd < source.length && source[lineEnd] !== '\n') {
|
|
126
|
+
lineEnd++;
|
|
127
|
+
}
|
|
128
|
+
const after = source.slice(adjustedEnd, lineEnd);
|
|
129
|
+
const onlyWhitespaceAfter = /^\s*$/.test(after);
|
|
130
|
+
|
|
131
|
+
if (onlyWhitespaceBefore && onlyWhitespaceAfter) {
|
|
132
|
+
// Remove the entire line including the trailing newline (if present)
|
|
133
|
+
const end = lineEnd < source.length ? lineEnd + 1 : lineEnd;
|
|
134
|
+
return { patchStart: lineStart, patchEnd: end };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Comment is inline on a code line — just remove the effective range
|
|
138
|
+
return { patchStart: effectiveStart, patchEnd: effectiveEnd };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Fix all comment findings in a single file.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} filePath - absolute file path
|
|
145
|
+
* @param {Finding[]} findings - array of { type:'comment', start, end, commentType, ... }
|
|
146
|
+
* @param {'clean'|'remove'} strategy
|
|
147
|
+
* @returns {number} count of comment findings actually patched (0 if none)
|
|
148
|
+
*/
|
|
149
|
+
module.exports = function fixZhCnInFile(filePath, findings, strategy = 'clean') {
|
|
150
|
+
// Filter to comment-type findings that have byte offsets
|
|
151
|
+
const commentFindings = findings.filter(
|
|
152
|
+
(f) => f.type === 'comment' && typeof f.start === 'number' && typeof f.end === 'number'
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (!commentFindings.length) return 0;
|
|
156
|
+
|
|
157
|
+
let source;
|
|
158
|
+
try {
|
|
159
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
160
|
+
} catch (err) {
|
|
161
|
+
log.error(`[nozhcn] Cannot read file for fixing: ${filePath} — ${err.message}`);
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Sort descending by start so we patch from the end backwards,
|
|
166
|
+
// preserving the validity of earlier byte offsets.
|
|
167
|
+
const sorted = [...commentFindings].sort((a, b) => b.start - a.start);
|
|
168
|
+
|
|
169
|
+
let patchedCount = 0;
|
|
170
|
+
|
|
171
|
+
for (const finding of sorted) {
|
|
172
|
+
const { start, end, commentType } = finding;
|
|
173
|
+
|
|
174
|
+
// Reconstruct the original comment object expected by computeReplacement
|
|
175
|
+
const originalSource = source.slice(start, end);
|
|
176
|
+
|
|
177
|
+
// Extract the raw value (without delimiters) from the source token
|
|
178
|
+
// so that computeReplacement works even if the finding.content is
|
|
179
|
+
// already formatted with delimiters.
|
|
180
|
+
let rawValue;
|
|
181
|
+
if (commentType === 'CommentLine') {
|
|
182
|
+
// source token = '//' + value
|
|
183
|
+
rawValue = originalSource.startsWith('//') ? originalSource.slice(2) : originalSource;
|
|
184
|
+
} else {
|
|
185
|
+
// source token = '/*' + value + '*/'
|
|
186
|
+
if (originalSource.startsWith('/*') && originalSource.endsWith('*/')) {
|
|
187
|
+
rawValue = originalSource.slice(2, -2);
|
|
188
|
+
} else {
|
|
189
|
+
rawValue = originalSource;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const pseudoComment = { type: commentType, value: rawValue, start, end };
|
|
194
|
+
const replacement = computeReplacement(pseudoComment, strategy);
|
|
195
|
+
|
|
196
|
+
if (replacement === null) {
|
|
197
|
+
// Delete the token (and possibly the whole line if it becomes empty)
|
|
198
|
+
const { patchStart, patchEnd } = calcDeletionRange(source, start, end);
|
|
199
|
+
source = source.slice(0, patchStart) + source.slice(patchEnd);
|
|
200
|
+
} else {
|
|
201
|
+
source = source.slice(0, start) + replacement + source.slice(end);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
patchedCount++;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (patchedCount > 0) {
|
|
208
|
+
try {
|
|
209
|
+
fs.writeFileSync(filePath, source, { encoding: 'utf-8' });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
log.error(`[nozhcn] Cannot write file: ${filePath} — ${err.message}`);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return patchedCount;
|
|
217
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fixZhCnInSvgFile.js
|
|
3
|
+
*
|
|
4
|
+
* Auto-fixes Chinese characters in standalone SVG files.
|
|
5
|
+
*
|
|
6
|
+
* Fixable types (from findZhCnInSvgFile.js):
|
|
7
|
+
* svg-comment ─ auto-fixable via comment clean/remove strategy
|
|
8
|
+
* svg-metadata ─ auto-fixable: remove element or empty its content
|
|
9
|
+
*
|
|
10
|
+
* Conditionally fixable types:
|
|
11
|
+
* svg-attr ─ attribute values (e.g. id="矩形", id="编组-11")
|
|
12
|
+
* typically design-tool layer-name artifacts; fixable
|
|
13
|
+
* when attrStrategy is set to 'clean' or 'remove'
|
|
14
|
+
*
|
|
15
|
+
* Non-fixable types (reported but not touched):
|
|
16
|
+
* svg-text ─ visible rendered text, requires design/i18n decision
|
|
17
|
+
*
|
|
18
|
+
* Strategies
|
|
19
|
+
* ──────────────────────────────────────────────────────────────────────
|
|
20
|
+
* commentStrategy ('clean' | 'remove')
|
|
21
|
+
* 'clean' : Remove only Chinese characters from the comment value.
|
|
22
|
+
* If the cleaned value is whitespace-only, delete the comment.
|
|
23
|
+
* 'remove' : Delete the entire comment unconditionally.
|
|
24
|
+
*
|
|
25
|
+
* metadataStrategy ('remove' | 'empty')
|
|
26
|
+
* 'remove' : Delete the entire <title>/<desc>/<metadata> element.
|
|
27
|
+
* 'empty' : Keep the element but empty its contents:
|
|
28
|
+
* <title>搜索</title> → <title></title>
|
|
29
|
+
*
|
|
30
|
+
* attrStrategy ('clean' | 'remove' | false)
|
|
31
|
+
* 'clean' : Strip Chinese (and CJK punctuation) from attribute values.
|
|
32
|
+
* If the cleaned value is empty, remove the attribute entirely.
|
|
33
|
+
* Renamed id attributes update internal url(#…) / href refs.
|
|
34
|
+
* 'remove' : Remove the entire attribute.
|
|
35
|
+
* false : Do not auto-fix attributes (default, backward-compat).
|
|
36
|
+
*
|
|
37
|
+
* Returns { fixed: number } (count of SVG elements/comments fixed)
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const fs = require('fs');
|
|
41
|
+
const log = require('./log');
|
|
42
|
+
|
|
43
|
+
const ZH_REGEX = /[\u4e00-\u9fa5]/g;
|
|
44
|
+
const ZH_TEST = /[\u4e00-\u9fa5]/;
|
|
45
|
+
// Chinese chars + CJK symbols/punctuation + fullwidth forms (design-tool junk)
|
|
46
|
+
const ZH_CLEAN_REGEX = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g;
|
|
47
|
+
|
|
48
|
+
function escapeRegExp(str) {
|
|
49
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Apply attribute fix strategy to the full source string.
|
|
54
|
+
* Handles id renames with internal SVG reference updates.
|
|
55
|
+
*/
|
|
56
|
+
function fixAttrs(source, strategy) {
|
|
57
|
+
const idRenames = new Map(); // oldValue → newValue | null
|
|
58
|
+
|
|
59
|
+
// Pass 1: Process ONLY id attributes with Chinese values
|
|
60
|
+
source = source.replace(
|
|
61
|
+
/(\s)(id)=(["'])([^"']*[\u4e00-\u9fa5][^"']*)\3/g,
|
|
62
|
+
(match, ws, attr, quote, value) => {
|
|
63
|
+
if (strategy === 'remove') {
|
|
64
|
+
idRenames.set(value, null);
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
// 'clean': strip Chinese + CJK punctuation
|
|
68
|
+
const cleaned = value.replace(ZH_CLEAN_REGEX, '').trim();
|
|
69
|
+
if (!cleaned) {
|
|
70
|
+
idRenames.set(value, null);
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
idRenames.set(value, cleaned);
|
|
74
|
+
return `${ws}id=${quote}${cleaned}${quote}`;
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Pass 2: Update internal references for renamed / removed ids
|
|
79
|
+
idRenames.forEach((newId, oldId) => {
|
|
80
|
+
const esc = escapeRegExp(oldId);
|
|
81
|
+
if (newId) {
|
|
82
|
+
source = source.replace(new RegExp(`url\\(#${esc}\\)`, 'g'), `url(#${newId})`);
|
|
83
|
+
source = source.replace(
|
|
84
|
+
new RegExp(`(xlink:href|href)=(["'])#${esc}\\2`, 'g'),
|
|
85
|
+
`$1=$2#${newId}$2`
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
// id was removed — neutralise dangling url() references
|
|
89
|
+
source = source.replace(new RegExp(`url\\(#${esc}\\)`, 'g'), 'none');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Pass 3: Process remaining (non-id) attributes with Chinese values
|
|
94
|
+
source = source.replace(
|
|
95
|
+
/(\s)([\w:_-]+)=(["'])([^"']*[\u4e00-\u9fa5][^"']*)\3/g,
|
|
96
|
+
(match, ws, attr, quote, value) => {
|
|
97
|
+
if (attr === 'id') return match; // already handled
|
|
98
|
+
if (strategy === 'remove') return '';
|
|
99
|
+
const cleaned = value.replace(ZH_CLEAN_REGEX, '').trim();
|
|
100
|
+
if (!cleaned) return '';
|
|
101
|
+
return `${ws}${attr}=${quote}${cleaned}${quote}`;
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return source;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Apply comment fix strategy to the full source string.
|
|
110
|
+
*/
|
|
111
|
+
function fixComments(source, strategy) {
|
|
112
|
+
return source.replace(/<!--([\s\S]*?)-->/g, (match, content) => {
|
|
113
|
+
if (!ZH_TEST.test(content)) return match;
|
|
114
|
+
if (strategy === 'remove') return '';
|
|
115
|
+
const cleaned = content.replace(ZH_REGEX, '');
|
|
116
|
+
if (!cleaned.trim()) return '';
|
|
117
|
+
return `<!--${cleaned}-->`;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Apply metadata element fix strategy to the full source string.
|
|
123
|
+
* Handles <title>, <desc>, <metadata> (case-insensitive, with attributes).
|
|
124
|
+
*/
|
|
125
|
+
function fixMetadata(source, strategy) {
|
|
126
|
+
return source.replace(/<(title|desc|metadata)(\s[^>]*)?>[\s\S]*?<\/\1>/gi, (match, tag, attrs) => {
|
|
127
|
+
if (!ZH_TEST.test(match)) return match;
|
|
128
|
+
if (strategy === 'remove') return '';
|
|
129
|
+
// 'empty': keep the opening/closing tags, remove content
|
|
130
|
+
const openTag = attrs ? `<${tag}${attrs}>` : `<${tag}>`;
|
|
131
|
+
return `${openTag}</${tag}>`;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fix Chinese in SVG comments and metadata elements.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} filePath - absolute path to the .svg file
|
|
139
|
+
* @param {Finding[]} findings - from findZhCnInSvgFile (all types)
|
|
140
|
+
* @param {object} opts
|
|
141
|
+
* - commentStrategy 'clean' | 'remove' (default: 'clean')
|
|
142
|
+
* - metadataStrategy 'remove' | 'empty' (default: 'remove')
|
|
143
|
+
* - attrStrategy 'clean' | 'remove' | false (default: false)
|
|
144
|
+
* @returns {{ fixed: number }}
|
|
145
|
+
*/
|
|
146
|
+
module.exports = function fixZhCnInSvgFile(filePath, findings, opts) {
|
|
147
|
+
const {
|
|
148
|
+
commentStrategy = 'clean',
|
|
149
|
+
metadataStrategy = 'remove',
|
|
150
|
+
attrStrategy = false,
|
|
151
|
+
} = opts || {};
|
|
152
|
+
|
|
153
|
+
const hasFixableFindings = findings.some(
|
|
154
|
+
(f) => f.type === 'svg-comment' || f.type === 'svg-metadata' ||
|
|
155
|
+
(f.type === 'svg-attr' && attrStrategy)
|
|
156
|
+
);
|
|
157
|
+
if (!hasFixableFindings) return { fixed: 0 };
|
|
158
|
+
|
|
159
|
+
let source;
|
|
160
|
+
try {
|
|
161
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
162
|
+
} catch (err) {
|
|
163
|
+
log.error(`[nozhcn] Cannot read SVG file: ${filePath} — ${err.message}`);
|
|
164
|
+
return { fixed: 0 };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const before = source;
|
|
168
|
+
|
|
169
|
+
// Fix comments
|
|
170
|
+
const hasCommentFindings = findings.some((f) => f.type === 'svg-comment');
|
|
171
|
+
if (hasCommentFindings) {
|
|
172
|
+
source = fixComments(source, commentStrategy);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Fix metadata elements
|
|
176
|
+
const hasMetadataFindings = findings.some((f) => f.type === 'svg-metadata');
|
|
177
|
+
if (hasMetadataFindings) {
|
|
178
|
+
source = fixMetadata(source, metadataStrategy);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fix attributes (opt-in)
|
|
182
|
+
const hasAttrFindings = findings.some((f) => f.type === 'svg-attr');
|
|
183
|
+
if (hasAttrFindings && attrStrategy) {
|
|
184
|
+
source = fixAttrs(source, attrStrategy);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (source === before) return { fixed: 0 };
|
|
188
|
+
|
|
189
|
+
// Clean up blank lines left behind by removed elements/comments
|
|
190
|
+
source = source.replace(/\n[ \t]*\n/g, '\n');
|
|
191
|
+
|
|
192
|
+
// Count how many fixable findings were addressed
|
|
193
|
+
const fixed = findings.filter(
|
|
194
|
+
(f) => f.type === 'svg-comment' || f.type === 'svg-metadata' ||
|
|
195
|
+
(f.type === 'svg-attr' && attrStrategy)
|
|
196
|
+
).length;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
fs.writeFileSync(filePath, source, { encoding: 'utf-8' });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
log.error(`[nozhcn] Cannot write SVG file: ${filePath} — ${err.message}`);
|
|
202
|
+
return { fixed: 0 };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { fixed };
|
|
206
|
+
};
|
|
@@ -159,19 +159,27 @@ module.exports = function (
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// XXX: [TRICKY] 防止中文转码为 unicode
|
|
162
|
+
// 注意:recast 打印 StringLiteral 时直接使用 node.value,而不是 extra.raw
|
|
163
|
+
// 所以需要直接创建值为 id 的字符串节点,同时保留原始中文在 extra 中
|
|
162
164
|
function hackValue(value, id) {
|
|
163
|
-
|
|
165
|
+
// 如果 id 未定义,使用 value 作为回退(用于第一次收集阶段)
|
|
166
|
+
const actualId = id || value;
|
|
167
|
+
if (actualId) hacked[actualId] = true;
|
|
164
168
|
|
|
165
169
|
// 字符串默认是单引号,如果模板字符串中出现单引号,需要转义,把 ' 换成 \'
|
|
166
170
|
// 就算用户自定配置字符串为双引号,也没事,默认会转义双引号,但是默认不会处理单引号,所以自己手动处理
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
let rawValue = actualId;
|
|
172
|
+
if (/\'/.test(actualId)) {
|
|
173
|
+
rawValue = actualId.replace(/\'/g, "\\'");
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
|
|
176
|
+
// 直接创建值为 actualId 的字符串节点,recast 会使用 node.value 作为输出
|
|
177
|
+
return Object.assign(t.StringLiteral(actualId), {
|
|
172
178
|
extra: {
|
|
173
|
-
raw: `'${
|
|
174
|
-
rawValue:
|
|
179
|
+
raw: `'${rawValue}'`,
|
|
180
|
+
rawValue: actualId,
|
|
181
|
+
// 保存原始中文,用于后续需要时使用
|
|
182
|
+
originalValue: value,
|
|
175
183
|
},
|
|
176
184
|
});
|
|
177
185
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const
|
|
3
|
+
const recast = require("recast");
|
|
4
4
|
const babel = require("@babel/core");
|
|
5
|
-
const generate = require("@babel/generator").default;
|
|
6
5
|
const traverse = require("@babel/traverse").default;
|
|
7
6
|
const pluginSyntaxJSX = require("@babel/plugin-syntax-jsx");
|
|
8
7
|
const pluginSyntaxProposalOptionalChaining = require("@babel/plugin-proposal-optional-chaining");
|
|
@@ -23,7 +22,9 @@ const log = require("./log");
|
|
|
23
22
|
// 获取文件中需要忽略转化通用国际化API规范的所有行号
|
|
24
23
|
function getIgnoreLines(ast) {
|
|
25
24
|
const ignoreBlocks = [];
|
|
26
|
-
|
|
25
|
+
|
|
26
|
+
// 收集注释的辅助函数
|
|
27
|
+
function processComment(comment) {
|
|
27
28
|
const { type, value, loc } = comment;
|
|
28
29
|
const last = ignoreBlocks.length - 1;
|
|
29
30
|
|
|
@@ -50,9 +51,25 @@ function getIgnoreLines(ast) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// 从 ast.comments 获取注释(Babel 格式)
|
|
55
|
+
if (ast.comments && Array.isArray(ast.comments)) {
|
|
56
|
+
for (const comment of ast.comments) {
|
|
57
|
+
processComment(comment);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 从 ast.tokens 获取注释(recast 格式)
|
|
62
|
+
if (ast.tokens && Array.isArray(ast.tokens)) {
|
|
63
|
+
for (const token of ast.tokens) {
|
|
64
|
+
if (token.type === "CommentLine" || token.type === "CommentBlock") {
|
|
65
|
+
processComment(token);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
// 如果缺少 disable-enable,直接作用到最后一行
|
|
54
71
|
const len = ignoreBlocks.length;
|
|
55
|
-
if (len > 0 && !ignoreBlocks[len - 1].end) {
|
|
72
|
+
if (len > 0 && !ignoreBlocks[len - 1].end && ast.loc && ast.loc.end) {
|
|
56
73
|
ignoreBlocks[len - 1].end = ast.loc.end.line;
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -89,6 +106,7 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
89
106
|
sourceType: "module",
|
|
90
107
|
ast: true,
|
|
91
108
|
configFile: false,
|
|
109
|
+
parserOpts: { tokens: true },
|
|
92
110
|
presets: [
|
|
93
111
|
...babelPresets,
|
|
94
112
|
[presetTypescript, { isTSX: true, allExtensions: true }],
|
|
@@ -116,14 +134,14 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
116
134
|
specialFileReg.some((item) => {
|
|
117
135
|
if (typeof item === "string") {
|
|
118
136
|
return item === filePath;
|
|
119
|
-
} else {
|
|
137
|
+
} else if (item instanceof RegExp) {
|
|
120
138
|
return item.test(filePath);
|
|
121
139
|
}
|
|
140
|
+
return false;
|
|
122
141
|
})
|
|
123
142
|
) {
|
|
124
143
|
file.special = true;
|
|
125
144
|
}
|
|
126
|
-
const isTSX = [".ts", ".tsx"].includes(path.extname(filePath));
|
|
127
145
|
|
|
128
146
|
const r = {
|
|
129
147
|
allWords,
|
|
@@ -137,8 +155,17 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
137
155
|
const sourceCode = fs.readFileSync(filePath, "utf8");
|
|
138
156
|
|
|
139
157
|
let ast;
|
|
158
|
+
let recastAst;
|
|
140
159
|
try {
|
|
141
|
-
|
|
160
|
+
// 使用 recast 解析,内部委托给 babel,保留原始格式信息
|
|
161
|
+
recastAst = recast.parse(sourceCode, {
|
|
162
|
+
parser: {
|
|
163
|
+
parse(source) {
|
|
164
|
+
return babel.parseSync(source, transformOptions);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
ast = recastAst.program;
|
|
142
169
|
} catch (error) {
|
|
143
170
|
replaceWords &&
|
|
144
171
|
log.error(`文件解析出错:${file.filePath}
|
|
@@ -146,7 +173,7 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
146
173
|
return;
|
|
147
174
|
}
|
|
148
175
|
|
|
149
|
-
opts.ignoreLines = getIgnoreLines(
|
|
176
|
+
opts.ignoreLines = getIgnoreLines(recastAst);
|
|
150
177
|
|
|
151
178
|
let makeVisitor = makeVisitorCollect;
|
|
152
179
|
if (type === "update") {
|
|
@@ -155,7 +182,7 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
155
182
|
|
|
156
183
|
try {
|
|
157
184
|
const visitor = makeVisitor(opts, r, file);
|
|
158
|
-
traverse(
|
|
185
|
+
traverse(recastAst, visitor);
|
|
159
186
|
} catch (e) {
|
|
160
187
|
replaceWords &&
|
|
161
188
|
log.error(`文件解析出错:${file.filePath}
|
|
@@ -167,11 +194,8 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
167
194
|
// 在只需要提取中文词条的情况下不传入replaceWords
|
|
168
195
|
if (!replaceWords) return;
|
|
169
196
|
|
|
170
|
-
//
|
|
171
|
-
let
|
|
172
|
-
retainLines: true,
|
|
173
|
-
decoratorsBeforeExport: true,
|
|
174
|
-
});
|
|
197
|
+
// 使用 recast 输出代码,仅重新打印被修改的 AST 节点,保留原始格式
|
|
198
|
+
let code = recast.print(recastAst).code;
|
|
175
199
|
|
|
176
200
|
if (!r.hasTouch) {
|
|
177
201
|
code = sourceCode;
|
|
@@ -184,13 +208,7 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
184
208
|
code = `${importCode}\n${code}`;
|
|
185
209
|
}
|
|
186
210
|
|
|
187
|
-
// 自定义格式化代码
|
|
188
211
|
if (r.hasTouch) {
|
|
189
|
-
if (conf.prettier) {
|
|
190
|
-
const parser = isTSX ? "typescript" : "babel";
|
|
191
|
-
code = prettier.format(code, { ...conf.prettier, parser });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
212
|
const target = file.currentOutput
|
|
195
213
|
? filePath.replace(file.currentEntry, file.currentOutput)
|
|
196
214
|
: filePath;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaoswise/intl",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"author": "cloudwiser",
|
|
5
5
|
"description": "intl",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"init": "npm i",
|
|
27
|
-
"build": "gulp build --gulpfile ./scripts/gulpfile.js"
|
|
27
|
+
"build": "gulp build --gulpfile ./scripts/gulpfile.js",
|
|
28
|
+
"test:nozhcn": "node --test --test-reporter=spec '__tests__/nozhcn/*.test.js'"
|
|
28
29
|
},
|
|
29
30
|
"keywords": [
|
|
30
31
|
"intl"
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"object-keys": "^1.0.11",
|
|
78
79
|
"prettier": "^2.8.1",
|
|
79
80
|
"react-intl-universal": "^2.6.11",
|
|
81
|
+
"recast": "^0.23.11",
|
|
80
82
|
"uuid": "^9.0.0",
|
|
81
83
|
"xlsx": "^0.18.5"
|
|
82
84
|
},
|