mason-parser 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,37 @@
1
1
  # mason-parser
2
2
 
3
- > Markdown Structured Object Notation (MSON) — The human-centric serialization format that bridges natural markdown visual structure and lightweight, structured JSON.
3
+ > Markdown Structured Object Notation (MaSON) — A human-centric serialization format bridging natural markdown visual hierarchy and structured JSON.
4
4
 
5
5
  [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
6
  [![NPM Version](https://img.shields.io/npm/v/mason-parser.svg)](https://www.npmjs.com/package/mason-parser)
7
7
 
8
- MSON is a super-lightweight alternative to JSON/YAML/TOML designed specifically for **configuration files**, **static content documents**, and **efficient Prompting / Context storage for Large Language Models (LLMs)**. It reduces token overhead by **15% to 30%** compared to standard JSON by replacing heavy syntactic delimiters (such as braces, brackets, and redundant key quotes) with standard, beautiful Markdown headers and itemized lists.
8
+ MaSON is a lightweight serialization format designed for **human-authored hierarchical documents**, **application configurations**, and **clean context-sharing for Large Language Models (LLMs)**. It replaces structural punctuation (such as curly braces, brackets, and redundant double-quoted keys) with natural Markdown headings and bulleted lists.
9
+
10
+ ---
11
+
12
+ ## 🎯 What Problem Does MaSON Solve?
13
+
14
+ MaSON fits a specific niche: **human-authored hierarchical documents where visual readability and writeability are more important than representing every complex or strict academic data type.**
15
+
16
+ ### How it Compares:
17
+ * **Why not JSON?** JSON is a fantastic machine-to-machine format but is tedious for humans to author or edit. Forgetting a trailing comma, double quote, or bracket results in immediate syntax errors. MaSON allows writing hierarchy naturally.
18
+ * **Why not YAML?** YAML is powerful, but its specification is substantially more complex than MaSON's. Its indentation-sensitive syntax can also lead to subtle formatting errors when editing by hand. MaSON uses standard Markdown headings to nest structures safely.
19
+ * **Why not TOML?** TOML is highly readable for flat structures, but its readability degrades rapidly when representing deeply nested hierarchies (requiring verbose table brackets like `[servers.database.credentials]`). MaSON utilizes standard Markdown heading depths (`#`, `##`, `###`) to establish nesting levels.
20
+
21
+ ---
22
+
23
+ ## 🎨 Design Philosophy
24
+
25
+ ### Design Goals
26
+ - **Easy to read in plain text:** Looks like regular Markdown documentation.
27
+ - **Easy to author manually:** No strict whitespace parsing, bracket-matching, or comma rules.
28
+ - **Easy to parse:** Extremely small parser footprint (under 2KB gzipped) with a small, maintainable grammar.
29
+ - **Deterministic & Round-trip Safe:** Standard structures parse and stringify back-and-forth cleanly.
30
+
31
+ ### Non-Goals
32
+ - **Full YAML/JSON feature-parity:** No support for advanced data types or custom type tagging.
33
+ - **Anchors, aliases, or references:** No complex object graphs or internal links.
34
+ - **YAML-style indentation rules:** No complex multi-space layout rules. MaSON prefers simple explicit visual suffixes (`[]`) or top-level annotations over indentation-sensitive arrays.
9
35
 
10
36
  ---
11
37
 
@@ -13,9 +39,9 @@ MSON is a super-lightweight alternative to JSON/YAML/TOML designed specifically
13
39
 
14
40
  - **Zero-Bracket Nesting:** Structure child objects implicitly via standard Markdown headings (`#`, `##`, `###`).
15
41
  - **Natural Array Triggers:** Bulleted lists (`*`, `-`, `+`) automatically morph parent containers into ordered arrays.
16
- - **Implicit Type Casting:** Native scanning of numbers, floats, booleans (`true`/`false`), and `null` values without tedious string conversions.
17
- - **LLM-Token Friendly:** Eliminates redundant nested syntax, resulting in massive prompt and response cost reductions when feeding data to Gemini, GPT, or Claude.
18
- - **Bi-directional Integrity:** Safely stringifies complex objects back to MSON, complete with sorted parameters and beautiful spaced structure.
42
+ - **Implicit Type Inference:** Native detection of numbers, floats, booleans (`true`/`false`), and `null` values without tedious string conversions.
43
+ - **Grounded Token Efficiency:** In nested configuration files and content-rich documents, MaSON can reduce raw characters compared to equivalent JSON by eliminating repeated brackets, braces, and quotation marks. This is especially useful in LLM workflows, where concise representations can help maximize available context and minimize token overhead.
44
+ - **Bi-directional Integrity:** Safely stringifies standard JavaScript objects back to MaSON, complete with sorted parameters and clean spacing.
19
45
 
20
46
  ---
21
47
 
@@ -52,7 +78,7 @@ host: localhost
52
78
  * BackupNode2
53
79
  ```
54
80
 
55
- ### 2. Parse MSON into JSON
81
+ ### 2. Parse MaSON into JSON
56
82
  ```typescript
57
83
  import { parse } from 'mason-parser';
58
84
  import fs from 'fs';
@@ -86,7 +112,7 @@ console.log(data);
86
112
  */
87
113
  ```
88
114
 
89
- ### 3. Stringify back to MSON
115
+ ### 3. Stringify back to MaSON
90
116
  ```typescript
91
117
  import { stringify } from 'mason-parser';
92
118
 
@@ -112,14 +138,84 @@ active: true
112
138
 
113
139
  ## 📜 Grammar & Formatting Rules
114
140
 
115
- 1. **Parameters:** Represented by `key: value` pairs. Value is implicitly parsed as a primitive (`number`, `boolean`, `null`, or `string`).
116
- 2. **Quoting:** To force any numerical sequence or boolean value to stay a string, simply wrap it in quotes: `zipCode: "16801"`.
141
+ At a high level, a MaSON document is parsed line-by-line using a small, deterministic grammar.
142
+
143
+ ```text
144
+ Document
145
+ ├── Line (Comment / Empty) -> Ignored
146
+ ├── Line (Top-Level Array: []) -> Initializes the document as an anonymous Array
147
+ ├── Line (Heading: #+) -> Opens a nested Object or Object within an Array
148
+ ├── Line (Bullet: *, -, +) -> Pushes a primitive value to an active array
149
+ └── Line (Property: k: v) -> Sets a property on the active container
150
+ ```
151
+
152
+ 1. **Parameters:** Represented by `key: value` pairs. The value is implicitly parsed as a primitive (`number`, `boolean`, `null`, or `string`).
153
+ 2. **Quoting:** To force any numerical sequence or boolean value to remain a string, wrap it in double or single quotes: `zipCode: "16801"`.
117
154
  3. **Headings:**
118
155
  - Any property before the first `#` is declared at the root level.
119
156
  - `#` headings denote level 1 object parameters.
120
157
  - `##` headings nest themselves inside the active `#` parent object.
121
- - If there is no active `#` heading, `##` headings fall back to root.
122
- 4. **Lists:** Bullet elements (`*`, `-`, `+`) immediately turn the nearest active heading key into an ordered array of typed primitives or objects.
158
+ - If there is no active parent at the required depth, headings fall back to root or nearest sibling.
159
+ 4. **Lists:** Bullet elements (`*`, `-`, `+`) immediately convert the nearest active heading key into an ordered array of typed primitives.
160
+ 5. **Explicit Array Suffix (`[]`):** To define an array of complex objects, append `[]` to the heading name (e.g., `# Users[]`). Any nested heading under it (e.g. `## User` or `##`) pushes a new object into that array.
161
+ 6. **Top-Level Anonymous Arrays:** To make the entire document parse as a top-level array, define `[]` on the very first non-empty line of the file. Each subsequent item header (like `##`) directly pushes a new anonymous object into this array.
162
+ 7. **Anonymous & Descriptive Heading Labels:** When defining elements of an array (explicit or top-level), heading names are fully optional. You can use empty heading lines (e.g., `## ` or `##`) or descriptive notes (e.g., `## Alice (Admin)`) without affecting the structured JSON properties.
163
+
164
+ ---
165
+
166
+ ## 🔍 Edge-Case Specification
167
+
168
+ To ensure predictable behavior, `mason-parser` handles edge cases according to the following rules:
169
+
170
+ ### 1. Duplicate Headings
171
+ ```markdown
172
+ # User
173
+ name: Alice
174
+
175
+ # User
176
+ name: Bob
177
+ ```
178
+ * **Behavior:** Overwrites. The second `# User` initializes a fresh object under the `User` key, overwriting the first one. To represent list-like collections, use bullet points under a single heading instead.
179
+
180
+ ### 2. Mixed-Type Arrays
181
+ ```markdown
182
+ # Items
183
+ * 1
184
+ * true
185
+ * hello
186
+ ```
187
+ * **Behavior:** Allowed. Bullets are parsed individually. The parser yields `[1, true, "hello"]`.
188
+
189
+ ### 3. Arrays of Objects
190
+ * **Behavior:** MaSON supports arrays of complex objects using the explicit array suffix (`[]`) or a top-level array modifier (`[]`). Inside these arrays, heading names can be customized with descriptive labels or left completely empty (anonymous objects). For example:
191
+ ```markdown
192
+ # Users[]
193
+
194
+ ## Administrator
195
+ name: Alice
196
+ role: admin
197
+
198
+ ##
199
+ name: Bob
200
+ role: user
201
+ ```
202
+ This parses directly into an array containing both objects under the key `"Users"`.
203
+
204
+ ### 4. Empty Headings
205
+ ```markdown
206
+ # Settings
207
+ ```
208
+ * **Behavior:** Resolves to `{}`. Encountering a heading immediately instantiates an empty object placeholder in the parent node.
209
+
210
+ ### 5. Escaping Special Characters
211
+ * To include colons or other special symbols in a string value, wrap the value in double quotes:
212
+ ```markdown
213
+ title: "Hello: World"
214
+ ```
215
+ * To represent a literal markdown header symbol inside a string value, wrap it:
216
+ ```markdown
217
+ banner: "# Welcome To My Server"
218
+ ```
123
219
 
124
220
  ---
125
221
 
package/dist/index.d.mts CHANGED
@@ -45,6 +45,6 @@ declare function parseWithTrace(text: string): ParseResult;
45
45
  /**
46
46
  * Stringifies a JavaScript object/array back into MSON text recursively.
47
47
  */
48
- declare function stringify(obj: any, level?: number): string;
48
+ declare function stringify(obj: any, level?: number, parentKey?: string): string;
49
49
 
50
50
  export { type ParseResult, type ParserTraceStep, parse, parse as parseMSON, parsePrimitiveValue, parseWithTrace, stringify, stringify as stringifyMSON, stringifyPrimitiveValue };
package/dist/index.d.ts CHANGED
@@ -45,6 +45,6 @@ declare function parseWithTrace(text: string): ParseResult;
45
45
  /**
46
46
  * Stringifies a JavaScript object/array back into MSON text recursively.
47
47
  */
48
- declare function stringify(obj: any, level?: number): string;
48
+ declare function stringify(obj: any, level?: number, parentKey?: string): string;
49
49
 
50
50
  export { type ParseResult, type ParserTraceStep, parse, parse as parseMSON, parsePrimitiveValue, parseWithTrace, stringify, stringify as stringifyMSON, stringifyPrimitiveValue };
package/dist/index.js CHANGED
@@ -60,7 +60,8 @@ function parseWithTrace(text) {
60
60
  const startTime = typeof performance !== "undefined" ? performance.now() : Date.now();
61
61
  const lines = text.split(/\r?\n/);
62
62
  const trace = [];
63
- const root = {};
63
+ let root = {};
64
+ let rootConvertedToArray = false;
64
65
  const stack = [];
65
66
  const getStackNames = () => stack.map((s) => `${"#".repeat(s.level)} ${s.key}`);
66
67
  for (let i = 0; i < lines.length; i++) {
@@ -78,47 +79,95 @@ function parseWithTrace(text) {
78
79
  });
79
80
  continue;
80
81
  }
82
+ if (line === "[]") {
83
+ const isRootEmpty = Array.isArray(root) ? root.length === 0 : Object.keys(root).length === 0;
84
+ if (stack.length === 0 && !rootConvertedToArray && isRootEmpty) {
85
+ root = [];
86
+ rootConvertedToArray = true;
87
+ trace.push({
88
+ lineNumber,
89
+ lineText: rawLine,
90
+ action: 'Converted root container to an Array via top-level "[]"',
91
+ stackDepth: stack.length,
92
+ currentStack: getStackNames(),
93
+ status: "success"
94
+ });
95
+ continue;
96
+ }
97
+ }
81
98
  if (line.startsWith("#")) {
82
99
  const headingMatch = line.match(/^(#+)\s*(.*)$/);
83
100
  if (headingMatch) {
84
101
  const hashCount = headingMatch[1].length;
85
- const headingName = headingMatch[2].trim();
86
- if (!headingName) {
87
- trace.push({
88
- lineNumber,
89
- lineText: rawLine,
90
- action: `Warning: Empty heading name at level ${hashCount}`,
91
- stackDepth: stack.length,
92
- currentStack: getStackNames(),
93
- status: "warning"
94
- });
95
- continue;
96
- }
102
+ let headingName = headingMatch[2].trim();
97
103
  while (stack.length > 0 && stack[stack.length - 1].level >= hashCount) {
98
104
  stack.pop();
99
105
  }
100
106
  let activeParent = root;
107
+ let activeParentItem = null;
101
108
  if (stack.length > 0) {
102
- const topItem = stack[stack.length - 1];
103
- activeParent = topItem.value;
109
+ activeParentItem = stack[stack.length - 1];
110
+ activeParent = activeParentItem.value;
104
111
  }
105
- const newNode = {};
106
- if (Array.isArray(activeParent)) {
107
- activeParent.push({ [headingName]: newNode });
112
+ if (!headingName) {
113
+ if (activeParentItem && activeParentItem.isExplicitArray || Array.isArray(activeParent)) {
114
+ headingName = "";
115
+ } else {
116
+ trace.push({
117
+ lineNumber,
118
+ lineText: rawLine,
119
+ action: `Warning: Empty heading name at level ${hashCount}`,
120
+ stackDepth: stack.length,
121
+ currentStack: getStackNames(),
122
+ status: "warning"
123
+ });
124
+ continue;
125
+ }
126
+ }
127
+ let isExplicitArray = false;
128
+ if (headingName.endsWith("[]")) {
129
+ headingName = headingName.slice(0, -2).trim();
130
+ isExplicitArray = true;
131
+ }
132
+ const newNode = isExplicitArray ? [] : {};
133
+ if (activeParentItem && activeParentItem.isExplicitArray) {
134
+ activeParent.push(newNode);
108
135
  trace.push({
109
136
  lineNumber,
110
137
  lineText: rawLine,
111
- action: `Created heading "${headingName}" at level ${hashCount} and pushed inside parent Array`,
138
+ action: `Pushed new item into explicit array "${activeParentItem.key}" via heading "${headingName}"`,
112
139
  stackDepth: stack.length,
113
140
  currentStack: getStackNames(),
114
141
  status: "success"
115
142
  });
143
+ } else if (Array.isArray(activeParent)) {
144
+ if (headingName === "") {
145
+ activeParent.push(newNode);
146
+ trace.push({
147
+ lineNumber,
148
+ lineText: rawLine,
149
+ action: `Pushed anonymous object directly into parent Array`,
150
+ stackDepth: stack.length,
151
+ currentStack: getStackNames(),
152
+ status: "success"
153
+ });
154
+ } else {
155
+ activeParent.push({ [headingName]: newNode });
156
+ trace.push({
157
+ lineNumber,
158
+ lineText: rawLine,
159
+ action: `Created heading "${headingName}" at level ${hashCount} and pushed inside parent Array`,
160
+ stackDepth: stack.length,
161
+ currentStack: getStackNames(),
162
+ status: "success"
163
+ });
164
+ }
116
165
  } else {
117
166
  activeParent[headingName] = newNode;
118
167
  trace.push({
119
168
  lineNumber,
120
169
  lineText: rawLine,
121
- action: `Created heading "${headingName}" at level ${hashCount} in parent Object`,
170
+ action: `Created heading "${headingName}" at level ${hashCount} in parent Object${isExplicitArray ? " as Array" : ""}`,
122
171
  stackDepth: stack.length,
123
172
  currentStack: getStackNames(),
124
173
  status: "success"
@@ -129,7 +178,8 @@ function parseWithTrace(text) {
129
178
  key: headingName,
130
179
  parent: activeParent,
131
180
  value: newNode,
132
- type: "object"
181
+ type: isExplicitArray ? "array" : "object",
182
+ isExplicitArray
133
183
  });
134
184
  continue;
135
185
  }
@@ -255,14 +305,35 @@ function parseWithTrace(text) {
255
305
  }
256
306
  };
257
307
  }
258
- function stringify(obj, level = 0) {
308
+ function stringify(obj, level = 0, parentKey) {
259
309
  if (obj === null || obj === void 0) return "";
260
310
  let output = "";
261
311
  const hashes = (lvl) => "#".repeat(lvl);
262
312
  if (Array.isArray(obj)) {
313
+ if (level === 0) {
314
+ output += "[]\n\n";
315
+ }
316
+ let itemName = "Item";
317
+ if (parentKey) {
318
+ if (parentKey.endsWith("ies")) {
319
+ itemName = parentKey.slice(0, -3) + "y";
320
+ } else if (parentKey.endsWith("s") && !parentKey.endsWith("ss")) {
321
+ itemName = parentKey.slice(0, -1);
322
+ } else {
323
+ itemName = parentKey;
324
+ }
325
+ itemName = itemName.charAt(0).toUpperCase() + itemName.slice(1);
326
+ }
263
327
  for (const item of obj) {
264
328
  if (typeof item === "object" && item !== null) {
265
- output += stringify(item, level);
329
+ const nextLevel = level + 1;
330
+ output += `${hashes(nextLevel)} ${itemName}
331
+ `;
332
+ const nestedString = stringify(item, nextLevel);
333
+ if (nestedString) {
334
+ output += nestedString;
335
+ }
336
+ output += "\n";
266
337
  } else {
267
338
  output += `* ${stringifyPrimitiveValue(item)}
268
339
  `;
@@ -281,10 +352,12 @@ function stringify(obj, level = 0) {
281
352
  }
282
353
  for (const key of complex) {
283
354
  const nextLevel = level + 1;
284
- output += `${hashes(nextLevel)} ${key}
285
- `;
286
355
  const val = obj[key];
287
- const nestedString = stringify(val, nextLevel);
356
+ const isArrayOfObjects = Array.isArray(val) && val.some((item) => typeof item === "object" && item !== null);
357
+ const headingNameSuffix = isArrayOfObjects ? "[]" : "";
358
+ output += `${hashes(nextLevel)} ${key}${headingNameSuffix}
359
+ `;
360
+ const nestedString = stringify(val, nextLevel, key);
288
361
  if (nestedString) {
289
362
  output += nestedString;
290
363
  }
package/dist/index.mjs CHANGED
@@ -30,7 +30,8 @@ function parseWithTrace(text) {
30
30
  const startTime = typeof performance !== "undefined" ? performance.now() : Date.now();
31
31
  const lines = text.split(/\r?\n/);
32
32
  const trace = [];
33
- const root = {};
33
+ let root = {};
34
+ let rootConvertedToArray = false;
34
35
  const stack = [];
35
36
  const getStackNames = () => stack.map((s) => `${"#".repeat(s.level)} ${s.key}`);
36
37
  for (let i = 0; i < lines.length; i++) {
@@ -48,47 +49,95 @@ function parseWithTrace(text) {
48
49
  });
49
50
  continue;
50
51
  }
52
+ if (line === "[]") {
53
+ const isRootEmpty = Array.isArray(root) ? root.length === 0 : Object.keys(root).length === 0;
54
+ if (stack.length === 0 && !rootConvertedToArray && isRootEmpty) {
55
+ root = [];
56
+ rootConvertedToArray = true;
57
+ trace.push({
58
+ lineNumber,
59
+ lineText: rawLine,
60
+ action: 'Converted root container to an Array via top-level "[]"',
61
+ stackDepth: stack.length,
62
+ currentStack: getStackNames(),
63
+ status: "success"
64
+ });
65
+ continue;
66
+ }
67
+ }
51
68
  if (line.startsWith("#")) {
52
69
  const headingMatch = line.match(/^(#+)\s*(.*)$/);
53
70
  if (headingMatch) {
54
71
  const hashCount = headingMatch[1].length;
55
- const headingName = headingMatch[2].trim();
56
- if (!headingName) {
57
- trace.push({
58
- lineNumber,
59
- lineText: rawLine,
60
- action: `Warning: Empty heading name at level ${hashCount}`,
61
- stackDepth: stack.length,
62
- currentStack: getStackNames(),
63
- status: "warning"
64
- });
65
- continue;
66
- }
72
+ let headingName = headingMatch[2].trim();
67
73
  while (stack.length > 0 && stack[stack.length - 1].level >= hashCount) {
68
74
  stack.pop();
69
75
  }
70
76
  let activeParent = root;
77
+ let activeParentItem = null;
71
78
  if (stack.length > 0) {
72
- const topItem = stack[stack.length - 1];
73
- activeParent = topItem.value;
79
+ activeParentItem = stack[stack.length - 1];
80
+ activeParent = activeParentItem.value;
74
81
  }
75
- const newNode = {};
76
- if (Array.isArray(activeParent)) {
77
- activeParent.push({ [headingName]: newNode });
82
+ if (!headingName) {
83
+ if (activeParentItem && activeParentItem.isExplicitArray || Array.isArray(activeParent)) {
84
+ headingName = "";
85
+ } else {
86
+ trace.push({
87
+ lineNumber,
88
+ lineText: rawLine,
89
+ action: `Warning: Empty heading name at level ${hashCount}`,
90
+ stackDepth: stack.length,
91
+ currentStack: getStackNames(),
92
+ status: "warning"
93
+ });
94
+ continue;
95
+ }
96
+ }
97
+ let isExplicitArray = false;
98
+ if (headingName.endsWith("[]")) {
99
+ headingName = headingName.slice(0, -2).trim();
100
+ isExplicitArray = true;
101
+ }
102
+ const newNode = isExplicitArray ? [] : {};
103
+ if (activeParentItem && activeParentItem.isExplicitArray) {
104
+ activeParent.push(newNode);
78
105
  trace.push({
79
106
  lineNumber,
80
107
  lineText: rawLine,
81
- action: `Created heading "${headingName}" at level ${hashCount} and pushed inside parent Array`,
108
+ action: `Pushed new item into explicit array "${activeParentItem.key}" via heading "${headingName}"`,
82
109
  stackDepth: stack.length,
83
110
  currentStack: getStackNames(),
84
111
  status: "success"
85
112
  });
113
+ } else if (Array.isArray(activeParent)) {
114
+ if (headingName === "") {
115
+ activeParent.push(newNode);
116
+ trace.push({
117
+ lineNumber,
118
+ lineText: rawLine,
119
+ action: `Pushed anonymous object directly into parent Array`,
120
+ stackDepth: stack.length,
121
+ currentStack: getStackNames(),
122
+ status: "success"
123
+ });
124
+ } else {
125
+ activeParent.push({ [headingName]: newNode });
126
+ trace.push({
127
+ lineNumber,
128
+ lineText: rawLine,
129
+ action: `Created heading "${headingName}" at level ${hashCount} and pushed inside parent Array`,
130
+ stackDepth: stack.length,
131
+ currentStack: getStackNames(),
132
+ status: "success"
133
+ });
134
+ }
86
135
  } else {
87
136
  activeParent[headingName] = newNode;
88
137
  trace.push({
89
138
  lineNumber,
90
139
  lineText: rawLine,
91
- action: `Created heading "${headingName}" at level ${hashCount} in parent Object`,
140
+ action: `Created heading "${headingName}" at level ${hashCount} in parent Object${isExplicitArray ? " as Array" : ""}`,
92
141
  stackDepth: stack.length,
93
142
  currentStack: getStackNames(),
94
143
  status: "success"
@@ -99,7 +148,8 @@ function parseWithTrace(text) {
99
148
  key: headingName,
100
149
  parent: activeParent,
101
150
  value: newNode,
102
- type: "object"
151
+ type: isExplicitArray ? "array" : "object",
152
+ isExplicitArray
103
153
  });
104
154
  continue;
105
155
  }
@@ -225,14 +275,35 @@ function parseWithTrace(text) {
225
275
  }
226
276
  };
227
277
  }
228
- function stringify(obj, level = 0) {
278
+ function stringify(obj, level = 0, parentKey) {
229
279
  if (obj === null || obj === void 0) return "";
230
280
  let output = "";
231
281
  const hashes = (lvl) => "#".repeat(lvl);
232
282
  if (Array.isArray(obj)) {
283
+ if (level === 0) {
284
+ output += "[]\n\n";
285
+ }
286
+ let itemName = "Item";
287
+ if (parentKey) {
288
+ if (parentKey.endsWith("ies")) {
289
+ itemName = parentKey.slice(0, -3) + "y";
290
+ } else if (parentKey.endsWith("s") && !parentKey.endsWith("ss")) {
291
+ itemName = parentKey.slice(0, -1);
292
+ } else {
293
+ itemName = parentKey;
294
+ }
295
+ itemName = itemName.charAt(0).toUpperCase() + itemName.slice(1);
296
+ }
233
297
  for (const item of obj) {
234
298
  if (typeof item === "object" && item !== null) {
235
- output += stringify(item, level);
299
+ const nextLevel = level + 1;
300
+ output += `${hashes(nextLevel)} ${itemName}
301
+ `;
302
+ const nestedString = stringify(item, nextLevel);
303
+ if (nestedString) {
304
+ output += nestedString;
305
+ }
306
+ output += "\n";
236
307
  } else {
237
308
  output += `* ${stringifyPrimitiveValue(item)}
238
309
  `;
@@ -251,10 +322,12 @@ function stringify(obj, level = 0) {
251
322
  }
252
323
  for (const key of complex) {
253
324
  const nextLevel = level + 1;
254
- output += `${hashes(nextLevel)} ${key}
255
- `;
256
325
  const val = obj[key];
257
- const nestedString = stringify(val, nextLevel);
326
+ const isArrayOfObjects = Array.isArray(val) && val.some((item) => typeof item === "object" && item !== null);
327
+ const headingNameSuffix = isArrayOfObjects ? "[]" : "";
328
+ output += `${hashes(nextLevel)} ${key}${headingNameSuffix}
329
+ `;
330
+ const nestedString = stringify(val, nextLevel, key);
258
331
  if (nestedString) {
259
332
  output += nestedString;
260
333
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mason-parser",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A lightweight, high-performance Markdown Structured Object Notation (MSON) parser and stringifier.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",