toonfile 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/LICENSE +21 -0
- package/README.md +302 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +230 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +161 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +199 -0
- package/dist/utils.js.map +1 -0
- package/package.json +72 -0
- package/src/index.ts +222 -0
- package/src/types.ts +198 -0
- package/src/universalify.d.ts +6 -0
- package/src/utils.ts +222 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as universalify from 'universalify'
|
|
3
|
+
import { parse, stringify, stripBom } from './utils'
|
|
4
|
+
import type { ReadOptions, WriteOptions, Callback } from './types'
|
|
5
|
+
|
|
6
|
+
let _fs: typeof fs
|
|
7
|
+
try {
|
|
8
|
+
_fs = require('graceful-fs') as typeof fs
|
|
9
|
+
} catch (_) {
|
|
10
|
+
_fs = fs
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ============ READ FILE ============
|
|
14
|
+
|
|
15
|
+
async function _readFile (file: string, options: ReadOptions | string = {}): Promise<any> {
|
|
16
|
+
if (typeof options === 'string') {
|
|
17
|
+
options = { encoding: options as BufferEncoding }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const fsModule = options.fs || _fs
|
|
21
|
+
|
|
22
|
+
const shouldThrow = 'throws' in options ? options.throws : true
|
|
23
|
+
|
|
24
|
+
let data: string
|
|
25
|
+
try {
|
|
26
|
+
const result = await new Promise<Buffer | string>((resolve, reject) => {
|
|
27
|
+
fsModule.readFile(file, options.encoding || 'utf8', (err: any, data: any) => {
|
|
28
|
+
if (err) reject(err)
|
|
29
|
+
else resolve(data)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
data = typeof result === 'string' ? result : result.toString('utf8')
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (shouldThrow) {
|
|
35
|
+
const error = err as NodeJS.ErrnoException
|
|
36
|
+
error.message = `${file}: ${error.message}`
|
|
37
|
+
throw error
|
|
38
|
+
} else {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
data = stripBom(data)
|
|
44
|
+
|
|
45
|
+
let obj: any
|
|
46
|
+
try {
|
|
47
|
+
obj = parse(data, options)
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (shouldThrow) {
|
|
50
|
+
const error = err as Error
|
|
51
|
+
error.message = `${file}: ${error.message}`
|
|
52
|
+
throw error
|
|
53
|
+
} else {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return obj
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Read and parse a TOON file asynchronously
|
|
63
|
+
*
|
|
64
|
+
* @param file - Path to the TOON file
|
|
65
|
+
* @param options - Read options or encoding string
|
|
66
|
+
* @param callback - Optional callback function
|
|
67
|
+
* @returns Promise that resolves to parsed object (if no callback provided)
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // With promise
|
|
71
|
+
* const data = await readFile('config.toon')
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // With callback
|
|
75
|
+
* readFile('config.toon', (err, data) => {
|
|
76
|
+
* if (err) console.error(err)
|
|
77
|
+
* console.log(data)
|
|
78
|
+
* })
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // With options
|
|
82
|
+
* const data = await readFile('config.toon', { throws: false })
|
|
83
|
+
*/
|
|
84
|
+
export const readFile: {
|
|
85
|
+
(file: string, options?: ReadOptions | string): Promise<any>
|
|
86
|
+
(file: string, callback: Callback<any>): void
|
|
87
|
+
(file: string, options: ReadOptions | string, callback: Callback<any>): void
|
|
88
|
+
} = universalify.fromPromise(_readFile) as any
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read and parse a TOON file synchronously
|
|
92
|
+
*
|
|
93
|
+
* @param file - Path to the TOON file
|
|
94
|
+
* @param options - Read options or encoding string
|
|
95
|
+
* @returns Parsed object
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* const data = readFileSync('config.toon')
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const data = readFileSync('config.toon', { throws: false })
|
|
102
|
+
*/
|
|
103
|
+
export function readFileSync (file: string, options: ReadOptions | string = {}): any {
|
|
104
|
+
if (typeof options === 'string') {
|
|
105
|
+
options = { encoding: options as BufferEncoding }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const fsModule = options.fs || _fs
|
|
109
|
+
|
|
110
|
+
const shouldThrow = 'throws' in options ? options.throws : true
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const buffer = fsModule.readFileSync(file, options as any)
|
|
114
|
+
let content = typeof buffer === 'string' ? buffer : buffer.toString('utf8')
|
|
115
|
+
content = stripBom(content)
|
|
116
|
+
return parse(content, options)
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (shouldThrow) {
|
|
119
|
+
const error = err as NodeJS.ErrnoException
|
|
120
|
+
error.message = `${file}: ${error.message}`
|
|
121
|
+
throw error
|
|
122
|
+
} else {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============ WRITE FILE ============
|
|
129
|
+
|
|
130
|
+
async function _writeFile (file: string, obj: any, options: WriteOptions = {}): Promise<void> {
|
|
131
|
+
const fsModule = options.fs || _fs
|
|
132
|
+
|
|
133
|
+
const str = stringify(obj, options)
|
|
134
|
+
|
|
135
|
+
const writeOptions: any = { encoding: options.encoding || 'utf8' }
|
|
136
|
+
if (options.mode) writeOptions.mode = options.mode
|
|
137
|
+
if (options.flag) writeOptions.flag = options.flag
|
|
138
|
+
|
|
139
|
+
await new Promise<void>((resolve, reject) => {
|
|
140
|
+
fsModule.writeFile(file, str, writeOptions, (err: any) => {
|
|
141
|
+
if (err) reject(err)
|
|
142
|
+
else resolve()
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Stringify object and write to TOON file asynchronously
|
|
149
|
+
*
|
|
150
|
+
* @param file - Path to the TOON file
|
|
151
|
+
* @param obj - Object to stringify and write
|
|
152
|
+
* @param options - Write options
|
|
153
|
+
* @param callback - Optional callback function
|
|
154
|
+
* @returns Promise that resolves when file is written (if no callback provided)
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* // With promise
|
|
158
|
+
* await writeFile('data.toon', { name: 'Alice', age: 30 })
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* // With callback
|
|
162
|
+
* writeFile('data.toon', obj, (err) => {
|
|
163
|
+
* if (err) console.error(err)
|
|
164
|
+
* })
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* // With options
|
|
168
|
+
* await writeFile('data.toon', obj, { indentSize: 4, delimiter: '|' })
|
|
169
|
+
*/
|
|
170
|
+
export const writeFile: {
|
|
171
|
+
(file: string, obj: any, options?: WriteOptions): Promise<void>
|
|
172
|
+
(file: string, obj: any, callback: Callback<void>): void
|
|
173
|
+
(file: string, obj: any, options: WriteOptions, callback: Callback<void>): void
|
|
174
|
+
} = universalify.fromPromise(_writeFile) as any
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Stringify object and write to TOON file synchronously
|
|
178
|
+
*
|
|
179
|
+
* @param file - Path to the TOON file
|
|
180
|
+
* @param obj - Object to stringify and write
|
|
181
|
+
* @param options - Write options
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* writeFileSync('data.toon', { name: 'Bob', age: 25 })
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* writeFileSync('data.toon', obj, { indentSize: 4 })
|
|
188
|
+
*/
|
|
189
|
+
export function writeFileSync (file: string, obj: any, options: WriteOptions = {}): void {
|
|
190
|
+
const fsModule = options.fs || _fs
|
|
191
|
+
|
|
192
|
+
const str = stringify(obj, options)
|
|
193
|
+
fsModule.writeFileSync(file, str, options as any)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============ EXPORTS ============
|
|
197
|
+
|
|
198
|
+
// Re-export utility functions
|
|
199
|
+
export { parse, stringify } from './utils'
|
|
200
|
+
|
|
201
|
+
// Re-export types
|
|
202
|
+
export type {
|
|
203
|
+
ReadOptions,
|
|
204
|
+
WriteOptions,
|
|
205
|
+
ParseOptions,
|
|
206
|
+
StringifyOptions,
|
|
207
|
+
Callback,
|
|
208
|
+
ToonValue,
|
|
209
|
+
ToonObject,
|
|
210
|
+
ToonArray
|
|
211
|
+
} from './types'
|
|
212
|
+
|
|
213
|
+
// NOTE: do not change this export format; required for ESM compat
|
|
214
|
+
// Default export for CommonJS compatibility
|
|
215
|
+
export default {
|
|
216
|
+
readFile,
|
|
217
|
+
readFileSync,
|
|
218
|
+
writeFile,
|
|
219
|
+
writeFileSync,
|
|
220
|
+
parse,
|
|
221
|
+
stringify
|
|
222
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON File Types and Interfaces
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type * as fs from 'fs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for reading TOON files
|
|
9
|
+
*/
|
|
10
|
+
export interface ReadOptions {
|
|
11
|
+
/**
|
|
12
|
+
* File encoding (default: 'utf8')
|
|
13
|
+
*/
|
|
14
|
+
encoding?: BufferEncoding
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Throw error on parse failure. If false, returns null instead.
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
throws?: boolean
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Custom fs module for testing or alternative file systems
|
|
24
|
+
*/
|
|
25
|
+
fs?: typeof fs
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Transform function applied to parsed values (similar to JSON.parse reviver)
|
|
29
|
+
*/
|
|
30
|
+
reviver?: (key: string, value: any) => any
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Indent size for parsing (default: 2)
|
|
34
|
+
*/
|
|
35
|
+
indentSize?: number
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Strict mode for parsing
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
strict?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Options for writing TOON files
|
|
46
|
+
*/
|
|
47
|
+
export interface WriteOptions {
|
|
48
|
+
/**
|
|
49
|
+
* File encoding (default: 'utf8')
|
|
50
|
+
*/
|
|
51
|
+
encoding?: BufferEncoding
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Custom fs module for testing or alternative file systems
|
|
55
|
+
*/
|
|
56
|
+
fs?: typeof fs
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Spaces per indent level
|
|
60
|
+
* @default 2
|
|
61
|
+
*/
|
|
62
|
+
indentSize?: number
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Delimiter for arrays: comma, tab, or pipe
|
|
66
|
+
* @default ','
|
|
67
|
+
*/
|
|
68
|
+
delimiter?: ',' | '\t' | '|'
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* End-of-line character(s)
|
|
72
|
+
* @default '\n'
|
|
73
|
+
*/
|
|
74
|
+
EOL?: '\n' | '\r\n' | string
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Include EOL at end of file
|
|
78
|
+
* @default true
|
|
79
|
+
*/
|
|
80
|
+
finalEOL?: boolean
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Use tabular format for uniform arrays of objects
|
|
84
|
+
* @default true
|
|
85
|
+
*/
|
|
86
|
+
tabularArrays?: boolean
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Minimum array length to use tabular format
|
|
90
|
+
* @default 2
|
|
91
|
+
*/
|
|
92
|
+
minArrayLengthForTabular?: number
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Transform function applied before stringifying (similar to JSON.stringify replacer)
|
|
96
|
+
*/
|
|
97
|
+
replacer?: (key: string, value: any) => any
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* File system flags (e.g., 'w', 'a')
|
|
101
|
+
* @default 'w'
|
|
102
|
+
*/
|
|
103
|
+
flag?: string
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* File mode (permissions)
|
|
107
|
+
*/
|
|
108
|
+
mode?: number
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Options for parsing TOON strings
|
|
113
|
+
*/
|
|
114
|
+
export interface ParseOptions {
|
|
115
|
+
/**
|
|
116
|
+
* Transform function applied to parsed values
|
|
117
|
+
*/
|
|
118
|
+
reviver?: (key: string, value: any) => any
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Indent size (default: 2)
|
|
122
|
+
*/
|
|
123
|
+
indentSize?: number
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Strict mode
|
|
127
|
+
* @default true
|
|
128
|
+
*/
|
|
129
|
+
strict?: boolean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Options for stringifying objects to TOON
|
|
134
|
+
*/
|
|
135
|
+
export interface StringifyOptions {
|
|
136
|
+
/**
|
|
137
|
+
* Spaces per indent level
|
|
138
|
+
* @default 2
|
|
139
|
+
*/
|
|
140
|
+
indentSize?: number
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Delimiter for arrays
|
|
144
|
+
* @default ','
|
|
145
|
+
*/
|
|
146
|
+
delimiter?: ',' | '\t' | '|' | string
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* End-of-line character(s)
|
|
150
|
+
* @default '\n'
|
|
151
|
+
*/
|
|
152
|
+
EOL?: '\n' | '\r\n' | string
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Include EOL at end of file
|
|
156
|
+
* @default true
|
|
157
|
+
*/
|
|
158
|
+
finalEOL?: boolean
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Use tabular format for uniform arrays
|
|
162
|
+
* @default true
|
|
163
|
+
*/
|
|
164
|
+
tabularArrays?: boolean
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Minimum array length for tabular format
|
|
168
|
+
* @default 2
|
|
169
|
+
*/
|
|
170
|
+
minArrayLengthForTabular?: number
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Transform function applied before stringifying
|
|
174
|
+
*/
|
|
175
|
+
replacer?: (key: string, value: any) => any
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Callback function for async operations
|
|
180
|
+
*/
|
|
181
|
+
export type Callback<T> = (err: Error | null, data?: T) => void
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* TOON value types
|
|
185
|
+
*/
|
|
186
|
+
export type ToonValue =
|
|
187
|
+
| string
|
|
188
|
+
| number
|
|
189
|
+
| boolean
|
|
190
|
+
| null
|
|
191
|
+
| ToonArray
|
|
192
|
+
| ToonObject
|
|
193
|
+
|
|
194
|
+
export interface ToonObject {
|
|
195
|
+
[key: string]: ToonValue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type ToonArray = ToonValue[]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
declare module 'universalify' {
|
|
2
|
+
export function fromCallback<T extends (...args: any[]) => any>(fn: T): T
|
|
3
|
+
export function fromPromise<T extends (...args: any[]) => Promise<any>>(fn: T): T & {
|
|
4
|
+
(...args: [...Parameters<T>, (err: Error | null, result?: any) => void]): void
|
|
5
|
+
}
|
|
6
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOON Format Parser and Serializer
|
|
3
|
+
*
|
|
4
|
+
* Custom implementation supporting basic TOON features.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ParseOptions,
|
|
9
|
+
StringifyOptions,
|
|
10
|
+
ToonValue
|
|
11
|
+
} from './types'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse TOON string to JavaScript object
|
|
15
|
+
*/
|
|
16
|
+
export function parse(content: string | Buffer, options: ParseOptions = {}): any {
|
|
17
|
+
if (Buffer.isBuffer(content)) {
|
|
18
|
+
content = content.toString('utf8')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
content = stripBom(content)
|
|
22
|
+
|
|
23
|
+
const lines = content.split(/\r?\n/)
|
|
24
|
+
const indentSize = options.indentSize || 2
|
|
25
|
+
|
|
26
|
+
interface StackItem {
|
|
27
|
+
obj: any
|
|
28
|
+
indent: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const stack: StackItem[] = [{ obj: {}, indent: -indentSize }]
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
const line = lines[i]
|
|
35
|
+
|
|
36
|
+
// Skip empty lines and comments
|
|
37
|
+
if (!line.trim() || line.trim().startsWith('#')) continue
|
|
38
|
+
|
|
39
|
+
const indent = getIndentLevel(line)
|
|
40
|
+
const trimmedLine = line.trim()
|
|
41
|
+
|
|
42
|
+
// Pop stack until we find the right parent
|
|
43
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
44
|
+
stack.pop()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parent = stack[stack.length - 1].obj
|
|
48
|
+
|
|
49
|
+
// Parse key-value
|
|
50
|
+
if (trimmedLine.includes(':')) {
|
|
51
|
+
const colonIndex = trimmedLine.indexOf(':')
|
|
52
|
+
const key = trimmedLine.substring(0, colonIndex).trim()
|
|
53
|
+
const valueStr = trimmedLine.substring(colonIndex + 1).trim()
|
|
54
|
+
|
|
55
|
+
// Check for array declaration: key[length]
|
|
56
|
+
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/)
|
|
57
|
+
|
|
58
|
+
if (arrayMatch) {
|
|
59
|
+
// Inline array: scores[3]: 95,87,92
|
|
60
|
+
const arrayName = arrayMatch[1]
|
|
61
|
+
const arrayLength = parseInt(arrayMatch[2], 10)
|
|
62
|
+
|
|
63
|
+
if (valueStr) {
|
|
64
|
+
parent[arrayName] = parseInlineArray(valueStr, arrayLength)
|
|
65
|
+
} else {
|
|
66
|
+
parent[arrayName] = []
|
|
67
|
+
}
|
|
68
|
+
} else if (valueStr) {
|
|
69
|
+
// Simple key-value
|
|
70
|
+
parent[key] = parseValue(valueStr)
|
|
71
|
+
} else {
|
|
72
|
+
// Nested object
|
|
73
|
+
const newObj: any = {}
|
|
74
|
+
parent[key] = newObj
|
|
75
|
+
stack.push({ obj: newObj, indent })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const result = stack[0].obj
|
|
81
|
+
|
|
82
|
+
if (options.reviver) {
|
|
83
|
+
return applyReviver(result, options.reviver)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Stringify JavaScript object to TOON format
|
|
91
|
+
*/
|
|
92
|
+
export function stringify(obj: any, options: StringifyOptions = {}): string {
|
|
93
|
+
const indentSize = options.indentSize || 2
|
|
94
|
+
const delimiter = options.delimiter || ','
|
|
95
|
+
const lines: string[] = []
|
|
96
|
+
|
|
97
|
+
function serializeObject(o: any, depth: number = 0): void {
|
|
98
|
+
const indent = ' '.repeat(depth * indentSize)
|
|
99
|
+
|
|
100
|
+
for (const [key, value] of Object.entries(o)) {
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
const arrayStr = value.map(v => serializeValue(v)).join(delimiter)
|
|
103
|
+
lines.push(`${indent}${key}[${value.length}]: ${arrayStr}`)
|
|
104
|
+
} else if (value !== null && typeof value === 'object') {
|
|
105
|
+
lines.push(`${indent}${key}:`)
|
|
106
|
+
serializeObject(value, depth + 1)
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(`${indent}${key}: ${serializeValue(value)}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function serializeValue(value: any): string {
|
|
114
|
+
if (value === null) return 'null'
|
|
115
|
+
if (typeof value === 'boolean') return value.toString()
|
|
116
|
+
if (typeof value === 'number') return value.toString()
|
|
117
|
+
if (typeof value === 'string') {
|
|
118
|
+
if (needsQuotes(value, delimiter)) {
|
|
119
|
+
return `"${escapeString(value)}"`
|
|
120
|
+
}
|
|
121
|
+
return value
|
|
122
|
+
}
|
|
123
|
+
return String(value)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
serializeObject(obj)
|
|
127
|
+
|
|
128
|
+
let result = lines.join('\n')
|
|
129
|
+
|
|
130
|
+
// Handle finalEOL (default: true)
|
|
131
|
+
const finalEOL = options.finalEOL !== undefined ? options.finalEOL : true
|
|
132
|
+
if (finalEOL && !result.endsWith('\n')) {
|
|
133
|
+
result += '\n'
|
|
134
|
+
} else if (!finalEOL && result.endsWith('\n')) {
|
|
135
|
+
result = result.slice(0, -1)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Handle EOL customization
|
|
139
|
+
const EOL = options.EOL || '\n'
|
|
140
|
+
if (EOL !== '\n') {
|
|
141
|
+
result = result.replace(/\n/g, EOL)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function stripBom(content: string | Buffer): string {
|
|
148
|
+
if (Buffer.isBuffer(content)) content = content.toString('utf8')
|
|
149
|
+
return content.replace(/^\uFEFF/, '')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function applyReviver(obj: any, reviver: (key: string, value: any) => any): any {
|
|
153
|
+
return reviver('', obj)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseInlineArray(valueStr: string, expectedLength?: number): any[] {
|
|
157
|
+
const delimiter = detectDelimiter(valueStr)
|
|
158
|
+
const values = valueStr.split(delimiter).map(v => parseValue(v.trim()))
|
|
159
|
+
|
|
160
|
+
if (expectedLength !== undefined && values.length !== expectedLength) {
|
|
161
|
+
console.warn(`[toonfile] Array length mismatch: expected ${expectedLength}, got ${values.length}`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return values
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function detectDelimiter(str: string): string | RegExp {
|
|
168
|
+
if (str.includes('\t')) return '\t'
|
|
169
|
+
if (str.includes('|')) return '|'
|
|
170
|
+
return ','
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseValue(str: string): ToonValue {
|
|
174
|
+
if (str === 'null') return null
|
|
175
|
+
if (str === 'true') return true
|
|
176
|
+
if (str === 'false') return false
|
|
177
|
+
|
|
178
|
+
if (/^-?\d+(\.\d+)?$/.test(str)) {
|
|
179
|
+
return parseFloat(str)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if ((str.startsWith('"') && str.endsWith('"')) ||
|
|
183
|
+
(str.startsWith("'") && str.endsWith("'"))) {
|
|
184
|
+
return unescapeString(str.slice(1, -1))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return str
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getIndentLevel(line: string): number {
|
|
191
|
+
const match = line.match(/^( *)/)
|
|
192
|
+
return match ? match[1].length : 0
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function needsQuotes(str: string, delimiter: string): boolean {
|
|
196
|
+
const reservedWords = ['true', 'false', 'null']
|
|
197
|
+
if (reservedWords.includes(str)) return true
|
|
198
|
+
if (str.includes(delimiter)) return true
|
|
199
|
+
if (str.includes(':')) return true
|
|
200
|
+
if (str.includes('\n') || str.includes('\r') || str.includes('\t')) return true
|
|
201
|
+
if (/^-?\d+(\.\d+)?$/.test(str)) return true
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function escapeString(str: string): string {
|
|
206
|
+
return str
|
|
207
|
+
.replace(/\\/g, '\\\\')
|
|
208
|
+
.replace(/"/g, '\\"')
|
|
209
|
+
.replace(/\n/g, '\\n')
|
|
210
|
+
.replace(/\r/g, '\\r')
|
|
211
|
+
.replace(/\t/g, '\\t')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function unescapeString(str: string): string {
|
|
215
|
+
return str
|
|
216
|
+
.replace(/\\n/g, '\n')
|
|
217
|
+
.replace(/\\r/g, '\r')
|
|
218
|
+
.replace(/\\t/g, '\t')
|
|
219
|
+
.replace(/\\"/g, '"')
|
|
220
|
+
.replace(/\\'/g, "'")
|
|
221
|
+
.replace(/\\\\/g, '\\')
|
|
222
|
+
}
|