nx-json-parser 1.0.0
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 +111 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +8 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +78 -0
- package/dist/parser.js.map +1 -0
- package/dist/plugins/bullet-sections.d.ts +2 -0
- package/dist/plugins/bullet-sections.d.ts.map +1 -0
- package/dist/plugins/bullet-sections.js +79 -0
- package/dist/plugins/bullet-sections.js.map +1 -0
- package/dist/transformer.d.ts +13 -0
- package/dist/transformer.d.ts.map +1 -0
- package/dist/transformer.js +99 -0
- package/dist/transformer.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/jest.config.js +16 -0
- package/package.json +44 -0
- package/src/index.ts +15 -0
- package/src/parser.ts +90 -0
- package/src/plugins/bullet-sections.ts +86 -0
- package/src/transformer.ts +116 -0
- package/src/types.ts +13 -0
- package/test/parser.2.test.ts +103 -0
- package/test/parser.test.ts +96 -0
- package/tsconfig.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# nx-json-parser
|
|
2
|
+
|
|
3
|
+
A robust Markdown-to-JSON transformer SDK built on top of `unified`, `remark`, and `remark-gfm`.
|
|
4
|
+
|
|
5
|
+
Designed for structured data extraction from LLM outputs and other Markdown-formatted strings.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **AST-Based Parsing**: Uses `remark` for reliable, production-grade Markdown parsing (no regex hell).
|
|
10
|
+
- **Table Support**: Automatically converts Markdown tables into arrays of JavaScript objects using `remark-gfm`.
|
|
11
|
+
- **List Support**: Smartly handles lists, converting them to arrays.
|
|
12
|
+
- **Bullet-Style Sections**: Includes a custom plugin to detect and transform bulleted "sections" (e.g., `- Title: Content`) into structured JSON keys.
|
|
13
|
+
- **CamelCase Normalization**: Automatically transforms headings and table headers into `camelCase` keys.
|
|
14
|
+
- **Extensible**: Built on the `unified` ecosystem, allowing for additional plugins (math, frontmatter, etc.).
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install nx-json-parser
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Basic Usage
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { markdownToJson } from 'nx-json-parser';
|
|
28
|
+
|
|
29
|
+
const md = `
|
|
30
|
+
# Summary
|
|
31
|
+
The product is a high-performance database.
|
|
32
|
+
|
|
33
|
+
## Key Features
|
|
34
|
+
- Scalable
|
|
35
|
+
- Secure
|
|
36
|
+
- Fast
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const result = markdownToJson(md);
|
|
40
|
+
/*
|
|
41
|
+
{
|
|
42
|
+
summary: "The product is a high-performance database.",
|
|
43
|
+
keyFeatures: ["Scalable", "Secure", "Fast"]
|
|
44
|
+
}
|
|
45
|
+
*/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Table Support
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const md = `
|
|
52
|
+
### Database Performance
|
|
53
|
+
| Metric | Value | Status |
|
|
54
|
+
|--------|-------|--------|
|
|
55
|
+
| Latency| 5ms | OK |
|
|
56
|
+
| Throughput | 10k ops/s | High |
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const result = markdownToJson(md);
|
|
60
|
+
/*
|
|
61
|
+
{
|
|
62
|
+
databasePerformance: [
|
|
63
|
+
{ metric: "Latency", value: "5ms", status: "OK" },
|
|
64
|
+
{ metric: "Throughput", value: "10k ops/s", status: "High" }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
*/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Bullet-Style Sections
|
|
71
|
+
|
|
72
|
+
LLMs often output sections using bullet points. `nx-json-parser` detects this pattern and treats them as structured keys:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const md = `
|
|
76
|
+
- Short Answer
|
|
77
|
+
The sky appears blue due to Rayleigh scattering.
|
|
78
|
+
|
|
79
|
+
- Technical Detail
|
|
80
|
+
Short-wavelength light is scattered more strongly by atmospheric gases.
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const result = markdownToJson(md);
|
|
84
|
+
/*
|
|
85
|
+
{
|
|
86
|
+
shortAnswer: "The sky appears blue due to Rayleigh scattering.",
|
|
87
|
+
technicalDetail: "Short-wavelength light is scattered more strongly by atmospheric gases."
|
|
88
|
+
}
|
|
89
|
+
*/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
### `markdownToJson(markdown: string): any`
|
|
95
|
+
Converts a Markdown string to a JSON object.
|
|
96
|
+
|
|
97
|
+
### `JSONTransformer`
|
|
98
|
+
The main class for transformation. You can provide a custom `RemarkParser` if needed.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { JSONTransformer, RemarkParser } from 'nx-json-parser';
|
|
102
|
+
|
|
103
|
+
const transformer = new JSONTransformer({
|
|
104
|
+
parser: new RemarkParser()
|
|
105
|
+
});
|
|
106
|
+
const result = transformer.transform(md);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
ISC
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './parser.js';
|
|
3
|
+
export * from './transformer.js';
|
|
4
|
+
/**
|
|
5
|
+
* Convenience function to transform markdown string to JSON.
|
|
6
|
+
* @param markdown The markdown string to parse.
|
|
7
|
+
* @returns A JSON object.
|
|
8
|
+
*/
|
|
9
|
+
export declare function markdownToJson(markdown: string): any;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AAIjC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAGpD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './parser.js';
|
|
3
|
+
export * from './transformer.js';
|
|
4
|
+
import { JSONTransformer } from './transformer.js';
|
|
5
|
+
/**
|
|
6
|
+
* Convenience function to transform markdown string to JSON.
|
|
7
|
+
* @param markdown The markdown string to parse.
|
|
8
|
+
* @returns A JSON object.
|
|
9
|
+
*/
|
|
10
|
+
export function markdownToJson(markdown) {
|
|
11
|
+
const transformer = new JSONTransformer();
|
|
12
|
+
return transformer.transform(markdown);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AAEjC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC3C,MAAM,WAAW,GAAG,IAAI,eAAe,EAAE,CAAC;IAC1C,OAAO,WAAW,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;AAC3C,CAAC"}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI7C,qBAAa,YAAY;IACrB,OAAO,CAAC,SAAS,CAGc;IAE/B,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE;IA0C1C,OAAO,CAAC,cAAc;IAmBtB,OAAO,CAAC,YAAY;CAcvB"}
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { toString } from 'mdast-util-to-string';
|
|
5
|
+
import { remarkBulletSections } from './plugins/bullet-sections.js';
|
|
6
|
+
import { toCamelCase } from 'nx-helpers';
|
|
7
|
+
export class RemarkParser {
|
|
8
|
+
processor = unified()
|
|
9
|
+
.use(remarkParse)
|
|
10
|
+
.use(remarkGfm)
|
|
11
|
+
.use(remarkBulletSections);
|
|
12
|
+
parse(markdown) {
|
|
13
|
+
const tree = this.processor.runSync(this.processor.parse(markdown));
|
|
14
|
+
const sections = [];
|
|
15
|
+
let currentSection = null;
|
|
16
|
+
let currentNodes = [];
|
|
17
|
+
const rootChildren = tree.children;
|
|
18
|
+
for (const node of rootChildren) {
|
|
19
|
+
if (node.type === 'heading') {
|
|
20
|
+
if (currentSection) {
|
|
21
|
+
currentSection.content = this.processContent(currentNodes);
|
|
22
|
+
sections.push(currentSection);
|
|
23
|
+
}
|
|
24
|
+
currentSection = {
|
|
25
|
+
heading: toString(node).trim(),
|
|
26
|
+
content: null,
|
|
27
|
+
level: node.depth,
|
|
28
|
+
format: 'heading'
|
|
29
|
+
};
|
|
30
|
+
currentNodes = [];
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
currentNodes.push(node);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (currentSection) {
|
|
37
|
+
currentSection.content = this.processContent(currentNodes);
|
|
38
|
+
sections.push(currentSection);
|
|
39
|
+
}
|
|
40
|
+
else if (currentNodes.length > 0) {
|
|
41
|
+
sections.push({
|
|
42
|
+
heading: 'Root',
|
|
43
|
+
content: this.processContent(currentNodes),
|
|
44
|
+
level: 0,
|
|
45
|
+
format: 'text'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return sections;
|
|
49
|
+
}
|
|
50
|
+
processContent(nodes) {
|
|
51
|
+
if (nodes.length === 0)
|
|
52
|
+
return '';
|
|
53
|
+
if (nodes.length === 1 && nodes[0].type === 'table') {
|
|
54
|
+
return this.tableToArray(nodes[0]);
|
|
55
|
+
}
|
|
56
|
+
if (nodes.length === 1 && nodes[0].type === 'list') {
|
|
57
|
+
return nodes[0].children.map((item) => toString(item).trim());
|
|
58
|
+
}
|
|
59
|
+
return nodes.map(node => {
|
|
60
|
+
if (node.type === 'table') {
|
|
61
|
+
return JSON.stringify(this.tableToArray(node));
|
|
62
|
+
}
|
|
63
|
+
return toString(node);
|
|
64
|
+
}).join('\n\n').trim();
|
|
65
|
+
}
|
|
66
|
+
tableToArray(tableNode) {
|
|
67
|
+
const headers = tableNode.children[0].children.map((cell) => toCamelCase(toString(cell).trim()));
|
|
68
|
+
return tableNode.children.slice(1).map((row) => {
|
|
69
|
+
const obj = {};
|
|
70
|
+
row.children.forEach((cell, i) => {
|
|
71
|
+
const key = headers[i] || `column${i}`;
|
|
72
|
+
obj[key] = toString(cell).trim();
|
|
73
|
+
});
|
|
74
|
+
return obj;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bullet-sections.d.ts","sourceRoot":"","sources":["../../src/plugins/bullet-sections.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,KACxB,MAAM,GAAG,UAkFpB"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { toString } from 'mdast-util-to-string';
|
|
2
|
+
export function remarkBulletSections() {
|
|
3
|
+
return (tree) => {
|
|
4
|
+
const children = tree.children;
|
|
5
|
+
if (!children)
|
|
6
|
+
return;
|
|
7
|
+
for (let i = 0; i < children.length; i++) {
|
|
8
|
+
const node = children[i];
|
|
9
|
+
if (node.type === 'list' && node.ordered === false) {
|
|
10
|
+
const items = node.children;
|
|
11
|
+
if (items.length === 0)
|
|
12
|
+
continue;
|
|
13
|
+
const newRootNodes = [];
|
|
14
|
+
let currentListItems = [];
|
|
15
|
+
items.forEach((item, idx) => {
|
|
16
|
+
const firstChild = item.children[0];
|
|
17
|
+
const text = firstChild ? toString(firstChild) : '';
|
|
18
|
+
const lines = text.trim().split('\n');
|
|
19
|
+
const firstLine = lines[0]?.trim() || '';
|
|
20
|
+
const isShort = firstLine.length > 0 && firstLine.length < 150;
|
|
21
|
+
const hasMoreContent = lines.length > 1 || item.children.length > 1;
|
|
22
|
+
// Section detection:
|
|
23
|
+
// 1. It's short and has more content.
|
|
24
|
+
// 2. OR it's short and is followed by a non-short item? (Hard to check here)
|
|
25
|
+
// 3. OR it's one of the "known" section keywords.
|
|
26
|
+
const keywords = ['answer', 'assumptions', 'unknowns', 'evidence', 'protection', 'control', 'management', 'design', 'logging', 'monitoring', 'backups', 'compliance', 'governance', 'modeling', 'incident', 'vendor', 'changes'];
|
|
27
|
+
const isSectionKeyword = keywords.some(k => firstLine.toLowerCase().includes(k));
|
|
28
|
+
if (isShort && (hasMoreContent || isSectionKeyword)) {
|
|
29
|
+
// Flush existing list items if any
|
|
30
|
+
if (currentListItems.length > 0) {
|
|
31
|
+
newRootNodes.push({
|
|
32
|
+
type: 'list',
|
|
33
|
+
ordered: false,
|
|
34
|
+
children: [...currentListItems]
|
|
35
|
+
});
|
|
36
|
+
currentListItems = [];
|
|
37
|
+
}
|
|
38
|
+
// Add as heading
|
|
39
|
+
newRootNodes.push({
|
|
40
|
+
type: 'heading',
|
|
41
|
+
depth: 2,
|
|
42
|
+
children: [{ type: 'text', value: firstLine }]
|
|
43
|
+
});
|
|
44
|
+
// Add content
|
|
45
|
+
if (lines.length > 1) {
|
|
46
|
+
newRootNodes.push({
|
|
47
|
+
type: 'paragraph',
|
|
48
|
+
children: [{ type: 'text', value: lines.slice(1).join('\n').trim() }]
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (item.children.length > 1) {
|
|
52
|
+
newRootNodes.push(...item.children.slice(1));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
currentListItems.push(item);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
// Flush remaining
|
|
60
|
+
if (currentListItems.length > 0) {
|
|
61
|
+
newRootNodes.push({
|
|
62
|
+
type: 'list',
|
|
63
|
+
ordered: false,
|
|
64
|
+
children: [...currentListItems]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (newRootNodes.length > 0) {
|
|
68
|
+
// If we performed any transformation (i.e., we found at least one heading)
|
|
69
|
+
const hasHeadings = newRootNodes.some(n => n.type === 'heading');
|
|
70
|
+
if (hasHeadings) {
|
|
71
|
+
children.splice(i, 1, ...newRootNodes);
|
|
72
|
+
i += newRootNodes.length - 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=bullet-sections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bullet-sections.js","sourceRoot":"","sources":["../../src/plugins/bullet-sections.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAEhD,MAAM,UAAU,oBAAoB;IAChC,OAAO,CAAC,IAAS,EAAE,EAAE;QACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/B,IAAI,CAAC,QAAQ;YAAE,OAAO;QAEtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACzB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;gBACjD,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC;gBAC5B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;oBAAE,SAAS;gBAEjC,MAAM,YAAY,GAAU,EAAE,CAAC;gBAC/B,IAAI,gBAAgB,GAAU,EAAE,CAAC;gBAEjC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAS,EAAE,GAAW,EAAE,EAAE;oBACrC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;oBACpC,MAAM,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;oBACpD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBACtC,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;oBAEzC,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,SAAS,CAAC,MAAM,GAAG,GAAG,CAAC;oBAC/D,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;oBAEpE,qBAAqB;oBACrB,sCAAsC;oBACtC,6EAA6E;oBAC7E,kDAAkD;oBAClD,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC;oBACjO,MAAM,gBAAgB,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;oBAEjF,IAAI,OAAO,IAAI,CAAC,cAAc,IAAI,gBAAgB,CAAC,EAAE,CAAC;wBAClD,mCAAmC;wBACnC,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;4BAC9B,YAAY,CAAC,IAAI,CAAC;gCACd,IAAI,EAAE,MAAM;gCACZ,OAAO,EAAE,KAAK;gCACd,QAAQ,EAAE,CAAC,GAAG,gBAAgB,CAAC;6BAClC,CAAC,CAAC;4BACH,gBAAgB,GAAG,EAAE,CAAC;wBAC1B,CAAC;wBAED,iBAAiB;wBACjB,YAAY,CAAC,IAAI,CAAC;4BACd,IAAI,EAAE,SAAS;4BACf,KAAK,EAAE,CAAC;4BACR,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;yBACjD,CAAC,CAAC;wBAEH,cAAc;wBACd,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;4BACnB,YAAY,CAAC,IAAI,CAAC;gCACd,IAAI,EAAE,WAAW;gCACjB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;6BACxE,CAAC,CAAC;wBACP,CAAC;wBACD,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;4BAC3B,YAAY,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;wBACjD,CAAC;oBACL,CAAC;yBAAM,CAAC;wBACJ,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAChC,CAAC;gBACL,CAAC,CAAC,CAAC;gBAEH,kBAAkB;gBAClB,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC9B,YAAY,CAAC,IAAI,CAAC;wBACd,IAAI,EAAE,MAAM;wBACZ,OAAO,EAAE,KAAK;wBACd,QAAQ,EAAE,CAAC,GAAG,gBAAgB,CAAC;qBAClC,CAAC,CAAC;gBACP,CAAC;gBAED,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC1B,2EAA2E;oBAC3E,MAAM,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;oBACjE,IAAI,WAAW,EAAE,CAAC;wBACd,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,YAAY,CAAC,CAAC;wBACvC,CAAC,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;oBACjC,CAAC;gBACL,CAAC;YACL,CAAC;QACL,CAAC;IACL,CAAC,CAAC;AACN,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RemarkParser } from './parser.js';
|
|
2
|
+
import { ParseResult } from './types.js';
|
|
3
|
+
export interface TransformerOptions {
|
|
4
|
+
parser?: RemarkParser;
|
|
5
|
+
}
|
|
6
|
+
export declare class JSONTransformer {
|
|
7
|
+
private parser;
|
|
8
|
+
constructor(options?: TransformerOptions);
|
|
9
|
+
transform(markdown: string): ParseResult;
|
|
10
|
+
private tryParseJson;
|
|
11
|
+
private parseFlexMd;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=transformer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformer.d.ts","sourceRoot":"","sources":["../src/transformer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAGzC,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,YAAY,CAAC;CACzB;AAED,qBAAa,eAAe;IACxB,OAAO,CAAC,MAAM,CAAe;gBAEjB,OAAO,GAAE,kBAAuB;IAI5C,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW;IAsCxC,OAAO,CAAC,YAAY;IAoCpB,OAAO,CAAC,WAAW;CA0BtB"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RemarkParser } from './parser.js';
|
|
2
|
+
import { toCamelCase } from 'nx-helpers';
|
|
3
|
+
export class JSONTransformer {
|
|
4
|
+
parser;
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.parser = options.parser || new RemarkParser();
|
|
7
|
+
}
|
|
8
|
+
transform(markdown) {
|
|
9
|
+
if (!markdown || typeof markdown !== 'string')
|
|
10
|
+
return {};
|
|
11
|
+
const trimmed = markdown.trim();
|
|
12
|
+
// 1. Handle "Proper JSONs" - First priority
|
|
13
|
+
const jsonResult = this.tryParseJson(trimmed);
|
|
14
|
+
if (jsonResult !== null && typeof jsonResult === 'object') {
|
|
15
|
+
return jsonResult;
|
|
16
|
+
}
|
|
17
|
+
// 2. Handle "Proper Flex MDs" (===key blocks)
|
|
18
|
+
const flexMdResult = this.parseFlexMd(trimmed);
|
|
19
|
+
if (Object.keys(flexMdResult).length > 0) {
|
|
20
|
+
return flexMdResult;
|
|
21
|
+
}
|
|
22
|
+
// 3. Fallback to Remark Parser (Unified/Remark)
|
|
23
|
+
const sections = this.parser.parse(markdown);
|
|
24
|
+
const result = {};
|
|
25
|
+
sections.forEach(section => {
|
|
26
|
+
const key = toCamelCase(section.heading);
|
|
27
|
+
if (result[key]) {
|
|
28
|
+
if (Array.isArray(result[key])) {
|
|
29
|
+
result[key].push(section.content);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
result[key] = [result[key], section.content];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
result[key] = section.content;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
tryParseJson(str) {
|
|
42
|
+
let clean = str.trim();
|
|
43
|
+
// Handle Markdown code blocks (e.g. ```json ... ```)
|
|
44
|
+
if (clean.startsWith('```')) {
|
|
45
|
+
const lines = clean.split('\n');
|
|
46
|
+
if (lines.length > 1) {
|
|
47
|
+
// Find the matching end ```
|
|
48
|
+
let endIdx = -1;
|
|
49
|
+
for (let i = lines.length - 1; i > 0; i--) {
|
|
50
|
+
if (lines[i].trim() === '```') {
|
|
51
|
+
endIdx = i;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (endIdx > 0) {
|
|
56
|
+
clean = lines.slice(1, endIdx).join('\n').trim();
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// No closing block, just strip first line if it looks like header
|
|
60
|
+
clean = lines.slice(1).join('\n').trim();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Basic JSON object/array check
|
|
65
|
+
if ((clean.startsWith('{') && clean.endsWith('}')) || (clean.startsWith('[') && clean.endsWith(']'))) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(clean);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
parseFlexMd(text) {
|
|
76
|
+
const result = {};
|
|
77
|
+
const regex = /^===\s*([a-zA-Z0-9_-]+)\s*$/gm;
|
|
78
|
+
const markers = [];
|
|
79
|
+
let match;
|
|
80
|
+
while ((match = regex.exec(text)) !== null) {
|
|
81
|
+
markers.push({
|
|
82
|
+
key: match[1],
|
|
83
|
+
nodeStart: match.index,
|
|
84
|
+
contentStart: match.index + match[0].length
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (markers.length === 0)
|
|
88
|
+
return result;
|
|
89
|
+
for (let i = 0; i < markers.length; i++) {
|
|
90
|
+
const current = markers[i];
|
|
91
|
+
const next = markers[i + 1];
|
|
92
|
+
const nextNodeStart = next ? next.nodeStart : text.length;
|
|
93
|
+
const content = text.substring(current.contentStart, nextNodeStart).trim();
|
|
94
|
+
result[toCamelCase(current.key)] = content;
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=transformer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformer.js","sourceRoot":"","sources":["../src/transformer.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface MarkdownSection {
|
|
2
|
+
heading: string;
|
|
3
|
+
content: any;
|
|
4
|
+
level: number;
|
|
5
|
+
format: 'heading' | 'list' | 'table' | 'text';
|
|
6
|
+
}
|
|
7
|
+
export type ParseResult = Record<string, any>;
|
|
8
|
+
export interface OutputFormatSpec {
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,GAAG,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;CAC/C;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAE/B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
preset: 'ts-jest/presets/default-esm',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
extensionsToTreatAsEsm: ['.ts'],
|
|
5
|
+
moduleNameMapper: {
|
|
6
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
7
|
+
},
|
|
8
|
+
transform: {
|
|
9
|
+
'^.+\\.tsx?$': [
|
|
10
|
+
'ts-jest',
|
|
11
|
+
{
|
|
12
|
+
useESM: true,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nx-json-parser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Transform strings to JSON based on Markdown structure using unified/remark",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"markdown",
|
|
21
|
+
"json",
|
|
22
|
+
"parser",
|
|
23
|
+
"unified",
|
|
24
|
+
"remark"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"mdast-util-to-string": "^4.0.0",
|
|
30
|
+
"nx-helpers": "^1.5.0",
|
|
31
|
+
"remark-gfm": "^4.0.1",
|
|
32
|
+
"remark-parse": "^11.0.0",
|
|
33
|
+
"unified": "^11.0.5",
|
|
34
|
+
"unist-util-visit": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/jest": "^29.5.12",
|
|
38
|
+
"@types/node": "^20.11.0",
|
|
39
|
+
"jest": "^29.7.0",
|
|
40
|
+
"ts-jest": "^29.1.2",
|
|
41
|
+
"ts-node": "^10.9.2",
|
|
42
|
+
"typescript": "^5.3.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './parser.js';
|
|
3
|
+
export * from './transformer.js';
|
|
4
|
+
|
|
5
|
+
import { JSONTransformer } from './transformer.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convenience function to transform markdown string to JSON.
|
|
9
|
+
* @param markdown The markdown string to parse.
|
|
10
|
+
* @returns A JSON object.
|
|
11
|
+
*/
|
|
12
|
+
export function markdownToJson(markdown: string): any {
|
|
13
|
+
const transformer = new JSONTransformer();
|
|
14
|
+
return transformer.transform(markdown);
|
|
15
|
+
}
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import { toString } from 'mdast-util-to-string';
|
|
5
|
+
import { MarkdownSection } from './types.js';
|
|
6
|
+
import { remarkBulletSections } from './plugins/bullet-sections.js';
|
|
7
|
+
import { toCamelCase } from 'nx-helpers';
|
|
8
|
+
|
|
9
|
+
export class RemarkParser {
|
|
10
|
+
private processor = unified()
|
|
11
|
+
.use(remarkParse)
|
|
12
|
+
.use(remarkGfm)
|
|
13
|
+
.use(remarkBulletSections);
|
|
14
|
+
|
|
15
|
+
parse(markdown: string): MarkdownSection[] {
|
|
16
|
+
const tree = this.processor.runSync(this.processor.parse(markdown));
|
|
17
|
+
const sections: MarkdownSection[] = [];
|
|
18
|
+
let currentSection: MarkdownSection | null = null;
|
|
19
|
+
let currentNodes: any[] = [];
|
|
20
|
+
|
|
21
|
+
const rootChildren = (tree as any).children;
|
|
22
|
+
|
|
23
|
+
for (const node of rootChildren) {
|
|
24
|
+
if (node.type === 'heading') {
|
|
25
|
+
if (currentSection) {
|
|
26
|
+
currentSection.content = this.processContent(currentNodes);
|
|
27
|
+
sections.push(currentSection);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
currentSection = {
|
|
31
|
+
heading: toString(node).trim(),
|
|
32
|
+
content: null,
|
|
33
|
+
level: node.depth,
|
|
34
|
+
format: 'heading'
|
|
35
|
+
};
|
|
36
|
+
currentNodes = [];
|
|
37
|
+
} else {
|
|
38
|
+
currentNodes.push(node);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (currentSection) {
|
|
43
|
+
currentSection.content = this.processContent(currentNodes);
|
|
44
|
+
sections.push(currentSection);
|
|
45
|
+
} else if (currentNodes.length > 0) {
|
|
46
|
+
sections.push({
|
|
47
|
+
heading: 'Root',
|
|
48
|
+
content: this.processContent(currentNodes),
|
|
49
|
+
level: 0,
|
|
50
|
+
format: 'text'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return sections;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private processContent(nodes: any[]): any {
|
|
58
|
+
if (nodes.length === 0) return '';
|
|
59
|
+
|
|
60
|
+
if (nodes.length === 1 && nodes[0].type === 'table') {
|
|
61
|
+
return this.tableToArray(nodes[0]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (nodes.length === 1 && nodes[0].type === 'list') {
|
|
65
|
+
return nodes[0].children.map((item: any) => toString(item).trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return nodes.map(node => {
|
|
69
|
+
if (node.type === 'table') {
|
|
70
|
+
return JSON.stringify(this.tableToArray(node));
|
|
71
|
+
}
|
|
72
|
+
return toString(node);
|
|
73
|
+
}).join('\n\n').trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private tableToArray(tableNode: any): any[] {
|
|
77
|
+
const headers = tableNode.children[0].children.map((cell: any) =>
|
|
78
|
+
toCamelCase(toString(cell).trim())
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return tableNode.children.slice(1).map((row: any) => {
|
|
82
|
+
const obj: any = {};
|
|
83
|
+
row.children.forEach((cell: any, i: number) => {
|
|
84
|
+
const key = headers[i] || `column${i}`;
|
|
85
|
+
obj[key] = toString(cell).trim();
|
|
86
|
+
});
|
|
87
|
+
return obj;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { toString } from 'mdast-util-to-string';
|
|
2
|
+
|
|
3
|
+
export function remarkBulletSections() {
|
|
4
|
+
return (tree: any) => {
|
|
5
|
+
const children = tree.children;
|
|
6
|
+
if (!children) return;
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < children.length; i++) {
|
|
9
|
+
const node = children[i];
|
|
10
|
+
if (node.type === 'list' && node.ordered === false) {
|
|
11
|
+
const items = node.children;
|
|
12
|
+
if (items.length === 0) continue;
|
|
13
|
+
|
|
14
|
+
const newRootNodes: any[] = [];
|
|
15
|
+
let currentListItems: any[] = [];
|
|
16
|
+
|
|
17
|
+
items.forEach((item: any, idx: number) => {
|
|
18
|
+
const firstChild = item.children[0];
|
|
19
|
+
const text = firstChild ? toString(firstChild) : '';
|
|
20
|
+
const lines = text.trim().split('\n');
|
|
21
|
+
const firstLine = lines[0]?.trim() || '';
|
|
22
|
+
|
|
23
|
+
const isShort = firstLine.length > 0 && firstLine.length < 150;
|
|
24
|
+
const hasMoreContent = lines.length > 1 || item.children.length > 1;
|
|
25
|
+
|
|
26
|
+
// Section detection:
|
|
27
|
+
// 1. It's short and has more content.
|
|
28
|
+
// 2. OR it's short and is followed by a non-short item? (Hard to check here)
|
|
29
|
+
// 3. OR it's one of the "known" section keywords.
|
|
30
|
+
const keywords = ['answer', 'assumptions', 'unknowns', 'evidence', 'protection', 'control', 'management', 'design', 'logging', 'monitoring', 'backups', 'compliance', 'governance', 'modeling', 'incident', 'vendor', 'changes'];
|
|
31
|
+
const isSectionKeyword = keywords.some(k => firstLine.toLowerCase().includes(k));
|
|
32
|
+
|
|
33
|
+
if (isShort && (hasMoreContent || isSectionKeyword)) {
|
|
34
|
+
// Flush existing list items if any
|
|
35
|
+
if (currentListItems.length > 0) {
|
|
36
|
+
newRootNodes.push({
|
|
37
|
+
type: 'list',
|
|
38
|
+
ordered: false,
|
|
39
|
+
children: [...currentListItems]
|
|
40
|
+
});
|
|
41
|
+
currentListItems = [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add as heading
|
|
45
|
+
newRootNodes.push({
|
|
46
|
+
type: 'heading',
|
|
47
|
+
depth: 2,
|
|
48
|
+
children: [{ type: 'text', value: firstLine }]
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Add content
|
|
52
|
+
if (lines.length > 1) {
|
|
53
|
+
newRootNodes.push({
|
|
54
|
+
type: 'paragraph',
|
|
55
|
+
children: [{ type: 'text', value: lines.slice(1).join('\n').trim() }]
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (item.children.length > 1) {
|
|
59
|
+
newRootNodes.push(...item.children.slice(1));
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
currentListItems.push(item);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Flush remaining
|
|
67
|
+
if (currentListItems.length > 0) {
|
|
68
|
+
newRootNodes.push({
|
|
69
|
+
type: 'list',
|
|
70
|
+
ordered: false,
|
|
71
|
+
children: [...currentListItems]
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (newRootNodes.length > 0) {
|
|
76
|
+
// If we performed any transformation (i.e., we found at least one heading)
|
|
77
|
+
const hasHeadings = newRootNodes.some(n => n.type === 'heading');
|
|
78
|
+
if (hasHeadings) {
|
|
79
|
+
children.splice(i, 1, ...newRootNodes);
|
|
80
|
+
i += newRootNodes.length - 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { RemarkParser } from './parser.js';
|
|
2
|
+
import { ParseResult } from './types.js';
|
|
3
|
+
import { toCamelCase } from 'nx-helpers';
|
|
4
|
+
|
|
5
|
+
export interface TransformerOptions {
|
|
6
|
+
parser?: RemarkParser;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class JSONTransformer {
|
|
10
|
+
private parser: RemarkParser;
|
|
11
|
+
|
|
12
|
+
constructor(options: TransformerOptions = {}) {
|
|
13
|
+
this.parser = options.parser || new RemarkParser();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
transform(markdown: string): ParseResult {
|
|
17
|
+
if (!markdown || typeof markdown !== 'string') return {};
|
|
18
|
+
|
|
19
|
+
const trimmed = markdown.trim();
|
|
20
|
+
|
|
21
|
+
// 1. Handle "Proper JSONs" - First priority
|
|
22
|
+
const jsonResult = this.tryParseJson(trimmed);
|
|
23
|
+
if (jsonResult !== null && typeof jsonResult === 'object') {
|
|
24
|
+
return jsonResult;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Handle "Proper Flex MDs" (===key blocks)
|
|
28
|
+
const flexMdResult = this.parseFlexMd(trimmed);
|
|
29
|
+
if (Object.keys(flexMdResult).length > 0) {
|
|
30
|
+
return flexMdResult;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 3. Fallback to Remark Parser (Unified/Remark)
|
|
34
|
+
const sections = this.parser.parse(markdown);
|
|
35
|
+
const result: ParseResult = {};
|
|
36
|
+
|
|
37
|
+
sections.forEach(section => {
|
|
38
|
+
const key = toCamelCase(section.heading);
|
|
39
|
+
|
|
40
|
+
if (result[key]) {
|
|
41
|
+
if (Array.isArray(result[key])) {
|
|
42
|
+
(result[key] as any[]).push(section.content);
|
|
43
|
+
} else {
|
|
44
|
+
result[key] = [result[key], section.content];
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
result[key] = section.content;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private tryParseJson(str: string): any | null {
|
|
55
|
+
let clean = str.trim();
|
|
56
|
+
|
|
57
|
+
// Handle Markdown code blocks (e.g. ```json ... ```)
|
|
58
|
+
if (clean.startsWith('```')) {
|
|
59
|
+
const lines = clean.split('\n');
|
|
60
|
+
if (lines.length > 1) {
|
|
61
|
+
// Find the matching end ```
|
|
62
|
+
let endIdx = -1;
|
|
63
|
+
for (let i = lines.length - 1; i > 0; i--) {
|
|
64
|
+
if (lines[i]!.trim() === '```') {
|
|
65
|
+
endIdx = i;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (endIdx > 0) {
|
|
71
|
+
clean = lines.slice(1, endIdx).join('\n').trim();
|
|
72
|
+
} else {
|
|
73
|
+
// No closing block, just strip first line if it looks like header
|
|
74
|
+
clean = lines.slice(1).join('\n').trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Basic JSON object/array check
|
|
80
|
+
if ((clean.startsWith('{') && clean.endsWith('}')) || (clean.startsWith('[') && clean.endsWith(']'))) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(clean);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private parseFlexMd(text: string): ParseResult {
|
|
91
|
+
const result: ParseResult = {};
|
|
92
|
+
const regex = /^===\s*([a-zA-Z0-9_-]+)\s*$/gm;
|
|
93
|
+
const markers: { key: string, contentStart: number, nodeStart: number }[] = [];
|
|
94
|
+
|
|
95
|
+
let match;
|
|
96
|
+
while ((match = regex.exec(text)) !== null) {
|
|
97
|
+
markers.push({
|
|
98
|
+
key: match[1]!,
|
|
99
|
+
nodeStart: match.index,
|
|
100
|
+
contentStart: match.index + match[0].length
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (markers.length === 0) return result;
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < markers.length; i++) {
|
|
107
|
+
const current = markers[i]!;
|
|
108
|
+
const next = markers[i + 1];
|
|
109
|
+
const nextNodeStart = next ? next.nodeStart : text.length;
|
|
110
|
+
const content = text.substring(current.contentStart, nextNodeStart).trim();
|
|
111
|
+
result[toCamelCase(current.key)] = content;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface MarkdownSection {
|
|
2
|
+
heading: string;
|
|
3
|
+
content: any; // Can be string, array of objects (for tables), etc.
|
|
4
|
+
level: number;
|
|
5
|
+
format: 'heading' | 'list' | 'table' | 'text';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ParseResult = Record<string, any>;
|
|
9
|
+
|
|
10
|
+
export interface OutputFormatSpec {
|
|
11
|
+
// Define if the user wants to enforce a specific schema later
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { markdownToJson } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
describe('nx-json-parser - Complex Input', () => {
|
|
4
|
+
const str = `- Short answer
|
|
5
|
+
Key considerations include data protection (encryption at rest and in transit), strong access control and IAM, secure network design, configuration and patch management, monitoring and logging, backups and disaster recovery, compliance and governance, and a clear shared responsibility model with incident response planning.
|
|
6
|
+
|
|
7
|
+
- Full answer
|
|
8
|
+
Deploying a database in the cloud shifts part of the security burden to the cloud provider, but the customer retains substantial responsibility for protecting data and configuring services properly. The main security considerations can be grouped as follows:
|
|
9
|
+
|
|
10
|
+
- Data protection
|
|
11
|
+
- Encryption at rest and in transit (use TLS for data in transit; enable encryption for data at rest; manage encryption keys securely).
|
|
12
|
+
- Key management and rotation (use a centralized Key Management Service or HSM where appropriate; implement regular key rotation and strict access controls).
|
|
13
|
+
- Data masking or tokenization for sensitive fields where full data visibility is not required.
|
|
14
|
+
|
|
15
|
+
- Access control and identity management
|
|
16
|
+
- Principle of least privilege (grant only the minimum rights needed to perform tasks).
|
|
17
|
+
- Strong authentication and MFA for human access; use short-lived credentials and service accounts with scoped permissions.
|
|
18
|
+
- Role-based access control (RBAC) and/or attribute-based access control (ABAC); separate duties to reduce risk of misuse.
|
|
19
|
+
- Regular review and revocation of unused accounts; automated credential rotation where feasible.
|
|
20
|
+
|
|
21
|
+
- Network design and perimeter controls
|
|
22
|
+
- Use private networks (VPC/subnets) with restricted exposure; limit public endpoints when possible.
|
|
23
|
+
- Firewalls/security groups and network ACLs with allow-by-default deny rules.
|
|
24
|
+
- Private endpoints or VPC endpoints to connect to the database without traversing the open internet.
|
|
25
|
+
- Segmentation and defense-in-depth to limit lateral movement.
|
|
26
|
+
|
|
27
|
+
- Configuration, hardening, and patch management
|
|
28
|
+
- Use secure baseline configurations; disable unnecessary features and ports.
|
|
29
|
+
- Keep the database engine and underlying software up to date with vendor patches.
|
|
30
|
+
- Implement automated configuration drift detection and enforce configuration compliance.
|
|
31
|
+
|
|
32
|
+
- Logging, monitoring, and alerting
|
|
33
|
+
- Enable comprehensive database auditing logs and integrate with centralized logging/SIEM.
|
|
34
|
+
- Monitor for anomalous patterns (e.g., unusual query volumes, off-hours access, failed login spikes).
|
|
35
|
+
- Maintain alerting with clear runbooks and escalation paths; ensure tamper-evident logging.
|
|
36
|
+
|
|
37
|
+
- Backups, recovery, and business continuity
|
|
38
|
+
- Encrypt backups; test restore procedures regularly; verify integrity of backups.
|
|
39
|
+
- Use cross-region replication and define RPO (recovery point objective) and RTO (recovery time objective) aligned with business needs.
|
|
40
|
+
- Protect against ransomware with immutable or write-once backup settings where available.
|
|
41
|
+
|
|
42
|
+
- Compliance, governance, and data residency
|
|
43
|
+
- Map data handling to applicable regulations (GDPR, HIPAA, PCI DSS, etc.) and maintain proper data classification and retention policies.
|
|
44
|
+
- Maintain data residency requirements and ensure data transfer controls are in place.
|
|
45
|
+
- Keep audit trails and documentation for compliance evidence.
|
|
46
|
+
|
|
47
|
+
- Threat modeling, vulnerability management, and secure development
|
|
48
|
+
- Regular threat modeling to identify new risks in cloud deployment.
|
|
49
|
+
- Continuous vulnerability scanning of databases and associated components; timely remediation.
|
|
50
|
+
- Secure software development practices for applications interacting with the database (code reviews, secrets management, secure APIs).
|
|
51
|
+
|
|
52
|
+
- Incident response and disaster recovery
|
|
53
|
+
- Define roles and responsibilities, incident playbooks, and communication plans.
|
|
54
|
+
- Regular tabletop exercises and live tests of disaster recovery procedures.
|
|
55
|
+
- Clear escalation paths and post-incident review processes.
|
|
56
|
+
|
|
57
|
+
- Vendor and service model considerations
|
|
58
|
+
- Understand the shared responsibility model for your chosen cloud provider and database service (PaaS vs IaaS) and document how security responsibilities are allocated.
|
|
59
|
+
- Evaluate provider security controls, uptime commitments, and data-handling practices; consider additional controls you must implement.
|
|
60
|
+
|
|
61
|
+
- Ongoing governance and changes
|
|
62
|
+
- Implement change management, version control for configuration, and approval processes for any changes affecting security.
|
|
63
|
+
- Periodic security reassessments, penetration testing (where allowed), and third-party risk assessments.
|
|
64
|
+
|
|
65
|
+
- Assumptions
|
|
66
|
+
- The user is deploying a database in a cloud environment (IaaS or managed service) and seeks a comprehensive security overview.
|
|
67
|
+
- The database may handle sensitive or regulated data, requiring compliance considerations.
|
|
68
|
+
- A shared responsibility model with the cloud provider applies, but customer controls and configurations are critical.
|
|
69
|
+
|
|
70
|
+
- Unknowns
|
|
71
|
+
- Which cloud provider and database service is in use (affects specific features and tooling).
|
|
72
|
+
- The type of data (sensitive/regulatory) and applicable compliance requirements.
|
|
73
|
+
- Whether the deployment is multi-tenant or dedicated, and its scale and regional footprint.
|
|
74
|
+
- The acceptable RPO/RTO and budget for security controls and ongoing monitoring.
|
|
75
|
+
|
|
76
|
+
- Evidence
|
|
77
|
+
1. Shared responsibility model: Cloud providers secure the underlying infrastructure; customers are responsible for securing data, access, and configurations.
|
|
78
|
+
2. Encryption: Encrypted data at rest and in transit is foundational for protecting data confidentiality in the cloud; keys should be managed securely with rotation.
|
|
79
|
+
3. Access control: Least privilege, MFA, short-lived credentials, and robust IAM/RBAC/ABAC controls reduce the risk of unauthorized access.
|
|
80
|
+
4. Network security: Private networks, restricted exposure, and private endpoints limit attack surface and exposure to the public internet.
|
|
81
|
+
5. Configuration and patch management: Secure baselines, disable unnecessary features, and timely patching reduce vulnerability exposure.
|
|
82
|
+
6. Logging and monitoring: Centralized, tamper-evident logging and proactive monitoring enable rapid detection and response to incidents.
|
|
83
|
+
7. Backups and DR: Encrypted backups and tested restore procedures with defined RPO/RTO ensure availability and recoverability.
|
|
84
|
+
8. Compliance and governance: Aligning data handling with regulations and maintaining auditable trails supports accountability and compliance.
|
|
85
|
+
9. Threat modeling and vulnerability management: Regular assessment and remediation of threats and vulnerabilities reduce risk over time.
|
|
86
|
+
10. Incident response planning: Prepared playbooks and drills enable faster containment and recovery from security incidents.
|
|
87
|
+
11. Data residency considerations: Data localization requirements can influence data storage and transfer controls.
|
|
88
|
+
12. Vendor/service model considerations: Clear understanding of the provider’s and customer’s security responsibilities and controls is essential for effective security posture.`;
|
|
89
|
+
|
|
90
|
+
it('should parse the large security overview correctly', () => {
|
|
91
|
+
const result = markdownToJson(str);
|
|
92
|
+
|
|
93
|
+
expect(result.shortAnswer).toBeDefined();
|
|
94
|
+
expect(result.fullAnswer).toBeDefined();
|
|
95
|
+
expect(result.dataProtection).toBeDefined();
|
|
96
|
+
expect(result.accessControlAndIdentityManagement).toBeDefined();
|
|
97
|
+
expect(result.assumptions).toBeInstanceOf(Array);
|
|
98
|
+
expect(result.evidence).toBeInstanceOf(Array);
|
|
99
|
+
|
|
100
|
+
expect((result.evidence as string[]).length).toBe(12);
|
|
101
|
+
expect(result.shortAnswer).toContain('Key considerations include data protection');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { markdownToJson } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
describe('nx-json-parser - Basic & Formats', () => {
|
|
4
|
+
it('should parse simple headings', () => {
|
|
5
|
+
const md = `
|
|
6
|
+
# Title
|
|
7
|
+
This is some content.
|
|
8
|
+
|
|
9
|
+
## Section One
|
|
10
|
+
More content here.
|
|
11
|
+
`;
|
|
12
|
+
const result = markdownToJson(md);
|
|
13
|
+
expect(result.title).toBe('This is some content.');
|
|
14
|
+
expect(result.sectionOne).toBe('More content here.');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should parse tables into arrays of objects', () => {
|
|
18
|
+
const md = `
|
|
19
|
+
### Users List
|
|
20
|
+
| Name | Age | Role |
|
|
21
|
+
|------|-----|------|
|
|
22
|
+
| Alice| 30 | Admin|
|
|
23
|
+
| Bob | 25 | User |
|
|
24
|
+
`;
|
|
25
|
+
const result = markdownToJson(md);
|
|
26
|
+
expect(result.usersList).toEqual([
|
|
27
|
+
{ name: 'Alice', age: '30', role: 'Admin' },
|
|
28
|
+
{ name: 'Bob', age: '25', role: 'User' }
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should parse lists into arrays', () => {
|
|
33
|
+
const md = `
|
|
34
|
+
### Features
|
|
35
|
+
- Feature A
|
|
36
|
+
- Feature B
|
|
37
|
+
- Feature C
|
|
38
|
+
`;
|
|
39
|
+
const result = markdownToJson(md);
|
|
40
|
+
expect(result.features).toEqual(['Feature A', 'Feature B', 'Feature C']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle bullet-style sections (via plugin)', () => {
|
|
44
|
+
const md = `
|
|
45
|
+
- Short Answer
|
|
46
|
+
The sky is blue.
|
|
47
|
+
|
|
48
|
+
- Full Answer
|
|
49
|
+
Rayleigh scattering causes the blue color...
|
|
50
|
+
`;
|
|
51
|
+
const result = markdownToJson(md);
|
|
52
|
+
expect(result.shortAnswer).toBe('The sky is blue.');
|
|
53
|
+
expect(result.fullAnswer).toBe('Rayleigh scattering causes the blue color...');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle "Proper JSON" strings', () => {
|
|
57
|
+
const jsonStr = JSON.stringify({
|
|
58
|
+
status: "success",
|
|
59
|
+
data: { id: 123, name: "Test" }
|
|
60
|
+
});
|
|
61
|
+
const result = markdownToJson(jsonStr);
|
|
62
|
+
expect(result.status).toBe("success");
|
|
63
|
+
expect(result.data.id).toBe(123);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle JSON inside code blocks', () => {
|
|
67
|
+
const md = `
|
|
68
|
+
\`\`\`json
|
|
69
|
+
{
|
|
70
|
+
"api": "v1",
|
|
71
|
+
"status": "active"
|
|
72
|
+
}
|
|
73
|
+
\`\`\`
|
|
74
|
+
`;
|
|
75
|
+
const result = markdownToJson(md);
|
|
76
|
+
expect(result.api).toBe("v1");
|
|
77
|
+
expect(result.status).toBe("active");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle "Proper Flex MD" (===key) format', () => {
|
|
81
|
+
const flexMd = `
|
|
82
|
+
=== TITLE
|
|
83
|
+
The Great Gatsby
|
|
84
|
+
|
|
85
|
+
=== AUTHOR
|
|
86
|
+
F. Scott Fitzgerald
|
|
87
|
+
|
|
88
|
+
=== SUMMARY
|
|
89
|
+
A story about wealth and love.
|
|
90
|
+
`;
|
|
91
|
+
const result = markdownToJson(flexMd);
|
|
92
|
+
expect(result.title).toBe("The Great Gatsby");
|
|
93
|
+
expect(result.author).toBe("F. Scott Fitzgerald");
|
|
94
|
+
expect(result.summary).toBe("A story about wealth and love.");
|
|
95
|
+
});
|
|
96
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "nodenext",
|
|
4
|
+
"target": "esnext",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"moduleResolution": "nodenext",
|
|
14
|
+
"isolatedModules": true
|
|
15
|
+
},
|
|
16
|
+
"include": [
|
|
17
|
+
"src/**/*"
|
|
18
|
+
],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"node_modules",
|
|
21
|
+
"**/*.test.ts"
|
|
22
|
+
]
|
|
23
|
+
}
|