diff --git a/README.md b/README.md index 6f32b69..37e54e5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ imgui_markdown currently supports the following markdown functionality: * Link * Image * Horizontal rule + * Tables ![imgui_markdown demo live editing](https://github.com/enkisoftware/Media/blob/main/imgui_markdown_demo_live_editing.gif?raw=true) diff --git a/imgui_markdown.h b/imgui_markdown.h index 10ef108..5b37af1 100644 --- a/imgui_markdown.h +++ b/imgui_markdown.h @@ -87,7 +87,7 @@ ___ // Example use on Windows with links opening in a browser -#include "ImGui.h" // https://github.com/ocornut/imgui +#include "imgui.h" // https://github.com/ocornut/imgui #include "imgui_markdown.h" // https://github.com/juliettef/imgui_markdown #include "IconsFontAwesome5.h" // https://github.com/juliettef/IconFontCppHeaders @@ -284,6 +284,8 @@ namespace ImGui UNORDERED_LIST, LINK, EMPHASIS, + CODE, + TABLE, }; struct MarkdownFormatInfo @@ -357,6 +359,7 @@ namespace ImGui struct Line; inline void UnderLine( ImColor col_ ); inline void RenderLine( const char* markdown_, Line& line_, TextRegion& textRegion_, const MarkdownConfig& mdConfig_ ); + inline void RenderTableRow( const char* markdown_, int lineStart_, int lineEnd_, int columnCount_, bool isHeader_, const MarkdownConfig& mdConfig_ ); struct TextRegion { @@ -400,8 +403,11 @@ namespace ImGui struct Line { bool isHeading = false; bool isEmphasis = false; + bool isCode = false; bool isUnorderedListStart = false; bool isLeadingSpace = true; // spaces at start of line + bool isTableRow = false; + bool isTableSeparator = false; int leadSpaceCount = 0; int headingCount = 0; int emphasisCount = 0; @@ -433,6 +439,15 @@ namespace ImGui int num_brackets_open = 0; }; + struct Code { + enum State { + NONE, + LEFT, + }; + State state = NONE; + TextBlock text; + }; + struct Emphasis { enum EmphasisState { NONE, @@ -445,6 +460,19 @@ namespace ImGui char sym; }; + struct Table { + enum State { + NONE, + HEADER, + SEPARATOR, + ROWS, + }; + State state = NONE; + int columnCount = 0; + int rowStart = 0; + bool active = false; + }; + inline void UnderLine( ImColor col_ ) { ImVec2 min = ImGui::GetItemRectMin(); @@ -453,6 +481,58 @@ namespace ImGui ImGui::GetWindowDrawList()->AddLine( min, max, col_, 1.0f ); } + inline void RenderTableRow( const char* markdown_, int lineStart_, int lineEnd_, int columnCount_, bool isHeader_, const MarkdownConfig& mdConfig_ ) + { + // Parse cells separated by pipes + int cellStart = lineStart_; + int cellIndex = 0; + + // Skip leading pipe + while( cellStart < lineEnd_ && markdown_[cellStart] == '|' ) + { + cellStart++; + } + + for( int i = cellStart; i <= lineEnd_ && cellIndex < columnCount_; ++i ) + { + if( i == lineEnd_ || markdown_[i] == '|' ) + { + // Found end of cell + int cellEnd = i; + + // Trim leading/trailing spaces + while( cellStart < cellEnd && markdown_[cellStart] == ' ' ) + { + cellStart++; + } + while( cellEnd > cellStart && markdown_[cellEnd - 1] == ' ' ) + { + cellEnd--; + } + + ImGui::TableNextColumn(); + + if( isHeader_ ) + { + MarkdownFormatInfo formatInfo; + formatInfo.config = &mdConfig_; + formatInfo.type = MarkdownFormatType::EMPHASIS; + formatInfo.level = 2; // Bold for headers + mdConfig_.formatCallback( formatInfo, true ); + ImGui::TextUnformatted( markdown_ + cellStart, markdown_ + cellEnd ); + mdConfig_.formatCallback( formatInfo, false ); + } + else + { + ImGui::TextUnformatted( markdown_ + cellStart, markdown_ + cellEnd ); + } + + cellStart = i + 1; + cellIndex++; + } + } + } + inline void RenderLine( const char* markdown_, Line& line_, TextRegion& textRegion_, const MarkdownConfig& mdConfig_ ) { // indent @@ -496,6 +576,13 @@ namespace ImGui const char* text = markdown_ + textStart; textRegion_.RenderTextWrapped(text, text + textSize); } + else if( line_.isCode ) + { + formatInfo.type = MarkdownFormatType::CODE; + mdConfig_.formatCallback( formatInfo, true ); + const char* text = markdown_ + textStart; + textRegion_.RenderTextWrapped( text, text + textSize ); + } else // render a normal paragraph chunk { formatInfo.type = MarkdownFormatType::NORMAL_TEXT; @@ -529,6 +616,8 @@ namespace ImGui Line prevLine; Link link; Emphasis em; + Code code; + Table table; TextRegion textRegion; int concurrentEmptyNewlines = 0; bool appliedExtraNewline = false; @@ -610,9 +699,66 @@ namespace ImGui continue; } } + else if( c == '|' ) + { + // Detect table rows + line.isTableRow = true; + line.isLeadingSpace = false; + + // Count columns by counting pipes + int pipeCount = 0; + int j = i; + bool isAllDashes = true; + bool hasDash = false; + + while( j < (int)markdownLength_ && markdown_[j] != '\n' ) + { + if( markdown_[j] == '|' ) + { + pipeCount++; + } + else if( markdown_[j] == '-' ) + { + hasDash = true; + } + else if( markdown_[j] != ' ' && markdown_[j] != ':' ) + { + isAllDashes = false; + } + j++; + } + + // Check if this is a separator row (all dashes and pipes) + if( isAllDashes && hasDash && pipeCount >= 2 ) + { + line.isTableSeparator = true; + table.state = Table::SEPARATOR; + table.columnCount = pipeCount - 1; + } + else if( pipeCount >= 2 ) + { + if( table.state == Table::NONE ) + { + table.state = Table::HEADER; + table.columnCount = pipeCount - 1; + } + else if( table.state == Table::SEPARATOR ) + { + table.state = Table::ROWS; + } + } + } } } + // Check if we need to end an active table + if( !line.isLeadingSpace && table.active && !line.isTableRow ) + { + // We're in a table but this line is not a table row - end the table + ImGui::EndTable(); + table = Table(); + } + if ( (mdConfig_.formatFlags & ImGuiMarkdownFormatFlags_DiscardExtraNewLines) ) { // In markdown spec, 2 or more consecutive newlines gets converted to a single blank @@ -623,6 +769,56 @@ namespace ImGui } } + // Skip inline parsing for table rows + if( line.isTableRow ) + { + // Just continue to newline handling + if( c != '\n' ) + { + continue; + } + } + + if( code.state == Code::NONE && c == '`' && !line.isHeading && link.state == Link::NO_LINK ) + { + int lineEnd = i; + if( lineEnd > line.lineStart ) + { + line.lineEnd = lineEnd; + RenderLine( markdown_, line, textRegion, mdConfig_ ); + ImGui::SameLine( 0.0f, 0.0f ); + line.isUnorderedListStart = false; + line.leadSpaceCount = 0; + } + code.state = Code::LEFT; + code.text.start = i; + line.isCode = true; + line.lineStart = i + 1; + line.lastRenderPosition = i; + continue; + } + else if( code.state == Code::LEFT ) + { + if( c == '`' ) + { + line.lineEnd = i; + if( line.lineEnd > line.lineStart ) + { + RenderLine( markdown_, line, textRegion, mdConfig_ ); + ImGui::SameLine( 0.0f, 0.0f ); + } + code.state = Code::NONE; + line.isCode = false; + line.lineStart = i + 1; + line.lastRenderPosition = i; + continue; + } + if( c != '\n' ) + { + continue; + } + } + // Test to see if we have a link switch( link.state ) { @@ -703,6 +899,7 @@ namespace ImGui } if( ImGui::IsItemHovered() ) { + ImGui::SetMouseCursor( ImGuiMouseCursor_Hand ); if( ImGui::IsMouseReleased( 0 ) && mdConfig_.linkCallback && useLinkCallback ) { mdConfig_.linkCallback( { markdown_ + link.text.start, link.text.size(), markdown_ + link.url.start, link.url.size(), mdConfig_.userData, true } ); @@ -831,8 +1028,38 @@ namespace ImGui { ImGui::Separator(); } + else if( line.isTableRow ) + { + // Handle table rendering + if( table.state == Table::HEADER ) + { + // Start new table + if( ImGui::BeginTable( "##mdtable", table.columnCount, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg ) ) + { + table.active = true; + ImGui::TableNextRow( ImGuiTableRowFlags_Headers ); + RenderTableRow( markdown_, line.lineStart, line.lineEnd, table.columnCount, true, mdConfig_ ); + } + } + else if( line.isTableSeparator && table.active ) + { + // Skip separator row, already handled by BeginTable with headers + } + else if( table.state == Table::ROWS && table.active ) + { + // Render data row + ImGui::TableNextRow(); + RenderTableRow( markdown_, line.lineStart, line.lineEnd, table.columnCount, false, mdConfig_ ); + } + } else { + // End table if we were in one + if( table.active ) + { + ImGui::EndTable(); + table = Table(); + } // render the line: multiline emphasis requires a complex implementation so not supporting RenderLine( markdown_, line, textRegion, mdConfig_ ); } @@ -841,6 +1068,7 @@ namespace ImGui prevLine = line; line = Line(); em = Emphasis(); + code = Code(); line.lineStart = i + 1; line.lastRenderPosition = i; @@ -861,6 +1089,13 @@ namespace ImGui } else { + // Close any open table + if( table.active ) + { + ImGui::EndTable(); + table = Table(); + } + // render any remaining text if last char wasn't 0 if( markdownLength_ && line.lineStart < (int)markdownLength_ && markdown_[ line.lineStart ] != 0 ) { @@ -904,6 +1139,7 @@ namespace ImGui if(bHovered) { + ImGui::SetMouseCursor( ImGuiMouseCursor_Hand ); if( ImGui::IsMouseReleased( 0 ) && mdConfig_.linkCallback ) { mdConfig_.linkCallback( { markdown_ + link_.text.start, link_.text.size(), markdown_ + link_.url.start, link_.url.size(), mdConfig_.userData, false } ); @@ -1090,7 +1326,7 @@ namespace ImGui { if( fmt.font ) { - #ifdef IMGUI_HAS_TEXTURES // used to detect dynamic font capability: + #ifdef IMGUI_HAS_TEXTURES // used to detect dynamic font capability: ImGui::PushFont( fmt.font, 0.0f ); // Change font and keep current size #else ImGui::PushFont( fmt.font ); @@ -1154,6 +1390,16 @@ namespace ImGui } case MarkdownFormatType::UNORDERED_LIST: break; + case MarkdownFormatType::CODE: + if( start_ ) + { + ImGui::PushStyleColor( ImGuiCol_Text, ImGui::GetStyle().Colors[ ImGuiCol_TextDisabled ] ); + } + else + { + ImGui::PopStyleColor(); + } + break; case MarkdownFormatType::LINK: if( start_ ) { @@ -1172,6 +1418,9 @@ namespace ImGui } } break; + case MarkdownFormatType::TABLE: + // Tables are handled inline during rendering + break; } } }