Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion markdown_it/rules_block/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
"html_block",
"lheading",
"list_block",
"make_fence_rule",
"paragraph",
"reference",
"table",
)

from .blockquote import blockquote
from .code import code
from .fence import fence
from .fence import fence, make_fence_rule
from .heading import heading
from .hr import hr
from .html_block import html_block
Expand Down
177 changes: 111 additions & 66 deletions markdown_it/rules_block/fence.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,146 @@
# fences (``` lang, ~~~ lang)
from __future__ import annotations

from collections.abc import Callable
import logging

from .state_block import StateBlock

LOGGER = logging.getLogger(__name__)


def fence(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
LOGGER.debug("entering fence: %s, %s, %s, %s", state, startLine, endLine, silent)
def make_fence_rule(
*,
markers: tuple[str, ...] = ("~", "`"),
token_type: str = "fence",
exact_match: bool = False,
disallow_marker_in_info: tuple[str, ...] = ("`",),
min_markers: int = 3,
) -> Callable[[StateBlock, int, int, bool], bool]:
"""Create a fence parsing rule with configurable options.

:param markers: Tuple of single characters that can be used as fence markers.
:param token_type: The token type name to emit (e.g. "fence", "colon_fence").
:param exact_match: If True, the closing fence must have exactly the same
number of marker characters as the opening fence (not "at least as many").
This enables nesting of fences with different marker counts.
:param disallow_marker_in_info: Tuple of marker characters that are not allowed
to appear in the info string. The check only applies when the actual opening
marker is in this tuple (e.g. a tilde fence is unaffected by ``"`"`` being
listed). Per CommonMark, backtick fences cannot have backticks in the info
string. Use ``()`` to disable this restriction.
:param min_markers: Minimum number of marker characters to form a fence.
:return: A block rule function with signature
``(state, startLine, endLine, silent) -> bool``.
"""

closing_matcher: Callable[[int, int], bool]
if exact_match:
# closing code fence must have exactly the same number of markers as the opening one
closing_matcher = lambda opening_len, closing_len: closing_len == opening_len # noqa: E731
else:
# closing code fence must be at least as long as the opening one
closing_matcher = lambda opening_len, closing_len: closing_len >= opening_len # noqa: E731

haveEndMarker = False
pos = state.bMarks[startLine] + state.tShift[startLine]
maximum = state.eMarks[startLine]
def _fence_rule(
state: StateBlock, startLine: int, endLine: int, silent: bool
) -> bool:
LOGGER.debug(
"entering fence: %s, %s, %s, %s", state, startLine, endLine, silent
)

if state.is_code_block(startLine):
return False
haveEndMarker = False
pos = state.bMarks[startLine] + state.tShift[startLine]
maximum = state.eMarks[startLine]

if pos + 3 > maximum:
return False
if state.is_code_block(startLine):
return False

marker = state.src[pos]
if pos + min_markers > maximum:
return False

if marker not in ("~", "`"):
return False
marker = state.src[pos]

# scan marker length
mem = pos
pos = state.skipCharsStr(pos, marker)
if marker not in markers:
return False

length = pos - mem
# scan marker length
mem = pos
pos = state.skipCharsStr(pos, marker)

if length < 3:
return False
length = pos - mem

markup = state.src[mem:pos]
params = state.src[pos:maximum]
if length < min_markers:
return False

if marker == "`" and marker in params:
return False
markup = state.src[mem:pos]
params = state.src[pos:maximum]

# Since start is found, we can report success here in validation mode
if silent:
return True
if marker in disallow_marker_in_info and marker in params:
return False

# search end of block
nextLine = startLine
# Since start is found, we can report success here in validation mode
if silent:
return True

while True:
nextLine += 1
if nextLine >= endLine:
# unclosed block should be autoclosed by end of document.
# also block seems to be autoclosed by end of parent
break
# search end of block
nextLine = startLine

pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
maximum = state.eMarks[nextLine]
while True:
nextLine += 1
if nextLine >= endLine:
# unclosed block should be autoclosed by end of document.
# also block seems to be autoclosed by end of parent
break

if pos < maximum and state.sCount[nextLine] < state.blkIndent:
# non-empty line with negative indent should stop the list:
# - ```
# test
break
pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
maximum = state.eMarks[nextLine]

if pos < maximum and state.sCount[nextLine] < state.blkIndent:
# non-empty line with negative indent should stop the list:
# - ```
# test
break

try:
if state.src[pos] != marker:
try:
if state.src[pos] != marker:
continue
except IndexError:
break

if state.is_code_block(nextLine):
continue
except IndexError:
break

if state.is_code_block(nextLine):
continue
pos = state.skipCharsStr(pos, marker)

pos = state.skipCharsStr(pos, marker)
if not closing_matcher(length, pos - mem):
continue

# closing code fence must be at least as long as the opening one
if pos - mem < length:
continue
# make sure tail has spaces only
pos = state.skipSpaces(pos)

if pos < maximum:
continue

haveEndMarker = True
# found!
break

# make sure tail has spaces only
pos = state.skipSpaces(pos)
# If a fence has heading spaces, they should be removed from its inner block
length = state.sCount[startLine]

if pos < maximum:
continue
state.line = nextLine + (1 if haveEndMarker else 0)

haveEndMarker = True
# found!
break
token = state.push(token_type, "code", 0)
token.info = params
token.content = state.getLines(startLine + 1, nextLine, length, True)
token.markup = markup
token.map = [startLine, state.line]

# If a fence has heading spaces, they should be removed from its inner block
length = state.sCount[startLine]
return True

state.line = nextLine + (1 if haveEndMarker else 0)
return _fence_rule

token = state.push("fence", "code", 0)
token.info = params
token.content = state.getLines(startLine + 1, nextLine, length, True)
token.markup = markup
token.map = [startLine, state.line]

return True
#: The default fence rule (backtick and tilde markers, CommonMark compliant).
fence = make_fence_rule()
Loading
Loading