@@ -12,10 +12,36 @@ function isNotEmpty(json: JSON): boolean {
1212 return ! isEmpty ( json ) ;
1313}
1414
15+ function typeOfJson ( json : JSON ) : 'string' | 'number' | 'boolean' | 'object' | 'array-simple' | 'array-object' | 'array-mixed' | 'null' {
16+ if ( Array . isArray ( json ) ) {
17+ if ( json . every ( ( item ) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean' || item === null ) ) {
18+ return 'array-simple' ;
19+ }
20+ if ( json . every ( ( item ) => typeof item === 'object' && item !== null && ! Array . isArray ( item ) ) ) {
21+ return 'array-object' ;
22+ }
23+ return 'array-mixed' ;
24+ }
25+ if ( json === null ) {
26+ return 'null' ;
27+ }
28+ return typeof json as 'string' | 'number' | 'boolean' | 'object' | 'null' ;
29+ }
30+
31+ function isOneLiner ( json : JSON ) : boolean {
32+ const type = typeOfJson ( json ) ;
33+ return type === 'string' || type === 'number' || type === 'boolean' || type === 'array-simple' ;
34+ }
35+
36+ function getIndent ( pad : number , withBullet : boolean ) : string {
37+ return ' ' . repeat ( ( pad + 1 - ( withBullet ? 1 : 0 ) ) * 2 ) + ( withBullet ? '- ' : '' ) ;
38+ }
39+
1540export function jsonToMarkdown ( json : Readonly < JSON > ) : string {
1641 const cloned = structuredClone ( json ) as JSON ; // Copy data to avoid mutating the original object
1742 const simplified = simplifyJson ( cloned ) ;
18- return serializeJsonToMarkdown ( simplified , 0 ) ;
43+ // TODO: clear null values
44+ return serializeJsonTopLevel ( simplified ) ;
1945}
2046
2147function simplifyJson ( json : JSON ) : JSON {
@@ -46,71 +72,96 @@ function simplifyJson(json: JSON): JSON {
4672 return result ;
4773}
4874
49- function serializeJsonToMarkdown ( json : JSON , pad = 0 ) : string {
50- if ( typeof json === 'string' || typeof json === 'number' || typeof json === 'boolean' ) {
51- return String ( json ) ;
52- }
75+ function serializeJsonTopLevel ( json : JSON ) : string {
76+ switch ( typeOfJson ( json ) ) {
77+ case 'string' :
78+ case 'number' :
79+ case 'boolean' :
80+ return String ( json ) ;
81+ case 'null' :
82+ return '' ;
83+ case 'object' :
84+ return serializeJson ( json , 0 ) ;
85+ case 'array-simple' :
86+ case 'array-mixed' :
87+ return serializeJson ( json , 0 ) ;
88+ case 'array-object' :
89+ return ( json as JSON [ ] ) . map ( ( unknownItem , index ) => {
90+ const item = unknownItem as Record < string , object > ;
91+ let title ;
92+ if ( item . title ) {
93+ title = `${ index + 1 } . ${ item . title } ` ;
94+ delete item . title ;
95+ } else if ( item . name ) {
96+ title = `${ index + 1 } . ${ item . name } ` ;
97+ delete item . name ;
98+ } else {
99+ title = `${ index + 1 } . Item` ;
100+ }
53101
54- if ( json === null ) {
55- return '' ; // Ignore null
102+ let result = '' ;
103+ result += `## ${ title } \n` ;
104+ result += serializeJson ( unknownItem , 0 ) ;
105+ return result ;
106+ } ) . join ( '\n\n' ) ;
107+ default :
108+ return serializeJson ( json , 0 ) ;
56109 }
110+ }
57111
58- // Trivial array will be just list like 1, 2, 3
59- if ( Array . isArray ( json ) ) {
60- if ( json . length === 0 ) {
61- return '' ; // Ignore empty arrays
62- }
63- if ( json . every ( ( item ) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean' || item === null ) ) {
64- // Null in array is ignored
65- return json . filter ( isNotEmpty ) . join ( ', ' ) ;
66- }
67-
68- // Advanced array will use bullets
69- const indent = ' ' . repeat ( pad * 2 ) ;
70- const singleLine = json . length === 1 && json . every ( ( item ) => {
71- const content = serializeJsonToMarkdown ( item , 0 ) ;
72- return ! content . includes ( '\n' ) ;
73- } ) ;
74- if ( singleLine ) {
75- // For single-item arrays with simple content, don't add indent
76- return json . filter ( isNotEmpty )
77- . map ( ( value ) => {
78- const content = serializeJsonToMarkdown ( value , 0 ) ;
79- return `- ${ content } ` ;
112+ function serializeJson ( json : JSON , pad : number ) : string {
113+ switch ( typeOfJson ( json ) ) {
114+ case 'string' :
115+ case 'number' :
116+ case 'boolean' :
117+ return pad === 0 ? getIndent ( pad , true ) + String ( json ) : String ( json ) ;
118+ case 'object' :
119+ return Object . entries ( json as Record < string , JSON > )
120+ . filter ( ( [ key , value ] ) => ! isEmpty ( value ) )
121+ . map ( ( [ key , value ] , index ) => {
122+ const indentLevel = pad === 0 ? 0 : 1 ;
123+ const prefix = `${ getIndent ( indentLevel , true ) } ${ key } :` ;
124+ if ( isOneLiner ( value ) ) {
125+ return `${ prefix } ${ serializeJson ( value , - 1 ) } ` ;
126+ }
127+ return `${ prefix } \n${ serializeJson ( value , pad + 1 ) } ` ;
80128 } )
81- . join ( ' ' ) ;
82- }
83- return json . filter ( isNotEmpty )
84- . map ( ( value , index ) => {
85- const content = serializeJsonToMarkdown ( value , 0 ) ;
86- const lines = content . split ( '\n' ) ;
87- if ( lines . length === 1 ) {
88- return `${ indent } - ${ lines [ 0 ] } ` ;
129+ . join ( '\n ' ) ;
130+ case 'array-simple' :
131+ return ` ${ ( json as JSON [ ] ) . filter ( isNotEmpty ) . join ( ', ' ) } ` ;
132+ case 'array-mixed' :
133+ return ( json as JSON [ ] ) . filter ( isNotEmpty ) . map ( ( unknownItem ) => {
134+ const itemType = typeOfJson ( unknownItem ) ;
135+ if ( itemType === 'array-simple' || itemType === 'array-object' ) {
136+ return `- ${ serializeJson ( unknownItem , - 1 ) } ` ;
89137 }
90- // Special case for top-level arrays to match expected inconsistent indentation
91- const nestedIndent = pad === 0 ? ' ' . repeat ( index === 0 ? 3 : 2 ) : ' ' . repeat ( pad * 2 + 2 ) ;
92- return `${ indent } - ${ lines [ 0 ] } \n${ lines . slice ( 1 ) . map ( ( line ) => nestedIndent + line ) . join ( '\n' ) } ` ;
93- } )
94- . join ( '\n' ) ;
138+ if ( itemType === 'object' ) {
139+ return Object . entries ( unknownItem as Record < string , JSON > )
140+ . filter ( ( [ key , value ] ) => ! isEmpty ( value ) )
141+ . map ( ( [ key , value ] , index ) => {
142+ const prefix = `${ getIndent ( pad , index === 0 ) } ${ key } :` ;
143+ if ( isOneLiner ( value ) ) {
144+ return `${ prefix } ${ serializeJson ( value , - 1 ) } ` ;
145+ }
146+ return `${ prefix } \n${ serializeJson ( value , pad + 1 ) } ` ;
147+ } )
148+ . join ( '\n' ) ;
149+ }
150+ return serializeJson ( unknownItem , pad ) ;
151+ } ) . join ( '\n' ) ;
152+ case 'array-object' :
153+ return ( json as JSON [ ] ) . filter ( isNotEmpty ) . map ( ( unknownItem ) => {
154+ return Object . entries ( unknownItem as Record < string , JSON > )
155+ . filter ( ( [ key , value ] ) => ! isEmpty ( value ) )
156+ . map ( ( [ key , value ] , index ) => {
157+ const indentLevel = pad === 1 ? 1 : pad ;
158+ const withBullet = pad === 1 ? index === 0 : true ;
159+ return `${ getIndent ( indentLevel , withBullet ) } ${ key } : ${ serializeJson ( value , - 1 ) } ` ;
160+ } ) . join ( '\n' ) ;
161+ } ) . join ( '\n' ) ;
162+ case 'null' :
163+ return '' ;
164+ default :
165+ throw new Error ( `Unknown type: ${ typeof json } ` ) ;
95166 }
96-
97- const indent = ' ' . repeat ( pad * 2 ) ;
98-
99- // Objects will be like key: value
100- return Object . entries ( json )
101- . filter ( ( [ _ , value ] ) => isNotEmpty ( value ) )
102- . map ( ( [ key , value ] ) => {
103- const valueStr = serializeJsonToMarkdown ( value , pad + 1 ) ;
104- if ( ( Array . isArray ( value ) && valueStr . includes ( '\n' ) )
105- || ( ! Array . isArray ( value ) && typeof value === 'object' && value !== null && valueStr . includes ( '\n' ) ) ) {
106- // Multi-line arrays or objects in objects should be on new lines with proper indentation
107- return `${ indent } ${ key } :\n${ valueStr } ` ;
108- }
109- // For inline values, don't add indent if we're in a nested context or if current object has single property with simple value
110- const currentObjectHasSingleProperty = Object . keys ( json ) . length === 1 ;
111- const valueIsSimple = typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ;
112- const keyIndent = ( pad > 0 && ( ( typeof value === 'object' && value !== null ) || ( currentObjectHasSingleProperty && valueIsSimple ) ) ) ? '' : indent ;
113- return `${ keyIndent } ${ key } : ${ valueStr } ` ;
114- } )
115- . join ( '\n' ) ;
116167}
0 commit comments