From e156ff52771cb521f8ed43ed1b72a45bb64f87dc Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 12 Oct 2026 03:06:18 +0800 Subject: [PATCH] [fix] handle Markdown separators in release notes --- .../src/gui/application/gui_application.cpp | 70 +-------------- apps/desktop/src/gui/ui/release_notes_text.h | 90 +++++++++++++++++++ 2 files changed, 92 insertions(+), 68 deletions(-) create mode 100644 apps/desktop/src/gui/ui/release_notes_text.h diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 393bc658..e80d965f 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -65,6 +65,7 @@ #include "signal_server_settings.h" #include "signal_server_address.h" #include "ui/announcement_text.h" +#include "ui/release_notes_text.h" #include "ui/ui_localization.h" #include "update_checker.h" #include "window_geometry.h" @@ -310,73 +311,6 @@ std::string Trim(std::string value) { return std::string(first, last); } -struct ParsedReleaseNoteBlock { - slint::StyledText content; - bool section_gap = false; -}; - -std::vector ParseReleaseNotesMarkdownForSlint( - std::string_view markdown) { - std::vector blocks; - bool section_gap = false; - - size_t line_start = 0; - while (line_start < markdown.size()) { - const size_t newline = markdown.find('\n', line_start); - const size_t line_end = - newline == std::string_view::npos ? markdown.size() : newline; - std::string_view line = markdown.substr(line_start, line_end - line_start); - if (!line.empty() && line.back() == '\r') { - line.remove_suffix(1); - } - - if (std::all_of(line.begin(), line.end(), - [](unsigned char ch) { return std::isspace(ch) != 0; })) { - section_gap = !blocks.empty(); - if (newline == std::string_view::npos) { - break; - } - line_start = newline + 1; - continue; - } - - size_t heading_end = 0; - while (heading_end < line.size() && heading_end < 6 && - line[heading_end] == '#') { - ++heading_end; - } - const bool is_heading = - heading_end > 0 && heading_end < line.size() && - std::isspace(static_cast(line[heading_end])) != 0; - if (is_heading) { - if (newline == std::string_view::npos) { - break; - } - line_start = newline + 1; - continue; - } - - const std::string block_markdown(line); - ParsedReleaseNoteBlock block; - if (const auto parsed = slint::StyledText::from_markdown(block_markdown)) { - block.content = *parsed; - } else { - LOG_WARN( - "Failed to parse a release-note Markdown block; using plain text"); - block.content = slint::StyledText::from_plain_text(block_markdown); - } - block.section_gap = section_gap; - blocks.push_back(std::move(block)); - section_gap = false; - - if (newline == std::string_view::npos) { - break; - } - line_start = newline + 1; - } - return blocks; -} - std::string DecodeDroppedPath(std::string text) { const size_t line_end = text.find_first_of("\r\n"); if (line_end != std::string::npos) { @@ -1983,7 +1917,7 @@ void GuiApplication::PollUpdateCheck() { release_date_ = info.release_date; update_available_ = update_checker_->update_available(); std::vector blocks; - for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) { + for (const auto& parsed : release_notes_text::Parse(release_notes_)) { ui::ReleaseNoteBlock block; block.content = parsed.content; block.section_gap = parsed.section_gap; diff --git a/apps/desktop/src/gui/ui/release_notes_text.h b/apps/desktop/src/gui/ui/release_notes_text.h new file mode 100644 index 00000000..f8bef267 --- /dev/null +++ b/apps/desktop/src/gui/ui/release_notes_text.h @@ -0,0 +1,90 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-12 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _RELEASE_NOTES_TEXT_H_ +#define _RELEASE_NOTES_TEXT_H_ + +#include + +#include +#include +#include +#include +#include + +namespace crossdesk::release_notes_text { + +struct Block { + slint::StyledText content; + bool section_gap = false; +}; + +inline bool IsThematicBreak(std::string_view line) { + const auto start = line.find_first_not_of(' '); + // Four leading spaces denote a code block, not a thematic break. + if (start == std::string_view::npos || start > 3) return false; + const char marker = line[start]; + if (marker != '-' && marker != '*' && marker != '_') return false; + size_t count = 0; + for (const char ch : line.substr(start)) { + if (ch == marker) + ++count; + else if (ch != ' ' && ch != '\t') + return false; + } + return count >= 3; +} + +inline std::vector Parse(std::string_view markdown) { + std::vector blocks; + bool section_gap = false; + size_t line_start = 0; + while (line_start < markdown.size()) { + const size_t newline = markdown.find('\n', line_start); + const size_t line_end = + newline == std::string_view::npos ? markdown.size() : newline; + auto line = markdown.substr(line_start, line_end - line_start); + line_start = newline == std::string_view::npos ? markdown.size() : newline + 1; + if (!line.empty() && line.back() == '\r') line.remove_suffix(1); + + // StyledText does not support thematic breaks. Represent them with the + // same spacing as blank lines instead of rendering raw Markdown markers. + if (std::all_of(line.begin(), line.end(), [](unsigned char ch) { + return std::isspace(ch) != 0; + }) || IsThematicBreak(line)) { + section_gap = !blocks.empty(); + continue; + } + + size_t heading_end = 0; + while (heading_end < line.size() && heading_end < 6 && + line[heading_end] == '#') { + ++heading_end; + } + // The dialog already supplies the release title. + if (heading_end > 0 && heading_end < line.size() && + std::isspace(static_cast(line[heading_end])) != 0) { + continue; + } + + Block block; + if (const auto parsed = slint::StyledText::from_markdown(line)) { + block.content = *parsed; + } else { + // Unsupported syntax is valid release content, not a parsing failure. + // Preserve it literally when it is outside Slint's Markdown subset. + block.content = slint::StyledText::from_plain_text(line); + } + block.section_gap = section_gap; + blocks.push_back(std::move(block)); + section_gap = false; + } + return blocks; +} + +} // namespace crossdesk::release_notes_text + +#endif