Skip to content

Commit 0d07bf5

Browse files
luginfluginfclaudepbekCopilot
authored
In note text tagging improvement (#283)
* adding txt2tags-it: support to txt2tags syntax in addition to the markdown one * fix: strikethrough, HTML options default, link rules ordering - Enable html:true by default so HTML comments (<!-- -->) are passed through as-is instead of being escaped as visible text - Fix --strikethrough-- to use <s> tag instead of <del>, consistent with markdown ~~strikethrough~~ rendering - Remove + item ordered list editor highlighting rule (was applying an unwanted heading style to list lines) - Fix basePath undefined variable → path in relative URL resolution - Register txt2tags_link before adding autolink/wikilink rules: ruler.before("txt2tags_link", ...) threw "Parser rule not found" when called before txt2tags_link was itself registered, aborting the entire plugin initialisation and breaking all txt2tags rendering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * updating and fixing * fix version and credits * fix: Qt6 compatibility — resolve markdown-it constructors via module namespace In Qt5, importing a JS file in QML binds the module's top-level `this` to the QML global scope, so UMD modules export their constructors onto the component via `g = this; g.markdownit = f()`, and `this.markdownit` works in init(). In Qt6, the module's top-level `this` is the module namespace object (e.g. MarkdownIt, MarkdownItTxt2tags), not the QML component scope. So the constructors land on `MarkdownIt.markdownit` etc., and `this.markdownit` in init() is undefined — causing silent init failure and a blank preview. Fix: resolve each constructor with `Namespace.name || this.name` so that Qt6 uses the module namespace and Qt5 falls back to the component scope. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: Qt6 UMD compat — use globalThis when module this is undefined In Qt6 QML, JavaScript files imported via 'import "foo.js" as Foo' run in strict mode where `this` at the top level is undefined. The UMD wrappers in markdown-it, markdown-it-deflist, markdown-it-katex and markdown-it-txt2tags all fell back to `g = this` when window/global/self were undefined, then threw TypeError setting a property on undefined. Fix: insert `globalThis` before the `this` fallback in each UMD chain. globalThis is always the real global object in Qt6's JS engine. In init(), resolve constructors via `_g = globalThis || this` so that Qt6 uses globalThis (where the modules now export) and Qt5 falls back to the component scope (where the modules previously exported via this). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: Qt6 compat — top-level var export; remove deflist and katex The root cause of Qt6 breakage: UMD modules used `g = this; g.markdownit = f()` where `this` at module top-level is undefined in Qt6 strict mode, so the assignment threw and nothing was exported. Fix: declare `var markdownit` and `var markdownitTxt2tags` at the top level of each JS file (outside the IIFE). Top-level vars are always accessible via the QML module qualifier (MarkdownIt.markdownit, MarkdownItTxt2tags.markdownitTxt2tags) in both Qt5 and Qt6 — no runtime this/globalThis lookup needed. Also removed the unused deflist and katex plugins to simplify the script. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove unused deflist and katex plugins Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix non white space before a tag. Allow to use either # or @ for tags (# is default) * Guard URL schemes Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * remove other commits outside the scope of this branch * Revert "remove other commits outside the scope of this branch" This reverts commit a72474a. * remove txt2tags-it * adding myself Changed > 0 to !== -1 on line 169 so a tag at position 0 (start of note) is correctly detected as already existing, preventing duplicates. * fixed a few reviews by copilot on #283 --------- Co-authored-by: luginf <alan@luginf> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Patrizio Bekerle <patrizio@bekerle.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent f0a4580 commit 0d07bf5

2 files changed

Lines changed: 139 additions & 26 deletions

File tree

in-note-text-tagging/in-note-text-tagging.qml

100644100755
Lines changed: 137 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,125 @@ import QOwnNotesTypes 1.0
33

44
/**
55
* This script handles tagging in a note for tags in the note text like:
6-
* @tag1 @tag2 @tag3
6+
* #tag1 #tag2 #tag3 #tag4
77
* @tag_one would tag the note with "tag one" tag.
8+
* One or both markers can be enabled independently via the settings.
9+
* - The two checkboxes are independent — you can enable # alone, @ alone, both, or freely change the characters
10+
* - If both are disabled, no tags are detected (clean empty return)
11+
* - maxTagLength = 0 disables the limit → * quantifier in the regex
12+
* - maxTagLength = 32 → {0,31} quantifier (1 mandatory letter + 31 more = 32 total)
13+
* - The tagBodyQuantifier() function computes the correct quantifier on each call
14+
815
*/
916

1017
Script {
1118
property bool putToBeginning
1219
property variant settingsVariables: [
20+
{
21+
"identifier": "useMarker1",
22+
"name": "Enable primary tag marker",
23+
"description": "Recognize words starting with the primary marker as tags",
24+
"type": "boolean",
25+
"default": "true"
26+
},
1327
{
1428
"identifier": "tagMarker",
15-
"name": "Tag word marker",
16-
"description": "A word that starts with this characters is recognized as tag",
29+
"name": "Primary tag marker character",
30+
"description": "Character used as primary tag prefix (default: #)",
31+
"type": "string",
32+
"default": "#"
33+
},
34+
{
35+
"identifier": "useMarker2",
36+
"name": "Enable secondary tag marker",
37+
"description": "Recognize words starting with the secondary marker as tags",
38+
"type": "boolean",
39+
"default": "false"
40+
},
41+
{
42+
"identifier": "tagMarker2",
43+
"name": "Secondary tag marker character",
44+
"description": "Character used as secondary tag prefix (default: @)",
1745
"type": "string",
1846
"default": "@"
1947
},
48+
{
49+
"identifier": "maxTagLength",
50+
"name": "Maximum tag length",
51+
"description": "Maximum number of characters allowed in a tag (0 = no limit, default: 32)",
52+
"type": "integer",
53+
"default": "32"
54+
},
2055
{
2156
"identifier": "putToBeginning",
2257
"name": "Put tags to beginning of note rather than to end",
23-
"description": "If enabled tags, added by UI, will be put to the first line of note or right after top headline",
58+
"description": "If enabled, tags added via UI will be put to the first line of note or right after top headline",
2459
"type": "boolean",
2560
"default": "false"
2661
},
2762
]
63+
property bool useMarker1
2864
property string tagMarker
65+
property bool useMarker2
66+
property string tagMarker2
67+
property int maxTagLength
68+
69+
// Returns the preferred marker for writing new tags: primary if enabled, otherwise secondary
70+
function writeMarker() {
71+
return (useMarker1 && tagMarker) ? tagMarker : tagMarker2;
72+
}
73+
74+
// Returns an array of currently active tag markers
75+
function allMarkers() {
76+
var markers = [];
77+
if (useMarker1 && tagMarker) markers.push(tagMarker);
78+
if (useMarker2 && tagMarker2) markers.push(tagMarker2);
79+
return markers;
80+
}
81+
82+
// Returns a regex alternation string matching any active marker, or null if none active
83+
function markerPattern() {
84+
var markers = allMarkers();
85+
if (markers.length === 0) return null;
86+
return "(?:" + markers.map(escapeRegExp).join("|") + ")";
87+
}
88+
89+
// Returns the quantifier for tag body chars based on maxTagLength setting.
90+
// First letter is always required; this governs the *remaining* characters.
91+
function tagBodyQuantifier() {
92+
if (maxTagLength > 0) return "{0," + (maxTagLength - 1) + "}";
93+
return "*";
94+
}
2995

3096
/**
31-
* Hook to feed the autocompletion with tags if the current word starts with the tag marker
97+
* Hook to feed the autocompletion with tags if the current word starts with any active marker
3298
*/
3399
function autocompletionHook() {
100+
var pattern = markerPattern();
101+
if (!pattern) return [];
102+
34103
// get the current word plus non-word-characters before the word to also get the tag marker
35104
var word = script.noteTextEditCurrentWord(true);
36105

37-
if (!word.startsWith(tagMarker)) {
106+
var matchedMarker = "";
107+
var markers = allMarkers();
108+
for (var i = 0; i < markers.length; i++) {
109+
if (word.startsWith(markers[i])) {
110+
matchedMarker = markers[i];
111+
break;
112+
}
113+
}
114+
115+
if (!matchedMarker) {
38116
return [];
39117
}
40118

41119
// cut the tag marker off of the string and do a substring search for tags
42-
var tags = script.searchTagsByName(word.substr(tagMarker.length));
120+
var tags = script.searchTagsByName(word.substr(matchedMarker.length));
43121

44122
// convert tag names with spaces to in-text tags with "_", "tag one" to @tag_one
45-
for (var i = 0; i < tags.length; i++) {
46-
tags[i] = tags[i].replace(/ /g, "_");
123+
for (var j = 0; j < tags.length; j++) {
124+
tags[j] = tags[j].replace(/ /g, "_");
47125
}
48126

49127
return tags;
@@ -54,6 +132,18 @@ Script {
54132
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
55133
}
56134

135+
// If the line already starts with a tag marker, appends the tag to it.
136+
// Otherwise inserts a new tag line before it, prefixed by prepend.
137+
function appendTag(text, tag, prepend) {
138+
var markers = allMarkers();
139+
for (var mi = 0; mi < markers.length; mi++) {
140+
var m = markers[mi];
141+
if (text.substring(0, m.length) == m || text.substring(1, m.length + 1) == m)
142+
return text + " " + tag;
143+
}
144+
return prepend + tag + "\n" + text;
145+
}
146+
57147
/**
58148
* Handles note tagging for a note
59149
*
@@ -67,20 +157,25 @@ Script {
67157
* @return string or string-list (if action = "list")
68158
*/
69159
function noteTaggingHook(note, action, tagName, newTagName) {
160+
var pattern = markerPattern();
161+
if (!pattern) return action === "list" ? [] : "";
162+
70163
var noteText = note.noteText;
71-
var tagRegExp = RegExp("\\B%1(?=($|\\s|\\b)) ?".arg(escapeRegExp(tagMarker + tagName).replace(/ /g, "_")));
164+
// Match a specific known tag with any active marker.
165+
// Group 1: leading space/newline, preserved on replace. Group 2: the matched marker.
166+
var tagRegExp = RegExp("(^|\\s)(%1)%2(?=($|\\s)) ?".arg(pattern).arg(escapeRegExp(tagName).replace(/ /g, "_")), "m");
72167

73168
switch (action) {
74169
// adds the tag "tagName" to the note
75170
// the new note text has to be returned so that the note can be updated
76171
// returning an empty string indicates that nothing has to be changed
77172
case "add":
78173
// check if tag already exists
79-
if (noteText.search(tagRegExp) > 0) {
174+
if (noteText.search(tagRegExp) !== -1) {
80175
return "";
81176
}
82177

83-
const tag = tagMarker + tagName.replace(/ /g, "_");
178+
var tag = writeMarker() + tagName.replace(/ /g, "_");
84179

85180
// add the tag to the beginning or to the end of the note
86181
if (putToBeginning) {
@@ -103,15 +198,6 @@ Script {
103198

104199
textLines.push(noteText.substring(lineStart));
105200

106-
// if line after headline is a line for tags add tag there,
107-
// or make a new line for tags after headline
108-
function appendTag(text, tag, prepend) {
109-
if (text.substring(0, tagMarker.length) == tagMarker || text.substring(1, tagMarker.length + 1) == tagMarker)
110-
return text + " " + tag;
111-
else
112-
return prepend + tag + "\n" + text;
113-
}
114-
115201
// use different tag line number depending on a headline type
116202
if (textLines[0].substring(0, 1) == "#")
117203
textLines[1] = appendTag(textLines[1], tag, "\n");
@@ -130,17 +216,44 @@ Script {
130216
// the new note text has to be returned so that the note can be updated
131217
// returning an empty string indicates that nothing has to be changed
132218
case "remove":
133-
return noteText.replace(tagRegExp, "");
219+
return noteText.replace(tagRegExp, "$1");
134220

135221
// renames the tag "tagName" in the note to "newTagName"
136222
// the new note text has to be returned so that the note can be updated
137223
// returning an empty string indicates that nothing has to be changed
138224
case "rename":
139-
return noteText.replace(tagRegExp, tagMarker + newTagName.replace(/ /g, "_"));
225+
return noteText.replace(tagRegExp, "$1$2" + newTagName.replace(/ /g, "_"));
140226

141227
// returns a list of all tag names of the note
142228
case "list":
143-
var re = new RegExp("\\B%1([^\\s,;%1]+)".arg(escapeRegExp(tagMarker)), "gi"), result, tagNameList = [];
229+
// Exclude all marker characters from tag content.
230+
// Requires a space/newline (or start of line) before the marker,
231+
// a letter (including accented/Unicode) as first char.
232+
// Max length is controlled by tagBodyQuantifier().
233+
var excludedChars = allMarkers().map(escapeRegExp).join("");
234+
var re = new RegExp(
235+
"(?:^|\\s)" + pattern +
236+
"([a-zA-Z" +
237+
"\\u00C0-\\u024F" + // Latin Extended A+B
238+
"\\u0250-\\u02AF" + // IPA Extensions
239+
"\\u0370-\\u03FF" + // Greek and Coptic
240+
"\\u0400-\\u052F" + // Cyrillic + Supplement
241+
"\\u0530-\\u058F" + // Armenian
242+
"\\u05D0-\\u05FF" + // Hebrew
243+
"\\u0600-\\u06FF" + // Arabic
244+
"\\u0900-\\u0D7F" + // Indic (Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada, Malayalam)
245+
"\\u0E00-\\u0EFF" + // Thai and Lao
246+
"\\u10A0-\\u10FF" + // Georgian
247+
"\\u1200-\\u137F" + // Ethiopic
248+
"\\u1E00-\\u1FFF" + // Latin Extended Additional + Greek Extended
249+
"\\u3040-\\u30FF" + // Hiragana + Katakana
250+
"\\u3400-\\u4DBF" + // CJK Extension A
251+
"\\u4E00-\\u9FFF" + // CJK Unified Ideographs
252+
"\\uAC00-\\uD7AF" + // Hangul Syllables
253+
"]" +
254+
"[^\\s,;" + excludedChars + "]" + tagBodyQuantifier() + ")",
255+
"gim"
256+
), result, tagNameList = [];
144257

145258
while ((result = re.exec(noteText)) !== null) {
146259
tagName = result[1].replace(/_/g, " ");

in-note-text-tagging/info.json

100644100755
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
"name": "@tag tagging in note text (experimental)",
33
"identifier": "in-note-text-tagging",
44
"script": "in-note-text-tagging.qml",
5-
"authors": ["@Maboroshy"],
5+
"authors": ["@Maboroshy", "@luginf"],
66
"platforms": ["linux", "macos", "windows"],
7-
"version": "0.2.0",
7+
"version": "0.2.2",
88
"minAppVersion": "20.6.0",
99
"description": "With this script you can <b>store your tags in your note-text</b>. Use tags like <i>@tag</i> or <i>@tag_one</i> for 'tag one' inside your note-text to tag your notes. You can change '@' to something else in script settings.\n\nYou also are able to use the functionality of the QOwnNotes user-interface to tag with this tags inside your note texts, like for adding a tag to the current note as well as bulk operations for adding and removing tags to your note. If you rename a tag inside QOwnNotes the text-tags in your notes are also updated.\n\nIf you start writing a tag you can also use the autocompleter to get a list of already existing tags.\n\nYou can also use this script as template for implementing your own, unique tagging mechanism.\n\n<b>If you install this script you will loose all links between notes and tags and instead your in-note tags will be used!\n\nThis functionality is still experimental!</b>\nPlease report your experiences."
1010
}

0 commit comments

Comments
 (0)