// Copyright 2022 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.import { exec } from 'child_process'; import DOM from './helpers/DOMFuncs'; import { FileIO } from './helpers/FileIO'; import { loadMIgrationList } from './helpers/migrationList'; import { processQueriedEntries, TEvalExistingEntry } from './helpers/processXML'; import { repoPath } from './helpers/rootPath'; import { groupReplace } from './helpers/textFuncs'; async function init() { const migrationMap = await loadMIgrationList(); const basePath = `${repoPath}/../tm-qpr-dev/frameworks/base/core/res/res/values/`; await processQueriedEntries(migrationMap, { containerQuery: 'declare-styleable[name="Theme"]', hidable: true, path: `${basePath}attrs.xml`, step: 0, tagName: 'attr', evalExistingEntry: (_attrValue, migItem, qItem) => { const { hidden, textContent: currentComment } = DOM.getElementComment(qItem); if (hidden) migItem.isHidden = hidden; const { newComment } = migItem; return [ hidden ? 'update' : 'duplicate', { attrs: { name: migItem.replaceToken }, ...(newComment ? { comment: `${newComment} @hide ` } : currentComment ? { comment: hidden ? currentComment : `${currentComment} @hide ` } : {}), }, ]; }, evalMissingEntry: (_originalToken, { replaceToken, newComment }) => { return { tagName: 'attr', attrs: { name: replaceToken, format: 'color', }, comment: `${newComment} @hide `, }; }, }); // only update all existing entries await processQueriedEntries(migrationMap, { tagName: 'item', path: `${basePath}themes_device_defaults.xml`, containerQuery: 'resources', step: 2, evalExistingEntry: (_attrValue, { isHidden, replaceToken, step }, _qItem) => { if (step[0] != 'ignore') return [ isHidden ? 'update' : 'duplicate', { attrs: { name: replaceToken }, }, ]; }, }); // add missing entries on specific container await processQueriedEntries(migrationMap, { tagName: 'item', path: `${basePath}themes_device_defaults.xml`, containerQuery: 'resources style[parent="Theme.Material"]', step: 3, evalMissingEntry: (originalToken, { newDefaultValue, replaceToken }) => { return { tagName: 'item', content: newDefaultValue, attrs: { name: replaceToken, }, }; }, }); const evalExistingEntry: TEvalExistingEntry = (_attrValue, { replaceToken, step }, _qItem) => { if (step[0] == 'update') return [ 'update', { attrs: { name: replaceToken }, }, ]; }; await processQueriedEntries(migrationMap, { tagName: 'item', containerQuery: 'resources', path: `${basePath}../values-night/themes_device_defaults.xml`, step: 4, evalExistingEntry, }); await processQueriedEntries(migrationMap, { tagName: 'java-symbol', path: `${basePath}symbols.xml`, containerQuery: 'resources', step: 5, evalExistingEntry, }); // update attributes on tracked XML files { const searchAttrs = [ 'android:color', 'android:indeterminateTint', 'app:tint', 'app:backgroundTint', 'android:background', 'android:tint', 'android:drawableTint', 'android:textColor', 'android:fillColor', 'android:startColor', 'android:endColor', 'name', 'ns1:color', ]; const filtered = new Map( [...migrationMap] .filter(([_originalToken, { step }]) => step[0] == 'update') .map(([originalToken, { replaceToken }]) => [originalToken, replaceToken]) ); const query = searchAttrs.map((str) => `*[${str}]`).join(',') + [...filtered.keys()].map((originalToken) => `item[name*="${originalToken}"]`).join(','); const trackedFiles = await FileIO.loadFileList( `${__dirname}/resources/whitelist/xmls1.json` ); const promises = trackedFiles.map(async (locaFilePath) => { const filePath = `${repoPath}/${locaFilePath}`; const doc = await FileIO.loadXML(filePath); const docUpdated = DOM.replaceStringInAttributeValueOnQueried( doc.documentElement, query, searchAttrs, filtered ); if (docUpdated) { await FileIO.saveFile(DOM.XMLDocToString(doc), filePath); } else { console.warn(`Failed to update tracked file: '${locaFilePath}'`); } }); await Promise.all(promises); } // updates tag content on tracked files { const searchPrefixes = ['?android:attr/', '?androidprv:attr/']; const filtered = searchPrefixes .reduce<Array<[string, string]>>((acc, prefix) => { return [ ...acc, ...[...migrationMap.entries()] .filter(([_originalToken, { step }]) => step[0] == 'update') .map( ([originalToken, { replaceToken }]) => [`${prefix}${originalToken}`, `${prefix}${replaceToken}`] as [ string, string ] ), ]; }, []) .sort((a, b) => b[0].length - a[0].length); const trackedFiles = await FileIO.loadFileList( `${__dirname}/resources/whitelist/xmls2.json` ); const promises = trackedFiles.map(async (locaFilePath) => { const filePath = `${repoPath}/${locaFilePath}`; const doc = await FileIO.loadXML(filePath); const docUpdated = DOM.replaceContentTextOnQueried( doc.documentElement, 'item, color', filtered ); if (docUpdated) { await FileIO.saveFile(DOM.XMLDocToString(doc), filePath); } else { console.warn(`Failed to update tracked file: '${locaFilePath}'`); } }); await Promise.all(promises); } // replace imports on Java / Kotlin { const replaceMap = new Map( [...migrationMap.entries()] .filter(([_originalToken, { step }]) => step[0] == 'update') .map( ([originalToken, { replaceToken }]) => [originalToken, replaceToken] as [string, string] ) .sort((a, b) => b[0].length - a[0].length) ); const trackedFiles = await FileIO.loadFileList( `${__dirname}/resources/whitelist/java.json` ); const promises = trackedFiles.map(async (locaFilePath) => { const filePath = `${repoPath}/${locaFilePath}`; const fileContent = await FileIO.loadFileAsText(filePath); const str = groupReplace(fileContent, replaceMap, 'R.attr.(#group#)(?![a-zA-Z])'); await FileIO.saveFile(str, filePath); }); await Promise.all(promises); } } init();