added linking support to headers
This commit is contained in:
parent
7a5011fa88
commit
1663d178ca
|
@ -45,6 +45,7 @@ COMMENT_MARKER = '%%'
|
|||
# Tags pattern
|
||||
OBSTAG_PATTERN = r'#([a-zA-Z0-9/_-]+)'
|
||||
|
||||
|
||||
def is_proper_url(s: str) -> bool:
|
||||
"""
|
||||
Checks to see if a string is a "proper" URL.
|
||||
|
@ -75,6 +76,19 @@ def sanitize_reference(s: str) -> str:
|
|||
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):
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""An extension that supports image tags the way Obsidian handles them."""
|
||||
__DIMS = re.compile(r'(.*)\|[ ]*(\d+)(?:[ ]*x[ ]*(\d+))?')
|
||||
|
@ -328,6 +418,11 @@ class ObsidianLinks(Extension):
|
|||
if len(t) > 1:
|
||||
text = t[1]
|
||||
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
|
||||
node, _ = self._context.src_index.lookup(contents)
|
||||
|
@ -336,8 +431,11 @@ class ObsidianLinks(Extension):
|
|||
if node:
|
||||
# record a backlink from the current node to the found one
|
||||
node.backlinks.add(self._context.current_node)
|
||||
return node.link_target(self._context.url_prefix,
|
||||
self._context.current_node if self._context.relative_links else None), text
|
||||
link = node.link_target(self._context.url_prefix,
|
||||
self._context.current_node if self._context.relative_links else None)
|
||||
if hashloc:
|
||||
link = f"{link}#hdr-{hashloc}"
|
||||
return link, text
|
||||
return None, text
|
||||
|
||||
class ObsidianLinksProc(InlineProcessor):
|
||||
|
@ -856,7 +954,7 @@ class ObsidianStyleFootnotes(FootnoteExtension):
|
|||
|
||||
if m_inline:
|
||||
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.footnotes.setFootnote(id, footnote)
|
||||
blocks.insert(0, block[:m_inline.start(0)].rstrip()
|
||||
|
@ -1114,6 +1212,7 @@ def create_markdown_parser(context: Context) -> markdown.Markdown:
|
|||
'tables',
|
||||
MetaStripper(),
|
||||
ObsidianComments(),
|
||||
ObsidianHeaders(),
|
||||
ObsidianStyleFootnotes(SUPERSCRIPT_TEXT='[{}]', SEPARATOR='-'),
|
||||
ObsidianStyleBlockquotes(context),
|
||||
ObsidianImages(context),
|
||||
|
|
|
@ -18,6 +18,10 @@ STYLESHEET_DATA = """/* Dragonglass default CSS file - ensure all generated HTML
|
|||
a {
|
||||
color: #8a5cf5;
|
||||
}
|
||||
a.hdrref {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.{{ invalid_reference }} {
|
||||
color: #ad8df8;
|
||||
text-decoration: underline;
|
||||
|
|
Loading…
Reference in New Issue
Block a user