Skip to content

Commit

Permalink
[WIP] Color format checking for bslint (#94)
Browse files Browse the repository at this point in the history
* Color format checking - first step/ example

* Color format checking

* Color format checking

* Color format, alpha, default alpha, case and color cert requirement checking

* Adding alpha to luma check and removing todo

* Updating readme

* Moving color validate functions to util for use in multiple files

* Updating readme

* Removing isXMLFile checking

* Temp example for traversing to XML node arrtibute values

* Moving XML color validation to codeStyle

* Moving XML color validation to codeStyle

* Moving XML color validation to codeStyle

* Starting refactor

* Continuing refactor

* Continuing refactor

* Continuing refactor

* Removing XML file checks - will add these in a seperate PR

* Continuing refactor

* PR comment fixes

* Continuing refactor

* Continuing refactor

* Continuing refactor

* Fixing build errors

* Including non color string checks

* Removing initial code for fixes. Due to add in a future PR

* Including non color string checks

* Continuing refactor

* Move all test brs/bs code inline

* Continuing refactor

* Fix templatestring quasi handling

* Remove unused color test files

* Removing breakpoint

* only validate string-like template strings

---------

Co-authored-by: Charlie Abbott <[email protected]>
Co-authored-by: Bronley Plumb <[email protected]>
  • Loading branch information
3 people committed Sep 28, 2023
1 parent bba427b commit 3995e5f
Show file tree
Hide file tree
Showing 8 changed files with 542 additions and 6 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ Default rules:

"type-annotations": "off",

"color-format": "off",
"color-case": "off",
"color-alpha": "off",
"color-alpha-defaults": "off",
"color-cert": "off",

"assign-all-paths": "error",
"unsafe-path-loop": "error",
"unsafe-iterators": "error",
Expand Down Expand Up @@ -210,6 +216,38 @@ Default rules:
- `never` enforces that files do not end with a newline
- `off`: do not validate

- `color-format`: ensures that all the color values follow the same prefix formatting. Can also use to prevent any colors values from being defined in the code-base (brs or bs files), except for values in a stand-alone file (ie. theme file).

- `hash-hex`: enforces all color values are type string or template string and use a `#` prefix
- `quoted-numeric-hex`: enforces all color values are type string or template string and use a `0x` prefix
- `never`: enforces that no color values can be defined in the code-base (brs or bs files). Useful if you define colors in a separate stand-alone file. To use this option you would list your stand-alone file in the `ignore` list or `diagnosticFilters`.
- `off`: do not validate (**default**)

- `color-case`: ensures that all color values follow the same case. Requires that `color-format` is set to `hash-hex` or `quoted-numeric-hex`.

- `lower`: enforces all color values that are type string or template string and all lowercase. ie. `#11bbdd`
- `upper`: enforces all color values that are type string or template string and all uppercase. ie. `#EEAA44`
- `off`: do not validate (**default**)

- `color-alpha`: defines the usage of the color alpha value. ie. `#xxxxxxFF`. Requires that `color-format` is set to `hash-hex` or `quoted-numeric-hex`.

- `always`: enforces all color values that are type string or template string define an alpha value
- `allowed`: allows color values that are type string or template string to define an alpha value
- `never`: enforces that none of the color values that are type string or template string define an alpha value
- `off`: do not validate (**default**)

- `color-alpha-defaults`: enforces default color-alpha values. ie. `#xxxxxxFF` or `#xxxxxx00`. Requires that `color-alpha` is not set to `off` and `color-format` is set to `hash-hex` or `quoted-numeric-hex`.

- `allowed`: allows both types of defaults to be used
- `only-hidden`: only allows opacity 0% (hidden) from being used
- `never`: enforces that no defaults can be used
- `off`: do not validate (**default**)

- `color-cert`: enforces Roku's [broadcast safe color 6.4 certification requirement](https://developer.roku.com/en-gb/docs/developer-program/certification/certification.md). Requires that `color-format` is set to `hash-hex` or `quoted-numeric-hex`.

- `always`: ensures all white and black color-format values either match or are darker/ lighter than the minimum recommended values. For white the minimum value is `#DBDBDB` and for black the minimum value is `#161616`
- `off`: do not validate (**default**)

### Strictness rules

- `type-annotations`: validation of presence of `as` type annotations, for function
Expand Down
105 changes: 105 additions & 0 deletions src/createColorValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { BsDiagnostic, Range } from 'brighterscript';
import { messages } from './plugins/codeStyle/diagnosticMessages';
import { BsLintRules, RuleColorFormat, RuleColorCase, RuleColorAlpha, RuleColorAlphaDefaults, RuleColorCertCompliant } from './index';

export function createColorValidator(severity: Readonly<BsLintRules>) {
const { colorFormat, colorCase, colorAlpha, colorAlphaDefaults, colorCertCompliant } = severity;
return (text, range, diagnostics) => {
const len = text.length;
if (len < 7 || len > 12) {
// we're only interested in string length is between 7 (#DBDBDB) to 12 ("0xDBDBDBff") chars long
return;
}

const hashHexRegex = /#[0-9A-Fa-f]{6}/g;
const quotedNumericHexRegex = /0x[0-9A-Fa-f]{6}/g;
const hashHexMatches = (text.startsWith('#') || text.startsWith('"#')) ? text.match(hashHexRegex) : undefined;
const quotedNumericHexMatches = (text.startsWith('0x') || text.startsWith('"0x')) ? text.match(quotedNumericHexRegex) : undefined;

if ((colorFormat === 'never') && (quotedNumericHexMatches || hashHexMatches)) {
diagnostics.push(messages.expectedColorFormat(range));
return;
}
const hashHexAlphaRegex = /#[0-9A-Fa-f]{8}/g;
const quotedNumericHexAlphaRegex = /0x[0-9A-Fa-f]{8}/g;

if (colorFormat === 'hash-hex') {
if (quotedNumericHexMatches) {
diagnostics.push(messages.expectedColorFormat(range));
}
validateColorCase(hashHexMatches, range, diagnostics, colorCase, colorFormat);
validateColorAlpha(text.match(hashHexAlphaRegex), hashHexMatches, quotedNumericHexMatches, range, diagnostics, colorAlpha, colorAlphaDefaults);
validateColorCertCompliance(hashHexMatches, range, diagnostics, colorFormat, colorCertCompliant);

} else if (colorFormat === 'quoted-numeric-hex') {
if (hashHexMatches) {
diagnostics.push(messages.expectedColorFormat(range));
}
validateColorCase(quotedNumericHexMatches, range, diagnostics, colorCase, colorFormat);
validateColorAlpha(text.match(quotedNumericHexAlphaRegex), hashHexMatches, quotedNumericHexMatches, range, diagnostics, colorAlpha, colorAlphaDefaults);
validateColorCertCompliance(quotedNumericHexMatches, range, diagnostics, colorFormat, colorCertCompliant);
}
};
}

function validateColorAlpha(alphaMatches: RegExpMatchArray, hashMatches: RegExpMatchArray, quotedNumericHexMatches: RegExpMatchArray, range: Range, diagnostics: (Omit<BsDiagnostic, 'file'>)[], alpha: RuleColorAlpha, alphaDefaults: RuleColorAlphaDefaults) {
const validateColorAlpha = (alpha === 'never' || alpha === 'always' || alpha === 'allowed');
if (validateColorAlpha) {
if (alpha === 'never' && alphaMatches) {
diagnostics.push(messages.expectedColorAlpha(range));
}
if ((alpha === 'always' && alphaMatches === null) && (hashMatches || quotedNumericHexMatches)) {
diagnostics.push(messages.expectedColorAlpha(range));
}
if ((alphaDefaults === 'never' || alphaDefaults === 'only-hidden') && alphaMatches) {
for (let i = 0; i < alphaMatches.length; i++) {
const colorHashAlpha = alphaMatches[i];
const alphaValue = colorHashAlpha.slice(-2).toLowerCase();
if (alphaValue === 'ff' || (alphaDefaults === 'never' && alphaValue === '00')) {
diagnostics.push(messages.expectedColorAlphaDefaults(range));
}
}
}
}
}

function validateColorCase(matches: RegExpMatchArray, range: Range, diagnostics: (Omit<BsDiagnostic, 'file'>)[], colorCase: RuleColorCase, colorFormat: RuleColorFormat) {
const validateColorCase = colorCase === 'upper' || colorCase === 'lower';
if (validateColorCase && matches) {
let colorValue = matches[0];
const charsToStrip = (colorFormat === 'hash-hex') ? 1 : 2;
colorValue = colorValue.substring(charsToStrip);
if (colorCase === 'lower' && colorValue !== colorValue.toLowerCase()) {
diagnostics.push(messages.expectedColorCase(range));
}
if (colorCase === 'upper' && colorValue !== colorValue.toUpperCase()) {
diagnostics.push(messages.expectedColorCase(range));
}
}
}

function validateColorCertCompliance(matches: RegExpMatchArray, range: Range, diagnostics: (Omit<BsDiagnostic, 'file'>)[], colorFormat: RuleColorFormat, certCompliant: RuleColorCertCompliant) {
const validateCertCompliant = certCompliant === 'always';
if (validateCertCompliant && matches) {
const BROADCAST_SAFE_BLACK = '161616';
const BROADCAST_SAFE_WHITE = 'DBDBDB';
const MAX_BLACK_LUMA = getColorLuma(BROADCAST_SAFE_BLACK);
const MAX_WHITE_LUMA = getColorLuma(BROADCAST_SAFE_WHITE);
let colorValue = matches[0];
const charsToStrip = (colorFormat === 'hash-hex') ? 1 : 2;
colorValue = colorValue.substring(charsToStrip);
const colorLuma = getColorLuma(colorValue);
if (colorLuma > MAX_WHITE_LUMA || colorLuma < MAX_BLACK_LUMA) {
diagnostics.push(messages.colorCertCompliance(range));
}
}
}

function getColorLuma(value: string) {
const rgb = parseInt(value, 16); // Convert rrggbb to decimal
const red = (rgb >> 16) & 0xff; // eslint-disable-line no-bitwise
const green = (rgb >> 8) & 0xff; // eslint-disable-line no-bitwise
const blue = (rgb >> 0) & 0xff; // eslint-disable-line no-bitwise
// Per ITU-R BT.709
return 0.2126 * red + 0.7152 * green + 0.0722 * blue; // eslint-disable-line
}
15 changes: 15 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export type RuleFunction = 'no-function' | 'no-sub' | 'auto' | 'off';
export type RuleAAComma = 'always' | 'no-dangling' | 'never' | 'off';
export type RuleTypeAnnotations = 'all' | 'return' | 'args' | 'off';
export type RuleEolLast = 'always' | 'never' | 'off';
export type RuleColorFormat = 'hash-hex' | 'quoted-numeric-hex' | 'never' | 'off';
export type RuleColorCase = 'upper' | 'lower' | 'off';
export type RuleColorAlpha = 'always' | 'allowed' | 'never' | 'off';
export type RuleColorAlphaDefaults = 'allowed' | 'only-hidden' | 'never' | 'off';
export type RuleColorCertCompliant = 'always' | 'off'; // Roku cert requirement for broadcast safe colors. 6.4

export type BsLintConfig = Pick<BsConfig, 'project' | 'rootDir' | 'files' | 'cwd' | 'watch'> & {
lintConfig?: string;
Expand All @@ -39,6 +44,11 @@ export type BsLintConfig = Pick<BsConfig, 'project' | 'rootDir' | 'files' | 'cwd
// Will be transformed to RegExp type when program context is created.
'todo-pattern'?: string;
'eol-last'?: RuleEolLast;
'color-format'?: RuleColorFormat;
'color-case'?: RuleColorCase;
'color-alpha'?: RuleColorAlpha;
'color-alpha-defaults'?: RuleColorAlphaDefaults;
'color-cert'?: RuleColorCertCompliant;
};
globals?: string[];
ignores?: string[];
Expand Down Expand Up @@ -67,6 +77,11 @@ export interface BsLintRules {
noTodo: BsLintSeverity;
noStop: BsLintSeverity;
eolLast: RuleEolLast;
colorFormat: RuleColorFormat;
colorCase: RuleColorCase;
colorAlpha: RuleColorAlpha;
colorAlphaDefaults: RuleColorAlphaDefaults;
colorCertCompliant: RuleColorCertCompliant;
}

export { Linter };
Expand Down
42 changes: 41 additions & 1 deletion src/plugins/codeStyle/diagnosticMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ export enum CodeStyleError {
NoTodo = 'LINT3015',
NoStop = 'LINT3016',
EolLastMissing = 'LINT3017',
EolLastFound = 'LINT3018'
EolLastFound = 'LINT3018',
ColorFormat = 'LINT3019',
ColorCase = 'LINT3020',
ColorAlpha = 'LINT3021',
ColorAlphaDefaults = 'LINT3022',
ColorCertCompliant = 'LINT3023'
}

const CS = 'Code style:';
Expand Down Expand Up @@ -159,5 +164,40 @@ export const messages = {
source: 'bslint',
message: `${CS} File should not end with a newline`,
range
}),
expectedColorFormat: (range: Range) => ({
severity: DiagnosticSeverity.Error,
code: CodeStyleError.ColorFormat,
source: 'bslint',
message: `${CS} File should follow color format`,
range
}),
expectedColorCase: (range: Range) => ({
severity: DiagnosticSeverity.Error,
code: CodeStyleError.ColorCase,
source: 'bslint',
message: `${CS} File should follow color case`,
range
}),
expectedColorAlpha: (range: Range) => ({
severity: DiagnosticSeverity.Error,
code: CodeStyleError.ColorAlpha,
source: 'bslint',
message: `${CS} File should follow color alpha rule`,
range
}),
expectedColorAlphaDefaults: (range: Range) => ({
severity: DiagnosticSeverity.Error,
code: CodeStyleError.ColorAlphaDefaults,
source: 'bslint',
message: `${CS} File should follow color alpha defaults rule`,
range
}),
colorCertCompliance: (range: Range) => ({
severity: DiagnosticSeverity.Error,
code: CodeStyleError.ColorCertCompliant,
source: 'bslint',
message: `${CS} File should follow Roku broadcast safe color cert requirement`,
range
})
};
Loading

0 comments on commit 3995e5f

Please sign in to comment.