Last active
November 17, 2023 18:22
-
-
Save nojvek/875d8cad1005da0b95da2840bbc894e6 to your computer and use it in GitHub Desktop.
Json pretty printer that respects maximum line length
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @ts-ignore | |
import units from 'mdn-data/css/units.json'; | |
const widgetSample = { | |
widget: { | |
debug: `on`, | |
window: { | |
title: `Sample Konfabulator Widget`, | |
name: `main_window`, | |
dimensions: { | |
width: 500, | |
height: 500, | |
}, | |
}, | |
image: [`Images/Sun.png`, `sun1`, 250, 250, `center`, `bott`], | |
text: { | |
data: `Click Here`, | |
size: 36, | |
style: { | |
fontWeight: `bold`, | |
name: `text1`, | |
hOffset: 250, | |
vOffset: 100, | |
alignment: `center`, | |
onMouseUp: `sun1.opacity = (sun1.opacity / 100) * 90;`, | |
}, | |
}, | |
}, | |
}; | |
const enum ChunkType { | |
Text, | |
Group, | |
IndentBreakPoint, | |
DedentBreakPoint, | |
SeparatorBreakPoint, | |
} | |
interface TextChunk { | |
kind: ChunkType.Text; | |
text: string; | |
} | |
/** A group chunk must contain a dedent if an indent is present */ | |
interface GroupChunk { | |
kind: ChunkType.Group; | |
chunks: Chunk[]; | |
/** if the group chunk was printed on a single line, how long would it be? value is memoized */ | |
_singleLineLength?: number; | |
} | |
interface BreakPointChunk { | |
kind: ChunkType.SeparatorBreakPoint | ChunkType.IndentBreakPoint | ChunkType.DedentBreakPoint; | |
} | |
type Chunk = TextChunk | GroupChunk | BreakPointChunk; | |
function breakPointChunk(kind: BreakPointChunk['kind']): BreakPointChunk { | |
return {kind}; | |
} | |
function textChunk(text: string): TextChunk { | |
return {kind: ChunkType.Text, text}; | |
} | |
function valueToChunk(value: any): Chunk { | |
if (value === null || typeof value === `number` || typeof value === `boolean` || typeof value === `string`) { | |
return {kind: ChunkType.Text, text: JSON.stringify(value)}; | |
} else if (typeof value === `object`) { | |
const childChunks: Chunk[] = []; | |
const groupChunk: GroupChunk = {kind: ChunkType.Group, chunks: childChunks}; | |
if (value.constructor === Object) { | |
childChunks.push(textChunk(`{`)); | |
childChunks.push(breakPointChunk(ChunkType.IndentBreakPoint)); | |
Object.keys(value).forEach((key, idx, keys) => { | |
childChunks.push(textChunk(`${key}: `)); | |
childChunks.push(valueToChunk(value[key])); | |
if (idx < keys.length - 1) { | |
childChunks.push(textChunk(`,`)); | |
childChunks.push(breakPointChunk(ChunkType.SeparatorBreakPoint)); | |
} | |
}); | |
childChunks.push(breakPointChunk(ChunkType.DedentBreakPoint)); | |
childChunks.push(textChunk(`}`)); | |
} else if (value.constructor === Array) { | |
childChunks.push(textChunk(`[`)); | |
childChunks.push(breakPointChunk(ChunkType.IndentBreakPoint)); | |
(value as any[]).forEach((item, idx) => { | |
childChunks.push(valueToChunk(item)); | |
if (idx < value.length - 1) { | |
childChunks.push(textChunk(`,`)); | |
childChunks.push(breakPointChunk(ChunkType.SeparatorBreakPoint)); | |
} | |
}); | |
childChunks.push(breakPointChunk(ChunkType.DedentBreakPoint)); | |
childChunks.push(textChunk(`]`)); | |
} else { | |
throw new Error(`Invalid object with constructor ${value.constructor}`); | |
} | |
return groupChunk; | |
} else { | |
throw new Error(`Invalid type ${typeof value}`); | |
} | |
} | |
const enum FormatType { | |
SingleLine, | |
MultiLine, | |
} | |
function emitStr(str: string, strParts: string[]): number { | |
strParts.push(str); | |
return str.length; | |
} | |
function emitNewLine(indent: number, strParts: string[]): number { | |
const str = `\n` + ` `.repeat(indent); | |
return emitStr(str, strParts); | |
} | |
function calcGroupChunkSingleLineLength(chunk: GroupChunk) { | |
// return memoized length if available | |
if (chunk._singleLineLength !== undefined) { | |
return chunk._singleLineLength; | |
} | |
let lineLength = 0; | |
for (const childChunk of chunk.chunks) { | |
if (childChunk.kind === ChunkType.Text) { | |
lineLength += childChunk.text.length; | |
} else if (childChunk.kind === ChunkType.SeparatorBreakPoint) { | |
lineLength += 1; // single line format adds a ' ' for separators | |
} else if (childChunk.kind === ChunkType.Group) { | |
lineLength += calcGroupChunkSingleLineLength(childChunk); | |
} | |
// NOTE: indent and dedent breakpoints don't do anything in a single line format | |
} | |
return (chunk._singleLineLength = lineLength); | |
} | |
function formatChunk( | |
chunk: Chunk, | |
maxLineLength: number, | |
strParts: string[], | |
indent = 0, | |
usedLineLength = 0, | |
): string[] { | |
if (chunk.kind === ChunkType.Group) { | |
// try single line format first, if it doesn't work, do a multiline | |
let formatType = FormatType.SingleLine; | |
const availableLineLength = maxLineLength - usedLineLength; | |
if (calcGroupChunkSingleLineLength(chunk) > availableLineLength) { | |
formatType = FormatType.MultiLine; | |
} | |
for (const childChunk of chunk.chunks) { | |
if (childChunk.kind === ChunkType.Text) { | |
usedLineLength += emitStr(childChunk.text, strParts); | |
} else if (childChunk.kind === ChunkType.Group) { | |
formatChunk(childChunk, maxLineLength, strParts, indent, usedLineLength); | |
} else if (childChunk.kind === ChunkType.IndentBreakPoint) { | |
if (formatType === FormatType.MultiLine) { | |
indent += 1; | |
usedLineLength = emitNewLine(indent, strParts); | |
} | |
} else if (childChunk.kind === ChunkType.DedentBreakPoint) { | |
if (formatType === FormatType.MultiLine) { | |
indent -= 1; | |
usedLineLength = emitNewLine(indent, strParts); | |
} | |
} else if (childChunk.kind === ChunkType.SeparatorBreakPoint) { | |
if (formatType === FormatType.MultiLine) { | |
usedLineLength = emitNewLine(indent, strParts); | |
} else if (formatType === FormatType.SingleLine) { | |
usedLineLength += emitStr(` `, strParts); | |
} | |
} | |
} | |
} else if (chunk.kind === ChunkType.Text) { | |
emitStr(chunk.text, strParts); | |
} | |
return strParts; | |
} | |
function prettyJson(value: any, maxLineLength = 66) { | |
const chunk = valueToChunk(value); | |
const strParts: string[] = formatChunk(chunk, maxLineLength, []); | |
const lines = strParts.join(``).split(`\n`); | |
// for debugging | |
lines.forEach((line, idx) => { | |
const availableLineLength = maxLineLength - line.length; | |
const padding = availableLineLength > 0 ? ` `.repeat(availableLineLength) : ``; | |
lines[idx] = line + padding + `|`; | |
}); | |
return lines.join(`\n`); | |
} | |
console.log(prettyJson(widgetSample)); | |
console.log(prettyJson(units)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is the output: