diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..f8960e1
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,31 @@
+# Build output
+build/
+dist/
+.svelte-kit/
+node_modules/
+
+# Test reports
+playwright-report/
+test-results/
+coverage/
+
+# Config and generated files
+*.config.js
+*.config.ts
+.DS_Store
+*.min.js
+
+# Temporary files
+*.tmp
+*.log
+.env
+.env.*
+!.env.example
+
+# IDE
+.vscode/
+.idea/
+
+# Documentation
+CLAUDE.md
+*.md
\ No newline at end of file
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index f4ad724..ace96c9 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -54,4 +54,4 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
- cache-to: type=gha,mode=max
\ No newline at end of file
+ cache-to: type=gha,mode=max
diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index b56901a..9914f67 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -9,7 +9,7 @@ on:
jobs:
lint-and-format:
runs-on: ubuntu-latest
-
+
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -30,4 +30,4 @@ jobs:
run: npm run format:check
- name: Type check
- run: npm run check
\ No newline at end of file
+ run: npm run check
diff --git a/README-ko.md b/README-ko.md
index 06e2ca2..93f9c00 100644
--- a/README-ko.md
+++ b/README-ko.md
@@ -1,6 +1,6 @@
# PDJsonEditor
-*[English](README.md)*
+_[English](README.md)_
SvelteKit과 Svelte 5로 구축된 강력한 JSON 시각화 및 편집 도구입니다. JSON 데이터를 코드 에디터와 인터랙티브 그래프 뷰에서 동시에 보고 편집할 수 있습니다.
@@ -11,18 +11,21 @@ SvelteKit과 Svelte 5로 구축된 강력한 JSON 시각화 및 편집 도구입
## ✨ 주요 기능
### 📝 고급 JSON 에디터
+
- **문법 강조**: CodeMirror 기반 JSON 문법 강조 기능
- **실시간 검증**: 즉시 JSON 문법 검증 및 오류 리포팅
- **포맷 & 압축**: 원클릭 JSON 포맷팅 및 압축 기능
- **네비게이션**: 그래프 노드 클릭으로 해당 JSON 위치로 이동
### 🔗 HTTP 요청 통합
+
- **다중 메소드 지원**: GET, POST, PUT, DELETE, PATCH 요청
- **커스텀 헤더**: HTTP 헤더 추가 및 관리
- **요청 본문**: POST/PUT/PATCH용 커스텀 요청 본문 설정
- **URL 가져오기**: URL에서 직접 JSON 데이터 가져오기
### 📊 인터랙티브 그래프 시각화
+
- **트리 구조**: JSON을 인터랙티브 트리 그래프로 시각화
- **컴팩트 노드**: 원시 값 그룹화 디스플레이
- **확장/축소**: 시각적 표시와 함께 노드 확장 토글
@@ -30,12 +33,14 @@ SvelteKit과 Svelte 5로 구축된 강력한 JSON 시각화 및 편집 도구입
- **자동 레이아웃**: Dagre 기반 자동 그래프 레이아웃
### 🎯 스마트 노드 디스플레이
+
- **그룹화된 원시 값**: 명확성을 위해 부모 노드에서 원시 값 그룹화
- **참조 타입**: 객체와 배열을 참조로 표시 (예: `address {3}`, `hobbies [3]`)
- **더 보기**: 20개 이상의 항목을 가진 노드를 "더 보기" 기능과 함께 자동 축소
- **개별 토글**: 개별 참조 항목 확장/축소
### 🌐 다국어 지원
+
- **다중 언어**: 영어 및 한국어 지원
- **언어 전환기**: 헤더에서 쉬운 언어 전환
- **지속적 설정**: localStorage에 언어 설정 저장
@@ -43,28 +48,33 @@ SvelteKit과 Svelte 5로 구축된 강력한 JSON 시각화 및 편집 도구입
## 🚀 시작하기
### 필수 조건
+
- Node.js v20.19 이상
- npm 또는 yarn 패키지 매니저
### 설치
1. **저장소 클론**
+
```bash
git clone https://github.com/podosoft-dev/pdjsoneditor.git
cd pdjsoneditor
```
2. **의존성 설치**
+
```bash
npm install
```
3. **개발 서버 시작**
+
```bash
npm run dev
```
4. **브라우저에서 열기**
+
```
http://localhost:5173
```
@@ -141,11 +151,12 @@ services:
image: ghcr.io/podosoft-dev/pdjsoneditor:latest
container_name: pdjsoneditor
ports:
- - "3000:3000"
+ - '3000:3000'
restart: unless-stopped
```
그 다음 실행:
+
```bash
docker-compose up -d
```
@@ -155,17 +166,20 @@ docker-compose up -d
## 📖 사용 방법
### 기본 JSON 편집
+
1. **JSON 붙여넣기 또는 입력**: 왼쪽 에디터 패널에서
2. **구조 보기**: 오른쪽 그래프 패널에서
3. **뷰 간 네비게이션**: 노드 클릭 또는 에디터 사용
### URL에서 데이터 가져오기
+
1. **HTTP 메소드 선택**: 드롭다운에서 (GET, POST, PUT, DELETE, PATCH)
2. **URL 입력**: 입력 필드에
3. **헤더와 본문 설정**: Settings 버튼 사용 (선택사항)
4. **"Go" 클릭**: JSON 데이터를 가져와서 로드
### 그래프 상호작용
+
- **확장/축소**: 노드의 색깔있는 핸들 클릭
- **더 보기**: 많은 항목이 있는 노드에서 "더 보기" 클릭
- **네비게이션**: 노드 클릭으로 해당 JSON 위치로 이동
@@ -193,4 +207,4 @@ docker-compose up -d
\ No newline at end of file
+
diff --git a/README.md b/README.md
index ef893df..6960418 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# PDJsonEditor
-*[한국어](README-ko.md)*
+_[한국어](README-ko.md)_
A powerful JSON visualization and editing tool built with SvelteKit and Svelte 5. View and edit JSON data simultaneously in both code editor and interactive graph views.
@@ -11,18 +11,21 @@ A powerful JSON visualization and editing tool built with SvelteKit and Svelte 5
## ✨ Features
### 📝 Advanced JSON Editor
+
- **Syntax Highlighting**: CodeMirror-powered editor with JSON syntax highlighting
- **Real-time Validation**: Instant JSON syntax validation and error reporting
- **Format & Minify**: One-click JSON formatting and minification
- **Navigation**: Click on graph nodes to jump to corresponding JSON location
### 🔗 HTTP Request Integration
+
- **Multi-Method Support**: GET, POST, PUT, DELETE, PATCH requests
- **Custom Headers**: Add and manage HTTP headers
- **Request Body**: Configure custom request bodies for POST/PUT/PATCH
- **URL Import**: Fetch JSON data directly from URLs
### 📊 Interactive Graph Visualization
+
- **Tree Structure**: Visualize JSON as an interactive tree graph
- **Compact Nodes**: Compact display grouping primitive values
- **Expand/Collapse**: Toggle node expansion with visual indicators
@@ -30,12 +33,14 @@ A powerful JSON visualization and editing tool built with SvelteKit and Svelte 5
- **Auto Layout**: Dagre-powered automatic graph layout
### 🎯 Smart Node Display
+
- **Grouped Primitives**: Primitive values grouped in parent nodes for clarity
- **Reference Types**: Objects and arrays shown as references (e.g., `address {3}`, `hobbies [3]`)
- **Show More**: Automatically collapse nodes with 20+ items with "show more" functionality
- **Individual Toggles**: Expand/collapse individual reference items
### 🌐 Internationalization
+
- **Multi-language**: English and Korean support
- **Language Switcher**: Easy language switching in header
- **Persistent Settings**: Language preference saved in localStorage
@@ -43,28 +48,33 @@ A powerful JSON visualization and editing tool built with SvelteKit and Svelte 5
## 🚀 Getting Started
### Prerequisites
+
- Node.js v20.19 or higher
- npm or yarn package manager
### Installation
1. **Clone the repository**
+
```bash
git clone https://github.com/podosoft-dev/pdjsoneditor.git
cd pdjsoneditor
```
2. **Install dependencies**
+
```bash
npm install
```
3. **Start development server**
+
```bash
npm run dev
```
4. **Open in browser**
+
```
http://localhost:5173
```
@@ -141,11 +151,12 @@ services:
image: ghcr.io/podosoft-dev/pdjsoneditor:latest
container_name: pdjsoneditor
ports:
- - "3000:3000"
+ - '3000:3000'
restart: unless-stopped
```
Then run:
+
```bash
docker-compose up -d
```
@@ -155,17 +166,20 @@ Access the application at `http://localhost:3000`
## 📖 Usage
### Basic JSON Editing
+
1. **Paste or type JSON** in the left editor panel
2. **View the structure** in the right graph panel
3. **Navigate between views** by clicking nodes or using the editor
### Fetching Data from URLs
+
1. **Select HTTP method** from the dropdown (GET, POST, PUT, DELETE, PATCH)
2. **Enter the URL** in the input field
3. **Configure headers and body** using the Settings button (optional)
4. **Click "Go"** to fetch and load the JSON data
### Graph Interaction
+
- **Expand/Collapse**: Click the colored handles on nodes
- **Show More**: Click "Show more" on nodes with many items
- **Navigate**: Click nodes to jump to corresponding JSON location
@@ -193,4 +207,4 @@ If you encounter any issues or have questions, please [open an issue](https://gi
\ No newline at end of file
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 73b546e..09762e6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -8,15 +8,15 @@ services:
image: pdjsoneditor:latest
container_name: pdjsoneditor
ports:
- - "3000:3000"
+ - '3000:3000'
environment:
- NODE_ENV=production
- HOST=0.0.0.0
- PORT=3000
restart: unless-stopped
healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"]
+ test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3000']
interval: 30s
timeout: 10s
retries: 3
- start_period: 40s
\ No newline at end of file
+ start_period: 40s
diff --git a/playwright.config.ts b/playwright.config.ts
index d2f8557..1ef6ec3 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -22,10 +22,7 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
- reporter: [
- ['html', { outputFolder: 'playwright-report' }],
- ['list']
- ],
+ reporter: [['html', { outputFolder: 'playwright-report' }], ['list']],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
@@ -33,25 +30,25 @@ export default defineConfig({
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Take screenshot on failure */
- screenshot: 'only-on-failure',
+ screenshot: 'only-on-failure'
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
- use: { ...devices['Desktop Chrome'] },
+ use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
- use: { ...devices['Desktop Firefox'] },
+ use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
- use: { ...devices['Desktop Safari'] },
- },
+ use: { ...devices['Desktop Safari'] }
+ }
/* Test against mobile viewports. */
// {
@@ -80,13 +77,13 @@ export default defineConfig({
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
- timeout: 120 * 1000,
+ timeout: 120 * 1000
},
{
command: 'npm run test:server',
url: 'http://localhost:3001',
reuseExistingServer: !process.env.CI,
- timeout: 120 * 1000,
+ timeout: 120 * 1000
}
- ],
-});
\ No newline at end of file
+ ]
+});
diff --git a/src/i18n/en/index.ts b/src/i18n/en/index.ts
index df875fc..dc96a3b 100644
--- a/src/i18n/en/index.ts
+++ b/src/i18n/en/index.ts
@@ -30,9 +30,10 @@ const en: BaseTranslation = {
bodyDescription: 'Configure the request body for POST/PUT/PATCH requests.',
bodyPlaceholder: 'Enter request body (JSON, XML, etc.)',
useEditorContent: 'Use editor content as request body',
- sendAsRawText: 'Send as raw text (don\'t parse as JSON)',
+ sendAsRawText: "Send as raw text (don't parse as JSON)",
clearAll: 'Clear All',
- clearAllConfirm: 'Are you sure you want to clear all settings? This will remove all saved headers, body, and URL from storage.',
+ clearAllConfirm:
+ 'Are you sure you want to clear all settings? This will remove all saved headers, body, and URL from storage.',
cancel: 'Cancel',
save: 'Save'
},
diff --git a/src/i18n/i18n-svelte.ts b/src/i18n/i18n-svelte.ts
index 6cdffb3..2d66bc9 100644
--- a/src/i18n/i18n-svelte.ts
+++ b/src/i18n/i18n-svelte.ts
@@ -1,12 +1,17 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
-import { initI18nSvelte } from 'typesafe-i18n/svelte'
-import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types'
-import { loadedFormatters, loadedLocales } from './i18n-util'
+import { initI18nSvelte } from 'typesafe-i18n/svelte';
+import type { Formatters, Locales, TranslationFunctions, Translations } from './i18n-types';
+import { loadedFormatters, loadedLocales } from './i18n-util';
-const { locale, LL, setLocale } = initI18nSvelte(loadedLocales, loadedFormatters)
+const { locale, LL, setLocale } = initI18nSvelte<
+ Locales,
+ Translations,
+ TranslationFunctions,
+ Formatters
+>(loadedLocales, loadedFormatters);
-export { locale, LL, setLocale }
+export { locale, LL, setLocale };
-export default LL
+export default LL;
diff --git a/src/i18n/i18n-types.ts b/src/i18n/i18n-types.ts
index 60a437c..702cb16 100644
--- a/src/i18n/i18n-types.ts
+++ b/src/i18n/i18n-types.ts
@@ -1,363 +1,365 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
-import type { BaseTranslation as BaseTranslationType, LocalizedString, RequiredParams } from 'typesafe-i18n'
+import type {
+ BaseTranslation as BaseTranslationType,
+ LocalizedString,
+ RequiredParams
+} from 'typesafe-i18n';
-export type BaseTranslation = BaseTranslationType
-export type BaseLocale = 'en'
+export type BaseTranslation = BaseTranslationType;
+export type BaseLocale = 'en';
-export type Locales =
- | 'en'
- | 'ko'
+export type Locales = 'en' | 'ko';
-export type Translation = RootTranslation
+export type Translation = RootTranslation;
-export type Translations = RootTranslation
+export type Translations = RootTranslation;
type RootTranslation = {
header: {
/**
* JSON Editor
*/
- title: string
+ title: string;
/**
* Clear
*/
- clear: string
+ clear: string;
/**
* Copy
*/
- copy: string
+ copy: string;
/**
* Copied to clipboard
*/
- copySuccess: string
+ copySuccess: string;
/**
* Failed to copy to clipboard
*/
- copyError: string
+ copyError: string;
/**
* Format
*/
- format: string
+ format: string;
/**
* Minify
*/
- minify: string
+ minify: string;
/**
* Sample Data
*/
- sample: string
+ sample: string;
/**
* Language
*/
- language: string
- }
+ language: string;
+ };
editor: {
/**
* Enter your JSON data here...
*/
- placeholder: string
+ placeholder: string;
/**
* Invalid JSON
*/
- invalidJson: string
+ invalidJson: string;
/**
* Valid JSON
*/
- validJson: string
+ validJson: string;
/**
* Enter URL to fetch JSON...
*/
- urlPlaceholder: string
+ urlPlaceholder: string;
/**
* URL is required
*/
- urlRequired: string
+ urlRequired: string;
/**
* Failed to fetch data
*/
- fetchError: string
+ fetchError: string;
/**
* Go
*/
- go: string
+ go: string;
/**
* Request Settings
*/
- requestSettings: string
+ requestSettings: string;
/**
* Configure HTTP headers and request body
*/
- requestSettingsDescription: string
+ requestSettingsDescription: string;
/**
* Headers
*/
- headers: string
+ headers: string;
/**
* Header key
*/
- headerKey: string
+ headerKey: string;
/**
* Header value
*/
- headerValue: string
+ headerValue: string;
/**
* Add Header
*/
- addHeader: string
+ addHeader: string;
/**
* Body
*/
- body: string
+ body: string;
/**
* Configure the request body for POST/PUT/PATCH requests.
*/
- bodyDescription: string
+ bodyDescription: string;
/**
* Enter request body (JSON, XML, etc.)
*/
- bodyPlaceholder: string
+ bodyPlaceholder: string;
/**
* Use editor content as request body
*/
- useEditorContent: string
+ useEditorContent: string;
/**
* Send as raw text (don't parse as JSON)
*/
- sendAsRawText: string
+ sendAsRawText: string;
/**
* Clear All
*/
- clearAll: string
+ clearAll: string;
/**
* Are you sure you want to clear all settings? This will remove all saved headers, body, and URL from storage.
*/
- clearAllConfirm: string
+ clearAllConfirm: string;
/**
* Cancel
*/
- cancel: string
+ cancel: string;
/**
* Save
*/
- save: string
- }
+ save: string;
+ };
graph: {
/**
* Show {count} more
* @param {unknown} count
*/
- showMore: RequiredParams<'count'>
+ showMore: RequiredParams<'count'>;
/**
* Show less
*/
- showLess: string
+ showLess: string;
/**
* Expand
*/
- expand: string
+ expand: string;
/**
* Collapse
*/
- collapse: string
+ collapse: string;
/**
* Expand all
*/
- expandAll: string
- }
+ expandAll: string;
+ };
languages: {
/**
* English
*/
- en: string
+ en: string;
/**
* 한국어
*/
- ko: string
+ ko: string;
/**
* 日本語
*/
- ja: string
- }
+ ja: string;
+ };
footer: {
/**
* Ready
*/
- ready: string
- }
-}
+ ready: string;
+ };
+};
export type TranslationFunctions = {
header: {
/**
* JSON Editor
*/
- title: () => LocalizedString
+ title: () => LocalizedString;
/**
* Clear
*/
- clear: () => LocalizedString
+ clear: () => LocalizedString;
/**
* Copy
*/
- copy: () => LocalizedString
+ copy: () => LocalizedString;
/**
* Copied to clipboard
*/
- copySuccess: () => LocalizedString
+ copySuccess: () => LocalizedString;
/**
* Failed to copy to clipboard
*/
- copyError: () => LocalizedString
+ copyError: () => LocalizedString;
/**
* Format
*/
- format: () => LocalizedString
+ format: () => LocalizedString;
/**
* Minify
*/
- minify: () => LocalizedString
+ minify: () => LocalizedString;
/**
* Sample Data
*/
- sample: () => LocalizedString
+ sample: () => LocalizedString;
/**
* Language
*/
- language: () => LocalizedString
- }
+ language: () => LocalizedString;
+ };
editor: {
/**
* Enter your JSON data here...
*/
- placeholder: () => LocalizedString
+ placeholder: () => LocalizedString;
/**
* Invalid JSON
*/
- invalidJson: () => LocalizedString
+ invalidJson: () => LocalizedString;
/**
* Valid JSON
*/
- validJson: () => LocalizedString
+ validJson: () => LocalizedString;
/**
* Enter URL to fetch JSON...
*/
- urlPlaceholder: () => LocalizedString
+ urlPlaceholder: () => LocalizedString;
/**
* URL is required
*/
- urlRequired: () => LocalizedString
+ urlRequired: () => LocalizedString;
/**
* Failed to fetch data
*/
- fetchError: () => LocalizedString
+ fetchError: () => LocalizedString;
/**
* Go
*/
- go: () => LocalizedString
+ go: () => LocalizedString;
/**
* Request Settings
*/
- requestSettings: () => LocalizedString
+ requestSettings: () => LocalizedString;
/**
* Configure HTTP headers and request body
*/
- requestSettingsDescription: () => LocalizedString
+ requestSettingsDescription: () => LocalizedString;
/**
* Headers
*/
- headers: () => LocalizedString
+ headers: () => LocalizedString;
/**
* Header key
*/
- headerKey: () => LocalizedString
+ headerKey: () => LocalizedString;
/**
* Header value
*/
- headerValue: () => LocalizedString
+ headerValue: () => LocalizedString;
/**
* Add Header
*/
- addHeader: () => LocalizedString
+ addHeader: () => LocalizedString;
/**
* Body
*/
- body: () => LocalizedString
+ body: () => LocalizedString;
/**
* Configure the request body for POST/PUT/PATCH requests.
*/
- bodyDescription: () => LocalizedString
+ bodyDescription: () => LocalizedString;
/**
* Enter request body (JSON, XML, etc.)
*/
- bodyPlaceholder: () => LocalizedString
+ bodyPlaceholder: () => LocalizedString;
/**
* Use editor content as request body
*/
- useEditorContent: () => LocalizedString
+ useEditorContent: () => LocalizedString;
/**
* Send as raw text (don't parse as JSON)
*/
- sendAsRawText: () => LocalizedString
+ sendAsRawText: () => LocalizedString;
/**
* Clear All
*/
- clearAll: () => LocalizedString
+ clearAll: () => LocalizedString;
/**
* Are you sure you want to clear all settings? This will remove all saved headers, body, and URL from storage.
*/
- clearAllConfirm: () => LocalizedString
+ clearAllConfirm: () => LocalizedString;
/**
* Cancel
*/
- cancel: () => LocalizedString
+ cancel: () => LocalizedString;
/**
* Save
*/
- save: () => LocalizedString
- }
+ save: () => LocalizedString;
+ };
graph: {
/**
* Show {count} more
*/
- showMore: (arg: { count: unknown }) => LocalizedString
+ showMore: (arg: { count: unknown }) => LocalizedString;
/**
* Show less
*/
- showLess: () => LocalizedString
+ showLess: () => LocalizedString;
/**
* Expand
*/
- expand: () => LocalizedString
+ expand: () => LocalizedString;
/**
* Collapse
*/
- collapse: () => LocalizedString
+ collapse: () => LocalizedString;
/**
* Expand all
*/
- expandAll: () => LocalizedString
- }
+ expandAll: () => LocalizedString;
+ };
languages: {
/**
* English
*/
- en: () => LocalizedString
+ en: () => LocalizedString;
/**
* 한국어
*/
- ko: () => LocalizedString
+ ko: () => LocalizedString;
/**
* 日本語
*/
- ja: () => LocalizedString
- }
+ ja: () => LocalizedString;
+ };
footer: {
/**
* Ready
*/
- ready: () => LocalizedString
- }
-}
+ ready: () => LocalizedString;
+ };
+};
-export type Formatters = {}
+export type Formatters = {};
diff --git a/src/i18n/i18n-util.async.ts b/src/i18n/i18n-util.async.ts
index 30fd5f7..cb0ba67 100644
--- a/src/i18n/i18n-util.async.ts
+++ b/src/i18n/i18n-util.async.ts
@@ -1,27 +1,27 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
-import { initFormatters } from './formatters'
-import type { Locales, Translations } from './i18n-types'
-import { loadedFormatters, loadedLocales, locales } from './i18n-util'
+import { initFormatters } from './formatters';
+import type { Locales, Translations } from './i18n-types';
+import { loadedFormatters, loadedLocales, locales } from './i18n-util';
const localeTranslationLoaders = {
en: () => import('./en'),
- ko: () => import('./ko'),
-}
+ ko: () => import('./ko')
+};
const updateDictionary = (locale: Locales, dictionary: Partial): Translations =>
- loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }
+ (loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary });
export const importLocaleAsync = async (locale: Locales): Promise =>
- (await localeTranslationLoaders[locale]()).default as unknown as Translations
+ (await localeTranslationLoaders[locale]()).default as unknown as Translations;
export const loadLocaleAsync = async (locale: Locales): Promise => {
- updateDictionary(locale, await importLocaleAsync(locale))
- loadFormatters(locale)
-}
+ updateDictionary(locale, await importLocaleAsync(locale));
+ loadFormatters(locale);
+};
-export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync))
+export const loadAllLocalesAsync = (): Promise => Promise.all(locales.map(loadLocaleAsync));
export const loadFormatters = (locale: Locales): void =>
- void (loadedFormatters[locale] = initFormatters(locale))
+ void (loadedFormatters[locale] = initFormatters(locale));
diff --git a/src/i18n/i18n-util.sync.ts b/src/i18n/i18n-util.sync.ts
index 410289d..bb7a35d 100644
--- a/src/i18n/i18n-util.sync.ts
+++ b/src/i18n/i18n-util.sync.ts
@@ -1,26 +1,26 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
-import { initFormatters } from './formatters'
-import type { Locales, Translations } from './i18n-types'
-import { loadedFormatters, loadedLocales, locales } from './i18n-util'
+import { initFormatters } from './formatters';
+import type { Locales, Translations } from './i18n-types';
+import { loadedFormatters, loadedLocales, locales } from './i18n-util';
-import en from './en'
-import ko from './ko'
+import en from './en';
+import ko from './ko';
const localeTranslations = {
en,
- ko,
-}
+ ko
+};
export const loadLocale = (locale: Locales): void => {
- if (loadedLocales[locale]) return
+ if (loadedLocales[locale]) return;
- loadedLocales[locale] = localeTranslations[locale] as unknown as Translations
- loadFormatters(locale)
-}
+ loadedLocales[locale] = localeTranslations[locale] as unknown as Translations;
+ loadFormatters(locale);
+};
-export const loadAllLocales = (): void => locales.forEach(loadLocale)
+export const loadAllLocales = (): void => locales.forEach(loadLocale);
export const loadFormatters = (locale: Locales): void =>
- void (loadedFormatters[locale] = initFormatters(locale))
+ void (loadedFormatters[locale] = initFormatters(locale));
diff --git a/src/i18n/i18n-util.ts b/src/i18n/i18n-util.ts
index 1828bff..94e9bff 100644
--- a/src/i18n/i18n-util.ts
+++ b/src/i18n/i18n-util.ts
@@ -1,38 +1,44 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
-import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n'
-import type { LocaleDetector } from 'typesafe-i18n/detectors'
-import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n'
-import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
-import { initExtendDictionary } from 'typesafe-i18n/utils'
-import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types'
+import {
+ i18n as initI18n,
+ i18nObject as initI18nObject,
+ i18nString as initI18nString
+} from 'typesafe-i18n';
+import type { LocaleDetector } from 'typesafe-i18n/detectors';
+import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n';
+import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors';
+import { initExtendDictionary } from 'typesafe-i18n/utils';
+import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types';
-export const baseLocale: Locales = 'en'
+export const baseLocale: Locales = 'en';
-export const locales: Locales[] = [
- 'en',
- 'ko'
-]
+export const locales: Locales[] = ['en', 'ko'];
-export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales)
+export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales);
-export const loadedLocales: Record = {} as Record
+export const loadedLocales: Record = {} as Record;
-export const loadedFormatters: Record = {} as Record
+export const loadedFormatters: Record = {} as Record;
-export const extendDictionary = initExtendDictionary()
+export const extendDictionary = initExtendDictionary();
-export const i18nString = (locale: Locales): TranslateByString => initI18nString(locale, loadedFormatters[locale])
+export const i18nString = (locale: Locales): TranslateByString =>
+ initI18nString(locale, loadedFormatters[locale]);
export const i18nObject = (locale: Locales): TranslationFunctions =>
initI18nObject(
locale,
loadedLocales[locale],
loadedFormatters[locale]
- )
+ );
export const i18n = (): LocaleTranslationFunctions =>
- initI18n(loadedLocales, loadedFormatters)
+ initI18n(
+ loadedLocales,
+ loadedFormatters
+ );
-export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn(baseLocale, locales, ...detectors)
+export const detectLocale = (...detectors: LocaleDetector[]): Locales =>
+ detectLocaleFn(baseLocale, locales, ...detectors);
diff --git a/src/lib/components/CompactNode.svelte b/src/lib/components/CompactNode.svelte
index a2cf915..3c22959 100644
--- a/src/lib/components/CompactNode.svelte
+++ b/src/lib/components/CompactNode.svelte
@@ -155,7 +155,9 @@
type="target"
position={Position.Left}
class="!w-1.5 !h-1.5"
- style="left: -3px; background-color: {mode.current === 'dark' ? '#6b7280' : '#9ca3af'}; border-color: {mode.current === 'dark' ? '#4b5563' : '#6b7280'};"
+ style="left: -3px; background-color: {mode.current === 'dark'
+ ? '#6b7280'
+ : '#9ca3af'}; border-color: {mode.current === 'dark' ? '#4b5563' : '#6b7280'};"
/>
{/if}
@@ -247,10 +249,18 @@
id={`${id}-${item.key}`}
class="!w-2.5 !h-2.5 item-handle"
style="position: relative; background-color: {item.isReferenceExpanded
- ? (mode.current === 'dark' ? '#10b981' : '#059669')
- : (mode.current === 'dark' ? '#6b7280' : '#9ca3af')}; border-color: {item.isReferenceExpanded
- ? (mode.current === 'dark' ? '#059669' : '#047857')
- : (mode.current === 'dark' ? '#4b5563' : '#6b7280')};"
+ ? mode.current === 'dark'
+ ? '#10b981'
+ : '#059669'
+ : mode.current === 'dark'
+ ? '#6b7280'
+ : '#9ca3af'}; border-color: {item.isReferenceExpanded
+ ? mode.current === 'dark'
+ ? '#059669'
+ : '#047857'
+ : mode.current === 'dark'
+ ? '#4b5563'
+ : '#6b7280'};"
/>
{/if}
@@ -310,7 +320,9 @@
type="source"
position={Position.Right}
class="!w-1.5 !h-1.5"
- style="right: -3px; background-color: {mode.current === 'dark' ? '#6b7280' : '#9ca3af'}; border-color: {mode.current === 'dark' ? '#4b5563' : '#6b7280'};"
+ style="right: -3px; background-color: {mode.current === 'dark'
+ ? '#6b7280'
+ : '#9ca3af'}; border-color: {mode.current === 'dark' ? '#4b5563' : '#6b7280'};"
/>
{/if}
@@ -521,5 +533,4 @@
color: var(--color-foreground);
border-color: var(--color-muted-foreground);
}
-
diff --git a/src/lib/components/FitViewController.svelte b/src/lib/components/FitViewController.svelte
index 5d9b14d..da4e0bb 100644
--- a/src/lib/components/FitViewController.svelte
+++ b/src/lib/components/FitViewController.svelte
@@ -1,24 +1,24 @@
-
\ No newline at end of file
+
diff --git a/src/lib/components/JsonEditor.svelte b/src/lib/components/JsonEditor.svelte
index 1099158..857af58 100644
--- a/src/lib/components/JsonEditor.svelte
+++ b/src/lib/components/JsonEditor.svelte
@@ -19,31 +19,31 @@
function buildPathToPositionMap(state: EditorState): Map {
const pathMap = new Map();
const tree = syntaxTree(state);
-
+
// Helper to get text content of a node
function getNodeText(from: number, to: number): string {
return state.doc.sliceString(from, to);
}
-
+
// Recursive function to traverse with path context
function traverse(cursor: any, path: string[] = []) {
do {
const nodeName = cursor.name;
-
+
if (nodeName === 'Property') {
// Handle object properties
let propertyKey = '';
let valueStart = -1;
let valueEnd = -1;
let valueType = '';
-
+
if (cursor.firstChild()) {
// Get property name
if (cursor.name === 'PropertyName') {
const keyText = getNodeText(cursor.from, cursor.to);
propertyKey = keyText.replace(/^"|"$/g, '');
}
-
+
// Skip to value (past the colon)
while (cursor.nextSibling()) {
if (cursor.name !== ':') {
@@ -53,16 +53,16 @@
break;
}
}
-
+
// Store the path and position
if (propertyKey && valueStart !== -1) {
const valuePath = [...path, propertyKey];
const pathStr = valuePath.join('.');
-
+
if (pathStr) {
pathMap.set(pathStr, { start: valueStart, end: valueEnd });
}
-
+
// If value is an array, handle array elements specially
if (valueType === 'Array') {
if (cursor.firstChild()) {
@@ -72,42 +72,47 @@
if (cursor.name === 'Object') {
const elementPath = [...valuePath, index.toString()];
const elementPathStr = elementPath.join('.');
-
+
if (elementPathStr) {
pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to });
}
-
+
// Traverse into the object
if (cursor.firstChild()) {
traverse(cursor, elementPath);
cursor.parent();
}
-
+
index++;
} else if (cursor.name === 'Array') {
const elementPath = [...valuePath, index.toString()];
const elementPathStr = elementPath.join('.');
-
+
if (elementPathStr) {
pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to });
}
-
+
// Recursive array
if (cursor.firstChild()) {
traverse(cursor, elementPath);
cursor.parent();
}
-
+
index++;
- } else if (cursor.name !== '[' && cursor.name !== ']' && cursor.name !== ',' && cursor.name !== '⚠') {
+ } else if (
+ cursor.name !== '[' &&
+ cursor.name !== ']' &&
+ cursor.name !== ',' &&
+ cursor.name !== '⚠'
+ ) {
// Primitive values in array
const elementPath = [...valuePath, index.toString()];
const elementPathStr = elementPath.join('.');
-
+
if (elementPathStr) {
pathMap.set(elementPathStr, { start: cursor.from, end: cursor.to });
}
-
+
index++;
}
} while (cursor.nextSibling());
@@ -121,7 +126,7 @@
}
}
}
-
+
cursor.parent();
}
} else if (nodeName === 'Array') {
@@ -134,44 +139,49 @@
// For object elements, store the indexed path and traverse
const elementPath = [...path, index.toString()];
const pathStr = elementPath.join('.');
-
+
// Store the position of this array element
if (pathStr) {
pathMap.set(pathStr, { start: cursor.from, end: cursor.to });
}
-
+
// Traverse into the object to get its properties
if (cursor.firstChild()) {
traverse(cursor, elementPath);
cursor.parent();
}
-
+
index++;
} else if (cursor.name === 'Array') {
// For nested arrays
const elementPath = [...path, index.toString()];
const pathStr = elementPath.join('.');
-
+
if (pathStr) {
pathMap.set(pathStr, { start: cursor.from, end: cursor.to });
}
-
+
// Traverse into the nested array
if (cursor.firstChild()) {
traverse(cursor, elementPath);
cursor.parent();
}
-
+
index++;
- } else if (cursor.name !== '[' && cursor.name !== ']' && cursor.name !== ',' && cursor.name !== '⚠') {
+ } else if (
+ cursor.name !== '[' &&
+ cursor.name !== ']' &&
+ cursor.name !== ',' &&
+ cursor.name !== '⚠'
+ ) {
// For primitive values
const elementPath = [...path, index.toString()];
const pathStr = elementPath.join('.');
-
+
if (pathStr) {
pathMap.set(pathStr, { start: cursor.from, end: cursor.to });
}
-
+
index++;
}
} while (cursor.nextSibling());
@@ -198,11 +208,11 @@
}
} while (cursor.nextSibling());
}
-
+
// Start traversal
const cursor = tree.cursor();
traverse(cursor);
-
+
return pathMap;
}
@@ -212,10 +222,10 @@
try {
// Build the path to position map using syntax tree
const pathMap = buildPathToPositionMap(view.state);
-
+
// Look up the position for this path
const position = pathMap.get(path);
-
+
if (position) {
// Navigate to the found position
view.dispatch({
@@ -223,7 +233,9 @@
scrollIntoView: true
});
view.focus();
- logger.debug(`[JsonEditor] Navigated to path: ${path} at position ${position.start}-${position.end}`);
+ logger.debug(
+ `[JsonEditor] Navigated to path: ${path} at position ${position.start}-${position.end}`
+ );
} else {
logger.warn(`[JsonEditor] Path not found: ${path}`);
}
diff --git a/src/lib/components/JsonGraph.svelte b/src/lib/components/JsonGraph.svelte
index 03c8a31..496d44e 100644
--- a/src/lib/components/JsonGraph.svelte
+++ b/src/lib/components/JsonGraph.svelte
@@ -19,10 +19,9 @@
import { logger } from '$lib/logger';
import { graphLoading } from '$lib/stores/graphLoading';
// Use Vite worker plugin to ensure bundling works in all environments
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore - Vite injects a Worker constructor type via ?worker
+ // @ts-expect-error - Vite injects a Worker constructor type via ?worker
import GraphLayoutWorker from '$lib/workers/graphLayout.worker.ts?worker&module';
- import type { JsonValue, JsonObject, NodeItem, JsonStructure } from '$lib/types/json';
+ import type { JsonValue, NodeItem } from '$lib/types/json';
interface Props {
jsonData: JsonValue;
@@ -94,86 +93,6 @@
return null;
}
- // Helper function to extract JSON structure (keys only, not values)
- function getJsonStructure(obj: JsonValue | null | undefined): JsonStructure | string | null {
- if (obj === null || obj === undefined) return null;
- if (typeof obj !== 'object') return typeof obj;
-
- if (Array.isArray(obj)) {
- // For arrays, track length and structure of first element
- return {
- _type: 'array',
- _length: obj.length,
- _sample: obj.length > 0 ? getJsonStructure(obj[0]) : null
- };
- }
-
- // For objects, track keys and their structures
- const structure: JsonStructure = { _type: 'object' };
- for (const key in obj) {
- structure[key] = getJsonStructure((obj as JsonObject)[key]);
- }
- return structure;
- }
-
- // Compare two JSON structures to detect structural changes
- function hasStructuralChange(
- oldStruct: JsonStructure | string | null,
- newStruct: JsonStructure | string | null
- ): boolean {
- if (oldStruct === newStruct) return false;
- if (oldStruct === null || newStruct === null) return true;
- if (typeof oldStruct !== typeof newStruct) return true;
-
- if (typeof oldStruct === 'string') {
- return oldStruct !== newStruct; // Type change
- }
-
- if (
- typeof oldStruct === 'object' &&
- typeof newStruct === 'object' &&
- oldStruct._type === 'array' &&
- newStruct._type === 'array'
- ) {
- // Consider it structural if array length changes significantly (more than 20% or by more than 10 items)
- const oldLength = oldStruct._length ?? 0;
- const newLength = newStruct._length ?? 0;
- const lengthDiff = Math.abs(oldLength - newLength);
- const percentChange = lengthDiff / Math.max(oldLength, 1);
- if (lengthDiff > 10 || percentChange > 0.2) return true;
-
- // Check sample structure
- return hasStructuralChange(oldStruct._sample ?? null, newStruct._sample ?? null);
- }
-
- if (
- typeof oldStruct === 'object' &&
- typeof newStruct === 'object' &&
- oldStruct._type === 'object' &&
- newStruct._type === 'object'
- ) {
- // Check if keys are different
- const oldKeys = Object.keys(oldStruct).filter((k) => k !== '_type');
- const newKeys = Object.keys(newStruct).filter((k) => k !== '_type');
-
- if (oldKeys.length !== newKeys.length) return true;
-
- for (const key of oldKeys) {
- if (!newKeys.includes(key)) return true;
- const oldValue = oldStruct[key] ?? null;
- const newValue = newStruct[key] ?? null;
- if (
- hasStructuralChange(
- oldValue as JsonStructure | string | null,
- newValue as JsonStructure | string | null
- )
- )
- return true;
- }
- }
-
- return false;
- }
const measuredHeights = new Map(); // Store actual measured heights
// --- dynamic reflow when heights change ---
let reflowScheduled = false;
@@ -324,11 +243,6 @@
});
}
- // Initial node height (used until actual measurements arrive)
- function getInitialNodeHeight(): number {
- return LAYOUT_CONFIG.INITIAL_HEIGHT;
- }
-
function estimateDisplayItemCount(node: Node): number {
if (!node.data?.isExpanded) return 0;
const total = node.data.items?.length ?? 0;
diff --git a/src/lib/components/ui/checkbox/checkbox.svelte b/src/lib/components/ui/checkbox/checkbox.svelte
index 1622e05..1978a19 100644
--- a/src/lib/components/ui/checkbox/checkbox.svelte
+++ b/src/lib/components/ui/checkbox/checkbox.svelte
@@ -1,8 +1,8 @@
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index b3eb794..9f1344e 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,11 +1,10 @@
export const STORAGE_KEYS = {
- URL: 'pdjsoneditor_url',
- METHOD: 'pdjsoneditor_method',
- HEADERS: 'pdjsoneditor_headers',
- BODY: 'pdjsoneditor_body',
- RAW_BODY_MODE: 'pdjsoneditor_raw_body_mode',
- USE_EDITOR_CONTENT: 'pdjsoneditor_use_editor_content'
+ URL: 'pdjsoneditor_url',
+ METHOD: 'pdjsoneditor_method',
+ HEADERS: 'pdjsoneditor_headers',
+ BODY: 'pdjsoneditor_body',
+ RAW_BODY_MODE: 'pdjsoneditor_raw_body_mode',
+ USE_EDITOR_CONTENT: 'pdjsoneditor_use_editor_content'
} as const;
export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];
-
diff --git a/src/lib/logger.ts b/src/lib/logger.ts
index 7a18ed0..3a8abbc 100644
--- a/src/lib/logger.ts
+++ b/src/lib/logger.ts
@@ -5,24 +5,23 @@ import { dev } from '$app/environment';
type Level = 'silly' | 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
const levelMap: Record = {
- silly: 0,
- trace: 1,
- debug: 2,
- info: 3,
- warn: 4,
- error: 5,
- fatal: 6
+ silly: 0,
+ trace: 1,
+ debug: 2,
+ info: 3,
+ warn: 4,
+ error: 5,
+ fatal: 6
};
function resolveMinLevel(): number {
- const raw = env.PUBLIC_LOG_LEVEL?.toLowerCase() as Level | undefined;
- if (raw && raw in levelMap) return levelMap[raw];
- // Default: debug in dev, warn in prod
- return dev ? levelMap.debug : levelMap.warn;
+ const raw = env.PUBLIC_LOG_LEVEL?.toLowerCase() as Level | undefined;
+ if (raw && raw in levelMap) return levelMap[raw];
+ // Default: debug in dev, warn in prod
+ return dev ? levelMap.debug : levelMap.warn;
}
export const logger = new Logger({
- name: 'pdjsoneditor',
- minLevel: resolveMinLevel()
+ name: 'pdjsoneditor',
+ minLevel: resolveMinLevel()
});
-
diff --git a/src/lib/services/http.ts b/src/lib/services/http.ts
index 4784e88..3d7cd99 100644
--- a/src/lib/services/http.ts
+++ b/src/lib/services/http.ts
@@ -3,100 +3,99 @@ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
export type HeaderKV = { key: string; value: string };
function normalizeAndBuildHeaders(pairs: HeaderKV[]): Record {
- const headers: Record = {};
- for (const h of pairs) {
- if (!h?.key || !h?.value) continue;
- // Overwrite case-insensitively
- const existing = Object.keys(headers).find((k) => k.toLowerCase() === h.key.toLowerCase());
- if (existing) delete headers[existing];
- headers[h.key] = h.value;
- }
- return headers;
+ const headers: Record = {};
+ for (const h of pairs) {
+ if (!h?.key || !h?.value) continue;
+ // Overwrite case-insensitively
+ const existing = Object.keys(headers).find((k) => k.toLowerCase() === h.key.toLowerCase());
+ if (existing) delete headers[existing];
+ headers[h.key] = h.value;
+ }
+ return headers;
}
export interface RequestJsonOptions {
- method: HttpMethod;
- url: string;
- headers: HeaderKV[];
- editorJson: string;
- customBody: string;
- sendAsRawText: boolean;
- useEditorContent: boolean;
- signal?: AbortSignal;
+ method: HttpMethod;
+ url: string;
+ headers: HeaderKV[];
+ editorJson: string;
+ customBody: string;
+ sendAsRawText: boolean;
+ useEditorContent: boolean;
+ signal?: AbortSignal;
}
export interface RequestJsonResult {
- ok: boolean;
- status: number;
- data?: unknown;
- rawText?: string;
- contentType?: string | null;
+ ok: boolean;
+ status: number;
+ data?: unknown;
+ rawText?: string;
+ contentType?: string | null;
}
export async function requestJson(opts: RequestJsonOptions): Promise {
- const {
- method,
- url,
- headers: headerPairs,
- editorJson,
- customBody,
- sendAsRawText,
- useEditorContent,
- signal
- } = opts;
+ const {
+ method,
+ url,
+ headers: headerPairs,
+ editorJson,
+ customBody,
+ sendAsRawText,
+ useEditorContent,
+ signal
+ } = opts;
- const headers = normalizeAndBuildHeaders(headerPairs);
- const init: RequestInit = { method, headers, signal };
+ const headers = normalizeAndBuildHeaders(headerPairs);
+ const init: RequestInit = { method, headers, signal };
- if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
- const bodyContent = (useEditorContent ? editorJson : customBody).trim();
- if (bodyContent) {
- if (sendAsRawText) {
- (init as any).body = bodyContent;
- } else {
- // Ensure JSON body is valid and set proper content-type
- const parsed = JSON.parse(bodyContent);
- (init as any).body = JSON.stringify(parsed);
- // Upsert Content-Type to application/json
- const hasCT = Object.keys(headers).some((k) => k.toLowerCase() === 'content-type');
- if (hasCT) {
- const ctKey = Object.keys(headers).find((k) => k.toLowerCase() === 'content-type')!;
- headers[ctKey] = 'application/json';
- } else {
- headers['Content-Type'] = 'application/json';
- }
- }
- }
- }
+ if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
+ const bodyContent = (useEditorContent ? editorJson : customBody).trim();
+ if (bodyContent) {
+ if (sendAsRawText) {
+ (init as any).body = bodyContent;
+ } else {
+ // Ensure JSON body is valid and set proper content-type
+ const parsed = JSON.parse(bodyContent);
+ (init as any).body = JSON.stringify(parsed);
+ // Upsert Content-Type to application/json
+ const hasCT = Object.keys(headers).some((k) => k.toLowerCase() === 'content-type');
+ if (hasCT) {
+ const ctKey = Object.keys(headers).find((k) => k.toLowerCase() === 'content-type')!;
+ headers[ctKey] = 'application/json';
+ } else {
+ headers['Content-Type'] = 'application/json';
+ }
+ }
+ }
+ }
- const resp = await fetch(url, init);
- const result: RequestJsonResult = { ok: resp.ok, status: resp.status };
+ const resp = await fetch(url, init);
+ const result: RequestJsonResult = { ok: resp.ok, status: resp.status };
- const contentType = resp.headers.get('content-type');
- result.contentType = contentType;
+ const contentType = resp.headers.get('content-type');
+ result.contentType = contentType;
- // 204/205 or no content
- if (resp.status === 204 || resp.status === 205) {
- return result;
- }
+ // 204/205 or no content
+ if (resp.status === 204 || resp.status === 205) {
+ return result;
+ }
- const text = await resp.text();
- if (text && contentType?.toLowerCase().includes('application/json')) {
- try {
- result.data = JSON.parse(text);
- } catch {
- // Fallback to text if parsing fails
- result.rawText = text;
- }
- } else {
- // Try JSON parse anyway, else return raw text
- try {
- result.data = JSON.parse(text);
- } catch {
- result.rawText = text;
- }
- }
+ const text = await resp.text();
+ if (text && contentType?.toLowerCase().includes('application/json')) {
+ try {
+ result.data = JSON.parse(text);
+ } catch {
+ // Fallback to text if parsing fails
+ result.rawText = text;
+ }
+ } else {
+ // Try JSON parse anyway, else return raw text
+ try {
+ result.data = JSON.parse(text);
+ } catch {
+ result.rawText = text;
+ }
+ }
- return result;
+ return result;
}
-
diff --git a/src/lib/stores/graphLoading.ts b/src/lib/stores/graphLoading.ts
index 0b1b455..2fbcfed 100644
--- a/src/lib/stores/graphLoading.ts
+++ b/src/lib/stores/graphLoading.ts
@@ -3,14 +3,13 @@ import { writable } from 'svelte/store';
export type GraphPhase = 'idle' | 'build' | 'layout' | 'finalize';
export interface GraphLoadingState {
- active: boolean;
- phase: GraphPhase;
- progress: number; // 0..1
+ active: boolean;
+ phase: GraphPhase;
+ progress: number; // 0..1
}
export const graphLoading = writable({
- active: false,
- phase: 'idle',
- progress: 0
+ active: false,
+ phase: 'idle',
+ progress: 0
});
-
diff --git a/src/lib/stores/requestSettings.ts b/src/lib/stores/requestSettings.ts
index ab5b1d3..e6ad004 100644
--- a/src/lib/stores/requestSettings.ts
+++ b/src/lib/stores/requestSettings.ts
@@ -3,66 +3,77 @@ import type { HttpMethod, HeaderKV } from '$lib/services/http';
import { STORAGE_KEYS } from '$lib/constants';
export interface RequestSettingsState {
- url: string;
- method: HttpMethod;
- headers: HeaderKV[];
- body: string;
- sendAsRawText: boolean;
- useEditorContent: boolean;
+ url: string;
+ method: HttpMethod;
+ headers: HeaderKV[];
+ body: string;
+ sendAsRawText: boolean;
+ useEditorContent: boolean;
}
const getInitial = (): RequestSettingsState => {
- if (typeof window === 'undefined') {
- return {
- url: 'https://jsonplaceholder.typicode.com/todos/1',
- method: 'GET',
- headers: [],
- body: '',
- sendAsRawText: false,
- useEditorContent: false
- };
- }
+ if (typeof window === 'undefined') {
+ return {
+ url: 'https://jsonplaceholder.typicode.com/todos/1',
+ method: 'GET',
+ headers: [],
+ body: '',
+ sendAsRawText: false,
+ useEditorContent: false
+ };
+ }
- let url = localStorage.getItem(STORAGE_KEYS.URL) || 'https://jsonplaceholder.typicode.com/todos/1';
- const rawMethod = localStorage.getItem(STORAGE_KEYS.METHOD) as HttpMethod | null;
- const method: HttpMethod = rawMethod && ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(rawMethod)
- ? rawMethod
- : 'GET';
- let headers: HeaderKV[] = [];
- const savedHeaders = localStorage.getItem(STORAGE_KEYS.HEADERS);
- if (savedHeaders) {
- try { headers = JSON.parse(savedHeaders); } catch {}
- }
- const body = localStorage.getItem(STORAGE_KEYS.BODY) || '';
- let sendAsRawText = false;
- const savedRaw = localStorage.getItem(STORAGE_KEYS.RAW_BODY_MODE);
- if (savedRaw) {
- try { sendAsRawText = JSON.parse(savedRaw); } catch {}
- }
- let useEditorContent = false;
- const savedUse = localStorage.getItem(STORAGE_KEYS.USE_EDITOR_CONTENT);
- if (savedUse) {
- try { useEditorContent = JSON.parse(savedUse); } catch {}
- }
+ let url =
+ localStorage.getItem(STORAGE_KEYS.URL) || 'https://jsonplaceholder.typicode.com/todos/1';
+ const rawMethod = localStorage.getItem(STORAGE_KEYS.METHOD) as HttpMethod | null;
+ const method: HttpMethod =
+ rawMethod && ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(rawMethod) ? rawMethod : 'GET';
+ let headers: HeaderKV[] = [];
+ const savedHeaders = localStorage.getItem(STORAGE_KEYS.HEADERS);
+ if (savedHeaders) {
+ try {
+ headers = JSON.parse(savedHeaders);
+ } catch {
+ // Ignore parse errors for invalid headers data
+ }
+ }
+ const body = localStorage.getItem(STORAGE_KEYS.BODY) || '';
+ let sendAsRawText = false;
+ const savedRaw = localStorage.getItem(STORAGE_KEYS.RAW_BODY_MODE);
+ if (savedRaw) {
+ try {
+ sendAsRawText = JSON.parse(savedRaw);
+ } catch {
+ // Ignore parse errors for invalid raw body mode
+ }
+ }
+ let useEditorContent = false;
+ const savedUse = localStorage.getItem(STORAGE_KEYS.USE_EDITOR_CONTENT);
+ if (savedUse) {
+ try {
+ useEditorContent = JSON.parse(savedUse);
+ } catch {
+ // Ignore parse errors for invalid use editor content flag
+ }
+ }
- return { url, method, headers, body, sendAsRawText, useEditorContent };
+ return { url, method, headers, body, sendAsRawText, useEditorContent };
};
export const requestSettings = writable(getInitial());
// Persist on change (browser only)
if (typeof window !== 'undefined') {
- requestSettings.subscribe((s) => {
- try {
- localStorage.setItem(STORAGE_KEYS.URL, s.url);
- localStorage.setItem(STORAGE_KEYS.METHOD, s.method);
- localStorage.setItem(STORAGE_KEYS.HEADERS, JSON.stringify(s.headers ?? []));
- localStorage.setItem(STORAGE_KEYS.BODY, s.body ?? '');
- localStorage.setItem(STORAGE_KEYS.RAW_BODY_MODE, JSON.stringify(!!s.sendAsRawText));
- localStorage.setItem(STORAGE_KEYS.USE_EDITOR_CONTENT, JSON.stringify(!!s.useEditorContent));
- } catch {
- // ignore storage errors
- }
- });
+ requestSettings.subscribe((s) => {
+ try {
+ localStorage.setItem(STORAGE_KEYS.URL, s.url);
+ localStorage.setItem(STORAGE_KEYS.METHOD, s.method);
+ localStorage.setItem(STORAGE_KEYS.HEADERS, JSON.stringify(s.headers ?? []));
+ localStorage.setItem(STORAGE_KEYS.BODY, s.body ?? '');
+ localStorage.setItem(STORAGE_KEYS.RAW_BODY_MODE, JSON.stringify(!!s.sendAsRawText));
+ localStorage.setItem(STORAGE_KEYS.USE_EDITOR_CONTENT, JSON.stringify(!!s.useEditorContent));
+ } catch {
+ // ignore storage errors
+ }
+ });
}
-
diff --git a/src/lib/types/json.ts b/src/lib/types/json.ts
index 642aba6..369b253 100644
--- a/src/lib/types/json.ts
+++ b/src/lib/types/json.ts
@@ -20,4 +20,4 @@ export interface JsonStructure {
_length?: number;
_sample?: JsonStructure | string | null;
[key: string]: JsonStructure | string | number | null | undefined;
-}
\ No newline at end of file
+}
diff --git a/src/lib/workers/graphLayout.worker.ts b/src/lib/workers/graphLayout.worker.ts
index 6612f49..1290b11 100644
--- a/src/lib/workers/graphLayout.worker.ts
+++ b/src/lib/workers/graphLayout.worker.ts
@@ -5,183 +5,182 @@
import dagre from 'dagre';
type NodeData = {
- label: string;
- items?: any[];
- allItems?: any[];
- isExpanded?: boolean;
- isArray?: boolean;
- nodeId?: string;
+ label: string;
+ items?: any[];
+ allItems?: any[];
+ isExpanded?: boolean;
+ isArray?: boolean;
+ nodeId?: string;
};
type FlowNode = {
- id: string;
- data: NodeData;
- position: { x: number; y: number };
+ id: string;
+ data: NodeData;
+ position: { x: number; y: number };
};
type Edge = { id: string; source: string; target: string };
type LayoutConfig = {
- NODE_WIDTH: number;
- MAX_DISPLAY_ITEMS: number;
- METRICS: {
- NODE_PADDING_Y: number;
- NODE_BORDER_Y: number;
- HEADER_HEIGHT: number;
- ITEMS_TOP_MARGIN: number;
- ITEM_ROW_HEIGHT: number;
- MORE_BUTTON_HEIGHT: number;
- };
- DAGRE: {
- RANK_DIR: 'LR' | 'TB' | 'BT' | 'RL';
- NODE_SEP: number;
- RANK_SEP: number;
- EDGE_SEP: number;
- RANKER: 'network-simplex' | 'tight-tree' | 'longest-path';
- ALIGN: 'UL' | 'UR' | 'DL' | 'DR' | undefined;
- MARGIN_X: number;
- MARGIN_Y: number;
- };
- OVERLAP: {
- MIN_SPACING: number;
- X_TOLERANCE: number;
- };
+ NODE_WIDTH: number;
+ MAX_DISPLAY_ITEMS: number;
+ METRICS: {
+ NODE_PADDING_Y: number;
+ NODE_BORDER_Y: number;
+ HEADER_HEIGHT: number;
+ ITEMS_TOP_MARGIN: number;
+ ITEM_ROW_HEIGHT: number;
+ MORE_BUTTON_HEIGHT: number;
+ };
+ DAGRE: {
+ RANK_DIR: 'LR' | 'TB' | 'BT' | 'RL';
+ NODE_SEP: number;
+ RANK_SEP: number;
+ EDGE_SEP: number;
+ RANKER: 'network-simplex' | 'tight-tree' | 'longest-path';
+ ALIGN: 'UL' | 'UR' | 'DL' | 'DR' | undefined;
+ MARGIN_X: number;
+ MARGIN_Y: number;
+ };
+ OVERLAP: {
+ MIN_SPACING: number;
+ X_TOLERANCE: number;
+ };
};
type LayoutRequest = {
- type: 'layout';
- nodes: FlowNode[];
- edges: Edge[];
- measuredHeights: Array<[string, number]>; // entries of Map
- showAllItemsNodeIds: string[];
- config: LayoutConfig;
+ type: 'layout';
+ nodes: FlowNode[];
+ edges: Edge[];
+ measuredHeights: Array<[string, number]>; // entries of Map
+ showAllItemsNodeIds: string[];
+ config: LayoutConfig;
};
type ProgressMsg = { type: 'progress'; phase: 'build' | 'layout'; progress: number };
type DoneMsg = { type: 'done'; nodes: FlowNode[] };
-type WorkerMsg = ProgressMsg | DoneMsg;
const ctx: any = self;
function estimateDisplayItemCount(node: FlowNode, cfg: LayoutConfig, showAll: boolean): number {
- if (!node.data?.isExpanded) return 0;
- const total = node.data.items?.length ?? 0;
- if (showAll) return total;
- return Math.min(total, cfg.MAX_DISPLAY_ITEMS);
+ if (!node.data?.isExpanded) return 0;
+ const total = node.data.items?.length ?? 0;
+ if (showAll) return total;
+ return Math.min(total, cfg.MAX_DISPLAY_ITEMS);
}
function hasMoreButton(node: FlowNode, cfg: LayoutConfig, showAll: boolean): boolean {
- if (!node.data?.isExpanded) return false;
- const total = node.data.items?.length ?? 0;
- return total > cfg.MAX_DISPLAY_ITEMS && !showAll;
+ if (!node.data?.isExpanded) return false;
+ const total = node.data.items?.length ?? 0;
+ return total > cfg.MAX_DISPLAY_ITEMS && !showAll;
}
function estimateNodeHeight(node: FlowNode, cfg: LayoutConfig, showAll: boolean): number {
- const M = cfg.METRICS;
- let h = M.NODE_PADDING_Y + M.NODE_BORDER_Y + M.HEADER_HEIGHT;
- if (node.data?.isExpanded) {
- const count = estimateDisplayItemCount(node, cfg, showAll);
- const extraBtn = hasMoreButton(node, cfg, showAll) ? M.MORE_BUTTON_HEIGHT : 0;
- h += M.ITEMS_TOP_MARGIN + count * M.ITEM_ROW_HEIGHT + extraBtn;
- }
- return Math.max(h, 32);
+ const M = cfg.METRICS;
+ let h = M.NODE_PADDING_Y + M.NODE_BORDER_Y + M.HEADER_HEIGHT;
+ if (node.data?.isExpanded) {
+ const count = estimateDisplayItemCount(node, cfg, showAll);
+ const extraBtn = hasMoreButton(node, cfg, showAll) ? M.MORE_BUTTON_HEIGHT : 0;
+ h += M.ITEMS_TOP_MARGIN + count * M.ITEM_ROW_HEIGHT + extraBtn;
+ }
+ return Math.max(h, 32);
}
ctx.onmessage = (ev: MessageEvent) => {
- const msg = ev.data;
- if (msg.type !== 'layout') return;
-
- const { nodes, edges, measuredHeights, showAllItemsNodeIds, config } = msg;
-
- const measuredMap = new Map(measuredHeights);
- const showAllSet = new Set(showAllItemsNodeIds);
-
- const graph = new dagre.graphlib.Graph();
- graph.setDefaultEdgeLabel(() => ({}));
-
- graph.setGraph({
- rankdir: config.DAGRE.RANK_DIR,
- nodesep: config.DAGRE.NODE_SEP,
- ranksep: config.DAGRE.RANK_SEP,
- edgesep: config.DAGRE.EDGE_SEP,
- ranker: config.DAGRE.RANKER,
- align: config.DAGRE.ALIGN,
- marginx: config.DAGRE.MARGIN_X,
- marginy: config.DAGRE.MARGIN_Y
- });
-
- const total = nodes.length;
- let processed = 0;
- const report = (phase: 'build' | 'layout', p: number) => {
- ctx.postMessage({ type: 'progress', phase, progress: p } as ProgressMsg);
- };
-
- // Build nodes
- for (const n of nodes) {
- const measured = measuredMap.get(n.id);
- const showAll = showAllSet.has(n.id);
- const height = measured ?? estimateNodeHeight(n, config, showAll);
- graph.setNode(n.id, { width: config.NODE_WIDTH, height });
- processed++;
- if (processed % 200 === 0) report('build', Math.min(0.9, processed / Math.max(1, total)));
- }
- // Build edges
- for (const e of edges) {
- graph.setEdge(e.source, e.target);
- }
- report('build', 1);
-
- // Run layout
- report('layout', 0);
- dagre.layout(graph);
- report('layout', 1);
-
- // Map positions back
- const result: FlowNode[] = nodes.map((node) => {
- const g = graph.node(node.id) as dagre.Node;
- const positioned: FlowNode = {
- ...node,
- position: { x: g.x, y: g.y }
- };
- return positioned;
- });
-
- // Column overlap adjustment
- const nodesByColumn = new Map>();
- for (const node of result) {
- const g = graph.node(node.id) as dagre.Node;
- if (!g) continue;
- let foundKey: number | null = null;
- for (const key of nodesByColumn.keys()) {
- if (Math.abs(g.x - key) <= config.OVERLAP.X_TOLERANCE) {
- foundKey = key;
- break;
- }
- }
- if (foundKey !== null) nodesByColumn.get(foundKey)!.push({ node, g });
- else nodesByColumn.set(g.x, [{ node, g }]);
- }
-
- nodesByColumn.forEach((nodesInColumn) => {
- if (nodesInColumn.length > 1) {
- nodesInColumn.sort((a, b) => a.node.position.y - b.node.position.y);
- for (let i = 1; i < nodesInColumn.length; i++) {
- const prev = nodesInColumn[i - 1];
- const curr = nodesInColumn[i];
- const prevBottom = prev.node.position.y + prev.g.height / 2;
- const currTop = curr.node.position.y - curr.g.height / 2;
- const minSpacing = config.OVERLAP.MIN_SPACING;
- if (currTop < prevBottom + minSpacing) {
- const adj = prevBottom + minSpacing - currTop;
- curr.node.position.y += adj;
- for (let j = i + 1; j < nodesInColumn.length; j++) nodesInColumn[j].node.position.y += adj;
- }
- }
- }
- });
-
- ctx.postMessage({ type: 'done', nodes: result } as DoneMsg);
+ const msg = ev.data;
+ if (msg.type !== 'layout') return;
+
+ const { nodes, edges, measuredHeights, showAllItemsNodeIds, config } = msg;
+
+ const measuredMap = new Map(measuredHeights);
+ const showAllSet = new Set(showAllItemsNodeIds);
+
+ const graph = new dagre.graphlib.Graph();
+ graph.setDefaultEdgeLabel(() => ({}));
+
+ graph.setGraph({
+ rankdir: config.DAGRE.RANK_DIR,
+ nodesep: config.DAGRE.NODE_SEP,
+ ranksep: config.DAGRE.RANK_SEP,
+ edgesep: config.DAGRE.EDGE_SEP,
+ ranker: config.DAGRE.RANKER,
+ align: config.DAGRE.ALIGN,
+ marginx: config.DAGRE.MARGIN_X,
+ marginy: config.DAGRE.MARGIN_Y
+ });
+
+ const total = nodes.length;
+ let processed = 0;
+ const report = (phase: 'build' | 'layout', p: number) => {
+ ctx.postMessage({ type: 'progress', phase, progress: p } as ProgressMsg);
+ };
+
+ // Build nodes
+ for (const n of nodes) {
+ const measured = measuredMap.get(n.id);
+ const showAll = showAllSet.has(n.id);
+ const height = measured ?? estimateNodeHeight(n, config, showAll);
+ graph.setNode(n.id, { width: config.NODE_WIDTH, height });
+ processed++;
+ if (processed % 200 === 0) report('build', Math.min(0.9, processed / Math.max(1, total)));
+ }
+ // Build edges
+ for (const e of edges) {
+ graph.setEdge(e.source, e.target);
+ }
+ report('build', 1);
+
+ // Run layout
+ report('layout', 0);
+ dagre.layout(graph);
+ report('layout', 1);
+
+ // Map positions back
+ const result: FlowNode[] = nodes.map((node) => {
+ const g = graph.node(node.id) as dagre.Node;
+ const positioned: FlowNode = {
+ ...node,
+ position: { x: g.x, y: g.y }
+ };
+ return positioned;
+ });
+
+ // Column overlap adjustment
+ const nodesByColumn = new Map>();
+ for (const node of result) {
+ const g = graph.node(node.id) as dagre.Node;
+ if (!g) continue;
+ let foundKey: number | null = null;
+ for (const key of nodesByColumn.keys()) {
+ if (Math.abs(g.x - key) <= config.OVERLAP.X_TOLERANCE) {
+ foundKey = key;
+ break;
+ }
+ }
+ if (foundKey !== null) nodesByColumn.get(foundKey)!.push({ node, g });
+ else nodesByColumn.set(g.x, [{ node, g }]);
+ }
+
+ nodesByColumn.forEach((nodesInColumn) => {
+ if (nodesInColumn.length > 1) {
+ nodesInColumn.sort((a, b) => a.node.position.y - b.node.position.y);
+ for (let i = 1; i < nodesInColumn.length; i++) {
+ const prev = nodesInColumn[i - 1];
+ const curr = nodesInColumn[i];
+ const prevBottom = prev.node.position.y + prev.g.height / 2;
+ const currTop = curr.node.position.y - curr.g.height / 2;
+ const minSpacing = config.OVERLAP.MIN_SPACING;
+ if (currTop < prevBottom + minSpacing) {
+ const adj = prevBottom + minSpacing - currTop;
+ curr.node.position.y += adj;
+ for (let j = i + 1; j < nodesInColumn.length; j++)
+ nodesInColumn[j].node.position.y += adj;
+ }
+ }
+ }
+ });
+
+ ctx.postMessage({ type: 'done', nodes: result } as DoneMsg);
};
export {};
-
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 768dba0..7600406 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -2,7 +2,7 @@
import { onMount } from 'svelte';
import JsonEditor from '$lib/components/JsonEditor.svelte';
import JsonGraph from '$lib/components/JsonGraph.svelte';
- import { Button } from '$lib/components/ui/button';
+ import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Dialog from '$lib/components/ui/dialog';
@@ -98,12 +98,12 @@
let editorRef: JsonEditor;
let parseTimeout: ReturnType;
- // LocalStorage keys are centralized in $lib/constants
+ // LocalStorage keys are centralized in $lib/constants
// Initialize with empty values (will be populated from localStorage in onMount)
let urlInput = $state('https://jsonplaceholder.typicode.com/todos/1');
let isLoading = $state(false);
- let httpMethod = $state('GET');
+ let httpMethod = $state('GET');
let isDialogOpen = $state(false);
let customHeaders = $state>([]);
let tempHeaders = $state>([]);
@@ -116,10 +116,10 @@
let httpStatusCode = $state(null);
let responseTime = $state(null);
- const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
+ const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
- // Track in-flight request for cancellation
- let abortController: AbortController | null = null;
+ // Track in-flight request for cancellation
+ let abortController: AbortController | null = null;
function openSettingsDialog() {
// Copy current headers to temp, ensure it's a proper array
@@ -162,7 +162,7 @@
function clearAllSettings() {
if (confirm($LL.editor.clearAllConfirm())) {
// Clear all localStorage keys
- Object.values(STORAGE_KEYS).forEach(key => {
+ Object.values(STORAGE_KEYS).forEach((key) => {
localStorage.removeItem(key);
});
@@ -255,70 +255,70 @@
}
}
- async function fetchJsonFromUrl() {
- if (!urlInput.trim()) {
- error = $LL.editor.urlRequired();
- return;
- }
-
- isLoading = true;
- // Only clear error if it's a fetch-related error
- if (error && (error.includes('fetch') || error.includes('HTTP'))) {
- error = '';
- }
- httpStatusCode = null;
- responseTime = null;
-
- try {
- // Cancel previous request if any
- if (abortController) {
- abortController.abort();
- }
- abortController = new AbortController();
-
- const startTime = performance.now();
- const res = await requestJson({
- method: httpMethod as HttpMethod,
- url: urlInput,
- headers: customHeaders,
- editorJson: jsonValue,
- customBody,
- sendAsRawText,
- useEditorContent,
- signal: abortController.signal
- });
- const endTime = performance.now();
- responseTime = Math.round(endTime - startTime);
- httpStatusCode = res.status;
-
- if (res.data !== undefined) {
- jsonValue = JSON.stringify(res.data, null, 2);
- } else if (res.rawText !== undefined) {
- // Non-JSON 응답은 텍스트로 보여줌
- jsonValue = JSON.stringify({ response: res.rawText }, null, 2);
- }
-
- if (res.ok && error && (error.includes('fetch') || error.includes('HTTP'))) {
- error = '';
- }
- } catch (e) {
- if ((e as any)?.name === 'AbortError') {
- // silently ignore aborted request
- return;
- }
- if (e instanceof Error) {
- if (e.message.includes('Failed to fetch')) {
- error = $LL.editor.fetchError();
- } else {
- error = e.message;
- }
- } else {
- error = $LL.editor.fetchError();
- }
- } finally {
- isLoading = false;
- }
- }
+ async function fetchJsonFromUrl() {
+ if (!urlInput.trim()) {
+ error = $LL.editor.urlRequired();
+ return;
+ }
+
+ isLoading = true;
+ // Only clear error if it's a fetch-related error
+ if (error && (error.includes('fetch') || error.includes('HTTP'))) {
+ error = '';
+ }
+ httpStatusCode = null;
+ responseTime = null;
+
+ try {
+ // Cancel previous request if any
+ if (abortController) {
+ abortController.abort();
+ }
+ abortController = new AbortController();
+
+ const startTime = performance.now();
+ const res = await requestJson({
+ method: httpMethod as HttpMethod,
+ url: urlInput,
+ headers: customHeaders,
+ editorJson: jsonValue,
+ customBody,
+ sendAsRawText,
+ useEditorContent,
+ signal: abortController.signal
+ });
+ const endTime = performance.now();
+ responseTime = Math.round(endTime - startTime);
+ httpStatusCode = res.status;
+
+ if (res.data !== undefined) {
+ jsonValue = JSON.stringify(res.data, null, 2);
+ } else if (res.rawText !== undefined) {
+ // Non-JSON 응답은 텍스트로 보여줌
+ jsonValue = JSON.stringify({ response: res.rawText }, null, 2);
+ }
+
+ if (res.ok && error && (error.includes('fetch') || error.includes('HTTP'))) {
+ error = '';
+ }
+ } catch (e) {
+ if ((e as any)?.name === 'AbortError') {
+ // silently ignore aborted request
+ return;
+ }
+ if (e instanceof Error) {
+ if (e.message.includes('Failed to fetch')) {
+ error = $LL.editor.fetchError();
+ } else {
+ error = e.message;
+ }
+ } else {
+ error = $LL.editor.fetchError();
+ }
+ } finally {
+ isLoading = false;
+ }
+ }
function handleUrlKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
@@ -625,7 +625,7 @@