created extensions mechanism and Breadcrumbs extension
This commit is contained in:
parent
17a9ed1273
commit
13bdcf7f58
|
@ -62,3 +62,11 @@ lang = "en"
|
||||||
sitebase = ""
|
sitebase = ""
|
||||||
# The site title. If supplied, this will be included in page metadata and used to formulate the default title.
|
# The site title. If supplied, this will be included in page metadata and used to formulate the default title.
|
||||||
sitetitle = ""
|
sitetitle = ""
|
||||||
|
|
||||||
|
[extensions]
|
||||||
|
# Full classnames of the extensions to be loaded.
|
||||||
|
load = ["dragonglass.extensions.breadcrumbs.BreadcrumbExtension"]
|
||||||
|
|
||||||
|
[extensions.BreadcrumbExtension]
|
||||||
|
# Configuration data for BreadcrumbExtension.
|
||||||
|
var = "value"
|
||||||
|
|
|
@ -16,6 +16,11 @@ tags
|
||||||
(Obsidian standard metadata)
|
(Obsidian standard metadata)
|
||||||
List of tags for this page. Tags may be defined here or inline in the text.
|
List of tags for this page. Tags may be defined here or inline in the text.
|
||||||
|
|
||||||
|
parent
|
||||||
|
(Breadcrumbs extension)
|
||||||
|
Reference to the parent page of this one. May be an Obsidian link, i.e. "[[pagename|text]]".
|
||||||
|
If this page is considered a "root" of a breadcrumb hierarchy, use the value ".".
|
||||||
|
|
||||||
template
|
template
|
||||||
The file name of the template to be used to render this page, overriding the default.
|
The file name of the template to be used to render this page, overriding the default.
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,12 @@ backlinks:
|
||||||
A list of pages that link to the page being rendered. Formatted as a list of dicts with
|
A list of pages that link to the page being rendered. Formatted as a list of dicts with
|
||||||
two elements, "name" and "link", containing the page title and the link to it, respectively.
|
two elements, "name" and "link", containing the page title and the link to it, respectively.
|
||||||
|
|
||||||
|
breadcrumbs:
|
||||||
|
(Breadcrumbs extension)
|
||||||
|
A list of pages forming the "breadcrumbs" from a root point to the page being rendered.
|
||||||
|
Formatted as a list of dicts with two elements, "name" and "link", containing the page title
|
||||||
|
and the link to it, respectively. The last element always has an empty "link" value.
|
||||||
|
|
||||||
default_stylesheet:
|
default_stylesheet:
|
||||||
The filename of the default stylesheet which is generated by dragonglass and added to the
|
The filename of the default stylesheet which is generated by dragonglass and added to the
|
||||||
generated pages.
|
generated pages.
|
||||||
|
|
|
@ -8,6 +8,7 @@ from typing import Any
|
||||||
|
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
|
from .extension import Extension
|
||||||
from .tree import SourceIndex, SourceNode
|
from .tree import SourceIndex, SourceNode
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,6 +49,33 @@ CALLOUT_ICONS = {
|
||||||
'todo': 'circle-check',
|
'todo': 'circle-check',
|
||||||
'warning': 'triangle-alert'
|
'warning': 'triangle-alert'
|
||||||
}
|
}
|
||||||
|
"""Cache of extension module names."""
|
||||||
|
EXTENSION_PKGS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def load_extension_class(name: str, sect: dict[str, Any]) -> Extension:
|
||||||
|
"""
|
||||||
|
Load a new extension class.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): Fully qualified classname of the extension to be loaded.
|
||||||
|
sect (dict[str, Any]): Reference to the [extensions] section of the configuration file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Extension: Instance of the extension class.
|
||||||
|
"""
|
||||||
|
components = name.split('.')
|
||||||
|
classname = components.pop()
|
||||||
|
modname = ".".join(components)
|
||||||
|
if modname not in EXTENSION_PKGS:
|
||||||
|
EXTENSION_PKGS[modname] = __import__(modname)
|
||||||
|
extclass = getattr(EXTENSION_PKGS[modname], classname)
|
||||||
|
cfg_sect = sect.get(classname, {})
|
||||||
|
rc = extclass(cfg_sect)
|
||||||
|
if not isinstance(rc, Extension):
|
||||||
|
raise RuntimeError(f"class {name} is not a valid Dragonglass extension")
|
||||||
|
return rc
|
||||||
|
|
||||||
|
|
||||||
class Context:
|
class Context:
|
||||||
"""
|
"""
|
||||||
|
@ -68,6 +96,7 @@ class Context:
|
||||||
self.src_index: SourceIndex | None = None
|
self.src_index: SourceIndex | None = None
|
||||||
self.current_node: SourceNode | None = None
|
self.current_node: SourceNode | None = None
|
||||||
self._default_template_name: str | None = None
|
self._default_template_name: str | None = None
|
||||||
|
self._extensions: list[Extension] = []
|
||||||
|
|
||||||
def load_config(self, args: Namespace) -> None:
|
def load_config(self, args: Namespace) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -82,10 +111,18 @@ class Context:
|
||||||
if config_path.exists() and config_path.is_file():
|
if config_path.exists() and config_path.is_file():
|
||||||
with open(config_path, "rb") as f:
|
with open(config_path, "rb") as f:
|
||||||
self.config = tomllib.load(f)
|
self.config = tomllib.load(f)
|
||||||
|
|
||||||
|
# Load several base variables.
|
||||||
templates_section = self.config.get("templates", {})
|
templates_section = self.config.get("templates", {})
|
||||||
self.template_dir = self.source_dir / templates_section.get("directory", DEFAULT_TEMPLATE_DIRECTORY)
|
self.template_dir = self.source_dir / templates_section.get("directory", DEFAULT_TEMPLATE_DIRECTORY)
|
||||||
self._default_template_name = templates_section.get("default", DEFAULT_TEMPLATE_NAME)
|
self._default_template_name = templates_section.get("default", DEFAULT_TEMPLATE_NAME)
|
||||||
|
|
||||||
|
# Load the extensions.
|
||||||
|
extensions_section = self.config.get("extensions", {})
|
||||||
|
load_list = extensions_section.get("load", [])
|
||||||
|
for ext_name in load_list:
|
||||||
|
self._extensions.append(load_extension_class(ext_name, extensions_section))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_stylesheet(self) -> str:
|
def default_stylesheet(self) -> str:
|
||||||
"""Returns the default stylesheet name."""
|
"""Returns the default stylesheet name."""
|
||||||
|
@ -182,3 +219,36 @@ class Context:
|
||||||
"""
|
"""
|
||||||
callout_icons = self.config.get('callout-icons', {})
|
callout_icons = self.config.get('callout-icons', {})
|
||||||
return callout_icons.get(callout, CALLOUT_ICONS.get(callout, callout_icons.get('_default', CALLOUT_DEFICON)))
|
return callout_icons.get(callout, CALLOUT_ICONS.get(callout, callout_icons.get('_default', CALLOUT_DEFICON)))
|
||||||
|
|
||||||
|
def on_loaded_metadata(self, node: SourceNode) -> None:
|
||||||
|
"""
|
||||||
|
Call the "loaded_metadata" method of each extension.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node (SourceNode): Current node that's bneen processed.
|
||||||
|
"""
|
||||||
|
for ext in self._extensions:
|
||||||
|
ext.loaded_metadata(self, node)
|
||||||
|
|
||||||
|
def on_indexed(self, index: SourceIndex) -> None:
|
||||||
|
"""
|
||||||
|
Call the "indexed" method of each extension.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index (SourceIndex): The node index.
|
||||||
|
"""
|
||||||
|
for ext in self._extensions:
|
||||||
|
ext.indexed(self, index)
|
||||||
|
|
||||||
|
def augment_template_vars(self, node: SourceNode, variables: dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Augment the template variables by checking extensions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node (SourceNode): The current node about to be rendered.
|
||||||
|
variables (dict[str, Any]): The template variables being builkt for template rendering.
|
||||||
|
"""
|
||||||
|
for ext in self._extensions:
|
||||||
|
new_vars = ext.supply_template_vars(self, node)
|
||||||
|
if new_vars:
|
||||||
|
variables |= new_vars
|
||||||
|
|
|
@ -81,8 +81,10 @@ def main() -> int:
|
||||||
logger.info(f"Loading metadata for {node}")
|
logger.info(f"Loading metadata for {node}")
|
||||||
context.current_node = node
|
context.current_node = node
|
||||||
node.load_metadata()
|
node.load_metadata()
|
||||||
|
context.on_loaded_metadata(node)
|
||||||
|
|
||||||
context.src_index = SourceIndex(nodes)
|
context.src_index = SourceIndex(nodes)
|
||||||
|
context.on_indexed(context.src_index)
|
||||||
|
|
||||||
mdparse = create_markdown_parser(context)
|
mdparse = create_markdown_parser(context)
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
|
68
src/dragonglass/extension.py
Normal file
68
src/dragonglass/extension.py
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .tree import SourceNode, SourceIndex
|
||||||
|
|
||||||
|
"""Base class for extensions to the conversion process."""
|
||||||
|
|
||||||
|
|
||||||
|
class Extension:
|
||||||
|
"""The base class for all dragonglass extensions."""
|
||||||
|
def __init__(self, config: dict[str, Any]):
|
||||||
|
"""
|
||||||
|
Initialize the extension.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config (dict[str, Any]): Configuration values for this extension.
|
||||||
|
"""
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def link_target(ctxt: Any, from_node: SourceNode, to_node: SourceNode) -> str:
|
||||||
|
"""
|
||||||
|
Produces a link target value from one node to another.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctxt (Context): Context for the operation.
|
||||||
|
from_node (SourceNode): The node that is linking to another page.
|
||||||
|
to_node (SourceNode): The node being linked to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The correct link target.
|
||||||
|
"""
|
||||||
|
return to_node.link_target(ctxt.url_prefix, from_node if ctxt.relative_links else None)
|
||||||
|
|
||||||
|
def loaded_metadata(self, ctxt: Any, node: SourceNode) -> None:
|
||||||
|
"""
|
||||||
|
Called after the metadata for a node has been loaded. Should be overridden by derived classes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctxt (Context): Context for the operation.
|
||||||
|
node (SourceNode): Node that's just had its metadata loaded.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def indexed(self, ctxt: Any, index: SourceIndex) -> None:
|
||||||
|
"""
|
||||||
|
Called after the index for the nodes has been generated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctxt (Context): Context for the operation.
|
||||||
|
index (SourceIndex): The index to all nodes.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def supply_template_vars(self, ctxt: Any, node: SourceNode) -> dict[str, Any] | None:
|
||||||
|
"""
|
||||||
|
Called to supply additional template variables to the template to be rendered.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctxt (Context): Context for the operation.
|
||||||
|
node (SourceNode): The node currently being processed for template rendering.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: If not ``None``, contains additional template variables to be added before
|
||||||
|
the template is rendered.
|
||||||
|
"""
|
||||||
|
return None
|
74
src/dragonglass/extensions/breadcrumbs.py
Normal file
74
src/dragonglass/extensions/breadcrumbs.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..extension import Extension
|
||||||
|
from ..tree import SourceNode, SourceIndex
|
||||||
|
|
||||||
|
"""The extension for generating breadcrumb informatioon."""
|
||||||
|
|
||||||
|
|
||||||
|
class BreadcrumbExtension(Extension):
|
||||||
|
LINK = re.compile(r'^\[\[(.+)(?:\|.+)?\]\]')
|
||||||
|
|
||||||
|
def __init__(self, config: dict[str, Any]):
|
||||||
|
super(BreadcrumbExtension, self).__init__(config)
|
||||||
|
self._root_nodes: list[SourceNode] = []
|
||||||
|
self._raw_targets: list[tuple[SourceNode, str]] = []
|
||||||
|
self._parents: dict[SourceNode, SourceNode] = {}
|
||||||
|
|
||||||
|
def loaded_metadata(self, ctxt: Any, node: SourceNode) -> None:
|
||||||
|
"""
|
||||||
|
Called after the metadata for a node has been loaded. Should be overridden by derived classes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctxt (Context): Context for the operation.
|
||||||
|
node (SourceNode): Node that's just had its metadata loaded.
|
||||||
|
"""
|
||||||
|
if node.publish:
|
||||||
|
target_str = node.metadata.get("parent", None)
|
||||||
|
if target_str:
|
||||||
|
m = self.LINK.match(str(target_str))
|
||||||
|
if m:
|
||||||
|
target_str = m.group(1)
|
||||||
|
if target_str == '.':
|
||||||
|
self._root_nodes.append(node)
|
||||||
|
else:
|
||||||
|
self._raw_targets.append((node, target_str))
|
||||||
|
|
||||||
|
def indexed(self, ctxt: Any, index: SourceIndex) -> None:
|
||||||
|
"""
|
||||||
|
Called after the index for the nodes has been generated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctxt (Context): Context for the operation.
|
||||||
|
index (SourceIndex): The index to all nodes.
|
||||||
|
"""
|
||||||
|
for node, target in self._raw_targets:
|
||||||
|
target_node, _ = index.lookup(node, target)
|
||||||
|
if target_node:
|
||||||
|
self._parents[node] = target_node
|
||||||
|
|
||||||
|
def supply_template_vars(self, ctxt: Any, node: SourceNode) -> dict[str, Any] | None:
|
||||||
|
"""
|
||||||
|
Called to supply additional template variables to the template to be rendered.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctxt (Context): Context for the operation.
|
||||||
|
node (SourceNode): The node currently being processed for template rendering.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, Any]: If not ``None``, contains additional template variables to be added before
|
||||||
|
the template is rendered.
|
||||||
|
"""
|
||||||
|
breadcrumbs: list[dict[str, str]] = [{"name": "." if node in self._root_nodes else node.page_title,
|
||||||
|
"link": ""}]
|
||||||
|
cur_node = node
|
||||||
|
while cur_node not in self._root_nodes:
|
||||||
|
cur_node = self._parents.get(cur_node, None)
|
||||||
|
if not cur_node:
|
||||||
|
return None
|
||||||
|
breadcrumbs.insert(0, {"name": "." if cur_node in self._root_nodes else cur_node.page_title,
|
||||||
|
"link": self.link_target(ctxt, node, cur_node)})
|
||||||
|
return {"breadcrumbs": breadcrumbs}
|
|
@ -4,7 +4,7 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from jinja2 import Environment, BaseLoader, ChoiceLoader, FunctionLoader, FileSystemLoader
|
from jinja2 import Environment, BaseLoader, ChoiceLoader, FunctionLoader, FileSystemLoader
|
||||||
from typing import Any, Callable
|
from typing import Any
|
||||||
from urllib.parse import quote as urlquote
|
from urllib.parse import quote as urlquote
|
||||||
|
|
||||||
from .config import Context, DEFAULT_TEMPLATE_NAME
|
from .config import Context, DEFAULT_TEMPLATE_NAME
|
||||||
|
@ -152,6 +152,8 @@ def template_vars(node: SourceNode, ctxt: Context) -> dict[str, Any]:
|
||||||
# Add reference to the default stylesheet.
|
# Add reference to the default stylesheet.
|
||||||
tvars['default_stylesheet'] = makepath(ctxt, node, ctxt.default_stylesheet)
|
tvars['default_stylesheet'] = makepath(ctxt, node, ctxt.default_stylesheet)
|
||||||
|
|
||||||
|
# Augment template variables with the extensions.
|
||||||
|
ctxt.augment_template_vars(node, tvars)
|
||||||
return tvars
|
return tvars
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user