From a784ede63d05fe691e1cc1cd5f67a242d0d55824 Mon Sep 17 00:00:00 2001 From: natete Date: Thu, 20 Oct 2016 12:06:35 +0200 Subject: [PATCH] fix(theming): fix CSS with nested rules parsing when registering a theme - replace the regex used to split the CSS string with a function that takes into account nested rules - remove unused `ruleMatchRegex` Fixes #9869 --- src/core/services/theming/theming.js | 55 +++++++++++++-- src/core/services/theming/theming.spec.js | 86 +++++++++++++++++++++++ 2 files changed, 136 insertions(+), 5 deletions(-) diff --git a/src/core/services/theming/theming.js b/src/core/services/theming/theming.js index 8349f9dc243..ae6354528d3 100644 --- a/src/core/services/theming/theming.js +++ b/src/core/services/theming/theming.js @@ -1138,14 +1138,13 @@ function generateAllThemes($injector, $mdTheming) { // Expose contrast colors for palettes to ensure that text is always readable angular.forEach(PALETTES, sanitizePalette); - // MD_THEME_CSS is a string generated by the build process that includes all the themable + // MD_THEME_CSS is a string generated by the build process that includes all the themeable // components as templates // Break the CSS into individual rules - var rules = themeCss - .split(/}(?!([}'";]))/) - .filter(function(rule) { return rule && rule.trim().length; }) - .map(function(rule) { return rule.trim() + '}'; }); + var rules = splitCss(themeCss).map(function(rule) { + return rule.trim(); + }); THEME_COLOR_TYPES.forEach(function(type) { rulesByType[type] = ''; @@ -1277,6 +1276,52 @@ function generateAllThemes($injector, $mdTheming) { }; }); } + + /** + * @param {string} themeCss + * @returns {[]} a string representing a CSS file that is split, producing an array with a rule + * at each index. + */ + function splitCss(themeCss) { + var result = []; + var currentRule = ''; + var openedCurlyBrackets = 0; + var closedCurlyBrackets = 0; + + for (var i = 0; i < themeCss.length; i++) { + var character = themeCss.charAt(i); + + // Check for content in quotes + if (character === '\'' || character === '"') { + // Append text in quotes to current rule + var textInQuotes = themeCss.substring(i, themeCss.indexOf(character, i + 1)); + currentRule += textInQuotes; + + // Jump to the closing quote char + i += textInQuotes.length; + } else { + currentRule += character; + + if (character === '}') { + closedCurlyBrackets++; + if (closedCurlyBrackets === openedCurlyBrackets) { + closedCurlyBrackets = 0; + openedCurlyBrackets = 0; + result.push(currentRule); + currentRule = ''; + } + } else if (character === '{') { + openedCurlyBrackets++; + } + } + } + // Add comments added after last valid rule. + if (currentRule !== '') { + result.push(currentRule); + } + + return result; + } } function generateTheme(theme, name, nonce) { diff --git a/src/core/services/theming/theming.spec.js b/src/core/services/theming/theming.spec.js index 807e71a6221..aa689bda59d 100644 --- a/src/core/services/theming/theming.spec.js +++ b/src/core/services/theming/theming.spec.js @@ -1055,3 +1055,89 @@ describe('md-themable directive', function() { expect(el.hasClass('md-default-theme')).toBe(true); })); }); + +describe('$mdThemeProvider with custom styles that include nested rules', function() { + it('appends the custom styles taking into account nesting', function() { + module('material.core', function($mdThemingProvider) { + $mdThemingProvider.generateThemesOnDemand(false); + var styles = + '@media (min-width: 0) and (max-width: 700px) {' + + ' .md-THEME_NAME-theme .layout-row {' + + ' background-color: "{{primary-500}}";' + + ' }' + + ' .md-THEME_NAME-theme .layout-column {' + + ' color: blue;' + + ' font-weight: bold;' + + ' }' + + '}'; + + $mdThemingProvider.registerStyles(styles); + $mdThemingProvider.theme('register-custom-nested-styles'); + }); + + inject(function($MD_THEME_CSS) { + // Verify that $MD_THEME_CSS is still set to '/**/' in the test environment. + // Check angular-material-mocks.js for $MD_THEME_CSS latest value if this test starts to fail. + expect($MD_THEME_CSS).toBe('/**/'); + }); + + var compiledStyles = + '@media (min-width: 0) and (max-width: 700px) {' + + ' .md-register-custom-nested-styles-theme .layout-row {' + + ' background-color: rgb(63,81,181);' + + ' }' + + ' .md-register-custom-nested-styles-theme .layout-column {' + + ' color: blue;' + + ' font-weight: bold;' + + ' }' + + '}'; + + // Find the string containing nested rules in the head tag. + expect(document.head.innerHTML).toContain(compiledStyles); + }); +}); + +describe('$mdThemeProvider with custom styles that include multiple nested rules', function() { + it('appends the custom styles taking into account multiple nesting', function() { + module('material.core', function($mdThemingProvider) { + $mdThemingProvider.generateThemesOnDemand(false); + var styles = + '@supports (display: bar) {' + + ' @media (min-width: 0) and (max-width: 700px) {' + + ' .md-THEME_NAME-theme .layout-row {' + + ' background-color: "{{primary-500}}";' + + ' }' + + ' .md-THEME_NAME-theme .layout-column {' + + ' color: blue;' + + ' font-weight: bold;' + + ' }' + + ' }' + + '}'; + + $mdThemingProvider.registerStyles(styles); + $mdThemingProvider.theme('register-custom-multiple-nested-styles'); + }); + + inject(function($MD_THEME_CSS) { + // Verify that $MD_THEME_CSS is still set to '/**/' in the test environment. + // Check angular-material-mocks.js for $MD_THEME_CSS latest value if this test starts to fail. + expect($MD_THEME_CSS).toBe('/**/'); + }); + + var compiledStyles = + '@supports (display: bar) {' + + ' @media (min-width: 0) and (max-width: 700px) {' + + ' .md-register-custom-multiple-nested-styles-theme .layout-row {' + + ' background-color: rgb(63,81,181);' + + ' }' + + ' .md-register-custom-multiple-nested-styles-theme .layout-column {' + + ' color: blue;' + + ' font-weight: bold;' + + ' }' + + ' }' + + '}'; + + // Find the string containing nested rules in the head tag. + expect(document.head.innerHTML).toContain(compiledStyles); + }); +});