Skip to content

Commit 8a1120b

Browse files
committed
quick-commands: support multi-word completions (#297)
Signed-off-by: Patrizio Bekerle <patrizio@bekerle.com>
1 parent c4dd236 commit 8a1120b

5 files changed

Lines changed: 141 additions & 9 deletions

File tree

quick-commands/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.2.0 - 2026-08-24
4+
5+
- Support quoted multi-word custom command values ([#297](https://github.com/qownnotes/scripts/issues/297)).
6+
37
## 0.1.1 - 2026-08-24
48

59
- Remove the quick command's leading backslash when applying a completion ([#296](https://github.com/qownnotes/scripts/issues/296)).

quick-commands/command-parser.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
function parseCommandLine(line) {
2+
var tokens = [];
3+
var token = "";
4+
var tokenStarted = false;
5+
var inQuotes = false;
6+
var escaping = false;
7+
8+
for (var i = 0; i < line.length; i++) {
9+
var character = line.charAt(i);
10+
11+
if (escaping) {
12+
if (character === '"' || character === "\\" || /\s/.test(character)) {
13+
token += character;
14+
} else {
15+
token += "\\" + character;
16+
}
17+
tokenStarted = true;
18+
escaping = false;
19+
} else if (character === "\\") {
20+
escaping = true;
21+
tokenStarted = true;
22+
} else if (character === '"') {
23+
inQuotes = !inQuotes;
24+
tokenStarted = true;
25+
} else if (/\s/.test(character) && !inQuotes) {
26+
if (tokenStarted) {
27+
tokens.push(token);
28+
token = "";
29+
tokenStarted = false;
30+
}
31+
} else {
32+
token += character;
33+
tokenStarted = true;
34+
}
35+
}
36+
37+
if (escaping) {
38+
token += "\\";
39+
}
40+
41+
if (inQuotes) {
42+
return null;
43+
}
44+
45+
if (tokenStarted) {
46+
tokens.push(token);
47+
}
48+
49+
if (tokens.length < 2 || tokens[0] === "") {
50+
return null;
51+
}
52+
53+
var values = [];
54+
for (var j = 1; j < tokens.length; j++) {
55+
if (tokens[j] !== "") {
56+
values.push(tokens[j]);
57+
}
58+
}
59+
60+
if (values.length === 0) {
61+
return null;
62+
}
63+
64+
return {
65+
name: tokens[0],
66+
values: values,
67+
};
68+
}

quick-commands/info.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"script": "quick-commands.qml",
55
"authors": ["@LockeBirdsey"],
66
"platforms": ["linux", "macos", "windows"],
7-
"version": "0.1.1",
7+
"version": "0.2.0",
88
"minAppVersion": "25.4.1",
9-
"description": "Short commands to help with repetive and/or annoying things to write frequently such as timestamps.\n\nEach command starts with a backslash (\\) and a single word for the command. Select the autocomplete option you desire and the command text will be replace.\n\nDefault commands are: today, tomorrow, yesterday, now, week.\n\nCustom commands can also be specified but are limited to simple text replacement."
9+
"description": "Short commands to help with repetive and/or annoying things to write frequently such as timestamps.\n\nEach command starts with a backslash (\\) and a single word for the command. Select the autocomplete option you desire and the command text will be replace.\n\nDefault commands are: today, tomorrow, yesterday, now, week.\n\nCustom commands can also be specified. Each line starts with the command name followed by one or more replacement values separated by spaces. Enclose multi-word values in double quotes, for example: myname \"Firstname Lastname\".",
10+
"resources": ["command-parser.js"]
1011
}

quick-commands/quick-commands.qml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import QtQml 2.0
22
import QOwnNotesTypes 1.0
3+
import "command-parser.js" as CommandParser
34

45
/**
56
* This script creates some easy to access commands that leverage the autocomplete functionalities to add more predefined
@@ -23,7 +24,7 @@ Script {
2324
{
2425
"identifier": "customCommands",
2526
"name": "Custom Commands",
26-
"description": "Custom quick commands. Each line is a separate command, with the options split by space and the first word being the command name. For example: 'myName first first-last last-first'",
27+
"description": "Custom quick commands. Each line starts with a command name followed by replacement values separated by spaces. Enclose multi-word values in double quotes. For example: 'myName first \"first last\" last-first'",
2728
"type": "text",
2829
"default": ""
2930
}
@@ -125,14 +126,12 @@ Script {
125126

126127
var customRows = customCommands.split("\n");
127128
for (let i = 0; i < customRows.length; i++) {
128-
var customCommandDetails = customRows[i].split(" ");
129-
var customCommandName = customCommandDetails[0];
130-
var customCommandValues = [];
131-
for (let j = 1; j < customCommandDetails.length; j++) {
132-
customCommandValues.push(customCommandDetails[j]);
129+
var customCommand = CommandParser.parseCommandLine(customRows[i]);
130+
if (customCommand === null) {
131+
continue;
133132
}
134133

135-
commands[customCommandName] = customCommandValues;
134+
commands[customCommand.name] = customCommand.values;
136135
}
137136
}
138137
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import QtQuick 2.0
2+
import QtTest 1.3
3+
import "../command-parser.js" as CommandParser
4+
5+
TestCase {
6+
name: "QuickCommandParser"
7+
8+
function compareValues(actual, expected) {
9+
compare(actual.length, expected.length);
10+
for (var i = 0; i < expected.length; i++) {
11+
compare(actual[i], expected[i]);
12+
}
13+
}
14+
15+
function test_plainValuesRemainSeparate() {
16+
var result = CommandParser.parseCommandLine("colour red green blue");
17+
18+
compare(result.name, "colour");
19+
compareValues(result.values, ["red", "green", "blue"]);
20+
}
21+
22+
function test_quotedMultiWordValue() {
23+
var result = CommandParser.parseCommandLine('myname "Firstname Lastname"');
24+
25+
compare(result.name, "myname");
26+
compareValues(result.values, ["Firstname Lastname"]);
27+
}
28+
29+
function test_mixedValues() {
30+
var result = CommandParser.parseCommandLine('greeting hello "Hello world" goodbye');
31+
32+
compareValues(result.values, ["hello", "Hello world", "goodbye"]);
33+
}
34+
35+
function test_whitespace() {
36+
var result = CommandParser.parseCommandLine(' greeting\t"Hello world" goodbye ');
37+
38+
compare(result.name, "greeting");
39+
compareValues(result.values, ["Hello world", "goodbye"]);
40+
}
41+
42+
function test_escapedCharacters() {
43+
var result = CommandParser.parseCommandLine('message "Say \\"hello\\" from C:\\\\Temp"');
44+
45+
compareValues(result.values, ['Say "hello" from C:\\Temp']);
46+
}
47+
48+
function test_escapedSpaceOutsideQuotes() {
49+
var result = CommandParser.parseCommandLine("myname Firstname\\ Lastname");
50+
51+
compareValues(result.values, ["Firstname Lastname"]);
52+
}
53+
54+
function test_invalidRows() {
55+
compare(CommandParser.parseCommandLine(""), null);
56+
compare(CommandParser.parseCommandLine("command"), null);
57+
compare(CommandParser.parseCommandLine('command ""'), null);
58+
compare(CommandParser.parseCommandLine('command "unclosed value'), null);
59+
}
60+
}

0 commit comments

Comments
 (0)