Render org links in the GUI body
Body text and list items now render org links ([[target]] and [[target][description]]) as clickable hyperlinks via StyledText. Other text is HTML-escaped and newlines become <br> so formatting is preserved. Activating a link opens external URL schemes (http(s), mailto, file) in the system handler; internal links (id:, #custom, *headline) are left for a future navigation pass. The pointer cursor shows over hovered links.
This commit is contained in:
parent
39ab7108ff
commit
b79970b62f
|
|
@ -41,6 +41,45 @@ ListView {
|
|||
return Math.max(outline.baseFontSize - 1, outline.baseFontSize + 5 - level)
|
||||
}
|
||||
|
||||
// Escape HTML special chars for safe StyledText rendering.
|
||||
function escHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
}
|
||||
|
||||
// Render inline org markup (currently links) to StyledText. Org links are
|
||||
// [[target]] or [[target][description]]; everything else is escaped. Line
|
||||
// breaks are converted to <br> so StyledText preserves them.
|
||||
function renderInline(text) {
|
||||
let re = /\[\[([^\]\[]+?)\](?:\[([^\]]*?)\])?\]/g
|
||||
let out = ""
|
||||
let last = 0
|
||||
let m
|
||||
function escBlock(s) { return escHtml(s).replace(/\n/g, "<br>") }
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
out += escBlock(text.slice(last, m.index))
|
||||
let target = m[1]
|
||||
let desc = (m[2] !== undefined && m[2] !== "") ? m[2] : m[1]
|
||||
let href = escHtml(target).replace(/"/g, """)
|
||||
out += '<a href="' + href + '">' + escHtml(desc) + '</a>'
|
||||
last = re.lastIndex
|
||||
}
|
||||
out += escBlock(text.slice(last))
|
||||
return out
|
||||
}
|
||||
|
||||
// Open a link target activated from body text. External URL schemes open
|
||||
// in the system handler; internal org links (id:, #custom, *headline) are
|
||||
// left for a future navigation pass.
|
||||
function openLink(target) {
|
||||
if (/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//.test(target)
|
||||
|| /^(mailto:|file:)/.test(target)) {
|
||||
Qt.openUrlExternally(target)
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: del
|
||||
width: outline.width
|
||||
|
|
@ -427,12 +466,19 @@ ListView {
|
|||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.content
|
||||
text: outline.renderInline(modelData.content)
|
||||
color: theme.placeholderTextColor
|
||||
linkColor: theme.linkColor
|
||||
font.pointSize: outline.baseFontSize + 1
|
||||
lineHeight: 1.3
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.PlainText
|
||||
textFormat: Text.StyledText
|
||||
onLinkActivated: (link) => outline.openLink(link)
|
||||
|
||||
HoverHandler {
|
||||
enabled: parent.hoveredLink !== ""
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -497,13 +543,20 @@ ListView {
|
|||
// Item text; checked items are dimmed and struck through.
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.text ?? ""
|
||||
text: outline.renderInline(modelData.text ?? "")
|
||||
color: (modelData.checkbox === "checked")
|
||||
? theme.placeholderTextColor : theme.textColor
|
||||
linkColor: theme.linkColor
|
||||
font.pointSize: outline.baseFontSize + 1
|
||||
font.strikeout: modelData.checkbox === "checked"
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.PlainText
|
||||
textFormat: Text.StyledText
|
||||
onLinkActivated: (link) => outline.openLink(link)
|
||||
|
||||
HoverHandler {
|
||||
enabled: parent.hoveredLink !== ""
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue