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:
parent
17906c06f9
commit
30fa1bf35e
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue