commit fc03ea88201a43a49d02fc8c7fd7762e9198814b Author: Dan Cojocaru Date: Wed Jul 28 23:55:17 2021 +0300 First kennson version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3582179 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Files and directories created by pub. +.dart_tool/ +.packages + +# Conventional directory for build output. +build/ + +# VS Code +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b9f7fed --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 + +- Initial version. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8b99ea --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# kennson + +A terminal utility to pretty print JSON. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..18b40b8 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,16 @@ +# Defines a default set of lint rules enforced for projects at Google. For +# details and rationale, see +# https://github.com/dart-lang/pedantic#enabled-lints. + +include: package:pedantic/analysis_options.yaml + +# For lint rules and documentation, see http://dart-lang.github.io/linter/lints. + +# Uncomment to specify additional rules. +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** diff --git a/bin/ansi_format.dart b/bin/ansi_format.dart new file mode 100644 index 0000000..475afdb --- /dev/null +++ b/bin/ansi_format.dart @@ -0,0 +1,138 @@ +const String ESC = '\x1b'; +const String CSI = '$ESC['; + +class TextFormat { + final List children; + + const TextFormat({required this.children}); + + @override + String toString() { + return children.join(''); + } +} + +class ANSIColorText extends TextFormat { + final ANSIColor? foreground; + final ANSIColor? background; + + ANSIColorText({this.foreground, this.background, required List children}) : super(children: children); + + @override + String toString() { + var result = ''; + for (final child in children) { + result += foreground?.enabler ?? ''; + result += background?.backgroundEnabler ?? ''; + result += child.toString(); + } + result += foreground?.disabler ?? ''; + result += background?.backgroundDisabler ?? ''; + return result; + } +} + +class ANSIFormat extends TextFormat { + final bool? bold; + final bool? italic; + final bool? underline; + final bool? reverseColor; + + ANSIFormat({this.bold, this.italic, this.underline, this.reverseColor, required List children}) : super(children: children); + + @override + String toString() { + var result = ''; + for (final child in children) { + if (bold != null) { + result += bold! ? ANSIBold().enabler : ANSIBold().disabler; + } + if (italic != null) { + result += italic! ? ANSIItalic().enabler : ANSIItalic().disabler; + } + if (underline != null) { + result += underline! ? ANSIUnderline().enabler : ANSIUnderline().disabler; + } + if (reverseColor != null) { + result += reverseColor! ? ANSIReverseVideo().enabler : ANSIReverseVideo().disabler; + } + result += child.toString(); + } + if (bold != null) { + result += bold! ? ANSIBold().disabler : ANSIBold().enabler; + } + if (italic != null) { + result += italic! ? ANSIItalic().disabler : ANSIItalic().enabler; + } + if (underline != null) { + result += underline! ? ANSIUnderline().disabler : ANSIUnderline().enabler; + } + if (reverseColor != null) { + result += reverseColor! ? ANSIReverseVideo().disabler : ANSIReverseVideo().enabler; + } + return result; + } +} + +class Text extends TextFormat { + final String value; + const Text(this.value) : super(children: const []); + @override + String toString() { + return value; + } +} + +class ANSIEscape { + final String enabler; + final String disabler; + + const ANSIEscape({required this.enabler, required this.disabler}); +} + +class ANSIBold extends ANSIEscape { + const ANSIBold() : super(enabler: '${CSI}1m', disabler: '${CSI}22m'); +} + +class ANSIItalic extends ANSIEscape { + const ANSIItalic() : super(enabler: '${CSI}3m', disabler: '${CSI}23m'); +} + +class ANSIUnderline extends ANSIEscape { + const ANSIUnderline() : super(enabler: '${CSI}4m', disabler: '${CSI}24m'); +} + +class ANSIReverseVideo extends ANSIEscape { + const ANSIReverseVideo() : super(enabler: '${CSI}7m', disabler: '${CSI}27m'); +} + +class ANSIColor extends ANSIEscape { + const ANSIColor.index(int index, [bool bright = false]) : super(enabler: '$CSI${bright ? 9 : 3}${index}m', disabler: '${CSI}39m'); + const ANSIColor.value(String value) : super(enabler: '$CSI${value}m', disabler: '${CSI}39m'); + String get backgroundEnabler { + final noCSI = enabler.substring(CSI.length); + final nom = noCSI.substring(0, noCSI.length - 1); + final number = int.parse(nom); + return '$CSI${number + 10}m'; + } + final String backgroundDisabler = '${CSI}49m'; +} + +class ANSIColors { + static const ANSIColor black = ANSIColor.index(0); + static const ANSIColor red = ANSIColor.index(1); + static const ANSIColor green = ANSIColor.index(2); + static const ANSIColor yellow = ANSIColor.index(3); + static const ANSIColor blue = ANSIColor.index(4); + static const ANSIColor magenta = ANSIColor.index(5); + static const ANSIColor cyan = ANSIColor.index(6); + static const ANSIColor white = ANSIColor.index(7); + static const ANSIColor brightBlack = ANSIColor.index(0, true); + static const ANSIColor brightRed = ANSIColor.index(1, true); + static const ANSIColor brightGreen = ANSIColor.index(2, true); + static const ANSIColor brightYellow = ANSIColor.index(3, true); + static const ANSIColor brightBlue = ANSIColor.index(4, true); + static const ANSIColor brightMagenta = ANSIColor.index(5, true); + static const ANSIColor brightCyan = ANSIColor.index(6, true); + static const ANSIColor brightWhite = ANSIColor.index(7, true); +} diff --git a/bin/kennson.dart b/bin/kennson.dart new file mode 100644 index 0000000..3e38293 --- /dev/null +++ b/bin/kennson.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:json_path/json_path.dart'; +import 'package:rfc_6901/rfc_6901.dart'; + +import 'ppjson.dart'; + +void main(List arguments) { + final parser = ArgParser() + ..addOption('file', abbr: 'f', help: 'Read JSON from file instead of stdin', valueHelp: 'filename') + ..addOption('input', help: 'Read input as parameter instead of stdin', valueHelp: 'json input') + ..addOption('jsonpath', aliases: ['path'], help: 'Display only the matches of the JSON document', valueHelp: 'JSONPath query') + ..addOption('jsonpointer', aliases: ['pointer'], abbr: 'p', help: 'Display only the matches of the JSON pointer') + ..addOption('indent', abbr: 'i', help: 'Set space indentation level (prefix with t for tab indentation)', defaultsTo: '2') + ..addOption('max-depth', abbr: 'm', help: 'Specify maximum nesting before stopping printing'); + // ..addFlag('force-color', help: "Output using colors even when the environment doesn't allow them", hide: true); + final ArgResults parsedArgs; + try { + parsedArgs = parser.parse(arguments); + } + on ArgParserException catch(e) { + print(e.message); + print(''); + print(parser.usage); + exit(1); + } + + // Get indent + final indentArg = parsedArgs['indent'] as String; + final indent = indentArg[0] == 't' ? '\t' * int.parse(indentArg.substring(1)) : ' ' * int.parse(indentArg); + + // Get max depth + final maxDepthArg = parsedArgs['max-depth'] as String?; + final maxDepth = int.tryParse(maxDepthArg ?? ''); + + // Get JSONPath + final jsonPath = parsedArgs['jsonpath'] as String?; + + // Get JSON Pointer + final jsonPointer = parsedArgs['jsonpointer'] as String?; + + // Read + String jsonInput; + if (parsedArgs['file'] != null) { + jsonInput = File(parsedArgs['file']).readAsStringSync(); + } + else if (parsedArgs['input'] != null) { + jsonInput = parsedArgs['input']; + } + else { + jsonInput = ''; + while (true) { + final line = stdin.readLineSync(retainNewlines: true); + if (line != null) { + jsonInput += line; + } + else { + break; + } + } + } + + // Parse + final decodedData; + try { + decodedData = jsonDecode(jsonInput); + } + catch(_) { + stderr.writeln('Unable to parse JSON input.'); + exit(1); + } + final List data; + try { + data = jsonPath != null ? followJsonPath(decodedData, jsonPath) : jsonPointer != null ? followJsonPointer(decodedData, jsonPointer) : [decodedData]; + } + catch (e) { + stderr.writeln('JSONPath Error: $e'); + exit(1); + } + + // Print + if (data.isEmpty) { + stderr.writeln('No match'); + } + else if (data.length == 1) { + stdout.write(prettyPrintJson(data[0], indent, maxDepth)); + } + else { + for (final match in data) { + print(prettyPrintJson(match, indent, maxDepth)); + } + } +} + +List followJsonPath(dynamic object, String jsonPath) { + final path = JsonPath(jsonPath); + return path.read(object).map((e) => e.value).toList(growable: false); +} + +List followJsonPointer(dynamic object, String jsonPointer) { + final pointer = JsonPointer(jsonPointer); + return [pointer.read(object)]; +} diff --git a/bin/ppjson.dart b/bin/ppjson.dart new file mode 100644 index 0000000..e0e2829 --- /dev/null +++ b/bin/ppjson.dart @@ -0,0 +1,158 @@ +import 'ansi_format.dart'; + +String prettyPrintJson(dynamic object, String indent, [int? maxDepth]) { + var result = ''; + final stack = <_PrettyPrintStackObject>[]; + stack.add(_PrettyPrintStackObject(object, 0)); + while (stack.isNotEmpty) { + final elem = stack.removeLast(); + final obj = elem.object is _PrettyPrintTrailingComma ? elem.object.object : elem.object; + final trailingComma = elem.object is _PrettyPrintTrailingComma; + var suppressTrailingComma = false; + + result += indent * elem.level; + String formatObject(dynamic obj) { + if (obj is Map) { + String startStr; + if (obj.isEmpty) { + startStr = '{}'; + } + else if (elem.level == maxDepth) { + startStr = '{...}'; + } + else { + startStr = '{'; + suppressTrailingComma = true; + } + final thisResult = ANSIColorText( + foreground: ANSIColors.blue, + children: [Text(startStr),], + ).toString(); + if (elem.level != maxDepth && obj.isNotEmpty) { + stack.add(_PrettyPrintStackObject(trailingComma ? _PrettyPrintTrailingComma(_PrettyPrintMapEnd()) : _PrettyPrintMapEnd(), elem.level)); + final entries = obj.entries; + stack.add(_PrettyPrintStackObject(entries.last, elem.level + 1)); + stack.addAll(entries.toList(growable: false).reversed.skip(1).map((e) => _PrettyPrintStackObject(_PrettyPrintTrailingComma(e), elem.level + 1))); + } + return thisResult; + } + else if (obj is List) { + String startStr; + if (obj.isEmpty) { + startStr = '[]'; + } + else if (elem.level == maxDepth) { + startStr = '[...]'; + } + else { + startStr = '['; + suppressTrailingComma = true; + } + final thisResult = ANSIColorText( + foreground: ANSIColors.cyan, + children: [Text(startStr),], + ).toString(); + if (elem.level != maxDepth && obj.isNotEmpty) { + stack.add(_PrettyPrintStackObject(trailingComma ? _PrettyPrintTrailingComma(_PrettyPrintArrayEnd()) : _PrettyPrintArrayEnd(), elem.level)); + stack.add(_PrettyPrintStackObject(obj.last, elem.level + 1)); + stack.addAll(obj.reversed.skip(1).map((o) => _PrettyPrintStackObject(_PrettyPrintTrailingComma(o), elem.level + 1))); + } + return thisResult; + } + else if (obj is MapEntry) { + return TextFormat( + children: [ + ANSIColorText( + foreground: ANSIColors.brightYellow, + children: [Text('"${obj.key}"'),] + ), + Text(': '), + Text(formatObject(obj.value)), + ], + ).toString(); + } + else if (obj is String) { + return ANSIColorText( + foreground: ANSIColors.magenta, + children: [Text('"$obj"'),], + ).toString(); + } + else if (obj is double || obj is int) { + return ANSIColorText( + foreground: ANSIColors.brightCyan, + children: [Text(obj.toString()),], + ).toString(); + } + else if (obj == true) { + return ANSIFormat( + underline: true, + children: [ + ANSIColorText( + foreground: ANSIColors.green, + children: [Text(obj.toString()),], + ), + ], + ).toString(); + } + else if (obj == false) { + return ANSIFormat( + underline: true, + children: [ + ANSIColorText( + foreground: ANSIColors.red, + children: [Text(obj.toString()),], + ), + ], + ).toString(); + } + else if (obj == null) { + return ANSIFormat( + underline: true, + children: [ + ANSIColorText( + foreground: ANSIColors.brightBlue, + children: [Text('null'),], + ), + ], + ).toString(); + } + else if (obj is _PrettyPrintMapEnd) { + return ANSIColorText( + foreground: ANSIColors.blue, + children: [Text('}'),], + ).toString(); + } + else if (obj is _PrettyPrintArrayEnd) { + return ANSIColorText( + foreground: ANSIColors.cyan, + children: [Text(']'),], + ).toString(); + } + else { + throw Exception('Unexpected object of unknown type: $obj ${obj.runtimeType}'); + } + } + result += formatObject(obj); + + if (trailingComma && !suppressTrailingComma) { + result += ','; + } + + result += '\n'; + } + return result; +} + +class _PrettyPrintStackObject { + final dynamic object; + final int level; + _PrettyPrintStackObject(this.object, this.level); +} + +class _PrettyPrintTrailingComma { + final dynamic object; + _PrettyPrintTrailingComma(this.object); +} + +class _PrettyPrintMapEnd {} +class _PrettyPrintArrayEnd {} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..d219765 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,47 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: "direct main" + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + json_path: + dependency: "direct main" + description: + name: json_path + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + pedantic: + dependency: "direct dev" + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.11.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" + rfc_6901: + dependency: "direct main" + description: + name: rfc_6901 + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" +sdks: + dart: ">=2.13.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..76099d3 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,16 @@ +name: kennson +description: A simple command-line application. +version: 1.0.0 +# homepage: https://www.example.com + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + args: ^2.2.0 + json_path: ^0.3.0 + rfc_6901: ^0.1.0 +# path: ^1.8.0 + +dev_dependencies: + pedantic: ^1.10.0