From 30fa1bf35eec384eb391f30d5aff46f0c7250213 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 1 Oct 2026 10:19:15 +0200 Subject: [PATCH] Render org tables as styled grids - Parse | delimited rows into cells - Detect header separator (|---+---|) - Header row: bold, text color - Data rows: normal, placeholder color - Same alternate background as code blocks --- ui/qml/OutlineView.qml | 97 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 5 deletions(-) diff --git a/ui/qml/OutlineView.qml b/ui/qml/OutlineView.qml index 4af0d5a..4364e3b 100644 --- a/ui/qml/OutlineView.qml +++ b/ui/qml/OutlineView.qml @@ -57,7 +57,7 @@ ListView { readonly property int sectionLevel: model.level ?? 1 readonly property int indent: (sectionLevel - 1) * outline.indentWidth - // Parse body into text and code block segments + // Parse body into text, code block, and table segments readonly property var bodyParts: { let parts = []; let body = sectionBody; @@ -68,30 +68,65 @@ ListView { let inCode = false; let codeLang = ""; let codeLines = []; + let inTable = false; + let tableLines = []; for (let i = 0; i < lines.length; i++) { let line = lines[i]; let trimmed = line.trim(); - if (!inCode && trimmed.toUpperCase().startsWith("#+BEGIN_SRC")) { + // Code block handling + if (!inCode && !inTable && trimmed.toUpperCase().startsWith("#+BEGIN_SRC")) { // Flush pending text if (currentText.length > 0) { let text = currentText.join('\n').trim(); if (text) parts.push({type: "text", content: text}); currentText = []; } - // Start code block inCode = true; codeLang = trimmed.substring(11).trim().split(/\s+/)[0] || ""; codeLines = []; } else if (inCode && trimmed.toUpperCase() === "#+END_SRC") { - // End code block parts.push({type: "code", language: codeLang, content: codeLines.join('\n')}); inCode = false; codeLang = ""; codeLines = []; } else if (inCode) { codeLines.push(line); + } + // Table handling + else if (!inTable && trimmed.startsWith("|")) { + // Flush pending text + if (currentText.length > 0) { + let text = currentText.join('\n').trim(); + if (text) parts.push({type: "text", content: text}); + currentText = []; + } + inTable = true; + tableLines = [trimmed]; + } else if (inTable && trimmed.startsWith("|")) { + tableLines.push(trimmed); + } else if (inTable) { + // End of table - skip #+TBLFM lines + if (!trimmed.startsWith("#+TBLFM")) { + // Parse table + let rows = []; + let hasHeader = false; + for (let tl of tableLines) { + if (tl.match(/^\|\s*[-+]+\s*\|/)) { + hasHeader = rows.length > 0; + } else { + let cells = tl.split("|").slice(1, -1).map(c => c.trim()); + rows.push(cells); + } + } + parts.push({type: "table", rows: rows, hasHeader: hasHeader}); + tableLines = []; + inTable = false; + + // Process current line as text if not empty + if (trimmed) currentText.push(line); + } } else { currentText.push(line); } @@ -100,6 +135,18 @@ ListView { // Flush remaining if (inCode && codeLines.length > 0) { parts.push({type: "code", language: codeLang, content: codeLines.join('\n')}); + } else if (inTable && tableLines.length > 0) { + let rows = []; + let hasHeader = false; + for (let tl of tableLines) { + if (tl.match(/^\|\s*[-+]+\s*\|/)) { + hasHeader = rows.length > 0; + } else { + let cells = tl.split("|").slice(1, -1).map(c => c.trim()); + rows.push(cells); + } + } + parts.push({type: "table", rows: rows, hasHeader: hasHeader}); } else if (currentText.length > 0) { let text = currentText.join('\n').trim(); if (text) parts.push({type: "text", content: text}); @@ -293,7 +340,8 @@ ListView { delegate: Loader { required property var modelData Layout.fillWidth: true - sourceComponent: modelData.type === "code" ? codeBlockComponent : textBlockComponent + sourceComponent: modelData.type === "code" ? codeBlockComponent : + modelData.type === "table" ? tableComponent : textBlockComponent Component { id: textBlockComponent @@ -352,6 +400,45 @@ ListView { } } } + + Component { + id: tableComponent + Rectangle { + color: theme.alternateBaseColor + radius: 4 + implicitHeight: tableGrid.implicitHeight + 12 + implicitWidth: tableGrid.implicitWidth + 12 + + GridLayout { + id: tableGrid + anchors.centerIn: parent + anchors.margins: 6 + columns: (modelData.rows && modelData.rows.length > 0) ? modelData.rows[0].length : 1 + rowSpacing: 4 + columnSpacing: 12 + + Repeater { + model: modelData.rows ?? [] + + Repeater { + required property var modelData + required property int index + property bool isHeader: index === 0 && tableComponent.modelData.hasHeader + model: modelData + + Label { + required property string modelData + text: modelData + color: parent.isHeader ? theme.textColor : theme.placeholderTextColor + font.bold: parent.isHeader + font.pointSize: outline.baseFontSize + horizontalAlignment: Text.AlignLeft + } + } + } + } + } + } } } }