created extensions mechanism and Breadcrumbs extension
This commit is contained in:
parent
17a9ed1273
commit
13bdcf7f58
|
@ -62,3 +62,11 @@ lang = "en"
|
|||
sitebase = ""
|
||||
# The site title. If supplied, this will be included in page metadata and used to formulate the default title.
|
||||
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)
|
||||
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
|
||||
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
|
||||
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:
|
||||
The filename of the default stylesheet which is generated by dragonglass and added to the
|
||||
generated pages.
|
||||
|
|
|
@ -8,6 +8,7 @@ from typing import Any
|
|||
|
||||
import tomllib
|
||||
|
||||
from .extension import Extension
|
||||
from .tree import SourceIndex, SourceNode
|
||||
|
||||
|
||||
|
@ -48,6 +49,33 @@ CALLOUT_ICONS = {
|
|||
'todo': 'circle-check',
|
||||
'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:
|
||||
"""
|
||||
|
@ -68,6 +96,7 @@ class Context:
|
|||
self.src_index: SourceIndex | None = None
|
||||
self.current_node: SourceNode | None = None
|
||||
self._default_template_name: str | None = None
|
||||
self._extensions: list[Extension] = []
|
||||
|
||||
def load_config(self, args: Namespace) -> None:
|
||||
"""
|
||||
|
@ -82,10 +111,18 @@ class Context:
|
|||
if config_path.exists() and config_path.is_file():
|
||||
with open(config_path, "rb") as f:
|
||||
self.config = tomllib.load(f)
|
||||
|
||||
# Load several base variables.
|
||||
templates_section = self.config.get("templates", {})
|
||||
self.template_dir = self.source_dir / templates_section.get("directory", DEFAULT_TEMPLATE_DIRECTORY)
|
||||
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
|
||||
def default_stylesheet(self) -> str:
|
||||
"""Returns the default stylesheet name."""
|
||||
|
@ -182,3 +219,36 @@ class Context:
|
|||
"""
|
||||
callout_icons = self.config.get('callout-icons', {})
|
||||
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}")
|
||||
context.current_node = node
|
||||
node.load_metadata()
|
||||
context.on_loaded_metadata(node)
|
||||
|
||||
context.src_index = SourceIndex(nodes)
|
||||
context.on_indexed(context.src_index)
|
||||
|
||||
mdparse = create_markdown_parser(context)
|
||||
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
|
||||
|
||||
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 .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.
|
||||
tvars['default_stylesheet'] = makepath(ctxt, node, ctxt.default_stylesheet)
|
||||
|
||||
# Augment template variables with the extensions.
|
||||
ctxt.augment_template_vars(node, tvars)
|
||||
return tvars
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user