@dialpad/stylelint-plugin-dialtone 1.3.1-next.1 → 1.4.0-next.2
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,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
// Physical to logical property mappings
|
|
7
|
+
const PROPERTY_MAP = {
|
|
8
|
+
// Margins
|
|
9
|
+
'margin-left': 'margin-inline-start',
|
|
10
|
+
'margin-right': 'margin-inline-end',
|
|
11
|
+
'margin-top': 'margin-block-start',
|
|
12
|
+
'margin-bottom': 'margin-block-end',
|
|
13
|
+
|
|
14
|
+
// Padding
|
|
15
|
+
'padding-left': 'padding-inline-start',
|
|
16
|
+
'padding-right': 'padding-inline-end',
|
|
17
|
+
'padding-top': 'padding-block-start',
|
|
18
|
+
'padding-bottom': 'padding-block-end',
|
|
19
|
+
|
|
20
|
+
// Positioning (inset)
|
|
21
|
+
'left': 'inset-inline-start',
|
|
22
|
+
'right': 'inset-inline-end',
|
|
23
|
+
'top': 'inset-block-start',
|
|
24
|
+
'bottom': 'inset-block-end',
|
|
25
|
+
|
|
26
|
+
// Border
|
|
27
|
+
'border-left': 'border-inline-start',
|
|
28
|
+
'border-right': 'border-inline-end',
|
|
29
|
+
'border-top': 'border-block-start',
|
|
30
|
+
'border-bottom': 'border-block-end',
|
|
31
|
+
'border-left-width': 'border-inline-start-width',
|
|
32
|
+
'border-right-width': 'border-inline-end-width',
|
|
33
|
+
'border-top-width': 'border-block-start-width',
|
|
34
|
+
'border-bottom-width': 'border-block-end-width',
|
|
35
|
+
'border-left-style': 'border-inline-start-style',
|
|
36
|
+
'border-right-style': 'border-inline-end-style',
|
|
37
|
+
'border-top-style': 'border-block-start-style',
|
|
38
|
+
'border-bottom-style': 'border-block-end-style',
|
|
39
|
+
'border-left-color': 'border-inline-start-color',
|
|
40
|
+
'border-right-color': 'border-inline-end-color',
|
|
41
|
+
'border-top-color': 'border-block-start-color',
|
|
42
|
+
'border-bottom-color': 'border-block-end-color',
|
|
43
|
+
|
|
44
|
+
// Border radius
|
|
45
|
+
'border-top-left-radius': 'border-start-start-radius',
|
|
46
|
+
'border-top-right-radius': 'border-start-end-radius',
|
|
47
|
+
'border-bottom-left-radius': 'border-end-start-radius',
|
|
48
|
+
'border-bottom-right-radius': 'border-end-end-radius',
|
|
49
|
+
|
|
50
|
+
// Sizing
|
|
51
|
+
'width': 'inline-size',
|
|
52
|
+
'height': 'block-size',
|
|
53
|
+
'min-width': 'min-inline-size',
|
|
54
|
+
'max-width': 'max-inline-size',
|
|
55
|
+
'min-height': 'min-block-size',
|
|
56
|
+
'max-height': 'max-block-size',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Value mappings for specific properties
|
|
60
|
+
const VALUE_MAP = {
|
|
61
|
+
'text-align': {
|
|
62
|
+
'left': 'start',
|
|
63
|
+
'right': 'end',
|
|
64
|
+
},
|
|
65
|
+
'float': {
|
|
66
|
+
'left': 'inline-start',
|
|
67
|
+
'right': 'inline-end',
|
|
68
|
+
},
|
|
69
|
+
'clear': {
|
|
70
|
+
'left': 'inline-start',
|
|
71
|
+
'right': 'inline-end',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Physical → logical utility class prefix mappings
|
|
76
|
+
// These map directional class names to their logical equivalents in templates.
|
|
77
|
+
// Sorted by longest prefix first to avoid partial matches (d-ml- before d-l-).
|
|
78
|
+
const CLASS_PREFIX_MAP = [
|
|
79
|
+
// Margin
|
|
80
|
+
['d-mt-', 'd-mbs-'],
|
|
81
|
+
['d-mr-', 'd-mie-'],
|
|
82
|
+
['d-mb-', 'd-mbe-'],
|
|
83
|
+
['d-ml-', 'd-mis-'],
|
|
84
|
+
// Padding
|
|
85
|
+
['d-pt-', 'd-pbs-'],
|
|
86
|
+
['d-pr-', 'd-pie-'],
|
|
87
|
+
['d-pb-', 'd-pbe-'],
|
|
88
|
+
['d-pl-', 'd-pis-'],
|
|
89
|
+
// Position (inset)
|
|
90
|
+
['d-t-', 'd-ibs-'],
|
|
91
|
+
['d-r-', 'd-iie-'],
|
|
92
|
+
['d-b-', 'd-ibe-'],
|
|
93
|
+
['d-l-', 'd-iis-'],
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
function escapeRegex(string) {
|
|
97
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fixLogicalProperties(content) {
|
|
101
|
+
// Process line by line to avoid matching in comments
|
|
102
|
+
const lines = content.split('\n');
|
|
103
|
+
const fixedLines = lines.map(line => {
|
|
104
|
+
// Skip comment lines (LESS/CSS comments)
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
|
|
107
|
+
return line;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let fixedLine = line;
|
|
111
|
+
|
|
112
|
+
// Fix property names - match property at start of declaration
|
|
113
|
+
for (const [physical, logical] of Object.entries(PROPERTY_MAP)) {
|
|
114
|
+
// Match: whitespace + property + colon (with optional whitespace)
|
|
115
|
+
// This ensures we only match actual CSS properties, not parts of other words
|
|
116
|
+
const propertyRegex = new RegExp(
|
|
117
|
+
`(^\\s*|[{;]\\s*)${escapeRegex(physical)}(\\s*:)`,
|
|
118
|
+
'g'
|
|
119
|
+
);
|
|
120
|
+
fixedLine = fixedLine.replace(propertyRegex, `$1${logical}$2`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fix values for specific properties
|
|
124
|
+
for (const [property, values] of Object.entries(VALUE_MAP)) {
|
|
125
|
+
for (const [physical, logical] of Object.entries(values)) {
|
|
126
|
+
// Match: property: value (with proper boundaries)
|
|
127
|
+
const valueRegex = new RegExp(
|
|
128
|
+
`(${escapeRegex(property)}\\s*:\\s*)${physical}(\\s*[;!}]|\\s*$)`,
|
|
129
|
+
'gi'
|
|
130
|
+
);
|
|
131
|
+
fixedLine = fixedLine.replace(valueRegex, `$1${logical}$2`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return fixedLine;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return fixedLines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Replace physical utility class prefixes with logical equivalents in a string.
|
|
143
|
+
* e.g. "d-pt-100 d-ml-200" → "d-pbs-100 d-mis-200"
|
|
144
|
+
*/
|
|
145
|
+
function fixLogicalClassNames(content) {
|
|
146
|
+
let fixed = content;
|
|
147
|
+
for (const [physical, logical] of CLASS_PREFIX_MAP) {
|
|
148
|
+
// Match the physical prefix followed by a token stop number or 'n' + number (negative)
|
|
149
|
+
// Word boundary ensures we don't match partial class names
|
|
150
|
+
const regex = new RegExp(escapeRegex(physical) + '(n?[0-9]+)', 'g');
|
|
151
|
+
fixed = fixed.replace(regex, `${logical}$1`);
|
|
152
|
+
}
|
|
153
|
+
return fixed;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Process class="..." and :class="..." attributes in template content,
|
|
158
|
+
* replacing physical class prefixes with logical equivalents.
|
|
159
|
+
*/
|
|
160
|
+
function processTemplateClassNames(content) {
|
|
161
|
+
let hasChanges = false;
|
|
162
|
+
|
|
163
|
+
// Match class="..." attributes (static)
|
|
164
|
+
const staticClassRegex = /(class=")(.*?)(")/g;
|
|
165
|
+
let fixed = content.replace(staticClassRegex, (_match, open, classes, close) => {
|
|
166
|
+
const fixedClasses = fixLogicalClassNames(classes);
|
|
167
|
+
if (fixedClasses !== classes) hasChanges = true;
|
|
168
|
+
return open + fixedClasses + close;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Match :class="'...'" attributes (dynamic with string literal)
|
|
172
|
+
const dynamicClassRegex = /(:class="')(.*?)(')/g;
|
|
173
|
+
fixed = fixed.replace(dynamicClassRegex, (_match, open, classes, close) => {
|
|
174
|
+
const fixedClasses = fixLogicalClassNames(classes);
|
|
175
|
+
if (fixedClasses !== classes) hasChanges = true;
|
|
176
|
+
return open + fixedClasses + close;
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return { fixed, hasChanges };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function processStyleBlocks(content) {
|
|
183
|
+
// Match all <style> blocks (with any attributes like lang, scoped, etc.)
|
|
184
|
+
const styleRegex = /(<style[^>]*>)([\s\S]*?)(<\/style>)/gi;
|
|
185
|
+
let hasChanges = false;
|
|
186
|
+
|
|
187
|
+
const fixed = content.replace(styleRegex, (_match, openTag, styleContent, closeTag) => {
|
|
188
|
+
const fixedStyle = fixLogicalProperties(styleContent);
|
|
189
|
+
if (fixedStyle !== styleContent) {
|
|
190
|
+
hasChanges = true;
|
|
191
|
+
}
|
|
192
|
+
return openTag + fixedStyle + closeTag;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return { fixed, hasChanges };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function processMarkdownFile(filePath) {
|
|
199
|
+
try {
|
|
200
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
201
|
+
|
|
202
|
+
// Split by fenced code blocks to avoid processing code examples
|
|
203
|
+
// Match ``` with optional language identifier and content until closing ```
|
|
204
|
+
const fencedCodeRegex = /(```[\s\S]*?```)/g;
|
|
205
|
+
const parts = content.split(fencedCodeRegex);
|
|
206
|
+
let hasChanges = false;
|
|
207
|
+
|
|
208
|
+
// Process only non-code-block parts
|
|
209
|
+
const fixedParts = parts.map((part, index) => {
|
|
210
|
+
// Odd indices are the fenced code blocks (captured groups)
|
|
211
|
+
if (index % 2 === 1) {
|
|
212
|
+
return part; // Keep code blocks unchanged
|
|
213
|
+
}
|
|
214
|
+
// Process style blocks in non-code parts
|
|
215
|
+
const styleResult = processStyleBlocks(part);
|
|
216
|
+
if (styleResult.hasChanges) hasChanges = true;
|
|
217
|
+
// Process class names in non-code parts
|
|
218
|
+
const classResult = processTemplateClassNames(styleResult.fixed);
|
|
219
|
+
if (classResult.hasChanges) hasChanges = true;
|
|
220
|
+
return classResult.fixed;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (hasChanges) {
|
|
224
|
+
fs.writeFileSync(filePath, fixedParts.join(''), 'utf8');
|
|
225
|
+
console.log(`Fixed: ${filePath}`);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`Error processing ${filePath}:`, error.message);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function processFileWithStyleBlocks(filePath) {
|
|
236
|
+
try {
|
|
237
|
+
let content = fs.readFileSync(filePath, 'utf8');
|
|
238
|
+
let hasChanges = false;
|
|
239
|
+
|
|
240
|
+
// Fix CSS properties in <style> blocks
|
|
241
|
+
const styleResult = processStyleBlocks(content);
|
|
242
|
+
if (styleResult.hasChanges) hasChanges = true;
|
|
243
|
+
content = styleResult.fixed;
|
|
244
|
+
|
|
245
|
+
// Fix class names in templates (outside <style> blocks)
|
|
246
|
+
const classResult = processTemplateClassNames(content);
|
|
247
|
+
if (classResult.hasChanges) hasChanges = true;
|
|
248
|
+
content = classResult.fixed;
|
|
249
|
+
|
|
250
|
+
if (hasChanges) {
|
|
251
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
252
|
+
console.log(`Fixed: ${filePath}`);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error(`Error processing ${filePath}:`, error.message);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function processFile(filePath) {
|
|
263
|
+
try {
|
|
264
|
+
// Markdown: skip fenced code blocks, only process <style> tags
|
|
265
|
+
if (filePath.endsWith('.md')) {
|
|
266
|
+
return processMarkdownFile(filePath);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Vue/HTML: only process <style> blocks
|
|
270
|
+
if (filePath.endsWith('.vue') || filePath.endsWith('.html')) {
|
|
271
|
+
return processFileWithStyleBlocks(filePath);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// CSS/LESS: process entire file
|
|
275
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
276
|
+
const fixed = fixLogicalProperties(content);
|
|
277
|
+
|
|
278
|
+
if (content !== fixed) {
|
|
279
|
+
fs.writeFileSync(filePath, fixed, 'utf8');
|
|
280
|
+
console.log(`Fixed: ${filePath}`);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
return false;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error(`Error processing ${filePath}:`, error.message);
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Main: process files passed as arguments
|
|
291
|
+
const files = process.argv.slice(2);
|
|
292
|
+
|
|
293
|
+
if (files.length === 0) {
|
|
294
|
+
console.log('Usage: fix-logical-properties <file1> [file2] ...');
|
|
295
|
+
} else {
|
|
296
|
+
let fixedCount = 0;
|
|
297
|
+
for (const file of files) {
|
|
298
|
+
if (processFile(file)) {
|
|
299
|
+
fixedCount++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (fixedCount > 0) {
|
|
304
|
+
console.log(`\nFixed ${fixedCount} file(s)`);
|
|
305
|
+
}
|
|
306
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const noBaseColorTokens = require('./rules/no-base-color-tokens');
|
|
4
|
+
const noDeprecatedSizeTokens = require('./rules/no-deprecated-size-tokens');
|
|
4
5
|
const noDeprecatedSpaceTokens = require('./rules/no-deprecated-space-tokens');
|
|
5
6
|
const noMixins = require('./rules/no-mixins');
|
|
6
7
|
const recommendFontStyleTokens = require('./rules/recommend-font-style-tokens');
|
|
7
8
|
const useDialtoneTokens = require('./rules/use-dialtone-tokens');
|
|
9
|
+
const useLogical = require('stylelint-use-logical');
|
|
8
10
|
|
|
9
11
|
module.exports = [
|
|
10
12
|
noBaseColorTokens,
|
|
13
|
+
noDeprecatedSizeTokens,
|
|
11
14
|
noDeprecatedSpaceTokens,
|
|
12
15
|
noMixins,
|
|
13
16
|
recommendFontStyleTokens,
|
|
14
17
|
useDialtoneTokens,
|
|
18
|
+
useLogical,
|
|
15
19
|
];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const stylelint = require('stylelint');
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createPlugin,
|
|
5
|
+
utils: { report, ruleMessages, validateOptions },
|
|
6
|
+
} = stylelint;
|
|
7
|
+
|
|
8
|
+
const ruleName = '@dialpad/stylelint-plugin-dialtone/no-deprecated-size-tokens';
|
|
9
|
+
|
|
10
|
+
const messages = ruleMessages(ruleName, {
|
|
11
|
+
deprecatedSizeToken:
|
|
12
|
+
'--dt-size-* tokens have been replaced. Use --dt-layout-* for layout (widths/heights) or --dt-spacing-* for spacing (padding/margin). Run "npx dialtone-migration-helper" and select "size-to-layout".',
|
|
13
|
+
deprecatedSpaceToken:
|
|
14
|
+
'--dt-space-* tokens have been replaced by --dt-spacing-*. Run "npx dialtone-migration-helper" and select "space-to-spacing".',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const meta = {
|
|
18
|
+
url: 'https://github.com/dialpad/dialtone/blob/staging/packages/stylelint-plugin-dialtone/docs/rules/no-deprecated-size-tokens.md',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** @type {import('stylelint').Rule} */
|
|
22
|
+
const ruleFunction = (primary) => {
|
|
23
|
+
return (root, result) => {
|
|
24
|
+
const validOptions = validateOptions(result, ruleName, {
|
|
25
|
+
actual: primary,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!validOptions) return;
|
|
29
|
+
|
|
30
|
+
root.walkDecls((declaration) => {
|
|
31
|
+
// Match --dt-size-{number} but NOT --dt-size-border-* or --dt-size-radius-* (those are valid)
|
|
32
|
+
const sizeTokenMatches = declaration.value.match(/var\(--dt-size-(?!border-|radius-)[^)]+\)/g);
|
|
33
|
+
if (sizeTokenMatches) {
|
|
34
|
+
sizeTokenMatches.forEach(() => {
|
|
35
|
+
report({
|
|
36
|
+
result,
|
|
37
|
+
ruleName,
|
|
38
|
+
node: declaration,
|
|
39
|
+
message: messages.deprecatedSizeToken,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const spaceTokenMatches = declaration.value.match(/var\(--dt-space-[^)]+\)/g);
|
|
45
|
+
if (spaceTokenMatches) {
|
|
46
|
+
spaceTokenMatches.forEach(() => {
|
|
47
|
+
report({
|
|
48
|
+
result,
|
|
49
|
+
ruleName,
|
|
50
|
+
node: declaration,
|
|
51
|
+
message: messages.deprecatedSpaceToken,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
ruleFunction.ruleName = ruleName;
|
|
60
|
+
ruleFunction.messages = messages;
|
|
61
|
+
ruleFunction.meta = meta;
|
|
62
|
+
|
|
63
|
+
module.exports = createPlugin(ruleName, ruleFunction);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dialpad/stylelint-plugin-dialtone",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0-next.2",
|
|
4
4
|
"description": "dialtone stylelint plugin",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Dialpad",
|
|
@@ -39,10 +39,17 @@
|
|
|
39
39
|
"directory": "packages/stylelint-plugin-dialtone"
|
|
40
40
|
},
|
|
41
41
|
"main": "./lib/index.js",
|
|
42
|
+
"bin": {
|
|
43
|
+
"fix-logical-properties": "./bin/fix-logical-properties.js"
|
|
44
|
+
},
|
|
42
45
|
"files": [
|
|
43
|
-
"lib"
|
|
46
|
+
"lib",
|
|
47
|
+
"bin"
|
|
44
48
|
],
|
|
45
49
|
"exports": "./lib/index.js",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"stylelint-use-logical": "^2.1.2"
|
|
52
|
+
},
|
|
46
53
|
"devDependencies": {
|
|
47
54
|
"eslint-plugin-n": "^17.0.0",
|
|
48
55
|
"stylelint-test-rule-node": "^0.2.1",
|