Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 137 additions & 24 deletions in-note-text-tagging/in-note-text-tagging.qml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,125 @@ import QOwnNotesTypes 1.0

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

*/

Script {
property bool putToBeginning
property variant settingsVariables: [
{
"identifier": "useMarker1",
"name": "Enable primary tag marker",
"description": "Recognize words starting with the primary marker as tags",
"type": "boolean",
"default": "true"
},
{
"identifier": "tagMarker",
"name": "Tag word marker",
"description": "A word that starts with this characters is recognized as tag",
"name": "Primary tag marker character",
"description": "Character used as primary tag prefix (default: #)",
"type": "string",
"default": "#"
},
{
"identifier": "useMarker2",
"name": "Enable secondary tag marker",
"description": "Recognize words starting with the secondary marker as tags",
"type": "boolean",
"default": "false"
},
{
"identifier": "tagMarker2",
"name": "Secondary tag marker character",
"description": "Character used as secondary tag prefix (default: @)",
"type": "string",
"default": "@"
},
{
"identifier": "maxTagLength",
"name": "Maximum tag length",
"description": "Maximum number of characters allowed in a tag (0 = no limit, default: 32)",
"type": "integer",
"default": "32"
},
{
"identifier": "putToBeginning",
"name": "Put tags to beginning of note rather than to end",
"description": "If enabled tags, added by UI, will be put to the first line of note or right after top headline",
"description": "If enabled, tags added via UI will be put to the first line of note or right after top headline",
"type": "boolean",
"default": "false"
},
]
property bool useMarker1
property string tagMarker
property bool useMarker2
property string tagMarker2
property int maxTagLength

// Returns the preferred marker for writing new tags: primary if enabled, otherwise secondary
function writeMarker() {
return (useMarker1 && tagMarker) ? tagMarker : tagMarker2;
}

// Returns an array of currently active tag markers
function allMarkers() {
var markers = [];
if (useMarker1 && tagMarker) markers.push(tagMarker);
if (useMarker2 && tagMarker2) markers.push(tagMarker2);
return markers;
}

// Returns a regex alternation string matching any active marker, or null if none active
function markerPattern() {
var markers = allMarkers();
if (markers.length === 0) return null;
return "(?:" + markers.map(escapeRegExp).join("|") + ")";
}

// Returns the quantifier for tag body chars based on maxTagLength setting.
// First letter is always required; this governs the *remaining* characters.
function tagBodyQuantifier() {
if (maxTagLength > 0) return "{0," + (maxTagLength - 1) + "}";
return "*";
}

/**
* Hook to feed the autocompletion with tags if the current word starts with the tag marker
* Hook to feed the autocompletion with tags if the current word starts with any active marker
*/
function autocompletionHook() {
var pattern = markerPattern();
if (!pattern) return [];

// get the current word plus non-word-characters before the word to also get the tag marker
var word = script.noteTextEditCurrentWord(true);

if (!word.startsWith(tagMarker)) {
var matchedMarker = "";
var markers = allMarkers();
for (var i = 0; i < markers.length; i++) {
if (word.startsWith(markers[i])) {
matchedMarker = markers[i];
break;
}
}

if (!matchedMarker) {
return [];
}

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

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

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

// If the line already starts with a tag marker, appends the tag to it.
// Otherwise inserts a new tag line before it, prefixed by prepend.
function appendTag(text, tag, prepend) {
var markers = allMarkers();
for (var mi = 0; mi < markers.length; mi++) {
var m = markers[mi];
if (text.substring(0, m.length) == m || text.substring(1, m.length + 1) == m)
return text + " " + tag;
}
return prepend + tag + "\n" + text;
}

/**
* Handles note tagging for a note
*
Expand All @@ -67,20 +157,25 @@ Script {
* @return string or string-list (if action = "list")
*/
function noteTaggingHook(note, action, tagName, newTagName) {
var pattern = markerPattern();
if (!pattern) return action === "list" ? [] : "";

var noteText = note.noteText;
var tagRegExp = RegExp("\\B%1(?=($|\\s|\\b)) ?".arg(escapeRegExp(tagMarker + tagName).replace(/ /g, "_")));
// Match a specific known tag with any active marker.
// Group 1: leading space/newline, preserved on replace. Group 2: the matched marker.
var tagRegExp = RegExp("(^|\\s)(%1)%2(?=($|\\s)) ?".arg(pattern).arg(escapeRegExp(tagName).replace(/ /g, "_")), "m");

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

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

Comment on lines 177 to 179
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var tag = tagMarker + ... always uses the primary marker when writing tags. If useMarker1 is disabled (and only the secondary marker is enabled), the script will still insert tags with the disabled marker. Pick the marker to write based on which marker(s) are enabled (e.g., prefer primary if enabled, otherwise secondary).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

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

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

// if line after headline is a line for tags add tag there,
// or make a new line for tags after headline
function appendTag(text, tag, prepend) {
if (text.substring(0, tagMarker.length) == tagMarker || text.substring(1, tagMarker.length + 1) == tagMarker)
return text + " " + tag;
else
return prepend + tag + "\n" + text;
}

// use different tag line number depending on a headline type
if (textLines[0].substring(0, 1) == "#")
textLines[1] = appendTag(textLines[1], tag, "\n");
Expand All @@ -130,17 +216,44 @@ Script {
// the new note text has to be returned so that the note can be updated
// returning an empty string indicates that nothing has to be changed
case "remove":
return noteText.replace(tagRegExp, "");
return noteText.replace(tagRegExp, "$1");

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

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

while ((result = re.exec(noteText)) !== null) {
tagName = result[1].replace(/_/g, " ");
Expand Down
4 changes: 2 additions & 2 deletions in-note-text-tagging/info.json
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"name": "@tag tagging in note text (experimental)",
"identifier": "in-note-text-tagging",
"script": "in-note-text-tagging.qml",
"authors": ["@Maboroshy"],
"authors": ["@Maboroshy", "@luginf"],
"platforms": ["linux", "macos", "windows"],
"version": "0.2.0",
"version": "0.2.2",
"minAppVersion": "20.6.0",
"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."
}