allow configuration of some code generation details within Markdown converter
This commit is contained in:
parent
72a2ea1ab5
commit
8c4ab0c4d5
|
@ -6,6 +6,41 @@ prefix = "/"
|
|||
# If true, generate relative URLs for all internal URLs. Default is false.
|
||||
relative = false
|
||||
|
||||
[classnames]
|
||||
# CSS class to use for an invalid reference.
|
||||
invalid-reference = "invalid-reference"
|
||||
# CSS class to use for an Obsidian link.
|
||||
obsidian-link = "obsidian-link"
|
||||
|
||||
[callout-icons]
|
||||
# The default callout icon.
|
||||
_default = "pencil"
|
||||
# Specific callout icons for other callouts.
|
||||
abstract = "clipboard-list"
|
||||
attention = "triangle-alert"
|
||||
bug = "bug"
|
||||
caution = "triangle-alert"
|
||||
check = "check"
|
||||
cite = "quote"
|
||||
danger = "zap"
|
||||
done = "check"
|
||||
error = "zap"
|
||||
example = "list"
|
||||
fail = "x"
|
||||
failure = "x"
|
||||
faq = "circle-help"
|
||||
help = "circle-help"
|
||||
info = "info"
|
||||
missing = "x"
|
||||
question = "circle-help"
|
||||
quote = "quote"
|
||||
success = "check"
|
||||
summary = "clipboard-list"
|
||||
tip = "flame"
|
||||
tldr = "clipboard-list"
|
||||
todo = "circle-check"
|
||||
warning = "triangle-alert"
|
||||
|
||||
[templates]
|
||||
# The template directory name under the Obsidian vault. Default is ".dragonglass.tmpl".
|
||||
directory = ".dragonglass.tmpl"
|
||||
|
|
|
@ -19,7 +19,35 @@ DEFAULT_TEMPLATE_DIRECTORY = ".dragonglass.tmpl"
|
|||
DEFAULT_TEMPLATE_NAME = "default.html"
|
||||
""" The default stylesheet name."""
|
||||
DEFAULT_STYLESHEET_NAME = "dragonglass.css"
|
||||
|
||||
"""The default callout icon."""
|
||||
CALLOUT_DEFICON = 'pencil'
|
||||
"""The default callout icons."""
|
||||
CALLOUT_ICONS = {
|
||||
'abstract': 'clipboard-list',
|
||||
'attention': 'triangle-alert',
|
||||
'bug': 'bug',
|
||||
'caution': 'triangle-alert',
|
||||
'check': 'check',
|
||||
'cite': 'quote',
|
||||
'danger': 'zap',
|
||||
'done': 'check',
|
||||
'error': 'zap',
|
||||
'example': 'list',
|
||||
'fail': 'x',
|
||||
'failure': 'x',
|
||||
'faq': 'circle-help',
|
||||
'help': 'circle-help',
|
||||
'info': 'info',
|
||||
'missing': 'x',
|
||||
'question': 'circle-help',
|
||||
'quote': 'quote',
|
||||
'success': 'check',
|
||||
'summary': 'clipboard-list',
|
||||
'tip': 'flame',
|
||||
'tldr': 'clipboard-list',
|
||||
'todo': 'circle-check',
|
||||
'warning': 'triangle-alert'
|
||||
}
|
||||
|
||||
class Context:
|
||||
"""
|
||||
|
@ -98,3 +126,29 @@ class Context:
|
|||
if not self.template_dir.is_dir():
|
||||
return DEFAULT_TEMPLATE_NAME
|
||||
return node.metadata.get("template", self._default_template_name)
|
||||
|
||||
def get_classname(self, classname: str) -> str:
|
||||
"""
|
||||
Return a classname configured in the configuration file.
|
||||
|
||||
Args:
|
||||
classname (str): The key for the classname to get, which is also the default value.
|
||||
|
||||
Returns:
|
||||
str: the configured classname, or the ``classname`` argument itself if nothing was configured.
|
||||
"""
|
||||
classnames_section = self.config.get("classnames", {})
|
||||
return classnames_section.get(classname, classname)
|
||||
|
||||
def get_callout_iconname(self, callout: str) -> str:
|
||||
"""
|
||||
Return the name for a Lucide icon that represents a particular callout type.
|
||||
|
||||
Args:
|
||||
callout (str): The type of the callout in question.
|
||||
|
||||
Returns:
|
||||
str: The name of an icon, or a default icon name.
|
||||
"""
|
||||
callout_icons = self.config.get('callout-icons', {})
|
||||
return callout_icons.get(callout, CALLOUT_ICONS.get(callout, callout_icons.get('_default', CALLOUT_DEFICON)))
|
||||
|
|
|
@ -73,23 +73,6 @@ def sanitize_reference(s: str) -> str:
|
|||
return rc
|
||||
|
||||
|
||||
def find_extension(md: markdown.Markdown, cls: type[Extension]) -> Extension | None:
|
||||
"""
|
||||
Locate a registered extension in the Markdown parser.
|
||||
|
||||
Args:
|
||||
md (markdown.Markdown): The Markdown parser to look through.
|
||||
cls (type): The class of the extension to be retrieved.
|
||||
|
||||
Returns:
|
||||
Extension: The retrieved extension, or ``None`` if it was not found.
|
||||
"""
|
||||
for ex in md.registeredExtensions:
|
||||
if isinstance(ex, cls):
|
||||
return ex
|
||||
return None
|
||||
|
||||
|
||||
class MetaStripper(Extension):
|
||||
"""
|
||||
An extension that strips the metadata off the front of Obsidian pages, as it's already been parsed in an
|
||||
|
@ -145,7 +128,7 @@ class ObsidianImages(Extension):
|
|||
"""
|
||||
Returns the CSS class name for an invalid reference in the text.
|
||||
"""
|
||||
return 'invalid-reference'
|
||||
return self._context.get_classname('invalid-reference')
|
||||
|
||||
def _parse_dimensions(self, s: str) -> tuple[str, int, int]:
|
||||
"""
|
||||
|
@ -306,18 +289,37 @@ class ObsidianLinks(Extension):
|
|||
Markdown link processing to handle Obsidian internal links as well as external links.
|
||||
"""
|
||||
def __init__(self, context: Context, **kwargs: dict[str, Any]) -> None:
|
||||
"""
|
||||
Initialize the ObsidianLinks extension.
|
||||
|
||||
Args:
|
||||
context (Context): Context object that contains configuration information.
|
||||
**kwargs (dict[str, Any]: Additional configuration information.
|
||||
"""
|
||||
super(ObsidianLinks, self).__init__(**kwargs)
|
||||
self._context = context
|
||||
|
||||
@property
|
||||
def obsidian_link_classname(self) -> str:
|
||||
return 'obsidian-link'
|
||||
"""Returns the classname for Obsidian links."""
|
||||
return self._context.get_classname('obsidian-link')
|
||||
|
||||
@property
|
||||
def invalid_reference_classname(self) -> str:
|
||||
return 'invalid-reference'
|
||||
"""Returns the classname for invalid references."""
|
||||
return self._context.get_classname('invalid-reference')
|
||||
|
||||
def _parse_reference(self, contents: str) -> tuple[str | None, str]:
|
||||
"""
|
||||
Parse a reference and break it into link target and title.
|
||||
|
||||
Args:
|
||||
contents (str): Contents of a link reference to look up.
|
||||
|
||||
Returns:
|
||||
str: The link target, or ``None`` if the link is invalid.
|
||||
str: The link title to be used.
|
||||
"""
|
||||
contents = contents.replace(r'\|','|') # handle case where we're inside tables
|
||||
text = None
|
||||
t = contents.split('|')
|
||||
|
@ -337,11 +339,34 @@ class ObsidianLinks(Extension):
|
|||
return None, text
|
||||
|
||||
class ObsidianLinksProc(InlineProcessor):
|
||||
"""Processor that handles Obsidian links, [[page]]."""
|
||||
def __init__(self, pattern: str, md: markdown.Markdown, extref: Any) -> None:
|
||||
"""
|
||||
Initialize the ObsidianLinksProc.
|
||||
|
||||
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(ObsidianLinks.ObsidianLinksProc, self).__init__(pattern, md)
|
||||
self._extref = extref
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]: # noqa: N802
|
||||
"""
|
||||
Handles a match on the reference for this processor.
|
||||
|
||||
Args:
|
||||
m (re.Match[str]): The regular expression match data.
|
||||
data (str): The entire block of text surrounding the pattern, as a multi-line string.
|
||||
|
||||
Returns:
|
||||
el (etree.Element): The new HTML element being added to the tree, or ``None`` if the match was rejected.
|
||||
int: The index of the first character in ``data`` that was "consumed" by the pattern, or ``None``
|
||||
if the match was rejected,
|
||||
int: The index of the first character in ``data`` that was *not* consumed by the pattern, or ``None``
|
||||
if the match was rejected.
|
||||
"""
|
||||
link, text = self._extref._parse_reference(m.group(1))
|
||||
if link is None:
|
||||
el = etree.Element('span')
|
||||
|
@ -355,11 +380,34 @@ class ObsidianLinks(Extension):
|
|||
return el, m.start(0), m.end(0)
|
||||
|
||||
class GenericLinksProc(InlineProcessor):
|
||||
"""Processor that handles generic links, [link text](url)."""
|
||||
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(ObsidianLinks.GenericLinksProc, self).__init__(pattern, md)
|
||||
self._extref = extref
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]: # noqa: N802
|
||||
"""
|
||||
Handles a match on the reference for this processor.
|
||||
|
||||
Args:
|
||||
m (re.Match[str]): The regular expression match data.
|
||||
data (str): The entire block of text surrounding the pattern, as a multi-line string.
|
||||
|
||||
Returns:
|
||||
el (etree.Element): The new HTML element being added to the tree, or ``None`` if the match was rejected.
|
||||
int: The index of the first character in ``data`` that was "consumed" by the pattern, or ``None``
|
||||
if the match was rejected,
|
||||
int: The index of the first character in ``data`` that was *not* consumed by the pattern, or ``None``
|
||||
if the match was rejected.
|
||||
"""
|
||||
text = m.group(1)
|
||||
link = m.group(2)
|
||||
if link.startswith('<') and link.endswith('>'): # handle whitespace encoding
|
||||
|
@ -635,7 +683,6 @@ class ObsidianLists(Extension):
|
|||
bool: ``True`` if this processor can handle the block, ``False`` if not.
|
||||
"""
|
||||
if self._extref.LIST_START in block:
|
||||
logger.debug("DETECT")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
@ -664,7 +711,6 @@ class ObsidianLists(Extension):
|
|||
blocks.insert(0, chunk[p + len(self._extref.LIST_END) + 1:])
|
||||
list_lines = chunk[:p].rstrip().split('\n')
|
||||
assert len(list_lines) > 0
|
||||
logger.debug(f"*** Found list: {list_lines}")
|
||||
_, excess = self._build_list(parent, list_lines)
|
||||
if len(excess) > 0:
|
||||
blocks.insert(0, '\n'.join(excess))
|
||||
|
@ -862,36 +908,36 @@ class ObsidianStyleFootnotes(FootnoteExtension):
|
|||
|
||||
|
||||
class ObsidianStyleBlockquotes(Extension):
|
||||
"""Extension that handles both blockquotes and callouts in the Obsidian style."""
|
||||
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(ObsidianStyleBlockquotes, self).__init__(**kwargs)
|
||||
self._context = context
|
||||
|
||||
def _callout_iconname(self, callout: str) -> str:
|
||||
"""
|
||||
Returns the Lucide icon name for a specific type of callout.
|
||||
|
||||
Args:
|
||||
callout (str): The callout type.
|
||||
|
||||
Returns:
|
||||
The icon name to use.
|
||||
"""
|
||||
return self._context.get_callout_iconname(callout)
|
||||
|
||||
class ObsidianBlockQuote(BlockQuoteProcessor):
|
||||
CALLOUT = re.compile(r'^\[!([a-z]+)\]([-+])?(?:[ ]+(.*))?')
|
||||
CALLOUT_DEFICON = 'pencil'
|
||||
CALLOUT_ICONS = {
|
||||
'abstract': 'clipboard-list',
|
||||
'attention': 'triangle-alert',
|
||||
'bug': 'bug',
|
||||
'caution': 'triangle-alert',
|
||||
'check': 'check',
|
||||
'cite': 'quote',
|
||||
'danger': 'zap',
|
||||
'done': 'check',
|
||||
'error': 'zap',
|
||||
'example': 'list',
|
||||
'fail': 'x',
|
||||
'failure': 'x',
|
||||
'faq': 'circle-help',
|
||||
'help': 'circle-help',
|
||||
'info': 'info',
|
||||
'missing': 'x',
|
||||
'question': 'circle-help',
|
||||
'quote': 'quote',
|
||||
'success': 'check',
|
||||
'summary': 'clipboard-list',
|
||||
'tip': 'flame',
|
||||
'tldr': 'clipboard-list',
|
||||
'todo': 'circle-check',
|
||||
'warning': 'triangle-alert'
|
||||
}
|
||||
|
||||
def __init__(self, parser: BlockParser, extref: Any) -> None:
|
||||
super(ObsidianStyleBlockquotes.ObsidianBlockQuote, self).__init__(parser)
|
||||
self._extref = extref
|
||||
|
||||
def normal_blockquote(self, parent: etree.Element, block: str) -> None:
|
||||
sibling = self.lastChild(parent)
|
||||
|
@ -908,7 +954,7 @@ class ObsidianStyleBlockquotes(Extension):
|
|||
self.parser.state.reset()
|
||||
|
||||
def callout_block(self, parent: etree.Element, lines: list[str]) -> None:
|
||||
m = self.CALLOUT.match(lines[0])
|
||||
m = self.CALLOUT.match(lines.pop(0))
|
||||
callout_type = m.group(1)
|
||||
folding = m.group(2)
|
||||
title = m.group(3)
|
||||
|
@ -923,7 +969,7 @@ class ObsidianStyleBlockquotes(Extension):
|
|||
title_div = etree.SubElement(base_div, 'div', {'class': 'callout-title'})
|
||||
icon_div = etree.SubElement(title_div, 'div', {'class': 'callout-icon'})
|
||||
etree.SubElement(icon_div, 'span',
|
||||
{'data-lucide': self.CALLOUT_ICONS.get(callout_type, self.CALLOUT_DEFICON)})
|
||||
{'data-lucide': self._extref._callout_iconname(callout_type)})
|
||||
inner_title_div = etree.SubElement(title_div, 'div', {'class': 'callout-title-inner'})
|
||||
inner_title_div.text = title
|
||||
if folding:
|
||||
|
@ -933,7 +979,6 @@ class ObsidianStyleBlockquotes(Extension):
|
|||
fold_div = etree.SubElement(title_div, 'div', {'class': baseclass})
|
||||
etree.SubElement(fold_div, 'span',
|
||||
{'data-lucide': 'chevron-right' if folding == '-' else 'chevron-down'})
|
||||
lines.pop(0)
|
||||
if len(lines) > 0:
|
||||
content_div = etree.SubElement(base_div, 'div', {'class': 'callout-content'})
|
||||
if folding:
|
||||
|
@ -955,11 +1000,10 @@ class ObsidianStyleBlockquotes(Extension):
|
|||
# Remove `> ` from beginning of each line.
|
||||
lines = [self.clean(line) for line in block[m.start():].split('\n')]
|
||||
callout = (self.CALLOUT.match(lines[0]) is not None)
|
||||
block = '\n'.join(lines)
|
||||
if callout:
|
||||
self.callout_block(parent, lines)
|
||||
else:
|
||||
self.normal_blockquote(parent, block)
|
||||
self.normal_blockquote(parent, '\n'.join(lines))
|
||||
|
||||
class CalloutLinesProcessor(BlockProcessor):
|
||||
def test(self, parent: etree.Element, block: str) -> bool:
|
||||
|
@ -984,7 +1028,7 @@ class ObsidianStyleBlockquotes(Extension):
|
|||
return True
|
||||
|
||||
def extendMarkdown(self, md) -> None:
|
||||
md.parser.blockprocessors.register(ObsidianStyleBlockquotes.ObsidianBlockQuote(md.parser), 'quote', 20)
|
||||
md.parser.blockprocessors.register(ObsidianStyleBlockquotes.ObsidianBlockQuote(md.parser, self), 'quote', 20)
|
||||
md.parser.blockprocessors.register(ObsidianStyleBlockquotes.CalloutLinesProcessor(md.parser),
|
||||
'callout-text', 11)
|
||||
|
||||
|
@ -1015,7 +1059,7 @@ def create_markdown_parser(context: Context) -> markdown.Markdown:
|
|||
MetaStripper(),
|
||||
ObsidianComments(),
|
||||
ObsidianStyleFootnotes(SUPERSCRIPT_TEXT='[{}]', SEPARATOR='-'),
|
||||
ObsidianStyleBlockquotes(),
|
||||
ObsidianStyleBlockquotes(context),
|
||||
ObsidianImages(context),
|
||||
ObsidianLinks(context),
|
||||
ObsidianLists(),
|
||||
|
|
|
@ -7,12 +7,18 @@ from jinja2 import Environment
|
|||
|
||||
from .config import Context
|
||||
|
||||
"""Class names we want to look up in the configuration file."""
|
||||
KNOWN_CLASSNAMES = [
|
||||
'invalid-reference',
|
||||
'obsidian-link'
|
||||
]
|
||||
|
||||
"""Template data for the default stylesheet."""
|
||||
STYLESHEET_DATA = """/* Dragonglass default CSS file - ensure all generated HTML pages reference this */
|
||||
a {
|
||||
color: #8a5cf5;
|
||||
}
|
||||
.invalid-reference {
|
||||
.{{ invalid_reference }} {
|
||||
color: #ad8df8;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #e6ddfd;
|
||||
|
@ -222,8 +228,15 @@ def write_default_stylesheet(ctxt: Context, tenv: Environment, dest_dir: Path) -
|
|||
tenv (Environment): Template engine used to render the default stylesheet data.
|
||||
dest_dir (Path): The destination directory to write the stylesheet to.
|
||||
"""
|
||||
vars = {}
|
||||
|
||||
# Fill in all the class names as variables.
|
||||
for n in KNOWN_CLASSNAMES:
|
||||
vname = n.replace("-", '_')
|
||||
vars[vname] = ctxt.get_classname(n)
|
||||
|
||||
to_file = dest_dir / ctxt.default_stylesheet
|
||||
tmpl = tenv.from_string(STYLESHEET_DATA)
|
||||
data = tmpl.render({})
|
||||
data = tmpl.render(vars)
|
||||
with to_file.open("wt") as f:
|
||||
f.write(data)
|
||||
|
|
Loading…
Reference in New Issue
Block a user