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 { 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)
}

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
}
// 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) {