Skip to content

Commit

Permalink
Added support for HTML and other variants
Browse files Browse the repository at this point in the history
Added support for HTML files if the style tag is withing a head tag
Added support for vue and svelt files if the style tag is in the root directory
refactored some typescript code
added check so that only angular components activate the plugin instead of all typescript files
updated readme to include the changes, as well as the TS changes

Note: parse5 was an added dependency for parsing HTML and providing source location information

resolves Raathigesh#14
resolves Raathigesh#10
work towards Raathigesh#20
  • Loading branch information
paustint committed Jun 1, 2019
1 parent a870847 commit e07ba3a
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 67 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ Fabulous supports the followings
- 💅 CSS-in-JS libs which supports template literal ([styled-components](https://github.com/styled-components/styled-components), [emotion](https://github.com/emotion-js/emotion), [linaria](https://github.com/callstack/linaria))
- 🎨 CSS rules from `.css` files
- 🌈 CSS rules from `.scss` files
- 🌏 CSS rules from `.html` files - these must be in a `<style>` tag and the style tag must be within the `<head>` tag
- 🌛 CSS rules from `component.ts` Angular component files that have inline styles within the ` @Component({ styles:[``] }) ` decorator
- 🌟 CSS rules from `.vue` files that have a `<style>` tag in the root of the file
- 💍 CSS rules from `.svelte` files that have a `<style>` tag in the root of the file

> Fabulous is still in Preview. Give it a try and [let us know](https://github.com/Raathigesh/fabulous/issues) when things don't go well.
## Getting started

- Install the [Fabulous](https://marketplace.visualstudio.com/items?itemName=Raathigeshan.fabulous) extension in VS Code
- After opening a `css`, `scss`, `js`, `jsx` or `tsx` file, click on the <img src="https://affectionate-booth-10a1f4.netlify.com/tiny-icon.png" width="20px" /> icon to toggle the side-bar
- After opening a `css`, `scss`, `js`, `jsx`, `tsx`, `component.ts`, `vue` or `svelte` file, click on the <img src="https://affectionate-booth-10a1f4.netlify.com/tiny-icon.png" width="20px" /> icon to toggle the side-bar
- Place your cursor in a CSS rule or in a Styled component template literal
- You should see the sidebar controls become active

Expand Down
22 changes: 21 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
],
"menus": {
"editor/title": [
{
"when": "resourceLangId == html",
"command": "fabulous.showPanel",
"alt": "fabulous.showPanel",
"group": "navigation"
},
{
"when": "resourceLangId == css",
"command": "fabulous.showPanel",
Expand All @@ -56,7 +62,7 @@
"group": "navigation"
},
{
"when": "resourceLangId == typescript",
"when": "resourceLangId == typescript && isAngularComponent",
"command": "fabulous.showPanel",
"alt": "fabulous.showPanel",
"group": "navigation"
Expand All @@ -78,6 +84,18 @@
"command": "fabulous.showPanel",
"alt": "fabulous.showPanel",
"group": "navigation"
},
{
"when": "resourceLangId == svelte",
"command": "fabulous.showPanel",
"alt": "fabulous.showPanel",
"group": "navigation"
},
{
"when": "resourceLangId == vue",
"command": "fabulous.showPanel",
"alt": "fabulous.showPanel",
"group": "navigation"
}
]
}
Expand All @@ -104,6 +122,7 @@
"@types/babel-traverse": "^6.25.5",
"@types/mocha": "^2.2.42",
"@types/node": "^10.12.21",
"@types/parse5": "^5.0.0",
"@types/react": "^16.8.14",
"@types/react-dom": "^16.8.4",
"@types/react-select": "^2.0.17",
Expand Down Expand Up @@ -135,6 +154,7 @@
},
"dependencies": {
"@babel/traverse": "^7.4.3",
"parse5": "^5.1.0",
"postcss": "^7.0.14",
"postcss-scss": "^2.0.0",
"rebass": "^3.1.0"
Expand Down
16 changes: 15 additions & 1 deletion src/extension/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { FileHandler, EditableBlock } from "./file-handlers/types";
import CSSFileInspector from "./file-handlers/css-file";
import StyledComponentsInspector from "./file-handlers/js";
import DecoratedClassComponentsInspector from "./file-handlers/ts";
import HtmlInspector from "./file-handlers/html";
import { isAngularComponentRegex } from "./file-handlers/utils";

export default class Manager {
private activeEditor: vscode.TextEditor | undefined;
Expand All @@ -18,7 +20,16 @@ export default class Manager {
const languageId = activeEditor
? activeEditor.document.languageId
: undefined;

if (
languageId === "html" ||
languageId === "svelte" ||
languageId === "vue"
) {
this.inspector = HtmlInspector;
this.activeEditor = activeEditor;
this.languageId = languageId;
} else if (
languageId === "css" ||
languageId === "scss" ||
languageId === "postcss"
Expand Down Expand Up @@ -60,13 +71,16 @@ export default class Manager {

isAcceptableLaguage(languageId: string) {
return (
languageId === "html" ||
languageId === "css" ||
languageId === "scss" ||
languageId === "postcss" ||
languageId === "javascript" ||
languageId === "typescript" ||
languageId === "javascriptreact" ||
languageId === "typescriptreact"
languageId === "typescriptreact" ||
languageId === "svelte" ||
languageId === "vue"
);
}

Expand Down
26 changes: 26 additions & 0 deletions src/extension/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,37 @@ import * as vscode from "vscode";
import ContentProvider from "./ContentProvider";
import { join } from "path";
import Manager from "./Manager";
import { isAngularComponentRegex } from "./file-handlers/utils";

export function activate(context: vscode.ExtensionContext) {
const contentProvider = new ContentProvider();
let currentPanel: vscode.WebviewPanel | undefined = undefined;

/**
* Set a custom context variable of "isAngularComponent" so that in package.json menus.editor/title "when" clause can distinguish
* Angular components from other typescript files so that the plugin can remain dormant for regular typescript files
*/
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(activeEditor => {
if (activeEditor) {
let isAngularComponent = false;
try {
isAngularComponent = isAngularComponentRegex.test(
activeEditor.document.uri.path
);
} catch (ex) {
console.log("Error checking isAngularComponent", ex);
} finally {
vscode.commands.executeCommand(
"setContext",
"isAngularComponent",
isAngularComponent
);
}
}
})
);

let disposable = vscode.commands.registerCommand("fabulous.showPanel", () => {
if (currentPanel) {
currentPanel.reveal(vscode.ViewColumn.Two);
Expand Down
110 changes: 110 additions & 0 deletions src/extension/file-handlers/html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as parse5 from "parse5";
import { EditableBlock, FileHandler, StyleExpressions } from "./types";
import {
getDeclarations,
getNodeSourceWithLocationOffset,
getRules,
removeProperty,
updateProperty
} from "./utils";
import console = require("console");

/**
* Parse a document for style tags
*
* The only two locations that tags are parsed:
* - At the root of the document, such as Vue SFC
* - A style tag within the <head> of a document
* Any other location will be ignored to ensure that the entire tree does not need to be walked
* @param document
*/
export function getStyleTags(
document: parse5.DocumentFragment
): StyleExpressions[] {
let results: StyleExpressions[] = [];
try {
// Get any root level style tags
let styleNodes: parse5.DefaultTreeTextNode[] = (document as any).childNodes
.filter((node: parse5.DefaultTreeNode) => node.nodeName === "style")
.map((styleNode: any) => styleNode.childNodes[0]);

try {
// Get any <style> tags that are within a <head> tag
if (
(document as any).childNodes[0].nodeName === "html" &&
(document as any).childNodes[0].childNodes[0].nodeName === "head"
) {
styleNodes = styleNodes.concat(
(document as any).childNodes[0].childNodes[0].childNodes
.filter((node: parse5.DefaultTreeNode) => node.nodeName === "style")
.map((styleNode: any) => styleNode.childNodes[0])
);
}
} catch (ex) {
// TODO: handle errors in some way (maybe just log to output so the user knows we had an error)
console.log("Error parsing file", ex);
}

// Convert format to StyleExpression
results = styleNodes.map(node => {
const loc = node.sourceCodeLocation as parse5.Location;
return {
name: node.nodeName,
cssString: node.value,
location: {
input: null as any,
start: {
column: loc.startCol,
line: loc.startLine - 1
},
end: {
column: loc.endCol,
line: loc.endLine - 1
}
}
} as StyleExpressions;
});
} catch (ex) {
// TODO: handle errors in some way (maybe just log to output so the user knows we had an error)
console.log("Error parsing file", ex);
}
return results;
}

export function getEditableBlocks(content: string) {
// Parse HTML document as a fragment to ensure extra tags are not added by parse5
const document: parse5.DocumentFragment = parse5.parseFragment(content, {
sourceCodeLocationInfo: true
}) as parse5.DefaultTreeDocument;
// Search document for style tags
const styledBlocks = getStyleTags(document);

const results: EditableBlock[] = [];

// parse the CSS from each style tag and add to EditableBlock[]
styledBlocks.forEach(({ cssString, location }) => {
getRules(cssString).forEach(rule => {
const declarations = getDeclarations(rule);
results.push({
selector: rule.selector,
declarations,
source: getNodeSourceWithLocationOffset(location, rule),
rule
});
});
});
return results;
}

const HtmlInspector: FileHandler = {
getEditableBlocks(fileContent: string, languageId?: string) {
return getEditableBlocks(fileContent);
},
updateProperty(activeBlock: EditableBlock, prop: string, value: string) {
return updateProperty(activeBlock.rule, prop, value);
},
removeProperty(activeBlock: EditableBlock, prop: string) {
return removeProperty(activeBlock.rule, prop);
}
};
export default HtmlInspector;
1 change: 1 addition & 0 deletions src/extension/file-handlers/js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function getTaggedTemplateExpressionStrings(ast: any) {
return results;
}

// FIXME: IS THIS USED? NO REFERENCE IN CODEBASE, WE SHOULD REMOVE THIS
export function updateCSSProperty(
content: string,
name: string,
Expand Down
78 changes: 15 additions & 63 deletions src/extension/file-handlers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@ import {
updateProperty,
getDeclarations,
getRules,
removeProperty
removeProperty,
getNodeSourceWithLocationOffset
} from "./utils";
import { FileHandler, EditableBlock, StyleExpressions } from "./types";
import console = require("console");

/**
* Traverse AST and listed for the ClassDeclaration event to fire
*
* Example:
* @Component()
* class Foo { }
*
* This is used for Angular components that have inline styles
*
* @param ast babel AST
*/
export function getClassDeclarationStrings(ast: any) {
const results: StyleExpressions[] = [];
traverse(ast, {
Expand Down Expand Up @@ -38,45 +49,13 @@ export function getClassDeclarationStrings(ast: any) {
});
}
} catch (ex) {
// ignore?
console.log("Error", ex);
// TODO: do something with exception
}
}
});
return results;
}

export function updateCSSProperty(
content: string,
name: string,
property: string,
value: string,
languageId: string
) {
const ast = parse(content, languageId);
let updatedCssString = "";

traverse(ast, {
ClassDeclaration(path: any) {
try {
if (path.node.id.name) {
const stylesNode = path.node.decorators[0].expression.arguments[0].properties.find(
(prop: any) => prop.key.name === "styles"
);
if (stylesNode && stylesNode.value.elements.length > 0) {
const cssString = stylesNode.value.elements[0].quasis[0].value.raw;
updatedCssString = updateProperty(cssString, property, value);
stylesNode.value.elements[0].quasis[0].value.raw = updatedCssString;
}
}
} catch (ex) {
// ignore?
console.log("Error", ex);
}
}
});
}

export function getEditableBlocks(content: string, languageId: string) {
const ast = parse(content, languageId);
const styledBlocks = getClassDeclarationStrings(ast);
Expand All @@ -87,37 +66,10 @@ export function getEditableBlocks(content: string, languageId: string) {
getRules(cssString).forEach(rule => {
const declarations = getDeclarations(rule);

// Get accurate overall locations based on total document and rule within styles string
const locStart = location.start ? location.start : { column: 0, line: 0 };
const sourceStart = (rule.source && rule.source.start) || {
column: 0,
line: 0
};
const sourceEnd = (rule.source && rule.source.end) || {
column: 0,
line: 0
};

const startLine = locStart.line + sourceStart.line - 1;
const endLine = startLine + sourceEnd.line - sourceStart.line;
// If ` is on the same line as the CSS tag, then the start column should be the actual column
let startColumn =
sourceStart.line === 1 ? locStart.column : sourceStart.column - 1;

results.push({
selector: rule.selector,
declarations,
source: {
start: {
column: startColumn,
line: startLine
},
end: {
column: sourceEnd.column,
line: endLine
},
input: (rule.source as any).input
},
source: getNodeSourceWithLocationOffset(location, rule),
rule
});
});
Expand Down
5 changes: 5 additions & 0 deletions src/extension/file-handlers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,8 @@ export interface StyleExpressions {
cssString: string;
location: NodeSource;
}

export interface LocationPosition {
column: 0;
line: 0;
}
Loading

0 comments on commit e07ba3a

Please sign in to comment.