diff --git a/dev/data/structured-content-overrides.css b/dev/data/structured-content-overrides.css new file mode 100644 index 00000000..31873760 --- /dev/null +++ b/dev/data/structured-content-overrides.css @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2021 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the entrys of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +.gloss-image-container { + font-size: 1px; +} +.gloss-image-link[data-background=true]>.gloss-image-container { + /* remove-property background-color */ +} +.gloss-image-container-overlay { + font-size: initial; + line-height: initial; + color: initial; +} +.gloss-image-background { + background-color: currentColor; +} +:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, +:root[data-browser=firefox] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background, +:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image, +:root[data-browser=firefox-mobile] .gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background { + /* remove-rule */ +} +.gloss-image-link-text { + line-height: initial; +} +.gloss-sc-thead, +.gloss-sc-tfoot, +.gloss-sc-th { + /* remove-property background-color */ +} +.gloss-sc-th, +.gloss-sc-td { + border-width: 1px; + border-color: currentColor; +} diff --git a/dev/generate-structured-content-style.js b/dev/generate-structured-content-style.js new file mode 100644 index 00000000..8335efc2 --- /dev/null +++ b/dev/generate-structured-content-style.js @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2021 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const fs = require('fs'); +const path = require('path'); +const css = require('css'); +const {testMain} = require('./util'); + +function indexOfRule(rules, selectors) { + const jj = selectors.length; + for (let i = 0, ii = rules.length; i < ii; ++i) { + const ruleSelectors = rules[i].selectors; + if (ruleSelectors.length !== jj) { continue; } + let okay = true; + for (let j = 0; j < jj; ++j) { + if (selectors[j] !== ruleSelectors[j]) { + okay = false; + break; + } + } + if (okay) { return i; } + } + return -1; +} + +function removeProperty(styles, property, removedProperties) { + let removeCount = removedProperties.get(property); + if (typeof removeCount !== 'undefined') { return removeCount; } + removeCount = 0; + for (let i = 0, ii = styles.length; i < ii; ++i) { + const key = styles[i][0]; + if (key !== property) { continue; } + styles.splice(i, 1); + --i; + --ii; + ++removeCount; + } + removedProperties.set(property, removeCount); + return removeCount; +} + +function formatRulesJson(rules) { + // Manually format JSON, for improved compactness + // return JSON.stringify(rules, null, 4); + const indent1 = ' '; + const indent2 = indent1.repeat(2); + const indent3 = indent1.repeat(3); + let result = ''; + result += '['; + let index1 = 0; + for (const {selectors, styles} of rules) { + if (index1 > 0) { result += ','; } + result += `\n${indent1}{\n${indent2}"selectors": `; + if (selectors.length === 1) { + result += `[${JSON.stringify(selectors[0], null, 4)}]`; + } else { + result += JSON.stringify(selectors, null, 4).replace(/\n/g, '\n' + indent2); + } + result += `,\n${indent2}"styles": [`; + let index2 = 0; + for (const [key, value] of styles) { + if (index2 > 0) { result += ','; } + result += `\n${indent3}[${JSON.stringify(key)}, ${JSON.stringify(value)}]`; + ++index2; + } + if (index2 > 0) { result += `\n${indent2}`; } + result += `]\n${indent1}}`; + ++index1; + } + if (index1 > 0) { result += '\n'; } + result += ']'; + return result; +} + +function generateRules() { + const content1 = fs.readFileSync(path.join(__dirname, '..', 'ext/css/structured-content.css'), {encoding: 'utf8'}); + const content2 = fs.readFileSync(path.join(__dirname, 'data/structured-content-overrides.css'), {encoding: 'utf8'}); + const stylesheet1 = css.parse(content1, {}).stylesheet; + const stylesheet2 = css.parse(content2, {}).stylesheet; + + const removePropertyPattern = /^remove-property\s+([a-zA-Z0-9-]+)$/; + const removeRulePattern = /^remove-rule$/; + + const rules = []; + + // Default stylesheet + for (const rule of stylesheet1.rules) { + if (rule.type !== 'rule') { continue; } + const {selectors, declarations} = rule; + const styles = []; + for (const declaration of declarations) { + if (declaration.type !== 'declaration') { console.log(declaration); continue; } + const {property, value} = declaration; + styles.push([property, value]); + } + if (styles.length > 0) { + rules.push({selectors, styles}); + } + } + + // Overrides + for (const rule of stylesheet2.rules) { + if (rule.type !== 'rule') { continue; } + const {selectors, declarations} = rule; + const removedProperties = new Map(); + for (const declaration of declarations) { + switch (declaration.type) { + case 'declaration': + { + const index = indexOfRule(rules, selectors); + let entry; + if (index >= 0) { + entry = rules[index]; + } else { + entry = {selectors, styles: []}; + rules.push(entry); + } + const {property, value} = declaration; + removeProperty(entry.styles, property, removedProperties); + entry.styles.push([property, value]); + } + break; + case 'comment': + { + const index = indexOfRule(rules, selectors); + if (index < 0) { throw new Error('Could not find rule with matching selectors'); } + const comment = declaration.comment.trim(); + let m; + if ((m = removePropertyPattern.exec(comment)) !== null) { + const property = m[1]; + const removeCount = removeProperty(rules[index].styles, property, removedProperties); + if (removeCount === 0) { throw new Error(`Property removal is unnecessary; ${property} does not exist`); } + } else if ((m = removeRulePattern.exec(comment)) !== null) { + rules.splice(index, 1); + } + } + break; + } + } + } + + // Remove empty + for (let i = 0, ii = rules.length; i < ii; ++i) { + if (rules[i].styles.length > 0) { continue; } + rules.splice(i, 1); + --i; + --ii; + } + + return rules; +} + +function generateRulesJson() { + return formatRulesJson(generateRules()); +} + +function getOutputPath() { + return path.join(__dirname, '..', 'ext/data/structured-content-style.json'); +} + +function main() { + const outputFileName = getOutputPath(); + const json = generateRulesJson(); + fs.writeFileSync(outputFileName, json, {encoding: 'utf8'}); +} + + +if (require.main === module) { + testMain(main, process.argv.slice(2)); +} + + +module.exports = { + generateRules, + generateRulesJson, + getOutputPath +}; diff --git a/ext/css/structured-content.css b/ext/css/structured-content.css index cff2c83c..ea0bf5cd 100644 --- a/ext/css/structured-content.css +++ b/ext/css/structured-content.css @@ -201,9 +201,6 @@ table-layout: auto; border-collapse: collapse; } -.gloss-sc-tbody { - background-color: transparent; -} .gloss-sc-thead, .gloss-sc-tfoot, .gloss-sc-th { diff --git a/ext/data/structured-content-style.json b/ext/data/structured-content-style.json new file mode 100644 index 00000000..80a71e1c --- /dev/null +++ b/ext/data/structured-content-style.json @@ -0,0 +1,330 @@ +[ + { + "selectors": [".gloss-image-container"], + "styles": [ + ["display", "inline-block"], + ["white-space", "nowrap"], + ["max-width", "100%"], + ["position", "relative"], + ["vertical-align", "top"], + ["line-height", "0"], + ["overflow", "hidden"], + ["font-size", "1px"] + ] + }, + { + "selectors": [".gloss-image-link"], + "styles": [ + ["cursor", "inherit"], + ["color", "inherit"], + ["display", "inline-block"], + ["position", "relative"], + ["line-height", "1"] + ] + }, + { + "selectors": [".gloss-image-link[href]:hover"], + "styles": [ + ["cursor", "pointer"] + ] + }, + { + "selectors": [".gloss-image-container-overlay"], + "styles": [ + ["position", "absolute"], + ["left", "0"], + ["top", "0"], + ["width", "100%"], + ["height", "100%"], + ["display", "table"], + ["table-layout", "fixed"], + ["white-space", "normal"], + ["font-size", "initial"], + ["line-height", "initial"], + ["color", "initial"] + ] + }, + { + "selectors": [".gloss-image-link[data-has-image=true][data-image-load-state=load-error] .gloss-image-container-overlay::after"], + "styles": [ + ["content", "'Image failed to load'"], + ["display", "table-cell"], + ["width", "100%"], + ["height", "100%"], + ["vertical-align", "middle"], + ["text-align", "center"], + ["padding", "0.25em"] + ] + }, + { + "selectors": [".gloss-image-background"], + "styles": [ + ["--image", "none"], + ["position", "absolute"], + ["left", "0"], + ["top", "0"], + ["width", "100%"], + ["height", "100%"], + ["-webkit-mask-repeat", "no-repeat"], + ["-webkit-mask-position", "center center"], + ["-webkit-mask-mode", "alpha"], + ["-webkit-mask-size", "contain"], + ["-webkit-mask-image", "var(--image)"], + ["mask-repeat", "no-repeat"], + ["mask-position", "center center"], + ["mask-mode", "alpha"], + ["mask-size", "contain"], + ["mask-image", "var(--image)"], + ["background-color", "currentColor"] + ] + }, + { + "selectors": [".gloss-image"], + "styles": [ + ["display", "inline-block"], + ["vertical-align", "top"], + ["object-fit", "contain"], + ["border", "none"], + ["outline", "none"] + ] + }, + { + "selectors": [".gloss-image-link[data-has-aspect-ratio=true] .gloss-image"], + "styles": [ + ["position", "absolute"], + ["left", "0"], + ["top", "0"], + ["width", "100%"], + ["height", "100%"] + ] + }, + { + "selectors": [".gloss-image:not([src])"], + "styles": [ + ["display", "none"] + ] + }, + { + "selectors": [ + ".gloss-image-link[data-image-rendering=pixelated] .gloss-image", + ".gloss-image-link[data-image-rendering=pixelated] .gloss-image-background" + ], + "styles": [ + ["image-rendering", "auto"], + ["image-rendering", "-moz-crisp-edges"], + ["image-rendering", "-webkit-optimize-contrast"], + ["image-rendering", "pixelated"], + ["image-rendering", "crisp-edges"] + ] + }, + { + "selectors": [ + ".gloss-image-link[data-image-rendering=crisp-edges] .gloss-image", + ".gloss-image-link[data-image-rendering=crisp-edges] .gloss-image-background" + ], + "styles": [ + ["image-rendering", "auto"], + ["image-rendering", "-moz-crisp-edges"], + ["image-rendering", "-webkit-optimize-contrast"], + ["image-rendering", "crisp-edges"] + ] + }, + { + "selectors": [".gloss-image-link[data-has-aspect-ratio=true] .gloss-image-aspect-ratio-sizer"], + "styles": [ + ["display", "inline-block"], + ["width", "0"], + ["vertical-align", "top"], + ["font-size", "0"] + ] + }, + { + "selectors": [".gloss-image-link-text"], + "styles": [ + ["display", "none"], + ["line-height", "initial"] + ] + }, + { + "selectors": [".gloss-image-link-text::before"], + "styles": [ + ["content", "'['"] + ] + }, + { + "selectors": [".gloss-image-link-text::after"], + "styles": [ + ["content", "']'"] + ] + }, + { + "selectors": [".gloss-image-description"], + "styles": [ + ["display", "block"], + ["white-space", "pre-line"] + ] + }, + { + "selectors": [".gloss-image-link[data-appearance=monochrome] .gloss-image"], + "styles": [ + ["visibility", "hidden"] + ] + }, + { + "selectors": [".gloss-image-link:not([data-appearance=monochrome]) .gloss-image-background"], + "styles": [ + ["display", "none"] + ] + }, + { + "selectors": [".gloss-image-link[data-size-units=em] .gloss-image-container"], + "styles": [ + ["font-size", "1em"] + ] + }, + { + "selectors": [".gloss-image-link[data-vertical-align=baseline]"], + "styles": [ + ["vertical-align", "baseline"] + ] + }, + { + "selectors": [".gloss-image-link[data-vertical-align=sub]"], + "styles": [ + ["vertical-align", "sub"] + ] + }, + { + "selectors": [".gloss-image-link[data-vertical-align=super]"], + "styles": [ + ["vertical-align", "super"] + ] + }, + { + "selectors": [".gloss-image-link[data-vertical-align=text-top]"], + "styles": [ + ["vertical-align", "top"] + ] + }, + { + "selectors": [".gloss-image-link[data-vertical-align=text-bottom]"], + "styles": [ + ["vertical-align", "bottom"] + ] + }, + { + "selectors": [".gloss-image-link[data-vertical-align=middle]"], + "styles": [ + ["vertical-align", "middle"] + ] + }, + { + "selectors": [".gloss-image-link[data-vertical-align=top]"], + "styles": [ + ["vertical-align", "top"] + ] + }, + { + "selectors": [".gloss-image-link[data-vertical-align=bottom]"], + "styles": [ + ["vertical-align", "bottom"] + ] + }, + { + "selectors": [ + ".gloss-image-link[data-collapsed=true]", + ":root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true]" + ], + "styles": [ + ["vertical-align", "baseline"] + ] + }, + { + "selectors": [ + ".gloss-image-link[data-collapsed=true] .gloss-image-container", + ":root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true] .gloss-image-container" + ], + "styles": [ + ["display", "none"], + ["position", "absolute"], + ["left", "0"], + ["top", "100%"], + ["z-index", "1"] + ] + }, + { + "selectors": [ + ".entry:nth-last-of-type(1):not(:nth-of-type(1)) .gloss-image-link[data-collapsed=true] .gloss-image-container", + ":root[data-glossary-layout-mode=compact] .entry:nth-last-of-type(1):not(:nth-of-type(1)) .gloss-image-link[data-collapsible=true] .gloss-image-container" + ], + "styles": [ + ["bottom", "100%"], + ["top", "auto"] + ] + }, + { + "selectors": [ + ".gloss-image-link[data-collapsed=true]:hover .gloss-image-container", + ".gloss-image-link[data-collapsed=true]:focus .gloss-image-container", + ":root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true]:hover .gloss-image-container", + ":root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true]:focus .gloss-image-container" + ], + "styles": [ + ["display", "block"] + ] + }, + { + "selectors": [ + ".gloss-image-link[data-collapsed=true] .gloss-image-link-text", + ":root[data-glossary-layout-mode=compact] .gloss-image-link[data-collapsible=true] .gloss-image-link-text" + ], + "styles": [ + ["display", "inline"] + ] + }, + { + "selectors": [ + ".gloss-image-link[data-collapsed=true]~.gloss-image-description", + ":root[data-glossary-layout-mode=compact] .gloss-image-description" + ], + "styles": [ + ["display", "inline"] + ] + }, + { + "selectors": [".gloss-sc-table-container"], + "styles": [ + ["display", "block"] + ] + }, + { + "selectors": [".gloss-sc-table"], + "styles": [ + ["table-layout", "auto"], + ["border-collapse", "collapse"] + ] + }, + { + "selectors": [ + ".gloss-sc-thead", + ".gloss-sc-tfoot", + ".gloss-sc-th" + ], + "styles": [ + ["font-weight", "bold"] + ] + }, + { + "selectors": [ + ".gloss-sc-th", + ".gloss-sc-td" + ], + "styles": [ + ["border-style", "solid"], + ["padding", "0.25em"], + ["vertical-align", "top"], + ["border-width", "1px"], + ["border-color", "currentColor"] + ] + } +] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bb1336e3..956b433e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1023,6 +1023,12 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, "atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -2067,6 +2073,17 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, "css-select": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", @@ -2231,6 +2248,12 @@ "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", "dev": true }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, "decompress-response": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", @@ -6606,6 +6629,16 @@ "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", "dev": true }, + "source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, "source-map-support": { "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", diff --git a/package.json b/package.json index ddc2648a..872f7dbb 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ }, "devDependencies": { "browserify": "^17.0.0", + "css": "^3.0.0", "eslint": "^7.29.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-no-unsanitized": "^3.1.5", diff --git a/test/test-structured-content-style.js b/test/test-structured-content-style.js new file mode 100644 index 00000000..ed8c24e9 --- /dev/null +++ b/test/test-structured-content-style.js @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +const fs = require('fs'); +const assert = require('assert'); +const {testMain} = require('../dev/util'); +const {generateRulesJson, getOutputPath} = require('../dev/generate-structured-content-style'); + + +function main() { + const outputPath = getOutputPath(); + const actual = fs.readFileSync(outputPath, {encoding: 'utf8'}); + const expected = generateRulesJson(); + assert.deepStrictEqual(actual, expected); +} + + +if (require.main === module) { testMain(main); }