'use strict'; /* exported calcStyleDigest MozDocMapper styleCodeEmpty styleJSONseemsValid styleSectionGlobal styleSectionsEqual */ const MozDocMapper = { TO_CSS: { urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp', }, FROM_CSS: { 'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps', }, /** * @param {Object} section * @param {function(func:string, value:string)} fn */ forEachProp(section, fn) { for (const [propName, func] of Object.entries(MozDocMapper.TO_CSS)) { const props = section[propName]; if (props) props.forEach(value => fn(func, value)); } }, /** * @param {Array} funcItems * @param {?Object} [section] * @returns {Object} section */ toSection(funcItems, section = {}) { for (const item of funcItems) { const [func, value] = item || []; const propName = MozDocMapper.FROM_CSS[func]; if (propName) { const props = section[propName] || (section[propName] = []); if (Array.isArray(value)) props.push(...value); else props.push(value); } } return section; }, /** * @param {StyleObj} style * @returns {string} */ styleToCss(style) { const res = []; for (const section of style.sections) { const funcs = []; MozDocMapper.forEachProp(section, (type, value) => funcs.push(`${type}("${value.replace(/[\\"]/g, '\\$&')}")`)); res.push(funcs.length ? `@-moz-document ${funcs.join(', ')} {\n${section.code}\n}` : section.code); } return res.join('\n\n'); }, }; function styleCodeEmpty(code) { if (!code) { return true; } let lastIndex = 0; const rx = /\s+|\/\*([^*]+|\*(?!\/))*(\*\/|$)|@namespace[^;]+;|@charset[^;]+;/giyu; while (rx.exec(code)) { lastIndex = rx.lastIndex; if (lastIndex === code.length) { return true; } } styleCodeEmpty.lastIndex = lastIndex; return false; } /** Checks if section is global i.e. has no targets at all */ function styleSectionGlobal(section) { return (!section.regexps || !section.regexps.length) && (!section.urlPrefixes || !section.urlPrefixes.length) && (!section.urls || !section.urls.length) && (!section.domains || !section.domains.length); } /** * The sections are checked in successive order because it matters when many sections * match the same URL and they have rules with the same CSS specificity * @param {Object} a - first style object * @param {Object} b - second style object * @returns {?boolean} */ function styleSectionsEqual({sections: a}, {sections: b}) { const targets = ['urls', 'urlPrefixes', 'domains', 'regexps']; return a && b && a.length === b.length && a.every(sameSection); function sameSection(secA, i) { return equalOrEmpty(secA.code, b[i].code, 'string', (a, b) => a === b) && targets.every(target => equalOrEmpty(secA[target], b[i][target], 'array', arrayMirrors)); } function equalOrEmpty(a, b, type, comparator) { const typeA = type === 'array' ? Array.isArray(a) : typeof a === type; const typeB = type === 'array' ? Array.isArray(b) : typeof b === type; return typeA && typeB && comparator(a, b) || (a == null || typeA && !a.length) && (b == null || typeB && !b.length); } function arrayMirrors(a, b) { return a.length === b.length && a.every(el => b.includes(el)) && b.every(el => a.includes(el)); } } async function calcStyleDigest(style) { // retain known properties in an arbitrarily predefined order const src = style.usercssData ? style.sourceCode // retain known properties in an arbitrarily predefined order : JSON.stringify((style.sections || []).map(section => /** @namespace StyleSection */({ code: section.code || '', urls: section.urls || [], urlPrefixes: section.urlPrefixes || [], domains: section.domains || [], regexps: section.regexps || [], }))); const srcBytes = new TextEncoder().encode(src); const res = await crypto.subtle.digest('SHA-1', srcBytes); return Array.from(new Uint8Array(res), b => (0x100 + b).toString(16).slice(1)).join(''); } function styleJSONseemsValid(json) { return json && typeof json.name == 'string' && json.name.trim() && Array.isArray(json.sections) && typeof (json.sections[0] || {}).code === 'string'; }