deno.land / std@0.224.0 / toml / stringify.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.// This module is browser compatible.
// Bare keys may only contain ASCII letters,// ASCII digits, underscores, and dashes (A-Za-z0-9_-).function joinKeys(keys: string[]): string { // Dotted keys are a sequence of bare or quoted keys joined with a dot. // This allows for grouping similar properties together: return keys .map((str: string): string => { return str.length === 0 || str.match(/[^A-Za-z0-9_-]/) ? JSON.stringify(str) : str; }) .join(".");}
type ArrayType = | "ONLY_PRIMITIVE" | "ONLY_OBJECT_EXCLUDING_ARRAY" | "MIXED";
/** * Formatting Options for {@linkcode stringify} */export interface FormatOptions { /** Define if the keys should be aligned or not */ keyAlignment?: boolean;}
class Dumper { maxPad = 0; srcObject: Record<string, unknown>; output: string[] = []; #arrayTypeCache = new Map<unknown[], ArrayType>(); constructor(srcObjc: Record<string, unknown>) { this.srcObject = srcObjc; } dump(fmtOptions: FormatOptions = {}): string[] { // deno-lint-ignore no-explicit-any this.output = this.#printObject(this.srcObject as any); this.output = this.#format(fmtOptions); return this.output; } #printObject(obj: Record<string, unknown>, keys: string[] = []): string[] { const out = []; const props = Object.keys(obj); const inlineProps = []; const multilineProps = []; for (const prop of props) { if (this.#isSimplySerializable(obj[prop])) { inlineProps.push(prop); } else { multilineProps.push(prop); } } const sortedProps = inlineProps.concat(multilineProps); for (const prop of sortedProps) { const value = obj[prop]; if (value instanceof Date) { out.push(this.#dateDeclaration([prop], value)); } else if (typeof value === "string" || value instanceof RegExp) { out.push(this.#strDeclaration([prop], value.toString())); } else if (typeof value === "number") { out.push(this.#numberDeclaration([prop], value)); } else if (typeof value === "boolean") { out.push(this.#boolDeclaration([prop], value)); } else if ( value instanceof Array ) { const arrayType = this.#getTypeOfArray(value); if (arrayType === "ONLY_PRIMITIVE") { out.push(this.#arrayDeclaration([prop], value)); } else if (arrayType === "ONLY_OBJECT_EXCLUDING_ARRAY") { // array of objects for (let i = 0; i < value.length; i++) { out.push(""); out.push(this.#headerGroup([...keys, prop])); out.push(...this.#printObject(value[i], [...keys, prop])); } } else { // this is a complex array, use the inline format. const str = value.map((x) => this.#printAsInlineValue(x)).join(","); out.push(`${this.#declaration([prop])}[${str}]`); } } else if (typeof value === "object") { out.push(""); out.push(this.#header([...keys, prop])); if (value) { const toParse = value as Record<string, unknown>; out.push(...this.#printObject(toParse, [...keys, prop])); } // out.push(...this._parse(value, `${path}${prop}.`)); } } out.push(""); return out; } #isPrimitive(value: unknown): boolean { return value instanceof Date || value instanceof RegExp || ["string", "number", "boolean"].includes(typeof value); } #getTypeOfArray(arr: unknown[]): ArrayType { if (this.#arrayTypeCache.has(arr)) { return this.#arrayTypeCache.get(arr)!; } const type = this.#doGetTypeOfArray(arr); this.#arrayTypeCache.set(arr, type); return type; } #doGetTypeOfArray(arr: unknown[]): ArrayType { if (!arr.length) { // any type should be fine return "ONLY_PRIMITIVE"; }
const onlyPrimitive = this.#isPrimitive(arr[0]); if (arr[0] instanceof Array) { return "MIXED"; } for (let i = 1; i < arr.length; i++) { if ( onlyPrimitive !== this.#isPrimitive(arr[i]) || arr[i] instanceof Array ) { return "MIXED"; } } return onlyPrimitive ? "ONLY_PRIMITIVE" : "ONLY_OBJECT_EXCLUDING_ARRAY"; } #printAsInlineValue(value: unknown): string | number { if (value instanceof Date) { return `"${this.#printDate(value)}"`; } else if (typeof value === "string" || value instanceof RegExp) { return JSON.stringify(value.toString()); } else if (typeof value === "number") { return value; } else if (typeof value === "boolean") { return value.toString(); } else if ( value instanceof Array ) { const str = value.map((x) => this.#printAsInlineValue(x)).join(","); return `[${str}]`; } else if (typeof value === "object") { if (!value) { throw new Error("should never reach"); } const str = Object.keys(value).map((key) => { return `${joinKeys([key])} = ${ // deno-lint-ignore no-explicit-any this.#printAsInlineValue((value as any)[key])}`; }).join(","); return `{${str}}`; }
throw new Error("should never reach"); } #isSimplySerializable(value: unknown): boolean { return ( typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof RegExp || value instanceof Date || (value instanceof Array && this.#getTypeOfArray(value) !== "ONLY_OBJECT_EXCLUDING_ARRAY") ); } #header(keys: string[]): string { return `[${joinKeys(keys)}]`; } #headerGroup(keys: string[]): string { return `[[${joinKeys(keys)}]]`; } #declaration(keys: string[]): string { const title = joinKeys(keys); if (title.length > this.maxPad) { this.maxPad = title.length; } return `${title} = `; } #arrayDeclaration(keys: string[], value: unknown[]): string { return `${this.#declaration(keys)}${JSON.stringify(value)}`; } #strDeclaration(keys: string[], value: string): string { return `${this.#declaration(keys)}${JSON.stringify(value)}`; } #numberDeclaration(keys: string[], value: number): string { switch (value) { case Infinity: return `${this.#declaration(keys)}inf`; case -Infinity: return `${this.#declaration(keys)}-inf`; default: return `${this.#declaration(keys)}${value}`; } } #boolDeclaration(keys: string[], value: boolean): string { return `${this.#declaration(keys)}${value}`; } #printDate(value: Date): string { function dtPad(v: string, lPad = 2): string { return v.padStart(lPad, "0"); } const m = dtPad((value.getUTCMonth() + 1).toString()); const d = dtPad(value.getUTCDate().toString()); const h = dtPad(value.getUTCHours().toString()); const min = dtPad(value.getUTCMinutes().toString()); const s = dtPad(value.getUTCSeconds().toString()); const ms = dtPad(value.getUTCMilliseconds().toString(), 3); // formatted date const fData = `${value.getUTCFullYear()}-${m}-${d}T${h}:${min}:${s}.${ms}`; return fData; } #dateDeclaration(keys: string[], value: Date): string { return `${this.#declaration(keys)}${this.#printDate(value)}`; } #format(options: FormatOptions = {}): string[] { const { keyAlignment = false } = options; const rDeclaration = /^(\".*\"|[^=]*)\s=/; const out = []; for (let i = 0; i < this.output.length; i++) { const l = this.output[i] as string; // we keep empty entry for array of objects if (l[0] === "[" && l[1] !== "[") { // non-empty object with only subobjects as properties if ( this.output[i + 1] === "" && this.output[i + 2]?.slice(0, l.length) === l.slice(0, -1) + "." ) { i += 1; continue; } out.push(l); } else { if (keyAlignment) { const m = rDeclaration.exec(l); if (m && m[1]) { out.push(l.replace(m[1], m[1].padEnd(this.maxPad))); } else { out.push(l); } } else { out.push(l); } } } // Cleaning multiple spaces const cleanedOutput = []; for (let i = 0; i < out.length; i++) { const l = out[i] as string; if (!(l === "" && out[i + 1] === "")) { cleanedOutput.push(l); } } return cleanedOutput; }}
/** * Stringify dumps source object into TOML string and returns it. * @param srcObj * @param [fmtOptions] format options * @param [fmtOptions.keyAlignment] whether to align keys */export function stringify( srcObj: Record<string, unknown>, fmtOptions?: FormatOptions,): string { return new Dumper(srcObj).dump(fmtOptions).join("\n");}
Version Info