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
This commit is contained in:
Levi Neely 2026-10-01 10:19:15 +02:00
parent 17906c06f9
commit 30fa1bf35e
1 changed files with 92 additions and 5 deletions

View File

@ -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
}
}
}
}
}
}
}
}
}