fix: prettify JSON request body with comments (#4399)
Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
@@ -104,8 +104,8 @@ import { readFileAsText } from "~/helpers/functional/files"
|
||||
import xmlFormat from "xml-formatter"
|
||||
import { useNestedSetting } from "~/composables/settings"
|
||||
import { toggleNestedSetting } from "~/newstore/settings"
|
||||
import * as LJSON from "lossless-json"
|
||||
import { useAIExperiments } from "~/composables/ai-experiments"
|
||||
import { prettifyJSONC } from "~/helpers/editor/linting/jsoncPretty"
|
||||
|
||||
type PossibleContentTypes = Exclude<
|
||||
ValidContentTypes,
|
||||
@@ -205,8 +205,7 @@ const prettifyRequestBody = () => {
|
||||
let prettifyBody = ""
|
||||
try {
|
||||
if (body.value.contentType.endsWith("json")) {
|
||||
const jsonObj = LJSON.parse(rawParamsBody.value as string)
|
||||
prettifyBody = LJSON.stringify(jsonObj, undefined, 2) as string
|
||||
prettifyBody = prettifyJSONC(rawParamsBody.value as string)
|
||||
} else if (body.value.contentType === "application/xml") {
|
||||
prettifyBody = prettifyXML(rawParamsBody.value as string)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -25,6 +25,16 @@ type JSONEOFValue = {
|
||||
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 = {
|
||||
kind: "Null"
|
||||
start: number
|
||||
@@ -65,6 +75,7 @@ export type JSONObjectValue = {
|
||||
end: number
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
members: JSONObjectMember[]
|
||||
comments?: JSONCommentValue[] // optional comments array
|
||||
}
|
||||
|
||||
export type JSONArrayValue = {
|
||||
@@ -73,6 +84,7 @@ export type JSONArrayValue = {
|
||||
end: number
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
values: JSONValue[]
|
||||
comments?: JSONCommentValue[] // optional comments array
|
||||
}
|
||||
|
||||
export type JSONValue = JSONObjectValue | JSONArrayValue | JSONPrimitiveValue
|
||||
@@ -83,26 +95,7 @@ export type JSONObjectMember = {
|
||||
end: number
|
||||
key: JSONStringValue
|
||||
value: JSONValue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
comments?: JSONCommentValue[] // optional comments array
|
||||
}
|
||||
|
||||
let string: string
|
||||
@@ -112,57 +105,144 @@ let end: number
|
||||
let lastEnd: number
|
||||
let code: number
|
||||
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 {
|
||||
const nodeStart = start
|
||||
const members = []
|
||||
const members: JSONObjectMember[] = []
|
||||
const comments = [...pendingComments] // Capture comments before the object
|
||||
pendingComments = []
|
||||
|
||||
expect("{")
|
||||
|
||||
let first = true
|
||||
while (!skip("}")) {
|
||||
members.push(parseMember())
|
||||
if (!skip(",")) {
|
||||
expect("}")
|
||||
break
|
||||
if (!first) {
|
||||
// Expect a comma between members
|
||||
expect(",")
|
||||
|
||||
// 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 {
|
||||
kind: "Object",
|
||||
start: nodeStart,
|
||||
end: lastEnd,
|
||||
members,
|
||||
}
|
||||
}
|
||||
|
||||
function parseMember(): JSONObjectMember {
|
||||
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,
|
||||
comments:
|
||||
comments.length > 0 || trailingComments.length > 0
|
||||
? [...comments, ...trailingComments]
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function parseArr(): JSONArrayValue {
|
||||
const nodeStart = start
|
||||
const values: JSONValue[] = []
|
||||
const comments = [...pendingComments] // Capture comments before the array
|
||||
pendingComments = []
|
||||
|
||||
expect("[")
|
||||
|
||||
let first = true
|
||||
while (!skip("]")) {
|
||||
values.push(parseVal())
|
||||
if (!skip(",")) {
|
||||
expect("]")
|
||||
break
|
||||
if (!first) {
|
||||
// Expect a comma between values
|
||||
expect(",")
|
||||
|
||||
// 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 {
|
||||
kind: "Array",
|
||||
start: nodeStart,
|
||||
end: lastEnd,
|
||||
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() {
|
||||
lastEnd = end
|
||||
|
||||
// Skip whitespace and comments
|
||||
while (true) {
|
||||
// Skip whitespace (space, tab, newline, etc.)
|
||||
// Skip whitespace
|
||||
while (code === 9 || code === 10 || code === 13 || code === 32) {
|
||||
ch()
|
||||
}
|
||||
|
||||
// Check for single-line comment (//)
|
||||
// Handle single-line comments
|
||||
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) {
|
||||
// Skip until newline or EOF
|
||||
commentText += String.fromCharCode(code)
|
||||
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) {
|
||||
// 42 is '*'
|
||||
ch() // Skip the '*'
|
||||
ch() // Move past the opening '/*'
|
||||
const commentStart = end
|
||||
ch() // Skip '/'
|
||||
ch() // Skip '*'
|
||||
|
||||
let commentText = ""
|
||||
while (
|
||||
code !== 0 &&
|
||||
!(code === 42 && string.charCodeAt(end + 1) === 47)
|
||||
) {
|
||||
// Look for '*/'
|
||||
commentText += String.fromCharCode(code)
|
||||
ch()
|
||||
}
|
||||
ch() // Skip the '*'
|
||||
ch() // Move past the closing '*/'
|
||||
continue // After skipping the comment, recheck for more whitespace/comments
|
||||
|
||||
if (code === 0) {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user