Skip to content

Commit

Permalink
feat: add balancer
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re committed Sep 25, 2024
1 parent 3ad3cfa commit 3aea579
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 25 deletions.
48 changes: 48 additions & 0 deletions src/balancer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
function replaceWithWhitespace(input: string, pattern: RegExp): string {
return input.replace(pattern, match => ' '.repeat(match.length));
}
export function isBalanced(sourceCode = ''): boolean {
function preprocess(code: string): string {
const patterns = [
// single-line comments
/\/\/.*$/gm,
// multi-line comments
/\/\*[\s\S]*?\*\//g,
// string literals (both single and double quotes), handling escaped quotes
/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g,
// regex literals, handling escaped forward slashes
/\/(?:[^\/\\]|\\.)*\/[gimsuy]*/g,
]
let result = code;
for (const pattern of patterns) {
result = replaceWithWhitespace(result, pattern);
}
return result;
}

// Step 2: Check for balanced brackets
function checkBalance(code: string): boolean {
const bracketPairs = {
")": "(",
"]": "[",
"}": "{"
};
const stack: string[] = [];
const bracketRegex = /[()[{}\]]/g;

let match: RegExpExecArray | null;
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
while ((match = bracketRegex.exec(code)) !== null) {
const char = match[0];
if ("([{".includes(char)) {
stack.push(char);
} else if (stack.length === 0 || stack.pop() !== bracketPairs[char]) {
return false;
}
}

return stack.length === 0;
}

return checkBalance(preprocess(sourceCode));
}
29 changes: 4 additions & 25 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isBalanced } from "./balancer.js";

export type Node =
| DocumentNode
| ElementNode
Expand Down Expand Up @@ -104,29 +106,6 @@ const RAW_TAGS = new Set<string>(["script", "style"]);
const SPLIT_ATTRS_RE = /([\@\.a-z0-9_\:\-]*)\s*?=?\s*?(['"]?)([\s\S]*?)\2\s+/gim;
const DOM_PARSER_RE =
/(?:<(\/?)([a-zA-Z][a-zA-Z0-9\:-]*)(?:\s([^>]*?))?((?:\s*\/)??)>|(<\!\-\-)([\s\S]*?)(\-\->)|(<\!)([\s\S]*?)(>))/gm;

function isBalanced(str?: string, attr: boolean = false) {
if (!str) return true;
if (!attr && !/=\{/.test(str)) return true;
let track: Record<string, number> = {
'{': 0,
'(': 0,
'[': 0,
}
for (let i = 0; i < str.length; i++) {
const c = str[i];
if (track[c] !== undefined) {
track[c]++;
} else if (c === '}') {
track['{']--;
} else if (c === ')') {
track['(']--;
} else if (c === ']') {
track['[']--;
}
}
return Object.values(track).every(v => v === 0);
}
function splitAttrs(str?: string) {
let obj: Record<string, string> = {};
let token: any;
Expand All @@ -137,14 +116,14 @@ function splitAttrs(str?: string) {
if (token[0] === " ") continue;
if (pending) {
obj[pending] += token[0];
if (isBalanced(obj[pending], true)) {
if (isBalanced(obj[pending])) {
obj[pending] = obj[pending].trimEnd();
pending = '';
}
continue;
}
if (token[0] === " ") continue;
if (token[3][0] === '{' && !isBalanced(token[3], true)) {
if (token[3][0] === '{' && !isBalanced(token[3])) {
pending = token[1];
obj[pending] = token[0].slice(token[1].length + 1);
continue;
Expand Down
75 changes: 75 additions & 0 deletions test/balancer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, test, expect } from "vitest";
import { isBalanced } from '../src/balancer';

describe('isBalanced', () => {
test('Empty string', () => {
expect(isBalanced('')).toBe(true);
});

test('Balanced brackets', () => {
expect(isBalanced('()')).toBe(true);
expect(isBalanced('()[]{}')).toBe(true);
expect(isBalanced('([{}])')).toBe(true);
});

test('Unbalanced brackets', () => {
expect(isBalanced('(')).toBe(false);
expect(isBalanced(')')).toBe(false);
expect(isBalanced('(]')).toBe(false);
expect(isBalanced('([)]')).toBe(false);
});

test('Brackets in strings', () => {
expect(isBalanced('"()"')).toBe(true);
expect(isBalanced("'()'")).toBe(true);
expect(isBalanced('const str = "(["')).toBe(true);
});

test('Escaped characters in strings', () => {
expect(isBalanced('"\\""')).toBe(true);
expect(isBalanced("'\\''")).toBe(true);
expect(isBalanced('const str = "(\\"["')).toBe(true);
});

test('Comments', () => {
expect(isBalanced('// )')).toBe(true);
expect(isBalanced('/* } */')).toBe(true);
expect(isBalanced('// (\nconst x = 1;')).toBe(true);
});

test('Regular expressions', () => {
expect(isBalanced('/\\(/')).toBe(true);
expect(isBalanced('const regex = /\\[/g')).toBe(true);
});

test('Complex code snippets', () => {
expect(isBalanced(`
function test() {
const str = "({[]})";
// Comment (])
return str.match(/\\(/g);
}
`)).toBe(true);

expect(isBalanced(`
const obj = {
key: '(]',
nested: {
array: [1, 2, 3]
}
};
/* Multi-line
comment } */
console.log(obj);
`)).toBe(true);
});

test('Unbalanced complex code', () => {
expect(isBalanced(`
function broken( {
const str = "(unmatched";
return str;
}
`)).toBe(false);
});
});

0 comments on commit 3aea579

Please sign in to comment.