stylpp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * STYL++ CSS Generator
3
+ * Converts the transformed AST into valid CSS output
4
+ * Supports minification, source maps, and vendor prefixes
5
+ */
6
+
7
+ class StylppGenerator {
8
+ constructor(options = {}) {
9
+ this.options = {
10
+ minify: false,
11
+ sourceMap: false,
12
+ vendorPrefix: true,
13
+ ...options
14
+ };
15
+ this.css = '';
16
+ this.sourceMap = { version: 3, sources: [], mappings: '' };
17
+ }
18
+
19
+ /**
20
+ * Generate CSS from AST
21
+ * @param {Object} ast - Transformed AST
22
+ * @returns {string} CSS code
23
+ */
24
+ generate(ast) {
25
+ this.css = '';
26
+ this.generateRules(ast.rules, 0);
27
+
28
+ if (this.options.minify) {
29
+ this.css = this.minifyCSS(this.css);
30
+ }
31
+
32
+ return this.css;
33
+ }
34
+
35
+ /**
36
+ * Generate rules recursively
37
+ * @param {Array} rules - Rules array
38
+ * @param {number} indent - Indentation level
39
+ */
40
+ generateRules(rules, indent = 0) {
41
+ const indentStr = this.options.minify ? '' : ' '.repeat(indent);
42
+ const newline = this.options.minify ? '' : '\n';
43
+
44
+ rules.forEach(rule => {
45
+ if (rule.type === 'rule') {
46
+ this.css += `${indentStr}${rule.selector} {${newline}`;
47
+
48
+ // Add declarations
49
+ if (rule.declarations && rule.declarations.length > 0) {
50
+ rule.declarations.forEach(decl => {
51
+ const property = this.normalizeProperty(decl.property);
52
+ const value = this.normalizeValue(decl.value);
53
+ this.css += `${indentStr} ${property}: ${value};${newline}`;
54
+
55
+ // Add vendor prefixes if needed
56
+ if (this.options.vendorPrefix) {
57
+ const prefixed = this.getVendorPrefixes(property, value);
58
+ prefixed.forEach(p => {
59
+ this.css += `${indentStr} ${p.property}: ${p.value};${newline}`;
60
+ });
61
+ }
62
+ });
63
+ }
64
+
65
+ // Add nested rules
66
+ if (rule.rules && rule.rules.length > 0) {
67
+ this.generateRules(rule.rules, indent + 1);
68
+ }
69
+
70
+ this.css += `${indentStr}}${newline}`;
71
+ }
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Normalize CSS property names (handle hyphenation)
77
+ * @param {string} property - Property name
78
+ * @returns {string} Normalized property
79
+ */
80
+ normalizeProperty(property) {
81
+ // Convert camelCase to kebab-case
82
+ return property.replace(/([A-Z])/g, '-$1').toLowerCase();
83
+ }
84
+
85
+ /**
86
+ * Normalize CSS values
87
+ * @param {string} value - Property value
88
+ * @returns {string} Normalized value
89
+ */
90
+ normalizeValue(value) {
91
+ // Remove extra whitespace
92
+ value = value.replace(/\s+/g, ' ').trim();
93
+
94
+ // Ensure calc() is properly formatted
95
+ if (value.includes('calc(')) {
96
+ value = value.replace(/calc\(\s+/g, 'calc(').replace(/\s+\)/g, ')');
97
+ }
98
+
99
+ return value;
100
+ }
101
+
102
+ /**
103
+ * Get vendor prefixes for a property
104
+ * @param {string} property - CSS property
105
+ * @param {string} value - CSS value
106
+ * @returns {Array} Vendor-prefixed versions
107
+ */
108
+ getVendorPrefixes(property, value) {
109
+ const prefixes = [];
110
+ const prefixMap = {
111
+ 'transform': ['-webkit-transform', '-moz-transform', '-o-transform'],
112
+ 'transition': ['-webkit-transition', '-moz-transition', '-o-transition'],
113
+ 'box-shadow': ['-webkit-box-shadow'],
114
+ 'border-radius': ['-webkit-border-radius'],
115
+ 'gradient': ['-webkit-linear-gradient', '-moz-linear-gradient'],
116
+ 'appearance': ['-webkit-appearance', '-moz-appearance'],
117
+ 'user-select': ['-webkit-user-select', '-moz-user-select', '-ms-user-select']
118
+ };
119
+
120
+ Object.keys(prefixMap).forEach(key => {
121
+ if (property.includes(key)) {
122
+ prefixMap[key].forEach(prefixed => {
123
+ prefixes.push({ property: prefixed, value: value });
124
+ });
125
+ }
126
+ });
127
+
128
+ return prefixes;
129
+ }
130
+
131
+ /**
132
+ * Minify CSS
133
+ * @param {string} css - CSS code
134
+ * @returns {string} Minified CSS
135
+ */
136
+ minifyCSS(css) {
137
+ return css
138
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
139
+ .replace(/\s+{/g, '{') // Remove spaces before {
140
+ .replace(/\s+}/g, '}') // Remove spaces before }
141
+ .replace(/;\s+/g, ';') // Remove spaces after ;
142
+ .replace(/:\s+/g, ':') // Remove spaces after :
143
+ .replace(/,\s+/g, ',') // Remove spaces after ,
144
+ .replace(/\n/g, '') // Remove newlines
145
+ .trim();
146
+ }
147
+
148
+ /**
149
+ * Generate source map
150
+ * @returns {Object} Source map object
151
+ */
152
+ generateSourceMap() {
153
+ return this.sourceMap;
154
+ }
155
+ }
156
+
157
+ module.exports = StylppGenerator;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * STYL++ Main Compiler
3
+ * Orchestrates parsing, transformation, and code generation
4
+ */
5
+
6
+ const StylppParser = require('./parser');
7
+ const StylppTransformer = require('./transformer');
8
+ const StylppGenerator = require('./generator');
9
+
10
+ class StylppCompiler {
11
+ constructor(options = {}) {
12
+ this.options = {
13
+ minify: false,
14
+ sourceMap: false,
15
+ vendorPrefix: true,
16
+ ...options
17
+ };
18
+
19
+ this.parser = new StylppParser();
20
+ this.transformer = new StylppTransformer();
21
+ this.generator = new StylppGenerator(this.options);
22
+ }
23
+
24
+ /**
25
+ * Compile STYL++ code to CSS
26
+ * @param {string} code - STYL++ source code
27
+ * @param {string} filename - Source filename (for error reporting)
28
+ * @returns {Object} Compilation result
29
+ */
30
+ compile(code, filename = 'input.stylpp') {
31
+ try {
32
+ // Phase 1: Parse
33
+ const ast = this.parser.parse(code);
34
+
35
+ if (this.parser.errors.length > 0) {
36
+ return {
37
+ success: false,
38
+ css: '',
39
+ ast: null,
40
+ errors: this.parser.errors,
41
+ filename
42
+ };
43
+ }
44
+
45
+ // Phase 2: Transform
46
+ const transformedAst = this.transformer.transform(ast);
47
+
48
+ if (this.transformer.errors.length > 0) {
49
+ return {
50
+ success: false,
51
+ css: '',
52
+ ast: ast,
53
+ transformedAst: null,
54
+ errors: this.transformer.errors,
55
+ filename
56
+ };
57
+ }
58
+
59
+ // Phase 3: Generate
60
+ const css = this.generator.generate(transformedAst);
61
+
62
+ // Optional: Generate source map
63
+ let sourceMap = null;
64
+ if (this.options.sourceMap) {
65
+ sourceMap = this.generator.generateSourceMap();
66
+ }
67
+
68
+ return {
69
+ success: true,
70
+ css: css,
71
+ ast: ast,
72
+ transformedAst: transformedAst,
73
+ sourceMap: sourceMap,
74
+ errors: [],
75
+ filename
76
+ };
77
+
78
+ } catch (error) {
79
+ return {
80
+ success: false,
81
+ css: '',
82
+ ast: null,
83
+ transformedAst: null,
84
+ errors: [{
85
+ type: 'CompileError',
86
+ message: error.message,
87
+ stack: error.stack,
88
+ filename
89
+ }],
90
+ filename
91
+ };
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Check if code is valid without compiling
97
+ * @param {string} code - STYL++ source code
98
+ * @returns {Object} Validation result
99
+ */
100
+ validate(code) {
101
+ try {
102
+ const ast = this.parser.parse(code);
103
+ return {
104
+ valid: true,
105
+ errors: []
106
+ };
107
+ } catch (error) {
108
+ return {
109
+ valid: false,
110
+ errors: [{
111
+ message: error.message,
112
+ line: error.line
113
+ }]
114
+ };
115
+ }
116
+ }
117
+ }
118
+
119
+ module.exports = StylppCompiler;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * STYL++ Parser
3
+ * Converts STYL++ source code into an Abstract Syntax Tree (AST)
4
+ * Handles semicolon-based indentation and property-value parsing
5
+ */
6
+
7
+ class StylppParser {
8
+ constructor(options = {}) {
9
+ this.options = options;
10
+ this.errors = [];
11
+ }
12
+
13
+ /**
14
+ * Parse STYL++ code and return AST
15
+ * @param {string} code - STYL++ source code
16
+ * @returns {Object} AST
17
+ */
18
+ parse(code) {
19
+ const lines = code.split('\n');
20
+ const ast = {
21
+ type: 'stylesheet',
22
+ rules: [],
23
+ variables: {}
24
+ };
25
+
26
+ let currentIndent = 0;
27
+ const stack = [];
28
+
29
+ for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
30
+ const line = lines[lineNumber];
31
+ const trimmed = line.trim();
32
+
33
+ // Skip empty lines
34
+ if (!trimmed) continue;
35
+
36
+ // Skip comment lines (start with ; at beginning)
37
+ if (trimmed.startsWith(';') && !trimmed.match(/^\s*;\s*\w+/)) continue;
38
+
39
+ // Count leading semicolons for indentation
40
+ const indentMatch = line.match(/^(;*)/);
41
+ const indentLevel = indentMatch ? indentMatch[0].length : 0;
42
+ const content = line.substring(indentLevel).trim();
43
+
44
+ // Skip empty content
45
+ if (!content) continue;
46
+
47
+ // Handle variables section
48
+ if (content === 'variables;' || content === 'variables') {
49
+ const variablesSection = this.parseVariablesSection(lines, lineNumber + 1);
50
+ ast.variables = variablesSection.variables;
51
+ lineNumber = variablesSection.endLine;
52
+ continue;
53
+ }
54
+
55
+ // Handle property-value pairs
56
+ if (content.endsWith(';') && indentLevel > 0) {
57
+ const propValue = this.parseProperty(content);
58
+ if (propValue && stack.length > 0) {
59
+ const parent = stack[stack.length - 1];
60
+ if (!parent.declarations) parent.declarations = [];
61
+ parent.declarations.push(propValue);
62
+ }
63
+ continue;
64
+ }
65
+
66
+ // Handle rules/selectors
67
+ const selector = content.replace(/;$/, '');
68
+ const newRule = {
69
+ type: 'rule',
70
+ selector: selector,
71
+ declarations: [],
72
+ rules: []
73
+ };
74
+
75
+ // Manage indentation stack
76
+ if (indentLevel > currentIndent) {
77
+ // Nested rule
78
+ if (stack.length > 0) {
79
+ const parent = stack[stack.length - 1];
80
+ parent.rules.push(newRule);
81
+ }
82
+ stack.push(newRule);
83
+ } else if (indentLevel < currentIndent) {
84
+ // Pop stack until we reach the right level
85
+ const popCount = (currentIndent - indentLevel);
86
+ for (let i = 0; i < popCount; i++) {
87
+ stack.pop();
88
+ }
89
+ if (stack.length > 0) {
90
+ const parent = stack[stack.length - 1];
91
+ parent.rules.push(newRule);
92
+ } else {
93
+ ast.rules.push(newRule);
94
+ }
95
+ stack.push(newRule);
96
+ } else {
97
+ // Same level
98
+ if (stack.length > 0) {
99
+ stack.pop();
100
+ if (stack.length > 0) {
101
+ const parent = stack[stack.length - 1];
102
+ parent.rules.push(newRule);
103
+ } else {
104
+ ast.rules.push(newRule);
105
+ }
106
+ } else {
107
+ ast.rules.push(newRule);
108
+ }
109
+ stack.push(newRule);
110
+ }
111
+
112
+ currentIndent = indentLevel;
113
+ }
114
+
115
+ return ast;
116
+ }
117
+
118
+ /**
119
+ * Parse variables section
120
+ * @param {Array} lines - All code lines
121
+ * @param {number} startLine - Starting line number
122
+ * @returns {Object} Variables and end line
123
+ */
124
+ parseVariablesSection(lines, startLine) {
125
+ const variables = {};
126
+ let endLine = startLine;
127
+
128
+ for (let i = startLine; i < lines.length; i++) {
129
+ const line = lines[i];
130
+ const trimmed = line.trim();
131
+
132
+ if (!trimmed || trimmed.startsWith(';')) continue;
133
+
134
+ // Check if we've exited the variables section
135
+ const leadingSemicolons = line.match(/^(;*)/)[0].length;
136
+ if (leadingSemicolons === 0 && trimmed && !trimmed.match(/^\w+\s+/)) {
137
+ break;
138
+ }
139
+
140
+ const parts = trimmed.split(/\s+/);
141
+ if (parts.length >= 2) {
142
+ const name = parts[0];
143
+ const value = parts.slice(1).join(' ').replace(/;$/, '');
144
+ variables[name] = value;
145
+ endLine = i;
146
+ } else {
147
+ break;
148
+ }
149
+ }
150
+
151
+ return { variables, endLine };
152
+ }
153
+
154
+ /**
155
+ * Parse a property-value pair
156
+ * @param {string} content - Property-value string (e.g., "background white;")
157
+ * @returns {Object} Declaration object
158
+ */
159
+ parseProperty(content) {
160
+ // Remove trailing semicolon
161
+ const cleaned = content.replace(/;$/, '').trim();
162
+
163
+ // Split by first space to separate property from value
164
+ const spaceIndex = cleaned.indexOf(' ');
165
+ if (spaceIndex === -1) return null;
166
+
167
+ const property = cleaned.substring(0, spaceIndex);
168
+ const value = cleaned.substring(spaceIndex + 1);
169
+
170
+ return {
171
+ type: 'declaration',
172
+ property: property,
173
+ value: value
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Parse for loop syntax: for i in 1 to 5;
179
+ * @param {string} content - For loop declaration
180
+ * @returns {Object} Loop object or null
181
+ */
182
+ parseForLoop(content) {
183
+ const match = content.match(/for\s+(\w+)\s+in\s+([\d\w]+)\s+to\s+([\d\w]+)/);
184
+ if (!match) return null;
185
+
186
+ return {
187
+ type: 'for-loop',
188
+ variable: match[1],
189
+ from: parseInt(match[2]),
190
+ to: parseInt(match[3])
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Parse if/conditional syntax: if condition;
196
+ * @param {string} content - If statement
197
+ * @returns {Object} Conditional object or null
198
+ */
199
+ parseConditional(content) {
200
+ const match = content.match(/@(\w+)/);
201
+ if (!match) return null;
202
+
203
+ return {
204
+ type: 'conditional',
205
+ condition: match[1]
206
+ };
207
+ }
208
+ }
209
+
210
+ module.exports = StylppParser;