Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"), README, CLI strings, and branch (coven → hack-house). Gitea repo renamed cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest + golden-vector test still green, binary is now `hack-house`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
147 lines
4.7 KiB
Python
147 lines
4.7 KiB
Python
# fences (``` lang, ~~~ lang)
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
import logging
|
|
|
|
from .state_block import StateBlock
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
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
|
|
|
|
def _fence_rule(
|
|
state: StateBlock, startLine: int, endLine: int, silent: bool
|
|
) -> bool:
|
|
LOGGER.debug(
|
|
"entering fence: %s, %s, %s, %s", state, startLine, endLine, silent
|
|
)
|
|
|
|
haveEndMarker = False
|
|
pos = state.bMarks[startLine] + state.tShift[startLine]
|
|
maximum = state.eMarks[startLine]
|
|
|
|
if state.is_code_block(startLine):
|
|
return False
|
|
|
|
if pos + min_markers > maximum:
|
|
return False
|
|
|
|
marker = state.src[pos]
|
|
|
|
if marker not in markers:
|
|
return False
|
|
|
|
# scan marker length
|
|
mem = pos
|
|
pos = state.skipCharsStr(pos, marker)
|
|
|
|
length = pos - mem
|
|
|
|
if length < min_markers:
|
|
return False
|
|
|
|
markup = state.src[mem:pos]
|
|
params = state.src[pos:maximum]
|
|
|
|
if marker in disallow_marker_in_info and marker in params:
|
|
return False
|
|
|
|
# Since start is found, we can report success here in validation mode
|
|
if silent:
|
|
return True
|
|
|
|
# search end of block
|
|
nextLine = startLine
|
|
|
|
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
|
|
|
|
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:
|
|
continue
|
|
except IndexError:
|
|
break
|
|
|
|
if state.is_code_block(nextLine):
|
|
continue
|
|
|
|
pos = state.skipCharsStr(pos, marker)
|
|
|
|
if not closing_matcher(length, pos - mem):
|
|
continue
|
|
|
|
# make sure tail has spaces only
|
|
pos = state.skipSpaces(pos)
|
|
|
|
if pos < maximum:
|
|
continue
|
|
|
|
haveEndMarker = True
|
|
# found!
|
|
break
|
|
|
|
# If a fence has heading spaces, they should be removed from its inner block
|
|
length = state.sCount[startLine]
|
|
|
|
state.line = nextLine + (1 if haveEndMarker else 0)
|
|
|
|
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]
|
|
|
|
return True
|
|
|
|
return _fence_rule
|
|
|
|
|
|
#: The default fence rule (backtick and tilde markers, CommonMark compliant).
|
|
fence = make_fence_rule()
|