added linking support to headers

This commit is contained in:
Amy G. Bowersox 2024-08-14 22:50:05 -06:00
parent 7a5011fa88
commit 1663d178ca
2 changed files with 106 additions and 3 deletions

View File

@ -45,6 +45,7 @@ COMMENT_MARKER = '%%'
# Tags pattern # Tags pattern
OBSTAG_PATTERN = r'#([a-zA-Z0-9/_-]+)' OBSTAG_PATTERN = r'#([a-zA-Z0-9/_-]+)'
def is_proper_url(s: str) -> bool: def is_proper_url(s: str) -> bool:
""" """
Checks to see if a string is a "proper" URL. Checks to see if a string is a "proper" URL.
@ -75,6 +76,19 @@ def sanitize_reference(s: str) -> str:
return rc return rc
def make_hash_value(s: str) -> str:
"""
Create a secure hash value for the passed-in string.
Args:
s (str): The string to be hashed.
Returns:
A secure hash value for the string, expressed as hex digits.
"""
return hashlib.sha1(bytes(s, 'utf-8')).hexdigest()
class MetaStripper(Extension): class MetaStripper(Extension):
""" """
An extension that strips the metadata off the front of Obsidian pages, as it's already been parsed in an An extension that strips the metadata off the front of Obsidian pages, as it's already been parsed in an
@ -110,6 +124,82 @@ class MetaStripper(Extension):
md.preprocessors.register(MetaStripper.MetaStripperProc(md), 'metastripper', PRIO_BASE) md.preprocessors.register(MetaStripper.MetaStripperProc(md), 'metastripper', PRIO_BASE)
class ObsidianHeaders(Extension):
"""An extension that augments the existing header processing."""
class HashHeaderProcessor(BlockProcessor):
""" Process Hash Headers. """
# Detect a header at start of any line in block
RE = re.compile(r'(?:^|\n)(?P<level>#{1,6})(?P<header>(?:\\.|[^\\])*?)#*(?:\n|$)')
def test(self, parent: etree.Element, block: str) -> bool:
"""
Tests to see whether the current block can be handled by this processor.
Args:
parent (etree.Element): The current parent element.
block (str): The current block to be tested.
Returns:
bool: ``True`` if this processor can handle the block, ``False`` if not.
"""
return bool(self.RE.search(block))
def run(self, parent: etree.Element, blocks: list[str]) -> None:
"""
Processes the text in the current block and adds elements to the parent element.
Args:
parent (etree.Element): Parent element to add new subelements to.
blocks (list[str]): Blocks of text to be processed. The first block in this list is the one for which
the ``test`` method returned ``True``. This method should remove any parsed text from the
``block`` list before it returns.
Returns:
bool: ``True`` if text was parsed by this method, ``False`` if not.
"""
block = blocks.pop(0)
m = self.RE.search(block)
if m:
before = block[:m.start()] # All lines before header
after = block[m.end():] # All lines after header
if before:
# As the header was not the first line of the block and the
# lines before the header must be parsed first,
# recursively parse this lines as a block.
self.parser.parseBlocks(parent, [before])
# Get the header text and its hash.
hdr_text = m.group('header').strip()
hval = make_hash_value(hdr_text)
# Create header (with anchor) using named groups from RE
a = etree.SubElement(parent, 'a', {'class': 'hdrref', 'name': f"hdr-{hval}"})
h = etree.SubElement(a, 'h%d' % len(m.group('level')))
h.text = hdr_text
if after:
# Insert remaining lines as first block for future parsing.
if self.parser.state.isstate('looselist'):
# This is a weird edge case where a header is a child of a loose list
# and there is no blank line after the header. To ensure proper
# parsing, the line(s) after need to be detabbed. See #1443.
after = self.looseDetab(after)
blocks.insert(0, after)
else: # pragma: no cover
# This should never happen, but just in case...
logger.warn("We've got a problem header: %r" % block)
def extendMarkdown(self, md: Markdown) -> None: # noqa: N802
"""
Registers the header processor with the Markdown parser.
Args:
md (markdown.Markdown): The Markdown parser to register the patterns with.
"""
md.parser.blockprocessors.register(ObsidianHeaders.HashHeaderProcessor(md.parser), 'hashheader', 70)
class ObsidianImages(Extension): class ObsidianImages(Extension):
"""An extension that supports image tags the way Obsidian handles them.""" """An extension that supports image tags the way Obsidian handles them."""
__DIMS = re.compile(r'(.*)\|[ ]*(\d+)(?:[ ]*x[ ]*(\d+))?') __DIMS = re.compile(r'(.*)\|[ ]*(\d+)(?:[ ]*x[ ]*(\d+))?')
@ -328,6 +418,11 @@ class ObsidianLinks(Extension):
if len(t) > 1: if len(t) > 1:
text = t[1] text = t[1]
contents = t[0].rstrip('\\') contents = t[0].rstrip('\\')
t = contents.split('#')
hashloc = None
if len(t) > 1:
hashloc = make_hash_value(t[1].strip())
contents = t[0]
assert self._context.src_index is not None assert self._context.src_index is not None
node, _ = self._context.src_index.lookup(contents) node, _ = self._context.src_index.lookup(contents)
@ -336,8 +431,11 @@ class ObsidianLinks(Extension):
if node: if node:
# record a backlink from the current node to the found one # record a backlink from the current node to the found one
node.backlinks.add(self._context.current_node) node.backlinks.add(self._context.current_node)
return node.link_target(self._context.url_prefix, link = node.link_target(self._context.url_prefix,
self._context.current_node if self._context.relative_links else None), text self._context.current_node if self._context.relative_links else None)
if hashloc:
link = f"{link}#hdr-{hashloc}"
return link, text
return None, text return None, text
class ObsidianLinksProc(InlineProcessor): class ObsidianLinksProc(InlineProcessor):
@ -856,7 +954,7 @@ class ObsidianStyleFootnotes(FootnoteExtension):
if m_inline: if m_inline:
footnote = m_inline.group(1).strip() footnote = m_inline.group(1).strip()
id = hashlib.sha1(bytes(f"{footnote}{self._inlines_nonce}", 'utf-8')).hexdigest() id = make_hash_value(f"{footnote}{self._inlines_nonce}")
self._inlines_nonce += 1 self._inlines_nonce += 1
self.footnotes.setFootnote(id, footnote) self.footnotes.setFootnote(id, footnote)
blocks.insert(0, block[:m_inline.start(0)].rstrip() blocks.insert(0, block[:m_inline.start(0)].rstrip()
@ -1114,6 +1212,7 @@ def create_markdown_parser(context: Context) -> markdown.Markdown:
'tables', 'tables',
MetaStripper(), MetaStripper(),
ObsidianComments(), ObsidianComments(),
ObsidianHeaders(),
ObsidianStyleFootnotes(SUPERSCRIPT_TEXT='[{}]', SEPARATOR='-'), ObsidianStyleFootnotes(SUPERSCRIPT_TEXT='[{}]', SEPARATOR='-'),
ObsidianStyleBlockquotes(context), ObsidianStyleBlockquotes(context),
ObsidianImages(context), ObsidianImages(context),

View File

@ -18,6 +18,10 @@ STYLESHEET_DATA = """/* Dragonglass default CSS file - ensure all generated HTML
a { a {
color: #8a5cf5; color: #8a5cf5;
} }
a.hdrref {
color: inherit;
text-decoration: none;
}
.{{ invalid_reference }} { .{{ invalid_reference }} {
color: #ad8df8; color: #ad8df8;
text-decoration: underline; text-decoration: underline;