From b4d4d3ed1e91e2d14b5bc3e79a5b315cc1944f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Fri, 29 Aug 2025 01:30:36 -0300 Subject: [PATCH] Add syntax highlighting for CSV and TSV files. --- src/eepp/system/parsermatcher.cpp | 158 ++++++++++++++++++ .../src/eepp/ui/doc/languages/csv.cpp | 35 ++++ .../src/eepp/ui/doc/languages/csv.hpp | 14 ++ .../src/eepp/ui/doc/languages/shellscript.cpp | 2 +- .../ui/doc/languagessyntaxhighlighting.cpp | 25 ++- 5 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/csv.cpp create mode 100644 src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/csv.hpp diff --git a/src/eepp/system/parsermatcher.cpp b/src/eepp/system/parsermatcher.cpp index dfeb90eaa..2de2cbc68 100644 --- a/src/eepp/system/parsermatcher.cpp +++ b/src/eepp/system/parsermatcher.cpp @@ -11,6 +11,148 @@ enum LanguageStandard { // Helper functions (needed by the logic, inferred from original code) namespace { // Use an anonymous namespace to keep helpers local to this translation unit +/** + * @brief Parses a single field in CSV/TSV, handling quoted content and including trailing separator + * + * @param input The input string + * @param pos Current position (will be updated) + * @param length Total length of input + * @param delimiter Field delimiter (',' for CSV, '\t' for TSV) + * @param startPos Output: start position of the field content + * @param endPos Output: end position of the field content (including separator if present) + * @return true if a field was successfully parsed + */ +inline bool parseCSVField( const char* input, int& pos, int length, char delimiter, int& startPos, + int& endPos ) { + if ( pos >= length ) { + return false; + } + + startPos = pos; + bool inQuotes = false; + bool hasQuotes = false; + + // Check if field starts with quote + if ( input[pos] == '"' ) { + hasQuotes = true; + inQuotes = true; + pos++; // Skip opening quote + } + + while ( pos < length ) { + char c = input[pos]; + + if ( hasQuotes && inQuotes ) { + if ( c == '"' ) { + // Check for escaped quote (double quote) + if ( pos + 1 < length && input[pos + 1] == '"' ) { + pos += 2; // Skip both quotes + continue; + } else { + // End of quoted field + pos++; // Skip closing quote + inQuotes = false; + + // Skip any whitespace until delimiter or end of line + while ( pos < length && input[pos] != delimiter && input[pos] != '\n' && + input[pos] != '\r' && + std::isspace( static_cast( input[pos] ) ) ) { + pos++; + } + + // Include delimiter if present + if ( pos < length && input[pos] == delimiter ) { + endPos = pos + 1; // Include the delimiter + pos++; // Move past delimiter + } else { + endPos = pos; // End of line or end of input + } + return true; + } + } else if ( c == '\n' || c == '\r' ) { + // Unescaped newline in quoted field - invalid + return false; + } else { + pos++; + } + } else { + // Not in quotes or unquoted field + if ( c == delimiter ) { + // Found delimiter - include it in the field + endPos = pos + 1; // Include the delimiter + pos++; // Move past delimiter + return true; + } else if ( c == '\n' || c == '\r' ) { + // End of line - don't include newline + endPos = pos; + return true; + } else if ( c == '"' && !hasQuotes ) { + // Quote in middle of unquoted field - treat as regular character + pos++; + } else { + pos++; + } + } + } + + // Reached end of input + endPos = pos; + return startPos < endPos; // Return true if we have any content +} + +/** + * @brief Parses a complete CSV/TSV line and creates match groups for each column + * + * @param input The input string + * @param startOffset Starting offset in the string + * @param length Total length of input + * @param delimiter Field delimiter (',' for CSV, '\t' for TSV) + * @param matchList Output array for match ranges + * @param maxMatches Maximum number of matches to store + * @return Number of columns found + */ +inline size_t parseCSVLine( const char* input, int startOffset, int length, char delimiter, + PatternMatcher::Range* matchList, size_t maxMatches ) { + if ( startOffset < 0 || startOffset >= length || !matchList ) { + return 0; + } + + int pos = startOffset; + size_t columnCount = 0; + + // Find end of current line + int lineEnd = pos; + while ( lineEnd < length && input[lineEnd] != '\n' && input[lineEnd] != '\r' ) { + lineEnd++; + } + + // If line is empty, return 0 + if ( pos >= lineEnd ) { + return 0; + } + + // Set the first match to cover the entire line + matchList[0].start = startOffset; + matchList[0].end = lineEnd; + columnCount = 1; + + // Parse individual fields/columns + while ( pos < lineEnd && columnCount < maxMatches ) { + int fieldStart, fieldEnd; + + if ( parseCSVField( input, pos, lineEnd, delimiter, fieldStart, fieldEnd ) ) { + // Add column match (group index starts from 1, 0 is the whole line) + matchList[columnCount].start = fieldStart; + matchList[columnCount].end = fieldEnd; + columnCount++; + } else { + break; + } + } + + return columnCount; +} + inline bool isBinaryDigit( char c ) { return c == '0' || c == '1'; } @@ -1049,6 +1191,22 @@ void ParserMatcherManager::registerBaseParsers() { return isJavaScriptRegEx( stringSearch, stringStartOffset, matchList, stringLength ); } ); + // CSV Parser - detects CSV fields and creates match groups for each column + registerParser( "csv_parser", []( const char* stringSearch, int stringStartOffset, + PatternMatcher::Range* matchList, size_t stringLength ) { + const size_t MAX_CSV_COLUMNS = 63; + return parseCSVLine( stringSearch, stringStartOffset, stringLength, ',', matchList, + std::min( MAX_CSV_COLUMNS, stringLength ) ); + } ); + + // TSV Parser - detects TSV fields and creates match groups for each column + registerParser( "tsv_parser", []( const char* stringSearch, int stringStartOffset, + PatternMatcher::Range* matchList, size_t stringLength ) { + const size_t MAX_TSV_COLUMNS = 63; + return parseCSVLine( stringSearch, stringStartOffset, stringLength, '\t', matchList, + std::min( MAX_TSV_COLUMNS, stringLength ) ); + } ); + mRegisteredBaseParsers = true; } diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/csv.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/csv.cpp new file mode 100644 index 000000000..e0c4c2945 --- /dev/null +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/csv.cpp @@ -0,0 +1,35 @@ +#include +#include + +namespace EE { namespace UI { namespace Doc { namespace Language { + +#define CSV_COLORING_PATTERN \ + "keyword", "type", "parameter", "string", "operator", "number", "literal", "function", + +SyntaxDefinition& addCSV() { + return SyntaxDefinitionManager::instance()->add( + { "CSV", + { "%.csv$" }, + { { { "csv_parser" }, + { "string", CSV_COLORING_PATTERN CSV_COLORING_PATTERN CSV_COLORING_PATTERN + CSV_COLORING_PATTERN CSV_COLORING_PATTERN CSV_COLORING_PATTERN + CSV_COLORING_PATTERN CSV_COLORING_PATTERN }, + "", + SyntaxPatternMatchType::Parser } }, + {} } ); +} + +SyntaxDefinition& addTSV() { + return SyntaxDefinitionManager::instance()->add( + { "TSV", + { "%.tsv$" }, + { { { "tsv_parser" }, + { "string", CSV_COLORING_PATTERN CSV_COLORING_PATTERN CSV_COLORING_PATTERN + CSV_COLORING_PATTERN CSV_COLORING_PATTERN CSV_COLORING_PATTERN + CSV_COLORING_PATTERN CSV_COLORING_PATTERN }, + "", + SyntaxPatternMatchType::Parser } }, + {} } ); +} + +}}}} // namespace EE::UI::Doc::Language diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/csv.hpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/csv.hpp new file mode 100644 index 000000000..4e811ad00 --- /dev/null +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/csv.hpp @@ -0,0 +1,14 @@ +#ifndef EE_UI_DOC_CSV +#define EE_UI_DOC_CSV + +#include + +namespace EE { namespace UI { namespace Doc { namespace Language { + +extern SyntaxDefinition& addCSV(); + +extern SyntaxDefinition& addTSV(); + +}}}} // namespace EE::UI::Doc::Language + +#endif diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/shellscript.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/shellscript.cpp index 74a251e9f..6bed470cd 100644 --- a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/shellscript.cpp +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languages/shellscript.cpp @@ -24,7 +24,7 @@ SyntaxDefinition& addShellScript() { { { "%f[%S][%+%-][%w%-_]+%f[=]" }, "function" }, { { "(%s%-%a[%w_%-]*%s+)(%d[%d%.]+)" }, { "normal", "function", "number" } }, { { "(%s%-%a[%w_%-]*%s+)(%a[%a%-_:=]+)" }, { "normal", "function", "symbol" } }, - { { "[_%a][%w_]+%f[%+=]" }, "type" }, + { { "[_%a][%w_-]+%f[%+=]" }, "type" }, { { "${.-}" }, "type" }, { { "$[%d$%a_@*][%w_]*" }, "type" }, { { "[%a_%-][%w_%-]*[%s]*%f[(]" }, "function" }, diff --git a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languagessyntaxhighlighting.cpp b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languagessyntaxhighlighting.cpp index 612f2f043..caa6b674c 100644 --- a/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languagessyntaxhighlighting.cpp +++ b/src/modules/languages-syntax-highlighting/src/eepp/ui/doc/languagessyntaxhighlighting.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -244,6 +245,18 @@ void LanguagesSyntaxHighlighting::load() { { "%.cr$" }, } ); + sdm->addPreDefinition( { + "Curry", + []() -> SyntaxDefinition& { return addCurry(); }, + { "%.curry$" }, + } ); + + sdm->addPreDefinition( { + "CSV", + []() -> SyntaxDefinition& { return addCSV(); }, + { "%.csv$" }, + } ); + sdm->addPreDefinition( { "C#", []() -> SyntaxDefinition& { return addCSharp(); }, @@ -252,12 +265,6 @@ void LanguagesSyntaxHighlighting::load() { "csharp", } ); - sdm->addPreDefinition( { - "Curry", - []() -> SyntaxDefinition& { return addCurry(); }, - { "%.curry$" }, - } ); - sdm->addPreDefinition( { "eC", []() -> SyntaxDefinition& { return addeC(); }, @@ -726,6 +733,12 @@ void LanguagesSyntaxHighlighting::load() { {}, "typescriptreact" } ); + sdm->addPreDefinition( { + "TSV", + []() -> SyntaxDefinition& { return addTSV(); }, + { "%.tsv$" }, + } ); + sdm->addPreDefinition( { "Typst", []() -> SyntaxDefinition& { return addTypst(); }, { "%.typ$" } } );