From 002da9fba8800b796ed98dd83e2ab68b4d37a1cc Mon Sep 17 00:00:00 2001
From: toasted-nutbread <toasted-nutbread@users.noreply.github.com>
Date: Sun, 27 Jun 2021 17:57:00 -0400
Subject: Structured content style json (#1771)

* Install css

* Remove unnecessary rule

* Create CSS overrides file

* Create script to generate CSS JSON file

* Generate JSON

* Add test
---
 dev/data/structured-content-overrides.css |  50 +++++
 dev/generate-structured-content-style.js  | 191 +++++++++++++++++
 ext/css/structured-content.css            |   3 -
 ext/data/structured-content-style.json    | 330 ++++++++++++++++++++++++++++++
 package-lock.json                         |  33 +++
 package.json                              |   1 +
 test/test-structured-content-style.js     |  32 +++
 7 files changed, 637 insertions(+), 3 deletions(-)
 create mode 100644 dev/data/structured-content-overrides.css
 create mode 100644 dev/generate-structured-content-style.js
 create mode 100644 ext/data/structured-content-style.json
 create mode 100644 test/test-structured-content-style.js

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 <https://www.gnu.org/licenses/>.
+ */
+
+.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 <https://www.gnu.org/licenses/>.
+ */
+
+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 <https://www.gnu.org/licenses/>.
+ */
+
+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); }
-- 
cgit v1.2.3