Skip to content

Commit 11f6591

Browse files
committed
feat(apon.js): Enhance APON.parse and APON.stringify for root-level structures
This commit introduces significant enhancements to `apon.js` to improve its flexibility and performance: 1. **APON.parse Improvements:** - Enabled parsing of root-level APON documents enclosed in curly braces (`{}`) for objects and square brackets (`[]`) for arrays. - Corrected parsing logic to properly handle empty root-level objects (`{}`) and arrays (`[]`). - Improved parsing of nested arrays within array structures. - Added robust error handling for unexpected content after root elements and misplaced closing braces/brackets at the top level, aligning with `AponParser.java` behavior. 2. **APON.stringify Enhancements:** - Modified `APON.stringify` to accept and correctly serialize root-level arrays, resolving the "APON.stringify input must be a non-array object" error. - Refactored string generation within `APON.stringify` and its helper functions (`stringifyObject`, `stringifyValue`) to use array-based concatenation (`Array.prototype.join()`) instead of repeated `+` operator. This improves performance and readability, especially for large or complex APON structures. These changes align `apon.js`'s behavior more closely with the Java `AponParser` and enhance its utility for handling diverse APON formats.
1 parent 9492ce1 commit 11f6591

File tree

2 files changed

+205
-46
lines changed

2 files changed

+205
-46
lines changed

lib/apon.js

Lines changed: 94 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
}
9797
return value;
9898
}
99-
99+
100100
function unescapeString(str) {
101101
// A simple unescape for common sequences. A full implementation would be more complex.
102102
return str.replace(/\"/g, '"').replace(/\'/, "'").replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\\\/g, '\\');
@@ -117,9 +117,17 @@
117117
return obj;
118118
}
119119

120+
if (endChar === null) {
121+
if (line === CURLY_BRACKET_CLOSE || line === SQUARE_BRACKET_CLOSE) {
122+
throw new Error('Invalid APON format: Unexpected closing brace "' + line + '" at top level on line ' + i);
123+
}
124+
}
125+
120126
if (Array.isArray(obj)) {
121127
if (line === CURLY_BRACKET_OPEN) {
122128
obj.push(parseObject(CURLY_BRACKET_CLOSE));
129+
} else if (line === SQUARE_BRACKET_OPEN) {
130+
obj.push(parseObject(SQUARE_BRACKET_CLOSE));
123131
} else {
124132
obj.push(parseValue(line, null));
125133
}
@@ -133,7 +141,7 @@
133141

134142
let name = line.substring(0, separatorIndex).trim();
135143
let value = line.substring(separatorIndex + 1).trim();
136-
144+
137145
let typeHint = null;
138146
const typeHintIndex = name.indexOf(ROUND_BRACKET_OPEN);
139147
if (typeHintIndex > 0 && name.endsWith(ROUND_BRACKET_CLOSE)) {
@@ -169,10 +177,65 @@
169177
obj[name] = parseValue(value, typeHint);
170178
}
171179
}
180+
181+
if (endChar) {
182+
throw new Error('Invalid APON format: Unclosed block, missing "' + endChar + '"');
183+
}
184+
172185
return obj;
173186
}
174187

175-
return parseObject(null); // Top-level is an object without a closing char
188+
let firstLine = '';
189+
let firstLineIndex = -1;
190+
for (let j = 0; j < lines.length; j++) {
191+
const trimmedLine = lines[j].trim();
192+
if (trimmedLine && !trimmedLine.startsWith(COMMENT_LINE_START)) {
193+
firstLine = trimmedLine;
194+
firstLineIndex = j;
195+
break;
196+
}
197+
}
198+
199+
if (firstLineIndex === -1) {
200+
const trimmedText = text.trim();
201+
if (trimmedText === SQUARE_BRACKET_OPEN + SQUARE_BRACKET_CLOSE) {
202+
return [];
203+
}
204+
return {};
205+
}
206+
207+
if (firstLine === CURLY_BRACKET_OPEN + CURLY_BRACKET_CLOSE) {
208+
return {};
209+
}
210+
if (firstLine === SQUARE_BRACKET_OPEN + SQUARE_BRACKET_CLOSE) {
211+
return [];
212+
}
213+
214+
if (firstLine === CURLY_BRACKET_OPEN) {
215+
i = firstLineIndex + 1;
216+
const result = parseObject(CURLY_BRACKET_CLOSE);
217+
while(i < lines.length) {
218+
const line = lines[i].trim();
219+
if (line && !line.startsWith(COMMENT_LINE_START)) {
220+
throw new Error('Invalid APON format: Unexpected content after closing brace "}" at line ' + (i + 1));
221+
}
222+
i++;
223+
}
224+
return result;
225+
} else if (firstLine === SQUARE_BRACKET_OPEN) {
226+
i = firstLineIndex + 1;
227+
const result = parseObject(SQUARE_BRACKET_CLOSE);
228+
while(i < lines.length) {
229+
const line = lines[i].trim();
230+
if (line && !line.startsWith(COMMENT_LINE_START)) {
231+
throw new Error('Invalid APON format: Unexpected content after closing bracket "]" at line ' + (i + 1));
232+
}
233+
i++;
234+
}
235+
return result;
236+
} else {
237+
return parseObject(null);
238+
}
176239
};
177240

178241
/**
@@ -183,8 +246,8 @@
183246
* @returns {string} The APON string representation of the value.
184247
*/
185248
APON.stringify = function(obj, options) {
186-
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
187-
throw new Error('APON.stringify input must be a non-array object.');
249+
if (typeof obj !== 'object' || obj === null) {
250+
throw new Error('APON.stringify input must be an object or an array.');
188251
}
189252

190253
let indentString = ' ';
@@ -203,7 +266,7 @@
203266
return true;
204267
}
205268
// Quote for special characters, keywords, and numeric-like strings.
206-
if (/[:#"'\\\[\]]/.test(str)) {
269+
if (/[{}:#"'\\\[\]]/.test(str) || /^\s*\(/.test(str)) {
207270
return true;
208271
}
209272
if (str === 'null' || str === 'true' || str === 'false') {
@@ -224,12 +287,8 @@
224287
if (typeof value === 'string') {
225288
if (value.includes('\n')) { // Multi-line text
226289
const lines = value.split('\n');
227-
let result = '(\n';
228-
for (const line of lines) {
229-
result += indent + indentString + TEXT_LINE_START + line + '\n';
230-
}
231-
result += indent + ')';
232-
return result;
290+
const content = lines.map(line => `${indent}${indentString}${TEXT_LINE_START}${line}`);
291+
return `(\n${content.join('\n')}\n${indent})`;
233292
} else {
234293
return needsQuotes(value) ? `"${escapeString(value)}"` : value;
235294
}
@@ -242,45 +301,37 @@
242301

243302
function stringifyObject(obj, indent) {
244303
const newIndent = indent + indentString;
245-
let str = '';
246-
247304
if (Array.isArray(obj)) {
248-
str += '[';
249-
if (obj.length > 0) {
250-
str += '\n';
251-
for (const item of obj) {
252-
str += newIndent + stringifyValue(item, newIndent) + '\n';
253-
}
254-
str += indent;
255-
}
256-
str += ']';
305+
if (obj.length === 0) return '[]';
306+
const content = obj.map(item => newIndent + stringifyValue(item, newIndent));
307+
return `[\n${content.join('\n')}\n${indent}]`;
257308
} else {
258-
str += '{';
259309
const keys = Object.keys(obj);
260-
if (keys.length > 0) {
261-
str += '\n';
262-
for (const key of keys) {
310+
if (keys.length === 0) return '{}';
311+
const content = keys
312+
.map(key => {
263313
const value = obj[key];
264-
if (value !== undefined) {
265-
str += newIndent + key + ': ' + stringifyValue(value, newIndent) + '\n';
266-
}
267-
}
268-
str += indent;
269-
}
270-
str += '}';
314+
if (value === undefined) return null;
315+
return `${newIndent}${key}: ${stringifyValue(value, newIndent)}`;
316+
})
317+
.filter(line => line !== null);
318+
return `{\n${content.join('\n')}\n${indent}}`;
271319
}
272-
return str;
273320
}
274-
275-
const keys = Object.keys(obj);
276-
const lines = [];
277-
for (const key of keys) {
278-
const value = obj[key];
279-
if (value !== undefined) {
280-
lines.push(`${key}: ${stringifyValue(value, '')}`);
321+
322+
if (Array.isArray(obj)) {
323+
return stringifyObject(obj, '');
324+
} else {
325+
const keys = Object.keys(obj);
326+
const lines = [];
327+
for (const key of keys) {
328+
const value = obj[key];
329+
if (value !== undefined) {
330+
lines.push(`${key}: ${stringifyValue(value, '')}`);
331+
}
281332
}
333+
return lines.join('\n');
282334
}
283-
return lines.join('\n');
284335
};
285336

286337
if (typeof module !== 'undefined' && module.exports) {

test/apon.test.js

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,89 @@ describe('APON.parse', () => {
4343
const expected = { name: 'Test' };
4444
expect(APON.parse(aponText)).toEqual(expected);
4545
});
46+
47+
it('should parse a root-level object enclosed in braces', () => {
48+
const aponText = `{
49+
name: John Doe
50+
age: 30
51+
}`;
52+
const expected = { name: 'John Doe', age: 30 };
53+
expect(APON.parse(aponText)).toEqual(expected);
54+
});
55+
56+
it('should parse a root-level array', () => {
57+
const aponText = `[
58+
"apple"
59+
"banana"
60+
"cherry"
61+
]`;
62+
const expected = ["apple", "banana", "cherry"];
63+
expect(APON.parse(aponText)).toEqual(expected);
64+
});
65+
66+
it('should parse a root-level array with nested objects', () => {
67+
const aponText = `[
68+
{
69+
name: John
70+
}
71+
{
72+
name: Jane
73+
}
74+
]`;
75+
const expected = [{ name: 'John' }, { name: 'Jane' }];
76+
expect(APON.parse(aponText)).toEqual(expected);
77+
});
78+
79+
it('should parse nested arrays', () => {
80+
const aponText = `[
81+
[
82+
1
83+
2
84+
]
85+
[
86+
3
87+
4
88+
]
89+
]`;
90+
const expected = [[1, 2], [3, 4]];
91+
expect(APON.parse(aponText)).toEqual(expected);
92+
});
93+
94+
it('should parse an empty root-level object', () => {
95+
const aponText = '{}';
96+
const expected = {};
97+
expect(APON.parse(aponText)).toEqual(expected);
98+
});
99+
100+
it('should parse an empty root-level array', () => {
101+
const aponText = '[]';
102+
const expected = [];
103+
expect(APON.parse(aponText)).toEqual(expected);
104+
});
105+
106+
it('should throw an error for unexpected content after a root object', () => {
107+
const aponText = `{
108+
name: Test
109+
}
110+
unexpected: content`;
111+
expect(() => APON.parse(aponText)).toThrow('Invalid APON format: Unexpected content after closing brace "}" at line 4');
112+
});
113+
114+
it('should throw an error for unexpected content after a root array', () => {
115+
const aponText = `[
116+
1
117+
]
118+
unexpected: content`;
119+
expect(() => APON.parse(aponText)).toThrow('Invalid APON format: Unexpected content after closing bracket "]" at line 4');
120+
});
121+
122+
it('should throw an error for unexpected closing brace at top level', () => {
123+
const aponText = `
124+
name: Test
125+
}
126+
`;
127+
expect(() => APON.parse(aponText)).toThrow('Invalid APON format: Unexpected closing brace "}" at top level on line 3');
128+
});
46129
});
47130

48131
describe('APON.stringify', () => {
@@ -70,9 +153,34 @@ describe('APON.stringify', () => {
70153
expect(APON.stringify(obj)).toBe(expected);
71154
});
72155

73-
it('should throw an error for non-object inputs', () => {
74-
expect(() => APON.stringify("a string")).toThrow('APON.stringify input must be a non-array object.');
75-
expect(() => APON.stringify([1, 2, 3])).toThrow('APON.stringify input must be a non-array object.');
156+
it('should stringify a root-level array', () => {
157+
const obj = ['a', 'b', 'c'];
158+
const expected = '[\n a\n b\n c\n]';
159+
expect(APON.stringify(obj)).toBe(expected);
160+
});
161+
162+
it('should stringify a root-level array with nested object blocks', () => {
163+
const obj = [
164+
{ name: 'Alice', age: 30 },
165+
{ name: 'Bob', age: 25 }
166+
];
167+
const expected = `[
168+
{
169+
name: Alice
170+
age: 30
171+
}
172+
{
173+
name: Bob
174+
age: 25
175+
}
176+
]`;
177+
expect(APON.stringify(obj)).toBe(expected);
178+
});
179+
180+
it('should throw an error for non-object/array inputs', () => {
181+
expect(() => APON.stringify("a string")).toThrow('APON.stringify input must be an object or an array.');
182+
expect(() => APON.stringify(123)).toThrow('APON.stringify input must be an object or an array.');
183+
expect(() => APON.stringify(null)).toThrow('APON.stringify input must be an object or an array.');
76184
});
77185
});
78186

0 commit comments

Comments
 (0)