diff --git a/doc/metadata-values.txt b/doc/metadata-values.txt index 25ed5f7..84dff5c 100644 --- a/doc/metadata-values.txt +++ b/doc/metadata-values.txt @@ -2,8 +2,7 @@ METADATA VALUES USED BY DRAGONGLASS aliases (Obsidian standard metadata) - List of alternative names that can be used to link to a - particular page. + List of alternative names that can be used to link to a particular page. description (Obsidian Publish standard metadata) @@ -13,6 +12,10 @@ publish (Obsidian Publish standard metadata) If this boolean value is False, the page will not be published. +tags + (Obsidian standard metadata) + List of tags for this page. Tags may be defined here or inline in the text. + template The file name of the template to be used to render this page, overriding the default. diff --git a/doc/template-vars.txt b/doc/template-vars.txt index f39355b..1c815a1 100644 --- a/doc/template-vars.txt +++ b/doc/template-vars.txt @@ -17,6 +17,9 @@ dragonglass_version python_version The version number of Python that's running dragonglass. +tags: + A list of all tags the page being rendered has, in sorted order. + text The text of the page being rendered. diff --git a/src/dragonglass/mparse.py b/src/dragonglass/mparse.py index 0fb9efc..76f80b4 100644 --- a/src/dragonglass/mparse.py +++ b/src/dragonglass/mparse.py @@ -42,6 +42,8 @@ INLINE_FOOTNOTE_REF_PATTERN = INLINE_FOOTNOTE_REF_PREFIX + "{}" + ETX # Obsidian comment marker COMMENT_MARKER = '%%' +# Tags pattern +OBSTAG_PATTERN = r'#([a-zA-Z0-9/_-]+)' def is_proper_url(s: str) -> bool: """ @@ -1033,6 +1035,49 @@ class ObsidianStyleBlockquotes(Extension): 'callout-text', 11) +class ObsidianTags(Extension): + def __init__(self, context: Context, **kwargs: dict[str, Any]) -> None: + """ + Initialize the ObsidianStyleBlockquotes extension. + + Args: + context (Context): Context object that contains configuration information. + **kwargs (dict[str, Any]: Additional configuration information. + """ + super(ObsidianTags, self).__init__(**kwargs) + self._context = context + + def stash_tag(self, tagname: str) -> None: + self._context.current_node.stash_tag(tagname) + + class ObsidianTagsProcessor(InlineProcessor): + def __init__(self, pattern: str, md: markdown.Markdown, extref: Any) -> None: + """ + Initialize the GenericLinksProc. + + Args: + pattern (str): Regular expression pattern to be matched by this processor. + md (markdown.Markdown): Reference to the Markdown parser. + extref (ObsidianLinks): Backreference to the outer object. + """ + super(ObsidianTags.ObsidianTagsProcessor, self).__init__(pattern, md) + self._extref = extref + + def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]: # noqa: N802 + tagname = m.group(1) + if not re.search(r'[^0-9]', tagname): + return None, None, None + self._extref.stash_tag(tagname) + tag = etree.Element('span') + tag.attrib['class'] = "tag" + tag.text = f"#{tagname}" + return tag, m.start(0), m.end(0) + + def extendMarkdown(self, md) -> None: + md.inlinePatterns.register(ObsidianTags.ObsidianTagsProcessor(OBSTAG_PATTERN, md, self), + 'obsidian-tags', PRIO_BASE + 40) + + def create_markdown_parser(context: Context) -> markdown.Markdown: """ Creates a Markdown parser with all our extensions loaded. @@ -1063,5 +1108,6 @@ def create_markdown_parser(context: Context) -> markdown.Markdown: ObsidianImages(context), ObsidianLinks(context), ObsidianLists(), - ObsidianInlines()], + ObsidianInlines(), + ObsidianTags(context)], extension_configs=extconfig) diff --git a/src/dragonglass/style.py b/src/dragonglass/style.py index 0665659..9e8d626 100644 --- a/src/dragonglass/style.py +++ b/src/dragonglass/style.py @@ -36,6 +36,17 @@ blockquote { span.task-list-item-checked { text-decoration-line: line-through; } +span.tag { + background-color: hsla(258, 88%, 66%, 0.1); + border: 0px solid hsla(258, 88%, 66%, 0.15); + border-radius: 2em; + color: hsl(258, 88%, 66%); + font-size: 0.875em; + font-weight: inherit; + text-decoration: none; + padding: 0.25em 0.65em; + line-height: 1; +} div.codehilite { padding: 0.1em 0.25em; margin-top: 0.5em; diff --git a/src/dragonglass/tree.py b/src/dragonglass/tree.py index 4ae39e4..35f9bad 100644 --- a/src/dragonglass/tree.py +++ b/src/dragonglass/tree.py @@ -44,6 +44,7 @@ class SourceNode: self._path = path self._is_dir = is_dir self._is_md = path.match(MARKDOWN_PAT) + self._tags: dict[str, str] = {} self.metadata: dict[str, Any] = {} self.text: str | None = None self.backlinks: set[Any] = set() @@ -117,6 +118,17 @@ class SourceNode: return urlquote(xpath.relative_to(rel_path.parent, walk_up=True).as_posix()) return urlquote(prefix + xpath.as_posix()) + def stash_tag(self, tagname: str) -> None: + """ + Add a tag to the set of tags in this node. + + Args: + tagname (str): The tag name to be added. + """ + rtag = tagname.lower() + if rtag not in self._tags: + self._tags[rtag] = tagname + def load_metadata(self) -> None: """ Loads the metadata for this particular node and saves it in the "metadata" attribute. @@ -133,6 +145,9 @@ class SourceNode: metalines.append(cur_line) cur_line = f.readline() self.metadata = yaml.full_load(''.join(metalines)) + # Load up the initial tags. + for tag in self.metadata.get("tags", []): + self.stash_tag(tag) def parse_markdown(self, markdown_parser: markdown.Markdown) -> None: """ @@ -156,7 +171,8 @@ class SourceNode: return { "text": self.text, "title": self.page_title, - "description": self.metadata.get("description", "") + "description": self.metadata.get("description", ""), + "tags": [self._tags[n] for n in sorted(self._tags.keys())] }