Add syntax highlighting for CSV and TSV files.

This commit is contained in:
Martín Lucas Golini
2025-08-29 01:30:36 -03:00
parent 39d4c30953
commit b4d4d3ed1e
5 changed files with 227 additions and 7 deletions
+158
View File
@@ -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<unsigned char>( 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;
}
@@ -0,0 +1,35 @@
#include <eepp/ui/doc/languages/d.hpp>
#include <eepp/ui/doc/syntaxdefinitionmanager.hpp>
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
@@ -0,0 +1,14 @@
#ifndef EE_UI_DOC_CSV
#define EE_UI_DOC_CSV
#include <eepp/ui/doc/syntaxdefinition.hpp>
namespace EE { namespace UI { namespace Doc { namespace Language {
extern SyntaxDefinition& addCSV();
extern SyntaxDefinition& addTSV();
}}}} // namespace EE::UI::Doc::Language
#endif
@@ -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" },
@@ -19,6 +19,7 @@
#include <eepp/ui/doc/languages/covscript.hpp>
#include <eepp/ui/doc/languages/crystal.hpp>
#include <eepp/ui/doc/languages/csharp.hpp>
#include <eepp/ui/doc/languages/csv.hpp>
#include <eepp/ui/doc/languages/curry.hpp>
#include <eepp/ui/doc/languages/d.hpp>
#include <eepp/ui/doc/languages/dart.hpp>
@@ -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$" } } );