From a63c054c56eafd0f3cc5841de272f3139662e578 Mon Sep 17 00:00:00 2001 From: chaos-zhu Date: Sat, 23 May 2026 20:32:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20=E6=96=B0=E5=A2=9E=20editor=5Ff?= =?UTF-8?q?ormatters=20JSON/YAML/XML=20=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shell/editor/editor_formatters.dart | 99 +++++++++++++++++++ .../shell/editor/editor_formatters_test.dart | 41 ++++++++ 2 files changed, 140 insertions(+) create mode 100644 mobile/lib/features/shell/editor/editor_formatters.dart create mode 100644 mobile/test/features/shell/editor/editor_formatters_test.dart diff --git a/mobile/lib/features/shell/editor/editor_formatters.dart b/mobile/lib/features/shell/editor/editor_formatters.dart new file mode 100644 index 0000000..87118ce --- /dev/null +++ b/mobile/lib/features/shell/editor/editor_formatters.dart @@ -0,0 +1,99 @@ +import 'dart:convert'; + +import 'package:xml/xml.dart'; +import 'package:yaml/yaml.dart'; + +String formatJson(String src) { + try { + final decoded = jsonDecode(src); + return const JsonEncoder.withIndent(' ').convert(decoded); + } on FormatException { + rethrow; + } +} + +String formatYaml(String src) { + try { + final decoded = loadYaml(src); + final buffer = StringBuffer(); + _dumpYaml(decoded, buffer, 0); + final output = buffer.toString(); + return output.endsWith('\n') ? output : '$output\n'; + } on YamlException catch (err) { + throw FormatException(err.message); + } +} + +String formatXml(String src) { + try { + final doc = XmlDocument.parse(src); + return doc.toXmlString(pretty: true, indent: ' '); + } on XmlException catch (err) { + throw FormatException(err.message); + } +} + +void _dumpYaml(dynamic node, StringBuffer buf, int indent) { + final pad = ' ' * indent; + if (node is YamlMap || node is Map) { + final map = node is YamlMap + ? node.nodes.map((k, v) => MapEntry(k.toString(), v.value)) + : (node as Map); + if (map.isEmpty) { + buf.write('{}\n'); + return; + } + var first = true; + for (final entry in map.entries) { + if (!first || indent > 0) buf.write(pad); + first = false; + buf.write('${_yamlKey(entry.key.toString())}:'); + final v = entry.value; + if (v is YamlMap || v is Map || v is YamlList || v is List) { + buf.write('\n'); + _dumpYaml(v, buf, indent + 1); + } else { + buf.write(' ${_yamlScalar(v)}\n'); + } + } + } else if (node is YamlList || node is List) { + final list = node is YamlList ? node.toList() : (node as List); + if (list.isEmpty) { + buf.write('$pad[]\n'); + return; + } + for (final item in list) { + buf.write('$pad- '); + if (item is YamlMap || item is Map || item is YamlList || item is List) { + buf.write('\n'); + _dumpYaml(item, buf, indent + 1); + } else { + buf.write('${_yamlScalar(item)}\n'); + } + } + } else { + buf.write('$pad${_yamlScalar(node)}\n'); + } +} + +String _yamlKey(String key) { + if (RegExp(r'^[A-Za-z_][\w\-]*$').hasMatch(key)) return key; + return _yamlQuote(key); +} + +String _yamlScalar(dynamic value) { + if (value == null) return 'null'; + if (value is bool) return value.toString(); + if (value is num) return value.toString(); + final s = value.toString(); + if (s.isEmpty) return '""'; + if (RegExp(r'^(true|false|null|~|\d|-)').hasMatch(s) || s.contains(': ') || s.contains('#')) { + return _yamlQuote(s); + } + return s; +} + +String _yamlQuote(String s) { + final escaped = s.replaceAll(r'\', r'\\').replaceAll('"', r'\"'); + return '"$escaped"'; +} diff --git a/mobile/test/features/shell/editor/editor_formatters_test.dart b/mobile/test/features/shell/editor/editor_formatters_test.dart new file mode 100644 index 0000000..ebaca67 --- /dev/null +++ b/mobile/test/features/shell/editor/editor_formatters_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobile/features/shell/editor/editor_formatters.dart'; + +void main() { + group('formatJson', () { + test('pretty-prints with 2-space indent', () { + final out = formatJson('{"a":1,"b":[1,2,3]}'); + expect(out, contains('\n "a": 1')); + expect(out, contains(' 1')); + }); + + test('throws FormatException on invalid JSON', () { + expect(() => formatJson('{not json'), throwsA(isA())); + }); + }); + + group('formatYaml', () { + test('round-trips a simple map with 2-space indent', () { + final out = formatYaml('foo: 1\nbar:\n baz: hello'); + expect(out, contains('foo: 1')); + expect(out, contains('bar:')); + expect(out, contains(' baz: hello')); + }); + + test('throws FormatException on invalid YAML', () { + expect(() => formatYaml(': : : not yaml'), throwsA(isA())); + }); + }); + + group('formatXml', () { + test('pretty-prints valid xml with 2-space indent', () { + final out = formatXml('x'); + expect(out, contains('')); + expect(out, contains(' x')); + }); + + test('throws FormatException on invalid XML', () { + expect(() => formatXml(''), throwsA(isA())); + }); + }); +}