diff --git a/lib/Md/MdParser.cpp b/lib/Md/MdParser.cpp index a1780874..372d5e1f 100644 --- a/lib/Md/MdParser.cpp +++ b/lib/Md/MdParser.cpp @@ -11,6 +11,8 @@ static EpdFontFamily::Style combineFlags(bool bold, bool italic) { return EpdFontFamily::REGULAR; } +static constexpr int TAB_WIDTH = 4; + static std::string trimLeft(const std::string& s) { size_t i = 0; while (i < s.size() && (s[i] == ' ' || s[i] == '\t')) i++; @@ -61,6 +63,9 @@ std::vector parseInline(const std::string& text) { } }; + char boldMarker = 0; + char italicMarker = 0; + while (i < text.size()) { char c = text[i]; @@ -74,7 +79,7 @@ std::vector parseInline(const std::string& text) { } } - // *** or ___ — toggle both bold and italic + // *** or ___ — toggle both bold and italic when marker matches the current open markers if ((c == '*' || c == '_') && i + 2 < text.size() && text[i + 1] == c && text[i + 2] == c) { if (c == '_' && !isUnderscoreEmphasis(text, i, 3)) { current.append(3, c); @@ -82,8 +87,17 @@ std::vector parseInline(const std::string& text) { continue; } emitSpan(); - bold = !bold; - italic = !italic; + if (bold && italic && boldMarker == c && italicMarker == c) { + bold = false; + italic = false; + boldMarker = 0; + italicMarker = 0; + } else { + bold = true; + italic = true; + boldMarker = c; + italicMarker = c; + } i += 3; continue; } @@ -96,7 +110,13 @@ std::vector parseInline(const std::string& text) { continue; } emitSpan(); - bold = !bold; + if (bold && boldMarker == c) { + bold = false; + boldMarker = 0; + } else { + bold = true; + boldMarker = c; + } i += 2; continue; } @@ -109,7 +129,13 @@ std::vector parseInline(const std::string& text) { continue; } emitSpan(); - italic = !italic; + if (italic && italicMarker == c) { + italic = false; + italicMarker = 0; + } else { + italic = true; + italicMarker = c; + } i += 1; continue; } @@ -190,9 +216,9 @@ static std::string handleTaskList(const std::string& content, std::string& listP if (content.size() >= 3 && content[0] == '[' && content[2] == ']') { char mark = content[1]; if (mark == 'x' || mark == 'X') { - listPrefix = "[x] "; + listPrefix = "☑ "; } else if (mark == ' ') { - listPrefix = "[ ] "; + listPrefix = "☐ "; } else { return content; // Not a checkbox — keep content as-is } @@ -229,11 +255,11 @@ ParsedLine parseLine(const std::string& rawLine, bool inCodeBlock) { if (rawLine[i] == ' ') leadingSpaces++; else if (rawLine[i] == '\t') - leadingSpaces += 2; // Treat tab as 2 spaces + leadingSpaces += TAB_WIDTH; // Treat tab as 4 spaces to match CommonMark nesting rules else break; } - result.indentLevel = static_cast(leadingSpaces / 2); + result.indentLevel = static_cast(leadingSpaces / TAB_WIDTH); std::string trimmed = trimLeft(rawLine); diff --git a/lib/Md/MdParser.h b/lib/Md/MdParser.h index 936db7c2..a4ba8ce5 100644 --- a/lib/Md/MdParser.h +++ b/lib/Md/MdParser.h @@ -27,10 +27,10 @@ enum class BlockType : uint8_t { }; struct ParsedLine { - BlockType blockType; + BlockType blockType = BlockType::Paragraph; std::vector spans; std::string listPrefix; // "• " or "1. " etc. - uint8_t indentLevel = 0; // Nesting depth (each 2 spaces = 1 level) + uint8_t indentLevel = 0; // Nesting depth (each 4 spaces = 1 level) }; // Parse a single raw line of markdown into block type and styled spans. diff --git a/lib/Txt/Txt.cpp b/lib/Txt/Txt.cpp index 692e3850..81c4cb3c 100644 --- a/lib/Txt/Txt.cpp +++ b/lib/Txt/Txt.cpp @@ -40,7 +40,7 @@ std::string Txt::getTitle() const { size_t lastSlash = filepath.find_last_of('/'); std::string filename = (lastSlash != std::string::npos) ? filepath.substr(lastSlash + 1) : filepath; - // Remove .txt or .mdextension + // Remove .txt or .md extension if (FsHelpers::hasTxtExtension(filename)) { filename = filename.substr(0, filename.length() - 4); } else if (FsHelpers::hasMarkdownExtension(filename)) { diff --git a/src/activities/reader/MDReaderActivity.h b/src/activities/reader/MDReaderActivity.h index 7fa9215d..604fc532 100644 --- a/src/activities/reader/MDReaderActivity.h +++ b/src/activities/reader/MDReaderActivity.h @@ -27,6 +27,7 @@ class MdReaderActivity final : public Activity { std::vector spans; int indent = 0; // left indent in pixels bool isHR = false; // draw as horizontal rule + bool isCodeBlock = false; }; // Streaming reader state @@ -71,14 +72,11 @@ class MdReaderActivity final : public Activity { void assignHeadingPageNumbers(); int getHeadingIndexForOffset(size_t offset) const; void jumpToHeading(bool next); - void scanHeadings(); - int getHeadingIndexForOffset(size_t offset) const; - void jumpToHeading(bool next); // Word-wrap a parsed markdown line into one or more RenderedLines. // Returns true if all content was emitted, false if truncated by maxLines. bool wordWrapParsedLine(const MdParser::ParsedLine& parsed, int indent, std::vector& outLines, - int maxLines); + int maxLines, bool isCodeBlock = false); // Measure total pixel width of a span list int measureSpans(const std::vector& spans) const; diff --git a/src/activities/reader/MdReaderActivity.cpp b/src/activities/reader/MdReaderActivity.cpp index b4830ebc..66b005fb 100644 --- a/src/activities/reader/MdReaderActivity.cpp +++ b/src/activities/reader/MdReaderActivity.cpp @@ -320,7 +320,7 @@ int MdReaderActivity::measureSpans(const std::vector& spans) con } bool MdReaderActivity::wordWrapParsedLine(const MdParser::ParsedLine& parsed, int indent, - std::vector& outLines, int maxLines) { + std::vector& outLines, int maxLines, bool isCodeBlock) { const size_t startSize = outLines.size(); if (parsed.spans.empty()) { @@ -347,6 +347,7 @@ bool MdReaderActivity::wordWrapParsedLine(const MdParser::ParsedLine& parsed, in RenderedLine rl; rl.spans = std::move(allSpans); rl.indent = indent; + rl.isCodeBlock = isCodeBlock; outLines.push_back(std::move(rl)); return true; } @@ -354,6 +355,7 @@ bool MdReaderActivity::wordWrapParsedLine(const MdParser::ParsedLine& parsed, in // Word-wrap across spans RenderedLine currentLine; currentLine.indent = indent; + currentLine.isCodeBlock = isCodeBlock; int currentWidth = 0; bool fullyConsumed = true; @@ -423,6 +425,7 @@ bool MdReaderActivity::wordWrapParsedLine(const MdParser::ParsedLine& parsed, in outLines.push_back(std::move(currentLine)); currentLine = RenderedLine(); currentLine.indent = indent; + currentLine.isCodeBlock = isCodeBlock; currentWidth = 0; size_t skip = breakPos; @@ -539,7 +542,8 @@ bool MdReaderActivity::loadPageAtOffset(size_t offset, bool startInCodeBlock, st if (!wasFence) { size_t linesBefore = outLines.size(); int remainingLines = linesPerPage - static_cast(outLines.size()); - bool fullyConsumed = wordWrapParsedLine(parsed, indent, outLines, remainingLines); + bool fullyConsumed = wordWrapParsedLine(parsed, indent, outLines, remainingLines, + parsed.blockType == MdParser::BlockType::CodeBlock); if (!fullyConsumed) { if (linesBefore > 0) { @@ -660,7 +664,12 @@ void MdReaderActivity::renderPage() { // Draw horizontal rule as a thin line int hrY = y + lineHeight / 2; renderer.drawLine(cachedOrientedMarginLeft + line.indent, hrY, cachedOrientedMarginLeft + viewportWidth, hrY); - } else if (!line.spans.empty()) { + } else { + if (line.isCodeBlock) { + const int barX = cachedOrientedMarginLeft + std::max(line.indent - 6, 0); + renderer.drawLine(barX, y + 2, barX, y + lineHeight - 2); + } + if (!line.spans.empty()) { int x = cachedOrientedMarginLeft + line.indent; // Apply text alignment for non-indented lines diff --git a/src/activities/reader/MdReaderTocSelectionActivity.cpp b/src/activities/reader/MdReaderTocSelectionActivity.cpp index 9a33ac7f..79c2ab59 100644 --- a/src/activities/reader/MdReaderTocSelectionActivity.cpp +++ b/src/activities/reader/MdReaderTocSelectionActivity.cpp @@ -1,10 +1,10 @@ #include "MdReaderTocSelectionActivity.h" -#include - #include #include +#include + #include "components/UITheme.h" #include "fontIds.h" @@ -99,7 +99,8 @@ void MdReaderTocSelectionActivity::render(RenderLock&&) { const auto& heading = headings[itemIndex]; const int indentSize = contentRect.x + 20 + (heading.level - 1) * 10; - const std::string title = renderer.truncatedText(UI_10_FONT_ID, heading.title.c_str(), contentRect.width - 40 - indentSize); + const std::string title = + renderer.truncatedText(UI_10_FONT_ID, heading.title.c_str(), contentRect.width - 40 - indentSize); renderer.drawText(UI_10_FONT_ID, indentSize, displayY, title.c_str(), !isSelected); } diff --git a/src/activities/reader/MdReaderTocSelectionActivity.h b/src/activities/reader/MdReaderTocSelectionActivity.h index 101591a9..0d8bc66c 100644 --- a/src/activities/reader/MdReaderTocSelectionActivity.h +++ b/src/activities/reader/MdReaderTocSelectionActivity.h @@ -2,8 +2,8 @@ #include -#include "MdReaderActivity.h" #include "../Activity.h" +#include "MdReaderActivity.h" #include "util/ButtonNavigator.h" class MdReaderTocSelectionActivity final : public Activity { @@ -16,7 +16,7 @@ class MdReaderTocSelectionActivity final : public Activity { public: explicit MdReaderTocSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - std::vector headings, int currentHeadingIndex) + std::vector headings, int currentHeadingIndex) : Activity("MdReaderTocSelection", renderer, mappedInput), headings(std::move(headings)), selectorIndex(0) { if (currentHeadingIndex >= 0 && currentHeadingIndex < static_cast(headings.size())) { selectorIndex = currentHeadingIndex; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 7e7f9325..06a5c05a 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -49,7 +49,7 @@ bool ReaderActivity::isXtcFile(const std::string& path) { return FsHelpers::hasX bool ReaderActivity::isTxtFile(const std::string& path) { return FsHelpers::hasTxtExtension(path); } -bool ReaderActivity::isMDFile(const std::string& path) { return FsHelpers::hasMarkdownExtension(path); } +bool ReaderActivity::isMdFile(const std::string& path) { return FsHelpers::hasMarkdownExtension(path); } bool ReaderActivity::isImageFile(const std::string& path) { return FsHelpers::hasBmpExtension(path) || FsHelpers::hasJpgExtension(path) || FsHelpers::hasPngExtension(path); @@ -167,7 +167,7 @@ void ReaderActivity::onEnter() { return; } onGoToXtcReader(std::move(xtc)); - } else if (isMDFile(initialBookPath)) { + } else if (isMdFile(initialBookPath)) { auto txt = loadTxt(initialBookPath); if (!txt) { onGoBack(); diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index 51258ad1..0292901f 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -16,7 +16,7 @@ class ReaderActivity final : public Activity { static std::unique_ptr loadTxt(const std::string& path); static bool isXtcFile(const std::string& path); static bool isTxtFile(const std::string& path); - static bool isMDFile(const std::string& path); + static bool isMdFile(const std::string& path); static bool isImageFile(const std::string& path); static std::string extractFolderPath(const std::string& filePath);