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 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
@@ -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"}
@@ -0,0 +1,8 @@
1
+ import { MarkdownSection } from './types.js';
2
+ export declare class RemarkParser {
3
+ private processor;
4
+ parse(markdown: string): MarkdownSection[];
5
+ private processContent;
6
+ private tableToArray;
7
+ }
8
+ //# sourceMappingURL=parser.d.ts.map
@@ -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,2 @@
1
+ export declare function remarkBulletSections(): (tree: any) => void;
2
+ //# sourceMappingURL=bullet-sections.d.ts.map
@@ -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":"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"}
@@ -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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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }