diff options
| author | Marcelo Arteiro <arteiro@google.com> | 2023-01-06 17:48:22 +0000 |
|---|---|---|
| committer | Marcelo Arteiro <arteiro@google.com> | 2023-01-25 16:48:18 +0000 |
| commit | f27a8e140de48492f5512717b234fa056fd996ef (patch) | |
| tree | 6fdd8ddbfd5a930670d15dd2afd99f82e2f3d8a0 /packages/SystemUI/scripts/token_alignment/helpers | |
| parent | b56558ee8d849ff5d27da5ff65dac36b91634fa7 (diff) | |
Scripts for token migration
Instructions:
1. Install packages with `npm i`
2. Update migration list resources/migrationList.csv
3. Check run scripts in package.json
4. To run with script use `npm run main`
5. To run with debg tokens `npm run main -- debug`
6. To reset all repos `npm run resetRepo`
Test: Manual
Bug: 241778903
Change-Id: I0f828f1d75fdbd39ef8dfe2ca25ac02aaff4bfca
Diffstat (limited to 'packages/SystemUI/scripts/token_alignment/helpers')
7 files changed, 650 insertions, 0 deletions
diff --git a/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts b/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts new file mode 100644 index 000000000000..80e075c9e070 --- /dev/null +++ b/packages/SystemUI/scripts/token_alignment/helpers/DOMFuncs.ts @@ -0,0 +1,297 @@ +// 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. + +type IElementComment = + | { commentNode: undefined; textContent: undefined; hidden: undefined } + | { commentNode: Node; textContent: string; hidden: boolean }; + +interface ITag { + attrs?: Record<string, string | number>; + tagName: string; +} + +export interface INewTag extends ITag { + content?: string | number; + comment?: string; +} + +export type IUpdateTag = Partial<Omit<INewTag, 'tagName'>>; + +export default class DOM { + static addEntry(containerElement: Element, tagOptions: INewTag) { + const doc = containerElement.ownerDocument; + const exists = this.alreadyHasEntry(containerElement, tagOptions); + + if (exists) { + console.log('Ignored adding entry already available: ', exists.outerHTML); + return; + } + + let insertPoint: Node | null = containerElement.lastElementChild; //.childNodes[containerElement.childNodes.length - 1]; + + if (!insertPoint) { + console.log('Ignored adding entry in empity parent: ', containerElement.outerHTML); + return; + } + + const { attrs, comment, content, tagName } = tagOptions; + + if (comment) { + const commentNode = doc.createComment(comment); + this.insertAfterIdented(commentNode, insertPoint); + insertPoint = commentNode; + } + + const newEl = doc.createElement(tagName); + if (content) newEl.innerHTML = content.toString(); + if (attrs) + Object.entries(attrs).forEach(([attr, value]) => + newEl.setAttribute(attr, value.toString()) + ); + this.insertAfterIdented(newEl, insertPoint); + + return true; + } + + static insertBeforeIndented(newNode: Node, referenceNode: Node) { + const paddingNode = referenceNode.previousSibling; + const ownerDoc = referenceNode.ownerDocument; + const containerNode = referenceNode.parentNode; + + if (!paddingNode || !ownerDoc || !containerNode) return; + + const currentPadding = paddingNode.textContent || ''; + const textNode = referenceNode.ownerDocument.createTextNode(currentPadding); + + containerNode.insertBefore(newNode, referenceNode); + containerNode.insertBefore(textNode, newNode); + } + + static insertAfterIdented(newNode: Node, referenceNode: Node) { + const paddingNode = referenceNode.previousSibling; + const ownerDoc = referenceNode.ownerDocument; + const containerNode = referenceNode.parentNode; + + if (!paddingNode || !ownerDoc || !containerNode) return; + + const currentPadding = paddingNode.textContent || ''; + const textNode = ownerDoc.createTextNode(currentPadding); + + containerNode.insertBefore(newNode, referenceNode.nextSibling); + containerNode.insertBefore(textNode, newNode); + } + + static getElementComment(el: Element): IElementComment { + const commentNode = el.previousSibling?.previousSibling; + + const out = { commentNode: undefined, textContent: undefined, hidden: undefined }; + + if (!commentNode) return out; + + const textContent = commentNode.textContent || ''; + const hidden = textContent.substring(textContent.length - 6) == '@hide '; + + if (!(commentNode && commentNode.nodeName == '#comment')) return out; + + return { commentNode, textContent, hidden: hidden }; + } + + static duplicateEntryWithChange( + templateElement: Element, + options: Omit<IUpdateTag, 'content'> + ) { + const exists = this.futureEntryAlreadyExist(templateElement, options); + if (exists) { + console.log('Ignored duplicating entry already available: ', exists.outerHTML); + return; + } + + const { commentNode } = this.getElementComment(templateElement); + let insertPoint: Node = templateElement; + + if (commentNode) { + const newComment = commentNode.cloneNode(); + this.insertAfterIdented(newComment, insertPoint); + insertPoint = newComment; + } + + const newEl = templateElement.cloneNode(true) as Element; + this.insertAfterIdented(newEl, insertPoint); + + this.updateElement(newEl, options); + return true; + } + + static replaceStringInAttributeValueOnQueried( + root: Element, + query: string, + attrArray: string[], + replaceMap: Map<string, string> + ): boolean { + let updated = false; + const queried = [...Array.from(root.querySelectorAll(query)), root]; + + queried.forEach((el) => { + attrArray.forEach((attr) => { + if (el.hasAttribute(attr)) { + const currentAttrValue = el.getAttribute(attr); + + if (!currentAttrValue) return; + + [...replaceMap.entries()].some(([oldStr, newStr]) => { + if ( + currentAttrValue.length >= oldStr.length && + currentAttrValue.indexOf(oldStr) == + currentAttrValue.length - oldStr.length + ) { + el.setAttribute(attr, currentAttrValue.replace(oldStr, newStr)); + updated = true; + return true; + } + return false; + }); + } + }); + }); + + return updated; + } + + static updateElement(el: Element, updateOptions: IUpdateTag) { + const exists = this.futureEntryAlreadyExist(el, updateOptions); + if (exists) { + console.log('Ignored updating entry already available: ', exists.outerHTML); + return; + } + + const { comment, attrs, content } = updateOptions; + + if (comment) { + const { commentNode } = this.getElementComment(el); + if (commentNode) { + commentNode.textContent = comment; + } + } + + if (attrs) { + for (const attr in attrs) { + const value = attrs[attr]; + + if (value != undefined) { + el.setAttribute(attr, `${value}`); + } else { + el.removeAttribute(attr); + } + } + } + + if (content != undefined) { + el.innerHTML = `${content}`; + } + + return true; + } + + static elementToOptions(el: Element): ITag { + return { + attrs: this.getAllElementAttributes(el), + tagName: el.tagName, + }; + } + + static getAllElementAttributes(el: Element): Record<string, string> { + return el + .getAttributeNames() + .reduce( + (acc, attr) => ({ ...acc, [attr]: el.getAttribute(attr) || '' }), + {} as Record<string, string> + ); + } + + static futureEntryAlreadyExist(el: Element, updateOptions: IUpdateTag) { + const currentElOptions = this.elementToOptions(el); + + if (!el.parentElement) { + console.log('Checked el has no parent'); + process.exit(); + } + + return this.alreadyHasEntry(el.parentElement, { + ...currentElOptions, + ...updateOptions, + attrs: { ...currentElOptions.attrs, ...updateOptions.attrs }, + }); + } + + static alreadyHasEntry( + containerElement: Element, + { attrs, tagName }: Pick<INewTag, 'attrs' | 'tagName'> + ) { + const qAttrs = attrs + ? Object.entries(attrs) + .map(([a, v]) => `[${a}="${v}"]`) + .join('') + : ''; + + return containerElement.querySelector(tagName + qAttrs); + } + + static replaceContentTextOnQueried( + root: Element, + query: string, + replacePairs: Array<[string, string]> + ) { + let updated = false; + let queried = Array.from(root.querySelectorAll(query)); + + if (queried.length == 0) queried = [...Array.from(root.querySelectorAll(query)), root]; + + queried.forEach((el) => { + replacePairs.forEach(([oldStr, newStr]) => { + if (el.innerHTML == oldStr) { + el.innerHTML = newStr; + updated = true; + } + }); + }); + + return updated; + } + + static XMLDocToString(doc: XMLDocument) { + let str = ''; + + doc.childNodes.forEach((node) => { + switch (node.nodeType) { + case 8: // comment + str += `<!--${node.nodeValue}-->\n`; + break; + + case 3: // text + str += node.textContent; + break; + + case 1: // element + str += (node as Element).outerHTML; + break; + + default: + console.log('Unhandled node type: ' + node.nodeType); + break; + } + }); + + return str; + } +} diff --git a/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts b/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts new file mode 100644 index 000000000000..359e3ab6568b --- /dev/null +++ b/packages/SystemUI/scripts/token_alignment/helpers/FileIO.ts @@ -0,0 +1,112 @@ +// 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 { exec } from 'child_process'; +import { parse } from 'csv-parse'; +import { promises as fs } from 'fs'; +import jsdom from 'jsdom'; + +const DOMParser = new jsdom.JSDOM('').window.DOMParser as typeof window.DOMParser; + +type TFileList = string[]; + +export type TCSVRecord = Array<string | boolean | number>; + +class _FileIO { + public parser = new DOMParser(); + public saved: string[] = []; + + public loadXML = async (path: string): Promise<XMLDocument> => { + try { + const src = await this.loadFileAsText(path); + return this.parser.parseFromString(src, 'text/xml') as XMLDocument; + } catch (error) { + console.log(`Failed to parse XML file '${path}'.`, error); + process.exit(); + } + }; + + public loadFileAsText = async (path: string): Promise<string> => { + try { + return await fs.readFile(path, { encoding: 'utf8' }); + } catch (error) { + console.log(`Failed to read file '${path}'.`, error); + process.exit(); + } + }; + + public saveFile = async (data: string, path: string) => { + try { + await fs.writeFile(path, data, { encoding: 'utf8' }); + this.saved.push(path); + } catch (error) { + console.log(error); + console.log(`Failed to write file '${path}'.`); + process.exit(); + } + }; + + public loadFileList = async (path: string): Promise<TFileList> => { + const src = await this.loadFileAsText(path); + + try { + return JSON.parse(src) as TFileList; + } catch (error) { + console.log(error); + console.log(`Failed to parse JSON file '${path}'.`); + process.exit(); + } + }; + + public loadCSV = (path: string): Promise<Array<TCSVRecord>> => { + return new Promise((resolve, reject) => { + this.loadFileAsText(path).then((src) => { + parse( + src, + { + delimiter: ' ', + }, + (err, records) => { + if (err) { + reject(err); + return; + } + + resolve(records); + } + ); + }); + }); + }; + + formatSaved = () => { + const cmd = `idea format ${this.saved.join(' ')}`; + + exec(cmd, (error, out, stderr) => { + if (error) { + console.log(error.message); + return; + } + + if (stderr) { + console.log(stderr); + return; + } + + console.log(out); + }); + }; +} + +export const FileIO = new _FileIO(); diff --git a/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts b/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts new file mode 100644 index 000000000000..8d506449e6fe --- /dev/null +++ b/packages/SystemUI/scripts/token_alignment/helpers/migrationList.ts @@ -0,0 +1,70 @@ +// 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 { FileIO, TCSVRecord } from './FileIO'; +import ProcessArgs from './processArgs'; + +interface IInputMigItem { + migrationToken: string; + materialToken: string; + newDefaultValue?: string; + newComment?: string; +} + +interface IAditionalKeys { + step: ('update' | 'duplicate' | 'add' | 'ignore')[]; + isHidden: boolean; + replaceToken: string; +} + +export type IMigItem = Omit<IInputMigItem, 'materialToken' | 'migrationToken'> & IAditionalKeys; + +export type IMigrationMap = Map<string, IMigItem>; + +function isMigrationRecord(record: TCSVRecord): record is string[] { + return !record.some((value) => typeof value != 'string') || record.length != 5; +} + +export const loadMIgrationList = async function (): Promise<IMigrationMap> { + const out: IMigrationMap = new Map(); + const csv = await FileIO.loadCSV('resources/migrationList.csv'); + + csv.forEach((record, i) => { + if (i == 0) return; // header + + if (typeof record[0] != 'string') return; + + if (!isMigrationRecord(record)) { + console.log(`Failed to validade CSV record as string[5].`, record); + process.exit(); + } + + const [originalToken, materialToken, newDefaultValue, newComment, migrationToken] = record; + + if (out.has(originalToken)) { + console.log('Duplicated entry on Migration CSV file: ', originalToken); + return; + } + + out.set(originalToken, { + replaceToken: ProcessArgs.isDebug ? migrationToken : materialToken, + ...(!!newDefaultValue && { newDefaultValue }), + ...(!!newComment && { newComment }), + step: [], + isHidden: false, + }); + }); + + return new Map([...out].sort((a, b) => b[0].length - a[0].length)); +}; diff --git a/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts b/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts new file mode 100644 index 000000000000..be0e232e66b1 --- /dev/null +++ b/packages/SystemUI/scripts/token_alignment/helpers/processArgs.ts @@ -0,0 +1,21 @@ +// 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'; + +const myArgs = process.argv.slice(2); + +const ProcessArgs = { + isDebug: myArgs.includes('debug'), +}; + +export default ProcessArgs; diff --git a/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts b/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts new file mode 100644 index 000000000000..368d4cbad3bd --- /dev/null +++ b/packages/SystemUI/scripts/token_alignment/helpers/processXML.ts @@ -0,0 +1,102 @@ +// 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, { INewTag, IUpdateTag } from './DOMFuncs'; +import { FileIO } from './FileIO'; +import { IMigItem, IMigrationMap } from './migrationList'; + +export type TResultExistingEval = ['update' | 'duplicate', IUpdateTag] | void; +export type TResultMissingEval = INewTag | void; + +interface IProcessXML { + attr?: string; + containerQuery?: string; + evalExistingEntry?: TEvalExistingEntry; + evalMissingEntry?: TEvalMissingEntry; + hidable?: boolean; + path: string; + step: number; + tagName: string; +} + +export type TEvalExistingEntry = ( + attrname: string, + migItem: IMigItem, + qItem: Element +) => TResultExistingEval; + +export type TEvalMissingEntry = (originalToken: string, migItem: IMigItem) => TResultMissingEval; + +export async function processQueriedEntries( + migrationMap: IMigrationMap, + { + attr = 'name', + containerQuery = '*', + evalExistingEntry, + path, + step, + tagName, + evalMissingEntry, + }: IProcessXML +) { + const doc = await FileIO.loadXML(path); + + const containerElement = + (containerQuery && doc.querySelector(containerQuery)) || doc.documentElement; + + migrationMap.forEach((migItem, originalToken) => { + migItem.step[step] = 'ignore'; + + const queryTiems = containerElement.querySelectorAll( + `${tagName}[${attr}="${originalToken}"]` + ); + + if (evalMissingEntry) { + const addinOptions = evalMissingEntry(originalToken, migItem); + + if (queryTiems.length == 0 && containerElement && addinOptions) { + DOM.addEntry(containerElement, addinOptions); + migItem.step[step] = 'add'; + return; + } + } + + if (evalExistingEntry) + queryTiems.forEach((qEl) => { + const attrName = qEl.getAttribute(attr); + const migItem = migrationMap.get(attrName || ''); + + if (!attrName || !migItem) return; + + const updateOptions = evalExistingEntry(attrName, migItem, qEl); + + if (!updateOptions) return; + + const [processType, processOptions] = updateOptions; + + switch (processType) { + case 'update': + if (DOM.updateElement(qEl, processOptions)) migItem.step[step] = 'update'; + break; + + case 'duplicate': + if (DOM.duplicateEntryWithChange(qEl, processOptions)) + migItem.step[step] = 'duplicate'; + break; + } + }); + }); + + await FileIO.saveFile(doc.documentElement.outerHTML, path); +} diff --git a/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts b/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts new file mode 100644 index 000000000000..2c6f6329d7ea --- /dev/null +++ b/packages/SystemUI/scripts/token_alignment/helpers/rootPath.ts @@ -0,0 +1,21 @@ +// 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'; + +if (!process?.env?.ANDROID_BUILD_TOP) { + console.log( + "Error: Couldn't find 'ANDROID_BUILD_TOP' environment variable. Make sure to run 'lunch' in this terminal" + ); +} + +export const repoPath = process?.env?.ANDROID_BUILD_TOP; diff --git a/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts b/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts new file mode 100644 index 000000000000..6679c5a9d777 --- /dev/null +++ b/packages/SystemUI/scripts/token_alignment/helpers/textFuncs.ts @@ -0,0 +1,27 @@ +// 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'; + +export function groupReplace(src: string, replaceMap: Map<string, string>, pattern: string) { + const fullPattern = pattern.replace('#group#', [...replaceMap.keys()].join('|')); + + const regEx = new RegExp(fullPattern, 'g'); + + ''.replace; + + return src.replace(regEx, (...args) => { + //match, ...matches, offset, string, groups + const [match, key] = args as string[]; + return match.replace(key, replaceMap.get(key) || ''); + }); +} |
