Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM node:22-alpine AS build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml* ./
COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./
RUN pnpm install --frozen-lockfile || pnpm install
COPY . .
RUN pnpm build
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "json-tool",
"private": true,
"version": "0.4.0",
"version": "0.5.0",
"type": "module",
"packageManager": "pnpm@10.28.0",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
Expand Down
5 changes: 5 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# pnpm settings (the new home for the former package.json "pnpm" field).
# esbuild needs its install script to fetch the platform binary; without
# this, pnpm 10 fails the CI install with ERR_PNPM_IGNORED_BUILDS.
onlyBuiltDependencies:
- esbuild
117 changes: 117 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,12 @@ body {
color: var(--ink-faint);
cursor: not-allowed;
}
.tbtn.is-active {
color: var(--accent);
}
.tbtn.is-active::after {
transform: scaleX(1);
}

/* ─────────────────────────── tree search ─────────────────────────── */

Expand Down Expand Up @@ -1481,6 +1487,117 @@ body {
background: var(--code-bg-2);
}

.modal-check.is-disabled {
opacity: 0.45;
cursor: not-allowed;
}
.modal-check.is-disabled input {
cursor: not-allowed;
}

/* ─────────────────────── markdown preview ─────────────────────── */
.modal-body-md {
background: var(--paper);
color: var(--ink);
font-family: var(--serif);
font-size: 15px;
line-height: 1.75;
white-space: normal;
background-image: none;
}
.modal-body-md > :first-child {
margin-top: 0;
}
.modal-body-md > :last-child {
margin-bottom: 0;
}
.modal-body-md .md-h {
font-family: var(--sans);
font-weight: 600;
line-height: 1.3;
margin: 1.2em 0 0.5em;
color: var(--ink);
letter-spacing: -0.005em;
}
.modal-body-md .md-h1 {
font-size: 26px;
border-bottom: 1px solid var(--rule);
padding-bottom: 0.25em;
}
.modal-body-md .md-h2 {
font-size: 22px;
border-bottom: 1px solid var(--rule);
padding-bottom: 0.2em;
}
.modal-body-md .md-h3 { font-size: 18px; }
.modal-body-md .md-h4 { font-size: 16px; }
.modal-body-md .md-h5 { font-size: 14px; color: var(--ink-muted); }
.modal-body-md .md-h6 {
font-size: 13px;
color: var(--ink-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.modal-body-md .md-p {
margin: 0.6em 0;
white-space: pre-wrap;
word-break: break-word;
}
.modal-body-md .md-list {
margin: 0.5em 0;
padding-left: 1.6em;
}
.modal-body-md .md-list li {
margin: 0.2em 0;
}
.modal-body-md .md-quote {
margin: 0.8em 0;
padding: 0.2em 0.9em;
border-left: 3px solid var(--accent);
background: var(--paper-2);
color: var(--ink-muted);
font-style: italic;
}
.modal-body-md .md-hr {
border: none;
border-top: 1px solid var(--rule);
margin: 1.2em 0;
}
.modal-body-md .md-code-inline {
font-family: var(--mono);
font-size: 0.88em;
padding: 1px 5px;
background: var(--code-bg);
color: var(--code-ink);
border-radius: 2px;
}
.modal-body-md .md-code-block {
margin: 0.8em 0;
padding: 12px 14px;
background: var(--code-bg);
color: var(--code-ink);
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.6;
border-radius: 2px;
overflow: auto;
white-space: pre;
}
.modal-body-md .md-code-block code {
font-family: inherit;
background: transparent;
padding: 0;
color: inherit;
}
.modal-body-md .md-link {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
}
.modal-body-md .md-link:hover {
text-decoration: none;
}

/* ─────────────────────────── animations ─────────────────────────── */

@keyframes fadeUp {
Expand Down
10 changes: 10 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default function App() {
const [searchScope, setSearchScope] = useState<SearchScope>('all');
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
const [sortKeys, setSortKeys] = useState<'none' | 'asc' | 'desc'>('none');
const [expandLong, setExpandLong] = useState(false);
const [shareLoading, setShareLoading] = useState(false);
const [shareLoadError, setShareLoadError] = useState<string | null>(null);

Expand Down Expand Up @@ -345,6 +346,14 @@ export default function App() {
? 'Key A→Z'
: 'Key Z→A'}
</button>
<button
className={`tbtn${expandLong ? ' is-active' : ''}`}
disabled={!parsed.ok}
onClick={() => setExpandLong((v) => !v)}
title="完整展示长字符串,不再省略为单行"
>
{expandLong ? '长文本:展开' : '长文本:省略'}
</button>
</div>
<div className="tree-search">
<input
Expand Down Expand Up @@ -428,6 +437,7 @@ export default function App() {
searchAncestors={ancestors}
searchCurrentIndex={currentMatchIndex}
sortKeys={sortKeys}
expandLong={expandLong}
/>
) : hasInput ? (
<div className="tree-empty error">
Expand Down
4 changes: 3 additions & 1 deletion src/components/JsonNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface TreeApi {
onOpenValue: (text: string, title?: string) => void;
onDeletePath: (path: string) => void;
sortKeys: SortKeysMode;
expandLong: boolean;
search: {
query: string;
scope: SearchScope;
Expand Down Expand Up @@ -271,7 +272,8 @@ function PrimitiveNode({ value, keyLabel, path, api }: JsonNodeProps) {
}

const looksLikeJson = isString && isLikelyJsonString(strValue);
const isLong = isString && strValue.length > LONG_VALUE_THRESHOLD;
const isLong =
isString && !api.expandLong && strValue.length > LONG_VALUE_THRESHOLD;
const hasEscape = isString && /\\["'\\/bfnrt]|\\u[0-9a-fA-F]{4}/.test(strValue);

const hasValueMatch =
Expand Down
4 changes: 4 additions & 0 deletions src/components/JsonTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface JsonTreeProps {
searchAncestors: Set<string>;
searchCurrentIndex: number;
sortKeys: SortKeysMode;
expandLong: boolean;
}

export function JsonTree({
Expand All @@ -26,6 +27,7 @@ export function JsonTree({
searchAncestors,
searchCurrentIndex,
sortKeys,
expandLong,
}: JsonTreeProps) {
const [expandedOverride, setExpandedOverride] = useState<
Map<string, boolean>
Expand Down Expand Up @@ -103,6 +105,7 @@ export function JsonTree({
onOpenValue,
onDeletePath,
sortKeys,
expandLong,
search: {
query: searchQuery,
scope: searchScope,
Expand All @@ -123,6 +126,7 @@ export function JsonTree({
currentPath,
registerNode,
sortKeys,
expandLong,
]
);

Expand Down
27 changes: 23 additions & 4 deletions src/components/ValuePopup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { unescapeString } from '../utils/jsonUtils';
import { renderMarkdown } from '../utils/markdown';

interface ValuePopupProps {
text: string;
Expand All @@ -10,9 +11,14 @@ interface ValuePopupProps {
export function ValuePopup({ text, title, onClose }: ValuePopupProps) {
const [wrap, setWrap] = useState(true);
const [unesc, setUnesc] = useState(false);
const [md, setMd] = useState(true);
const [copied, setCopied] = useState(false);

const displayed = unesc ? unescapeString(text) : text;
const rendered = useMemo(
() => (md ? renderMarkdown(displayed) : null),
[md, displayed]
);

useEffect(() => {
const onKey = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -44,9 +50,18 @@ export function ValuePopup({ text, title, onClose }: ValuePopupProps) {
{displayed.length.toLocaleString()} 字符
</span>
<label className="modal-check">
<input
type="checkbox"
checked={md}
onChange={(e) => setMd(e.target.checked)}
/>
Markdown
</label>
<label className={`modal-check${md ? ' is-disabled' : ''}`}>
<input
type="checkbox"
checked={wrap}
disabled={md}
onChange={(e) => setWrap(e.target.checked)}
/>
自动换行
Expand All @@ -66,9 +81,13 @@ export function ValuePopup({ text, title, onClose }: ValuePopupProps) {
关闭
</button>
</div>
<pre className={`modal-body ${wrap ? 'wrap' : 'nowrap'}`}>
{displayed}
</pre>
{md ? (
<div className="modal-body modal-body-md">{rendered}</div>
) : (
<pre className={`modal-body ${wrap ? 'wrap' : 'nowrap'}`}>
{displayed}
</pre>
)}
</div>
</div>
);
Expand Down
Loading
Loading