@@ -17,6 +17,17 @@ static std::string trimLeft(const std::string& s) {
|
||||
return s.substr(i);
|
||||
}
|
||||
|
||||
static bool isWordChar(char c) { return std::isalnum(static_cast<unsigned char>(c)) || c == '_'; }
|
||||
|
||||
static bool isUnderscoreEmphasis(const std::string& text, size_t pos, size_t count) {
|
||||
if (pos == 0 || pos + count >= text.size()) {
|
||||
return true;
|
||||
}
|
||||
const char before = text[pos - 1];
|
||||
const char after = text[pos + count];
|
||||
return !(isWordChar(before) && isWordChar(after));
|
||||
}
|
||||
|
||||
static bool isHorizontalRuleLine(const std::string& line) {
|
||||
// Must be at least 3 chars of the same marker (-, *, _) with optional spaces
|
||||
if (line.size() < 3) return false;
|
||||
@@ -65,6 +76,11 @@ std::vector<Span> parseInline(const std::string& text) {
|
||||
|
||||
// *** or ___ — toggle both bold and italic
|
||||
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);
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
emitSpan();
|
||||
bold = !bold;
|
||||
italic = !italic;
|
||||
@@ -74,6 +90,11 @@ std::vector<Span> parseInline(const std::string& text) {
|
||||
|
||||
// ** or __ — toggle bold
|
||||
if ((c == '*' || c == '_') && i + 1 < text.size() && text[i + 1] == c) {
|
||||
if (c == '_' && !isUnderscoreEmphasis(text, i, 2)) {
|
||||
current.append(2, c);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
emitSpan();
|
||||
bold = !bold;
|
||||
i += 2;
|
||||
@@ -82,6 +103,11 @@ std::vector<Span> parseInline(const std::string& text) {
|
||||
|
||||
// * or _ — toggle italic
|
||||
if (c == '*' || c == '_') {
|
||||
if (c == '_' && !isUnderscoreEmphasis(text, i, 1)) {
|
||||
current.push_back(c);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
emitSpan();
|
||||
italic = !italic;
|
||||
i += 1;
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
#include "CrossPointSettings.h"
|
||||
#include "activities/Activity.h"
|
||||
|
||||
struct MdHeading {
|
||||
size_t offset = 0;
|
||||
int level = 1;
|
||||
std::string title;
|
||||
int pageIndex = -1;
|
||||
};
|
||||
|
||||
class MdReaderActivity final : public Activity {
|
||||
std::unique_ptr<Txt> txt;
|
||||
|
||||
@@ -26,6 +33,11 @@ class MdReaderActivity final : public Activity {
|
||||
std::vector<size_t> pageOffsets;
|
||||
std::vector<uint8_t> pageCodeBlockState; // 1 if page starts inside a code block
|
||||
std::vector<RenderedLine> currentPageLines;
|
||||
std::vector<uint8_t> pageBuffer;
|
||||
|
||||
std::vector<MdHeading> headings;
|
||||
int currentHeadingIndex = -1;
|
||||
|
||||
int linesPerPage = 0;
|
||||
int viewportWidth = 0;
|
||||
bool initialized = false;
|
||||
@@ -55,6 +67,13 @@ class MdReaderActivity final : public Activity {
|
||||
void savePageIndexCache() const;
|
||||
void saveProgress() const;
|
||||
void loadProgress();
|
||||
void scanHeadings();
|
||||
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.
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
#include <Serialization.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "MdReaderTocSelectionActivity.h"
|
||||
#include "ReaderUtils.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
@@ -19,8 +21,18 @@
|
||||
|
||||
namespace {
|
||||
constexpr size_t CHUNK_SIZE = 8 * 1024;
|
||||
constexpr size_t MAX_LINE_LENGTH = 64 * 1024;
|
||||
constexpr unsigned long HEADING_SKIP_MS = 700;
|
||||
constexpr uint32_t CACHE_MAGIC = 0x4D4B4449; // "MKDI"
|
||||
constexpr uint8_t CACHE_VERSION = 3; // Bumped: nested list indent + task checkboxes
|
||||
|
||||
static std::string flattenHeadingText(const MdParser::ParsedLine& parsed) {
|
||||
std::string result;
|
||||
for (const auto& span : parsed.spans) {
|
||||
result += span.text;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void MdReaderActivity::onEnter() {
|
||||
@@ -38,11 +50,163 @@ void MdReaderActivity::onEnter() {
|
||||
auto fileName = filePath.substr(filePath.rfind('/') + 1);
|
||||
APP_STATE.openEpubPath = filePath;
|
||||
APP_STATE.saveToFile();
|
||||
RECENT_BOOKS.addBook(filePath, fileName, "", "");
|
||||
RECENT_BOOKS.addBook(filePath, fileName, "", "", "");
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void MdReaderActivity::assignHeadingPageNumbers() {
|
||||
if (pageOffsets.empty()) {
|
||||
return;
|
||||
}
|
||||
for (auto& heading : headings) {
|
||||
const auto it = std::upper_bound(pageOffsets.begin(), pageOffsets.end(), heading.offset);
|
||||
heading.pageIndex = static_cast<int>((it == pageOffsets.begin()) ? 0 : (it - pageOffsets.begin() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
int MdReaderActivity::getHeadingIndexForOffset(size_t offset) const {
|
||||
if (headings.empty()) {
|
||||
return -1;
|
||||
}
|
||||
int index = -1;
|
||||
for (int i = 0; i < static_cast<int>(headings.size()); i++) {
|
||||
if (headings[i].offset <= offset) {
|
||||
index = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void MdReaderActivity::jumpToHeading(bool next) {
|
||||
if (headings.empty() || pageOffsets.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t currentOffset = (currentPage >= 0 && currentPage < totalPages) ? pageOffsets[currentPage] : 0;
|
||||
int headingIndex = getHeadingIndexForOffset(currentOffset);
|
||||
|
||||
if (headingIndex < 0) {
|
||||
headingIndex = next ? 0 : static_cast<int>(headings.size()) - 1;
|
||||
} else {
|
||||
headingIndex += next ? 1 : -1;
|
||||
}
|
||||
|
||||
if (headingIndex < 0) {
|
||||
headingIndex = 0;
|
||||
} else if (headingIndex >= static_cast<int>(headings.size())) {
|
||||
headingIndex = static_cast<int>(headings.size()) - 1;
|
||||
}
|
||||
|
||||
const size_t headingOffset = headings[headingIndex].offset;
|
||||
const auto it = std::upper_bound(pageOffsets.begin(), pageOffsets.end(), headingOffset);
|
||||
if (it == pageOffsets.begin()) {
|
||||
currentPage = 0;
|
||||
} else {
|
||||
currentPage = static_cast<int>(it - pageOffsets.begin() - 1);
|
||||
}
|
||||
currentHeadingIndex = headingIndex;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void MdReaderActivity::scanHeadings() {
|
||||
headings.clear();
|
||||
if (!txt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t fileSize = txt->getFileSize();
|
||||
if (fileSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string pending;
|
||||
size_t pendingOffset = 0;
|
||||
bool hasPending = false;
|
||||
bool inCodeBlock = false;
|
||||
|
||||
pageBuffer.resize(CHUNK_SIZE + 1);
|
||||
size_t offset = 0;
|
||||
|
||||
while (offset < fileSize) {
|
||||
const size_t toRead = std::min(CHUNK_SIZE, fileSize - offset);
|
||||
if (!txt->readContent(pageBuffer.data(), offset, toRead)) {
|
||||
return;
|
||||
}
|
||||
pageBuffer[toRead] = '\0';
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < toRead) {
|
||||
size_t lineEnd = pos;
|
||||
while (lineEnd < toRead && pageBuffer[lineEnd] != '\n') {
|
||||
lineEnd++;
|
||||
}
|
||||
|
||||
const bool hasNewline = (lineEnd < toRead && pageBuffer[lineEnd] == '\n');
|
||||
const bool fileHasMore = (offset + toRead < fileSize);
|
||||
const size_t rawLen = lineEnd - pos;
|
||||
const bool hasCR = (rawLen > 0 && pageBuffer[pos + rawLen - 1] == '\r');
|
||||
const size_t displayLen = hasCR ? rawLen - 1 : rawLen;
|
||||
const size_t lineStartOffset = hasPending ? pendingOffset : (offset + pos);
|
||||
|
||||
std::string rawLine;
|
||||
if (hasPending) {
|
||||
rawLine = std::move(pending);
|
||||
pending.clear();
|
||||
hasPending = false;
|
||||
}
|
||||
rawLine.append(reinterpret_cast<char*>(pageBuffer.data() + pos), displayLen);
|
||||
|
||||
if (!hasNewline && fileHasMore) {
|
||||
if (!hasPending) {
|
||||
pendingOffset = lineStartOffset;
|
||||
}
|
||||
pending = std::move(rawLine);
|
||||
hasPending = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (MdParser::isCodeFence(rawLine)) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
}
|
||||
const MdParser::ParsedLine parsed = MdParser::parseLine(rawLine, inCodeBlock);
|
||||
if (parsed.blockType == MdParser::BlockType::Header1 || parsed.blockType == MdParser::BlockType::Header2 ||
|
||||
parsed.blockType == MdParser::BlockType::Header3) {
|
||||
int level = 1;
|
||||
if (parsed.blockType == MdParser::BlockType::Header2) {
|
||||
level = 2;
|
||||
} else if (parsed.blockType == MdParser::BlockType::Header3) {
|
||||
level = 3;
|
||||
}
|
||||
headings.push_back({lineStartOffset, level, flattenHeadingText(parsed)});
|
||||
}
|
||||
|
||||
pos = hasNewline ? lineEnd + 1 : lineEnd;
|
||||
}
|
||||
|
||||
offset += toRead;
|
||||
}
|
||||
|
||||
if (hasPending) {
|
||||
if (MdParser::isCodeFence(pending)) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
}
|
||||
const MdParser::ParsedLine parsed = MdParser::parseLine(pending, inCodeBlock);
|
||||
if (parsed.blockType == MdParser::BlockType::Header1 || parsed.blockType == MdParser::BlockType::Header2 ||
|
||||
parsed.blockType == MdParser::BlockType::Header3) {
|
||||
int level = 1;
|
||||
if (parsed.blockType == MdParser::BlockType::Header2) {
|
||||
level = 2;
|
||||
} else if (parsed.blockType == MdParser::BlockType::Header3) {
|
||||
level = 3;
|
||||
}
|
||||
headings.push_back({pendingOffset, level, flattenHeadingText(parsed)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MdReaderActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
@@ -68,17 +232,40 @@ void MdReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !headings.empty()) {
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
ReaderUtils::enforceExitFullRefresh(renderer);
|
||||
startActivityForResult(
|
||||
std::make_unique<MdReaderTocSelectionActivity>(renderer, mappedInput, headings, currentHeadingIndex),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
currentPage = std::get<PageResult>(result.data).page;
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||
if (!prevTriggered && !nextTriggered) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool headingSkip = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > HEADING_SKIP_MS;
|
||||
if (headingSkip && !headings.empty()) {
|
||||
jumpToHeading(nextTriggered);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevTriggered && currentPage > 0) {
|
||||
currentPage--;
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
} else if (nextTriggered) {
|
||||
if (currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
currentHeadingIndex = getHeadingIndexForOffset(pageOffsets[currentPage]);
|
||||
requestUpdate();
|
||||
} else {
|
||||
onGoHome();
|
||||
@@ -107,6 +294,9 @@ void MdReaderActivity::initializeReader() {
|
||||
const int viewportHeight = renderer.getScreenHeight() - cachedOrientedMarginTop - cachedOrientedMarginBottom;
|
||||
const int lineHeight = renderer.getLineHeight(cachedFontId);
|
||||
|
||||
pageBuffer.reserve(CHUNK_SIZE + 1);
|
||||
scanHeadings();
|
||||
|
||||
linesPerPage = viewportHeight / lineHeight;
|
||||
if (linesPerPage < 1) linesPerPage = 1;
|
||||
|
||||
@@ -116,6 +306,7 @@ void MdReaderActivity::initializeReader() {
|
||||
buildPageIndex();
|
||||
savePageIndexCache();
|
||||
}
|
||||
assignHeadingPageNumbers();
|
||||
|
||||
loadProgress();
|
||||
|
||||
@@ -259,42 +450,57 @@ bool MdReaderActivity::loadPageAtOffset(size_t offset, bool startInCodeBlock, st
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t chunkSize = std::min(CHUNK_SIZE, fileSize - offset);
|
||||
auto* buffer = static_cast<uint8_t*>(malloc(chunkSize + 1));
|
||||
if (!buffer) {
|
||||
LOG_ERR("MDR", "Failed to allocate %zu bytes", chunkSize);
|
||||
size_t bufferSize = std::min(CHUNK_SIZE, fileSize - offset);
|
||||
pageBuffer.resize(bufferSize + 1);
|
||||
if (!txt->readContent(pageBuffer.data(), offset, bufferSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!txt->readContent(buffer, offset, chunkSize)) {
|
||||
free(buffer);
|
||||
return false;
|
||||
}
|
||||
buffer[chunkSize] = '\0';
|
||||
pageBuffer[bufferSize] = '\0';
|
||||
|
||||
bool inCodeBlock = startInCodeBlock;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < chunkSize && static_cast<int>(outLines.size()) < linesPerPage) {
|
||||
// Find end of line
|
||||
while (pos < bufferSize && static_cast<int>(outLines.size()) < linesPerPage) {
|
||||
// Find end of line and extend the read buffer if we are at chunk boundary.
|
||||
size_t lineEnd = pos;
|
||||
while (lineEnd < chunkSize && buffer[lineEnd] != '\n') {
|
||||
while (lineEnd < bufferSize && pageBuffer[lineEnd] != '\n') {
|
||||
lineEnd++;
|
||||
}
|
||||
|
||||
// Check if we have a complete line
|
||||
bool lineComplete = (lineEnd < chunkSize) || (offset + lineEnd >= fileSize);
|
||||
while (lineEnd == bufferSize && offset + bufferSize < fileSize && bufferSize < MAX_LINE_LENGTH) {
|
||||
size_t extra = std::min(CHUNK_SIZE, fileSize - offset - bufferSize);
|
||||
if (bufferSize + extra > MAX_LINE_LENGTH) {
|
||||
extra = MAX_LINE_LENGTH - bufferSize;
|
||||
}
|
||||
if (extra == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
pageBuffer.resize(bufferSize + extra + 1);
|
||||
if (!txt->readContent(pageBuffer.data() + bufferSize, offset + bufferSize, extra)) {
|
||||
return false;
|
||||
}
|
||||
bufferSize += extra;
|
||||
pageBuffer[bufferSize] = '\0';
|
||||
|
||||
while (lineEnd < bufferSize && pageBuffer[lineEnd] != '\n') {
|
||||
lineEnd++;
|
||||
}
|
||||
}
|
||||
|
||||
const bool isAtBufferEnd = (lineEnd == bufferSize);
|
||||
const bool isEOF = (offset + bufferSize >= fileSize);
|
||||
const bool lineComplete = (lineEnd < bufferSize) || isEOF || (isAtBufferEnd && bufferSize >= MAX_LINE_LENGTH);
|
||||
if (!lineComplete && !outLines.empty()) {
|
||||
// Incomplete line at chunk boundary and we already have content — stop here
|
||||
break;
|
||||
}
|
||||
|
||||
size_t lineContentLen = lineEnd - pos;
|
||||
bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r');
|
||||
bool hasCR = (lineContentLen > 0 && pageBuffer[pos + lineContentLen - 1] == '\r');
|
||||
size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen;
|
||||
|
||||
std::string rawLine(reinterpret_cast<char*>(buffer + pos), displayLen);
|
||||
std::string rawLine(reinterpret_cast<char*>(pageBuffer.data() + pos), displayLen);
|
||||
|
||||
// Check for code fence toggle
|
||||
bool wasFence = false;
|
||||
@@ -363,8 +569,6 @@ bool MdReaderActivity::loadPageAtOffset(size_t offset, bool startInCodeBlock, st
|
||||
}
|
||||
|
||||
endInCodeBlock = inCodeBlock;
|
||||
free(buffer);
|
||||
|
||||
return !outLines.empty();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "MdReaderTocSelectionActivity.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
int MdReaderTocSelectionActivity::getTotalItems() const { return static_cast<int>(headings.size()); }
|
||||
|
||||
int MdReaderTocSelectionActivity::getPageItems() const {
|
||||
constexpr int lineHeight = 30;
|
||||
const Rect contentRect = UITheme::getContentRect(renderer, true, false);
|
||||
const int startY = 60 + contentRect.y;
|
||||
const int availableHeight = contentRect.y + contentRect.height - startY - lineHeight;
|
||||
return std::max(1, availableHeight / lineHeight);
|
||||
}
|
||||
|
||||
void MdReaderTocSelectionActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
if (selectorIndex < 0 || selectorIndex >= getTotalItems()) {
|
||||
selectorIndex = 0;
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void MdReaderTocSelectionActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void MdReaderTocSelectionActivity::loop() {
|
||||
const int pageItems = getPageItems();
|
||||
const int totalItems = getTotalItems();
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (selectorIndex >= 0 && selectorIndex < totalItems) {
|
||||
setResult(PageResult{static_cast<uint32_t>(headings[selectorIndex].pageIndex)});
|
||||
} else {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
ActivityResult result;
|
||||
result.isCancelled = true;
|
||||
setResult(std::move(result));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
buttonNavigator.onNextRelease([this, totalItems] {
|
||||
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousRelease([this, totalItems] {
|
||||
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
|
||||
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
|
||||
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void MdReaderTocSelectionActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const Rect contentRect = UITheme::getContentRect(renderer, true, false);
|
||||
const int pageItems = getPageItems();
|
||||
const int totalItems = getTotalItems();
|
||||
|
||||
const int titleX =
|
||||
contentRect.x +
|
||||
(contentRect.width - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_SELECT_CHAPTER), EpdFontFamily::BOLD)) / 2;
|
||||
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentRect.y, tr(STR_SELECT_CHAPTER), true, EpdFontFamily::BOLD);
|
||||
|
||||
const int pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||
renderer.fillRect(contentRect.x, 60 + contentRect.y + (selectorIndex % pageItems) * 30 - 2, contentRect.width - 1,
|
||||
30);
|
||||
|
||||
for (int i = 0; i < pageItems; i++) {
|
||||
int itemIndex = pageStartIndex + i;
|
||||
if (itemIndex >= totalItems) break;
|
||||
const int displayY = 60 + contentRect.y + i * 30;
|
||||
const bool isSelected = (itemIndex == selectorIndex);
|
||||
|
||||
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);
|
||||
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, title.c_str(), !isSelected);
|
||||
}
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "MdReaderActivity.h"
|
||||
#include "../Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
class MdReaderTocSelectionActivity final : public Activity {
|
||||
std::vector<MdHeading> headings;
|
||||
ButtonNavigator buttonNavigator;
|
||||
int selectorIndex = 0;
|
||||
|
||||
int getPageItems() const;
|
||||
int getTotalItems() const;
|
||||
|
||||
public:
|
||||
explicit MdReaderTocSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
std::vector<MdHeading> headings, int currentHeadingIndex)
|
||||
: Activity("MdReaderTocSelection", renderer, mappedInput), headings(std::move(headings)), selectorIndex(0) {
|
||||
if (currentHeadingIndex >= 0 && currentHeadingIndex < static_cast<int>(headings.size())) {
|
||||
selectorIndex = currentHeadingIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../../lib/Md/MdParser.h"
|
||||
|
||||
static int testsPassed = 0;
|
||||
static int testsFailed = 0;
|
||||
|
||||
#define ASSERT_EQ(a, b) \
|
||||
do { \
|
||||
if ((a) != (b)) { \
|
||||
fprintf(stderr, " FAIL: %s:%d: %s != %s\n", __FILE__, __LINE__, #a, #b); \
|
||||
testsFailed++; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define PASS() testsPassed++
|
||||
|
||||
static std::string flattenText(const std::vector<MdParser::Span>& spans) {
|
||||
std::string result;
|
||||
for (const auto& span : spans) {
|
||||
result += span.text;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool allRegular(const std::vector<MdParser::Span>& spans) {
|
||||
for (const auto& span : spans) {
|
||||
if (span.style != EpdFontFamily::REGULAR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void testSnakeCaseUnderscoresRemainLiteral() {
|
||||
printf("testSnakeCaseUnderscoresRemainLiteral...\n");
|
||||
auto spans = MdParser::parseInline("foo_bar_baz");
|
||||
ASSERT_EQ(flattenText(spans), "foo_bar_baz");
|
||||
ASSERT_EQ(allRegular(spans), true);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testUnderscoreWithinExpressionRemainsLiteral() {
|
||||
printf("testUnderscoreWithinExpressionRemainsLiteral...\n");
|
||||
auto spans = MdParser::parseInline("a_b + c_d");
|
||||
ASSERT_EQ(flattenText(spans), "a_b + c_d");
|
||||
ASSERT_EQ(allRegular(spans), true);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testUnderscoreEmphasisStillWorks() {
|
||||
printf("testUnderscoreEmphasisStillWorks...\n");
|
||||
auto spans = MdParser::parseInline("foo _bar_ baz");
|
||||
ASSERT_EQ(flattenText(spans), "foo bar baz");
|
||||
ASSERT_EQ(spans.size(), 3);
|
||||
ASSERT_EQ(spans[1].style == EpdFontFamily::ITALIC || spans[1].style == EpdFontFamily::BOLD_ITALIC, true);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testAsteriskEmphasisStillWorks() {
|
||||
printf("testAsteriskEmphasisStillWorks...\n");
|
||||
auto spans = MdParser::parseInline("foo *bar* baz");
|
||||
ASSERT_EQ(flattenText(spans), "foo bar baz");
|
||||
ASSERT_EQ(spans.size(), 3);
|
||||
ASSERT_EQ(spans[1].style == EpdFontFamily::ITALIC || spans[1].style == EpdFontFamily::BOLD_ITALIC, true);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testUnderscoreBoldWorks() {
|
||||
printf("testUnderscoreBoldWorks...\n");
|
||||
auto spans = MdParser::parseInline("foo __bar__ baz");
|
||||
ASSERT_EQ(flattenText(spans), "foo bar baz");
|
||||
ASSERT_EQ(spans.size(), 3);
|
||||
ASSERT_EQ(spans[1].style == EpdFontFamily::BOLD || spans[1].style == EpdFontFamily::BOLD_ITALIC, true);
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
printf("=== Markdown Parser Tests ===\n\n");
|
||||
|
||||
testSnakeCaseUnderscoresRemainLiteral();
|
||||
testUnderscoreWithinExpressionRemainsLiteral();
|
||||
testUnderscoreEmphasisStillWorks();
|
||||
testAsteriskEmphasisStillWorks();
|
||||
testUnderscoreBoldWorks();
|
||||
|
||||
printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed);
|
||||
return testsFailed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user