fix: prettify JSON request body with comments (#4399)

Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
Anwarul Islam
2024-10-03 19:26:01 +06:00
committed by GitHub
parent 8535004023
commit a283edec6e
3 changed files with 423 additions and 62 deletions

View File

@@ -104,8 +104,8 @@ import { readFileAsText } from "~/helpers/functional/files"
import xmlFormat from "xml-formatter" import xmlFormat from "xml-formatter"
import { useNestedSetting } from "~/composables/settings" import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings" import { toggleNestedSetting } from "~/newstore/settings"
import * as LJSON from "lossless-json"
import { useAIExperiments } from "~/composables/ai-experiments" import { useAIExperiments } from "~/composables/ai-experiments"
import { prettifyJSONC } from "~/helpers/editor/linting/jsoncPretty"
type PossibleContentTypes = Exclude< type PossibleContentTypes = Exclude<
ValidContentTypes, ValidContentTypes,
@@ -205,8 +205,7 @@ const prettifyRequestBody = () => {
let prettifyBody = "" let prettifyBody = ""
try { try {
if (body.value.contentType.endsWith("json")) { if (body.value.contentType.endsWith("json")) {
const jsonObj = LJSON.parse(rawParamsBody.value as string) prettifyBody = prettifyJSONC(rawParamsBody.value as string)
prettifyBody = LJSON.stringify(jsonObj, undefined, 2) as string
} else if (body.value.contentType === "application/xml") { } else if (body.value.contentType === "application/xml") {
prettifyBody = prettifyXML(rawParamsBody.value as string) prettifyBody = prettifyXML(rawParamsBody.value as string)
} }

View File

@@ -0,0 +1,258 @@
import jsonParse, {
JSONArrayValue,
JSONCommentValue,
JSONObjectValue,
JSONValue,
} from "~/helpers/jsoncParse"
type PrettifyOptions = {
indent?: string | number
maxLength?: number
commentSpace?: boolean
trailingComma?: boolean
}
const DEFAULT_OPTIONS: Required<PrettifyOptions> = {
indent: 2,
maxLength: 80,
commentSpace: true,
trailingComma: true,
}
function prettify(
ast: JSONObjectValue | JSONArrayValue,
options: PrettifyOptions = {}
): string {
const opts = { ...DEFAULT_OPTIONS, ...options }
const indent =
typeof opts.indent === "number" ? " ".repeat(opts.indent) : opts.indent
return formatValue(ast, opts, 0, indent)
}
function formatValue(
node: JSONValue,
options: Required<PrettifyOptions>,
depth: number,
indent: string
): string {
switch (node.kind) {
case "Object":
return formatObject(node, options, depth, indent)
case "Array":
return formatArray(node, options, depth, indent)
case "String":
return JSON.stringify(node.value)
case "Number":
case "Boolean":
return String(node.value)
case "Null":
return "null"
default:
return ""
}
}
function formatComments(
comments: JSONCommentValue[] | undefined,
options: Required<PrettifyOptions>,
indentation: string,
inline: boolean = false
): string {
if (!comments?.length) return ""
return comments
.map((comment) => {
if (comment.kind === "SingleLineComment") {
const space = options.commentSpace ? " " : ""
return inline
? ` //${space}${comment.value}`
: `\n${indentation}//${space}${comment.value}`
}
const space = options.commentSpace ? " " : ""
const commentLines = comment.value.split("\n")
if (commentLines.length === 1) {
return inline
? ` /*${space}${comment.value}${space}*/`
: `\n${indentation}/*${space}${comment.value}${space}*/`
}
return (
`\n${indentation}/*\n` +
commentLines.map((line) => `${indentation} * ${line}`).join("\n") +
`\n${indentation} */`
)
})
.join("")
}
function formatObject(
node: JSONObjectValue,
options: Required<PrettifyOptions>,
depth: number,
indent: string
): string {
if (node.members.length === 0) {
const comments = formatComments(node.comments, options, "", true)
return `{${comments}}`
}
const indentation = indent.repeat(depth)
const nextIndentation = indent.repeat(depth + 1)
let result = "{"
// Leading comments (before any members)
if (node.comments?.length) {
const leadingComments = node.comments.filter(
(c) => c.start < node.members[0].start
)
if (leadingComments.length) {
result += formatComments(leadingComments, options, nextIndentation)
}
}
// Format each member
node.members.forEach((member, index) => {
const isLast = index === node.members.length - 1
// Member's leading comments
if (member.comments?.length) {
const leadingComments = member.comments.filter(
(c) => c.start < member.key.start
)
if (leadingComments.length) {
result += formatComments(leadingComments, options, nextIndentation)
}
}
// Member key-value pair
result += "\n" + nextIndentation
result += JSON.stringify(member.key.value) + ": "
result += formatValue(member.value, options, depth + 1, indent)
// Inline comments after the value
if (member.comments?.length) {
const inlineComments = member.comments.filter((c) => c.start > member.end)
if (inlineComments.length) {
result += formatComments(inlineComments, options, "", true)
}
}
// Add comma if not last item or if trailing comma is enabled
if (!isLast || options.trailingComma) {
result += ","
}
// Comments between members
if (!isLast && node.comments?.length) {
const betweenComments = node.comments.filter(
(c) => c.start > member.end && c.end < node.members[index + 1].start
)
if (betweenComments.length) {
result += formatComments(betweenComments, options, nextIndentation)
}
}
})
// Trailing comments (after last member)
if (node.comments?.length) {
const trailingComments = node.comments.filter(
(c) =>
c.start > node.members[node.members.length - 1].end && c.end < node.end
)
if (trailingComments.length) {
result += formatComments(trailingComments, options, nextIndentation)
}
}
result += "\n" + indentation + "}"
return result
}
function formatArray(
node: JSONArrayValue,
options: Required<PrettifyOptions>,
depth: number,
indent: string
): string {
if (node.values.length === 0) {
const comments = formatComments(node.comments, options, "", true)
return `[${comments}]`
}
const indentation = indent.repeat(depth)
const nextIndentation = indent.repeat(depth + 1)
let result = "["
// Leading comments (before any values)
if (node.comments?.length) {
const leadingComments = node.comments.filter(
(c) => c.start < node.values[0].start
)
if (leadingComments.length) {
result += formatComments(leadingComments, options, nextIndentation)
}
}
// Format each value
node.values.forEach((value, index) => {
const isLast = index === node.values.length - 1
// Value's leading comments
if ("comments" in value && value.comments?.length) {
const leadingComments = value.comments.filter(
(c) => c.start < value.start
)
if (leadingComments.length) {
result += formatComments(leadingComments, options, nextIndentation)
}
}
result += "\n" + nextIndentation
result += formatValue(value, options, depth + 1, indent)
// Inline comments after the value
if ("comments" in value && value.comments?.length) {
const inlineComments = value.comments.filter((c) => c.start > value.end)
if (inlineComments.length) {
result += formatComments(inlineComments, options, "", true)
}
}
// Add comma if not last item or if trailing comma is enabled
if (!isLast || options.trailingComma) {
result += ","
}
// Comments between values
if (!isLast && node.comments?.length) {
const betweenComments = node.comments.filter(
(c) => c.start > value.end && c.end < node.values[index + 1].start
)
if (betweenComments.length) {
result += formatComments(betweenComments, options, nextIndentation)
}
}
})
// Trailing comments (after last value)
if (node.comments?.length) {
const trailingComments = node.comments.filter(
(c) =>
c.start > node.values[node.values.length - 1].end && c.end < node.end
)
if (trailingComments.length) {
result += formatComments(trailingComments, options, nextIndentation)
}
}
result += "\n" + indentation + "]"
return result
}
export function prettifyJSONC(str: string, options: PrettifyOptions = {}) {
const ast = jsonParse(str)
return prettify(ast, options)
}

View File

@@ -25,6 +25,16 @@ type JSONEOFValue = {
end: number end: number
} }
// First, add the new comment types
type JSONCommentKind = "SingleLineComment" | "MultiLineComment"
export type JSONCommentValue = {
kind: JSONCommentKind
start: number
end: number
value: string
}
type JSONNullValue = { type JSONNullValue = {
kind: "Null" kind: "Null"
start: number start: number
@@ -65,6 +75,7 @@ export type JSONObjectValue = {
end: number end: number
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
members: JSONObjectMember[] members: JSONObjectMember[]
comments?: JSONCommentValue[] // optional comments array
} }
export type JSONArrayValue = { export type JSONArrayValue = {
@@ -73,6 +84,7 @@ export type JSONArrayValue = {
end: number end: number
// eslint-disable-next-line no-use-before-define // eslint-disable-next-line no-use-before-define
values: JSONValue[] values: JSONValue[]
comments?: JSONCommentValue[] // optional comments array
} }
export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue
@@ -83,26 +95,7 @@ export type JSONObjectMember = {
end: number end: number
key: JSONStringValue key: JSONStringValue
value: JSONValue value: JSONValue
} comments?: JSONCommentValue[] // optional comments array
export default function jsonParse(
str: string
): JSONObjectValue | JSONArrayValue {
string = str
strLen = str.length
start = end = lastEnd = -1
ch()
lex() // Pass the allowComments flag to lex()
try {
const ast = parseObj()
expect("EOF")
return ast
} catch (e) {
// Try parsing expecting a root array
const ast = parseArr()
expect("EOF")
return ast
}
} }
let string: string let string: string
@@ -112,57 +105,144 @@ let end: number
let lastEnd: number let lastEnd: number
let code: number let code: number
let kind: string let kind: string
let pendingComments: JSONCommentValue[] = []
export default function jsonParse(
str: string
): JSONObjectValue | JSONArrayValue {
string = str
strLen = str.length
start = end = lastEnd = -1
pendingComments = [] // Reset pending comments
ch()
lex()
try {
const ast = parseObj()
expect("EOF")
return ast
} catch (e) {
pendingComments = [] // Reset pending comments
const ast = parseArr()
expect("EOF")
return ast
}
}
function parseObj(): JSONObjectValue { function parseObj(): JSONObjectValue {
const nodeStart = start const nodeStart = start
const members = [] const members: JSONObjectMember[] = []
const comments = [...pendingComments] // Capture comments before the object
pendingComments = []
expect("{") expect("{")
let first = true
while (!skip("}")) { while (!skip("}")) {
members.push(parseMember()) if (!first) {
if (!skip(",")) { // Expect a comma between members
expect("}") expect(",")
break
// After comma, check for closing brace (handling trailing comma)
if (skip("}")) {
break
}
} }
first = false
// Capture any comments before the member
const memberComments = [...pendingComments]
pendingComments = []
const member = parseMember()
if (memberComments.length > 0) {
member.comments = memberComments
}
members.push(member)
} }
// Capture any trailing comments inside the object
const trailingComments = [...pendingComments]
pendingComments = []
return { return {
kind: "Object", kind: "Object",
start: nodeStart, start: nodeStart,
end: lastEnd, end: lastEnd,
members, members,
} comments:
} comments.length > 0 || trailingComments.length > 0
? [...comments, ...trailingComments]
function parseMember(): JSONObjectMember { : undefined,
const nodeStart = start
const key = kind === "String" ? (curToken() as JSONStringValue) : null
expect("String")
expect(":")
const value = parseVal()
return {
kind: "Member",
start: nodeStart,
end: lastEnd,
key: key!,
value,
} }
} }
function parseArr(): JSONArrayValue { function parseArr(): JSONArrayValue {
const nodeStart = start const nodeStart = start
const values: JSONValue[] = [] const values: JSONValue[] = []
const comments = [...pendingComments] // Capture comments before the array
pendingComments = []
expect("[") expect("[")
let first = true
while (!skip("]")) { while (!skip("]")) {
values.push(parseVal()) if (!first) {
if (!skip(",")) { // Expect a comma between values
expect("]") expect(",")
break
// After comma, check for closing bracket (handling trailing comma)
if (skip("]")) {
break
}
} }
first = false
// Add value and attach any pending comments to it
const value = parseVal()
if (pendingComments.length > 0 && typeof value === "object") {
;(value as JSONObjectValue).comments = [
...((value as JSONObjectValue).comments || []),
...pendingComments,
]
pendingComments = []
}
values.push(value)
} }
// Capture any trailing comments inside the array
const trailingComments = [...pendingComments]
pendingComments = []
return { return {
kind: "Array", kind: "Array",
start: nodeStart, start: nodeStart,
end: lastEnd, end: lastEnd,
values, values,
comments:
comments.length > 0 || trailingComments.length > 0
? [...comments, ...trailingComments]
: undefined,
}
}
function parseMember(): JSONObjectMember {
const nodeStart = start
const memberComments = [...pendingComments] // Capture comments before the member
pendingComments = []
const key = kind === "String" ? (curToken() as JSONStringValue) : null
expect("String")
expect(":")
const value = parseVal()
return {
kind: "Member",
start: nodeStart,
end: lastEnd,
key: key!,
value,
comments: memberComments.length > 0 ? memberComments : undefined,
} }
} }
@@ -239,41 +319,65 @@ function ch() {
function lex() { function lex() {
lastEnd = end lastEnd = end
// Skip whitespace and comments
while (true) { while (true) {
// Skip whitespace (space, tab, newline, etc.) // Skip whitespace
while (code === 9 || code === 10 || code === 13 || code === 32) { while (code === 9 || code === 10 || code === 13 || code === 32) {
ch() ch()
} }
// Check for single-line comment (//) // Handle single-line comments
if (code === 47 && string.charCodeAt(end + 1) === 47) { if (code === 47 && string.charCodeAt(end + 1) === 47) {
// 47 is '/' const commentStart = end
ch() // Skip first '/'
ch() // Skip second '/'
let commentText = ""
while (code !== 10 && code !== 13 && code !== 0) { while (code !== 10 && code !== 13 && code !== 0) {
// Skip until newline or EOF commentText += String.fromCharCode(code)
ch() ch()
} }
continue // After skipping the comment, recheck for more whitespace/comments
pendingComments.push({
kind: "SingleLineComment",
start: commentStart,
end,
value: commentText.trim(),
})
continue
} }
// Check for multi-line comment (/* */) // Handle multi-line comments
if (code === 47 && string.charCodeAt(end + 1) === 42) { if (code === 47 && string.charCodeAt(end + 1) === 42) {
// 42 is '*' const commentStart = end
ch() // Skip the '*' ch() // Skip '/'
ch() // Move past the opening '/*' ch() // Skip '*'
let commentText = ""
while ( while (
code !== 0 && code !== 0 &&
!(code === 42 && string.charCodeAt(end + 1) === 47) !(code === 42 && string.charCodeAt(end + 1) === 47)
) { ) {
// Look for '*/' commentText += String.fromCharCode(code)
ch() ch()
} }
ch() // Skip the '*'
ch() // Move past the closing '*/' if (code === 0) {
continue // After skipping the comment, recheck for more whitespace/comments throw syntaxError("Unterminated multi-line comment")
}
ch() // Skip '*'
ch() // Skip '/'
pendingComments.push({
kind: "MultiLineComment",
start: commentStart,
end,
value: commentText.trim(),
})
continue
} }
break // Exit loop when no more comments or whitespace break
} }
if (code === 0) { if (code === 0) {