diff --git a/common/markdown_parser/constants.ts b/common/markdown_parser/constants.ts
index f976b23e..8475048b 100644
--- a/common/markdown_parser/constants.ts
+++ b/common/markdown_parser/constants.ts
@@ -1,5 +1,5 @@
export const wikiLinkRegex = /(!?\[\[)([^\]\|]+)(?:\|([^\]]+))?(\]\])/g; // [fullMatch, firstMark, url, alias, lastMark]
export const mdLinkRegex = /!?\[(?
[^\]]*)\]\((?.+)\)/g; // [fullMatch, alias, url]
export const tagRegex =
- /#[^\d\s!@#$%^&*(),.?":{}|<>\\][^\s!@#$%^&*(),.?":{}|<>\\]*/;
+ /#(?:(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\][^\s!@#$%^&*(),.?":{}|<>\\]*)|(?:"[^"\n]+")|(?:<[^>\n]+>))/;
export const pWikiLinkRegex = new RegExp("^" + wikiLinkRegex.source); // Modified regex used only in parser
diff --git a/common/markdown_parser/parser.test.ts b/common/markdown_parser/parser.test.ts
index 3709242a..00606d3f 100644
--- a/common/markdown_parser/parser.test.ts
+++ b/common/markdown_parser/parser.test.ts
@@ -162,3 +162,40 @@ Deno.test("Test lua directive parser", () => {
const simpleExample = `Simple \${{a=}}`;
console.log(JSON.stringify(parseMarkdown(simpleExample), null, 2));
});
+
+const hashtagSample = `
+Hashtags, e.g. #mytag but ignore in code \`#mytag\`.
+They can contain slashes like #level/beginner, single quotes, and dashes: #Mike's-idea.
+Can be just #a single letter.
+But no other #interpunction: #exclamation! #question?
+There are two ways to make #"tag with spaces and " #
+These cannot span #"multiple
+lines"
+#no#spacing also works.
+Hashtags can start with number if there's something after it: #3dprint #15-52_Trip-to-NYC.
+But magazine issue #123 is not a hashtag.
+Should support other languages, like #żółć or #井号
+`;
+
+Deno.test("Test hashtag parser", () => {
+ const tree = parseMarkdown(hashtagSample);
+ const hashtags = collectNodesOfType(tree, "Hashtag");
+ assertEquals(hashtags.length, 15);
+
+ assertEquals(hashtags[0].children![0].text, "#mytag");
+ assertEquals(hashtags[1].children![0].text, "#level/beginner");
+ assertEquals(hashtags[2].children![0].text, "#Mike's-idea");
+ assertEquals(hashtags[2].children![0].text, "#a");
+ assertEquals(hashtags[3].children![0].text, "#interpunction");
+ assertEquals(hashtags[4].children![0].text, "#exclamation");
+ assertEquals(hashtags[5].children![0].text, "#question");
+ assertEquals(hashtags[6].children![0].text, '#"tag with spaces and "');
+ assertEquals(hashtags[7].children![0].text, '#');
+ // multiple lines not allowed
+ assertEquals(hashtags[8].children![0].text, "#no");
+ assertEquals(hashtags[9].children![0].text, "#spacing");
+ assertEquals(hashtags[10].children![0].text, "#3dprint");
+ assertEquals(hashtags[11].children![0].text, "#15-52_Trip-to-NYC");
+ assertEquals(hashtags[12].children![0].text, "#żółć");
+ assertEquals(hashtags[13].children![0].text, "#井号");
+});
diff --git a/website/Markdown/Extensions.md b/website/Markdown/Extensions.md
index b030c612..91441a29 100644
--- a/website/Markdown/Extensions.md
+++ b/website/Markdown/Extensions.md
@@ -10,7 +10,7 @@ In addition to supporting [[Markdown/Basics|markdown basics]] as standardized by
* [[Transclusions]] syntax
* [[Markdown/Anchors]]
* [[Markdown/Admonitions]]
-* Hashtags, e.g. `#mytag`.
+* [[Markdown/Hashtags]]
* [[Markdown/Command links]] syntax
* [Tables](https://www.markdownguide.org/extended-syntax/#tables)
* [Task lists](https://www.markdownguide.org/extended-syntax/#task-lists)
diff --git a/website/Markdown/Hashtags.md b/website/Markdown/Hashtags.md
new file mode 100644
index 00000000..4cdcaf42
--- /dev/null
+++ b/website/Markdown/Hashtags.md
@@ -0,0 +1,16 @@
+#level/beginner
+
+These can be used in text to assign an [[Objects#tag]] #like-this. If hashtags are the only content of first paragraph, they are applied to the entire page.
+
+Hashtags can contain letters, dashes, underscores and other characters, but not:
+- Whitespace (space, newline etc.)
+- Characters from this list `!@#$%^&*(),.?":{}|<>\`
+- Consist of digits only #123 (but #3dprint is recognised)
+
+If you need your tags to contain these characters, you have to surround the tag content with either:
+- Double quotes `#"tag in quotes"` #“tag in quotes”
+- Angle brackets # #
+
+```query
+tag where page = @page.name
+```
\ No newline at end of file