diff --git a/src/datastructures/json-render-buffer.js b/src/datastructures/json-render-buffer.js index 3128110..c881102 100644 --- a/src/datastructures/json-render-buffer.js +++ b/src/datastructures/json-render-buffer.js @@ -1,6 +1,6 @@ import FixedSizeQueue from "./fixed-size-queue"; -class RenderBuffer { +class JSONRenderBuffer { constructor() { this.queue = new FixedSizeQueue(8); } diff --git a/src/processors/beautifier.js b/src/processors/beautifier.js new file mode 100644 index 0000000..53c8bad --- /dev/null +++ b/src/processors/beautifier.js @@ -0,0 +1,216 @@ +import FixedSizeQueue from "../datastructures/fixed-size-queue.js"; + +const MAX_ARRAY_TOKENS = 8; +const MAX_ARRAY_LENGTH = 32; + +class Token { + constructor() { + + } + + getValue() { + throw new Error('Get value was not implemented'); + } + + getValueExpanded() { + return this.getValue(); + } + + indentable() { + return true; + } +} + +class OpenBracket extends Token { + constructor() { + super(); + } + + getValue() { + return '['; + } + + getValueExpanded() { + return '[\n'; + } +} + + +class CloseBracket extends Token { + constructor() { + super(); + } + + getValue() { + return ']'; + } + + getValueExpanded() { + return '\n]'; + } +} + + +class NonBracketToken extends Token {} + +class Comma extends NonBracketToken { + constructor() { + super(); + } + + getValue() { + return ', '; + } + + getValueExpanded() { + return ',\n'; + } + + indentable() { + return false; + } +} + + + +class Value extends NonBracketToken { + constructor(str) { + super(); + this.str = str; + } + + getValue() { + return this.str; + } +} + +class JString extends Value { + +} + +class Number extends Value { + +} + +class Literal extends Token { + +} + + + + +class Beautifier { + constructor() { + this.queue = new FixedSizeQueue(MAX_ARRAY_TOKENS + 2); // beginArray + max_tokens + 1 extra token + this.indent_level = 0; + this.open_arrays = 0; + this.output = ''; + } + + beginArray() { + // Add the square bracket to the queue + this.queue.enqueue(new OpenBracket()); + this.open_arrays++; + // Adding a new array may overextend the furthest array, check if it does + this._check_queue(); + return this; + } + + endArray() { + // If there is one array open, it can be printed collapsed + this.queue.enqueue(new CloseBracket()); + this._check_queue(); + + if(this.open_arrays <= 1) { + this._print_queue_collapsed(); + } + + // If there are more than one array, then it may be nested, and we cannot might not be able to print the outer array, do nothing + + // TODO If there is no array open, then, it has been printed already, print a new line and then print this symbol + + this.open_arrays--; + return this; + } + + comma() { + this.queue.enqueue(new Comma()); + // Adding a new array may overextend the furthest array, check if it does + this._check_queue(this.queue.getSize() === 1); + return this; + } + + // Represents all other tokens, for brevity + token(str) { + this.queue.enqueue(new JString(str)); + // Adding a new array may overextend the furthest array, check if it does + this._check_queue(this.queue.getSize() === 1); + return this; + } + + _check_queue(force = false) { + // TODO check also that the char length has not been exceeded + if(force|| this.queue.getSize() >= MAX_ARRAY_TOKENS + 1) { + if(this.queue.peek() instanceof OpenBracket) { + let token = this.queue.dequeue(); + this._add_output(token.getValueExpanded()); + this.open_arrays--; + this.indent_level++; + } + + while(!this.queue.isEmpty() && + this.queue.peek() instanceof NonBracketToken) { + let next_token = this.queue.dequeue(); + this._add_output(next_token.getValueExpanded(), next_token.indentable()); + } + + this._check_queue(); // May have been overextended on more than one level + } + } + + _print_queue_collapsed() { + let open_arrays = 0; + let text = ''; + //this._add_output(this.queue.toArray();); + while(!this.queue.isEmpty()) { + let token = this.queue.dequeue(); + if(token instanceof OpenBracket) { + text += token.getValue(); + open_arrays++; + } else if(token instanceof NonBracketToken) { + text += token.getValue(); + /*if(this.queue.peek().type === 'token') { + text += ' '; + }*/ + } else if(token instanceof CloseBracket) { + if(open_arrays > 0) { + text += token.getValue(); + open_arrays--; + } else { + + this.indent_level--; + + text += '\n'; + for(let i = 0; i < this.indent_level; i++) { + text += '\t'; + } + + text += token.getValue(); + + } + } + } + this._add_output(text); + } + + _add_output(str, indentable=true) { + if (indentable) { + for (let i = 0; i < this.indent_level; i++) { + this.output += '\t'; + } + } + this.output += str; + } +} + +export default Beautifier; \ No newline at end of file diff --git a/tests/processors/beautifier.test.js b/tests/processors/beautifier.test.js new file mode 100644 index 0000000..9c30df9 --- /dev/null +++ b/tests/processors/beautifier.test.js @@ -0,0 +1,210 @@ +import Beautifier from '../../src/processors/beautifier.js'; + +describe('Beautifier', () => { + let buffer; + + beforeEach(() => { + buffer = new Beautifier(); + }); + + describe('empty arrays', () => { + test('creates an empty array', () => { + buffer.beginArray().endArray(); + expect(buffer.output).toBe('[]'); + }); + + test('creates multiple consecutive empty arrays', () => { + buffer.beginArray().endArray().beginArray().endArray(); + expect(buffer.output).toBe('[][]'); + }); + }); + + describe('simple arrays with strings', () => { + test('creates a single-element array', () => { + buffer.beginArray().token('"hello"').endArray(); + expect(buffer.output).toBe('["hello"]'); + }); + + test('creates a multi-element array', () => { + buffer.beginArray() + .token('"hello"') + .comma() + .token('"world"') + .endArray(); + expect(buffer.output).toBe('["hello", "world"]'); + }); + + test('creates an array with multiple elements that stays collapsed', () => { + buffer.beginArray() + .token('"one"') + .comma() + .token('"two"') + .comma() + .token('"three"') + .endArray(); + expect(buffer.output).toBe('["one", "two", "three"]'); + }); + }); + + describe('nested arrays', () => { + test('creates nested arrays that stay collapsed', () => { + buffer.beginArray() + .beginArray() + .token('"inner"') + .endArray() + .endArray(); + expect(buffer.output).toBe('[["inner"]]'); + }); + + test.only('creates nested arrays with multiple elements', () => { + buffer.beginArray() + .beginArray() + .token('"a"') + .comma() + .token('"b"') + .endArray() + .comma() + .beginArray() + .token('"c"') + .comma() + .token('"d"') + .endArray() + .endArray(); + expect(buffer.output).toBe('[["a", "b"], ["c", "d"]]'); + }); + }); + + describe('array expansion', () => { + test('expands array when it exceeds MAX_ARRAY_TOKENS', () => { + buffer.beginArray(); + for (let i = 0; i < 8; i++) { + if (i > 0) buffer.comma(); + buffer.token(`"item${i}"`); + } + // Exceeding the limit should cause expansion + buffer.comma().token('"overflow"'); + + // The array should be formatted with newlines and indentation + expect(buffer.output).toMatch(/\[\n\t"item0"/); + expect(buffer.output).toMatch(/,\n\t"item1"/); + expect(buffer.output).toMatch(/,\n\t"overflow"/); + }); + + test('handles nested arrays that cause expansion', () => { + buffer.beginArray() + .beginArray(); + + // Fill the inner array to the point of expansion + for (let i = 0; i < 9; i++) { + if (i > 0) buffer.comma(); + buffer.token(`"nested${i}"`); + } + buffer.endArray().endArray(); + + // Check that the output is expanded with proper indentation + expect(buffer.output).toMatch(/\[\n\s*\[/); + expect(buffer.output).toMatch(/\n\t\t"nested0"/); + }); + }); + + describe('mixed collapsed and expanded arrays', () => { + test('creates a mix of collapsed inner arrays within an expanded outer array', () => { + buffer.beginArray(); + + // Add multiple short inner arrays + for (let i = 0; i < 9; i++) { + if (i > 0) buffer.comma(); + buffer.beginArray() + .token(`"short${i}"`) + .endArray(); + } + buffer.endArray(); + + // The outer array should be expanded but inner arrays should stay collapsed + expect(buffer.output).toMatch(/\[\n\t\["short0"\]/); + expect(buffer.output).toMatch(/,\n\t\["short1"\]/); + expect(buffer.output).toMatch(/\n\]/); + }); + }); + + describe('complex structures', () => { + test('handles deeply nested arrays with mixed expansion', () => { + buffer.beginArray() // Outer array + .beginArray() // First inner array + .beginArray() // Deeply nested array + .token('"deep"'); + + // Fill to cause expansion + for (let i = 1; i < 9; i++) { + buffer.comma().token(`"deep${i}"`); + } + + buffer.endArray() + .endArray() + .comma() + .beginArray() // Second inner array + .token('"simple"') + .endArray() + .endArray(); + + // Check proper nesting and indentation + expect(buffer.output).toMatch(/\[\n\t\[\n\t\t\[/); + expect(buffer.output).toMatch(/\n\t\t\t"deep"/); + expect(buffer.output).toMatch(/\["simple"\]/); + }); + }); + + describe('edge cases', () => { + test('handles arrays with only commas', () => { + buffer.beginArray().comma().endArray(); + expect(buffer.output).toBe('[, ]'); + }); + + test('handles empty strings as array elements', () => { + buffer.beginArray() + .token('""') + .comma() + .token('""') + .endArray(); + expect(buffer.output).toBe('["", ""]'); + }); + + test('handles commas without preceding or following elements', () => { + buffer.beginArray() + .comma() + .comma() + .token('"only_value"') + .comma() + .comma() + .endArray(); + expect(buffer.output).toBe('[, , "only_value", , ]'); + }); + + test('handles immediate expansion due to force parameter', () => { + buffer.beginArray() + .token('"forced"') + .comma(); // This forces expansion immediately + + expect(buffer.output).toMatch(/\[\n\t"forced",\n/); + }); + }); + + describe('queue behavior', () => { + test('checks queue size limits', () => { + const maxTokens = 8; + + buffer.beginArray(); + for (let i = 0; i < maxTokens; i++) { + if (i > 0) buffer.comma(); + buffer.token(`"item${i}"`); + } + + // Should still be collapsed + expect(buffer.output).toBe(''); + + // Adding one more should trigger expansion + buffer.comma().token('"overflow"'); + expect(buffer.output).not.toBe(''); + }); + }); +}); \ No newline at end of file