1
+ /**
2
+ * STX file formatter for automatically formatting .stx files
3
+ */
4
+
5
+ export interface FormatterOptions {
6
+ /** Number of spaces for indentation (default: 2) */
7
+ indentSize ?: number
8
+ /** Use tabs instead of spaces for indentation */
9
+ useTabs ?: boolean
10
+ /** Maximum line length before wrapping (default: 120) */
11
+ maxLineLength ?: number
12
+ /** Normalize whitespace in template expressions */
13
+ normalizeWhitespace ?: boolean
14
+ /** Sort attributes alphabetically */
15
+ sortAttributes ?: boolean
16
+ /** Remove trailing whitespace */
17
+ trimTrailingWhitespace ?: boolean
18
+ }
19
+
20
+ const DEFAULT_OPTIONS : Required < FormatterOptions > = {
21
+ indentSize : 2 ,
22
+ useTabs : false ,
23
+ maxLineLength : 120 ,
24
+ normalizeWhitespace : true ,
25
+ sortAttributes : true ,
26
+ trimTrailingWhitespace : true
27
+ }
28
+
29
+ /**
30
+ * Format STX file content
31
+ */
32
+ export function formatStxContent ( content : string , options : FormatterOptions = { } ) : string {
33
+ const opts = { ...DEFAULT_OPTIONS , ...options }
34
+
35
+ let formatted = content
36
+
37
+ // Remove trailing whitespace from all lines
38
+ if ( opts . trimTrailingWhitespace ) {
39
+ formatted = formatted . replace ( / [ \t ] + $ / gm, '' )
40
+ }
41
+
42
+ // Normalize script tag formatting
43
+ formatted = formatScriptTags ( formatted , opts )
44
+
45
+ // Format HTML structure
46
+ formatted = formatHtml ( formatted , opts )
47
+
48
+ // Format STX directives
49
+ formatted = formatStxDirectives ( formatted , opts )
50
+
51
+ // Normalize line endings and ensure file ends with newline
52
+ formatted = formatted . replace ( / \r \n / g, '\n' ) . replace ( / \r / g, '\n' )
53
+ if ( ! formatted . endsWith ( '\n' ) ) {
54
+ formatted += '\n'
55
+ }
56
+
57
+ return formatted
58
+ }
59
+
60
+ /**
61
+ * Format script tags within STX files
62
+ */
63
+ function formatScriptTags ( content : string , options : Required < FormatterOptions > ) : string {
64
+ return content . replace ( / < s c r i p t \b [ ^ > ] * > ( [ \s \S ] * ?) < \/ s c r i p t > / gi, ( match , scriptContent ) => {
65
+ // Basic script formatting - normalize indentation
66
+ const lines = scriptContent . split ( '\n' )
67
+ const formattedLines = lines . map ( ( line : string , index : number ) => {
68
+ if ( index === 0 && line . trim ( ) === '' ) return '' // Empty first line
69
+ if ( index === lines . length - 1 && line . trim ( ) === '' ) return '' // Empty last line
70
+
71
+ // Add consistent indentation
72
+ const trimmed = line . trim ( )
73
+ if ( trimmed === '' ) return ''
74
+
75
+ const indent = options . useTabs ? '\t' : ' ' . repeat ( options . indentSize )
76
+ return `${ indent } ${ trimmed } `
77
+ } ) . filter ( ( line : string , index : number , arr : string [ ] ) => {
78
+ // Remove empty lines at start and end
79
+ if ( index === 0 || index === arr . length - 1 ) return line !== ''
80
+ return true
81
+ } )
82
+
83
+ const formattedScript = formattedLines . length > 0
84
+ ? '\n' + formattedLines . join ( '\n' ) + '\n'
85
+ : ''
86
+
87
+ return match . replace ( scriptContent , formattedScript )
88
+ } )
89
+ }
90
+
91
+ /**
92
+ * Format HTML structure with proper indentation
93
+ */
94
+ function formatHtml ( content : string , options : Required < FormatterOptions > ) : string {
95
+ const lines = content . split ( '\n' )
96
+ const formattedLines : string [ ] = [ ]
97
+ let indentLevel = 0
98
+ const indent = options . useTabs ? '\t' : ' ' . repeat ( options . indentSize )
99
+
100
+ for ( let i = 0 ; i < lines . length ; i ++ ) {
101
+ const line = lines [ i ] . trim ( )
102
+
103
+ if ( line === '' ) {
104
+ formattedLines . push ( '' )
105
+ continue
106
+ }
107
+
108
+ // Handle closing tags
109
+ if ( line . startsWith ( '</' ) || line . includes ( '@end' ) ) {
110
+ indentLevel = Math . max ( 0 , indentLevel - 1 )
111
+ }
112
+
113
+ // Add the line with proper indentation
114
+ const indentedLine = indentLevel > 0 ? indent . repeat ( indentLevel ) + line : line
115
+ formattedLines . push ( indentedLine )
116
+
117
+ // Handle opening tags (but not self-closing)
118
+ if ( line . startsWith ( '<' ) && ! line . includes ( '/>' ) && ! line . startsWith ( '</' ) ) {
119
+ // Check if it's not a self-closing tag or comment
120
+ if ( ! line . includes ( '<!' ) && ! isSelfClosingTag ( line ) ) {
121
+ indentLevel ++
122
+ }
123
+ }
124
+
125
+ // Handle STX directives that open blocks
126
+ if ( line . startsWith ( '@' ) && isOpeningDirective ( line ) ) {
127
+ indentLevel ++
128
+ }
129
+ }
130
+
131
+ return formattedLines . join ( '\n' )
132
+ }
133
+
134
+ /**
135
+ * Format STX directives for better readability
136
+ */
137
+ function formatStxDirectives ( content : string , options : Required < FormatterOptions > ) : string {
138
+ // Format @if , @foreach, @for etc. directives
139
+ content = content . replace ( / @ ( i f | e l s e i f | f o r e a c h | f o r | w h i l e ) \s * \( \s * ( [ ^ ) ] + ) \s * \) / g, ( match , directive , condition ) => {
140
+ const normalizedCondition = condition . trim ( ) . replace ( / \s + / g, ' ' )
141
+ return `@${ directive } (${ normalizedCondition } )`
142
+ } )
143
+
144
+ // Format simple directives like @csrf , @method etc.
145
+ content = content . replace ( / @ ( c s r f | m e t h o d ) \s * \( \s * ( [ ^ ) ] * ) \s * \) / g, ( match , directive , param ) => {
146
+ if ( param . trim ( ) === '' ) {
147
+ return `@${ directive } `
148
+ }
149
+ return `@${ directive } (${ param . trim ( ) } )`
150
+ } )
151
+
152
+ // Format variable expressions {{ variable }}
153
+ if ( options . normalizeWhitespace ) {
154
+ content = content . replace ( / \{ \{ \s * ( [ ^ } ] + ) \s * \} \} / g, ( match , expression ) => {
155
+ return `{{ ${ expression . trim ( ) } }}`
156
+ } )
157
+
158
+ // Format raw expressions {!! variable !!}
159
+ content = content . replace ( / \{ ! ! \s * ( [ ^ ! ] + ) \s * ! ! \} / g, ( match , expression ) => {
160
+ return `{!! ${ expression . trim ( ) } !!}`
161
+ } )
162
+ }
163
+
164
+ return content
165
+ }
166
+
167
+ /**
168
+ * Check if a tag is self-closing
169
+ */
170
+ function isSelfClosingTag ( line : string ) : boolean {
171
+ const selfClosingTags = [ 'br' , 'hr' , 'img' , 'input' , 'meta' , 'link' , 'area' , 'base' , 'col' , 'embed' , 'source' , 'track' , 'wbr' ]
172
+
173
+ if ( line . includes ( '/>' ) ) return true
174
+
175
+ const tagMatch = line . match ( / < ( \w + ) / )
176
+ if ( tagMatch ) {
177
+ const tagName = tagMatch [ 1 ] . toLowerCase ( )
178
+ return selfClosingTags . includes ( tagName )
179
+ }
180
+
181
+ return false
182
+ }
183
+
184
+ /**
185
+ * Check if a directive opens a block that needs closing
186
+ */
187
+ function isOpeningDirective ( line : string ) : boolean {
188
+ const blockDirectives = [ 'if' , 'unless' , 'foreach' , 'for' , 'while' , 'section' , 'push' , 'component' , 'slot' , 'markdown' , 'wrap' ]
189
+
190
+ for ( const directive of blockDirectives ) {
191
+ if ( line . startsWith ( `@${ directive } ` ) ) {
192
+ return ! line . includes ( `@end${ directive } ` ) // Not a single-line directive
193
+ }
194
+ }
195
+
196
+ return false
197
+ }
198
+
199
+ /**
200
+ * Format attributes in HTML tags
201
+ */
202
+ function formatAttributes ( content : string , options : Required < FormatterOptions > ) : string {
203
+ if ( ! options . sortAttributes ) return content
204
+
205
+ return content . replace ( / < ( \w + ) ( [ ^ > ] * ) > / g, ( match , tagName , attributes ) => {
206
+ if ( ! attributes . trim ( ) ) return match
207
+
208
+ // Parse attributes
209
+ const attrRegex = / ( \w + ) (?: = ( " [ ^ " ] * " | ' [ ^ ' ] * ' | [ ^ \s > ] + ) ) ? / g
210
+ const attrs : Array < { name : string , value ?: string } > = [ ]
211
+ let attrMatch
212
+
213
+ while ( ( attrMatch = attrRegex . exec ( attributes ) ) !== null ) {
214
+ attrs . push ( {
215
+ name : attrMatch [ 1 ] ,
216
+ value : attrMatch [ 2 ]
217
+ } )
218
+ }
219
+
220
+ // Sort attributes (class and id first, then alphabetically)
221
+ attrs . sort ( ( a , b ) => {
222
+ if ( a . name === 'id' ) return - 1
223
+ if ( b . name === 'id' ) return 1
224
+ if ( a . name === 'class' ) return - 1
225
+ if ( b . name === 'class' ) return 1
226
+ return a . name . localeCompare ( b . name )
227
+ } )
228
+
229
+ // Rebuild attributes
230
+ const formattedAttrs = attrs
231
+ . map ( attr => attr . value ? `${ attr . name } =${ attr . value } ` : attr . name )
232
+ . join ( ' ' )
233
+
234
+ return `<${ tagName } ${ formattedAttrs ? ' ' + formattedAttrs : '' } >`
235
+ } )
236
+ }
0 commit comments