Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

ploon

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ploon - npm Package Compare versions

Comparing version
1.0.6
to
1.0.7
+37
dist/config.d.cts
/**
* PLOON Configuration Management
*/
import type { PloonConfig, StringifyOptions, ParseOptions } from './types';
/**
* Merge user-provided config with defaults
*/
export declare function resolveConfig(config?: Partial<PloonConfig>): PloonConfig;
/**
* Resolve stringify options with defaults
*/
export declare function resolveStringifyOptions(options?: StringifyOptions): {
format: 'standard' | 'compact';
config: PloonConfig;
};
/**
* Resolve parse options with defaults
*/
export declare function resolveParseOptions(options?: ParseOptions): {
strict: boolean;
config: PloonConfig;
};
/**
* Validate configuration
* Ensures all config values are single characters (except recordSeparator)
*/
export declare function validateConfig(config: PloonConfig): void;
/**
* Detect format from PLOON string content
* Returns 'standard' if uses newlines, 'compact' if uses semicolons
*/
export declare function detectFormat(ploonString: string): 'standard' | 'compact';
/**
* Auto-detect configuration from PLOON string
* Attempts to detect delimiters used
*/
export declare function detectConfig(ploonString: string): Partial<PloonConfig>;
/**
* PLOON Constants and Default Configurations
*/
import type { PloonConfig } from './types';
/**
* Standard PLOON configuration (human-readable, newline-separated)
*/
export declare const PLOON_STANDARD: PloonConfig;
/**
* Compact PLOON configuration (token-optimized, semicolon-separated)
*/
export declare const PLOON_COMPACT: PloonConfig;
/**
* Default configuration (uses standard format)
*/
export declare const DEFAULT_CONFIG: PloonConfig;
/**
* Default stringify options
*/
export declare const DEFAULT_STRINGIFY_OPTIONS: {
format: 'standard';
config: PloonConfig;
};
/**
* Default parse options
*/
export declare const DEFAULT_PARSE_OPTIONS: {
strict: boolean;
config: PloonConfig;
};
/**
* PLOON Minify
* Convert Standard format (newline-separated) to Compact format (semicolon-separated)
*/
/**
* Minify PLOON string (Standard → Compact)
* Replaces newlines with semicolons and removes unnecessary whitespace
*/
export declare function minify(ploonString: string): string;
/**
* PLOON Parse - Public API
* Converts PLOON string to JavaScript object
*/
import type { JsonValue, ParseOptions } from '../types';
/**
* Parse PLOON string to JavaScript object
*
* @param ploonString - PLOON formatted string
* @param options - Parse options (strict, config)
* @returns JavaScript object
*
* @example
* ```typescript
* const ploon = `[products#1](id,name)
*
* 1|1|Shirt`
* const data = parse(ploon)
* // { products: [{ id: 1, name: "Shirt" }] }
* ```
*/
export declare function parse(ploonString: string, options?: ParseOptions): JsonValue;
/**
* PLOON Prettify
* Convert Compact format (semicolon-separated) to Standard format (newline-separated)
*/
/**
* Prettify PLOON string (Compact → Standard)
* Replaces semicolons with newlines for readability
*/
export declare function prettify(ploonString: string): string;
/**
* PLOON Stringify - Public API
* Converts JavaScript object to PLOON string
*/
import type { StringifyOptions } from '../types';
/**
* Convert JavaScript object to PLOON string
*
* @param input - JavaScript object, array, or primitive
* @param options - Stringify options (format, config)
* @returns PLOON formatted string
*
* @example
* ```typescript
* const data = { products: [{ id: 1, name: "Shirt" }] }
* const ploon = stringify(data)
* // [products#1](id,name)
* //
* // 1|1|Shirt
* ```
*/
export declare function stringify(input: unknown, options?: StringifyOptions): string;
/**
* PLOON Validation
* Validate PLOON format and schema
*/
import type { ValidationResult, PloonConfig } from '../types';
/**
* Check if a string is valid PLOON
*/
export declare function isValid(ploonString: string, config?: PloonConfig): boolean;
/**
* Validate PLOON string and return detailed result
*/
export declare function validate(ploonString: string, config?: PloonConfig): ValidationResult;
/**
* PLOON Decoder
* Reconstructs objects from PLOON records
*
* PLOON Record Format:
* - Array record: "4:1|value1|value2" (depth:index|values)
* - Object record: "4 |value1|value2" (depth |values)
* - Empty marker: "4:1|value1|" (trailing | means empty marker at end)
*/
import type { JsonValue, PloonConfig } from '../types';
/**
* Decode PLOON string to JavaScript object
*/
export declare function decode(ploonString: string, config: PloonConfig): JsonValue;
/**
* PLOON Schema Parser
* Parses PLOON schema to extract structure
*/
import type { PloonConfig } from '../types';
export interface ParsedSchema {
rootName: string;
count: number;
fields: ParsedField[];
}
export interface ParsedField {
name: string;
isArray: boolean;
isObject: boolean;
isPrimitiveArray?: boolean;
isOptional?: boolean;
nested?: ParsedSchema;
}
/**
* Parse PLOON schema
* Example: [products#2](id,name,price|colors#(name,hex))
*/
export declare function parseSchema(schema: string, config: PloonConfig): ParsedSchema;
/**
* PLOON Scanner
* Splits PLOON string into schema and records
*/
import type { PloonConfig } from '../types';
export interface ScannedRecord {
path: string;
values: string[];
}
export interface ScanResult {
schema: string;
records: ScannedRecord[];
}
/**
* Scan PLOON string and extract schema + records
*/
export declare function scan(ploonString: string, config: PloonConfig): ScanResult;
/**
* PLOON Encoder
* Main encoding logic - converts data to PLOON format
*
* PLOON Record Format:
* - Array record: "4:1|value1|value2" (depth:index|values)
* - Object record: "4 |value1|value2" (depth |values)
* - Empty marker: "4:1|value1|" (trailing | means empty marker at end)
*/
import type { JsonValue, PloonConfig } from '../types';
/**
* Encode data to PLOON format
*/
export declare function encode(data: JsonValue, config: PloonConfig): string;
/**
* Input Normalization
* Converts unknown input to JsonValue (adapted from TOON)
*/
import type { JsonValue, JsonArray, JsonObject, JsonPrimitive } from '../types';
/**
* Normalize unknown value to JsonValue
* Handles Date, BigInt, Set, Map, and other special types
*/
export declare function normalizeValue(value: unknown): JsonValue;
/**
* Type guards
*/
export declare function isJsonPrimitive(value: unknown): value is JsonPrimitive;
export declare function isJsonArray(value: unknown): value is JsonArray;
export declare function isJsonObject(value: unknown): value is JsonObject;
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
/**
* Array type detection
*/
export declare function isArrayOfPrimitives(value: JsonArray): value is JsonPrimitive[];
export declare function isArrayOfArrays(value: JsonArray): value is JsonArray[];
export declare function isArrayOfObjects(value: JsonArray): value is JsonObject[];
/**
* Check if array of objects has uniform schema
*/
export declare function hasUniformSchema(objects: JsonObject[]): boolean;
/**
* Get all keys from array of objects (union)
*/
export declare function getAllKeys(objects: JsonObject[]): string[];
/**
* Analyze fields in array of objects to determine which are required vs optional
* A field is optional if it's missing from any object OR empty (array/object with no content) in any object
* A field is required only if it appears in ALL objects AND always has content
*/
export declare function analyzeFields(objects: JsonObject[]): Map<string, {
isOptional: boolean;
}>;
/**
* Path Writer
* Builds path strings like:
* - 1:1, 2:1, 3:1 (depth:index format for arrays)
* - 1 , 2 , 3 (depth + space format for objects)
*/
import type { PloonConfig } from '../types';
export declare class PathWriter {
private pathSegments;
private config;
constructor(config: PloonConfig);
/**
* Get current path as string
* - Arrays: "depth:index" (e.g., "2:1")
* - Objects: "depth " (e.g., "2 ")
*/
getCurrentPath(): string;
/**
* Push an array segment (with index)
*/
pushArray(index: number): void;
/**
* Push an object segment (no index)
*/
pushObject(): void;
/**
* Push a new path segment (backwards compatibility)
* Defaults to array type
*/
push(index: number): void;
/**
* Pop the last path segment
*/
pop(): void;
/**
* Get depth (number of path segments)
*/
getDepth(): number;
/**
* Reset to empty path
*/
reset(): void;
/**
* Clone the current path writer
*/
clone(): PathWriter;
}
/**
* Schema Generation
* Generates PLOON schema from data structure
*/
import type { JsonValue, PloonConfig } from '../types';
/**
* Generate PLOON schema from data
* Example: [products#2](id,name,price|colors#(name,hex|sizes#(size,sku)))
*/
export declare function generateSchema(data: JsonValue, config: PloonConfig): string;
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
const fast_xml_parser = __toESM(require("fast-xml-parser"));
const yaml = __toESM(require("yaml"));
//#region src/encode/normalize.ts
/**
* Normalize unknown value to JsonValue
* Handles Date, BigInt, Set, Map, and other special types
*/
function normalizeValue(value) {
if (value === null) return null;
if (typeof value === "string" || typeof value === "boolean") return value;
if (typeof value === "number") {
if (Object.is(value, -0)) return 0;
if (!Number.isFinite(value)) return null;
return value;
}
if (typeof value === "bigint") {
if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER) return Number(value);
return value.toString();
}
if (value instanceof Date) return value.toISOString();
if (Array.isArray(value)) return value.map(normalizeValue);
if (value instanceof Set) return Array.from(value).map(normalizeValue);
if (value instanceof Map) return Object.fromEntries(Array.from(value, ([k, v]) => [String(k), normalizeValue(v)]));
if (isPlainObject(value)) {
const result = {};
for (const key in value) if (Object.prototype.hasOwnProperty.call(value, key)) result[key] = normalizeValue(value[key]);
return result;
}
return null;
}
function isJsonArray(value) {
return Array.isArray(value);
}
function isJsonObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function isPlainObject(value) {
if (value === null || typeof value !== "object") return false;
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
}
function isArrayOfObjects(value) {
return value.every((item) => isJsonObject(item));
}
/**
* Get all keys from array of objects (union)
*/
function getAllKeys(objects) {
const keySet = /* @__PURE__ */ new Set();
for (const obj of objects) for (const key of Object.keys(obj)) keySet.add(key);
return Array.from(keySet).sort();
}
/**
* Analyze fields in array of objects to determine which are required vs optional
* A field is optional if it's missing from any object OR empty (array/object with no content) in any object
* A field is required only if it appears in ALL objects AND always has content
*/
function analyzeFields(objects) {
const fieldMetadata = /* @__PURE__ */ new Map();
if (objects.length === 0) return fieldMetadata;
const fieldCounts = /* @__PURE__ */ new Map();
const fieldEverEmpty = /* @__PURE__ */ new Map();
for (const obj of objects) for (const key of Object.keys(obj)) {
fieldCounts.set(key, (fieldCounts.get(key) || 0) + 1);
const value = obj[key];
const isEmpty = isJsonArray(value) && value.length === 0 || isJsonObject(value) && !isJsonArray(value) && Object.keys(value).length === 0;
if (isEmpty) fieldEverEmpty.set(key, true);
}
const totalObjects = objects.length;
for (const [fieldName, count] of fieldCounts.entries()) {
const isMissingSomewhere = count < totalObjects;
const isEmptySomewhere = fieldEverEmpty.get(fieldName) || false;
fieldMetadata.set(fieldName, { isOptional: isMissingSomewhere || isEmptySomewhere });
}
return fieldMetadata;
}
//#endregion
//#region src/encode/schema.ts
/**
* Generate PLOON schema from data
* Example: [products#2](id,name,price|colors#(name,hex|sizes#(size,sku)))
*/
function generateSchema(data, config) {
const { name, array } = findRootArray$1(data);
if (!array) throw new Error("Data must contain at least one array to generate PLOON schema");
const schema = generateArraySchema(name, array, config);
return schema;
}
/**
* Find the root array in the data
* If data is an object with one array property, use that
* If data is an array, name it 'root'
* If data is an object with no arrays, wrap it as a single-element array
*/
function findRootArray$1(data) {
if (isJsonArray(data)) return {
name: "root",
array: data
};
if (isJsonObject(data)) {
for (const [key, value] of Object.entries(data)) if (isJsonArray(value)) return {
name: key,
array: value
};
return {
name: "data",
array: [data]
};
}
return {
name: "data",
array: [data]
};
}
/**
* Generate schema for an array
*/
function generateArraySchema(name, array, config) {
const { arraySizeMarker, fieldsOpen, fieldsClose, nestedSeparator, schemaOpen, schemaClose } = config;
const count = array.length;
const fields = analyzeArrayFields(array);
const fieldStrings = [];
for (const field of fields) if (field.type === "array" && field.nested && isJsonArray(field.nested)) {
const nestedSchema = generateNestedArraySchema(field.name, field.nested, field.isOptional, config);
fieldStrings.push(nestedSchema);
} else if (field.type === "object" && field.nested && isJsonObject(field.nested)) {
const nestedSchema = generateNestedObjectSchema(field.name, field.nested, field.isOptional, config);
fieldStrings.push(nestedSchema);
} else if (field.type === "primitiveArray") {
const fieldName = formatFieldName(field.name, field.isOptional, config);
fieldStrings.push(`${fieldName}${config.arraySizeMarker}${config.fieldsOpen}${config.fieldsClose}`);
} else {
const fieldName = formatFieldName(field.name, field.isOptional, config);
fieldStrings.push(fieldName);
}
const fieldsStr = fieldStrings.join(config.schemaFieldSeparator);
return `${schemaOpen}${name}${arraySizeMarker}${count}${schemaClose}${fieldsOpen}${fieldsStr}${fieldsClose}`;
}
/**
* Generate schema for nested array (without outer brackets)
*/
function generateNestedArraySchema(name, array, isOptional, config) {
const { arraySizeMarker, fieldsOpen, fieldsClose } = config;
const count = array.length;
const fields = analyzeArrayFields(array);
const fieldStrings = [];
for (const field of fields) if (field.type === "array" && field.nested && isJsonArray(field.nested)) {
const nestedSchema = generateNestedArraySchema(field.name, field.nested, field.isOptional, config);
fieldStrings.push(nestedSchema);
} else if (field.type === "object" && field.nested && isJsonObject(field.nested)) {
const nestedSchema = generateNestedObjectSchema(field.name, field.nested, field.isOptional, config);
fieldStrings.push(nestedSchema);
} else if (field.type === "primitiveArray") {
const fieldName = formatFieldName(field.name, field.isOptional, config);
fieldStrings.push(`${fieldName}${arraySizeMarker}${fieldsOpen}${fieldsClose}`);
} else {
const fieldName = formatFieldName(field.name, field.isOptional, config);
fieldStrings.push(fieldName);
}
const fieldsStr = fieldStrings.join(config.schemaFieldSeparator);
const formattedName = formatFieldName(name, isOptional, config);
return `${formattedName}${arraySizeMarker}${count}${fieldsOpen}${fieldsStr}${fieldsClose}`;
}
/**
* Generate schema for nested object
* Format: name{field1,field2,nested{field3}}
*/
function generateNestedObjectSchema(name, obj, isOptional, config) {
const keys = Object.keys(obj).sort();
const primitiveKeys = [];
const requiredComplexKeys = [];
const optionalComplexKeys = [];
for (const key of keys) {
const value = obj[key];
if (isJsonArray(value)) {
const isPrimArray = value.length === 0 || !isArrayOfObjects(value);
if (isPrimArray) primitiveKeys.push(key);
else requiredComplexKeys.push(key);
} else if (isJsonObject(value) && !isJsonArray(value)) if (Object.keys(value).length === 0) optionalComplexKeys.push(key);
else requiredComplexKeys.push(key);
else primitiveKeys.push(key);
}
const fieldStrings = [];
for (const key of primitiveKeys) {
const value = obj[key];
if (isJsonArray(value)) fieldStrings.push(`${key}${config.arraySizeMarker}${config.fieldsOpen}${config.fieldsClose}`);
else fieldStrings.push(key);
}
for (const key of requiredComplexKeys) {
const value = obj[key];
if (isJsonArray(value)) {
const nestedSchema = generateNestedArraySchema(key, value, false, config);
fieldStrings.push(nestedSchema);
} else if (isJsonObject(value) && !isJsonArray(value)) {
const nestedSchema = generateNestedObjectSchema(key, value, false, config);
fieldStrings.push(nestedSchema);
}
}
for (const key of optionalComplexKeys) {
const value = obj[key];
if (isJsonArray(value)) {
const nestedSchema = generateNestedArraySchema(key, value, true, config);
fieldStrings.push(nestedSchema);
} else if (isJsonObject(value) && !isJsonArray(value)) {
const nestedSchema = generateNestedObjectSchema(key, value, true, config);
fieldStrings.push(nestedSchema);
}
}
const fieldsStr = fieldStrings.join(config.schemaFieldSeparator);
const formattedName = formatFieldName(name, isOptional, config);
return `${formattedName}{${fieldsStr}}`;
}
/**
* Format field name with optional marker suffix if needed
*/
function formatFieldName(name, isOptional, config) {
if (isOptional) return `${name}${config.optionalFieldMarker}`;
return name;
}
/**
* Analyze array to determine fields and nested arrays/objects
*/
function analyzeArrayFields(array) {
if (array.length === 0) return [];
if (isArrayOfObjects(array)) {
const keys = getAllKeys(array);
const fieldMetadata = analyzeFields(array);
const fields = [];
for (const key of keys) {
const metadata = fieldMetadata.get(key);
const isOptional = metadata?.isOptional ?? false;
const nestedArray = findNestedArray(array, key);
if (nestedArray) {
fields.push({
name: key,
type: "array",
isOptional,
nested: nestedArray
});
continue;
}
const nestedObject = findNestedObject(array, key);
if (nestedObject) {
fields.push({
name: key,
type: "object",
isOptional,
nested: nestedObject
});
continue;
}
const firstValue = array.find((obj) => key in obj)?.[key];
if (isJsonArray(firstValue)) {
const isPrimArray = firstValue.length === 0 || !isArrayOfObjects(firstValue);
if (isPrimArray) {
fields.push({
name: key,
type: "primitiveArray",
isOptional
});
continue;
}
}
fields.push({
name: key,
type: "primitive",
isOptional
});
}
return fields.sort((a, b) => {
const aIsPrimitive = a.type === "primitive" || a.type === "primitiveArray";
const bIsPrimitive = b.type === "primitive" || b.type === "primitiveArray";
if (aIsPrimitive !== bIsPrimitive) return aIsPrimitive ? -1 : 1;
if (a.isOptional !== b.isOptional) return a.isOptional ? 1 : -1;
return 0;
});
}
return [];
}
/**
* Find nested array for a given key across all objects
* Only returns array if MAJORITY of present values are arrays (handles type inconsistencies)
*/
function findNestedArray(objects, key) {
let arrayCount = 0;
let primitiveCount = 0;
let objectCount = 0;
let foundArray;
for (const obj of objects) {
if (!(key in obj)) continue;
const value = obj[key];
if (isJsonArray(value)) {
arrayCount++;
if (!foundArray || foundArray.length === 0 && value.length > 0) foundArray = value;
} else if (isJsonObject(value)) objectCount++;
else primitiveCount++;
}
if (arrayCount > primitiveCount && arrayCount > objectCount) {
if (foundArray && foundArray.length > 0 && isArrayOfObjects(foundArray)) return foundArray;
}
return void 0;
}
/**
* Find nested object for a given key across all objects
* Only returns object if MAJORITY of present values are objects (handles type inconsistencies)
*/
function findNestedObject(objects, key) {
let arrayCount = 0;
let primitiveCount = 0;
let objectCount = 0;
let foundObject;
for (const obj of objects) {
if (!(key in obj)) continue;
const value = obj[key];
if (isJsonObject(value) && !isJsonArray(value)) {
objectCount++;
if (!foundObject || Object.keys(foundObject).length === 0 && Object.keys(value).length > 0) foundObject = value;
} else if (isJsonArray(value)) arrayCount++;
else primitiveCount++;
}
if (objectCount > primitiveCount && objectCount > arrayCount) return foundObject;
return void 0;
}
//#endregion
//#region src/encode/path-writer.ts
var PathWriter = class PathWriter {
pathSegments = [];
config;
constructor(config) {
this.config = config;
}
/**
* Get current path as string
* - Arrays: "depth:index" (e.g., "2:1")
* - Objects: "depth " (e.g., "2 ")
*/
getCurrentPath() {
if (this.pathSegments.length === 0) return "";
const depth = this.pathSegments.length;
const lastSegment = this.pathSegments[this.pathSegments.length - 1];
if (!lastSegment) return "";
if (lastSegment.type === "array") return `${depth}${this.config.pathSeparator}${lastSegment.index}`;
else return `${depth} `;
}
/**
* Push an array segment (with index)
*/
pushArray(index) {
this.pathSegments.push({
type: "array",
index
});
}
/**
* Push an object segment (no index)
*/
pushObject() {
this.pathSegments.push({ type: "object" });
}
/**
* Push a new path segment (backwards compatibility)
* Defaults to array type
*/
push(index) {
this.pushArray(index);
}
/**
* Pop the last path segment
*/
pop() {
this.pathSegments.pop();
}
/**
* Get depth (number of path segments)
*/
getDepth() {
return this.pathSegments.length;
}
/**
* Reset to empty path
*/
reset() {
this.pathSegments = [];
}
/**
* Clone the current path writer
*/
clone() {
const clone = new PathWriter(this.config);
clone.pathSegments = [...this.pathSegments];
return clone;
}
};
//#endregion
//#region src/shared/string-utils.ts
/**
* Escape special characters in a value
*/
function escapeValue(value, config) {
const { escapeChar, fieldDelimiter, recordSeparator, pathSeparator } = config;
let escaped = value;
escaped = escaped.replace(new RegExp(`\\${escapeChar}`, "g"), `${escapeChar}${escapeChar}`);
escaped = escaped.replace(new RegExp(`\\${fieldDelimiter}`, "g"), `${escapeChar}${fieldDelimiter}`);
if (recordSeparator.length === 1) escaped = escaped.replace(new RegExp(`\\${recordSeparator}`, "g"), `${escapeChar}${recordSeparator}`);
else escaped = escaped.replace(new RegExp(escapeRegex(recordSeparator), "g"), `${escapeChar}${recordSeparator}`);
escaped = escaped.replace(/,/g, `${escapeChar},`);
return escaped;
}
/**
* Unescape special characters in a value
*/
function unescapeValue(value, config) {
const { escapeChar, fieldDelimiter, recordSeparator } = config;
let unescaped = value;
unescaped = unescaped.replace(new RegExp(`\\${escapeChar}\\${fieldDelimiter}`, "g"), fieldDelimiter);
if (recordSeparator.length === 1) unescaped = unescaped.replace(new RegExp(`\\${escapeChar}\\${recordSeparator}`, "g"), recordSeparator);
else unescaped = unescaped.replace(new RegExp(`\\${escapeChar}${escapeRegex(recordSeparator)}`, "g"), recordSeparator);
unescaped = unescaped.replace(new RegExp(`\\${escapeChar},`, "g"), ",");
unescaped = unescaped.replace(new RegExp(`\\${escapeChar}\\${escapeChar}`, "g"), escapeChar);
return unescaped;
}
/**
* Format a value for output (converts to string and escapes)
*/
function formatValue(value, config, isOptional) {
if (value === null) return "null";
if (value === void 0) return isOptional ? "" : "null";
const str = String(value);
return escapeValue(str, config);
}
/**
* Format a primitive array for inline encoding (comma-separated)
* Handles null values and respects preserveEmptyFields config
*/
function formatPrimitiveArray(array, config, preserveEmpty) {
let elements = array.map((item) => {
if (item === null) return "";
if (item === void 0) return "";
const str = String(item);
return escapeValue(str, config);
});
if (!preserveEmpty) elements = elements.filter((el) => el !== "");
return elements.join(",");
}
/**
* Escape regex special characters
*/
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Split a string by delimiter, respecting escapes
* Only unescapes the delimiter and escape char itself, preserves other escape sequences
*/
function splitEscaped(str, delimiter, escapeChar) {
const parts = [];
let current = "";
let i = 0;
while (i < str.length) if (str[i] === escapeChar && i + 1 < str.length) {
const nextChar = str[i + 1];
if (nextChar === delimiter || nextChar === escapeChar) {
current += nextChar;
i += 2;
} else {
current += str[i] + nextChar;
i += 2;
}
} else if (str.slice(i, i + delimiter.length) === delimiter) {
parts.push(current);
current = "";
i += delimiter.length;
} else {
current += str[i];
i++;
}
parts.push(current);
return parts;
}
//#endregion
//#region src/encode/encoder.ts
/**
* Encode data to PLOON format
*/
function encode(data, config) {
const schema = generateSchema(data, config);
const rootArray = findRootArray(data);
const records = encodeArray(rootArray, new PathWriter(config), config);
const { recordSeparator } = config;
const result = schema + recordSeparator + recordSeparator + records.join(recordSeparator);
return result;
}
/**
* Find root array in data
* Auto-wraps single objects/primitives into arrays
*/
function findRootArray(data) {
if (isJsonArray(data)) return data;
if (isJsonObject(data)) {
for (const value of Object.values(data)) if (isJsonArray(value)) return value;
return [data];
}
return [data];
}
/**
* Encode an array to records
*/
function encodeArray(array, pathWriter, config) {
const records = [];
if (!isArrayOfObjects(array)) {
const { fieldDelimiter } = config;
array.forEach((value, index) => {
const itemIndex = index + 1;
pathWriter.push(itemIndex);
const path = pathWriter.getCurrentPath();
const formattedValue = formatValue(value, config);
const record = `${path}${fieldDelimiter}${formattedValue}`;
records.push(record);
pathWriter.pop();
});
return records;
}
const keys = getAllKeys(array);
const fieldMetadata = analyzeFields(array);
const fields = keys.map((key) => {
const value = array.find((obj) => obj[key] !== void 0)?.[key];
const isOptional = fieldMetadata.get(key)?.isOptional ?? false;
if (isJsonArray(value)) if (value.length > 0 && isArrayOfObjects(value)) return {
name: key,
type: "array",
isOptional,
nested: value
};
else return {
name: key,
type: "primitiveArray",
isOptional
};
else if (isJsonObject(value) && !isJsonArray(value)) return {
name: key,
type: "object",
isOptional,
nested: value
};
else return {
name: key,
type: "primitive",
isOptional
};
});
fields.sort((a, b) => {
const aIsPrimitive = a.type === "primitive" || a.type === "primitiveArray";
const bIsPrimitive = b.type === "primitive" || b.type === "primitiveArray";
if (aIsPrimitive !== bIsPrimitive) return aIsPrimitive ? -1 : 1;
if (a.isOptional !== b.isOptional) return a.isOptional ? 1 : -1;
return 0;
});
array.forEach((obj, index) => {
const itemIndex = index + 1;
pathWriter.push(itemIndex);
const record = encodeObject(obj, fields, pathWriter, config);
records.push(record);
for (const field of fields) {
if (field.type === "primitive") continue;
const value = obj[field.name];
if (field.type === "array" && isJsonArray(value)) {
const nestedRecords = encodeArray(value, pathWriter, config);
records.push(...nestedRecords);
} else if (field.type === "object" && isJsonObject(value) && !isJsonArray(value)) {
const nestedRecords = encodeNestedObject(value, pathWriter, config);
records.push(...nestedRecords);
}
}
pathWriter.pop();
});
return records;
}
/**
* Encode a single object to a record
*/
function encodeObject(obj, fields, pathWriter, config) {
const { fieldDelimiter } = config;
const path = pathWriter.getCurrentPath();
const values = [path];
const primitiveFields = fields.filter((f) => f.type === "primitive" || f.type === "primitiveArray");
const complexFields = fields.filter((f) => f.type === "array" || f.type === "object");
for (const field of primitiveFields) {
const value = obj[field.name];
if (field.isOptional && !(field.name in obj)) values.push("");
else if (field.type === "primitiveArray" && Array.isArray(value)) if (value.length === 0) values.push(",");
else values.push(formatPrimitiveArray(value, config, config.preserveEmptyFields));
else values.push(formatValue(value, config, false));
}
/**
* Optional Complex Field Marker Strategy:
*
* Optional arrays/objects are placed at the END of the schema in alphabetical order.
* Their data comes as child records, but we need to tell the decoder which fields
* are absent vs present.
*
* Encoding rules:
* 1. Find the LAST optional field that has data (non-empty or non-absent)
* 2. For each optional field UP TO that last one:
* - If ABSENT or EMPTY: write || marker
* - If HAS DATA: write nothing (data comes as child records)
* 3. After the last non-empty field: stop (trailing absent fields omitted)
*
* Example with fields [_collections?, collections?, is_yalla?, variants?]:
* Hit has collections=38, is_yalla=2 (no _collections, no variants)
* → Last non-empty is is_yalla at index 2
* → Write: || (for _collections) then stop
* → Child records: 38 for collections, 2 for is_yalla
*/
let lastPresentOptionalIndex = -1;
for (let i = 0; i < complexFields.length; i++) {
const field = complexFields[i];
if (!field.isOptional) continue;
if (field.name in obj) lastPresentOptionalIndex = i;
}
for (let i = 0; i < complexFields.length; i++) {
const field = complexFields[i];
if (!field.isOptional) continue;
if (i > lastPresentOptionalIndex) break;
if (field.name in obj) {
const value = obj[field.name];
const isEmpty = field.type === "array" && isJsonArray(value) && value.length === 0 || field.type === "object" && isJsonObject(value) && !isJsonArray(value) && Object.keys(value).length === 0;
if (isEmpty) values.push("");
}
}
return values.join(fieldDelimiter);
}
/**
* Encode a nested object to records
* Objects use "depth " path notation (no index)
*/
function encodeNestedObject(obj, pathWriter, config) {
const records = [];
pathWriter.pushObject();
const keys = Object.keys(obj).sort();
const { fieldDelimiter } = config;
const path = pathWriter.getCurrentPath();
const values = [path];
const primitiveKeys = [];
const complexKeys = [];
for (const key of keys) {
const value = obj[key];
if (isJsonArray(value)) {
const isPrimArray = value.length === 0 || !isArrayOfObjects(value);
if (isPrimArray) primitiveKeys.push(key);
else complexKeys.push(key);
} else if (isJsonObject(value) && !isJsonArray(value)) complexKeys.push(key);
else primitiveKeys.push(key);
}
for (const key of primitiveKeys) {
const value = obj[key];
if (isJsonArray(value)) if (value.length === 0) values.push(",");
else values.push(formatPrimitiveArray(value, config, config.preserveEmptyFields));
else values.push(formatValue(value, config));
}
let lastNonEmptyIndex = -1;
for (let i = 0; i < complexKeys.length; i++) {
const key = complexKeys[i];
const value = obj[key];
const isEmpty = isJsonArray(value) && value.length === 0 || isJsonObject(value) && !isJsonArray(value) && Object.keys(value).length === 0;
if (!isEmpty) lastNonEmptyIndex = i;
}
for (let i = 0; i <= lastNonEmptyIndex; i++) {
const key = complexKeys[i];
const value = obj[key];
const isEmpty = isJsonArray(value) && value.length === 0 || isJsonObject(value) && !isJsonArray(value) && Object.keys(value).length === 0;
if (isEmpty) values.push("");
}
records.push(values.join(fieldDelimiter));
for (const key of keys) {
const value = obj[key];
if (isJsonArray(value)) {
const isPrimArray = value.length === 0 || !isArrayOfObjects(value);
if (!isPrimArray) {
const nestedRecords = encodeArray(value, pathWriter, config);
records.push(...nestedRecords);
}
} else if (isJsonObject(value) && !isJsonArray(value)) {
const nestedRecords = encodeNestedObject(value, pathWriter, config);
records.push(...nestedRecords);
}
}
pathWriter.pop();
return records;
}
//#endregion
//#region src/constants.ts
/**
* Standard PLOON configuration (human-readable, newline-separated)
*/
const PLOON_STANDARD = {
fieldDelimiter: "|",
pathSeparator: ":",
arraySizeMarker: "#",
recordSeparator: "\n",
escapeChar: "\\",
schemaOpen: "[",
schemaClose: "]",
fieldsOpen: "(",
fieldsClose: ")",
nestedSeparator: "|",
schemaFieldSeparator: "|",
schemaWhitespace: " ",
optionalFieldMarker: "?",
preserveEmptyFields: false
};
/**
* Compact PLOON configuration (token-optimized, semicolon-separated)
*/
const PLOON_COMPACT = {
...PLOON_STANDARD,
recordSeparator: ";"
};
/**
* Default configuration (uses standard format)
*/
const DEFAULT_CONFIG = PLOON_STANDARD;
//#endregion
//#region src/config.ts
/**
* Merge user-provided config with defaults
*/
function resolveConfig(config) {
if (!config) return DEFAULT_CONFIG;
return {
...DEFAULT_CONFIG,
...config
};
}
/**
* Resolve stringify options with defaults
*/
function resolveStringifyOptions(options) {
const format = options?.format ?? "standard";
const baseConfig = format === "compact" ? PLOON_COMPACT : PLOON_STANDARD;
const config = options?.config ? {
...baseConfig,
...options.config
} : baseConfig;
return {
format,
config
};
}
/**
* Resolve parse options with defaults
*/
function resolveParseOptions(options) {
return {
strict: options?.strict ?? true,
config: resolveConfig(options?.config)
};
}
/**
* Validate configuration
* Ensures all config values are single characters (except recordSeparator)
*/
function validateConfig(config) {
const errors = [];
const singleCharFields = [
"fieldDelimiter",
"pathSeparator",
"arraySizeMarker",
"escapeChar",
"schemaOpen",
"schemaClose",
"fieldsOpen",
"fieldsClose",
"nestedSeparator",
"schemaFieldSeparator",
"schemaWhitespace",
"optionalFieldMarker"
];
for (const field of singleCharFields) {
const value = config[field];
if (typeof value === "string" && value.length !== 1) errors.push(`${field} must be exactly 1 character, got: "${value}"`);
}
if (config.recordSeparator.length === 0) errors.push("recordSeparator cannot be empty");
const chars = /* @__PURE__ */ new Map();
const addChar = (char, field) => {
if (!chars.has(char)) chars.set(char, []);
chars.get(char).push(field);
};
addChar(config.fieldDelimiter, "fieldDelimiter");
addChar(config.pathSeparator, "pathSeparator");
addChar(config.arraySizeMarker, "arraySizeMarker");
addChar(config.escapeChar, "escapeChar");
addChar(config.schemaOpen, "schemaOpen");
addChar(config.schemaClose, "schemaClose");
addChar(config.fieldsOpen, "fieldsOpen");
addChar(config.fieldsClose, "fieldsClose");
addChar(config.nestedSeparator, "nestedSeparator");
addChar(config.schemaFieldSeparator, "schemaFieldSeparator");
addChar(config.schemaWhitespace, "schemaWhitespace");
addChar(config.optionalFieldMarker, "optionalFieldMarker");
for (const [char, fields] of chars.entries()) if (fields.length > 1) errors.push(`Character "${char}" is used for multiple purposes: ${fields.join(", ")}`);
if (errors.length > 0) throw new Error(`Invalid PLOON configuration:\n${errors.join("\n")}`);
}
/**
* Detect format from PLOON string content
* Returns 'standard' if uses newlines, 'compact' if uses semicolons
*/
function detectFormat(ploonString) {
const schemaEnd = ploonString.indexOf(")");
if (schemaEnd === -1) return "standard";
const afterSchema = ploonString.slice(schemaEnd + 1);
if (afterSchema.includes(";") && !afterSchema.includes("\n")) return "compact";
if (afterSchema.includes("\n")) return "standard";
return "standard";
}
/**
* Auto-detect configuration from PLOON string
* Attempts to detect delimiters used
*/
function detectConfig(ploonString) {
const format = detectFormat(ploonString);
const baseConfig = format === "compact" ? PLOON_COMPACT : PLOON_STANDARD;
return baseConfig;
}
//#endregion
//#region src/core/stringify.ts
/**
* Convert JavaScript object to PLOON string
*
* @param input - JavaScript object, array, or primitive
* @param options - Stringify options (format, config)
* @returns PLOON formatted string
*
* @example
* ```typescript
* const data = { products: [{ id: 1, name: "Shirt" }] }
* const ploon = stringify(data)
* // [products#1](id,name)
* //
* // 1|1|Shirt
* ```
*/
function stringify(input, options) {
const resolved = resolveStringifyOptions(options);
const normalized = normalizeValue(input);
const ploon = encode(normalized, resolved.config);
return ploon;
}
//#endregion
//#region src/decode/scanner.ts
/**
* Scan PLOON string and extract schema + records
*/
function scan(ploonString, config) {
const { recordSeparator, fieldDelimiter, escapeChar } = config;
const parts = splitEscaped(ploonString, recordSeparator, escapeChar);
if (parts.length < 1) throw new Error("Invalid PLOON: No schema found");
let schemaIndex = 0;
let schema = parts[0].trim();
while (schemaIndex + 1 < parts.length && parts[schemaIndex + 1].trim() === "") schemaIndex++;
const recordParts = parts.slice(schemaIndex + 1);
const records = [];
for (const part of recordParts) {
const trimmed = part.trim();
if (trimmed === "") continue;
const fields = splitEscaped(trimmed, fieldDelimiter, escapeChar);
if (fields.length < 1) continue;
const [path, ...values] = fields;
records.push({
path,
values
});
}
return {
schema,
records
};
}
//#endregion
//#region src/decode/parser.ts
/**
* Parse PLOON schema
* Example: [products#2](id,name,price|colors#(name,hex))
*/
function parseSchema(schema, config) {
const { schemaOpen, schemaClose, fieldsOpen, fieldsClose, arraySizeMarker } = config;
const headerStart = schema.indexOf(schemaOpen);
const headerEnd = schema.indexOf(schemaClose, headerStart);
if (headerStart === -1 || headerEnd === -1) throw new Error(`Invalid PLOON schema: ${schema}`);
const header = schema.slice(headerStart + 1, headerEnd);
const [rootName, countStr] = header.split(arraySizeMarker);
const count = countStr ? parseInt(countStr, 10) : 0;
const fieldsStart = schema.indexOf(fieldsOpen, headerEnd);
if (fieldsStart === -1) throw new Error(`Invalid PLOON schema: No fields found`);
const fieldsEnd = findMatchingCloseParen(schema, fieldsStart, fieldsOpen, fieldsClose);
if (fieldsEnd === -1) throw new Error(`Invalid PLOON schema: Unmatched parentheses`);
const fieldsStr = schema.slice(fieldsStart + 1, fieldsEnd);
const fields = parseFields(fieldsStr, config);
return {
rootName,
count,
fields
};
}
/**
* Parse fields string with proper parentheses matching
*/
function parseFields(fieldsStr, config) {
const fields = [];
let pos = 0;
while (pos < fieldsStr.length) {
while (pos < fieldsStr.length && fieldsStr[pos] === config.schemaWhitespace) pos++;
if (pos >= fieldsStr.length) break;
const { field, nextPos } = parseNextField(fieldsStr, pos, config);
fields.push(field);
pos = nextPos;
while (pos < fieldsStr.length && (fieldsStr[pos] === config.schemaFieldSeparator || fieldsStr[pos] === config.schemaWhitespace)) pos++;
}
return fields;
}
/**
* Parse a single field definition
* Handles nested arrays like "colors#(name,hex)" and objects like "customer{id,name}"
*/
function parseNextField(str, start, config) {
const { arraySizeMarker, fieldsOpen, fieldsClose, schemaFieldSeparator } = config;
let depth = 0;
let braceDepth = 0;
let pos = start;
while (pos < str.length) {
if (str[pos] === fieldsOpen) depth++;
else if (str[pos] === fieldsClose) depth--;
else if (str[pos] === "{") braceDepth++;
else if (str[pos] === "}") braceDepth--;
else if (str[pos] === schemaFieldSeparator && depth === 0 && braceDepth === 0) break;
pos++;
}
const fieldStr = str.slice(start, pos).trim();
const field = parseFieldDefinition(fieldStr, config);
return {
field,
nextPos: pos
};
}
/**
* Parse field definition string
* Returns ParsedField with nested schema if array or object
* Examples:
* - Array: "colors#(name,hex)"
* - Array (optional): "colors?#(name,hex)"
* - Object: "customer{id,name,address{street,city}}"
* - Object (optional): "customer?{id,name}"
* - Primitive (optional): "email?"
*/
function parseFieldDefinition(fieldStr, config) {
const { arraySizeMarker, fieldsOpen, fieldsClose, optionalFieldMarker } = config;
let arrayMarkerIndex = fieldStr.indexOf(arraySizeMarker);
let objectBraceIndex = fieldStr.indexOf("{");
let hasArray = arrayMarkerIndex !== -1;
let hasObject = objectBraceIndex !== -1;
if (hasArray && hasObject) if (arrayMarkerIndex < objectBraceIndex) hasObject = false;
else hasArray = false;
let isOptional = false;
let fieldStrWithoutMarker = fieldStr;
if (hasObject) {
const beforeBrace = fieldStr.slice(0, objectBraceIndex);
if (beforeBrace.endsWith(optionalFieldMarker)) {
isOptional = true;
fieldStrWithoutMarker = beforeBrace.slice(0, -optionalFieldMarker.length) + fieldStr.slice(objectBraceIndex);
}
} else if (hasArray) {
const beforeMarker = fieldStr.slice(0, arrayMarkerIndex);
if (beforeMarker.endsWith(optionalFieldMarker)) {
isOptional = true;
fieldStrWithoutMarker = beforeMarker.slice(0, -optionalFieldMarker.length) + fieldStr.slice(arrayMarkerIndex);
}
} else if (fieldStr.endsWith(optionalFieldMarker)) {
isOptional = true;
fieldStrWithoutMarker = fieldStr.slice(0, -optionalFieldMarker.length);
}
fieldStr = fieldStrWithoutMarker;
if (hasArray) arrayMarkerIndex = fieldStr.indexOf(arraySizeMarker);
if (hasObject) objectBraceIndex = fieldStr.indexOf("{");
if (hasObject) {
const name = fieldStr.slice(0, objectBraceIndex).trim();
const closePos = findMatchingCloseBrace(fieldStr, objectBraceIndex);
if (closePos === -1) throw new Error(`Unmatched braces in field: ${fieldStr}`);
const nestedFieldsStr = fieldStr.slice(objectBraceIndex + 1, closePos);
const nestedFields = parseFields(nestedFieldsStr, config);
return {
name,
isArray: false,
isObject: true,
isOptional,
nested: {
rootName: name,
count: 0,
fields: nestedFields
}
};
}
if (hasArray) {
const name = fieldStr.slice(0, arrayMarkerIndex).trim();
let openPos = arrayMarkerIndex + arraySizeMarker.length;
let count = 0;
const openParenPos = fieldStr.indexOf(fieldsOpen, openPos);
if (openParenPos !== -1 && openParenPos > openPos) {
const countStr = fieldStr.slice(openPos, openParenPos);
count = parseInt(countStr, 10) || 0;
openPos = openParenPos;
}
const closePos = findMatchingCloseParen(fieldStr, openPos, fieldsOpen, fieldsClose);
if (closePos === -1) throw new Error(`Unmatched parentheses in field: ${fieldStr}`);
const nestedFieldsStr = fieldStr.slice(openPos + 1, closePos).trim();
const nestedFields = parseFields(nestedFieldsStr, config);
const isPrimitiveArray = nestedFields.length === 0;
return {
name,
isArray: true,
isObject: false,
isPrimitiveArray,
isOptional,
nested: {
rootName: name,
count,
fields: nestedFields
}
};
}
return {
name: fieldStr.trim(),
isArray: false,
isObject: false,
isOptional
};
}
/**
* Find matching closing parenthesis
*/
function findMatchingCloseParen(str, openPos, openChar, closeChar) {
let depth = 1;
let pos = openPos + 1;
while (pos < str.length && depth > 0) {
if (str[pos] === openChar) depth++;
else if (str[pos] === closeChar) depth--;
pos++;
}
return depth === 0 ? pos - 1 : -1;
}
/**
* Find matching closing brace for objects
*/
function findMatchingCloseBrace(str, openPos) {
let depth = 1;
let pos = openPos + 1;
while (pos < str.length && depth > 0) {
if (str[pos] === "{") depth++;
else if (str[pos] === "}") depth--;
pos++;
}
return depth === 0 ? pos - 1 : -1;
}
//#endregion
//#region src/decode/decoder.ts
/**
* Parse path format
* - Array: "depth:index" (e.g., "2:1") → { depth: 2, index: 1, isObject: false }
* - Object: "depth " (e.g., "2 ") → { depth: 2, isObject: true }
*/
function parsePath(path, separator) {
const trimmed = path.trim();
if (!trimmed.includes(separator)) return {
depth: parseInt(trimmed, 10),
isObject: true
};
const [depthStr, indexStr] = trimmed.split(separator);
return {
depth: parseInt(depthStr, 10),
index: parseInt(indexStr, 10),
isObject: false
};
}
/**
* Decode PLOON string to JavaScript object
*/
function decode(ploonString, config) {
const { schema, records } = scan(ploonString, config);
const parsedSchema = parseSchema(schema, config);
const result = reconstruct(records, parsedSchema, config);
return result;
}
/**
* Reconstruct object tree from records using BFS (level-by-level)
*/
function reconstruct(records, schema, config) {
const tree = buildTree(records, schema, config);
return { [schema.rootName]: tree };
}
function buildRoutingMap(childRecords, allFields, presenceMarkers, pathSeparator) {
const groups = [];
let currentBatch = [];
let lastIndex = -1;
let lastWasObject = false;
let fieldIdx = 0;
for (const item of childRecords) {
const { record } = item;
const parsed = parsePath(record.path, pathSeparator);
const currentIndex = parsed.index !== void 0 ? parsed.index : 0;
const isObject = parsed.isObject;
const fieldBoundary = currentBatch.length > 0 && currentIndex <= lastIndex || currentBatch.length > 0 && isObject !== lastWasObject || lastWasObject && isObject;
if (fieldBoundary) if (fieldIdx < allFields.length) {
const field = allFields[fieldIdx];
groups.push({
fieldName: field.name,
fieldIndex: fieldIdx,
isArray: field.isArray,
isObject: field.isObject,
records: currentBatch.map((it) => it.record),
recordIndices: currentBatch.map((it) => it.index)
});
fieldIdx++;
currentBatch = [];
if (fieldIdx >= allFields.length) break;
} else break;
currentBatch.push(item);
lastIndex = currentIndex;
lastWasObject = isObject;
}
if (currentBatch.length > 0 && fieldIdx < allFields.length) {
const field = allFields[fieldIdx];
groups.push({
fieldName: field.name,
fieldIndex: fieldIdx,
isArray: field.isArray,
isObject: field.isObject,
records: currentBatch.map((it) => it.record),
recordIndices: currentBatch.map((it) => it.index)
});
}
return groups;
}
/**
* Pre-compute parent-child relationships for all records
* Returns a map from record index to array of child record indices
*/
function buildParentChildMap(records, pathSeparator) {
const parentChildMap = /* @__PURE__ */ new Map();
for (let i = 0; i < records.length; i++) {
const record = records[i];
const parsed = parsePath(record.path, pathSeparator);
const myDepth = parsed.depth;
const children = [];
for (let j = i + 1; j < records.length; j++) {
const childRecord = records[j];
const childParsed = parsePath(childRecord.path, pathSeparator);
const childDepth = childParsed.depth;
if (childDepth === myDepth + 1) children.push(j);
else if (childDepth <= myDepth) break;
}
parentChildMap.set(i, children);
}
return parentChildMap;
}
/**
* Build tree structure from flat records using BFS (Breadth-First Search)
* Process level by level with sequential consumption of children
*/
function buildTree(records, schema, config) {
const { pathSeparator } = config;
if (records.length === 0) return [];
const parentChildMap = buildParentChildMap(records, pathSeparator);
const recordsByDepth = /* @__PURE__ */ new Map();
for (const record of records) {
const parsed = parsePath(record.path, pathSeparator);
if (!recordsByDepth.has(parsed.depth)) recordsByDepth.set(parsed.depth, []);
recordsByDepth.get(parsed.depth).push(record);
}
const rootItems = [];
const depth1Records = recordsByDepth.get(1) || [];
const currentLevelParents = [];
for (let i = 0; i < records.length; i++) {
const record = records[i];
const parsed = parsePath(record.path, pathSeparator);
if (parsed.depth === 1 && !parsed.isObject) {
const { obj } = createObjectFromRecord(record, schema.fields, config);
rootItems.push(obj);
currentLevelParents.push({
obj,
fields: schema.fields,
recordIndex: i
});
}
}
let currentDepth = 1;
let parentsAtCurrentDepth = currentLevelParents;
while (parentsAtCurrentDepth.length > 0) {
const childDepth = currentDepth + 1;
const nextLevelParents = [];
for (const parent of parentsAtCurrentDepth) {
const { obj: parentObj, fields: parentFields, recordIndex: parentIdx } = parent;
const presenceMarkers = parentObj.__presenceMarkers__ || /* @__PURE__ */ new Set();
const childIndices = parentChildMap.get(parentIdx) || [];
const myChildrenWithIndices = childIndices.map((idx) => ({
record: records[idx],
index: idx
})).filter((item) => {
const parsed = parsePath(item.record.path, pathSeparator);
return parsed.depth === childDepth;
});
if (myChildrenWithIndices.length === 0) continue;
const complexFields = parentFields.filter((f) => !f.isPrimitiveArray && (f.isArray || f.isObject)).filter((f) => !presenceMarkers.has(f.name));
const routingMap = buildRoutingMap(myChildrenWithIndices, complexFields, presenceMarkers, pathSeparator);
if (routingMap.length === 0) continue;
for (const group of routingMap) {
const field = complexFields[group.fieldIndex];
if (group.isArray) {
if (!(field.name in parentObj)) parentObj[field.name] = [];
const targetArray = parentObj[field.name];
for (let i = 0; i < group.records.length; i++) {
const rec = group.records[i];
const recIdx = group.recordIndices[i];
const fields = field.nested?.fields || [];
if (fields.length === 0) {
const value = rec.values.length > 0 ? parseValue(rec.values[0], config) : null;
targetArray.push(value);
} else {
const { obj: arrayItem } = createObjectFromRecord(rec, fields, config);
targetArray.push(arrayItem);
nextLevelParents.push({
obj: arrayItem,
fields,
recordIndex: recIdx
});
}
}
} else if (group.isObject) {
if (field.nested && group.records.length > 0) {
const fields = field.nested.fields || [];
const rec = group.records[0];
const recIdx = group.recordIndices[0];
const { obj: nestedObj } = createObjectFromRecord(rec, fields, config);
parentObj[field.name] = nestedObj;
nextLevelParents.push({
obj: nestedObj,
fields,
recordIndex: recIdx
});
}
}
}
}
parentsAtCurrentDepth = nextLevelParents;
currentDepth++;
}
for (const item of rootItems) cleanupOptionalFields(item, schema.fields, config);
return rootItems;
}
/**
* Create object from record based on schema fields
* Returns both the object and presence markers for optional empty fields
*/
function createObjectFromRecord(record, fields, config) {
const obj = {};
const presenceMarkers = /* @__PURE__ */ new Set();
const primitiveFields = fields.filter((f) => f.isPrimitiveArray || !f.isArray && !f.isObject);
const complexFields = fields.filter((f) => !f.isPrimitiveArray && (f.isArray || f.isObject));
const optionalComplexFields = complexFields.filter((f) => f.isOptional);
let valueIndex = 0;
for (const field of primitiveFields) {
if (valueIndex >= record.values.length) break;
const rawValue = record.values[valueIndex];
if (field.isOptional && rawValue === "") {
valueIndex++;
continue;
}
if (field.isPrimitiveArray) if (rawValue === ",") obj[field.name] = [];
else {
const elements = splitEscaped(rawValue, ",", config.escapeChar);
obj[field.name] = elements.map(parseValueFromUnescaped);
}
else obj[field.name] = parseValue(rawValue, config);
valueIndex++;
}
/**
* Optional Complex Field Marker Decoding:
*
* When creating an object, we read || markers to determine which optional
* fields are absent/empty vs present.
*
* Decoding rules:
* 1. Read markers sequentially for optional complex fields
* 2. || marker means: field is absent or empty (create empty array/object)
* 3. No marker but more data exists: field has data (will be populated from child records)
* 4. No more markers/values: all remaining optional fields are absent (don't create them)
*
* Example: fields [_collections?, collections?, is_yalla?, variants?]
* Marker: ||
* Child records: 38 records, then 2 records
* → _collections: empty (from marker)
* → collections: populate from 38 child records
* → is_yalla: populate from 2 child records
* → variants: absent (no marker, no data after cutoff)
*/
let markersRead = 0;
let hitDataField = false;
for (let i = 0; i < optionalComplexFields.length; i++) {
const field = optionalComplexFields[i];
if (hitDataField) {
obj[field.name] = field.isArray ? [] : {};
continue;
}
if (valueIndex < record.values.length) {
const markerValue = record.values[valueIndex];
if (markerValue === "") {
presenceMarkers.add(field.name);
obj[field.name] = field.isArray ? [] : {};
valueIndex++;
markersRead++;
continue;
} else {
hitDataField = true;
obj[field.name] = field.isArray ? [] : {};
continue;
}
} else if (markersRead > 0) {
hitDataField = true;
obj[field.name] = field.isArray ? [] : {};
} else break;
}
for (const field of complexFields) if (!field.isOptional && !(field.name in obj)) obj[field.name] = field.isArray ? [] : {};
if (presenceMarkers.size > 0) obj.__presenceMarkers__ = presenceMarkers;
return {
obj,
presenceMarkers
};
}
/**
* Recursively clean up empty optional fields that have no presence marker
* This runs AFTER all child records have been processed
*/
function cleanupOptionalFields(obj, fields, config) {
const presenceMarkers = obj.__presenceMarkers__;
for (const field of fields) {
const value = obj[field.name];
if (field.isArray && !field.isPrimitiveArray && Array.isArray(value)) {
if (field.nested?.fields) {
for (const item of value) if (item && typeof item === "object" && !Array.isArray(item)) cleanupOptionalFields(item, field.nested.fields, config);
}
if (field.isOptional && value.length === 0 && !presenceMarkers?.has(field.name)) delete obj[field.name];
} else if (field.isObject && value && typeof value === "object" && !Array.isArray(value)) {
if (field.nested?.fields) cleanupOptionalFields(value, field.nested.fields, config);
if (field.isOptional && Object.keys(value).filter((k) => k !== "__presenceMarkers__").length === 0 && !presenceMarkers?.has(field.name)) delete obj[field.name];
}
}
delete obj.__presenceMarkers__;
}
/**
* Parse a value string to its JSON type
*/
function parseValue(value, config) {
const unescaped = unescapeValue(value, config);
return parseValueFromUnescaped(unescaped);
}
/**
* Parse an already-unescaped value to its JSON type
* Used for primitive array elements that were unescaped by splitEscaped
*/
function parseValueFromUnescaped(value) {
if (/^-?\d+(\.\d+)?$/.test(value)) return parseFloat(value);
if (value === "null") return null;
if (value === "true") return true;
if (value === "false") return false;
return value;
}
//#endregion
//#region src/core/parse.ts
/**
* Parse PLOON string to JavaScript object
*
* @param ploonString - PLOON formatted string
* @param options - Parse options (strict, config)
* @returns JavaScript object
*
* @example
* ```typescript
* const ploon = `[products#1](id,name)
*
* 1|1|Shirt`
* const data = parse(ploon)
* // { products: [{ id: 1, name: "Shirt" }] }
* ```
*/
function parse(ploonString, options) {
const resolved = resolveParseOptions(options);
const result = decode(ploonString, resolved.config);
return result;
}
//#endregion
//#region src/core/minify.ts
/**
* Minify PLOON string (Standard → Compact)
* Replaces newlines with semicolons and removes unnecessary whitespace
*/
function minify(ploonString) {
const lines = ploonString.split("\n");
const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
const minified = nonEmptyLines.join(";");
return minified.endsWith(";") ? minified.slice(0, -1) : minified;
}
//#endregion
//#region src/core/prettify.ts
/**
* PLOON Prettify
* Convert Compact format (semicolon-separated) to Standard format (newline-separated)
*/
/**
* Prettify PLOON string (Compact → Standard)
* Replaces semicolons with newlines for readability
*/
function prettify(ploonString) {
const parts = ploonString.split(";");
if (parts.length < 2) return ploonString;
const [schema, ...records] = parts;
return `${schema}\n\n${records.join("\n")}`;
}
//#endregion
//#region src/core/validate.ts
/**
* Check if a string is valid PLOON
*/
function isValid(ploonString, config = DEFAULT_CONFIG) {
try {
const result = validate(ploonString, config);
return result.valid;
} catch {
return false;
}
}
/**
* Validate PLOON string and return detailed result
*/
function validate(ploonString, config = DEFAULT_CONFIG) {
const errors = [];
const warnings = [];
try {
const { schema, records } = scan(ploonString, config);
if (!schema) errors.push("No schema found");
if (records.length === 0) warnings.push("No records found");
try {
parseSchema(schema, config);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
errors.push(`Schema parsing failed: ${message}`);
}
const paths = records.map((r) => r.path);
const uniquePaths = new Set(paths);
if (paths.length !== uniquePaths.size) warnings.push("Duplicate paths found");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
errors.push(message);
}
return {
valid: errors.length === 0,
errors: errors.length > 0 ? errors : void 0,
warnings: warnings.length > 0 ? warnings : void 0
};
}
//#endregion
//#region src/parsers/json.ts
/**
* Parse JSON string to JavaScript object
* Uses native JSON.parse() - zero dependencies
*/
function fromJSON(input) {
try {
return JSON.parse(input);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Invalid JSON: ${message}`);
}
}
/**
* Check if a string is valid JSON
*/
function isJSON(input) {
try {
JSON.parse(input);
return true;
} catch {
return false;
}
}
/**
* Convert JavaScript object to JSON string
* Convenience wrapper around JSON.stringify
*/
function toJSON(value, pretty = false) {
return JSON.stringify(value, null, pretty ? 2 : 0);
}
//#endregion
//#region src/parsers/xml.ts
/**
* Parse XML string to JavaScript object
*/
function fromXML(input) {
const validation = fast_xml_parser.XMLValidator.validate(input);
if (validation !== true) {
const error = validation.err;
throw new Error(`Invalid XML at line ${error.line}: ${error.msg}`);
}
const parser = new fast_xml_parser.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: "@_",
textNodeName: "#text",
parseAttributeValue: true,
parseTagValue: true,
trimValues: true
});
try {
return parser.parse(input);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`XML parsing failed: ${message}`);
}
}
/**
* Check if a string is valid XML
*/
function isXML(input) {
const validation = fast_xml_parser.XMLValidator.validate(input);
return validation === true;
}
/**
* Convert JavaScript object to XML string
*/
function toXML(value, pretty = false) {
const builder = new fast_xml_parser.XMLBuilder({
ignoreAttributes: false,
attributeNamePrefix: "@_",
textNodeName: "#text",
format: pretty,
indentBy: pretty ? " " : "",
suppressEmptyNode: true
});
try {
return builder.build(value);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`XML building failed: ${message}`);
}
}
//#endregion
//#region src/parsers/yaml.ts
/**
* Parse YAML string to JavaScript object
*/
function fromYAML(input) {
try {
return yaml.default.parse(input);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (error && typeof error === "object" && "linePos" in error) {
const linePos = error.linePos;
throw new Error(`Invalid YAML at line ${linePos.line}, col ${linePos.col}: ${message}`);
}
throw new Error(`Invalid YAML: ${message}`);
}
}
/**
* Check if a string is valid YAML
*/
function isYAML(input) {
try {
yaml.default.parse(input);
return true;
} catch {
return false;
}
}
/**
* Convert JavaScript object to YAML string
*/
function toYAML(value, pretty = false) {
try {
return yaml.default.stringify(value, {
indent: pretty ? 2 : 0,
lineWidth: 0,
minContentWidth: 0
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`YAML serialization failed: ${message}`);
}
}
//#endregion
exports.PLOON_COMPACT = PLOON_COMPACT;
exports.PLOON_STANDARD = PLOON_STANDARD;
exports.detectConfig = detectConfig;
exports.detectFormat = detectFormat;
exports.fromJSON = fromJSON;
exports.fromXML = fromXML;
exports.fromYAML = fromYAML;
exports.isJSON = isJSON;
exports.isValid = isValid;
exports.isXML = isXML;
exports.isYAML = isYAML;
exports.minify = minify;
exports.parse = parse;
exports.prettify = prettify;
exports.resolveConfig = resolveConfig;
exports.stringify = stringify;
exports.toJSON = toJSON;
exports.toXML = toXML;
exports.toYAML = toYAML;
exports.validate = validate;
exports.validateConfig = validateConfig;
//# sourceMappingURL=index.cjs.map

Sorry, the diff of this file is too big to display

/**
* PLOON - Path-Oriented Notation
* The most token-efficient format for nested hierarchical data
*/
export { stringify } from './core/stringify';
export { parse } from './core/parse';
export { minify } from './core/minify';
export { prettify } from './core/prettify';
export { isValid, validate } from './core/validate';
export { fromJSON, isJSON, toJSON } from './parsers/json';
export { fromXML, isXML, toXML } from './parsers/xml';
export { fromYAML, isYAML, toYAML } from './parsers/yaml';
export { PLOON_STANDARD, PLOON_COMPACT } from './constants';
export { resolveConfig, validateConfig, detectFormat, detectConfig } from './config';
export type { JsonValue, JsonObject, JsonArray, JsonPrimitive, PloonConfig, StringifyOptions, ParseOptions, ValidationResult } from './types';
/**
* JSON Parser - Native JSON.parse() with error handling
*/
import type { JsonValue } from '../types';
/**
* Parse JSON string to JavaScript object
* Uses native JSON.parse() - zero dependencies
*/
export declare function fromJSON(input: string): JsonValue;
/**
* Check if a string is valid JSON
*/
export declare function isJSON(input: string): boolean;
/**
* Convert JavaScript object to JSON string
* Convenience wrapper around JSON.stringify
*/
export declare function toJSON(value: JsonValue, pretty?: boolean): string;
/**
* XML Parser - Uses fast-xml-parser
*/
import type { JsonValue } from '../types';
/**
* Parse XML string to JavaScript object
*/
export declare function fromXML(input: string): JsonValue;
/**
* Check if a string is valid XML
*/
export declare function isXML(input: string): boolean;
/**
* Convert JavaScript object to XML string
*/
export declare function toXML(value: JsonValue, pretty?: boolean): string;
/**
* YAML Parser - Uses yaml library (eemeli)
*/
import type { JsonValue } from '../types';
/**
* Parse YAML string to JavaScript object
*/
export declare function fromYAML(input: string): JsonValue;
/**
* Check if a string is valid YAML
*/
export declare function isYAML(input: string): boolean;
/**
* Convert JavaScript object to YAML string
*/
export declare function toYAML(value: JsonValue, pretty?: boolean): string;
/**
* String Utilities
* Escaping, quoting, and string manipulation
*/
import type { PloonConfig } from '../types';
/**
* Escape special characters in a value
*/
export declare function escapeValue(value: string, config: PloonConfig): string;
/**
* Unescape special characters in a value
*/
export declare function unescapeValue(value: string, config: PloonConfig): string;
/**
* Format a value for output (converts to string and escapes)
*/
export declare function formatValue(value: unknown, config: PloonConfig, isOptional?: boolean): string;
/**
* Format a primitive array for inline encoding (comma-separated)
* Handles null values and respects preserveEmptyFields config
*/
export declare function formatPrimitiveArray(array: unknown[], config: PloonConfig, preserveEmpty: boolean): string;
/**
* Split a string by delimiter, respecting escapes
* Only unescapes the delimiter and escape char itself, preserves other escape sequences
*/
export declare function splitEscaped(str: string, delimiter: string, escapeChar: string): string[];
/**
* PLOON Type Definitions
*/
export type JsonPrimitive = string | number | boolean | null;
export type JsonArray = JsonValue[];
export type JsonObject = {
[key: string]: JsonValue;
};
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
/**
* PLOON Configuration
* Defines all configurable characters and delimiters
*/
export interface PloonConfig {
/** Field delimiter (separates values in a record) */
fieldDelimiter: string;
/** Path separator (separates path segments) */
pathSeparator: string;
/** Array size marker (indicates array length in schema) */
arraySizeMarker: string;
/** Record separator (separates records) */
recordSeparator: string;
/** Escape character (escapes special characters) */
escapeChar: string;
/** Schema opening bracket */
schemaOpen: string;
/** Schema closing bracket */
schemaClose: string;
/** Fields opening parenthesis */
fieldsOpen: string;
/** Fields closing parenthesis */
fieldsClose: string;
/** Nested separator (separates nested schemas) */
nestedSeparator: string;
/** Schema field separator (separates field names in schema) */
schemaFieldSeparator: string;
/** Schema whitespace (optional whitespace character in schema) */
schemaWhitespace: string;
/** Optional field marker (suffix for fields not present in all array elements) */
optionalFieldMarker: string;
/** Preserve empty arrays/objects with presence markers (fieldname() or fieldname{}) */
preserveEmptyFields: boolean;
}
/**
* Options for stringify function
*/
export interface StringifyOptions {
/** Output format */
format?: 'standard' | 'compact';
/** Custom configuration (overrides defaults) */
config?: Partial<PloonConfig>;
}
/**
* Options for parse function
*/
export interface ParseOptions {
/** Strict mode - validate schema consistency */
strict?: boolean;
/** Custom configuration (overrides defaults) */
config?: Partial<PloonConfig>;
}
/**
* Validation result
*/
export interface ValidationResult {
/** Whether the input is valid */
valid: boolean;
/** Error messages (if invalid) */
errors?: string[];
/** Warning messages */
warnings?: string[];
}
/**
* Internal types for encoding
*/
export type FieldType = 'primitive' | 'primitiveArray' | 'array' | 'object';
export interface SchemaField {
name: string;
type: FieldType;
isOptional?: boolean;
nested?: JsonArray | JsonObject;
fields?: SchemaField[];
}
export interface SchemaNode {
count?: number;
fields: SchemaField[];
}
export interface PathNode {
path: string;
values: JsonValue[];
children?: PathNode[];
}
+5
-3
{
"name": "ploon",
"version": "1.0.6",
"version": "1.0.7",
"description": "Path-Level Object Oriented Notation - The Most Token-Efficient Format for Nested Hierarchical Data",
"type": "module",
"sideEffects": false,
"main": "./dist/index.js",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",

@@ -12,3 +13,4 @@ "exports": {

"types": "./dist/index.d.ts",
"default": "./dist/index.js"
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}

@@ -15,0 +17,0 @@ },