Skip to content

Commit c418feb

Browse files
committed
chore: wip
1 parent 0a9072e commit c418feb

File tree

2 files changed

+56
-17
lines changed

2 files changed

+56
-17
lines changed

eslint.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const config: ESLintConfig = stacks({
2323
'**/*.md',
2424
'packages/benchmarks/**',
2525
'packages/collections/**',
26+
'**/.stx/cache/**',
2627
],
2728
})
2829

packages/stx/src/formatter.ts

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -195,39 +195,77 @@ function splitInlineTags(content: string): string {
195195
// Split on tags, but keep mixed text/tag content together
196196
let result = content
197197

198+
// Protect whitespace-preserving tags (pre, code, textarea) from splitting
199+
const protectedBlocks: string[] = []
200+
const placeholder = '__PROTECTED_BLOCK_'
201+
result = result.replace(/(<(?:pre|code|textarea)[^>]*>)([\s\S]*?)(<\/(?:pre|code|textarea)>)/gi, (match) => {
202+
const index = protectedBlocks.length
203+
protectedBlocks.push(match)
204+
return `${placeholder}${index}__`
205+
})
206+
198207
// Split directives onto their own lines FIRST (before other processing)
199-
// Split opening block directives
200-
result = result.replace(/(@(?:if|foreach|for|while|unless|section|component|slot|push|prepend)(?:\([^)]*\))?)/g, '\n$1\n')
208+
// Split opening block directives - need to handle nested parentheses and quotes
209+
result = result.replace(/(@(?:if|foreach|for|while|unless|section|component|slot|push|prepend|error|auth|guest|can)(?:\((?:[^()'"]|'[^']*'|"[^"]*"|\((?:[^()'"]|'[^']*'|"[^"]*")*\))*\))?)/g, '\n$1\n')
201210
// Split closing directives
202211
result = result.replace(/(@end\w+)/g, '\n$1\n')
203-
// Split middle directives
204-
result = result.replace(/(@(?:else|elseif)(?:\([^)]*\))?)/g, '\n$1\n')
212+
// Split middle directives - same pattern for nested parens/quotes
213+
result = result.replace(/(@(?:else|elseif|empty)(?:\((?:[^()'"]|'[^']*'|"[^"]*"|\((?:[^()'"]|'[^']*'|"[^"]*")*\))*\))?)/g, '\n$1\n')
214+
// Split standalone directives (csrf, method, etc.)
215+
result = result.replace(/(@(?:csrf|method|include|extends|yield|stack|vite|asset)(?:\([^)]*\))?)/g, '\n$1\n')
205216

206217
// Split opening tags onto new lines (unless there's text before them)
207-
result = result.replace(/(?<=>)(<(?!\/)[^>]+>)/g, '\n$1')
208-
209-
// Split closing tags onto new lines (unless there's text after them on same line)
210-
result = result.replace(/(<\/[^>]+>)(?=<)/g, '$1\n')
218+
// But not for empty self-closing elements
219+
result = result.replace(/(?<=>)(<(?!\/)[^>]+>)/g, (match, _p1, offset, string) => {
220+
// Don't split if this is immediately followed by its closing tag (empty element)
221+
const after = string.substring(offset + match.length, Math.min(string.length, offset + match.length + 20))
222+
const tagName = match.match(/<(\w+)/)?.[1]
223+
if (tagName && after.match(new RegExp(`^<\/${tagName}>`))) {
224+
return match // Keep empty elements together
225+
}
226+
return `\n${match}`
227+
})
211228

212229
// Also split before closing tags if there's an opening tag before
213230
// BUT don't split when it's an empty element (opening immediately followed by closing)
214-
result = result.replace(/(>)(<\/)(?![\w\s]*>$)/g, (match, gt, closeTag, offset, string) => {
231+
result = result.replace(/(>)(<\/)/g, (match, gt, closeTag, offset, string) => {
215232
// Check if this is part of an empty element pattern like <tag></tag>
216233
const before = string.substring(Math.max(0, offset - 50), offset)
217234
const after = string.substring(offset + match.length, Math.min(string.length, offset + match.length + 20))
218235

219-
// If the closing tag immediately follows opening (empty element), don't split
220-
if (before.match(/<\w[^>]*>$/) && after.match(/^\w[^>]*>/)) {
221-
// eslint-disable-next-line regexp/optimal-quantifier-concatenation
222-
const openTagName = before.match(/<(\w+)[^>]*>$/)?.[1]
223-
const closeTagName = after.match(/^(\w+)>/)?.[1]
224-
if (openTagName === closeTagName) {
225-
return match // Keep empty elements together
226-
}
236+
// Extract tag name from the part after >< /
237+
// after looks like "script>" or "div>" etc (without the </)
238+
const closeTagName = after.match(/^(\w+)>/)?.[1]
239+
// Extract tag name from the opening tag immediately before > (before doesn't include the >)
240+
// eslint-disable-next-line regexp/optimal-quantifier-concatenation
241+
const openTagName = before.match(/<(\w+)[^>]*$/)?.[1]
242+
243+
// If the closing tag immediately follows opening tag with same name (empty element), don't split
244+
if (openTagName && closeTagName && openTagName === closeTagName) {
245+
return match // Keep empty elements together
227246
}
228247
return `${gt}\n${closeTag}`
229248
})
230249

250+
// Split closing tags onto new lines (when followed by another tag or at end of content)
251+
// But skip empty elements (opening tag immediately followed by closing with no content)
252+
result = result.replace(/(<\/[^>]+>)(?=<|$)/g, (match, _p1, offset, string) => {
253+
// Check if this closing tag is part of an empty element (e.g., <script></script>)
254+
const before = string.substring(Math.max(0, offset - 20), offset)
255+
const tagName = match.match(/<\/(\w+)>/)?.[1]
256+
// Only skip newline if opening tag is immediately before closing tag (empty element)
257+
const emptyPattern = new RegExp(`<${tagName}[^>]*>$`)
258+
if (tagName && before.match(emptyPattern)) {
259+
return match // Don't add newline for empty elements
260+
}
261+
return `${match}\n`
262+
})
263+
264+
// Restore protected blocks
265+
result = result.replace(new RegExp(`${placeholder}(\\d+)__`, 'g'), (_match, index) => {
266+
return protectedBlocks[Number.parseInt(index, 10)]
267+
})
268+
231269
// Clean up multiple consecutive newlines
232270
result = result.replace(/\n{2,}/g, '\n')
233271

0 commit comments

Comments
 (0)