blob: 11119c5b681f5bb2f4c0393934a715243d17c8fc [file] [log] [blame]
Lorne Mitchell072e6f82019-05-22 21:58:44 +00001// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5const fs = require('fs');
6const md5 = require('./md5');
7const {promisify} = require('util');
8const path = require('path');
9const readFileAsync = promisify(fs.readFile);
10const readDirAsync = promisify(fs.readdir);
11const statAsync = promisify(fs.stat);
12
13const esprimaTypes = {
14 BI_EXPR: 'BinaryExpression',
15 CALL_EXPR: 'CallExpression',
16 COND_EXPR: 'ConditionalExpression',
17 IDENTIFIER: 'Identifier',
18 LITERAL: 'Literal',
19 MEMBER_EXPR: 'MemberExpression',
20 TAGGED_TEMP_EXPR: 'TaggedTemplateExpression',
21 TEMP_LITERAL: 'TemplateLiteral'
22};
23
24const excludeFiles = ['lighthouse-dt-bundle.js', 'Tests.js'];
25const excludeDirs = ['test_runner', 'Images', 'langpacks', 'node_modules'];
26const cppSpecialCharactersMap = {
27 '"': '\\"',
28 '\\': '\\\\',
29 '\n': '\\n'
30};
31const IDSPrefix = 'IDS_DEVTOOLS_';
32
33const THIRD_PARTY_PATH = path.resolve(__dirname, '..', '..', '..', '..', '..');
34const SRC_PATH = path.resolve(THIRD_PARTY_PATH, '..');
35const GRD_PATH = path.resolve(__dirname, '..', '..', 'front_end', 'langpacks', 'devtools_ui_strings.grd');
36const REPO_NODE_MODULES_PATH = path.resolve(THIRD_PARTY_PATH, 'node', 'node_modules');
37const escodegen = require(path.resolve(REPO_NODE_MODULES_PATH, 'escodegen'));
38const esprima = require(path.resolve(REPO_NODE_MODULES_PATH, 'esprima'));
39
40function getRelativeFilePathFromSrc(filePath) {
41 return path.relative(SRC_PATH, filePath);
42}
43
44function shouldParseDirectory(directoryName) {
45 return !excludeDirs.some(dir => directoryName.includes(dir));
46}
47
48/**
49 * @filepath can be partial path or full path, as long as it contains the file name.
50 */
51function shouldParseFile(filepath) {
52 return !excludeFiles.includes(path.basename(filepath));
53}
54
55async function parseFileContent(filePath) {
56 const fileContent = await readFileAsync(filePath);
57 return fileContent.toString();
58}
59
60function isNodeCallOnObject(node, objectName, propertyName) {
61 return node !== undefined && node.type === esprimaTypes.CALL_EXPR &&
62 verifyCallExpressionCallee(node.callee, objectName, propertyName);
63}
64
65function isNodeCommonUIStringCall(node) {
66 return isNodeCallOnObject(node, 'Common', 'UIString');
67}
68
69function isNodeUIformatLocalized(node) {
70 return isNodeCallOnObject(node, 'UI', 'formatLocalized');
71}
72
73function isNodelsTaggedTemplateExpression(node) {
74 return node !== undefined && node.type === esprimaTypes.TAGGED_TEMP_EXPR && verifyIdentifier(node.tag, 'ls') &&
75 node.quasi !== undefined && node.quasi.type !== undefined && node.quasi.type === esprimaTypes.TEMP_LITERAL;
76}
77
78/**
79 * Verify callee of objectName.propertyName(), e.g. Common.UIString().
80 */
81function verifyCallExpressionCallee(callee, objectName, propertyName) {
82 return callee !== undefined && callee.type === esprimaTypes.MEMBER_EXPR && callee.computed === false &&
83 verifyIdentifier(callee.object, objectName) && verifyIdentifier(callee.property, propertyName);
84}
85
86function verifyIdentifier(node, name) {
87 return node !== undefined && node.type === esprimaTypes.IDENTIFIER && node.name === name;
88}
89
90function getLocalizationCase(node) {
91 if (isNodeCommonUIStringCall(node))
92 return 'Common.UIString';
93 else if (isNodelsTaggedTemplateExpression(node))
94 return 'Tagged Template';
95 else if (isNodeUIformatLocalized(node))
96 return 'UI.formatLocalized';
97 else
98 return null;
99}
100
101function isLocalizationCall(node) {
102 return isNodeCommonUIStringCall(node) || isNodelsTaggedTemplateExpression(node) || isNodeUIformatLocalized(node);
103}
104
105/**
106 * Verify if callee is functionName() or object.functionName().
107 */
108function verifyFunctionCallee(callee, functionName) {
109 return callee !== undefined &&
110 ((callee.type === esprimaTypes.IDENTIFIER && callee.name === functionName) ||
111 (callee.type === esprimaTypes.MEMBER_EXPR && verifyIdentifier(callee.property, functionName)));
112}
113
114function getLocationMessage(location) {
115 if (location !== undefined && location.start !== undefined && location.end !== undefined &&
116 location.start.line !== undefined && location.end.line !== undefined) {
117 const startLine = location.start.line;
118 const endLine = location.end.line;
119 if (startLine === endLine)
120 return ` Line ${startLine}`;
121 else
122 return ` Line ${location.start.line}-${location.end.line}`;
123 }
124 return '';
125}
126
127function sanitizeStringIntoGRDFormat(str) {
128 return str.replace(/&/g, '&')
129 .replace(/</g, '&lt;')
130 .replace(/>/g, '&gt;')
131 .replace(/"/g, '&quot;')
132 .replace(/'/g, '&apos;')
133}
134
135function sanitizeStringIntoFrontendFormat(str) {
136 return str.replace(/&apos;/g, '\'')
137 .replace(/&quot;/g, '"')
138 .replace(/&gt;/g, '>')
139 .replace(/&lt;/g, '<')
140 .replace(/&amp;/g, '&');
141}
142
143function sanitizeString(str, specialCharactersMap) {
144 let sanitizedStr = '';
145 for (let i = 0; i < str.length; i++) {
146 let currChar = str.charAt(i);
147 if (specialCharactersMap[currChar] !== undefined)
148 currChar = specialCharactersMap[currChar];
149
150 sanitizedStr += currChar;
151 }
152 return sanitizedStr;
153}
154
155function sanitizeStringIntoCppFormat(str) {
156 return sanitizeString(str, cppSpecialCharactersMap);
157}
158
159async function getFilesFromItem(itemPath, filePaths, acceptedFileEndings) {
160 const stat = await statAsync(itemPath);
161 if (stat.isDirectory() && shouldParseDirectory(itemPath))
162 return await getFilesFromDirectory(itemPath, filePaths, acceptedFileEndings);
163
164 const hasAcceptedEnding =
165 acceptedFileEndings.some(acceptedEnding => itemPath.toLowerCase().endsWith(acceptedEnding.toLowerCase()));
166 if (hasAcceptedEnding && shouldParseFile(itemPath))
167 filePaths.push(itemPath);
168}
169
170async function getFilesFromDirectory(directoryPath, filePaths, acceptedFileEndings) {
171 const itemNames = await readDirAsync(directoryPath);
172 const promises = [];
173 for (const itemName of itemNames) {
174 const itemPath = path.resolve(directoryPath, itemName);
175 promises.push(getFilesFromItem(itemPath, filePaths, acceptedFileEndings));
176 }
177 return Promise.all(promises);
178}
179
180async function getChildDirectoriesFromDirectory(directoryPath) {
181 const dirPaths = [];
182 const itemNames = await readDirAsync(directoryPath);
183 for (const itemName of itemNames) {
184 const itemPath = path.resolve(directoryPath, itemName);
185 const stat = await statAsync(itemPath);
186 if (stat.isDirectory() && shouldParseDirectory(itemName))
187 dirPaths.push(itemPath);
188 }
189 return dirPaths;
190}
191
192/**
193 * Get the parent grdp file path for the input frontend file path.
194 * NOTE: Naming convention of a grdp file is the name of the child directory under
195 * devtools/front_end plus _strings.grdp
196 */
197function getGRDPFilePath(frontendFilepath, frontendDirs) {
198 const frontendDirsLowerCase = frontendDirs.map(dir => dir.toLowerCase());
199 const dirpath = path.dirname(frontendFilepath);
200 if (frontendDirsLowerCase.includes(dirpath.toLowerCase()))
201 return path.resolve(dirpath, `${path.basename(dirpath)}_strings.grdp`);
202}
203
204function modifyStringIntoGRDFormat(str, args) {
205 let sanitizedStr = sanitizeStringIntoGRDFormat(str);
206
207 const phRegex = /%d|%f|%s|%.[0-9]f/gm;
208 if (!str.match(phRegex))
209 return sanitizedStr;
210
211 let phNames;
212 if (args !== undefined)
213 phNames = args.map(arg => arg.replace(/[^a-zA-Z]/gm, '_').toUpperCase());
214 else
215 phNames = ['PH1', 'PH2', 'PH3', 'PH4', 'PH5', 'PH6', 'PH7', 'PH8', 'PH9'];
216
217 // It replaces all placeholders with <ph> tags.
218 let match;
219 let count = 1;
220 while ((match = phRegex.exec(sanitizedStr)) !== null) {
221 // This is necessary to avoid infinite loops with zero-width matches
222 if (match.index === phRegex.lastIndex)
223 phRegex.lastIndex++;
224
225 // match[0]: the placeholder (e.g. %d, %s, %.2f, etc.)
226 const ph = match[0];
227 // e.g. $1s, $1d, $1.2f
228 const newPh = `$${count}` + ph.substr(1);
229
230 const i = sanitizedStr.indexOf(ph);
231 sanitizedStr = `${sanitizedStr.substring(0, i)}<ph name="${phNames[count - 1]}">${newPh}</ph>${
232 sanitizedStr.substring(i + ph.length)}`;
233 count++;
234 }
235 return sanitizedStr;
236}
237
238function createGrdpMessage(ids, stringObj) {
239 let message = ` <message name="${ids}" desc="">\n`;
240 message += ` ${modifyStringIntoGRDFormat(stringObj.string, stringObj.arguments)}\n`;
241 message += ' </message>\n';
242 return message;
243}
244
245function getIDSKey(str) {
246 return `${IDSPrefix}${md5(str)}`
247}
248
249module.exports = {
250 createGrdpMessage,
251 escodegen,
252 esprima,
253 esprimaTypes,
254 getChildDirectoriesFromDirectory,
255 getFilesFromDirectory,
256 getGRDPFilePath,
257 getIDSKey,
258 getLocalizationCase,
259 getLocationMessage,
260 getRelativeFilePathFromSrc,
261 GRD_PATH,
262 IDSPrefix,
263 isLocalizationCall,
264 modifyStringIntoGRDFormat,
265 parseFileContent,
266 sanitizeStringIntoCppFormat,
267 sanitizeStringIntoFrontendFormat,
268 verifyFunctionCallee
269};