Skip to content

Commit e22bb40

Browse files
committed
#3549 scripting: add extended syntax highlighting
Signed-off-by: Patrizio Bekerle <patrizio@bekerle.com>
1 parent d188517 commit e22bb40

9 files changed

Lines changed: 496 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## 26.4.9
44

5+
- Extended the **scripting engine's syntax highlighting** support with **custom
6+
colors and styles**: `addHighlightingRule` now accepts an optional format map
7+
with `foregroundColor`, `backgroundColor`, `bold`, `italic`, `underline`, and
8+
`fontSize` properties, so scripts can define their own highlight colors instead
9+
of being limited to the predefined highlighting states
10+
(for [#3549](https://github.com/pbek/QOwnNotes/issues/3549))
11+
- Added a new **`highlightingHook`** scripting hook that is called for each text
12+
block during syntax highlighting, allowing scripts to perform dynamic,
13+
context-aware highlighting with custom colors and styles
14+
(for [#3549](https://github.com/pbek/QOwnNotes/issues/3549))
515
- Fixed a possible **startup crash** when restoring the last opened note from
616
**note history** while the **note tree** was enabled: history restoration
717
searched the tree by note name and could match a folder item with the same label
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import QtQml 2.0
2+
import QOwnNotesTypes 1.0
3+
4+
/**
5+
* This script demonstrates the custom highlighting features:
6+
* 1. Static rules with custom colors/styles via addHighlightingRule()
7+
* 2. Dynamic per-block highlighting via highlightingHook()
8+
*/
9+
Script {
10+
function init() {
11+
// Highlight "TODO" with bold white text on a red background
12+
script.addHighlightingRule("TODO", "TODO", -1, 0, 0, {
13+
"foregroundColor": "#ffffff",
14+
"backgroundColor": "#cc0000",
15+
"bold": true
16+
});
17+
18+
// Highlight "DONE" with green strikethrough-like italic text
19+
script.addHighlightingRule("DONE", "DONE", -1, 0, 0, {
20+
"foregroundColor": "#22aa22",
21+
"italic": true
22+
});
23+
24+
// Highlight lines starting with ">" and containing "IMPORTANT"
25+
// using blockquote state (18) with a custom red foreground color
26+
script.addHighlightingRule("^>.*IMPORTANT.*", "IMPORTANT", 18, 0, 0, {
27+
"foregroundColor": "#ff4444"
28+
});
29+
30+
// Highlight "@username" mentions with a custom color and underline
31+
script.addHighlightingRule("@\\w+", "@", -1, 0, 0, {
32+
"foregroundColor": "#3366cc",
33+
"underline": true
34+
});
35+
}
36+
37+
/**
38+
* Dynamic per-block highlighting hook.
39+
*
40+
* This function is called for every text block in the editor during
41+
* syntax highlighting. It receives the block text and the previous
42+
* block's highlighter state, allowing context-aware highlighting.
43+
*
44+
* @param text the text of the current block
45+
* @param previousBlockState the state of the previous block (-1 for the first block)
46+
* @return an array of highlight range objects
47+
*/
48+
function highlightingHook(text, previousBlockState) {
49+
var highlights = [];
50+
51+
// Highlight "#tag" style tags with a custom teal color
52+
var tagRegex = /#[a-zA-Z]\w*/g;
53+
var match;
54+
while ((match = tagRegex.exec(text)) !== null) {
55+
highlights.push({
56+
"start": match.index,
57+
"length": match[0].length,
58+
"foregroundColor": "#008080",
59+
"bold": true
60+
});
61+
}
62+
63+
// Highlight "FIXME" and "HACK" keywords with red underlined text
64+
var keywordRegex = /\b(FIXME|HACK)\b/g;
65+
while ((match = keywordRegex.exec(text)) !== null) {
66+
highlights.push({
67+
"start": match.index,
68+
"length": match[0].length,
69+
"foregroundColor": "#ff0000",
70+
"underline": true,
71+
"bold": true
72+
});
73+
}
74+
75+
// Highlight dates in YYYY-MM-DD format with a purple color
76+
var dateRegex = /\b\d{4}-\d{2}-\d{2}\b/g;
77+
while ((match = dateRegex.exec(text)) !== null) {
78+
highlights.push({
79+
"start": match.index,
80+
"length": match[0].length,
81+
"foregroundColor": "#8855cc"
82+
});
83+
}
84+
85+
return highlights;
86+
}
87+
}

docs/scripting/examples/highlighting.qml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,59 @@ Script {
1010
// capturingGroup 1 means the expression from the first bracketed part of the pattern will be highlighted
1111
// maskedGroup -1 means that no masking should be done
1212
script.addHighlightingRule("^.{32}(.+)", "", 24, 1, -1);
13+
14+
// Highlight "IMPORTANT" with custom colors: red bold text on a yellow background
15+
// The 6th parameter is a map with custom format properties:
16+
// foregroundColor, backgroundColor, bold, italic, underline, fontSize
17+
// Use state -1 (NoState) to use only custom formatting without a predefined state
18+
script.addHighlightingRule("IMPORTANT", "IMPORTANT", -1, 0, 0, {
19+
"foregroundColor": "#ff0000",
20+
"backgroundColor": "#ffff00",
21+
"bold": true
22+
});
23+
24+
// Highlight "NOTE:" with custom italic green text
25+
script.addHighlightingRule("NOTE:", "NOTE:", -1, 0, 0, {
26+
"foregroundColor": "#00aa00",
27+
"italic": true
28+
});
29+
}
30+
31+
/**
32+
* This hook is called for each text block during syntax highlighting.
33+
* It allows context-aware, dynamic highlighting that goes beyond
34+
* static regex rules.
35+
*
36+
* @param text - the text of the current block being highlighted
37+
* @param previousBlockState - the highlighter state of the previous block
38+
* (-1 if this is the first block)
39+
* @return an array of highlight range objects, each with:
40+
* start {int} - start position in the text
41+
* length {int} - number of characters to highlight
42+
* state {int} - the HighlighterState to use (optional, -1 for custom only)
43+
* foregroundColor {string} - foreground color (optional)
44+
* backgroundColor {string} - background color (optional)
45+
* bold {bool} - bold (optional)
46+
* italic {bool} - italic (optional)
47+
* underline {bool} - underline (optional)
48+
* fontSize {int} - font point size (optional)
49+
*/
50+
function highlightingHook(text, previousBlockState) {
51+
var highlights = [];
52+
53+
// Example: highlight all occurrences of "FIXME" with red underline
54+
var re = /FIXME/g;
55+
var match;
56+
while ((match = re.exec(text)) !== null) {
57+
highlights.push({
58+
"start": match.index,
59+
"length": match[0].length,
60+
"foregroundColor": "#ff0000",
61+
"underline": true,
62+
"bold": true
63+
});
64+
}
65+
66+
return highlights;
1367
}
1468
}

src/helpers/qownnotesmarkdownhighlighter.cpp

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <QApplication>
2222
#include <QColor>
2323
#include <QDebug>
24+
#include <QFont>
2425
#include <QObject>
2526
#include <QRegularExpression>
2627
#include <QRegularExpressionMatch>
@@ -109,6 +110,10 @@ void QOwnNotesMarkdownHighlighter::highlightBlock(const QString &text) {
109110
#endif
110111

111112
highlightScriptingRules(ScriptingService::instance()->getHighlightingRules(), text);
113+
114+
// Call the per-block highlightingHook in scripts (only invoked if at
115+
// least one script provides the hook, checked via cached flag)
116+
highlightScriptingHook(text);
112117
}
113118

114119
// Only check for encryption end marker if we're highlighting encrypted text
@@ -134,18 +139,49 @@ void QOwnNotesMarkdownHighlighter::highlightScriptingRules(
134139
auto iterator = rule.pattern.globalMatch(text);
135140
const uint8_t capturingGroup = rule.capturingGroup;
136141
const uint8_t maskedGroup = rule.maskedGroup;
137-
const QTextCharFormat &format = _formats[rule.state];
138142

139-
// find and format all occurrences
143+
// Build the format: use custom format if provided, otherwise use the
144+
// predefined format for the given state
145+
QTextCharFormat format;
146+
if (rule.hasCustomFormat) {
147+
// Start from the state format if a valid state was specified
148+
if (rule.state != NoState) {
149+
format = _formats[rule.state];
150+
}
151+
152+
// Override with custom properties
153+
if (!rule.foregroundColor.isEmpty()) {
154+
format.setForeground(QColor(rule.foregroundColor));
155+
}
156+
if (!rule.backgroundColor.isEmpty()) {
157+
format.setBackground(QColor(rule.backgroundColor));
158+
}
159+
if (rule.bold) {
160+
format.setFontWeight(QFont::Bold);
161+
}
162+
if (rule.italic) {
163+
format.setFontItalic(true);
164+
}
165+
if (rule.underline) {
166+
format.setFontUnderline(true);
167+
}
168+
if (rule.fontSize > 0) {
169+
format.setFontPointSize(rule.fontSize);
170+
}
171+
} else {
172+
format = _formats[rule.state];
173+
}
174+
175+
// Find and format all occurrences
140176
while (iterator.hasNext()) {
141177
QRegularExpressionMatch match = iterator.next();
142178

143-
// if there is a capturingGroup set then first highlight
179+
// If there is a capturingGroup set then first highlight
144180
// everything as MaskedSyntax and highlight capturingGroup
145181
// with the real format
146182
if (capturingGroup > 0) {
147183
QTextCharFormat currentMaskedFormat = maskedFormat;
148-
// set the font size from the current rule's font format
184+
// Set the font size from the current rule's font format
149185
if (format.fontPointSize() > 0) {
150186
currentMaskedFormat.setFontPointSize(format.fontPointSize());
151187
}
@@ -160,6 +196,69 @@ void QOwnNotesMarkdownHighlighter::highlightScriptingRules(
160196
}
161197
}
162198

199+
/**
200+
* Calls the highlightingHook in all script components for the current text
201+
* block and applies the returned highlight ranges
202+
*/
203+
void QOwnNotesMarkdownHighlighter::highlightScriptingHook(const QString &text) {
204+
ScriptingService *scriptingService = ScriptingService::instance();
205+
206+
if (!scriptingService->highlightingHookExists()) {
207+
return;
208+
}
209+
210+
const QVariantList highlights =
211+
scriptingService->callHighlightingHook(text, previousBlockState());
212+
213+
for (const QVariant &item : highlights) {
214+
const QVariantMap m = item.toMap();
215+
const int start = m.value(QStringLiteral("start")).toInt();
216+
const int length = m.value(QStringLiteral("length")).toInt();
217+
218+
if (length <= 0) {
219+
continue;
220+
}
221+
222+
// Determine the format from the state or custom properties
223+
const int state = m.value(QStringLiteral("state"), -1).toInt();
224+
QTextCharFormat format;
225+
226+
if (state >= 0) {
227+
format = _formats[static_cast<HighlighterState>(state)];
228+
}
229+
230+
// Apply custom format overrides
231+
const QString fg = m.value(QStringLiteral("foregroundColor")).toString();
232+
if (!fg.isEmpty()) {
233+
format.setForeground(QColor(fg));
234+
}
235+
236+
const QString bg = m.value(QStringLiteral("backgroundColor")).toString();
237+
if (!bg.isEmpty()) {
238+
format.setBackground(QColor(bg));
239+
}
240+
241+
if (m.value(QStringLiteral("bold")).toBool()) {
242+
format.setFontWeight(QFont::Bold);
243+
}
244+
245+
if (m.value(QStringLiteral("italic")).toBool()) {
246+
format.setFontItalic(true);
247+
}
248+
249+
if (m.value(QStringLiteral("underline")).toBool()) {
250+
format.setFontUnderline(true);
251+
}
252+
253+
const qreal fontSize = m.value(QStringLiteral("fontSize")).toReal();
254+
if (fontSize > 0) {
255+
format.setFontPointSize(fontSize);
256+
}
257+
258+
setFormat(start, length, format);
259+
}
260+
}
261+
163262
void QOwnNotesMarkdownHighlighter::updateCachedRegexes(const QString &newExt) {
164263
_regexTagStyleLink = QRegularExpression(R"(<([^\s`][^`]*?\.)" + newExt + R"()>)");
165264
_regexBracketLink = QRegularExpression(R"(\[[^\[\]]+\]\((\S+\.)" + newExt + R"(|.+?\.)" +

src/helpers/qownnotesmarkdownhighlighter.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ class QOwnNotesMarkdownHighlighter : public MarkdownHighlighter {
5050
HighlighterState state = NoState;
5151
uint8_t capturingGroup = 0;
5252
uint8_t maskedGroup = 0;
53+
54+
// Custom format fields for script-defined colors and styles
55+
bool hasCustomFormat = false;
56+
QString foregroundColor;
57+
QString backgroundColor;
58+
bool bold = false;
59+
bool italic = false;
60+
bool underline = false;
61+
qreal fontSize = 0;
5362
};
5463

5564
protected:
@@ -82,4 +91,5 @@ class QOwnNotesMarkdownHighlighter : public MarkdownHighlighter {
8291
QHash<QString, bool> _wikiLinkCache;
8392
void highlightScriptingRules(const QVector<ScriptingHighlightingRule> &rules,
8493
const QString &text);
94+
void highlightScriptingHook(const QString &text);
8595
};

0 commit comments

Comments
 (0)