Skip to content

Commit 3ac66fa

Browse files
committed
Add support for HTML and JSX file compaction and refactor associated tests
1 parent 787cfca commit 3ac66fa

32 files changed

Lines changed: 407 additions & 148 deletions

File tree

‎packages/vscode/src/context/utils/compact-file/compact-file.spec.ts‎

Lines changed: 14 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,65 +16,19 @@ describe('compact_file', () => {
1616
return { input, expected }
1717
}
1818

19-
it('should remove single line comments', () => {
20-
const { input, expected } = load_test_case('remove-single-line-comments')
21-
expect(compact_file(input, '.ts')).toBe(expected)
22-
})
23-
24-
it('should remove block comments on a single line', () => {
25-
const { input, expected } = load_test_case(
26-
'remove-block-comments-single-line'
27-
)
28-
expect(compact_file(input, '.ts')).toBe(expected)
29-
})
30-
31-
it('should remove multi-line block comments', () => {
32-
const { input, expected } = load_test_case(
33-
'remove-multi-line-block-comments'
34-
)
35-
expect(compact_file(input, '.ts')).toBe(expected)
36-
})
37-
38-
it('should preserve comments inside double quotes', () => {
39-
const { input, expected } = load_test_case('preserve-double-quotes')
40-
expect(compact_file(input, '.ts')).toBe(expected)
41-
})
42-
43-
it('should preserve comments inside single quotes', () => {
44-
const { input, expected } = load_test_case('preserve-single-quotes')
45-
expect(compact_file(input, '.ts')).toBe(expected)
46-
})
47-
48-
it('should preserve comments inside template literals', () => {
49-
const { input, expected } = load_test_case('preserve-template-literals')
50-
expect(compact_file(input, '.ts')).toBe(expected)
51-
})
52-
53-
it('should preserve block comments inside strings', () => {
54-
const { input, expected } = load_test_case(
55-
'preserve-block-comments-in-strings'
19+
it('should handle c-style content (combined)', () => {
20+
const case_path = path.join(__dirname, 'test-cases', 'c-style')
21+
const input = fs.readFileSync(path.join(case_path, 'input.txt'), 'utf-8')
22+
const expected = fs.readFileSync(
23+
path.join(case_path, 'output.txt'),
24+
'utf-8'
5625
)
5726
expect(compact_file(input, '.ts')).toBe(expected)
5827
})
5928

60-
it('should handle escaped quotes inside strings', () => {
61-
const { input, expected } = load_test_case('handle-escaped-quotes')
62-
expect(compact_file(input, '.ts')).toBe(expected)
63-
})
64-
65-
it('should handle complex mixed content', () => {
66-
const { input, expected } = load_test_case('complex-mixed-content')
67-
expect(compact_file(input, '.ts')).toBe(expected)
68-
})
69-
70-
it('should handle input with only comments', () => {
71-
const { input, expected } = load_test_case('only-comments')
72-
expect(compact_file(input, '.ts')).toBe(expected)
73-
})
74-
75-
it('should strip function bodies when enabled', () => {
76-
const { input, expected } = load_test_case('javascript-bodies')
77-
expect(compact_file(input, '.ts')).toBe(expected)
29+
it('should strip jsx bodies', () => {
30+
const { input, expected } = load_test_case('jsx', '.')
31+
expect(compact_file(input, '.tsx')).toBe(expected)
7832
})
7933

8034
it('should strip python function bodies', () => {
@@ -96,4 +50,9 @@ describe('compact_file', () => {
9650
const { input, expected } = load_test_case('sql', '.')
9751
expect(compact_file(input, '.sql')).toBe(expected)
9852
})
53+
54+
it('should strip html comments', () => {
55+
const { input, expected } = load_test_case('html', '.')
56+
expect(compact_file(input, '.html')).toBe(expected)
57+
})
9958
})
Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { compact_c_style } from './languages/c-style'
22
import { compact_css } from './languages/css'
3+
import { compact_html } from './languages/html'
4+
import { compact_jsx } from './languages/jsx'
35
import { compact_python } from './languages/python'
46
import { compact_ruby } from './languages/ruby'
57
import { compact_sql } from './languages/sql'
68

79
export const compact_file = (content: string, extension: string): string => {
810
if (extension == '.py') {
911
return compact_python(content)
10-
}
11-
if (extension == '.rb') {
12+
} else if (['.tsx', '.jsx'].includes(extension)) {
13+
return compact_jsx(content)
14+
} else if (extension == '.rb') {
1215
return compact_ruby(content)
13-
}
14-
if (['.css', '.scss', '.less'].includes(extension)) {
16+
} else if (['.css', '.scss', '.sass'].includes(extension)) {
1517
return compact_css(content)
16-
}
17-
if (extension == '.sql') {
18+
} else if (extension == '.sql') {
1819
return compact_sql(content)
20+
} else if (['.html', '.htm'].includes(extension)) {
21+
return compact_html(content)
22+
} else {
23+
return compact_c_style(content, true)
1924
}
20-
21-
return compact_c_style(content, true)
2225
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { compact_c_style } from './c-style'
2+
import { compact_css } from './css'
3+
4+
export const compact_html = (content: string): string => {
5+
const scripts: { open: string; content: string }[] = []
6+
const styles: { open: string; content: string }[] = []
7+
8+
// 1. Extract and mask Scripts
9+
// We use a placeholder to preserve the location while we process the rest of the HTML
10+
let processed = content.replace(
11+
/(<script\b[^>]*>)([\s\S]*?)(<\/script>)/gi,
12+
(match, open, inner) => {
13+
scripts.push({ open, content: inner })
14+
return `___SCRIPT_${scripts.length - 1}___`
15+
}
16+
)
17+
18+
// 2. Extract and mask Styles
19+
processed = processed.replace(
20+
/(<style\b[^>]*>)([\s\S]*?)(<\/style>)/gi,
21+
(match, open, inner) => {
22+
styles.push({ open, content: inner })
23+
return `___STYLE_${styles.length - 1}___`
24+
}
25+
)
26+
27+
// 3. Strip HTML Comments
28+
processed = processed.replace(/<!--[\s\S]*?-->/g, '')
29+
30+
// 4. Compact HTML Lines (Trim and remove empty lines)
31+
const lines = processed.split(/\r?\n/)
32+
const compacted_lines: string[] = []
33+
for (const line of lines) {
34+
const trimmed = line.trim()
35+
if (trimmed) {
36+
compacted_lines.push(trimmed)
37+
}
38+
}
39+
processed = compacted_lines.join('\n')
40+
41+
// 5. Restore Scripts
42+
processed = processed.replace(/___SCRIPT_(\d+)___/g, (_, idx) => {
43+
const { open, content } = scripts[parseInt(idx)]
44+
// Treat as C-style but do NOT strip bodies (preserve logic, just strip comments)
45+
// as scripts in HTML are often minimal or structural.
46+
const compacted = compact_c_style(content, false).trim()
47+
return `${open}\n${compacted}\n</script>`
48+
})
49+
50+
// 6. Restore Styles
51+
processed = processed.replace(/___STYLE_(\d+)___/g, (_, idx) => {
52+
const { open, content } = styles[parseInt(idx)]
53+
// Compact CSS (strips bodies/properties usually)
54+
const compacted = compact_css(content).trim()
55+
return `${open}\n${compacted}\n</style>`
56+
})
57+
58+
return processed + '\n'
59+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
export const compact_jsx = (content: string): string => {
2+
// Split by newline to process line by line, handling both LF and CRLF
3+
const lines = content.split(/\r?\n/)
4+
const result: string[] = []
5+
let is_in_block_comment = false
6+
let skip_body_depth = 0
7+
// Buffer to track context for stripping bodies (e.g. "class Foo ")
8+
let last_code_buffer = ''
9+
10+
for (const line of lines) {
11+
let processed_line = ''
12+
let i = 0
13+
let is_in_string: false | '"' | "'" | '`' = false
14+
15+
while (i < line.length) {
16+
const char = line[i]
17+
const next_char = line[i + 1]
18+
19+
// Handle Block Comments
20+
if (is_in_block_comment) {
21+
if (char == '*' && next_char == '/') {
22+
is_in_block_comment = false
23+
i += 2
24+
} else {
25+
i++
26+
}
27+
continue
28+
}
29+
30+
// Handle Strings (ignore comments inside strings)
31+
if (is_in_string) {
32+
if (char == '\\') {
33+
// Handle escaped characters inside string
34+
if (skip_body_depth === 0) {
35+
processed_line += char + (next_char || '')
36+
}
37+
last_code_buffer += char + (next_char || '')
38+
i += 2
39+
continue
40+
}
41+
if (char === is_in_string) {
42+
is_in_string = false
43+
}
44+
if (skip_body_depth === 0) {
45+
processed_line += char
46+
}
47+
last_code_buffer += char
48+
i++
49+
continue
50+
}
51+
52+
// Start of String
53+
if (char == '"' || char === "'" || char == '`') {
54+
is_in_string = char
55+
if (skip_body_depth === 0) {
56+
processed_line += char
57+
}
58+
last_code_buffer += char
59+
i++
60+
continue
61+
}
62+
63+
// Start of Line Comment
64+
if (char == '/' && next_char == '/') {
65+
break // Ignore the rest of the line
66+
}
67+
68+
// Start of Block Comment
69+
if (char == '/' && next_char == '*') {
70+
is_in_block_comment = true
71+
i += 2
72+
continue
73+
}
74+
75+
// Handle Function Body Stripping
76+
if (char == '{') {
77+
if (skip_body_depth > 0) {
78+
// Already skipping, just deepen the level
79+
skip_body_depth++
80+
last_code_buffer += char
81+
i++
82+
continue
83+
} else {
84+
// Check if we should preserve this block (class, interface, etc.)
85+
// We look at last_code_buffer for keywords
86+
const is_keeper =
87+
/(^|\s)(class|interface|enum|namespace|type)(\s|$)/.test(
88+
last_code_buffer
89+
)
90+
// Also preserve object literals (preceded by =, :, or open paren for destructuring)
91+
const is_object_literal = /(=|:|\()\s*$/.test(
92+
last_code_buffer.trimEnd()
93+
)
94+
95+
if (!is_keeper && !is_object_literal) {
96+
// Start skipping
97+
skip_body_depth = 1
98+
const indent = processed_line.match(/^\s*/)?.[0] || ''
99+
processed_line += `{\n${indent} // ...\n${indent}}`
100+
last_code_buffer += '{}'
101+
i++
102+
continue
103+
} else {
104+
// Keeper: reset buffer to avoid false positives in nested scopes
105+
last_code_buffer = ''
106+
}
107+
}
108+
} else if (char == '}') {
109+
if (skip_body_depth > 0) {
110+
skip_body_depth--
111+
last_code_buffer += char
112+
i++
113+
continue
114+
}
115+
}
116+
117+
// Regular character
118+
if (skip_body_depth == 0) {
119+
processed_line += char
120+
}
121+
last_code_buffer += char
122+
i++
123+
}
124+
125+
// We trim the line to actually "compact" the file.
126+
// If a line contained only comments, it becomes empty and is skipped.
127+
const trimmed = processed_line.trim()
128+
if (trimmed) {
129+
result.push(trimmed)
130+
}
131+
132+
// Add a space to buffer for newline to avoid concatenation issues (e.g. "class Foo" + "{")
133+
last_code_buffer += ' '
134+
// Limit buffer size to avoid memory issues, keeping enough for context
135+
if (last_code_buffer.length > 200) {
136+
last_code_buffer = last_code_buffer.slice(-200)
137+
}
138+
}
139+
140+
return result.length > 0 ? result.join('\n') + '\n' : ''
141+
}

‎packages/vscode/src/context/utils/compact-file/test-cases/c-style/complex-mixed-content/input.txt‎

Lines changed: 0 additions & 6 deletions
This file was deleted.

‎packages/vscode/src/context/utils/compact-file/test-cases/c-style/complex-mixed-content/output.txt‎

Lines changed: 0 additions & 3 deletions
This file was deleted.

‎packages/vscode/src/context/utils/compact-file/test-cases/c-style/handle-escaped-quotes/input.txt‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎packages/vscode/src/context/utils/compact-file/test-cases/c-style/handle-escaped-quotes/output.txt‎

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
function test() {
2+
const url = "http://example.com"; // URL with //
3+
/* Start block
4+
End block */
5+
return `Result: ${url}`;
6+
}
7+
8+
const str = "String with \"// quote"; // real comment
9+
10+
function add(a, b) {
11+
return a + b;
12+
}
13+
14+
class Calculator {
15+
constructor() {
16+
this.value = 0;
17+
}
18+
19+
add(n) {
20+
this.value += n;
21+
}
22+
}
23+
24+
interface Item {
25+
id: string;
26+
name: string;
27+
}
28+
29+
const double = (x) => {
30+
return x * 2;
31+
}
32+
33+
const config = {
34+
env: "production",
35+
debug: false
36+
};
37+
38+
// comment
39+
/* block */
40+
41+
const str = "/* this is not a comment */";
42+
43+
const str = "// this is not a comment";
44+
45+
const str = '// this is not a comment';
46+
47+
const str = `// this is not a comment`;
48+
49+
const a = 1; /* comment */ const b = 2;
50+
51+
const a = 1;
52+
/*
53+
This is a multi-line
54+
comment
55+
*/
56+
const b = 2;
57+
58+
const a = 1; // This is a comment
59+
// This is a full line comment
60+
const b = 2;

0 commit comments

Comments
 (0)