highlight.js/tools/checkTheme.js

289 lines
6.0 KiB
JavaScript
Executable File

#!/usr/bin/env node
const fs = require("fs");
const css = require("css");
const wcagContrast = require("wcag-contrast");
const Table = require('cli-table');
const csscolors = require('css-color-names');
require("@colors/colors");
const CODE = {
name: "program code",
scopes: [
"comment",
"keyword",
"built_in",
"type",
"literal",
"number",
"property",
"regexp",
"string",
"subst",
"symbol",
// "class",
// "function",
"variable",
"title",
"params",
"comment",
"doctag",
"meta",
"attr",
"attribute"
]
};
const OTHER = {
name: "nice to haves (optional, but many grammars use)",
scopes: [
"meta keyword",
"meta string"
]
};
const HIGH_FIDELITY = {
name: "high fidelity highlighting (this is optional)",
scopes: [
"title.class",
"title.class.inherited",
"punctuation",
"operator",
"title.function",
"char.escape",
"variable.language"
]
};
const CONFIG = {
required: true,
name: "Config files",
scopes: [
"meta",
"number",
"string",
"variable",
"keyword",
"section",
"attribute"
]
};
const MARKUP = {
required: true,
name: "Markup (Markdown, etc)",
scopes: [
"section",
"bullet",
"code",
"emphasis",
"strong",
"formula",
"link",
"quote"
]
};
const CSS = {
name: "CSS/Less/etc",
required: true,
scopes: [
"attribute",
"string",
"keyword",
"built_in",
"selector-tag",
"selector-id",
"selector-class",
"selector-attr",
"selector-pseudo"
]
};
const TEMPLATES = {
name: "Templates/HTML/XML, etc.",
required: true,
scopes: [
"tag",
"name",
"attr",
"attribute",
"template-tag",
"template-variable"
]
};
const DIFF = {
name: "Diff",
required: true,
scopes: [
"meta",
"comment",
"addition",
"deletion"
]
};
function matching_rules(selector, rules) {
const found = [];
rules.forEach(rule => {
if (!rule.selectors) return;
if (rule.selectors.includes(selector)) {
found.push(rule);
}
});
return found;
}
function has_rule(selector, rules) {
if (matching_rules(selector, rules).length > 0) return true;
return false;
}
function skips_rule(selector, rules) {
return matching_rules(selector, rules)
.some(rule => rule.declarations.length === 0);
}
const expandScopeName = (name, { prefix }) => {
if (name.includes(".")) {
const pieces = name.split(".");
return [
`${prefix}${pieces.shift()}`,
...(pieces.map((x, i) => `${x}${"_".repeat(i + 1)}`))
].join(".");
}
return `${prefix}${name}`;
};
function scopeToSelector(name) {
return name.split(" ").map(n => expandScopeName(n, { prefix: ".hljs-" })).join(" ");
}
function check_group(group, rules) {
const has_rules = group.scopes.map(scope => {
const selector = scopeToSelector(scope);
return [scope, has_rule(selector, rules), skips_rule(selector, rules)];
});
const doesNotSupport = has_rules.map(x => x[1]).includes(false);
const skipped = has_rules.find(x => x[2]);
if (doesNotSupport || skipped) {
console.log(group.name.yellow);
if (doesNotSupport) {
console.log(`- Theme does not fully support.`.brightMagenta);
}
has_rules.filter(x => !x[1]).forEach(([scope, _]) => {
const selector = scopeToSelector(scope);
console.log(`- scope ${scope.cyan} is not highlighted\n (css: ${selector.green})`);
});
has_rules.filter(x => x[2]).forEach(([scope, _]) => {
console.log(` - scope ${scope.cyan} [purposely] un-highlighted.`.cyan);
});
console.log();
}
}
const round2 = (x) => Math.round(x*100)/100;
class CSSRule {
constructor(rule, body) {
this.rule = rule;
if (rule.declarations) {
this.bg = rule.declarations.find(x => x.property == "background-color")?.value;
if (!this.bg) {
this.bg = rule.declarations.find(x => x.property == "background")?.value;
}
this.fg = rule.declarations.find(x => x.property =="color")?.value;
if (this.bg) {
this.bg = csscolors[this.bg] || this.bg;
}
if (this.fg) {
this.fg = csscolors[this.fg] || this.fg;
}
// inherit from body if we're missing fg or bg
if (this.hasColor) {
if (!this.bg) this.bg = body.background;
if (!this.fg) this.fg = body.foreground;
}
}
}
get background() {
return this.bg
}
get foreground() {
return this.fg
}
get hasColor() {
if (!this.rule.declarations) return false;
return this.fg || this.bg;
}
toString() {
return ` ${this.foreground} on ${this.background}`
}
contrastRatio() {
if (!this.foreground) return "unknown (no fg)"
if (!this.background) return "unknown (no bg)"
return round2(wcagContrast.hex(this.foreground, this.background));
}
}
function contrast_report(rules) {
console.log("Accessibility Report".yellow);
var hljs = rules.find (x => x.selectors && x.selectors.includes(".hljs"));
var body = new CSSRule(hljs);
const table = new Table({
chars: {'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''},
head: ['ratio', 'selector', 'fg', 'bg'],
colWidths: [7, 40, 10, 10],
style: {
head: ['grey']
}
});
rules.forEach(rule => {
var color = new CSSRule(rule, body);
if (!color.hasColor) return;
table.push([
color.contrastRatio(),
rule.selectors,
color.foreground,
color.background
])
// console.log(r.selectors[0], color.contrastRatio(), color.toString());
})
console.log(table.toString())
}
function validate(data) {
const rules = data.stylesheet.rules;
check_group(DIFF, rules);
check_group(TEMPLATES, rules);
check_group(CONFIG, rules);
check_group(CSS, rules);
check_group(MARKUP, rules);
check_group(CODE, rules);
check_group(OTHER, rules);
check_group(HIGH_FIDELITY, rules);
contrast_report(rules);
}
process.argv.shift();
process.argv.shift();
const file = process.argv[0];
const content = fs.readFileSync(file).toString();
const parsed = css.parse(content, {});
validate(parsed);