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