1// Copyright 2022 Google LLC 2 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6 7// http://www.apache.org/licenses/LICENSE-2.0 8 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License.import { exec } from 'child_process'; 14 15import DOM from './helpers/DOMFuncs'; 16import { FileIO } from './helpers/FileIO'; 17import { loadMIgrationList } from './helpers/migrationList'; 18import { processQueriedEntries, TEvalExistingEntry } from './helpers/processXML'; 19import { repoPath } from './helpers/rootPath'; 20import { groupReplace } from './helpers/textFuncs'; 21 22async function init() { 23 const migrationMap = await loadMIgrationList(); 24 const basePath = `${repoPath}/../tm-qpr-dev/frameworks/base/core/res/res/values/`; 25 26 await processQueriedEntries(migrationMap, { 27 containerQuery: 'declare-styleable[name="Theme"]', 28 hidable: true, 29 path: `${basePath}attrs.xml`, 30 step: 0, 31 tagName: 'attr', 32 evalExistingEntry: (_attrValue, migItem, qItem) => { 33 const { hidden, textContent: currentComment } = DOM.getElementComment(qItem); 34 35 if (hidden) migItem.isHidden = hidden; 36 37 const { newComment } = migItem; 38 return [ 39 hidden ? 'update' : 'duplicate', 40 { 41 attrs: { name: migItem.replaceToken }, 42 ...(newComment 43 ? { comment: `${newComment} @hide ` } 44 : currentComment 45 ? { comment: hidden ? currentComment : `${currentComment} @hide ` } 46 : {}), 47 }, 48 ]; 49 }, 50 evalMissingEntry: (_originalToken, { replaceToken, newComment }) => { 51 return { 52 tagName: 'attr', 53 attrs: { 54 name: replaceToken, 55 format: 'color', 56 }, 57 comment: `${newComment} @hide `, 58 }; 59 }, 60 }); 61 62 // only update all existing entries 63 await processQueriedEntries(migrationMap, { 64 tagName: 'item', 65 path: `${basePath}themes_device_defaults.xml`, 66 containerQuery: 'resources', 67 step: 2, 68 evalExistingEntry: (_attrValue, { isHidden, replaceToken, step }, _qItem) => { 69 if (step[0] != 'ignore') 70 return [ 71 isHidden ? 'update' : 'duplicate', 72 { 73 attrs: { name: replaceToken }, 74 }, 75 ]; 76 }, 77 }); 78 79 // add missing entries on specific container 80 await processQueriedEntries(migrationMap, { 81 tagName: 'item', 82 path: `${basePath}themes_device_defaults.xml`, 83 containerQuery: 'resources style[parent="Theme.Material"]', 84 step: 3, 85 evalMissingEntry: (originalToken, { newDefaultValue, replaceToken }) => { 86 return { 87 tagName: 'item', 88 content: newDefaultValue, 89 attrs: { 90 name: replaceToken, 91 }, 92 }; 93 }, 94 }); 95 96 const evalExistingEntry: TEvalExistingEntry = (_attrValue, { replaceToken, step }, _qItem) => { 97 if (step[0] == 'update') 98 return [ 99 'update', 100 { 101 attrs: { name: replaceToken }, 102 }, 103 ]; 104 }; 105 106 await processQueriedEntries(migrationMap, { 107 tagName: 'item', 108 containerQuery: 'resources', 109 path: `${basePath}../values-night/themes_device_defaults.xml`, 110 step: 4, 111 evalExistingEntry, 112 }); 113 114 await processQueriedEntries(migrationMap, { 115 tagName: 'java-symbol', 116 path: `${basePath}symbols.xml`, 117 containerQuery: 'resources', 118 step: 5, 119 evalExistingEntry, 120 }); 121 122 // update attributes on tracked XML files 123 { 124 const searchAttrs = [ 125 'android:color', 126 'android:indeterminateTint', 127 'app:tint', 128 'app:backgroundTint', 129 'android:background', 130 'android:tint', 131 'android:drawableTint', 132 'android:textColor', 133 'android:fillColor', 134 'android:startColor', 135 'android:endColor', 136 'name', 137 'ns1:color', 138 ]; 139 140 const filtered = new Map( 141 [...migrationMap] 142 .filter(([_originalToken, { step }]) => step[0] == 'update') 143 .map(([originalToken, { replaceToken }]) => [originalToken, replaceToken]) 144 ); 145 146 const query = 147 searchAttrs.map((str) => `*[${str}]`).join(',') + 148 [...filtered.keys()].map((originalToken) => `item[name*="${originalToken}"]`).join(','); 149 150 const trackedFiles = await FileIO.loadFileList( 151 `${__dirname}/resources/whitelist/xmls1.json` 152 ); 153 154 const promises = trackedFiles.map(async (locaFilePath) => { 155 const filePath = `${repoPath}/${locaFilePath}`; 156 157 const doc = await FileIO.loadXML(filePath); 158 const docUpdated = DOM.replaceStringInAttributeValueOnQueried( 159 doc.documentElement, 160 query, 161 searchAttrs, 162 filtered 163 ); 164 if (docUpdated) { 165 await FileIO.saveFile(DOM.XMLDocToString(doc), filePath); 166 } else { 167 console.warn(`Failed to update tracked file: '${locaFilePath}'`); 168 } 169 }); 170 await Promise.all(promises); 171 } 172 173 // updates tag content on tracked files 174 { 175 const searchPrefixes = ['?android:attr/', '?androidprv:attr/']; 176 const filtered = searchPrefixes 177 .reduce<Array<[string, string]>>((acc, prefix) => { 178 return [ 179 ...acc, 180 ...[...migrationMap.entries()] 181 .filter(([_originalToken, { step }]) => step[0] == 'update') 182 .map( 183 ([originalToken, { replaceToken }]) => 184 [`${prefix}${originalToken}`, `${prefix}${replaceToken}`] as [ 185 string, 186 string 187 ] 188 ), 189 ]; 190 }, []) 191 .sort((a, b) => b[0].length - a[0].length); 192 193 const trackedFiles = await FileIO.loadFileList( 194 `${__dirname}/resources/whitelist/xmls2.json` 195 ); 196 197 const promises = trackedFiles.map(async (locaFilePath) => { 198 const filePath = `${repoPath}/${locaFilePath}`; 199 const doc = await FileIO.loadXML(filePath); 200 const docUpdated = DOM.replaceContentTextOnQueried( 201 doc.documentElement, 202 'item, color', 203 filtered 204 ); 205 if (docUpdated) { 206 await FileIO.saveFile(DOM.XMLDocToString(doc), filePath); 207 } else { 208 console.warn(`Failed to update tracked file: '${locaFilePath}'`); 209 } 210 }); 211 await Promise.all(promises); 212 } 213 214 // replace imports on Java / Kotlin 215 { 216 const replaceMap = new Map( 217 [...migrationMap.entries()] 218 .filter(([_originalToken, { step }]) => step[0] == 'update') 219 .map( 220 ([originalToken, { replaceToken }]) => 221 [originalToken, replaceToken] as [string, string] 222 ) 223 .sort((a, b) => b[0].length - a[0].length) 224 ); 225 226 const trackedFiles = await FileIO.loadFileList( 227 `${__dirname}/resources/whitelist/java.json` 228 ); 229 230 const promises = trackedFiles.map(async (locaFilePath) => { 231 const filePath = `${repoPath}/${locaFilePath}`; 232 const fileContent = await FileIO.loadFileAsText(filePath); 233 const str = groupReplace(fileContent, replaceMap, 'R.attr.(#group#)(?![a-zA-Z])'); 234 await FileIO.saveFile(str, filePath); 235 }); 236 await Promise.all(promises); 237 } 238} 239 240init(); 241