Skip to content

Commit 28a36da

Browse files
committed
fix/style: Fix positions of styles in sources, refactor typescript parsing, update jsdocs globally
1 parent 2d45d20 commit 28a36da

File tree

10 files changed

+283
-220
lines changed

10 files changed

+283
-220
lines changed

src/parse-typescript.js

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

src/parse.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1-
import { Document } from "postcss";
2-
import { Input } from "postcss";
1+
import { Document, Input } from "postcss";
32
import { AngularParser } from "./parser-angular.js";
43

54
/**
6-
* Parses Angular code and returns a Document.
5+
* Represents an object that can be converted to a string.
76
*
7+
* @typedef {object} Stringifiable
8+
* @property {function(): string} toString - Method to convert the object to a string.
9+
*/
10+
11+
/**
12+
* Parses Angular code and generates a PostCSS Document representing the Abstract Syntax Tree (AST).
13+
*
14+
* @param {string | Stringifiable} angularCode - The Angular source code to be parsed, provided either as a string or as an object implementing the toString() method.
15+
* @param {import('postcss').ProcessOptions} opts - The PostCSS processing options.
16+
* @returns {Document} A PostCSS Document object representing the parsed AST.
817
* @type {import('postcss').Parser<Document>}
918
*/
1019
export default function parse(angularCode, opts) {

src/parser-angular.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { Document, Input } from "postcss";
2-
import { parseTypescript } from "./parse-typescript.js";
2+
import { TypescriptParser } from "./parser-typescript.js";
33
import { StyleParser } from "./parser-styles.js";
44

5+
/**
6+
* Represents a parser that processes Angular component styles within TypeScript files.
7+
*/
58
export class AngularParser {
69
/**
7-
* Initializes the parser
10+
* Constructs a new instance of the AngularParser.
811
*
9-
* @param {Input} input The PostCSS wrapper around a file
12+
* @param {Input} input - The PostCSS `Input` object wrapping the source file.
1013
*/
1114
constructor(input) {
1215
this.input = input;
@@ -16,12 +19,14 @@ export class AngularParser {
1619
}
1720

1821
/**
19-
* Sleeper parser activation phrase ;)
22+
* Parses the input TypeScript file to extract and process Angular component styles.
2023
*
21-
* @param {Pick<import("postcss").ProcessOptions, "from" | "map">} opts PostCSS process options
24+
* @param {Pick<import("postcss").ProcessOptions, "from" | "map">} opts - The PostCSS processing options.
25+
* @returns {Document} The root node of the generated Abstract Syntax Tree (AST).
2226
*/
2327
parse(opts) {
24-
const styleLiterals = parseTypescript(this.input.css, opts && opts.from);
28+
const typescriptParser = new TypescriptParser(this.input);
29+
const styleLiterals = typescriptParser.parse();
2530

2631
let index = 0;
2732
styleLiterals.forEach((styleLiteral, idx) => {
@@ -43,5 +48,7 @@ export class AngularParser {
4348
index = end + 1;
4449
this.doc.push(stylesRoot);
4550
});
51+
52+
return this.doc;
4653
}
4754
}

src/parser-styles.js

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,65 @@
11
import postcss from "postcss";
2+
23
/**
3-
* Class to adjust node locations within the source code.
4+
* Represents a parser that adjusts the positions of nodes within the source code.
45
*/
56
export class StyleParser {
67
/**
7-
* Constructor
8+
* Constructs a new instance of the StyleParser class.
89
*
9-
* @param {postcss.Input} input - An instance of the Locations class.
10-
* @param {import("ts-morph").StringLiteral | import("ts-morph").NoSubstitutionTemplateLiteral} styleLiteral - The style literal node.
10+
* @param {postcss.Input} input - The input source containing the style information.
11+
* @param {import("ts-morph").StringLiteral | import("ts-morph").NoSubstitutionTemplateLiteral} styleLiteral - The style literal node to be processed.
1112
*/
1213
constructor(input, styleLiteral) {
1314
this.input = input;
14-
15-
const { col, line } = input.fromOffset(styleLiteral.getStart());
16-
this.column = col - 1;
17-
this.line = line - 1;
1815
this.styleLiteral = styleLiteral;
1916
}
2017

21-
_adjustPosition(object) {
22-
if (object) {
23-
if (object.line === 1) {
24-
object.column += this.column;
25-
}
26-
object.line += this.line;
27-
if (typeof object.offset === "number") {
28-
object.offset += this.styleLiteral.getStart();
29-
}
30-
if (typeof object.endLine === "number") {
31-
if (object.endLine === 1) {
32-
object.endColumn += this.column;
33-
}
34-
object.endLine += this.line;
35-
}
18+
/**
19+
* Modifies a position object to be relative to the input file instead of the parsed CSS.
20+
*
21+
* @param {postcss.Position} position - The position object that requires adjustment.
22+
*/
23+
_adjustPosition(position) {
24+
const { col, line } = this.input.fromOffset(this.styleLiteral.getStart());
25+
26+
if (position.line === 1) {
27+
position.column += col;
3628
}
29+
position.line += line - 1;
30+
position.offset += this.styleLiteral.getStart() + 1;
3731
}
3832

33+
/**
34+
* Adjusts the positional data of a specific AST node to align with the input file's context.
35+
*
36+
* @param {postcss.AnyNode} node - The AST node whose position needs to be adjusted.
37+
*/
3938
_adjustNode(node) {
4039
this._adjustPosition(node.source.start);
4140
this._adjustPosition(node.source.end);
41+
node.source.end.column -= 1;
42+
node.source.end.offset -= 1;
4243
}
4344

4445
/**
45-
* Adjusts the position of source to be relative to the entire file not input css
46+
* Updates the positions of all nodes within the provided root AST to correspond with the input file.
4647
*
47-
* @param {postcss.Root} root The root of all styles to be adjusted
48+
* @param {postcss.Root} root - The root node of the stylesheet AST that requires adjustment.
4849
*/
4950
_adjustAllNodes(root) {
5051
this._adjustNode(root);
51-
root.source = {
52-
...root.source,
53-
input: this.input,
54-
};
55-
5652
root.walk((node) => {
5753
this._adjustNode(node);
5854
});
5955
}
6056

57+
/**
58+
* Modifies error positions to reflect the context of the input file accurately.
59+
*
60+
* @param {any} error - The error object that needs positional adjustment.
61+
* @returns {any} The error object with updated position information.
62+
*/
6163
_adjustError(error) {
6264
if (error && error.name === "CssSyntaxError") {
6365
this._adjustPosition(error);
@@ -70,19 +72,28 @@ export class StyleParser {
7072
return error;
7173
}
7274

75+
/**
76+
* Parses the style literal and generates the corresponding Abstract Syntax Tree (AST).
77+
*
78+
* @param {Pick<import("postcss").ProcessOptions, "from" | "map">} opts - The PostCSS processing options.
79+
* @returns {postcss.Root} The root node of the generated AST.
80+
* @throws Will throw an error if parsing fails, with adjusted error positions.
81+
*/
7382
parse(opts) {
7483
const styleLiteral = this.styleLiteral;
7584
let root;
7685
try {
77-
root = postcss.parse(
78-
styleLiteral.getLiteralText(),
79-
Object.assign({}, opts, { map: false }),
80-
);
86+
root = postcss.parse(styleLiteral.getLiteralText(), opts);
8187
} catch (error) {
8288
this._adjustError(error);
8389
throw error;
8490
}
91+
8592
this._adjustAllNodes(root);
93+
root.source = {
94+
...root.source,
95+
input: this.input,
96+
};
8697

8798
return root;
8899
}

src/parser-typescript.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Project, SyntaxKind } from "ts-morph";
2+
3+
/**
4+
* @typedef {import("ts-morph").StringLiteral | import("ts-morph").NoSubstitutionTemplateLiteral} StyleLiteral
5+
*/
6+
7+
export class TypescriptParser {
8+
/**
9+
* Creates an instance of TypescriptParser.
10+
*
11+
* @param {import("postcss").Input} input - The PostCSS input containing the source CSS and its originating file.
12+
*/
13+
constructor(input) {
14+
const project = new Project({
15+
useInMemoryFileSystem: true,
16+
});
17+
18+
this.sourceFile = project.createSourceFile(input.from, input.css);
19+
}
20+
21+
/**
22+
* Parses the TypeScript source file to extract style literals from component decorators.
23+
*
24+
* @returns {StyleLiteral[]} An array of style literals extracted from the components within the source file.
25+
*/
26+
parse() {
27+
return this.sourceFile
28+
.getClasses()
29+
.flatMap((cls) => cls.getDecorators())
30+
.filter(isComponentDecorator)
31+
.flatMap((decorator) => decorator.getArguments())
32+
.filter(isObjectLiteral)
33+
.flatMap(getStylesProperty)
34+
.flatMap(getStyleValues)
35+
.map(castToStyleLiteral);
36+
}
37+
}
38+
39+
/**
40+
* Determines whether a given decorator is a Component decorator.
41+
*
42+
* @param {import("ts-morph").Decorator} decorator - The decorator to evaluate.
43+
* @returns {boolean} `true` if the decorator is named "Component"; otherwise, `false`.
44+
*/
45+
function isComponentDecorator(decorator) {
46+
return decorator.getName() === "Component";
47+
}
48+
49+
/**
50+
* Checks if the provided expression is an object literal.
51+
*
52+
* @param {import("ts-morph").Expression} expr - The expression to check.
53+
* @returns {boolean} `true` if the expression is an ObjectLiteralExpression; otherwise, `false`.
54+
*/
55+
function isObjectLiteral(expr) {
56+
return expr.getKind() === SyntaxKind.ObjectLiteralExpression;
57+
}
58+
59+
/**
60+
* Extracts the 'styles' property from an ObjectLiteralExpression.
61+
*
62+
* @param {import("ts-morph").ObjectLiteralExpression} obj - The object literal representing component metadata.
63+
* @returns {import("ts-morph").Expression[]} An array containing the initializer of the 'styles' property, if it exists.
64+
*/
65+
function getStylesProperty(obj) {
66+
const prop = obj.getProperty("styles");
67+
return prop && prop.getKind() === SyntaxKind.PropertyAssignment
68+
? [
69+
prop
70+
.asKindOrThrow(SyntaxKind.PropertyAssignment)
71+
.getInitializerOrThrow(),
72+
]
73+
: [];
74+
}
75+
76+
/**
77+
* Retrieves the style values from an expression, handling both single expressions and array literals.
78+
*
79+
* @param {import("ts-morph").Expression} expr - The initializer expression of the 'styles' property.
80+
* @returns {import("ts-morph").Expression[]} An array of expressions representing individual style literals.
81+
*/
82+
function getStyleValues(expr) {
83+
if (expr.getKind() === SyntaxKind.ArrayLiteralExpression) {
84+
return expr.asKindOrThrow(SyntaxKind.ArrayLiteralExpression).getElements();
85+
}
86+
return [expr];
87+
}
88+
89+
/**
90+
* Converts an expression to a StyleLiteral type, ensuring it is a supported literal.
91+
*
92+
* @param {import("ts-morph").Expression} expr - The expression to cast.
93+
* @returns {StyleLiteral} The expression cast to a StyleLiteral.
94+
* @throws {Error} If the expression is not a StringLiteral or NoSubstitutionTemplateLiteral.
95+
*/
96+
function castToStyleLiteral(expr) {
97+
if (expr.getKind() === SyntaxKind.StringLiteral) {
98+
return expr.asKindOrThrow(SyntaxKind.StringLiteral);
99+
} else if (expr.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral) {
100+
return expr.asKindOrThrow(SyntaxKind.NoSubstitutionTemplateLiteral);
101+
}
102+
throw new Error("Unsupported style literal type.");
103+
}

0 commit comments

Comments
 (0)