1
0
Fork 0
forked from Mirrors/gomuks

web/viewsource: add fancy JSON rendering

This commit is contained in:
Tulir Asokan 2024-10-27 16:11:41 +02:00
parent 6b4b12435a
commit fa004a639e
4 changed files with 158 additions and 9 deletions

36
web/src/ui/JSONView.css Normal file
View file

@ -0,0 +1,36 @@
pre.json-view {
white-space: wrap;
overflow-wrap: anywhere;
margin: 0;
ul, ol {
margin: 0;
> li {
list-style-type: none;
}
}
span.json-collapsed {
user-select: none;
}
button {
padding: 0 .25rem;
}
/* If the screen is wide enough, make line-wrapped strings aligned after the object key */
@media screen and (min-width: 800px) {
li.json-object-entry:has(> span.json-comma-container > span.json-string) {
display: flex;
span.json-object-key {
white-space: nowrap;
}
span.json-object-entry-colon {
white-space: pre;
}
}
}
}

120
web/src/ui/JSONView.tsx Normal file
View file

@ -0,0 +1,120 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { useReducer } from "react"
import "./JSONView.css"
interface JSONViewProps {
data: unknown
}
interface JSONViewPropsWithKey extends JSONViewProps {
objectKey?: string
trailingComma?: boolean
noCollapse?: boolean
}
function renderJSONString(data: string, styleClass: string = "s2") {
return <span className={`json-string ${styleClass}`}>{JSON.stringify(data)}</span>
}
function renderJSONValue(data: unknown, collapsed: boolean) {
switch (typeof data) {
case "object":
if (data === null) {
return <span className="json-null kc">null</span>
} else if (Array.isArray(data)) {
if (data.length === 0) {
return null
} else if (collapsed) {
return <span className="json-collapsed"></span>
}
return <ol className="json-array-children">
{data.map((item, i) =>
<li key={i} className="json-array-entry"><JSONValueWithKey data={item}/></li>)}
</ol>
} else {
const entries = Object.entries(data)
if (entries.length === 0) {
return null
} else if (collapsed) {
return <span className="json-collapsed"></span>
}
return <ul className="json-object-children">
{entries.map(([key, value], index, arr) =>
<li key={key} className="json-object-entry">
<JSONValueWithKey data={value} objectKey={key} trailingComma={index < arr.length - 1}/>
</li>)}
</ul>
}
case "string":
return renderJSONString(data)
case "number":
return <span className="json-number mf">{data}</span>
case "boolean":
return <span className="json-boolean kc">{data ? "true" : "false"}</span>
default:
return <span className="json-unknown">undefined</span>
}
}
function JSONValueWithKey({ data, objectKey, trailingComma, noCollapse }: JSONViewPropsWithKey) {
const [collapsed, toggleCollapsed] = useReducer(collapsed => !collapsed, false)
const renderedKey = objectKey
? <span className="json-object-key">
{renderJSONString(objectKey, "nt")}
<span className="json-object-entry-colon p">: </span>
</span>
: null
const renderedSuffix = trailingComma
? <span className="json-object-comma p">,</span>
: null
const collapseButton = noCollapse ? null :
<button onClick={toggleCollapsed} title={collapsed ? "Expand" : "Collapse"}>
{collapsed ? "+" : "-"}
</button>
if (Array.isArray(data)) {
return <>
{renderedKey}
{collapseButton}
<span className="json-array-bracket p">[</span>
{renderJSONValue(data, collapsed)}
<span className="json-array-bracket p">]</span>
{renderedSuffix}
</>
} else if (data !== null && typeof data === "object") {
return <>
{renderedKey}
{collapseButton}
<span className="json-object-brace p">{"{"}</span>
{renderJSONValue(data, collapsed)}
<span className="json-object-brace p">{"}"}</span>
{renderedSuffix}
</>
}
return <>
{renderedKey}
<span className="json-comma-container">
{renderJSONValue(data, collapsed)}
{renderedSuffix}
</span>
</>
}
export default function JSONView({ data }: JSONViewProps) {
return <pre className="json-view chroma">
<JSONValueWithKey data={data} noCollapse={true} />
</pre>
}

View file

@ -14,6 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { MemDBEvent } from "@/api/types"
import JSONView from "../../JSONView.tsx"
interface ViewSourceModalProps {
evt: MemDBEvent
@ -21,9 +22,7 @@ interface ViewSourceModalProps {
const ViewSourceModal = ({ evt }: ViewSourceModalProps) => {
return <div className="view-source-modal">
<pre>
{JSON.stringify(evt, null, " ")}
</pre>
<JSONView data={evt} />
</div>
}

View file

@ -56,12 +56,6 @@ div.view-source-modal {
background-color: white;
border-radius: 1rem;
padding: 1rem;
> pre {
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
}
div.confirm-message-modal {