Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

fix(theming): fix CSS with nested rules parsing when registering a theme #12049

Merged
merged 1 commit into from
Dec 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 50 additions & 5 deletions src/core/services/theming/theming.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] = '';
Expand Down Expand Up @@ -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) {
Expand Down
86 changes: 86 additions & 0 deletions src/core/services/theming/theming.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});