@runtimestudio/tailwind-sort-php 0.2.1 → 0.3.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.
package/src/transform.ts CHANGED
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { findIslands, type Island, type IslandOptions } from './islands.ts';
21
21
  import { maskIslands, findClassAttributes, type HtmlScanOptions } from './html.ts';
22
+ import { findSortablePhpStrings } from './php-strings.ts';
22
23
 
23
24
  /**
24
25
  * Sorting strategy injected by the caller. Receives the class tokens of a single static run;
@@ -27,9 +28,14 @@ import { maskIslands, findClassAttributes, type HtmlScanOptions } from './html.t
27
28
  export type SortFn = (classes: string[]) => string[];
28
29
 
29
30
  /**
30
- * Combined options for both lexer passes.
31
+ * Combined options for all lexer passes.
31
32
  */
32
- export interface TransformOptions extends IslandOptions, HtmlScanOptions {}
33
+ export interface TransformOptions extends IslandOptions, HtmlScanOptions {
34
+ /**
35
+ * Sort classes in PHP string-literal values too. Off by default; see `php-strings.ts` for eligibility.
36
+ */
37
+ sortPhpStrings?: boolean;
38
+ }
33
39
 
34
40
  /**
35
41
  * Rewrite all class attribute values in the template source with sorted classes.
@@ -45,90 +51,104 @@ export interface TransformOptions extends IslandOptions, HtmlScanOptions {}
45
51
  * // '<div class="mt-4 z-10 <?= $x ?> a b">'
46
52
  */
47
53
  export function transform(src: string, sortFn: SortFn, opts: TransformOptions = {}): string {
48
- const islands = findIslands(src, opts);
49
- const masked = maskIslands(src, islands);
50
- const attrs = findClassAttributes(masked, opts);
51
-
52
- // Apply replacements back-to-front so offsets stay valid.
53
- let out = src;
54
- for (let a = attrs.length - 1; a >= 0; a--) {
55
- const { valueStart, valueEnd } = attrs[a];
56
- const original = src.slice(valueStart, valueEnd);
57
- const inner = islands.filter((isl) => isl.start >= valueStart && isl.end <= valueEnd);
58
- const rewritten = rewriteValue(original, valueStart, inner, sortFn);
59
- if (rewritten !== original) {
60
- out = out.slice(0, valueStart) + rewritten + out.slice(valueEnd);
54
+ const islands = findIslands(src, opts);
55
+ const masked = maskIslands(src, islands);
56
+ const attrs = findClassAttributes(masked, opts);
57
+
58
+ // Collect every edit as a {start, end, text} range, then apply them back-to-front so offsets stay valid.
59
+ // HTML class-attribute values live outside islands; PHP string values live inside them, so the two sets of
60
+ // ranges never overlap.
61
+ const edits: { start: number; end: number; text: string }[] = [];
62
+
63
+ for (const { valueStart, valueEnd } of attrs) {
64
+ const original = src.slice(valueStart, valueEnd);
65
+ const inner = islands.filter((isl) => isl.start >= valueStart && isl.end <= valueEnd);
66
+ const rewritten = rewriteValue(original, valueStart, inner, sortFn);
67
+ if (rewritten !== original) edits.push({ start: valueStart, end: valueEnd, text: rewritten });
68
+ }
69
+
70
+ if (opts.sortPhpStrings) {
71
+ for (const { start, end } of findSortablePhpStrings(src, islands)) {
72
+ const original = src.slice(start, end);
73
+ const tokens = original.split(/\s+/).filter(Boolean);
74
+ if (tokens.length < 2) continue; // nothing to reorder; leave byte-identical
75
+ const rewritten = sortFn(tokens).join(' ');
76
+ if (rewritten !== original) edits.push({ start, end, text: rewritten });
77
+ }
61
78
  }
62
- }
63
- return out;
79
+
80
+ edits.sort((a, b) => b.start - a.start);
81
+ let out = src;
82
+ for (const e of edits) out = out.slice(0, e.start) + e.text + out.slice(e.end);
83
+ return out;
64
84
  }
65
85
 
66
86
  interface Part {
67
- type: 'static' | 'island';
68
- text: string;
87
+ type: 'static' | 'island';
88
+ text: string;
69
89
  }
70
90
 
71
91
  function rewriteValue(value: string, base: number, islands: Island[], sortFn: SortFn): string {
72
- // Build alternating static/island parts.
73
- const parts: Part[] = [];
74
- let pos = 0;
75
- for (const isl of islands) {
76
- const s = isl.start - base;
77
- const e = isl.end - base;
78
- parts.push({ type: 'static', text: value.slice(pos, s) });
79
- parts.push({ type: 'island', text: value.slice(s, e) });
80
- pos = e;
81
- }
82
- parts.push({ type: 'static', text: value.slice(pos) });
83
-
84
- let out = '';
85
- for (let p = 0; p < parts.length; p++) {
86
- const part = parts[p];
87
- if (part.type === 'island') {
88
- out += part.text;
89
- continue;
92
+ // Build alternating static/island parts.
93
+ const parts: Part[] = [];
94
+ let pos = 0;
95
+ for (const isl of islands) {
96
+ const s = isl.start - base;
97
+ const e = isl.end - base;
98
+ parts.push({ type: 'static', text: value.slice(pos, s) });
99
+ parts.push({ type: 'island', text: value.slice(s, e) });
100
+ pos = e;
90
101
  }
91
-
92
- const prevIsIsland = p > 0;
93
- const nextIsIsland = p < parts.length - 1;
94
- const t = part.text;
95
-
96
- const hasLeadingWs = /^\s/.test(t);
97
- const hasTrailingWs = /\s$/.test(t);
98
- const tokens = t.split(/\s+/).filter(Boolean);
99
-
100
- // Whitespace-only run between islands → preserve a single space.
101
- if (tokens.length === 0) {
102
- if (t.length > 0 && prevIsIsland && nextIsIsland) out += ' ';
103
- continue;
104
- }
105
-
106
- const pinStart = prevIsIsland && !hasLeadingWs;
107
- const pinEnd = nextIsIsland && !hasTrailingWs;
108
-
109
- let head: string[] = [];
110
- let tail: string[] = [];
111
- let middle: string[];
112
-
113
- if (pinStart && pinEnd && tokens.length === 1) {
114
- // Single fragment glued to islands on both sides.
115
- middle = [];
116
- head = [tokens[0]];
117
- } else {
118
- const from = pinStart ? 1 : 0;
119
- const to = pinEnd ? tokens.length - 1 : tokens.length;
120
- if (pinStart) head = [tokens[0]];
121
- if (pinEnd) tail = [tokens[tokens.length - 1]];
122
- middle = tokens.slice(from, to);
102
+ parts.push({ type: 'static', text: value.slice(pos) });
103
+
104
+ let out = '';
105
+ for (let p = 0; p < parts.length; p++) {
106
+ const part = parts[p];
107
+ if (part.type === 'island') {
108
+ out += part.text;
109
+ continue;
110
+ }
111
+
112
+ const prevIsIsland = p > 0;
113
+ const nextIsIsland = p < parts.length - 1;
114
+ const t = part.text;
115
+
116
+ const hasLeadingWs = /^\s/.test(t);
117
+ const hasTrailingWs = /\s$/.test(t);
118
+ const tokens = t.split(/\s+/).filter(Boolean);
119
+
120
+ // Whitespace-only run between islands → preserve a single space.
121
+ if (tokens.length === 0) {
122
+ if (t.length > 0 && prevIsIsland && nextIsIsland) out += ' ';
123
+ continue;
124
+ }
125
+
126
+ const pinStart = prevIsIsland && !hasLeadingWs;
127
+ const pinEnd = nextIsIsland && !hasTrailingWs;
128
+
129
+ let head: string[] = [];
130
+ let tail: string[] = [];
131
+ let middle: string[];
132
+
133
+ if (pinStart && pinEnd && tokens.length === 1) {
134
+ // Single fragment glued to islands on both sides.
135
+ middle = [];
136
+ head = [tokens[0]];
137
+ } else {
138
+ const from = pinStart ? 1 : 0;
139
+ const to = pinEnd ? tokens.length - 1 : tokens.length;
140
+ if (pinStart) head = [tokens[0]];
141
+ if (pinEnd) tail = [tokens[tokens.length - 1]];
142
+ middle = tokens.slice(from, to);
143
+ }
144
+
145
+ const sorted = middle.length > 1 ? sortFn(middle) : middle;
146
+ const joined = [...head, ...sorted, ...tail].join(' ');
147
+
148
+ const prefix = prevIsIsland && hasLeadingWs ? ' ' : '';
149
+ const suffix = nextIsIsland && hasTrailingWs ? ' ' : '';
150
+ out += prefix + joined + suffix;
123
151
  }
124
152
 
125
- const sorted = middle.length > 1 ? sortFn(middle) : middle;
126
- const joined = [...head, ...sorted, ...tail].join(' ');
127
-
128
- const prefix = prevIsIsland && hasLeadingWs ? ' ' : '';
129
- const suffix = nextIsIsland && hasTrailingWs ? ' ' : '';
130
- out += prefix + joined + suffix;
131
- }
132
-
133
- return out;
153
+ return out;
134
154
  }