diff --git a/.vscode/spell.json b/.vscode/spell.json new file mode 100644 index 00000000..1ce704e4 --- /dev/null +++ b/.vscode/spell.json @@ -0,0 +1,26 @@ +{ + "language": "en", + "ignoreWordsList": [], + "mistakeTypeToStatus": { + "Passive voice": "Hint", + "Spelling": "Error", + "Complex Expression": "Disable", + "Hidden Verbs": "Information", + "Hyphen Required": "Disable", + "Redundant Expression": "Disable", + "Did you mean...": "Disable", + "Repeated Word": "Warning", + "Missing apostrophe": "Warning", + "Cliches": "Disable", + "Missing Word": "Disable", + "Make I uppercase": "Warning" + }, + "languageIDs": [ + "markdown", + "plaintext" + ], + "ignoreRegExp": [ + "/\\(.*\\.(jpg|jpeg|png|gif|JPG|JPEG|PNG|GIF)\\)/g", + "/((http|https|ftp|git)\\S*)/g" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ddc76f..f5e350ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [0.3.0] - 2017.02.04 + +- Use Promise for background generation/update for easy chaining +- Add new regex for rgb(a) colors detection +- Add new activationEvents (pcss and sss) +- Add rgb(a) color extraction +- Add rgb and luminance properties in color.ts + ## [0.2.1] - 2017.01.26 ## [0.2.0] - 2017.01.25 diff --git a/README.md b/README.md index ba747db4..8008c1ae 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![codebeat badge](https://codebeat.co/badges/aec222e1-64ae-4360-a849-d077040694ca)](https://codebeat.co/projects/github-com-kamikillerto-vscode-colorize) [![Build Status](https://travis-ci.org/KamiKillertO/vscode-colorize.svg?branch=master)](https://travis-ci.org/KamiKillertO/vscode-colorize) [![Build status](https://ci.appveyor.com/api/projects/status/errygb6n97kiq75a?svg=true)](https://ci.appveyor.com/project/KamiKillertO/vscode-colorize) [![Licence](https://img.shields.io/github/license/KamiKillertO/vscode_colorize.svg)](https://github.com/KamiKillertO/vscode_colorize) ![VS Code Marketplace](http://vsmarketplacebadge.apphb.com/version-short/kamikillerto.vscode-colorize.svg) -Instantly visualize css colors in your css/sass/scss/less files. +Instantly visualize css colors in your css/sass/scss/less/pcss/sss files. This extension scan your styles files looking for colors and generate a colored background for each of them. The background is generated/updated from the color. @@ -12,14 +12,16 @@ The background is generated/updated from the color. ## Features - Generate colored background for css hexa color +- 🆕 Generate colored background for rgb hexa color +- 🆕 Generate colored background for rgba hexa color - Update the background when the color is updated ## Roadmap - [x] Generate background for hexa colors - [x] Update background on color updates -- [ ] Generate background for rgb colors -- [ ] Generate background for rgba colors +- [x] Generate background for rgb colors +- [~] Generate background for rgba colors - [ ] Generate background for hsl colors - [ ] Generate background for hsla colors - [ ] Generate background for Predefined/Cross-browser colors @@ -28,7 +30,15 @@ The background is generated/updated from the color. ## Release Notes -### Latest 0.2.1 (2017.01.26) +### Latest 0.3.0 (2017.02.04) + +- Add support for PostCSS +- Generate background for RGB colors +- Generate background for RGBa colors + +### 0.2.1 (2017.01.26) + +- Fix some background update issues ### 0.2.0 (2017.01.25) diff --git a/package.json b/package.json index 0443d5f1..01d660a9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-colorize", "displayName": "colorize", "description": "A vscode extension to help visualize css colors in files.", - "version": "0.2.1", + "version": "0.3.0", "publisher": "kamikillerto", "license": "Apache-2.0", "icon": "assets/logo.png", @@ -35,7 +35,9 @@ "onLanguage:css", "onLanguage:sass", "onLanguage:scss", - "onLanguage:less" + "onLanguage:less", + "onLanguage:pcss", + "onLanguage:sss" ], "main": "./out/src/extension", "contributes": { diff --git a/src/color-decoration.ts b/src/color-decoration.ts index 8fbc028b..2c835359 100644 --- a/src/color-decoration.ts +++ b/src/color-decoration.ts @@ -43,8 +43,8 @@ class ColorDecoration { let backgroundDecorationType = window.createTextEditorDecorationType({ borderWidth: "1px", borderStyle: "solid", - borderColor: this.color.value, - backgroundColor: this.color.value, + borderColor: this.color.toRGBString(), + backgroundColor: this.color.toRGBString(), color: textColor }); this.decoration = backgroundDecorationType; diff --git a/src/color-regex.ts b/src/color-regex.ts index adf7de4c..d70d1c0b 100644 --- a/src/color-regex.ts +++ b/src/color-regex.ts @@ -7,8 +7,9 @@ /** * Utils object for color manipulation */ -export const HEXA_COLOR = /(#[\da-f]{3}|#[\da-f]{6})($|,| |;|\n)/gi; - +export const HEXA_COLOR = /(#[\da-f]{3}|#[\da-f]{6})(?:$|,| |;|\n)/gi; +export const RGB_COLOR = /((?:rgb\((?:\d{1,3}\s*,\s*){2}\d{1,3}\))|(?:rgba\((?:\d{1,3}\s*,\s*){3}[0-1](?:\.\d+){0,1}\)))(?:$|,| |;|\n)/gi; export default { - HEXA_COLOR + HEXA_COLOR, + RGB_COLOR }; diff --git a/src/color-util.ts b/src/color-util.ts index 80963178..42649bc5 100644 --- a/src/color-util.ts +++ b/src/color-util.ts @@ -1,62 +1,64 @@ import { - HEXA_COLOR + HEXA_COLOR, + RGB_COLOR } from './color-regex'; import Color from './color'; +// Flatten Array +// flatten(arr[[1,2,3],[4,5]]) -> arr[1,2,3,4,5] +const flatten = arr => arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []); + class ColorUtil { - public static getRGB(color: Color): number[] { - let rgb: any[] = []; - if (color.model === 'hexa') { - rgb = /#(.+)/gi.exec(color.value); - if (rgb[1].length === 3) { - return rgb[1].split('').map(_ => parseInt(_ + _, 16)); - } - rgb = rgb[1].split('').map(_ => parseInt(_, 16)); - return [16 * rgb[0] + rgb[1], 16 * rgb[2] + rgb[3], 16 * rgb[4] + rgb[5]]; - } - return []; - } public static luminance(color: Color): number { - let rgb = this.getRGB(color); - if (!rgb) { - return null; - } - rgb = rgb.map(_ => { - _ = _ / 255; - if (_ < 0.03928) { - _ = _ / 12.92; + let rgb = color.rgb; + rgb = rgb.map(c => { + c = c / 255; + if (c < 0.03928) { + c = c / 12.92; } else { - _ = (_ + .055) / 1.055; - _ = Math.pow(_, 2.4); + c = (c + .055) / 1.055; + c = Math.pow(c, 2.4); } - return _; + return c; }); - return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; + return (0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]); } - public static extractColor(text: string):Color[] { - let colors:Color[] = []; - colors = colors.concat(this._extractHexa(text)); - return colors; + public static findColors(text): Promise < Color[] > { + return Promise.all([ + this._extractHexa(text), + this._extractRGB(text) + ]).then(colors => { + return flatten(colors); + }); } - public static match(text: string, model: string):boolean { - switch(model) { - case 'hexa': - return !!text.match(HEXA_COLOR); - default: - return false; - } + private static _extractHexa(text: string): Promise < Color[] > { + return new Promise((resolve, reject) => { + let match = null; + let colors: Color[] = []; + while ((match = HEXA_COLOR.exec(text)) !== null) { + colors.push(new Color('hexa', match[1], match.index)); + } + return resolve(colors); + }); } - - private static _extractHexa(text: string): Color[] { - let match = null; - let colors:Color[] = []; - while((match = HEXA_COLOR.exec(text)) !== null) { - colors.push(new Color('hexa', match[1], match.index)) - } - return colors; + + private static _extractRGB(text: string): Promise < Color[] > { + return new Promise((resolve, reject) => { + let match = null; + let colors: Color[] = []; + // Get rgb "like" colors + while ((match = RGB_COLOR.exec(text)) !== null) { + let rgba = match[1].replace(/rgb(a){0,1}\(/, '').replace(/\)/, '').split(/,/gi).map(c => parseFloat(c)); + // Check if it's a valid rgb(a) color + if (rgba.slice(0, 3).every(c => c <= 255) && (rgba[4] || 1) <= 1) { + colors.push(new Color('rgb', match[0], match.index)); + } + } + return resolve(colors); + }); } }; export default ColorUtil; diff --git a/src/color.ts b/src/color.ts index dc2c0e70..895b5b9e 100644 --- a/src/color.ts +++ b/src/color.ts @@ -3,12 +3,37 @@ import ColorUtil from './color-util'; class Color { public model: string; public value: string; + public rgb: number[]; + public alpha: number; public positionInText: number; public constructor(model: string, value: string, positionInText: number = 0) { this.model = model; this.value = value; this.positionInText = positionInText; + this._getRGBValue(); + } + + private _getRGBValue() { + switch (this.model) { + case 'hexa': + this.alpha = 1; + let rgb: any = /#(.+)/gi.exec(this.value); + if (rgb[1].length === 3) { + return this.rgb = rgb[1].split('').map(_ => parseInt(_ + _, 16)); + } + rgb = rgb[1].split('').map(_ => parseInt(_, 16)); + return this.rgb = [16 * rgb[0] + rgb[1], 16 * rgb[2] + rgb[3], 16 * rgb[4] + rgb[5]]; + case 'rgb': + let rgba = this.value.replace(/rgb(a){0,1}\(/, '').replace(/\)/, '').split(/,/gi).map(c => parseFloat(c)); + this.rgb = rgba.slice(0, 3); + this.alpha = rgba[4] || 1; + return; + } + } + + public toRGBString(): string { + return `rgb(${this.rgb.join(',')})`; } } export default Color; diff --git a/src/extension.ts b/src/extension.ts index a80d6be7..946345e5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,7 +29,7 @@ import Queue from './queue'; // this method is called when your extension is activated // your extension is activated the very first time the command is executed -function mapToArray(map: Map < number, any > ) { +function mapKeysToArray(map: Map < number, any > ) { let it = map.keys(); let tmp = it.next(); let array = []; @@ -40,16 +40,15 @@ function mapToArray(map: Map < number, any > ) { return array; }; -function generateTextDocumentContentChange(startLine: number, text: string): TextDocumentContentChangeEvent { +function generateTextDocumentContentChange(line: number, text: string): TextDocumentContentChangeEvent { return { rangeLength: 0, text: text, - range: new Range(new Position(startLine, 0), new Position(startLine, 0)) + range: new Range(new Position(line, 0), new Position(line, 0)) }; } function mutEditedLIneForDeletion(editedLine: TextDocumentContentChangeEvent[]): TextDocumentContentChangeEvent[] { - let newEditedLine: TextDocumentContentChangeEvent[] = []; let startLine = 0; let before = 0; @@ -57,7 +56,7 @@ function mutEditedLIneForDeletion(editedLine: TextDocumentContentChangeEvent[]): editedLine.forEach(line => { startLine = line.range.start.line + before; for (let i = line.range.start.line; i <= line.range.end.line; i++) { - newEditedLine.push(generateTextDocumentContentChange(startLine, line.text)); + newEditedLine.push(generateTextDocumentContentChange(i, line.text)); before--; } before++; @@ -75,9 +74,8 @@ function mutEditedLIne(editedLine: TextDocumentContentChangeEvent[]): TextDocume if (line.text === "\n") { newEditedLine.push(line); } else { - line.text.split(/\n/).forEach(text => { - newEditedLine.push(generateTextDocumentContentChange(startLine, line.text)); - startLine++; + line.text.split(/\n/).map(text => { + newEditedLine.push(generateTextDocumentContentChange(startLine++, line.text)); before++; }); before--; @@ -86,10 +84,40 @@ function mutEditedLIne(editedLine: TextDocumentContentChangeEvent[]): TextDocume return newEditedLine.reverse(); } +function updatePositionsForDeletion(range, positions) { + let rangeLength = range.end.line - range.start.line; + positions.forEach(position => { + if (position.newPosition === null) { + return; + } + if (position.oldPosition >= range.start.line && position.oldPosition < (range.end.line + 1)) { + position.newPosition = null; + return; + } + if (position.oldPosition >= range.end.line) { + position.newPosition = position.newPosition - rangeLength; + } + if (position.newPosition < 0) { + position.newPosition = 0; + } + }); + return positions; +} + +function handleLineRemoved(editedLine: TextDocumentContentChangeEvent[], positions) { + editedLine.reverse(); + editedLine.forEach((line) => { + positions = updatePositionsForDeletion(line.range, positions); + }); + editedLine.reverse(); + + return mutEditedLIneForDeletion(editedLine); +} + function handleLineDiff(editedLine: TextDocumentContentChangeEvent[], context, diffLine: number) { - let positions = mapToArray(context.deco).map(_ => Object({ - oldPosition: _, - newPosition: _ + let positions = mapKeysToArray(context.deco).map(position => Object({ + oldPosition: position, + newPosition: position })); if (diffLine < 0) { @@ -128,36 +156,6 @@ function handleLineAdded(editedLine: TextDocumentContentChangeEvent[], position) return editedLine; } -function updatePositionsForDeletion(range, positions) { - let rangeLength = range.end.line - range.start.line; - positions.forEach(position => { - if (position.newPosition === null) { - return; - } - if (position.oldPosition >= range.start.line && position.oldPosition < (range.end.line + 1)) { - position.newPosition = null; - return; - } - if (position.oldPosition >= range.end.line) { - position.newPosition = position.newPosition - rangeLength; - } - if (position.newPosition < 0) { - position.newPosition = 0; - } - }); - return positions; -} - -function handleLineRemoved(editedLine: TextDocumentContentChangeEvent[], positions) { - editedLine.reverse(); - editedLine.forEach((line) => { - positions = updatePositionsForDeletion(line.range, positions); - }); - editedLine.reverse(); - - return mutEditedLIneForDeletion(editedLine); -} - function updateDecorations(editedLine: TextDocumentContentChangeEvent[], context, cb: Function) { let diffLine = context.current_editor.document.lineCount - context.nbLine; @@ -168,32 +166,22 @@ function updateDecorations(editedLine: TextDocumentContentChangeEvent[], context checkDecorationForUpdate(editedLine, context, cb); } -function checkDecorationForUpdate(editedLine: TextDocumentContentChangeEvent[], context, cb: Function) { - editedLine.forEach((line: TextDocumentContentChangeEvent) => { - if (context.deco.has(line.range.start.line)) { - context.deco.get(line.range.start.line).forEach(decoration => { - decoration.dispose(); - }); - } - context.deco.set(line.range.start.line, []); +function checkDecorationForUpdate(editedLine: TextDocumentContentChangeEvent[], context, cb) { + Promise.all( + editedLine.map(({range}: TextDocumentContentChangeEvent) => { + if (context.deco.has(range.start.line)) { + context.deco.get(range.start.line).forEach(decoration => { + decoration.dispose(); + }); + } + context.deco.set(range.start.line, []); - let colors = ColorUtil.extractColor(context.current_editor.document.lineAt(line.range.start.line).text); - let decorations: ColorDecoration[] = []; - colors.forEach((color) => { - let startPos = new Position(line.range.start.line, color.positionInText); - let endPos = new Position(line.range.start.line, color.positionInText + color.value.length); + return ColorUtil.findColors(context.current_editor.document.lineAt(range.start.line).text) + .then(colors => generateDecorations(colors, range.start.line, context)) + .then(decorateEditor); + }) - let range = new Range(startPos, endPos); - let decoration = new ColorDecoration(range, color); - if (context.deco.has(startPos.line)) { - context.deco.set(startPos.line, context.deco.get(startPos.line).concat([decoration])); - } else { - context.deco.set(startPos.line, [decoration]); - } - context.current_editor.setDecorations(decoration.decoration, [range]); - }); - }); - cb(); + ).then(cb); } function initDecorations(context, cb) { @@ -202,66 +190,69 @@ function initDecorations(context, cb) { } context.nbLine = context.current_editor.document.lineCount; - let text = context.current_editor.document.getText(); //should read line by line instead - let colors = ColorUtil.extractColor(text); - let decorations = generateDecorations(colors, context); - updateEditorDecorationFromMap(context.deco, context.current_editor); - cb(); + let text = context.current_editor.document.getText(); // should read line by line instead or not? + let n: number = context.current_editor.document.lineCount; + + Promise.all(context.current_editor.document.getText().split(/\n/).map((text, index) => ColorUtil.findColors(text) + .then(colors => generateDecorations(colors, index, context)) + .then(decorateEditor))).then(cb); + } -function generateDecorations(colors: Color[], context): ColorDecoration[] { - let decorations: ColorDecoration[] = []; +function generateDecorations(colors: Color[], line, context) { colors.forEach((color) => { - let startPos = context.current_editor.document.positionAt(color.positionInText); - let endPos = context.current_editor.document.positionAt(color.positionInText + color.value.length); + let startPos = new Position(line, color.positionInText); + let endPos = new Position(line, color.positionInText + color.value.length); let range = new Range(startPos, endPos); - if (context.deco.has(startPos.line)) { - context.deco.set(startPos.line, context.deco.get(startPos.line).concat([new ColorDecoration(range, color)])); + if (context.deco.has(line)) { + context.deco.set(line, context.deco.get(line).concat([new ColorDecoration(range, color)])); } else { - context.deco.set(startPos.line, [new ColorDecoration(range, color)]); + context.deco.set(line, [new ColorDecoration(range, color)]); } }); - return decorations; + return context; } -function updateEditorDecorationFromMap(decorations: Map < number, ColorDecoration[] >, editor: TextEditor ) { - let it = decorations.entries(); - let tmp = it.next(); - while(!tmp.done) { - tmp.value[1].forEach(decoration => editor.setDecorations(decoration.decoration, [decoration.textPosition])) - tmp= it.next(); - } + +function decorateEditor(context) { + let it = context.deco.entries(); + let tmp = it.next(); + while (!tmp.done) { + tmp.value[1].forEach(decoration => context.current_editor.setDecorations(decoration.decoration, [decoration.textPosition])); + tmp = it.next(); + } + return; } export function activate(context: ExtensionContext) { - let decorations : Map < string, Map < number, ColorDecoration[] > > = new Map(); + let decorations: Map < string, Map < number, ColorDecoration[] > > = new Map(); let extension = { - current_editor: null, + current_editor: window.activeTextEditor, nbLine: 0, deco: null - } + }; let q = new Queue(); - let editor = window.activeTextEditor; - if (editor) { - extension.current_editor = editor, - extension.deco = new Map(); - decorations.set(editor.document.fileName, extension.deco); - q.push((cb)=> initDecorations(extension, cb)) + if (extension.current_editor) { + extension.deco = new Map(); + decorations.set(extension.current_editor.document.fileName, extension.deco); + q.push((cb) => { + initDecorations(extension, cb); + }); } window.onDidChangeActiveTextEditor(newEditor => { - extension.current_editor = newEditor + extension.current_editor = newEditor; if (newEditor && !decorations.has(newEditor.document.fileName)) { extension.deco = new Map(); } else { extension.deco = decorations.get(newEditor.document.fileName); } - return q.push((cb)=> initDecorations(extension, cb)) - + return q.push((cb) => initDecorations(extension, cb)); + }, null, context.subscriptions); workspace.onDidChangeTextDocument((event: TextDocumentChangeEvent) => { - if (editor && event.document === editor.document) { - q.push((cb)=> updateDecorations(event.contentChanges, extension, cb)) + if (extension.current_editor && event.document === extension.current_editor.document) { + q.push((cb) => updateDecorations(event.contentChanges, extension, cb)); } }, null, context.subscriptions); } diff --git a/src/queue.ts b/src/queue.ts index 5ca89e75..6d8a3e19 100644 --- a/src/queue.ts +++ b/src/queue.ts @@ -21,9 +21,6 @@ class Queue { if (action) { this._running = true; new Promise((resolve, reject) => action(resolve)).then(this._next.bind(this)); - // action(() => { - // this._next(); - // }); } } } diff --git a/test/color-regex.test.ts b/test/color-regex.test.ts index c60c60c1..54bb6eed 100644 --- a/test/color-regex.test.ts +++ b/test/color-regex.test.ts @@ -1,6 +1,6 @@ import { assert } from 'chai'; -import { HEXA_COLOR } from '../src/color-regex'; +import { HEXA_COLOR, RGB_COLOR} from '../src/color-regex'; // Defines a Mocha test suite to group tests of similar kind together describe("Test CSS hexa shorthand color Regex", () => { @@ -54,3 +54,20 @@ describe("Test CSS hexa color Regex", () => { assert.notOk('#fffff'.match(HEXA_COLOR)); }); }); +describe("Test rgb(a) color Regex", () => { + it('Should match a simple rgb color', function () { + assert.ok('rgb(123,123,123)'.match(RGB_COLOR)); + }); + it('Should match a simple rgba color', function () { + assert.ok('rgba(123,123,123, 0)'.match(RGB_COLOR)); + assert.ok('rgba(123,123,123, 0.3)'.match(RGB_COLOR)); + assert.ok('rgba(123,123,123, 1)'.match(RGB_COLOR)); + }); + + it('Should match with different characters at the end', function () { + assert.ok('rgb(123,123,123) '.match(RGB_COLOR)); + assert.ok('rgb(123,123,123),'.match(RGB_COLOR)); + assert.ok('rgb(123,123,123);'.match(RGB_COLOR)); + assert.ok('rgb(123,123,123)\n'.match(RGB_COLOR)); + }); +}); diff --git a/test/color-util.test.ts b/test/color-util.test.ts index 833edf61..dbe9d024 100644 --- a/test/color-util.test.ts +++ b/test/color-util.test.ts @@ -6,9 +6,18 @@ import ColorUtil from '../src/color-util'; import Color from '../src/color'; describe('Test utility fonction', () => { - it('Should return the rgb value of a color', () => { - assert.deepEqual(ColorUtil.getRGB(new Color('hexa', '#fff')), [255, 255, 255], 'Should return rgb values for CSS hexa shorthand color'); - assert.deepEqual(ColorUtil.getRGB(new Color('hexa', '#ffffff')), [255, 255, 255], 'Should return rgb values for CSS hexa color'); + it('Should not extract invalid colors from a text', (done) => { + ColorUtil.findColors('#ffg, rgb(323,123,123)').then(colors => { + assert.equal(0, colors.length, 'Should have found 0 colors'); + done(); + }); + }); + it('Should extract Colors from a text', (done) => { + ColorUtil.findColors('#fff, rgb(123,123,123), #dccdcd').then(colors => { + assert.equal(3, colors.length, 'Should have found 3 colors'); + assert.deepEqual(['hexa', 'hexa', 'rgb'], colors.map(color => color.model)); + done(); + }); }); it('Should return the color luminance', () => { assert.equal(ColorUtil.luminance(new Color('hexa', '#fff')), 1, 'Should be "1" for #fff');