#include "MdParser.h" #include namespace MdParser { static EpdFontFamily::Style combineFlags(bool bold, bool italic) { if (bold && italic) return EpdFontFamily::BOLD_ITALIC; if (bold) return EpdFontFamily::BOLD; if (italic) return EpdFontFamily::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++; 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) { 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; char marker = 0; int count = 0; for (char c : line) { if (c == ' ' || c == '\t') continue; if (c == '-' || c == '*' || c == '_') { if (marker == 0) marker = c; if (c != marker) return false; count++; } else { return false; } } return count >= 3; } std::vector parseInline(const std::string& text) { std::vector spans; std::string current; bool bold = false; bool italic = false; size_t i = 0; auto emitSpan = [&]() { if (!current.empty()) { spans.push_back({std::move(current), combineFlags(bold, italic)}); current.clear(); } }; char boldMarker = 0; char italicMarker = 0; while (i < text.size()) { char c = text[i]; // Escaped character if (c == '\\' && i + 1 < text.size()) { char next = text[i + 1]; if (next == '*' || next == '_' || next == '`' || next == '[' || next == '!' || next == '\\') { current += next; i += 2; continue; } } // *** 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); i += 3; continue; } emitSpan(); 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; } // ** 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(); if (bold && boldMarker == c) { bold = false; boldMarker = 0; } else { bold = true; boldMarker = c; } i += 2; continue; } // * or _ — toggle italic if (c == '*' || c == '_') { if (c == '_' && !isUnderscoreEmphasis(text, i, 1)) { current.push_back(c); i += 1; continue; } emitSpan(); if (italic && italicMarker == c) { italic = false; italicMarker = 0; } else { italic = true; italicMarker = c; } i += 1; continue; } // Backtick code span — strip backticks, render as regular if (c == '`') { size_t end = text.find('`', i + 1); if (end != std::string::npos) { emitSpan(); spans.push_back({text.substr(i + 1, end - i - 1), EpdFontFamily::REGULAR}); i = end + 1; continue; } current += c; i++; continue; } // Image ![alt](url) — show [alt] if (c == '!' && i + 1 < text.size() && text[i + 1] == '[') { size_t closeBracket = text.find(']', i + 2); if (closeBracket != std::string::npos && closeBracket + 1 < text.size() && text[closeBracket + 1] == '(') { size_t closeParen = text.find(')', closeBracket + 2); if (closeParen != std::string::npos) { std::string alt = text.substr(i + 2, closeBracket - i - 2); current += "["; current += alt; current += "]"; i = closeParen + 1; continue; } } current += c; i++; continue; } // Link [text](url) — show text only if (c == '[') { size_t closeBracket = text.find(']', i + 1); if (closeBracket != std::string::npos && closeBracket + 1 < text.size() && text[closeBracket + 1] == '(') { size_t closeParen = text.find(')', closeBracket + 2); if (closeParen != std::string::npos) { current += text.substr(i + 1, closeBracket - i - 1); i = closeParen + 1; continue; } } current += c; i++; continue; } current += c; i++; } emitSpan(); // If bold/italic were left open, the text had unmatched markers. // The spans are still usable — the trailing text just keeps the toggled style. return spans; } bool isCodeFence(const std::string& line) { auto trimmed = trimLeft(line); if (trimmed.size() < 3) return false; // Must start with ``` (with optional language tag after) if (trimmed[0] == '`' && trimmed[1] == '`' && trimmed[2] == '`') return true; // Also support ~~~ fences if (trimmed[0] == '~' && trimmed[1] == '~' && trimmed[2] == '~') return true; return false; } // Detect task list checkbox at start of list content, update prefix accordingly. // Returns content with the checkbox marker stripped. static std::string handleTaskList(const std::string& content, std::string& listPrefix) { if (content.size() >= 3 && content[0] == '[' && content[2] == ']') { char mark = content[1]; if (mark == 'x' || mark == 'X') { listPrefix = "☑ "; } else if (mark == ' ') { listPrefix = "☐ "; } else { return content; // Not a checkbox — keep content as-is } size_t skip = 3; if (skip < content.size() && content[skip] == ' ') skip++; return content.substr(skip); } return content; } ParsedLine parseLine(const std::string& rawLine, bool inCodeBlock) { ParsedLine result; // Inside a code block: either closing fence or verbatim text if (inCodeBlock) { if (isCodeFence(rawLine)) { result.blockType = BlockType::CodeBlock; return result; } result.blockType = BlockType::CodeBlock; result.spans.push_back({rawLine, EpdFontFamily::REGULAR}); return result; } // Opening code fence if (isCodeFence(rawLine)) { result.blockType = BlockType::CodeBlock; return result; } // 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] == ' ') leadingSpaces++; else if (rawLine[i] == '\t') leadingSpaces += TAB_WIDTH; // Treat tab as 4 spaces to match CommonMark nesting rules else break; } result.indentLevel = parseListIndentLevel(leadingSpaces); std::string trimmed = trimLeft(rawLine); // Blank line if (trimmed.empty()) { result.blockType = BlockType::BlankLine; return result; } // Horizontal rule (must check BEFORE unordered list since --- and *** overlap) if (isHorizontalRuleLine(trimmed)) { result.blockType = BlockType::HorizontalRule; return result; } // ATX headers: # H1, ## H2, ### H3+ if (trimmed[0] == '#') { int level = 0; size_t pos = 0; while (pos < trimmed.size() && trimmed[pos] == '#') { level++; pos++; } if (pos < trimmed.size() && trimmed[pos] == ' ') { std::string content = trimmed.substr(pos + 1); // Strip optional trailing # sequence size_t trail = content.size(); while (trail > 0 && content[trail - 1] == '#') trail--; while (trail > 0 && content[trail - 1] == ' ') trail--; if (trail < content.size()) content = content.substr(0, trail); if (level <= 1) result.blockType = BlockType::Header1; else if (level == 2) result.blockType = BlockType::Header2; else result.blockType = BlockType::Header3; result.spans = parseInline(content); // Force bold on all header spans for (auto& span : result.spans) { if (span.style == EpdFontFamily::REGULAR) span.style = EpdFontFamily::BOLD; else if (span.style == EpdFontFamily::ITALIC) span.style = EpdFontFamily::BOLD_ITALIC; } return result; } } // Unordered list: - , * , + (marker followed by space) if (trimmed.size() > 1 && trimmed[1] == ' ' && (trimmed[0] == '-' || trimmed[0] == '*' || trimmed[0] == '+')) { result.blockType = BlockType::UnorderedList; result.listPrefix = "\xe2\x80\xa2 "; // "• " std::string content = handleTaskList(trimmed.substr(2), result.listPrefix); result.spans = parseInline(content); return result; } // Ordered list: 1. , 2. , etc. (up to 3-digit number) { size_t dotPos = trimmed.find(". "); if (dotPos != std::string::npos && dotPos <= 3 && dotPos > 0) { bool allDigits = true; for (size_t j = 0; j < dotPos; j++) { if (!std::isdigit(static_cast(trimmed[j]))) { allDigits = false; break; } } if (allDigits) { result.blockType = BlockType::OrderedList; result.listPrefix = trimmed.substr(0, dotPos + 2); // e.g. "1. " std::string content = handleTaskList(trimmed.substr(dotPos + 2), result.listPrefix); result.spans = parseInline(content); return result; } } } // Blockquote: > text if (trimmed[0] == '>') { result.blockType = BlockType::Blockquote; std::string content = trimmed.substr(1); if (!content.empty() && content[0] == ' ') content = content.substr(1); result.spans = parseInline(content); return result; } // Default: paragraph result.blockType = BlockType::Paragraph; result.spans = parseInline(trimmed); return result; } } // namespace MdParser