mirror of
https://github.com/voson-wang/toon.git
synced 2026-01-29 15:24:10 +08:00
perf: improve empty object checks
This commit is contained in:
@@ -73,7 +73,8 @@ function decodeObject(cursor: LineCursor, baseDepth: Depth, options: ResolvedDec
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (line.depth === computedDepth) {
|
if (line.depth === computedDepth) {
|
||||||
const [key, value, isQuoted] = decodeKeyValuePair(line, cursor, computedDepth, options)
|
cursor.advance()
|
||||||
|
const { key, value, isQuoted } = decodeKeyValue(line.content, cursor, computedDepth, options)
|
||||||
obj[key] = value
|
obj[key] = value
|
||||||
|
|
||||||
// Track quoted dotted keys for expansion phase
|
// Track quoted dotted keys for expansion phase
|
||||||
@@ -134,17 +135,6 @@ function decodeKeyValue(
|
|||||||
return { key, value: decodedValue, followDepth: baseDepth + 1, isQuoted }
|
return { key, value: decodedValue, followDepth: baseDepth + 1, isQuoted }
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeKeyValuePair(
|
|
||||||
line: ParsedLine,
|
|
||||||
cursor: LineCursor,
|
|
||||||
baseDepth: Depth,
|
|
||||||
options: ResolvedDecodeOptions,
|
|
||||||
): [key: string, value: JsonValue, isQuoted: boolean] {
|
|
||||||
cursor.advance()
|
|
||||||
const { key, value, isQuoted } = decodeKeyValue(line.content, cursor, baseDepth, options)
|
|
||||||
return [key, value, isQuoted]
|
|
||||||
}
|
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// #region Array decoding
|
// #region Array decoding
|
||||||
@@ -396,7 +386,8 @@ function decodeObjectFromListItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (line.depth === followDepth && !line.content.startsWith(LIST_ITEM_PREFIX)) {
|
if (line.depth === followDepth && !line.content.startsWith(LIST_ITEM_PREFIX)) {
|
||||||
const [k, v, kIsQuoted] = decodeKeyValuePair(line, cursor, followDepth, options)
|
cursor.advance()
|
||||||
|
const { key: k, value: v, isQuoted: kIsQuoted } = decodeKeyValue(line.content, cursor, followDepth, options)
|
||||||
obj[k] = v
|
obj[k] = v
|
||||||
|
|
||||||
// Track quoted dotted keys
|
// Track quoted dotted keys
|
||||||
|
|||||||
@@ -52,13 +52,11 @@ export function expandPathsSafe(value: JsonValue, strict: boolean): JsonValue {
|
|||||||
|
|
||||||
if (isJsonObject(value)) {
|
if (isJsonObject(value)) {
|
||||||
const expandedObject: JsonObject = {}
|
const expandedObject: JsonObject = {}
|
||||||
const keys = Object.keys(value)
|
|
||||||
|
|
||||||
// Check if this object has quoted key metadata
|
// Check if this object has quoted key metadata
|
||||||
const quotedKeys = (value as ObjectWithQuotedKeys)[QUOTED_KEY_MARKER]
|
const quotedKeys = (value as ObjectWithQuotedKeys)[QUOTED_KEY_MARKER]
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const [key, keyValue] of Object.entries(value)) {
|
||||||
const keyValue = value[key]!
|
|
||||||
|
|
||||||
// Skip expansion for keys that were originally quoted
|
// Skip expansion for keys that were originally quoted
|
||||||
const isQuoted = quotedKeys?.has(key)
|
const isQuoted = quotedKeys?.has(key)
|
||||||
@@ -207,8 +205,7 @@ function mergeObjects(
|
|||||||
source: JsonObject,
|
source: JsonObject,
|
||||||
strict: boolean,
|
strict: boolean,
|
||||||
): void {
|
): void {
|
||||||
for (const key of Object.keys(source)) {
|
for (const [key, sourceValue] of Object.entries(source)) {
|
||||||
const sourceValue = source[key]!
|
|
||||||
const targetValue = target[key]
|
const targetValue = target[key]
|
||||||
|
|
||||||
if (targetValue === undefined) {
|
if (targetValue === undefined) {
|
||||||
|
|||||||
@@ -302,12 +302,11 @@ export function parseQuotedKey(content: string, start: number): { key: string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseKeyToken(content: string, start: number): { key: string, end: number, isQuoted: boolean } {
|
export function parseKeyToken(content: string, start: number): { key: string, end: number, isQuoted: boolean } {
|
||||||
if (content[start] === DOUBLE_QUOTE) {
|
const isQuoted = content[start] === DOUBLE_QUOTE
|
||||||
return { ...parseQuotedKey(content, start), isQuoted: true }
|
const result = isQuoted
|
||||||
}
|
? parseQuotedKey(content, start)
|
||||||
else {
|
: parseUnquotedKey(content, start)
|
||||||
return { ...parseUnquotedKey(content, start), isQuoted: false }
|
return { ...result, isQuoted }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|||||||
@@ -47,17 +47,7 @@ export class LineCursor {
|
|||||||
|
|
||||||
peekAtDepth(targetDepth: Depth): ParsedLine | undefined {
|
peekAtDepth(targetDepth: Depth): ParsedLine | undefined {
|
||||||
const line = this.peek()
|
const line = this.peek()
|
||||||
if (!line || line.depth < targetDepth) {
|
return line?.depth === targetDepth ? line : undefined
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
if (line.depth === targetDepth) {
|
|
||||||
return line
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMoreAtDepth(targetDepth: Depth): boolean {
|
|
||||||
return this.peekAtDepth(targetDepth) !== undefined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,8 @@ export function validateNoExtraListItems(
|
|||||||
itemDepth: Depth,
|
itemDepth: Depth,
|
||||||
expectedCount: number,
|
expectedCount: number,
|
||||||
): void {
|
): void {
|
||||||
if (cursor.atEnd())
|
|
||||||
return
|
|
||||||
|
|
||||||
const nextLine = cursor.peek()
|
const nextLine = cursor.peek()
|
||||||
if (nextLine && nextLine.depth === itemDepth && nextLine.content.startsWith(LIST_ITEM_PREFIX)) {
|
if (nextLine?.depth === itemDepth && nextLine.content.startsWith(LIST_ITEM_PREFIX)) {
|
||||||
throw new RangeError(`Expected ${expectedCount} list array items, but found more`)
|
throw new RangeError(`Expected ${expectedCount} list array items, but found more`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,13 +38,9 @@ export function validateNoExtraTabularRows(
|
|||||||
rowDepth: Depth,
|
rowDepth: Depth,
|
||||||
header: ArrayHeaderInfo,
|
header: ArrayHeaderInfo,
|
||||||
): void {
|
): void {
|
||||||
if (cursor.atEnd())
|
|
||||||
return
|
|
||||||
|
|
||||||
const nextLine = cursor.peek()
|
const nextLine = cursor.peek()
|
||||||
if (
|
if (
|
||||||
nextLine
|
nextLine?.depth === rowDepth
|
||||||
&& nextLine.depth === rowDepth
|
|
||||||
&& !nextLine.content.startsWith(LIST_ITEM_PREFIX)
|
&& !nextLine.content.startsWith(LIST_ITEM_PREFIX)
|
||||||
&& isDataRow(nextLine.content, header.delimiter)
|
&& isDataRow(nextLine.content, header.delimiter)
|
||||||
) {
|
) {
|
||||||
@@ -71,14 +64,13 @@ export function validateNoBlankLinesInRange(
|
|||||||
// Find blank lines within the range
|
// Find blank lines within the range
|
||||||
// Note: We don't filter by depth because ANY blank line between array items is an error,
|
// Note: We don't filter by depth because ANY blank line between array items is an error,
|
||||||
// regardless of its indentation level
|
// regardless of its indentation level
|
||||||
const blanksInRange = blankLines.filter(
|
const firstBlank = blankLines.find(
|
||||||
blank => blank.lineNumber > startLine
|
blank => blank.lineNumber > startLine && blank.lineNumber < endLine,
|
||||||
&& blank.lineNumber < endLine,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (blanksInRange.length > 0) {
|
if (firstBlank) {
|
||||||
throw new SyntaxError(
|
throw new SyntaxError(
|
||||||
`Line ${blanksInRange[0]!.lineNumber}: Blank lines inside ${context} are not allowed in strict mode`,
|
`Line ${firstBlank.lineNumber}: Blank lines inside ${context} are not allowed in strict mode`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Depth, JsonArray, JsonObject, JsonPrimitive, JsonValue, ResolvedEncodeOptions } from '../types'
|
import type { Depth, JsonArray, JsonObject, JsonPrimitive, JsonValue, ResolvedEncodeOptions } from '../types'
|
||||||
import { DOT, LIST_ITEM_MARKER } from '../constants'
|
import { DOT, LIST_ITEM_MARKER } from '../constants'
|
||||||
import { tryFoldKeyChain } from './folding'
|
import { tryFoldKeyChain } from './folding'
|
||||||
import { isArrayOfArrays, isArrayOfObjects, isArrayOfPrimitives, isJsonArray, isJsonObject, isJsonPrimitive } from './normalize'
|
import { isArrayOfArrays, isArrayOfObjects, isArrayOfPrimitives, isEmptyObject, isJsonArray, isJsonObject, isJsonPrimitive } from './normalize'
|
||||||
import { encodeAndJoinPrimitives, encodeKey, encodePrimitive, formatHeader } from './primitives'
|
import { encodeAndJoinPrimitives, encodeKey, encodePrimitive, formatHeader } from './primitives'
|
||||||
import { LineWriter } from './writer'
|
import { LineWriter } from './writer'
|
||||||
|
|
||||||
@@ -38,8 +38,8 @@ export function encodeObject(value: JsonObject, writer: LineWriter, depth: Depth
|
|||||||
|
|
||||||
const effectiveFlattenDepth = remainingDepth ?? options.flattenDepth
|
const effectiveFlattenDepth = remainingDepth ?? options.flattenDepth
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const [key, val] of Object.entries(value)) {
|
||||||
encodeKeyValuePair(key, value[key]!, writer, depth, options, keys, rootLiteralKeys, pathPrefix, effectiveFlattenDepth)
|
encodeKeyValuePair(key, val, writer, depth, options, keys, rootLiteralKeys, pathPrefix, effectiveFlattenDepth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export function encodeKeyValuePair(key: string, value: JsonValue, writer: LineWr
|
|||||||
encodeArray(foldedKey, leafValue, writer, depth, options)
|
encodeArray(foldedKey, leafValue, writer, depth, options)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
else if (isJsonObject(leafValue) && Object.keys(leafValue).length === 0) {
|
else if (isJsonObject(leafValue) && isEmptyObject(leafValue)) {
|
||||||
writer.push(depth, `${encodedFoldedKey}:`)
|
writer.push(depth, `${encodedFoldedKey}:`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -94,13 +94,8 @@ export function encodeKeyValuePair(key: string, value: JsonValue, writer: LineWr
|
|||||||
encodeArray(key, value, writer, depth, options)
|
encodeArray(key, value, writer, depth, options)
|
||||||
}
|
}
|
||||||
else if (isJsonObject(value)) {
|
else if (isJsonObject(value)) {
|
||||||
const nestedKeys = Object.keys(value)
|
|
||||||
if (nestedKeys.length === 0) {
|
|
||||||
// Empty object
|
|
||||||
writer.push(depth, `${encodedKey}:`)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
writer.push(depth, `${encodedKey}:`)
|
writer.push(depth, `${encodedKey}:`)
|
||||||
|
if (!isEmptyObject(value)) {
|
||||||
encodeObject(value, writer, depth + 1, options, rootLiteralKeys, currentPath, effectiveFlattenDepth)
|
encodeObject(value, writer, depth + 1, options, rootLiteralKeys, currentPath, effectiveFlattenDepth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,16 +274,14 @@ export function encodeMixedArrayAsListItems(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function encodeObjectAsListItem(obj: JsonObject, writer: LineWriter, depth: Depth, options: ResolvedEncodeOptions): void {
|
export function encodeObjectAsListItem(obj: JsonObject, writer: LineWriter, depth: Depth, options: ResolvedEncodeOptions): void {
|
||||||
const keys = Object.keys(obj)
|
if (isEmptyObject(obj)) {
|
||||||
if (keys.length === 0) {
|
|
||||||
writer.push(depth, LIST_ITEM_MARKER)
|
writer.push(depth, LIST_ITEM_MARKER)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// First key-value on the same line as "- "
|
const entries = Object.entries(obj)
|
||||||
const firstKey = keys[0]!
|
const [firstKey, firstValue] = entries[0]!
|
||||||
const encodedKey = encodeKey(firstKey)
|
const encodedKey = encodeKey(firstKey)
|
||||||
const firstValue = obj[firstKey]!
|
|
||||||
|
|
||||||
if (isJsonPrimitive(firstValue)) {
|
if (isJsonPrimitive(firstValue)) {
|
||||||
writer.pushListItem(depth, `${encodedKey}: ${encodePrimitive(firstValue, options.delimiter)}`)
|
writer.pushListItem(depth, `${encodedKey}: ${encodePrimitive(firstValue, options.delimiter)}`)
|
||||||
@@ -327,20 +320,16 @@ export function encodeObjectAsListItem(obj: JsonObject, writer: LineWriter, dept
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (isJsonObject(firstValue)) {
|
else if (isJsonObject(firstValue)) {
|
||||||
const nestedKeys = Object.keys(firstValue)
|
|
||||||
if (nestedKeys.length === 0) {
|
|
||||||
writer.pushListItem(depth, `${encodedKey}:`)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
writer.pushListItem(depth, `${encodedKey}:`)
|
writer.pushListItem(depth, `${encodedKey}:`)
|
||||||
|
if (!isEmptyObject(firstValue)) {
|
||||||
encodeObject(firstValue, writer, depth + 2, options)
|
encodeObject(firstValue, writer, depth + 2, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remaining keys on indented lines
|
// Remaining entries on indented lines
|
||||||
for (let i = 1; i < keys.length; i++) {
|
for (let i = 1; i < entries.length; i++) {
|
||||||
const key = keys[i]!
|
const [key, value] = entries[i]!
|
||||||
encodeKeyValuePair(key, obj[key]!, writer, depth + 1, options)
|
encodeKeyValuePair(key, value, writer, depth + 1, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { JsonValue, ResolvedEncodeOptions } from '../types'
|
import type { JsonValue, ResolvedEncodeOptions } from '../types'
|
||||||
import { DOT } from '../constants'
|
import { DOT } from '../constants'
|
||||||
import { isIdentifierSegment } from '../shared/validation'
|
import { isIdentifierSegment } from '../shared/validation'
|
||||||
import { isJsonObject } from './normalize'
|
import { isEmptyObject, isJsonObject } from './normalize'
|
||||||
|
|
||||||
// #region Key folding helpers
|
// #region Key folding helpers
|
||||||
|
|
||||||
@@ -160,25 +160,13 @@ function collectSingleKeyChain(
|
|||||||
currentValue = nextValue
|
currentValue = nextValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the tail - simplified with early returns
|
// Determine the tail
|
||||||
if (!isJsonObject(currentValue)) {
|
if (!isJsonObject(currentValue) || isEmptyObject(currentValue)) {
|
||||||
// Array, primitive, or null - this is a leaf value
|
// Array, primitive, null, or empty object - this is a leaf value
|
||||||
return { segments, tail: undefined, leafValue: currentValue }
|
return { segments, tail: undefined, leafValue: currentValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Object.keys(currentValue)
|
// Has keys - return as tail (remainder)
|
||||||
|
|
||||||
if (keys.length === 0) {
|
|
||||||
// Empty object is a leaf
|
|
||||||
return { segments, tail: undefined, leafValue: currentValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (keys.length === 1 && segments.length === maxDepth) {
|
|
||||||
// Hit depth limit with remaining chain
|
|
||||||
return { segments, tail: currentValue, leafValue: currentValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-key object is the remainder
|
|
||||||
return { segments, tail: currentValue, leafValue: currentValue }
|
return { segments, tail: currentValue, leafValue: currentValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export function isJsonObject(value: unknown): value is JsonObject {
|
|||||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isEmptyObject(value: JsonObject): boolean {
|
||||||
|
return Object.keys(value).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
if (value === null || typeof value !== 'object') {
|
if (value === null || typeof value !== 'object') {
|
||||||
return false
|
return false
|
||||||
@@ -108,15 +112,15 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
|
|||||||
// #region Array type detection
|
// #region Array type detection
|
||||||
|
|
||||||
export function isArrayOfPrimitives(value: JsonArray): value is readonly JsonPrimitive[] {
|
export function isArrayOfPrimitives(value: JsonArray): value is readonly JsonPrimitive[] {
|
||||||
return value.every(item => isJsonPrimitive(item))
|
return value.length === 0 || value.every(item => isJsonPrimitive(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isArrayOfArrays(value: JsonArray): value is readonly JsonArray[] {
|
export function isArrayOfArrays(value: JsonArray): value is readonly JsonArray[] {
|
||||||
return value.every(item => isJsonArray(item))
|
return value.length === 0 || value.every(item => isJsonArray(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isArrayOfObjects(value: JsonArray): value is readonly JsonObject[] {
|
export function isArrayOfObjects(value: JsonArray): value is readonly JsonObject[] {
|
||||||
return value.every(item => isJsonObject(item))
|
return value.length === 0 || value.every(item => isJsonObject(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function encodePrimitive(value: JsonPrimitive, delimiter?: string): strin
|
|||||||
return encodeStringLiteral(value, delimiter)
|
return encodeStringLiteral(value, delimiter)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function encodeStringLiteral(value: string, delimiter: string = COMMA): string {
|
export function encodeStringLiteral(value: string, delimiter: string = DEFAULT_DELIMITER): string {
|
||||||
if (isSafeUnquoted(value, delimiter)) {
|
if (isSafeUnquoted(value, delimiter)) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ export function encodeKey(key: string): string {
|
|||||||
|
|
||||||
// #region Value joining
|
// #region Value joining
|
||||||
|
|
||||||
export function encodeAndJoinPrimitives(values: readonly JsonPrimitive[], delimiter: string = COMMA): string {
|
export function encodeAndJoinPrimitives(values: readonly JsonPrimitive[], delimiter: string = DEFAULT_DELIMITER): string {
|
||||||
return values.map(v => encodePrimitive(v, delimiter)).join(delimiter)
|
return values.map(v => encodePrimitive(v, delimiter)).join(delimiter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { COMMA, LIST_ITEM_MARKER } from '../constants'
|
import { DEFAULT_DELIMITER, LIST_ITEM_MARKER } from '../constants'
|
||||||
import { isBooleanOrNullLiteral } from './literal-utils'
|
import { isBooleanOrNullLiteral } from './literal-utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +39,7 @@ export function isIdentifierSegment(key: string): boolean {
|
|||||||
* - Contains the active delimiter
|
* - Contains the active delimiter
|
||||||
* - Starts with a list marker (hyphen)
|
* - Starts with a list marker (hyphen)
|
||||||
*/
|
*/
|
||||||
export function isSafeUnquoted(value: string, delimiter: string = COMMA): boolean {
|
export function isSafeUnquoted(value: string, delimiter: string = DEFAULT_DELIMITER): boolean {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user