11import type { ArrayHeaderInfo , Depth , JsonArray , JsonObject , JsonPrimitive , JsonValue , ParsedLine , ResolvedDecodeOptions } from '../types'
2+ import type { ObjectWithQuotedKeys } from './expand'
23import type { LineCursor } from './scanner'
3- import { COLON , DEFAULT_DELIMITER , LIST_ITEM_PREFIX } from '../constants'
4+ import { COLON , DEFAULT_DELIMITER , DOT , LIST_ITEM_PREFIX } from '../constants'
45import { findClosingQuote } from '../shared/string-utils'
6+ import { QUOTED_KEY_MARKER } from './expand'
57import { isArrayHeaderAfterHyphen , isObjectFirstFieldAfterHyphen , mapRowValuesToPrimitives , parseArrayHeaderLine , parseDelimitedValues , parseKeyToken , parsePrimitiveToken } from './parser'
68import { assertExpectedCount , validateNoBlankLinesInRange , validateNoExtraListItems , validateNoExtraTabularRows } from './validation'
79
@@ -55,6 +57,7 @@ function isKeyValueLine(line: ParsedLine): boolean {
5557
5658function decodeObject ( cursor : LineCursor , baseDepth : Depth , options : ResolvedDecodeOptions ) : JsonObject {
5759 const obj : JsonObject = { }
60+ const quotedKeys : Set < string > = new Set ( )
5861
5962 // Detect the actual depth of the first field (may differ from baseDepth in nested structures)
6063 let computedDepth : Depth | undefined
@@ -70,15 +73,25 @@ function decodeObject(cursor: LineCursor, baseDepth: Depth, options: ResolvedDec
7073 }
7174
7275 if ( line . depth === computedDepth ) {
73- const [ key , value ] = decodeKeyValuePair ( line , cursor , computedDepth , options )
76+ const [ key , value , isQuoted ] = decodeKeyValuePair ( line , cursor , computedDepth , options )
7477 obj [ key ] = value
78+
79+ // Track quoted dotted keys for expansion phase
80+ if ( isQuoted && key . includes ( DOT ) ) {
81+ quotedKeys . add ( key )
82+ }
7583 }
7684 else {
7785 // Different depth (shallower or deeper) - stop object parsing
7886 break
7987 }
8088 }
8189
90+ // Attach quoted key metadata if any were found
91+ if ( quotedKeys . size > 0 ) {
92+ ( obj as ObjectWithQuotedKeys ) [ QUOTED_KEY_MARKER ] = quotedKeys
93+ }
94+
8295 return obj
8396}
8497
@@ -87,48 +100,49 @@ function decodeKeyValue(
87100 cursor : LineCursor ,
88101 baseDepth : Depth ,
89102 options : ResolvedDecodeOptions ,
90- ) : { key : string , value : JsonValue , followDepth : Depth } {
103+ ) : { key : string , value : JsonValue , followDepth : Depth , isQuoted : boolean } {
91104 // Check for array header first (before parsing key)
92105 const arrayHeader = parseArrayHeaderLine ( content , DEFAULT_DELIMITER )
93106 if ( arrayHeader && arrayHeader . header . key ) {
94- const value = decodeArrayFromHeader ( arrayHeader . header , arrayHeader . inlineValues , cursor , baseDepth , options )
107+ const decodedValue = decodeArrayFromHeader ( arrayHeader . header , arrayHeader . inlineValues , cursor , baseDepth , options )
95108 // After an array, subsequent fields are at baseDepth + 1 (where array content is)
96109 return {
97110 key : arrayHeader . header . key ,
98- value,
111+ value : decodedValue ,
99112 followDepth : baseDepth + 1 ,
113+ isQuoted : false , // Array keys parsed separately in `parseArrayHeaderLine`
100114 }
101115 }
102116
103117 // Regular key-value pair
104- const { key, end } = parseKeyToken ( content , 0 )
118+ const { key, end, isQuoted } = parseKeyToken ( content , 0 )
105119 const rest = content . slice ( end ) . trim ( )
106120
107121 // No value after colon - expect nested object or empty
108122 if ( ! rest ) {
109123 const nextLine = cursor . peek ( )
110124 if ( nextLine && nextLine . depth > baseDepth ) {
111125 const nested = decodeObject ( cursor , baseDepth + 1 , options )
112- return { key, value : nested , followDepth : baseDepth + 1 }
126+ return { key, value : nested , followDepth : baseDepth + 1 , isQuoted }
113127 }
114128 // Empty object
115- return { key, value : { } , followDepth : baseDepth + 1 }
129+ return { key, value : { } , followDepth : baseDepth + 1 , isQuoted }
116130 }
117131
118132 // Inline primitive value
119- const value = parsePrimitiveToken ( rest )
120- return { key, value, followDepth : baseDepth + 1 }
133+ const decodedValue = parsePrimitiveToken ( rest )
134+ return { key, value : decodedValue , followDepth : baseDepth + 1 , isQuoted }
121135}
122136
123137function decodeKeyValuePair (
124138 line : ParsedLine ,
125139 cursor : LineCursor ,
126140 baseDepth : Depth ,
127141 options : ResolvedDecodeOptions ,
128- ) : [ key : string , value : JsonValue ] {
142+ ) : [ key : string , value : JsonValue , isQuoted : boolean ] {
129143 cursor . advance ( )
130- const { key, value } = decodeKeyValue ( line . content , cursor , baseDepth , options )
131- return [ key , value ]
144+ const { key, value, isQuoted } = decodeKeyValue ( line . content , cursor , baseDepth , options )
145+ return [ key , value , isQuoted ]
132146}
133147
134148// #endregion
@@ -364,9 +378,15 @@ function decodeObjectFromListItem(
364378 options : ResolvedDecodeOptions ,
365379) : JsonObject {
366380 const afterHyphen = firstLine . content . slice ( LIST_ITEM_PREFIX . length )
367- const { key, value, followDepth } = decodeKeyValue ( afterHyphen , cursor , baseDepth , options )
381+ const { key, value, followDepth, isQuoted } = decodeKeyValue ( afterHyphen , cursor , baseDepth , options )
368382
369383 const obj : JsonObject = { [ key ] : value }
384+ const quotedKeys : Set < string > = new Set ( )
385+
386+ // Track if first key was quoted and dotted
387+ if ( isQuoted && key . includes ( DOT ) ) {
388+ quotedKeys . add ( key )
389+ }
370390
371391 // Read subsequent fields
372392 while ( ! cursor . atEnd ( ) ) {
@@ -376,14 +396,24 @@ function decodeObjectFromListItem(
376396 }
377397
378398 if ( line . depth === followDepth && ! line . content . startsWith ( LIST_ITEM_PREFIX ) ) {
379- const [ k , v ] = decodeKeyValuePair ( line , cursor , followDepth , options )
399+ const [ k , v , kIsQuoted ] = decodeKeyValuePair ( line , cursor , followDepth , options )
380400 obj [ k ] = v
401+
402+ // Track quoted dotted keys
403+ if ( kIsQuoted && k . includes ( DOT ) ) {
404+ quotedKeys . add ( k )
405+ }
381406 }
382407 else {
383408 break
384409 }
385410 }
386411
412+ // Attach quoted key metadata if any were found
413+ if ( quotedKeys . size > 0 ) {
414+ ( obj as ObjectWithQuotedKeys ) [ QUOTED_KEY_MARKER ] = quotedKeys
415+ }
416+
387417 return obj
388418}
389419
0 commit comments