Skip to content

Instantly share code, notes, and snippets.

@nojvek
Last active November 17, 2023 18:22
Show Gist options
  • Save nojvek/875d8cad1005da0b95da2840bbc894e6 to your computer and use it in GitHub Desktop.
Save nojvek/875d8cad1005da0b95da2840bbc894e6 to your computer and use it in GitHub Desktop.
Json pretty printer that respects maximum line length
// @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));
@nojvek
Copy link
Author

nojvek commented Mar 29, 2020

This is the output:

{                                                                 |
  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;"    |
      }                                                           |
    }                                                             |
  }                                                               |
}                                                                 |
{                                                                 |
  ch: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  cm: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  deg: {groups: ["CSS Units", "CSS Angles"], status: "standard"}, |
  dpcm: {                                                         |
    groups: ["CSS Units", "CSS Resolutions"],                     |
    status: "standard"                                            |
  },                                                              |
  dpi: {                                                          |
    groups: ["CSS Units", "CSS Resolutions"],                     |
    status: "standard"                                            |
  },                                                              |
  dppx: {                                                         |
    groups: ["CSS Units", "CSS Resolutions"],                     |
    status: "standard"                                            |
  },                                                              |
  em: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  ex: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  fr: {                                                           |
    groups: [                                                     |
      "CSS Units",                                                |
      "CSS Flexible Lengths",                                     |
      "CSS Grid Layout"                                           |
    ],                                                            |
    status: "standard"                                            |
  },                                                              |
  grad: {groups: ["CSS Units", "CSS Angles"], status: "standard"},|
  Hz: {                                                           |
    groups: ["CSS Units", "CSS Frequencies"],                     |
    status: "standard"                                            |
  },                                                              |
  in: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  kHz: {                                                          |
    groups: ["CSS Units", "CSS Frequencies"],                     |
    status: "standard"                                            |
  },                                                              |
  mm: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  ms: {groups: ["CSS Units", "CSS Times"], status: "standard"},   |
  pc: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  pt: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  px: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  Q: {groups: ["CSS Units", "CSS Lengths"], status: "standard"},  |
  rad: {groups: ["CSS Units", "CSS Angles"], status: "standard"}, |
  rem: {groups: ["CSS Units", "CSS Lengths"], status: "standard"},|
  s: {groups: ["CSS Units", "CSS Times"], status: "standard"},    |
  turn: {groups: ["CSS Units", "CSS Angles"], status: "standard"},|
  vh: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  vmax: {                                                         |
    groups: ["CSS Units", "CSS Lengths"],                         |
    status: "standard"                                            |
  },                                                              |
  vmin: {                                                         |
    groups: ["CSS Units", "CSS Lengths"],                         |
    status: "standard"                                            |
  },                                                              |
  vw: {groups: ["CSS Units", "CSS Lengths"], status: "standard"}, |
  x: {                                                            |
    groups: ["CSS Units", "CSS Resolutions"],                     |
    status: "standard"                                            |
  }                                                               |
}                                                                 |

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment