#pragma once // Step 574: Markdown Spec Parser #include "IntakeTextUtil.h" #include #include #include #include #include struct ParsedRequirementItem { std::string text; std::string sectionTitle; std::string anchor; int line = 0; }; struct ParsedSection { std::string title; std::string anchor; int headingLine = 0; std::vector content; }; struct ParsedMarkdownSpec { std::vector sections; std::vector goals; std::vector constraints; std::vector dependencies; std::vector acceptanceCriteria; }; class MarkdownSpecParser { public: static bool parse(const std::string& markdown, ParsedMarkdownSpec* outSpec, std::string* error) { if (!outSpec || !error) return false; error->clear(); if (markdown.empty()) { *error = "markdown_empty"; return false; } ParsedMarkdownSpec spec; ParsedSection currentSection; bool hasActiveSection = false; std::istringstream stream(markdown); std::string line; int lineNo = 0; while (std::getline(stream, line)) { ++lineNo; if (startsWith(line, "## ")) { if (hasActiveSection) spec.sections.push_back(currentSection); currentSection = ParsedSection{}; currentSection.title = trim(line.substr(3)); currentSection.anchor = makeAnchor(currentSection.title); currentSection.headingLine = lineNo; hasActiveSection = true; continue; } if (!hasActiveSection) continue; currentSection.content.push_back(line); if (!startsWith(trimLeft(line), "- ")) continue; ParsedRequirementItem item; item.text = trim(trimLeft(line).substr(2)); item.sectionTitle = currentSection.title; item.anchor = currentSection.anchor; item.line = lineNo; if (item.text.empty()) continue; const std::string lowerTitle = intakeToLower(currentSection.title); if (contains(lowerTitle, "goal")) { spec.goals.push_back(item); } else if (contains(lowerTitle, "constraint")) { spec.constraints.push_back(item); } else if (contains(lowerTitle, "dependenc")) { spec.dependencies.push_back(item); } else if (contains(lowerTitle, "acceptance")) { spec.acceptanceCriteria.push_back(item); } } if (hasActiveSection) spec.sections.push_back(currentSection); if (spec.sections.empty()) { *error = "no_sections_found"; return false; } *outSpec = spec; return true; } private: static bool startsWith(const std::string& text, const std::string& prefix) { return text.rfind(prefix, 0) == 0; } static bool contains(const std::string& text, const std::string& needle) { return text.find(needle) != std::string::npos; } static std::string trimLeft(const std::string& value) { std::size_t i = 0; while (i < value.size() && std::isspace(static_cast(value[i]))) ++i; return value.substr(i); } static std::string trim(const std::string& value) { std::size_t begin = 0; while (begin < value.size() && std::isspace(static_cast(value[begin]))) ++begin; std::size_t end = value.size(); while (end > begin && std::isspace(static_cast(value[end - 1]))) --end; return value.substr(begin, end - begin); } static std::string makeAnchor(const std::string& title) { std::string anchor = intakeSanitizeToken(title, '-'); return anchor.empty() ? "section" : anchor; } };