[Lit Mangler]: Maintain property cases (#30306)

This commit is contained in:
Jay Harris
2025-07-29 08:17:44 +12:00
committed by GitHub
parent 3a63d15502
commit 432fb0ff68
3 changed files with 129 additions and 24 deletions
@@ -437,7 +437,7 @@ exports[`mangled files should have up to date snapshots ./chromium_src/chrome/br
</div>
- <profile-card-menu .profileState=\\"\${this.profileState}\\"
- ?hidden=\\"\${isGlicVersion()}\\">
+ <profile-card-menu .profilestate=\\"\${this.profileState}\\" ?hidden=\\"\${isGlicVersion()}\\">
+ <profile-card-menu .profileState=\\"\${this.profileState}\\" ?hidden=\\"\${isGlicVersion()}\\">
</profile-card-menu>
</div>
-<cr-tooltip id=\\"gaiaNameTooltip\\" for=\\"gaiaName\\" manual-mode offset=\\"0\\"
@@ -459,22 +459,19 @@ exports[`mangled files should have up to date snapshots ./chromium_src/chrome/br
"===================================================================
--- ../chrome/browser/resources/signin/profile_picker/profile_picker_main_view.html.ts
+++ gen/chrome/browser/resources/signin/profile_picker/preprocessed/profile_picker_main_view.html.ts
@@ -14,49 +14,35 @@
@@ -14,10 +14,9 @@
}
}()}
<div class=\\"flex-container\\">
<div class=\\"title-container\\">
- <img id=\\"picker-logo\\" @click=\\"\${this.onProductLogoClick_}\\"
- src=\\"picker_logo.svg\\" role=\\"presentation\\">
- <h1 class=\\"title\\" .innerHTML=\\"\${this.getTitle_()}\\"></h1>
- <div class=\\"subtitle\\" .innerHTML=\\"\${this.getSubtitle_()}\\"></div>
+ <img id=\\"picker-logo\\" @click=\\"\${this.onProductLogoClick_}\\" src=\\"picker_logo.svg\\" role=\\"presentation\\">
+ <h1 class=\\"title\\" .innerhtml=\\"\${this.getTitle_()}\\"></h1>
+ <div class=\\"subtitle\\" .innerhtml=\\"\${this.getSubtitle_()}\\"></div>
<h1 class=\\"title\\" .innerHTML=\\"\${this.getTitle_()}\\"></h1>
<div class=\\"subtitle\\" .innerHTML=\\"\${this.getSubtitle_()}\\"></div>
</div>
<div id=\\"profilesWrapper\\" ?hidden=\\"\${(this.shouldHideProfilesWrapper_())}\\">
<div id=\\"profilesContainer\\" class=\\"custom-scrollbar\\">
\${this.profilesList_.map((item, index) => html\`
@@ -26,37 +25,24 @@
<profile-card class=\\"profile-item\\" .profileState=\\"\${item}\\"
data-index=\\"\${index}\\">
</profile-card>
@@ -6,6 +6,21 @@
import { utilsForTest, HTMLTemplateTags } from './lit_mangler'
import ts from 'typescript'
const loadTextForTesting = (text: string) => {
const sourceFile = ts.createSourceFile('test.ts', text, ts.ScriptTarget.Latest)
const template: HTMLTemplateTags = { id: 0, text, children: [] }
utilsForTest.resetTemplateId()
utilsForTest.cachePropertyCasesFromText(text)
utilsForTest.setResult(template)
utilsForTest.getTemplateLiterals(sourceFile, sourceFile, template)
utilsForTest.injectPlaceholders(template)
return template
}
const getOutputForTesting = (template: HTMLTemplateTags) => {
return utilsForTest.restorePropertyCases(utilsForTest.replacePlaceholders(template))
}
describe('Attribute handling', () => {
it('should not break when attributes are unquoted', () => {
const template: HTMLTemplateTags = { text: '<div ?hidden=${this.foo}>Hello</div>', children: [], id: 0 }
@@ -131,7 +146,7 @@ describe('Attribute handling', () => {
const template: HTMLTemplateTags = { text: `<meta name="viewport" content="width=device-width, initial-scale=\${this.scale}">`, children: [], id: 0 }
utilsForTest.setResult(template)
utilsForTest.mangle(() => {}, template)
utilsForTest.mangle(() => { }, template)
expect(template.text).toBe(`<meta name="viewport" content="width=device-width, initial-scale=\${this.scale}">`)
})
@@ -148,6 +163,64 @@ describe('Attribute handling', () => {
})
});
describe('Property handling', () => {
it('property names should roundtrip through the mangler', () => {
const original = `<div .fooBar="\${this.foo}" .baz="1" .hElLo="2">Hello</div>`
const template = loadTextForTesting(original)
utilsForTest.mangleAll(e => {
// don't do anything, we're just checking the output is sane
}, () => true)
expect(getOutputForTesting(template)).toBe(`<div .fooBar="\${this.foo}" .baz="1" .hElLo="2">Hello</div>`)
})
it('should still lowercase attributes', () => {
const original = `<div .fooBar="\${this.foo}" .baz="1" .hElLo="2" BAZ="7" ?hiDDen="true">Hello</div>`
const template = loadTextForTesting(original)
utilsForTest.mangleAll(e => {
// don't do anything, we're just checking the output is sane
}, () => true)
expect(getOutputForTesting(template)).toBe(original)
})
it('should handle camelCase properties in children', () => {
const original = `<div .fooBar="\${this.foo}" .baz="1" .hElLo="2">
<span .propertyName="Hello">World</span>
</div>`
const template = loadTextForTesting(original)
utilsForTest.mangle(e => {
// don't do anything, we're just checking the output is sane
}, template)
expect(getOutputForTesting(template)).toBe(original)
})
it('should handle camelCase properties in nested templates', () => {
const original = `<div .fooBar="\${this.foo}" .baz="1" .hElLo="2">
\${items.map(item => html\`<span .propertyName="\${item.value}">World</span>\`)}
</div>`
const template = loadTextForTesting(original)
// Run a mangler over all templates
utilsForTest.mangleAll(e => {
// don't do anything, we're just checking the output is sane
}, () => true)
expect(getOutputForTesting(template)).toBe(original)
})
it('not running any manglers should not change the output', () => {
const original = `<div .fooBar="\${this.foo}" .baz="1" .hElLo="2">
\${items.map(item => html\`<span .propertyName="\${item.value}">World</span>\`)}
</div>`
const template = loadTextForTesting(original)
expect(getOutputForTesting(template)).toBe(original)
})
})
describe('Escaping', () => {
it('should escape quotes', () => {
const template: HTMLTemplateTags = { text: `<div attr=\${"foo"}>"\${this.foo}"</div>`, children: [], id: 0 }
@@ -163,7 +236,7 @@ describe('Escaping', () => {
utilsForTest.mangle(t => {
expect(t.querySelector('script')).toBeNull()
}, template)
}, template)
expect(template.text).toBe(`<div>\${"<script>alert('pwnd')</script>"}</div>`)
})
@@ -173,56 +246,56 @@ describe('Escaping', () => {
utilsForTest.mangle(t => {
expect(t.querySelector('h1')).toBeNull()
}, template)
}, template)
expect(template.text).toBe(`<div>\${"<h1>Jay</h1>"}</div>`)
})
it('should be fine with escaped HTML entities', () => {
const template: HTMLTemplateTags = { text: `<div>&lt;script&gt;alert('hahaha')&lt;script&gt;</div>`, children: [], id: 0 }
utilsForTest.setResult(template)
utilsForTest.mangle(t => {}, template)
utilsForTest.mangle(t => { }, template)
expect(template.text).toBe(`<div>&lt;script&gt;alert('hahaha')&lt;script&gt;</div>`)
})
it('should be fine with escaped HTML entities in interpolated strings', () => {
const template: HTMLTemplateTags = { text: `<div>\${"&lt;script&gt;alert('hahaha')&lt;script&gt;"}</div>`, children: [], id: 0 }
utilsForTest.setResult(template)
utilsForTest.mangle(t => {}, template)
utilsForTest.mangle(t => { }, template)
expect(template.text).toBe(`<div>\${"&lt;script&gt;alert('hahaha')&lt;script&gt;"}</div>`)
})
it('should be fine with escaped HTML entities in interpolated strings', () => {
const template: HTMLTemplateTags = { text: `<div>\${"&lt;script&gt;alert('hahaha')&lt;script&gt;"}</div>`, children: [], id: 0 }
utilsForTest.setResult(template)
utilsForTest.mangle(t => {}, template)
utilsForTest.mangle(t => { }, template)
expect(template.text).toBe(`<div>\${"&lt;script&gt;alert('hahaha')&lt;script&gt;"}</div>`)
})
it('should be fine with unescaped HTML entities in interpolated strings', () => {
const template: HTMLTemplateTags = { text: `<div>\${"<foo&"}</div>`, children: [], id: 0 }
utilsForTest.setResult(template)
utilsForTest.mangle(t => {}, template)
utilsForTest.mangle(t => { }, template)
expect(template.text).toBe(`<div>\${"<foo&"}</div>`)
})
it('should be fine with unescaped tags in interpolated strings', () => {
const template: HTMLTemplateTags = { text: `<div>\${"<script>alert('pwnd')</script>"}</div>`, children: [], id: 0 }
utilsForTest.setResult(template)
utilsForTest.mangle(t => {}, template)
utilsForTest.mangle(t => { }, template)
expect(template.text).toBe(`<div>\${"<script>alert('pwnd')</script>"}</div>`)
})
it('should be fine with unescaped tags and entities in interpolated strings', () => {
const template: HTMLTemplateTags = { text: `<div>\${"$lt;<script$gt;>alert('pwnd')&lt;</script&gt;>"}</div>`, children: [], id: 0 }
utilsForTest.setResult(template)
utilsForTest.mangle(t => {}, template)
utilsForTest.mangle(t => { }, template)
expect(template.text).toBe(`<div>\${"$lt;<script$gt;>alert('pwnd')&lt;</script&gt;>"}</div>`)
})
it('should be fine with quotes in attributes', () => {
const template: HTMLTemplateTags = { text: `<img src=\${"foo onload='javascript:alert(\`pwnd\`)'"}>`, children: [], id: 0 }
utilsForTest.setResult(template)
utilsForTest.mangle(t => {}, template)
utilsForTest.mangle(t => { }, template)
expect(template.text).toBe(`<img src="\${"foo onload='javascript:alert(\`pwnd\`)'"}">`)
})
})
@@ -407,10 +480,12 @@ export function getList(this: string[]) {
})
it('should be possible to mangle all matching templates', () => {
const result: HTMLTemplateTags = { id: 0, text: exampleHtml, children: [
{ id: 1, text: '<div>Hello</div>', children: [] },
{ id: 2, text: '<div>Hello</div>', children: [] },
] }
const result: HTMLTemplateTags = {
id: 0, text: exampleHtml, children: [
{ id: 1, text: '<div>Hello</div>', children: [] },
{ id: 2, text: '<div>Hello</div>', children: [] },
]
}
utilsForTest.resetTemplateId()
utilsForTest.getTemplateLiterals(sourceFile, sourceFile, result)
utilsForTest.injectPlaceholders(result)
+35 -2
View File
@@ -24,10 +24,17 @@ export interface HTMLTemplateTags {
let nextId = 1
let isHTML = false
// Properties are camelCase but will be converted to lowercase by the HTML
// parser. We need to keep track of the proper casing so we don't break setting
// properties.
const propertyRegex = /\s\.(\w+)/gm
const propertyCases: Record<string, string> = {}
const loadRaw = (filepath: string) => {
isHTML = filepath.endsWith('.html')
const text = readFileSync(filepath, 'utf-8')
cachePropertyCasesFromText(text)
const tsFile = ts.createSourceFile(path.basename(filepath), text, ts.ScriptTarget.Latest)
return tsFile
@@ -61,6 +68,30 @@ const getTemplateLiterals = (source: ts.SourceFile, node: ts.Node, templateTags:
})
}
/**
* Stores the property cases from the source text in |propertyCases|. They need
* to be restored when we save the file.
* @param text The source text of the file
*/
const cachePropertyCasesFromText = (text: string) => {
const matches = text.matchAll(propertyRegex)
for (const match of matches) {
propertyCases[match[0].toLocaleLowerCase()] = match[0]
}
}
/**
* Restores the property cases from |propertyCases| in the source text.
* @param text The source text of the file
* @returns The source text with the property cases restored
*/
const restorePropertyCases = (text: string) => {
for (const [key, value] of Object.entries(propertyCases)) {
text = text.replaceAll(key, value)
}
return text
}
/**
* Loads the HTML template literals from a file.
*
@@ -186,7 +217,7 @@ export const write = (file: string) => {
throw new Error('This should only be called after load')
}
const text = replacePlaceholders(result)
const text = restorePropertyCases(replacePlaceholders(result))
writeFileSync(file, text)
}
@@ -299,5 +330,7 @@ export const utilsForTest = {
},
getResult: () => result,
resetTemplateId: () => nextId = 1,
load
load,
cachePropertyCasesFromText,
restorePropertyCases
}