Skip to content

Commit 7acff29

Browse files
committed
chore: wip
1 parent b02e640 commit 7acff29

File tree

14 files changed

+5813
-29
lines changed

14 files changed

+5813
-29
lines changed

packages/stx/bin/cli.ts

Lines changed: 712 additions & 5 deletions
Large diffs are not rendered by default.

packages/stx/src/analyzer.ts

Lines changed: 419 additions & 0 deletions
Large diffs are not rendered by default.

packages/stx/src/formatter.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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(/<script\b[^>]*>([\s\S]*?)<\/script>/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(/@(if|elseif|foreach|for|while)\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(/@(csrf|method)\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+
}

packages/stx/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './a11y'
2+
export * from './analyzer'
23
export * from './animation'
34
export * from './assets'
45
export * from './auth'
@@ -10,6 +11,7 @@ export * from './dev-server'
1011
export * from './docs'
1112
export * from './expressions'
1213
export * from './forms'
14+
export * from './formatter'
1315
export * from './i18n'
1416
export * from './includes'
1517
export * from './init'

packages/stx/src/performance-utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ export class TemplateCache {
5959
// If cache is full, remove oldest entries
6060
if (this.cache.size >= this.maxSize) {
6161
const oldestKey = this.cache.keys().next().value
62-
this.cache.delete(oldestKey)
62+
if (oldestKey) {
63+
this.cache.delete(oldestKey)
64+
}
6365
}
6466

6567
this.cache.set(key, {
@@ -161,7 +163,9 @@ export function memoize<T extends (...args: any[]) => any>(
161163
// If cache is full, remove oldest entry
162164
if (cache.size >= maxCacheSize) {
163165
const firstKey = cache.keys().next().value
164-
cache.delete(firstKey)
166+
if (firstKey) {
167+
cache.delete(firstKey)
168+
}
165169
}
166170

167171
const result = func(...args)

0 commit comments

Comments
 (0)