Skip to content

Instantly share code, notes, and snippets.

@elisherer
Created May 23, 2024 10:56
Show Gist options
  • Save elisherer/9263d2f12672139edd997e7a572269f4 to your computer and use it in GitHub Desktop.
Save elisherer/9263d2f12672139edd997e7a572269f4 to your computer and use it in GitHub Desktop.
JSON 2 YAML naive solution
import toYaml from "./toYaml";
describe("toYaml", () => {
test("string", () => {
expect(toYaml("Hello World")).toEqual("Hello World\n");
// keywords
expect(toYaml("null")).toEqual('"null"\n');
expect(toYaml("true")).toEqual('"true"\n');
expect(toYaml("false")).toEqual('"false"\n');
// escapes
expect(toYaml(`"hello"`)).toEqual(`'"hello"'\n`);
expect(toYaml(`'hello'`)).toEqual(`"'hello'"\n`);
expect(toYaml(`'"'`)).toEqual('"false"\n');
});
test("boolean", () => {
expect(toYaml(true)).toEqual("true\n");
expect(toYaml(false)).toEqual("false\n");
});
test("null", () => {
expect(toYaml(null)).toEqual("\n");
expect(toYaml(undefined)).toEqual("\n");
});
test("object", () => {
expect(toYaml({ a: 1, b: "2", c: true, d: ["hello", "world"] })).toEqual(
`
a: 1
b: "2"
c: true
d:
- hello
- world
`.substring(1),
);
});
test("object (nested)", () => {
expect(toYaml({ a: { b: { c: 1 } } })).toEqual(
`
a:
b:
c: 1
`.substring(1),
);
});
test("array", () => {
expect("\n" + toYaml([{ a: "hello" }, { a: "hello", b: "world" }, "hello world"])).toEqual(`
- a: hello
- a: hello
b: world
- hello world
`);
});
test("array (nested)", () => {
expect(toYaml(["hello", ["hello", "world"]])).toEqual(
`
- hello
- - hello
- world
`.substring(1),
);
});
test("array inside object", () => {
expect(toYaml({ x: [{ a: "hello" }, { a: "hello", b: "world" }, "hello world"] })).toEqual(
`
x:
- a: hello
- a: hello
b: world
- hello world
`.substring(1),
);
});
test("complex", () => {
expect(
toYaml({
employees: {
employee: [
{
id: "1",
firstName: "Tom",
lastName: "Cruise",
group: "A",
},
{
id: "2",
firstName: "Maria",
lastName: "Sharapova",
group: "B",
},
{
id: "007",
firstName: "James",
lastName: "Bond",
group: "A",
},
],
},
}),
).toEqual(
`
employees:
employee:
- id: "1"
firstName: Tom
lastName: Cruise
group: "A"
- id: "2"
firstName: "Maria"
lastName: "Sharapova"
group: B
- id: "007"
firstName: "James"
lastName: Bond
group: "A"
`.substring(1),
);
});
});
export interface CompareValue {
key: string;
value: any;
}
export type CompareFunction = (first: CompareValue, second: CompareValue) => number;
export type Options = {
preserve_undefined?: boolean;
show_nulls?: boolean;
cmp?: CompareFunction;
};
const MUST_QUOTE_LITERALS = new Set(["null", "true", "false", "~", ""]),
TAB_WIDTH = 2,
// eslint-disable-next-line no-control-regex
MUST_DOUBLE_QUOTE_REGEXP = /[\x00-\x06\b\t\n\v\f\r\x0e-\x1a\x1c-\x1f"]/i,
MUST_QUOTE_REGEXP = /^[\s.#:&*|>!?-]|: |\s$|(^-?\d+(\.\d+)?(e[-+]?\d+)?$)|(^\d{4}-\d\d-\d\d)/i;
const DEFAULT_OPTIONS: Options = {},
EMPTY_STRING = "",
TAB = " ".repeat(TAB_WIDTH),
DEFAULT_HANDLER = () => undefined,
NULL_HANDLER = () => EMPTY_STRING,
BOOLEAN_HANDLER = (x: any) => (x ? "true" : "false"),
NUMBER_HANDLER = (x: any) => {
// !!float
if (Number.isNaN(x)) {
return ".nan";
}
if (x === Infinity) {
return ".inf";
}
if (x === -Infinity) {
return "-.inf";
}
return x;
},
STRING_HANDLER = (x: any) => {
if ((x.length <= 5 && MUST_QUOTE_LITERALS.has(x.toLowerCase())) || MUST_QUOTE_REGEXP.test(x)) {
const doubleQuoted = JSON.stringify(x);
const containsDoubleQuotes = doubleQuoted.indexOf('"', 1) !== doubleQuoted.length - 1;
return containsDoubleQuotes ? doubleQuoted : `'${doubleQuoted.slice(1, -1).replace(/'/g, "\\'")}'`;
}
return x;
},
SUPPORTED_TYPES = ["boolean", "string", "number", "object", "undefined"];
function typeOf(obj: any) {
if (obj === null) {
return "null";
}
if (Array.isArray(obj)) {
return "array";
}
const type = typeof obj;
return SUPPORTED_TYPES.includes(type) ? type : "default";
}
function toYaml(data: any, options: Options = DEFAULT_OPTIONS) {
if (typeof data === "undefined") {
return "\n";
}
let indent = EMPTY_STRING;
const cmp =
options.cmp &&
(f => {
return (node: any) => {
return (a: string, b: string) => {
const aobj = { key: a, value: node[a] };
const bobj = { key: b, value: node[b] };
return f(aobj, bobj);
};
};
})(options.cmp);
const handlers: Record<string, (x: any, parentType?: string | -1, index?: number) => any> = {
default: DEFAULT_HANDLER, // for non supported yaml value types
null: NULL_HANDLER,
boolean: BOOLEAN_HANDLER,
undefined: () => (options.preserve_undefined ? EMPTY_STRING : undefined),
number: NUMBER_HANDLER,
string: STRING_HANDLER,
array: (x, parentType) => {
if (x.length === 0) {
return "[]";
}
if (parentType) {
indent += TAB;
}
let output = EMPTY_STRING;
for (let i = 0; i < x.length; i++) {
let handlerOutput = handlers[typeOf(x[i])](x[i], "array", i);
if (typeof handlerOutput === "undefined") {
handlerOutput = EMPTY_STRING; // no hiding objects inside arrays to preserve length and indexes of array
}
output +=
(parentType === "array" && i === 0 ? EMPTY_STRING : (!parentType && i === 0 ? EMPTY_STRING : "\n") + indent) +
"- " +
handlerOutput;
}
indent = indent.substring(0, indent.length - TAB_WIDTH); // remove last tab
return output;
},
object: function (x, parentType) {
let keys = Object.keys(x);
if (keys.length === 0) {
return "{}";
}
if (cmp) {
keys = keys.sort(cmp(x));
}
if (parentType) {
indent += TAB;
}
let output = EMPTY_STRING;
for (let i = 0; i < keys.length; i++) {
const val = x[keys[i]];
if ((val === null || typeof val === "undefined") && !options.show_nulls) {
continue;
}
const handlerOutput = handlers[typeOf(val)](val, "object");
output +=
(parentType === "array" && i === 0 ? EMPTY_STRING : (!parentType && i === 0 ? EMPTY_STRING : "\n") + indent) +
keys[i] +
": " +
handlerOutput;
}
indent = indent.substring(0, indent.length - TAB_WIDTH); // remove last tab
return output;
},
};
return handlers[typeOf(data)](data) + "\n";
}
export default toYaml;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment