Skip to content

Commit

Permalink
Merge pull request #21 from sondregronas/hotfix/fix-incorrect-spacing
Browse files Browse the repository at this point in the history
Hotfix/fix incorrect spacing
  • Loading branch information
sondregronas authored Nov 11, 2024
2 parents e0dcefd + 3c9b89e commit 1e3809a
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 42 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mkdocs-callouts"
version = "1.14.0"
version = "1.14.1"
keywords = ["mkdocs", "mkdocs-plugin", "markdown", "callouts", "admonitions", "obsidian"]
description = "A simple plugin that converts Obsidian style callouts and converts them into mkdocs supported 'admonitions' (a.k.a. callouts)."
readme = "README.md"
Expand Down
91 changes: 50 additions & 41 deletions src/mkdocs_callouts/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import re

CALLOUT_BLOCK_REGEX = re.compile(r'^(\s*)((?:> ?)+) *\[!([^\]]*)\]([\-\+]?)(.*)?')
CALLOUT_BLOCK_REGEX = re.compile(r"^(\s*)((?:> ?)+) *\[!([^\]]*)\]([\-\+]?)(.*)?")
# (1): leading whitespace (all tabs and 4x spaces get reused)
# (2): indents (all leading '>' symbols)
# (3): callout type ([!'capture'] or [!'capture | attribute'] excl. brackets and leading !)
# (4): foldable token (+ or - or <blank>)
# (5): title

CALLOUT_CONTENT_SYNTAX_REGEX = re.compile(r'^(\s*)((?:> ?)+)')
CALLOUT_CONTENT_SYNTAX_REGEX = re.compile(r"^(\s*)((?:> ?)+)")


# (1): leading whitespace (all tabs and 4x spaces get reused)
Expand All @@ -16,18 +16,21 @@

class CalloutParser:
"""Class to parse callout blocks from markdown and convert them to mkdocs supported admonitions."""

# From https://help.obsidian.md/How+to/Use+callouts#Types
aliases = {
'abstract': ['summary', 'tldr'],
'tip': ['hint', 'important'],
'success': ['check', 'done'],
'question': ['help', 'faq'],
'warning': ['caution', 'attention'],
'failure': ['fail', 'missing'],
'danger': ['error'],
'quote': ['cite']
"abstract": ["summary", "tldr"],
"tip": ["hint", "important"],
"success": ["check", "done"],
"question": ["help", "faq"],
"warning": ["caution", "attention"],
"failure": ["fail", "missing"],
"danger": ["error"],
"quote": ["cite"],
}
alias_tuples = [(alias, c_type) for c_type, aliases in aliases.items() for alias in aliases]
alias_tuples = [
(alias, c_type) for c_type, aliases in aliases.items() for alias in aliases
]

def __init__(self, convert_aliases: bool = True, breakless_lists: bool = True):
# Stack to keep track of the current indentation level
Expand All @@ -49,19 +52,21 @@ def _get_indent(self, indent_level: int, is_block: bool = False) -> str:
"""
Returns the correct indent string for the current indent level.
"""
indent = ''
indent = ""
# If we are parsing a block, then we should not add a tab for the last indent level (only content needs it)
for i in range(indent_level - int(is_block)):
# If the indent level exists in the list, add a tab (callout), otherwise add a '> ' (blockquote)
indent += '\t' if i + 1 in self.indent_levels else '> '
indent += "\t" if i + 1 in self.indent_levels else "> "

# Blockquotes use spaces instead of tabs for consistent indentation (must be 4 spaces into the callout)
# When not using blockquotes, a tab should count as 4 spaces by default because of no preceding symbols
# blockquote: > !!! note "Title" || blockquote: > !!! note "Title"
# with \t > SSContent (wrong) || with spaces > SSSSContent (correct)
is_part_of_blockquote = '> ' in indent # indent should only be '\t' symbols if false.
is_part_of_blockquote = (
"> " in indent
) # indent should only be '\t' symbols if false.
if is_part_of_blockquote:
indent = indent.replace('\t', ' ' * 4)
indent = indent.replace("\t", " " * 4)

return indent

Expand All @@ -71,38 +76,40 @@ def _parse_block_syntax(self, block) -> str:
Takes an argument block, which is a regex match.
"""
# Group 1: Leading whitespace (we need to reuse tabs and 4x spaces)
whitespace = block.group(1).replace(' ', '\t')
whitespace = '\t' * whitespace.count('\t') # Ignore everything but tabs
whitespace = block.group(1).replace(" ", "\t")
whitespace = "\t" * whitespace.count("\t") # Ignore everything but tabs

# Group 2: Leading > symbols (indentation, for nested callouts)
indent_level = block.group(2).count('>')
indent = f'{whitespace}{self._get_indent(indent_level=indent_level, is_block=True)}'
indent_level = block.group(2).count(">")
indent = (
f"{whitespace}{self._get_indent(indent_level=indent_level, is_block=True)}"
)

# Group 3: Callout block type (note, warning, info, etc.) + inline block syntax
c_type = block.group(3).lower()
c_type = re.sub(r' *\| *(inline|left) *$', ' inline', c_type)
c_type = re.sub(r' *\| *(inline end|right) *$', ' inline end', c_type)
c_type = re.sub(r' *\|.*', '', c_type)
c_type = re.sub(r" *\| *(inline|left) *$", " inline", c_type)
c_type = re.sub(r" *\| *(inline end|right) *$", " inline end", c_type)
c_type = re.sub(r" *\|.*", "", c_type)
# Convert aliases, if enabled
if self.convert_aliases:
c_type = self._convert_aliases(c_type)

# Group 4: Foldable callouts
syntax = {'-': '???', '+': '???+'}
syntax = syntax.get(block.group(4), '!!!')
syntax = {"-": "???", "+": "???+"}
syntax = syntax.get(block.group(4), "!!!")

# Group 5: Title, add leading whitespace and quotation marks, if it exists
title = block.group(5).strip()
title = f' "{title}"' if title else ''
title = f' "{title}"' if title else ""

# Construct the new callout syntax ({indent}!!! note "Title")
return f'{indent}{syntax} {c_type}{title}'
return f"{indent}{syntax} {c_type}{title}"

@staticmethod
def _convert_aliases(c_type: str) -> str:
"""Converts aliases to their respective callout type, if its enabled"""
for alias, identifier in CalloutParser.alias_tuples:
c_type = re.sub(rf'^{alias}\b', identifier, c_type)
c_type = re.sub(rf"^{alias}\b", identifier, c_type)
return c_type

def _breakless_list_handler(self, line: str) -> str:
Expand All @@ -112,17 +119,17 @@ def _breakless_list_handler(self, line: str) -> str:
This is a workaround for Obsidian's default behavior, which allows for lists to be created
without a blank line between them.
"""
is_list = re.search(r'^\s*(?:[-+*]|\d+\.)\s', line)
is_list = re.search(r"^\s*(?:[-+*]|\d+\.)\s", line)
if is_list and self.text_in_prev_line:
# If the previous line was a list, keep the line as is
if self.list_in_prev_line:
return line
# If the previous line was text, add a newline before the list
indent = re.search(r'^\t*', line).group()
line = f'{indent}\n{line}'
indent = re.search(r"^\t*", line).group()
line = f"{indent}\n{line}"
else:
# Set text_in_prev_line according to the current line
self.text_in_prev_line = line.strip() != ''
self.text_in_prev_line = line.strip() != ""
self.list_in_prev_line = is_list
return line

Expand All @@ -131,7 +138,7 @@ def _convert_block(self, line: str) -> str:
match = re.search(CALLOUT_BLOCK_REGEX, line)
if match:
# Store the current indent level and add it to the list if it doesn't exist
indent_level = match.group(2).count('>')
indent_level = match.group(2).count(">")

# If the indent level is not in the indent levels, add it to the list
if indent_level not in self.indent_levels:
Expand All @@ -156,13 +163,13 @@ def _convert_content(self, line: str) -> str:
match = re.search(CALLOUT_CONTENT_SYNTAX_REGEX, line)
if match and self.indent_levels:
# Group 1: Leading whitespace (we need to reuse tabs and 4x spaces)
whitespace = match.group(1).replace(' ', '\t')
whitespace = '\t' * whitespace.count('\t') # Ignore everything but tabs
whitespace = match.group(1).replace(" ", "\t")
whitespace = "\t" * whitespace.count("\t") # Ignore everything but tabs

# Remove any higher level indents compared to the current indent level
# i.e. if we are at 1, and indent_levels is [1, 2, 3], remove 2 and 3
try:
while match.group(2).count('>') < self.indent_levels[-1]:
while match.group(2).count(">") < self.indent_levels[-1]:
self.indent_levels = self.indent_levels[:-1]
except IndexError:
# If the indent levels list is empty, reset the states and return the line
Expand All @@ -171,9 +178,11 @@ def _convert_content(self, line: str) -> str:
return line

# Construct the new indent level
indent = f'{whitespace}{self._get_indent(indent_level=max(self.indent_levels))}'
indent = (
f"{whitespace}{self._get_indent(indent_level=max(self.indent_levels))}"
)

line = re.sub(rf'^\s*(?:> ?){{{self.indent_levels[-1]}}} ?', indent, line)
line = re.sub(rf"^\s*(?:> ?){{{self.indent_levels[-1]}}}", indent, line)

# Handle breakless lists before returning the line, if enabled
if self.breakless_lists:
Expand Down Expand Up @@ -207,8 +216,8 @@ def convert_line(self, line: str) -> str:
"""
# Toggle codefence indices if we encounter a codefence
# (If a line starts with '```' before any meaningful content, it's a codefence)
if re.match(r'^\s*(?:>\s*)*```', line):
self._toggle_codefence_at_index(line.index('```'))
if re.match(r"^\s*(?:>\s*)*```", line):
self._toggle_codefence_at_index(line.index("```"))
# Codefences get treated like content (because they could contain callouts inside them that should not convert)
if self.in_codefence and self.indent_levels:
return self._convert_content(line)
Expand All @@ -220,7 +229,7 @@ def parse(self, markdown: str) -> str:
"""Takes a markdown file input returns a version with converted callout syntax"""
self.indent_levels = list() # Reset (redundant in conjunction with mkdocs)
# If markdown file does not contain a callout, skip it
if not re.search(r'> *\[!', markdown):
if not re.search(r"> *\[!", markdown):
return markdown
# Convert markdown line by line, then return it
return '\n'.join(self.convert_line(line) for line in markdown.split('\n'))
return "\n".join(self.convert_line(line) for line in markdown.split("\n"))

0 comments on commit 1e3809a

Please sign in to comment.