diff --git a/README.md b/README.md index 0a2e902..6d4ae84 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ This plugin is designed to display JSON strings in a Treeview format and highlig 5. Voila, that's it! If the JSON is valid, it will be displayed in a Treeview format. +## Format JSON Lines + +Use **Plugins > JSON Viewer > Format JSON Lines** (directly below **Format JSON**) to pretty-print each JSON record in the selection, or the whole document when nothing is selected. Each nonblank input line must contain one complete JSON value. Blank lines are skipped. + +The command uses the existing indentation, array formatting, line ending, comment, and trailing comma settings. It preserves record order and numeric precision, and retains a final newline if present. If any record is invalid, no text is replaced; the error identifies the input line and highlights the original error position. + +The output contains pretty-printed JSON documents spanning multiple lines; it is no longer strict JSONL. The existing tree viewer and other commands still accept a single JSON document. + ## Latest Updates: ### 2.2.0.0 diff --git a/src/NppJsonViewer/Define.h b/src/NppJsonViewer/Define.h index 0e2a365..25add1c 100644 --- a/src/NppJsonViewer/Define.h +++ b/src/NppJsonViewer/Define.h @@ -9,6 +9,7 @@ enum class CallBackID : int { SHOW_DOC_PANEL = 0, FORMAT, + FORMAT_LINES, COMPRESS, SORT_BY_KEY, SEP_1, @@ -25,6 +26,7 @@ const TCHAR PLUGIN_CONFIG[] = TEXT("JSONViewer.ini"); const TCHAR TITLE_JSON_PANEL[] = TEXT("JSON Viewer"); const TCHAR MENU_SHOW_JSON_PANEL[] = TEXT("Show &JSON Viewer"); const TCHAR MENU_FORMAT_JSON[] = TEXT("&Format JSON"); +const TCHAR MENU_FORMAT_JSON_LINES[] = TEXT("Format JSON &Lines"); const TCHAR MENU_COMPRESS_JSON[] = TEXT("&Compress JSON"); const TCHAR MENU_SORT_BY_KEY[] = TEXT("Sort by &key (ascending)"); const TCHAR MENU_SETTING[] = TEXT("&Settings"); diff --git a/src/NppJsonViewer/JsonHandler.cpp b/src/NppJsonViewer/JsonHandler.cpp index a3f3659..e9640ac 100644 --- a/src/NppJsonViewer/JsonHandler.cpp +++ b/src/NppJsonViewer/JsonHandler.cpp @@ -30,6 +30,53 @@ auto JsonHandler::FormatJson(const std::string& jsonText, LE le, LF lf, char ind return ParseJson(jsonText, sb, handler); } +auto JsonHandler::FormatJsonLines(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result +{ + const std::string eol = le == LE::kCrLf ? "\r\n" : le == LE::kCr ? "\r" : "\n"; + std::string output; + size_t start = 0; + size_t lineNumber = 1; + bool hasRecord = false; + + while (start < jsonText.size()) + { + const size_t end = jsonText.find_first_of("\r\n", start); + const std::string line = jsonText.substr(start, end == std::string::npos ? end : end - start); + if (line.find_first_not_of(" \t") != std::string::npos) + { + auto result = FormatJson(line, le, lf, indentChar, indentLen); + if (!result.success) + { + result.error_pos += static_cast(start); + result.error_str = "Line " + std::to_string(lineNumber) + ": " + result.error_str; + return result; + } + if (hasRecord) + output += eol; + output += result.response; + hasRecord = true; + } + + if (end == std::string::npos) + break; + start = end + 1; + if (jsonText[end] == '\r' && start < jsonText.size() && jsonText[start] == '\n') + ++start; + ++lineNumber; + } + + if (!hasRecord) + return FormatJson(jsonText, le, lf, indentChar, indentLen); + + if (!jsonText.empty() && (jsonText.back() == '\r' || jsonText.back() == '\n')) + output += eol; + + Result result; + result.success = true; + result.response = std::move(output); + return result; +} + auto JsonHandler::SortJsonByKey(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result { auto res = ValidateJson(jsonText); diff --git a/src/NppJsonViewer/JsonHandler.h b/src/NppJsonViewer/JsonHandler.h index 9d4f67f..2c9788d 100644 --- a/src/NppJsonViewer/JsonHandler.h +++ b/src/NppJsonViewer/JsonHandler.h @@ -45,6 +45,7 @@ class JsonHandler auto GetCompressedJson(const std::string& jsonText) -> const Result; auto FormatJson(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result; + auto FormatJsonLines(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result; auto SortJsonByKey(const std::string& jsonText, LE le, LF lf, char indentChar, unsigned indentLen) -> const Result; auto ValidateJson(const std::string& jsonText) -> const Result; diff --git a/src/NppJsonViewer/JsonViewDlg.cpp b/src/NppJsonViewer/JsonViewDlg.cpp index e7b580f..bfc4313 100644 --- a/src/NppJsonViewer/JsonViewDlg.cpp +++ b/src/NppJsonViewer/JsonViewDlg.cpp @@ -108,6 +108,36 @@ void JsonViewDlg::FormatJson() ReDrawJsonTree(); } +void JsonViewDlg::FormatJsonLines() +{ + UpdateTitle(); + m_pEditor->RefreshViewHandle(); + + const auto selectedData = m_pEditor->GetJsonText(); + const auto selectedText = IsSelectionValidJson(selectedData); + if (!selectedText.has_value() || selectedText.value().empty()) + { + const std::wstring msg = IsMultiSelection(selectedData) ? JSON_ERR_MULTI_SELECTION : JSON_ERR_PARSE; + ShowMessage(JSON_INFO_TITLE, msg, MB_OK | MB_ICONINFORMATION); + return; + } + + auto [le, lf, indentChar, indentLen] = GetFormatSetting(); + const Result res = JsonHandler(m_pSetting->parseOptions).FormatJsonLines(selectedText.value(), le, lf, indentChar, indentLen); + if (res.success) + { + m_pEditor->ReplaceSelection(res.response); + HighlightAsJson(); + } + else + { + ReportError(res); + } + + // The tree viewer accepts a single JSON document, not a stream of records. + // Do not redraw it here: doing so would report a second, unrelated parse error. +} + void JsonViewDlg::CompressJson() { UpdateTitle(); diff --git a/src/NppJsonViewer/JsonViewDlg.h b/src/NppJsonViewer/JsonViewDlg.h index 605db34..86a762c 100644 --- a/src/NppJsonViewer/JsonViewDlg.h +++ b/src/NppJsonViewer/JsonViewDlg.h @@ -43,6 +43,7 @@ class JsonViewDlg void ShowDlg(bool bShow); void FormatJson(); + void FormatJsonLines(); void CompressJson(); void SortJsonByKey(); void HandleTabActivated(); diff --git a/src/NppJsonViewer/NppJsonPlugin.cpp b/src/NppJsonViewer/NppJsonPlugin.cpp index 40e96c0..7b905d1 100644 --- a/src/NppJsonViewer/NppJsonPlugin.cpp +++ b/src/NppJsonViewer/NppJsonPlugin.cpp @@ -127,6 +127,8 @@ void NppJsonPlugin::InitCommandMenu() m_shortcutCommands.SetShortCut(CallBackID::FORMAT, {true, true, true, 'M'}); m_shortcutCommands.SetCommand(CallBackID::FORMAT, MENU_FORMAT_JSON, Callback::FormatJson, false); + m_shortcutCommands.SetCommand(CallBackID::FORMAT_LINES, MENU_FORMAT_JSON_LINES, Callback::FormatJsonLines, false); + m_shortcutCommands.SetShortCut(CallBackID::COMPRESS, {true, true, true, 'C'}); m_shortcutCommands.SetCommand(CallBackID::COMPRESS, MENU_COMPRESS_JSON, Callback::CompressJson, false); @@ -202,6 +204,15 @@ void NppJsonPlugin::FormatJson() } } +void NppJsonPlugin::FormatJsonLines() +{ + ConstructJsonDlg(); + if (m_pJsonViewDlg) + { + m_pJsonViewDlg->FormatJsonLines(); + } +} + void NppJsonPlugin::CompressJson() { ConstructJsonDlg(); diff --git a/src/NppJsonViewer/NppJsonPlugin.h b/src/NppJsonViewer/NppJsonPlugin.h index cdbaaf0..aeb4f7b 100644 --- a/src/NppJsonViewer/NppJsonPlugin.h +++ b/src/NppJsonViewer/NppJsonPlugin.h @@ -51,6 +51,10 @@ class NppJsonPlugin { m_pNppJsonPlugin->FormatJson(); } + static void FormatJsonLines() + { + m_pNppJsonPlugin->FormatJsonLines(); + } static void CompressJson() { m_pNppJsonPlugin->CompressJson(); @@ -81,6 +85,7 @@ class NppJsonPlugin void ShowJsonDlg(); void FormatJson(); + void FormatJsonLines(); void CompressJson(); void SortJsonByKey(); void OpenSettingDlg(); diff --git a/tests/UnitTest/JsonLinesFormatTest.cpp b/tests/UnitTest/JsonLinesFormatTest.cpp new file mode 100644 index 0000000..80d0db5 --- /dev/null +++ b/tests/UnitTest/JsonLinesFormatTest.cpp @@ -0,0 +1,77 @@ +#include +#include "JsonHandler.h" + +TEST(JsonLinesFormatTest, FormatsRecordsUsingExistingSettings) +{ + JsonHandler handler({}); + const auto result = handler.FormatJsonLines("{\"a\":1}\n{\"b\":[2,3]}", LE::kLf, LF::kFormatSingleLineArray, '\t', 1); + ASSERT_TRUE(result.success); + EXPECT_EQ(result.response, "{\n\t\"a\": 1\n}\n{\n\t\"b\": [2, 3]\n}"); +} + +TEST(JsonLinesFormatTest, HandlesLineEndingsAndFinalNewline) +{ + JsonHandler handler({}); + for (const auto& inputEol : {"\n", "\r\n", "\r"}) + { + const std::string input = std::string("{}") + inputEol + "[]" + inputEol; + for (const auto& [ending, expectedEol] : {std::pair{LE::kLf, "\n"}, std::pair{LE::kCrLf, "\r\n"}, std::pair{LE::kCr, "\r"}}) + { + const auto result = handler.FormatJsonLines(input, ending, {}, ' ', 2); + ASSERT_TRUE(result.success); + EXPECT_EQ(result.response, std::string("{}") + expectedEol + "[]" + expectedEol); + } + } +} + +TEST(JsonLinesFormatTest, SkipsBlankLinesAndPreservesNumericText) +{ + JsonHandler handler({}); + const auto result = handler.FormatJsonLines("\n \t\r\n{\"n\":99999999999999999999}\n\n1e+10\n", LE::kLf, {}, ' ', 2); + ASSERT_TRUE(result.success); + EXPECT_EQ(result.response, "{\n \"n\": 99999999999999999999\n}\n1e+10\n"); +} + +TEST(JsonLinesFormatTest, SupportsAllRootValueTypes) +{ + JsonHandler handler({}); + const auto result = handler.FormatJsonLines("{}\n[]\n\"text\\nvalue\"\n42\ntrue\nfalse\nnull", LE::kLf, {}, ' ', 2); + ASSERT_TRUE(result.success); + EXPECT_EQ(result.response, "{}\n[]\n\"text\\nvalue\"\n42\ntrue\nfalse\nnull"); +} + +TEST(JsonLinesFormatTest, ReportsOriginalErrorOffsetWithoutPartialOutput) +{ + JsonHandler handler({}); + const std::string badLine = "{\"b\":}"; + const std::string prefix = "{}\r\n\r\n"; + const auto single = handler.FormatJson(badLine, LE::kLf, {}, ' ', 2); + const auto result = handler.FormatJsonLines(prefix + badLine + "\r\n{}", LE::kLf, {}, ' ', 2); + ASSERT_FALSE(result.success); + EXPECT_TRUE(result.response.empty()); + EXPECT_EQ(result.error_pos, static_cast(prefix.size()) + single.error_pos); + EXPECT_EQ(result.error_code, single.error_code); + EXPECT_EQ(result.error_str, "Line 3: " + single.error_str); +} + +TEST(JsonLinesFormatTest, RejectsMultipleValuesOnOneLineAndMultilineRecords) +{ + JsonHandler handler({}); + EXPECT_FALSE(handler.FormatJsonLines("{} {}", LE::kLf, {}, ' ', 2).success); + EXPECT_FALSE(handler.FormatJsonLines("{\n\"a\":1\n}", LE::kLf, {}, ' ', 2).success); + EXPECT_FALSE(handler.FormatJsonLines(" \t\r\n", LE::kLf, {}, ' ', 2).success); + EXPECT_FALSE(handler.FormatJsonLines("", LE::kLf, {}, ' ', 2).success); +} + +TEST(JsonLinesFormatTest, HonorsRelaxedParsingSettings) +{ + ParseOptions options; + JsonHandler relaxed(options); + const std::string input = "{\"a\":1,}\n{/* comment */\"b\":2}"; + EXPECT_TRUE(relaxed.FormatJsonLines(input, LE::kLf, {}, ' ', 2).success); + options.bIgnoreTrailingComma = false; + EXPECT_FALSE(JsonHandler(options).FormatJsonLines(input, LE::kLf, {}, ' ', 2).success); + options.bIgnoreTrailingComma = true; + options.bIgnoreComment = false; + EXPECT_FALSE(JsonHandler(options).FormatJsonLines(input, LE::kLf, {}, ' ', 2).success); +} diff --git a/tests/UnitTest/UnitTest.vcxproj b/tests/UnitTest/UnitTest.vcxproj index 70446d7..c79135a 100644 --- a/tests/UnitTest/UnitTest.vcxproj +++ b/tests/UnitTest/UnitTest.vcxproj @@ -156,6 +156,7 @@ + diff --git a/tests/UnitTest/UnitTest.vcxproj.filters b/tests/UnitTest/UnitTest.vcxproj.filters index 355880a..e4042ad 100644 --- a/tests/UnitTest/UnitTest.vcxproj.filters +++ b/tests/UnitTest/UnitTest.vcxproj.filters @@ -48,6 +48,9 @@ Source Files\NppJsonViewer + + Source Files + Source Files