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 +107 -11
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +99 -26
- package/dist/index.mjs +99 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
# mason-parser
|
|
2
2
|
|
|
3
|
-
> Markdown Structured Object Notation (
|
|
3
|
+
> Markdown Structured Object Notation (MaSON) — A human-centric serialization format bridging natural markdown visual hierarchy and structured JSON.
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
6
6
|
[](https://www.npmjs.com/package/mason-parser)
|
|
7
7
|
|
|
8
|
-
|
|
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
|
|
17
|
-
- **
|
|
18
|
-
- **Bi-directional Integrity:** Safely stringifies
|
|
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
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
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
|
|
122
|
-
4. **Lists:** Bullet elements (`*`, `-`, `+`) immediately
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
activeParent =
|
|
109
|
+
activeParentItem = stack[stack.length - 1];
|
|
110
|
+
activeParent = activeParentItem.value;
|
|
104
111
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
activeParent =
|
|
79
|
+
activeParentItem = stack[stack.length - 1];
|
|
80
|
+
activeParent = activeParentItem.value;
|
|
74
81
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
|
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