From e968a54e77c708d46e3eec6c5536b10fd9ce020a Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 23 Apr 2026 22:04:19 +0200 Subject: [PATCH] Improve nested lists Co-authored-by: Copilot --- lib/Md/MdParser.cpp | 14 ++++++++++++-- src/activities/reader/MdReaderActivity.cpp | 15 ++++++++++++--- test/md_parser/MdParserTest.cpp | 22 ++++++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/lib/Md/MdParser.cpp b/lib/Md/MdParser.cpp index 372d5e1f..ea4bb3c7 100644 --- a/lib/Md/MdParser.cpp +++ b/lib/Md/MdParser.cpp @@ -19,6 +19,15 @@ static std::string trimLeft(const std::string& s) { return s.substr(i); } +static uint8_t parseListIndentLevel(size_t leadingSpaces) { + // Top-level list markers may be preceded by up to 3 spaces. + // Nested list items require at least 4 spaces before the marker. + if (leadingSpaces < TAB_WIDTH) { + return 0; + } + return static_cast((leadingSpaces - TAB_WIDTH) / TAB_WIDTH + 1); +} + static bool isWordChar(char c) { return std::isalnum(static_cast(c)) || c == '_'; } static bool isUnderscoreEmphasis(const std::string& text, size_t pos, size_t count) { @@ -249,7 +258,8 @@ ParsedLine parseLine(const std::string& rawLine, bool inCodeBlock) { return result; } - // Count leading whitespace for nesting level before trimming + // Count leading whitespace for nesting level before trimming. + // Up to 3 spaces before a list marker are still top-level in CommonMark. size_t leadingSpaces = 0; for (size_t i = 0; i < rawLine.size(); i++) { if (rawLine[i] == ' ') @@ -259,7 +269,7 @@ ParsedLine parseLine(const std::string& rawLine, bool inCodeBlock) { else break; } - result.indentLevel = static_cast(leadingSpaces / TAB_WIDTH); + result.indentLevel = parseListIndentLevel(leadingSpaces); std::string trimmed = trimLeft(rawLine); diff --git a/src/activities/reader/MdReaderActivity.cpp b/src/activities/reader/MdReaderActivity.cpp index 1e846371..5ccf5bab 100644 --- a/src/activities/reader/MdReaderActivity.cpp +++ b/src/activities/reader/MdReaderActivity.cpp @@ -335,13 +335,18 @@ bool MdReaderActivity::wordWrapParsedLine(const MdParser::ParsedLine& parsed, in const int availableWidth = viewportWidth - indent; if (availableWidth <= 0) return true; - // Build a flat list of all spans, prepending the list prefix if present + // Build a flat list of all spans, prepending the list prefix if present. std::vector allSpans; if (!parsed.listPrefix.empty()) { allSpans.push_back({parsed.listPrefix, EpdFontFamily::REGULAR}); } allSpans.insert(allSpans.end(), parsed.spans.begin(), parsed.spans.end()); + const int listPrefixIndent = + !parsed.listPrefix.empty() + ? renderer.getTextAdvanceX(cachedFontId, parsed.listPrefix.c_str(), EpdFontFamily::REGULAR) + : 0; + // Check if everything fits on one line int totalWidth = measureSpans(allSpans); if (totalWidth <= availableWidth) { @@ -359,6 +364,7 @@ bool MdReaderActivity::wordWrapParsedLine(const MdParser::ParsedLine& parsed, in currentLine.isCodeBlock = isCodeBlock; int currentWidth = 0; bool fullyConsumed = true; + bool firstLine = true; for (size_t si = 0; si < allSpans.size(); si++) { const auto& span = allSpans[si]; @@ -415,8 +421,10 @@ bool MdReaderActivity::wordWrapParsedLine(const MdParser::ParsedLine& parsed, in } } else { outLines.push_back(std::move(currentLine)); + firstLine = false; currentLine = RenderedLine(); - currentLine.indent = indent; + currentLine.indent = indent + (firstLine ? 0 : listPrefixIndent); + currentLine.isCodeBlock = isCodeBlock; currentWidth = 0; continue; } @@ -424,8 +432,9 @@ bool MdReaderActivity::wordWrapParsedLine(const MdParser::ParsedLine& parsed, in currentLine.spans.push_back({remaining.substr(0, breakPos), style}); outLines.push_back(std::move(currentLine)); + firstLine = false; currentLine = RenderedLine(); - currentLine.indent = indent; + currentLine.indent = indent + (firstLine ? 0 : listPrefixIndent); currentLine.isCodeBlock = isCodeBlock; currentWidth = 0; diff --git a/test/md_parser/MdParserTest.cpp b/test/md_parser/MdParserTest.cpp index de1c9f34..57492dc3 100644 --- a/test/md_parser/MdParserTest.cpp +++ b/test/md_parser/MdParserTest.cpp @@ -78,6 +78,26 @@ void testUnderscoreBoldWorks() { PASS(); } +void testNestedUnorderedListIndentLevel() { + printf("testNestedUnorderedListIndentLevel...\n"); + auto parsed = MdParser::parseLine(" - nested item", false); + ASSERT_EQ(parsed.blockType, MdParser::BlockType::UnorderedList); + ASSERT_EQ(parsed.listPrefix, "\xe2\x80\xa2 "); + ASSERT_EQ(parsed.indentLevel, 1); + ASSERT_EQ(flattenText(parsed.spans), "nested item"); + PASS(); +} + +void testNestedOrderedListIndentLevel() { + printf("testNestedOrderedListIndentLevel...\n"); + auto parsed = MdParser::parseLine(" 1. nested ordered", false); + ASSERT_EQ(parsed.blockType, MdParser::BlockType::OrderedList); + ASSERT_EQ(parsed.listPrefix, "1. "); + ASSERT_EQ(parsed.indentLevel, 2); + ASSERT_EQ(flattenText(parsed.spans), "nested ordered"); + PASS(); +} + int main() { printf("=== Markdown Parser Tests ===\n\n"); @@ -86,6 +106,8 @@ int main() { testUnderscoreEmphasisStillWorks(); testAsteriskEmphasisStillWorks(); testUnderscoreBoldWorks(); + testNestedUnorderedListIndentLevel(); + testNestedOrderedListIndentLevel(); printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed); return testsFailed > 0 ? 1 : 0;