Skip to content

Commit 99fcddd

Browse files
IdrinthBjörn Büttner
andauthored
Handle sub media matches (#1)
* handle partial matches * next step, more sensible latest search * add caching * fix js clearing * add basic import support * adding support for entry points config --------- Co-authored-by: Björn Büttner <[email protected]>
1 parent 72982d2 commit 99fcddd

13 files changed

+1778
-105
lines changed

package-lock.json

Lines changed: 1559 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
"@types/css": "^0.0.37",
2424
"@types/node": "^20.10.6",
2525
"typescript": "^5.3.3",
26-
"ts-node": "^10.9.2"
26+
"ts-node": "^10.9.2",
27+
"eslint": "^8.56.0",
28+
"eslint-plugin-json": "^3.1.0",
29+
"@typescript-eslint/parser": "^6.20.0"
2730
},
2831
"bin": {
2932
"lint-css-duplicates": "bin/lint-css-duplicates.js",

src/build-map.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/handle-at-rule.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import css from "css";
2-
import Rules from "./rules.js";
32
import handleRule from "./handle-rule.js";
3+
import Media from "./media.js";
4+
import RuleStore from "./rule-store.js";
5+
import handleFile from "./handle-file.js";
46

5-
export default (rule: css.Media, rules: Rules): boolean => {
7+
const handleAtRule = (rule: css.Media, rules: RuleStore, media: Media): boolean => {
68
let fails = false;
79
for (const rul of rule.rules || []) {
8-
fails = handleRule(rul, rules, rule.media) || fails;
10+
if (rul.type === 'rule') {
11+
const ru: css.Rule = rul;
12+
fails = handleRule(ru, rules, media, ru.position.source) || fails;
13+
} else if (rul.type === 'media') {
14+
const ru: css.Media = rul;
15+
fails = handleAtRule(ru, rules, media.createChild(ru.media || '',))
16+
} else if (rul.type === 'import') {
17+
const ru: css.Import = rul;
18+
if (ru.import.match(/^[^/]/,)) {
19+
fails = handleFile(ru.position.source.replace(/[\\/].*?\.css$/, '/') + ru.import, rules, media);
20+
}
21+
}
922
}
1023
return fails;
1124
};
25+
export default handleAtRule;

src/handle-declaration.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
import css from "css";
2-
import Rules from "./rules.js";
2+
import Media from "./media.js";
3+
import RuleStore from "./rule-store.js";
34

4-
export default (selector: string, declaration: css.Declaration, position: css.Position | undefined, media: string, rules: Rules): boolean => {
5+
export default (selector: string, declaration: css.Declaration, position: css.Position, media: Media, rules: RuleStore, file: string): boolean => {
56
let fails = false;
6-
if (rules[selector]) {
7-
if (rules[selector][media]) {
8-
if (rules[selector][media][declaration.property || '']) {
9-
fails = true;
10-
console.error(`${declaration.property} of ${selector} overwrites a previous definition in line ${position?.line} for media ${media}.`);
11-
}
12-
}
13-
if (media !== '' && rules[selector]['']) {
14-
if (rules[selector][''][declaration.property || ''] && rules[selector][''][declaration.property || ''] === declaration.value || '') {
15-
fails = true;
16-
console.error(`${declaration.property} of ${selector} overwrites a previous definition in line ${position?.line} general with the same value.`);
17-
}
7+
const property = declaration.property || '';
8+
const latest = rules.latest(selector, media, property);
9+
if (latest) {
10+
if (latest.media.toString() === media.toString()) {
11+
fails = true;
12+
console.error(`${property} of ${selector} in file:line:column ${file}:${position.line}:${position.column} overwrites a previous definition for the same @media in file:line:column ${latest.file}:${latest.line}:${latest.column}.`);
13+
} else if (latest.value === declaration.value) {
14+
fails = true;
15+
console.error(`${property} of ${selector} in file:line:column ${file}:${position.line}:${position.column} overwrites a previous definition in file:line:column ${latest.file}:${latest.line}:${latest.column} with the same value.`);
1816
}
1917
}
20-
rules[selector] = rules[selector] || {};
21-
rules[selector][media] = rules[selector][media] || {};
22-
rules[selector][media][declaration.property || ''] = declaration.value || '';
18+
rules.add(selector, media, property, declaration.value, position.line, position.column, file)
2319
return fails;
2420
}

src/handle-file.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import css from 'css';
2+
import {readFileSync} from 'fs';
3+
import handleRule from "./handle-rule.js";
4+
import handleAtRule from "./handle-at-rule.js";
5+
import Media from "./media.js";
6+
import RuleStore from "./rule-store.js";
7+
8+
const handleFile = (file: string, ruleStore: RuleStore|undefined = undefined, parentMedia: Media|undefined = undefined): boolean => {
9+
const rules = ruleStore || new RuleStore();
10+
const media = parentMedia || new Media('');
11+
let fails = false;
12+
const code = css.parse(readFileSync(file, 'utf8'), {
13+
source: file,
14+
});
15+
for (const rule of (code.stylesheet?.rules || [])) {
16+
if (rule.type === 'rule') {
17+
const rul: css.Rule = rule;
18+
fails = handleRule(rul, rules, media, rul.position.source) || fails;
19+
} else if(rule.type === 'media') {
20+
const rul: css.Media = rule;
21+
fails = handleAtRule(rul, rules, media.createChild(rul.media)) || fails;
22+
} else if (rule.type === 'import') {
23+
const rul: css.Import = rule;
24+
if (rul.import.match(/^[^/]/,)) {
25+
fails = handleFile(file.replace(/[\\/].*?\.css$/, '/') + rul.import, rules, media);
26+
}
27+
}
28+
}
29+
return !fails;
30+
};
31+
export default handleFile;

src/handle-rule.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import css from "css";
2-
import Rules from "./rules.js";
3-
import {handleDeclaration} from "./handle-declaration.js";
2+
import handleDeclaration from "./handle-declaration.js";
3+
import Media from "./media.js";
4+
import RuleStore from "./rule-store.js";
45

5-
export default (rule: css.Rule, rules: Rules, media: string = '') => {
6+
export default (rule: css.Rule, rules: RuleStore, media: Media, file: string|undefined) => {
67
let fails = false;
78
for (const selector of rule.selectors || []) {
89
for (const declaration of rule.declarations || []) {
910
if (declaration.type === 'declaration') {
10-
fails = handleDeclaration(selector, declaration, rule.position?.start, media, rules) || fails;
11+
fails = handleDeclaration(selector, declaration, rule.position?.start, media, rules, file) || fails;
1112
}
1213
}
1314
}

src/index.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1-
import {readdirSync} from 'node:fs';
2-
import buildMap from './build-map.js';
1+
import {existsSync, readdirSync} from 'node:fs';
2+
import checkFile from './handle-file.js';
3+
import {readFileSync} from "fs";
34

45
export default (cwd: string): boolean => {
5-
let success = false;
6+
let success = true;
7+
if (existsSync(cwd + '/.idrinth-duplicate-style-check.json')) {
8+
const data = JSON.parse(readFileSync(cwd + '/.idrinth-duplicate-style-check.json', 'utf8'));
9+
if (typeof data === 'object' && typeof data.entrypoints === 'object' && Array.isArray(data.entrypoints)) {
10+
for(const file of data.entrypoints) {
11+
if (!existsSync(cwd + '/' + file)) {
12+
success = false;
13+
console.error(`Css file ${file} doesn't exist.`);
14+
} else {
15+
success = checkFile(cwd + '/' + file) && success;
16+
}
17+
}
18+
return success;
19+
}
20+
}
21+
console.warn('No configuration found, trying every css file outside node_modules as entry point.');
622
for (const file of readdirSync(cwd, {recursive: true, encoding: 'utf8'})) {
7-
if (file.endsWith('.css') && !file.includes('node_modules') && !file.includes('coverage') && !file.includes('dist')) {
8-
success = buildMap(cwd + '/' + file) || success;
23+
if (file.endsWith('.css') && !file.includes('node_modules')) {
24+
success = checkFile(cwd + '/' + file) && success;
925
}
1026
}
1127
return success;

src/media.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
export default class Media
2+
{
3+
private requirements: string[] = [];
4+
private media: string;
5+
private options: string[] = [];
6+
constructor(media: string) {
7+
for (const requirement of media.split('and')) {
8+
const trimmed = requirement.replace(/^\s+|\s+$/ug, '');
9+
if (requirement && ! this.requirements.includes(trimmed)) {
10+
this.requirements.push(trimmed);
11+
}
12+
}
13+
this.requirements.sort();
14+
}
15+
public toString(): string
16+
{
17+
if (this.media) {
18+
return this.media;
19+
}
20+
if (this.requirements.length === 0) {
21+
return this.media = 'general';
22+
}
23+
return this.media = this.requirements.join(' and ');
24+
}
25+
private * getAllOptions(index: number): Generator<string>
26+
{
27+
if (! this.requirements[index]) {
28+
return;
29+
}
30+
yield this.requirements[index];
31+
for (const option of this.getAllOptions(index + 1)) {
32+
yield `${ this.requirements[index] } and ${ option }`
33+
yield option;
34+
}
35+
}
36+
public * getOptions(): Generator<string>
37+
{
38+
if (this.options.length > 0) {
39+
for(const option of this.options) {
40+
yield option;
41+
}
42+
return;
43+
}
44+
this.options.push('');
45+
yield '';
46+
for (const option of this.getAllOptions(0)) {
47+
this.options.push(option);
48+
yield option;
49+
}
50+
}
51+
public contains(media: Media): boolean
52+
{
53+
for (const option of this.getOptions()) {
54+
if (media.toString() === option) {
55+
return true;
56+
}
57+
}
58+
return false;
59+
}
60+
public createChild(media: string) : Media
61+
{
62+
if (this.requirements.length === 0) {
63+
return new Media(media);
64+
}
65+
if (media === '') {
66+
return this;
67+
}
68+
return new Media(this.toString() + ' and ' + media);
69+
}
70+
}

src/rule-store.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Media from "./media.js";
2+
import Rule from "./rule.js";
3+
import Rules from "./rules.js";
4+
5+
export default class RuleStore {
6+
private index = 0;
7+
private data: Rules = {};
8+
public latest(selector: string, media: Media, property: string): Rule|undefined
9+
{
10+
if (typeof this.data[selector] === 'undefined' || typeof this.data[selector][property] === 'undefined') {
11+
return;
12+
}
13+
let latest: Rule = undefined;
14+
for (const md of Object.keys(this.data[selector][property])) {
15+
if(media.contains(this.data[selector][property][md].media)) {
16+
if (typeof latest === 'undefined') {
17+
latest = this.data[selector][property][md];
18+
} else if (latest.index < this.data[selector][property][md].index) {
19+
latest = this.data[selector][property][md];
20+
}
21+
}
22+
}
23+
return latest;
24+
}
25+
public add(
26+
selector: string,
27+
media: Media,
28+
property: string,
29+
value: string,
30+
line: number|undefined = undefined,
31+
column: number|undefined = undefined,
32+
file: string|undefined = undefined,
33+
): void {
34+
this.data[selector] = this.data[selector] || {};
35+
this.data[selector][property] = this.data[selector][property] || {};
36+
this.data[selector][property][media.toString()] = new Rule(selector, property, media, value, this.index, line, column, file);
37+
this.index++;
38+
}
39+
}

src/rule.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Media from "./media.js";
2+
3+
export default class Rule {
4+
constructor(
5+
public readonly selector: string,
6+
public readonly property: string,
7+
public readonly media: Media,
8+
public readonly value: string,
9+
public readonly index: number,
10+
public readonly line: number|undefined = undefined,
11+
public readonly column: number|undefined = undefined,
12+
public readonly file: string|undefined = undefined,
13+
) {
14+
}
15+
}

src/rules.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import Rule from "./rule.js";
2+
13
export default interface Rules {
24
[selector: string]: {
3-
[media: string]: {
4-
[property: string]: string
5+
[property: string]: {
6+
[media: string]: Rule
57
}
68
}
79
}

tools/clear-js-files-from-src.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,3 @@ const clearFolder = (folder,) => {
1616
}
1717
};
1818
clearFolder(__dirname + '../src',);
19-
if (existsSync(__dirname + '../index.js')) {
20-
rmSync(__dirname + '../index.js',);
21-
}

0 commit comments

Comments
 (0)