@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.1-next.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",