From f473564f515f86fbd3d5e02d30bc741c5da9e87e Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Thu, 18 Jun 2026 11:11:10 -0700 Subject: [PATCH 1/2] feat(tools): add toml structured data conversion --- package-lock.json | 13 +++ package.json | 1 + src/core/i18n/translations/de.json | 18 +++- src/core/i18n/translations/en.json | 18 +++- src/core/i18n/translations/fr.json | 24 +++-- src/core/i18n/translations/ja.json | 18 +++- src/core/i18n/translations/ko.json | 18 +++- src/core/i18n/translations/zh-CN.json | 18 +++- src/core/i18n/translations/zh-TW.json | 18 +++- .../tools/yaml-json-converter/manifest.ts | 6 +- .../tools/yaml-json-converter/page.tsx | 97 +++++++++++++++---- .../tools/yaml-json-converter/utils.ts | 42 +++++++- tests/unit/yaml-json-converter-utils.test.ts | 30 +++++- 13 files changed, 270 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index e418fe9f..31b00869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", + "smol-toml": "^1.6.1", "sonner": "^2.0.7", "sql-formatter": "^15.7.2", "tailwind-merge": "^3.5.0", @@ -12513,6 +12514,18 @@ "node": ">= 10" } }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", diff --git a/package.json b/package.json index 6c29d983..a4d6d940 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", + "smol-toml": "^1.6.1", "sonner": "^2.0.7", "sql-formatter": "^15.7.2", "tailwind-merge": "^3.5.0", diff --git a/src/core/i18n/translations/de.json b/src/core/i18n/translations/de.json index 08f3ca33..d20df3f6 100644 --- a/src/core/i18n/translations/de.json +++ b/src/core/i18n/translations/de.json @@ -406,15 +406,16 @@ "error_minify_css": "CSS konnte nicht minimiert werden. Syntax prüfen und erneut versuchen." }, "yaml_json_converter": { - "title": "YAML/JSON-Konverter", - "description": "Konvertieren Sie YAML in JSON und JSON in YAML.", + "title": "JSON/YAML/TOML-Konverter", + "description": "Konvertieren Sie JSON-, YAML- und TOML-Konfigurationsdaten lokal im Browser.", "import_file": "Datei importieren", "convert_action": "Konvertieren", "drag_drop_import_hint": "Textdatei hierher ziehen oder eine Datei zum Importieren auswählen.", "import_failed": "Datei konnte nicht importiert werden.", "copied_desc": "Die konvertierte Ausgabe wurde kopiert.", "error_yaml_to_json": "YAML konnte nicht in JSON konvertiert werden. Bitte Eingabe prüfen und erneut versuchen.", - "error_json_to_yaml": "JSON konnte nicht in YAML konvertiert werden. Bitte Eingabe prüfen und erneut versuchen." + "error_json_to_yaml": "JSON konnte nicht in YAML konvertiert werden. Bitte Eingabe prüfen und erneut versuchen.", + "error_convert": "Die ausgewählten Formate konnten nicht konvertiert werden. Prüfen Sie die Syntax und versuchen Sie es erneut." }, "markdown_preview": { "title": "Markdown-Vorschau", @@ -2540,6 +2541,17 @@ "category": "Kategorie", "flags": "Markierungen", "truncated_warning": "Die Eingabe überschreitet das lokale Prüflimit. Zeichenzeilen, Statistiken und kopiertes JSON basieren nur auf dem geprüften Ausschnitt." + }, + "base_encoding_converter": { + "title": "Base32/Base58-Konverter", + "description": "Kodieren und dekodieren Sie Base32- und Base58-Text lokal, ohne Daten hochzuladen.", + "encoding_label": "Kodierung", + "operation_label": "Aktion", + "encode": "Kodieren", + "decode": "Dekodieren", + "convert": "Konvertieren", + "invalid_base32": "Ungültige Base32-Eingabe. Prüfen Sie Zeichen und Padding.", + "invalid_base58": "Ungültige Base58-Eingabe. Base58 schließt 0, O, I und l aus." } }, "categories": { diff --git a/src/core/i18n/translations/en.json b/src/core/i18n/translations/en.json index 89ee02ac..66c48a2c 100644 --- a/src/core/i18n/translations/en.json +++ b/src/core/i18n/translations/en.json @@ -434,15 +434,16 @@ "download_html_success": "Saved HTML file:" }, "yaml_json_converter": { - "title": "YAML/JSON Converter", - "description": "Convert bidirectionally between YAML and JSON seamlessly.", + "title": "JSON/YAML/TOML Converter", + "description": "Convert JSON, YAML, and TOML configuration data locally in your browser.", "import_file": "Import File", "convert_action": "Convert", "drag_drop_import_hint": "Drag and drop a text file here, or choose a file to import.", "import_failed": "Unable to import file.", "copied_desc": "The converted output has been copied.", "error_yaml_to_json": "Unable to convert YAML to JSON. Check the input and try again.", - "error_json_to_yaml": "Unable to convert JSON to YAML. Check the input and try again." + "error_json_to_yaml": "Unable to convert JSON to YAML. Check the input and try again.", + "error_convert": "Unable to convert between the selected formats. Check syntax and try again." }, "markdown_preview": { "title": "Markdown Preview", @@ -2560,6 +2561,17 @@ "category": "Category", "flags": "Flags", "truncated_warning": "Input exceeds the local inspection budget. Character rows, stats, and copied JSON are based on the inspected subset." + }, + "base_encoding_converter": { + "title": "Base32/Base58 Converter", + "description": "Encode and decode Base32 and Base58 text locally without uploading data.", + "encoding_label": "Encoding", + "operation_label": "Operation", + "encode": "Encode", + "decode": "Decode", + "convert": "Convert", + "invalid_base32": "Invalid Base32 input. Check characters and padding.", + "invalid_base58": "Invalid Base58 input. Base58 excludes 0, O, I, and l." } }, "pages": { diff --git a/src/core/i18n/translations/fr.json b/src/core/i18n/translations/fr.json index 29a8a01c..a803a5d4 100644 --- a/src/core/i18n/translations/fr.json +++ b/src/core/i18n/translations/fr.json @@ -406,15 +406,16 @@ "error_minify_css": "Impossible de minifier le CSS. Vérifiez la syntaxe puis réessayez." }, "yaml_json_converter": { - "title": "Convertisseur YAML/JSON", - "description": "Convertissez YAML en JSON et JSON en YAML instantanément.", + "title": "Convertisseur JSON/YAML/TOML", + "description": "Convertissez des données de configuration JSON, YAML et TOML localement dans le navigateur.", "import_file": "Importer un fichier", "convert_action": "Convertir", - "drag_drop_import_hint": "Glissez un fichier texte ici ou choisissez un fichier a importer.", + "drag_drop_import_hint": "Glissez un fichier texte ici ou choisissez un fichier à importer.", "import_failed": "Impossible d'importer le fichier.", - "copied_desc": "La sortie convertie a ete copiee.", - "error_yaml_to_json": "Impossible de convertir YAML en JSON. Verifiez l'entree puis reessayez.", - "error_json_to_yaml": "Impossible de convertir JSON en YAML. Verifiez l'entree puis reessayez." + "copied_desc": "La sortie convertie a été copiée.", + "error_yaml_to_json": "Impossible de convertir YAML en JSON. Vérifiez l’entrée puis réessayez.", + "error_json_to_yaml": "Impossible de convertir JSON en YAML. Vérifiez l’entrée puis réessayez.", + "error_convert": "Impossible de convertir entre les formats sélectionnés. Vérifiez la syntaxe puis réessayez." }, "markdown_preview": { "title": "Aperçu Markdown", @@ -2540,6 +2541,17 @@ "category": "Catégorie", "flags": "Indicateurs", "truncated_warning": "L'entrée dépasse le budget d'inspection local. Les lignes de caractères, les statistiques et le JSON copié reposent sur le sous-ensemble inspecté." + }, + "base_encoding_converter": { + "title": "Convertisseur Base32/Base58", + "description": "Encodez et décodez du texte Base32 et Base58 localement, sans téléverser de données.", + "encoding_label": "Encodage", + "operation_label": "Opération", + "encode": "Encoder", + "decode": "Décoder", + "convert": "Convertir", + "invalid_base32": "Entrée Base32 invalide. Vérifiez les caractères et le padding.", + "invalid_base58": "Entrée Base58 invalide. Base58 exclut 0, O, I et l." } }, "categories": { diff --git a/src/core/i18n/translations/ja.json b/src/core/i18n/translations/ja.json index 6b664372..fed5a9c5 100644 --- a/src/core/i18n/translations/ja.json +++ b/src/core/i18n/translations/ja.json @@ -406,15 +406,16 @@ "error_minify_css": "CSS を圧縮できませんでした。構文を確認して再試行してください。" }, "yaml_json_converter": { - "title": "YAML/JSON 変換", - "description": "YAML と JSON を相互変換します。", + "title": "JSON/YAML/TOML 変換", + "description": "JSON、YAML、TOML の設定データをブラウザ内でローカル変換します。", "import_file": "ファイルを読み込む", "convert_action": "変換", "drag_drop_import_hint": "テキストファイルをここにドラッグするか、ファイルを選択して読み込んでください。", "import_failed": "ファイルを読み込めませんでした。", "copied_desc": "変換結果をコピーしました。", "error_yaml_to_json": "YAML を JSON に変換できませんでした。入力を確認して再試行してください。", - "error_json_to_yaml": "JSON を YAML に変換できませんでした。入力を確認して再試行してください。" + "error_json_to_yaml": "JSON を YAML に変換できませんでした。入力を確認して再試行してください。", + "error_convert": "選択した形式間で変換できません。構文を確認してもう一度試してください。" }, "markdown_preview": { "title": "Markdown プレビュー", @@ -2540,6 +2541,17 @@ "category": "カテゴリ", "flags": "フラグ", "truncated_warning": "入力がローカル検査の上限を超えています。文字行、統計、コピーされる JSON は検査済みの部分だけに基づきます。" + }, + "base_encoding_converter": { + "title": "Base32/Base58 変換", + "description": "Base32 と Base58 のテキストをアップロードせずにローカルでエンコード、デコードします。", + "encoding_label": "エンコード形式", + "operation_label": "操作", + "encode": "エンコード", + "decode": "デコード", + "convert": "変換", + "invalid_base32": "Base32 入力が無効です。文字とパディングを確認してください。", + "invalid_base58": "Base58 入力が無効です。Base58 では 0、O、I、l は使いません。" } }, "categories": { diff --git a/src/core/i18n/translations/ko.json b/src/core/i18n/translations/ko.json index 6a5e2112..54ed5eb8 100644 --- a/src/core/i18n/translations/ko.json +++ b/src/core/i18n/translations/ko.json @@ -406,15 +406,16 @@ "error_minify_css": "CSS 최소화에 실패했습니다. 구문을 확인 후 다시 시도하세요." }, "yaml_json_converter": { - "title": "YAML/JSON 변환기", - "description": "YAML과 JSON을 즉시 상호 변환합니다.", + "title": "JSON/YAML/TOML 변환기", + "description": "JSON, YAML, TOML 설정 데이터를 브라우저에서 로컬로 변환합니다.", "import_file": "파일 가져오기", "convert_action": "변환", "drag_drop_import_hint": "텍스트 파일을 여기로 끌어다 놓거나 파일을 선택해 가져오세요.", "import_failed": "파일을 가져올 수 없습니다.", "copied_desc": "변환 결과를 복사했습니다.", "error_yaml_to_json": "YAML을 JSON으로 변환할 수 없습니다. 입력을 확인한 뒤 다시 시도하세요.", - "error_json_to_yaml": "JSON을 YAML로 변환할 수 없습니다. 입력을 확인한 뒤 다시 시도하세요." + "error_json_to_yaml": "JSON을 YAML로 변환할 수 없습니다. 입력을 확인한 뒤 다시 시도하세요.", + "error_convert": "선택한 형식 간 변환에 실패했습니다. 구문을 확인하고 다시 시도하세요." }, "markdown_preview": { "title": "Markdown 미리보기", @@ -2540,6 +2541,17 @@ "category": "분류", "flags": "플래그", "truncated_warning": "입력이 로컬 검사 예산을 초과했습니다. 문자 행, 통계, 복사되는 JSON은 검사된 하위 집합을 기준으로 합니다." + }, + "base_encoding_converter": { + "title": "Base32/Base58 변환기", + "description": "Base32와 Base58 텍스트를 업로드 없이 로컬에서 인코딩하고 디코딩합니다.", + "encoding_label": "인코딩", + "operation_label": "작업", + "encode": "인코딩", + "decode": "디코딩", + "convert": "변환", + "invalid_base32": "Base32 입력이 올바르지 않습니다. 문자와 패딩을 확인하세요.", + "invalid_base58": "Base58 입력이 올바르지 않습니다. Base58은 0, O, I, l을 제외합니다." } }, "categories": { diff --git a/src/core/i18n/translations/zh-CN.json b/src/core/i18n/translations/zh-CN.json index dd8eb154..590bbd30 100644 --- a/src/core/i18n/translations/zh-CN.json +++ b/src/core/i18n/translations/zh-CN.json @@ -406,15 +406,16 @@ "error_minify_css": "CSS 压缩失败,请检查语法后重试。" }, "yaml_json_converter": { - "title": "YAML/JSON 转换器", - "description": "在 YAML 和 JSON 格式之间相互转换。", + "title": "JSON/YAML/TOML 转换器", + "description": "在浏览器本地转换 JSON、YAML 和 TOML 配置数据。", "import_file": "导入文件", "convert_action": "转换", "drag_drop_import_hint": "将文本文件拖到这里,或选择文件导入。", "import_failed": "无法导入文件。", "copied_desc": "转换结果已复制。", "error_yaml_to_json": "无法将 YAML 转为 JSON。请检查输入后重试。", - "error_json_to_yaml": "无法将 JSON 转为 YAML。请检查输入后重试。" + "error_json_to_yaml": "无法将 JSON 转为 YAML。请检查输入后重试。", + "error_convert": "无法在所选格式之间转换。请检查语法后重试。" }, "markdown_preview": { "title": "Markdown 预览", @@ -2540,6 +2541,17 @@ "category": "类别", "flags": "标记", "truncated_warning": "输入超过本地检查预算。字符行、统计和复制的 JSON 仅基于已检查的子集。" + }, + "base_encoding_converter": { + "title": "Base32/Base58 转换器", + "description": "在本地编码和解码 Base32、Base58 文本,无需上传数据。", + "encoding_label": "编码", + "operation_label": "操作", + "encode": "编码", + "decode": "解码", + "convert": "转换", + "invalid_base32": "Base32 输入无效。请检查字符和填充。", + "invalid_base58": "Base58 输入无效。Base58 不包含 0、O、I 和 l。" } }, "categories": { diff --git a/src/core/i18n/translations/zh-TW.json b/src/core/i18n/translations/zh-TW.json index e9085bf5..a2649ed7 100644 --- a/src/core/i18n/translations/zh-TW.json +++ b/src/core/i18n/translations/zh-TW.json @@ -406,15 +406,16 @@ "error_minify_css": "CSS 壓縮失敗,請檢查語法後重試。" }, "yaml_json_converter": { - "title": "YAML/JSON 轉換器", - "description": "在 YAML 和 JSON 格式之間互相轉換。", + "title": "JSON/YAML/TOML 轉換器", + "description": "在瀏覽器本機轉換 JSON、YAML 和 TOML 設定資料。", "import_file": "匯入檔案", "convert_action": "轉換", "drag_drop_import_hint": "將文字檔拖到這裡,或選擇檔案匯入。", "import_failed": "無法匯入檔案。", "copied_desc": "轉換結果已複製。", "error_yaml_to_json": "無法將 YAML 轉為 JSON。請檢查輸入後再試。", - "error_json_to_yaml": "無法將 JSON 轉為 YAML。請檢查輸入後再試。" + "error_json_to_yaml": "無法將 JSON 轉為 YAML。請檢查輸入後再試。", + "error_convert": "無法在所選格式之間轉換。請檢查語法後再試。" }, "markdown_preview": { "title": "Markdown 預覽", @@ -2540,6 +2541,17 @@ "category": "類別", "flags": "標記", "truncated_warning": "輸入超過本機檢查預算。字元列、統計和複製的 JSON 僅基於已檢查的子集。" + }, + "base_encoding_converter": { + "title": "Base32/Base58 轉換器", + "description": "在本機編碼與解碼 Base32、Base58 文字,無需上傳資料。", + "encoding_label": "編碼", + "operation_label": "操作", + "encode": "編碼", + "decode": "解碼", + "convert": "轉換", + "invalid_base32": "Base32 輸入無效。請檢查字元與填充。", + "invalid_base58": "Base58 輸入無效。Base58 不包含 0、O、I 和 l。" } }, "categories": { diff --git a/src/features/tools/yaml-json-converter/manifest.ts b/src/features/tools/yaml-json-converter/manifest.ts index f00a18b8..6e80280f 100644 --- a/src/features/tools/yaml-json-converter/manifest.ts +++ b/src/features/tools/yaml-json-converter/manifest.ts @@ -4,7 +4,7 @@ export const toolManifest = { key: "yaml_json_converter", slug: "yaml-json-converter", category: "formatters", - relatedTools: ["json_formatter", "json_to_typescript", "jsonpath_playground", "xml_formatter"], - keywords: ["yaml to json", "json to yaml", "yaml converter online", "yaml json transform"], - searchKeywords: ["convert yaml", "yaml parser", "json converter", "YAML转换", "YAML変換", "YAML 변환", "配置转换"], + relatedTools: ["json_formatter", "json_to_typescript", "structured_data_visualizer", "xml_formatter"], + keywords: ["yaml to json", "json to yaml", "toml to json", "json to toml", "yaml json toml converter"], + searchKeywords: ["convert yaml", "convert toml", "toml parser", "json converter", "YAML转换", "TOML转换", "YAML変換", "TOML変換", "YAML 변환", "TOML 변환", "配置转换"], } satisfies ToolMeta diff --git a/src/features/tools/yaml-json-converter/page.tsx b/src/features/tools/yaml-json-converter/page.tsx index d4a6149b..44d4b292 100644 --- a/src/features/tools/yaml-json-converter/page.tsx +++ b/src/features/tools/yaml-json-converter/page.tsx @@ -12,10 +12,17 @@ import { readStorageString, removeStorageKey, writeStorageString } from "@/core/ import { buildToolHandoffLink } from "@/core/routing/tool-handoff" import { importTextFile, TEXT_FILE_IMPORT_ACCEPT } from "@/core/files/text-file-import" import { safeClipboardWrite } from "@/core/clipboard/clipboard" -import { convertYamlJson, type YamlJsonMode } from "./utils" +import { convertStructuredData, type StructuredDataFormat } from "./utils" const INPUT_STORAGE_KEY = "byteflow:yaml-json-converter:input" const MODE_STORAGE_KEY = "byteflow:yaml-json-converter:mode" +const FROM_FORMAT_STORAGE_KEY = "byteflow:yaml-json-converter:from-format" +const TO_FORMAT_STORAGE_KEY = "byteflow:yaml-json-converter:to-format" +const FORMAT_OPTIONS: StructuredDataFormat[] = ["yaml", "json", "toml"] + +function monacoLanguage(format: StructuredDataFormat) { + return format === "toml" ? "ini" : format +} export function YamlJsonConverterPage() { const { t, lang } = useLang() @@ -23,7 +30,8 @@ export function YamlJsonConverterPage() { const text = React.useCallback((key: string) => toolT[key], [toolT]) const [input, setInput] = React.useState("") const [output, setOutput] = React.useState("") - const [mode, setMode] = React.useState("yaml-to-json") + const [fromFormat, setFromFormat] = React.useState("yaml") + const [toFormat, setToFormat] = React.useState("json") const [error, setError] = React.useState(null) const [importError, setImportError] = React.useState(null) const [isImportDragActive, setIsImportDragActive] = React.useState(false) @@ -37,9 +45,23 @@ export function YamlJsonConverterPage() { setInput(savedInput) } + const savedFromFormat = readStorageString(FROM_FORMAT_STORAGE_KEY) + const savedToFormat = readStorageString(TO_FORMAT_STORAGE_KEY) + if (savedFromFormat && FORMAT_OPTIONS.includes(savedFromFormat as StructuredDataFormat)) { + setFromFormat(savedFromFormat as StructuredDataFormat) + } + if (savedToFormat && FORMAT_OPTIONS.includes(savedToFormat as StructuredDataFormat)) { + setToFormat(savedToFormat as StructuredDataFormat) + return + } + const savedMode = readStorageString(MODE_STORAGE_KEY) - if (savedMode === "yaml-to-json" || savedMode === "json-to-yaml") { - setMode(savedMode) + if (savedMode === "yaml-to-json") { + setFromFormat("yaml") + setToFormat("json") + } else if (savedMode === "json-to-yaml") { + setFromFormat("json") + setToFormat("yaml") } }, []) @@ -49,12 +71,13 @@ export function YamlJsonConverterPage() { }, [input]) React.useEffect(() => { - writeStorageString(MODE_STORAGE_KEY, mode) - }, [mode]) + writeStorageString(FROM_FORMAT_STORAGE_KEY, fromFormat) + writeStorageString(TO_FORMAT_STORAGE_KEY, toFormat) + }, [fromFormat, toFormat]) - const toggleMode = () => { - setMode(prev => prev === "yaml-to-json" ? "json-to-yaml" : "yaml-to-json") - // Optionally swap input/output contents + const swapFormats = () => { + setFromFormat(toFormat) + setToFormat(fromFormat) const temp = input setInput(output) setOutput(temp) @@ -70,12 +93,12 @@ export function YamlJsonConverterPage() { } try { - setOutput(convertYamlJson(input, mode)) + setOutput(convertStructuredData(input, { from: fromFormat, to: toFormat })) setError(null) } catch { - setError(mode === "yaml-to-json" ? text("error_yaml_to_json") : text("error_json_to_yaml")) + setError(text("error_convert")) } - }, [input, mode, text]) + }, [fromFormat, input, text, toFormat]) const openImportPicker = () => { fileInputRef.current?.click() @@ -134,9 +157,9 @@ export function YamlJsonConverterPage() { return () => window.removeEventListener("keydown", handleKeyDown) }, [output, handleCopy, doConvert]) - const inputLang = mode === "yaml-to-json" ? "yaml" : "json" - const outputLang = mode === "yaml-to-json" ? "json" : "yaml" - const jsonFormatterHandoffPayload = mode === "yaml-to-json" ? output : "" + const inputLang = monacoLanguage(fromFormat) + const outputLang = monacoLanguage(toFormat) + const jsonFormatterHandoffPayload = toFormat === "json" ? output : "" const jsonFormatterHandoff = React.useMemo( () => buildToolHandoffLink(lang, "json-formatter", jsonFormatterHandoffPayload), [jsonFormatterHandoffPayload, lang], @@ -161,11 +184,47 @@ export function YamlJsonConverterPage() {

- + ))} +
+ +
+ {FORMAT_OPTIONS.map((format) => ( + + ))} +
+ ))} + + +
+ +
+ {(["encode", "decode"] as Operation[]).map((item) => ( + + ))} +
+
+ + +
+ + + + +
+ + {error ?
{error}
: null} + +
+
+
{t.common.input}
+