#!/usr/bin/env python3 """Lint Markdown, logic, terminology, and fidelity contracts.""" from __future__ import annotations import argparse import bisect import hashlib import html import re import sys import unicodedata from collections import Counter, defaultdict from dataclasses import dataclass from pathlib import Path from typing import Any, Iterable from urllib.parse import unquote from harness_common import ( DEFAULT_RULES_PATH, InputError, decode_utf8, load_json_text, load_rules, prepare_report_output, publish_report_json, rule_index, schema_version, sha256_file, snapshot_file, utc_now, validate_with_schema, ) from markdown_structure import ( advance_html_block, closing_fence, crosses_paragraph_boundary, fence_container_continues, indented_code_container, indentation_columns, inline_code_spans, inside_any_span, is_thematic_break, mask_closed_fence_candidates, list_continuation_indent, opening_fence, ordered_list_interrupts_paragraph, strip_blockquotes, ) @dataclass(frozen=True) class Heading: level: int text: str line: int start: int end: int slug: str @dataclass(frozen=True) class TextSpan: start: int end: int line: int text: str @dataclass(frozen=True) class FencedBlock: start: int end: int line: int text: str closed: bool kind: str = "fenced_code" @dataclass(frozen=True) class ProtectedOccurrence: value: str context: str @dataclass(frozen=True) class ReferenceDefinition: identifier: str destination: str start: int end: int target_start: int target_end: int @dataclass(frozen=True) class ReferenceLink: start: int end: int metadata_start: int metadata_end: int raw_label: str identifier: str ABSOLUTE_URI_PATTERN = re.compile( r"(?\"'\])}]+", re.IGNORECASE, ) def absolute_uri_matches(text: str) -> list[tuple[int, int, str]]: """Return common reader-visible absolute URIs without trailing prose punctuation.""" matches: list[tuple[int, int, str]] = [] for match in ABSOLUTE_URI_PATTERN.finditer(text): value = match.group(0).rstrip(".,;:") if value: matches.append((match.start(), match.start() + len(value), value)) return matches REQUIRED_LOGIC_TOP = ( "schema_version", "title", "document_kind", "core_claim", "sections", "closure", ) REQUIRED_LOGIC_SECTION = ( "id", "heading", "role", "depends_on", "reader_state_before", "question", "answer_plain", "claim_ids", "new_terms", "transition_to", "reader_state_after", ) REQUIRED_TERM = ( "id", "canonical", "plain_definition", "why_needed", "aliases", "first_section", "first_use", ) REQUIRED_READER = ( "schema_version", "document_kind", "primary_audience", "purpose", "reader_question", "reader_outcome", "prerequisites", "assumed_known", "must_explain", "non_goals", ) def line_starts(text: str) -> list[int]: starts = [0] starts.extend(match.end() for match in re.finditer("\n", text)) return starts def location(starts: list[int], offset: int) -> tuple[int, int]: index = bisect.bisect_right(starts, max(0, offset)) - 1 return index + 1, offset - starts[index] + 1 def is_backslash_escaped(text: str, position: int) -> bool: count = 0 position -= 1 while position >= 0 and text[position] == "\\": count += 1 position -= 1 return count % 2 == 1 def html_tag_spans(text: str) -> list[tuple[int, int]]: """Return real inline/block HTML tag spans, excluding comments/autolinks.""" spans: list[tuple[int, int]] = [] cursor = 0 while cursor < len(text): start = text.find("<", cursor) if start < 0: break name = re.match(r"= len(text) or text[prefix_end] not in " \t\r\n/>": cursor = start + 1 continue quote: str | None = None end = prefix_end while end < len(text): character = text[end] if quote is not None: if character == quote: quote = None elif character in "\"'": quote = character elif character == ">": spans.append((start, end + 1)) end += 1 break end += 1 cursor = max(start + 1, end) return spans def html_tag_name_and_attributes(raw: str) -> tuple[str, dict[str, str]] | None: """Parse the tag name and explicit attribute values from one HTML tag.""" tag = re.match(r"<(?P/)?(?P[A-Za-z][A-Za-z0-9-]*)", raw) if tag is None or tag.group("closing"): return None attributes: dict[str, str] = {} cursor = tag.end() while cursor < len(raw): while cursor < len(raw) and raw[cursor].isspace(): cursor += 1 if cursor >= len(raw) or raw[cursor] in "/>": break name = re.match(r"[A-Za-z_:][A-Za-z0-9_.:-]*", raw[cursor:]) if name is None: cursor += 1 continue attribute_name = name.group(0).casefold() cursor += name.end() while cursor < len(raw) and raw[cursor].isspace(): cursor += 1 value = "" if cursor < len(raw) and raw[cursor] == "=": cursor += 1 while cursor < len(raw) and raw[cursor].isspace(): cursor += 1 if cursor < len(raw) and raw[cursor] in "\"'": quote = raw[cursor] cursor += 1 start = cursor while cursor < len(raw) and raw[cursor] != quote: cursor += 1 value = raw[start:cursor] if cursor < len(raw): cursor += 1 else: start = cursor while cursor < len(raw) and raw[cursor] not in " \t\r\n\"'=<>`": cursor += 1 value = raw[start:cursor] attributes.setdefault(attribute_name, value) return tag.group("name").casefold(), attributes def normalize_reference_identifier(value: str) -> str: value = re.sub(r"\\([!\"#$%&'()*+,./:;<=>?@\[\\\]^_`{|}~-])", r"\1", value) decoded, _ = decode_html_entities_with_offsets(value) return " ".join(decoded.split()).casefold() def reference_definitions(text: str) -> list[ReferenceDefinition]: """Parse CommonMark-style link definitions without exposing their metadata.""" definitions: list[ReferenceDefinition] = [] lines = text.splitlines(keepends=True) offsets: list[int] = [] offset = 0 for line in lines: offsets.append(offset) offset += len(line) title_patterns = ( re.compile(r'^"(?:\\.|[^"\r\n])*"$'), re.compile(r"^'(?:\\.|[^'\r\n])*'$"), re.compile(r"^\((?:\\.|[^()\r\n])*\)$"), ) def container_content(content: str) -> tuple[str, int, int, bool]: rest, quote_depth = strip_blockquotes(content) prefix = len(content) - len(rest) item = re.match( r"^(?P[ \t]*)(?:[-+*]|[0-9]{1,9}[.)])(?P[ \t]+)", rest, ) if item: prefix += item.end() rest = rest[item.end() :] return rest, prefix, quote_depth, item is not None def parse_header(content: str) -> tuple[str, int] | None: leading = len(content) - len(content.lstrip(" ")) if leading > 3: return None cursor = leading if cursor >= len(content) or content[cursor] != "[": return None label_start = cursor + 1 cursor = label_start while cursor < len(content): if content[cursor] == "]" and not is_backslash_escaped(content, cursor): break cursor += 1 if cursor >= len(content) or not content[label_start:cursor]: return None label = content[label_start:cursor] cursor += 1 if cursor >= len(content) or content[cursor] != ":": return None cursor += 1 while cursor < len(content) and content[cursor] in " \t": cursor += 1 return label, cursor def parse_destination_prefix( content: str, cursor: int ) -> tuple[int, int, int] | None: if cursor >= len(content): return None if content[cursor] == "<": target_start = cursor + 1 target_end = target_start while target_end < len(content) and content[target_end] != ">": if content[target_end] in "<>" or ( content[target_end] == "\\" and target_end + 1 >= len(content) ): return None target_end += 2 if content[target_end] == "\\" else 1 if target_end >= len(content) or content[target_end] != ">": return None tail = target_end + 1 else: target_start = cursor depth = 0 while cursor < len(content) and not content[cursor].isspace(): if content[cursor] == "\\" and cursor + 1 < len(content): cursor += 2 continue if content[cursor] == "(": depth += 1 elif content[cursor] == ")": if depth == 0: return None depth -= 1 cursor += 1 if depth: return None target_end = cursor tail = cursor return target_start, target_end, tail def parse_destination( content: str, cursor: int ) -> tuple[int, int, bool] | None: prefix = parse_destination_prefix(content, cursor) if prefix is None: return None target_start, target_end, tail = prefix raw_rest = content[tail:] rest = raw_rest.strip() if rest and (not raw_rest or raw_rest[0] not in " \t\r\n"): return None if rest and not any(pattern.fullmatch(rest) for pattern in title_patterns): return None return target_start, target_end, not rest def parse_line(content: str) -> tuple[str, int, int, bool] | None: header = parse_header(content) if header is None: return None label, cursor = header destination = parse_destination(content, cursor) if destination is None: return None target_start, target_end, title_may_follow = destination return label, target_start, target_end, title_may_follow def consume_multiline_title( start_index: int, start_content: str, opener_position: int, quote_depth: int, ) -> int | None: opener = start_content[opener_position] closer = ")" if opener == "(" else opener line_index = start_index content = start_content cursor = opener_position + 1 while True: while cursor < len(content): if content[cursor] == "\\" and cursor + 1 < len(content): cursor += 2 continue if opener == "(" and content[cursor] == "(": return None if content[cursor] == closer: return line_index if not content[cursor + 1 :].strip() else None cursor += 1 line_index += 1 if line_index >= len(lines): return None continuation_raw = lines[line_index].rstrip("\r\n") ( content, _, continuation_quote_depth, continuation_list_item, ) = container_content(continuation_raw) if ( not content.strip() or continuation_quote_depth != quote_depth or continuation_list_item ): return None cursor = 0 paragraph_open = False active_quote_depth = 0 skip_through = -1 for index, line in enumerate(lines): if index <= skip_through: continue raw_content = line.rstrip("\r\n") content, prefix, quote_depth, list_item = container_content(raw_content) if not raw_content.strip(): paragraph_open = False active_quote_depth = 0 continue list_interrupt = ordered_list_interrupts_paragraph( strip_blockquotes(raw_content)[0] ) if quote_depth != active_quote_depth or ( list_item and (not paragraph_open or list_interrupt) ): paragraph_open = False active_quote_depth = quote_depth parsed = None if paragraph_open else parse_line(content) target_line_index = index target_prefix = prefix definition_end_index = index header = None if paragraph_open else parse_header(content) if parsed is None and header is not None: destination_prefix = parse_destination_prefix(content, header[1]) if destination_prefix is not None: target_start, target_end, tail = destination_prefix raw_rest = content[tail:] title_position = tail + (len(raw_rest) - len(raw_rest.lstrip())) if ( raw_rest and raw_rest[0].isspace() and title_position < len(content) and content[title_position] in "\"'(" ): title_end = consume_multiline_title( index, content, title_position, quote_depth ) if title_end is not None: definition_end_index = title_end parsed = (header[0], target_start, target_end, False) if parsed is None and not paragraph_open: if header is not None and header[1] == len(content) and index + 1 < len(lines): next_raw = lines[index + 1].rstrip("\r\n") ( next_content, next_prefix, next_quote_depth, next_list_item, ) = container_content(next_raw) next_leading = len(next_content) - len(next_content.lstrip(" ")) next_destination_prefix = ( parse_destination_prefix(next_content, next_leading) if 1 <= next_leading <= 3 and next_quote_depth == quote_depth and not next_list_item else None ) if next_destination_prefix is not None: target_start, target_end, tail = next_destination_prefix raw_rest = next_content[tail:] title_position = tail + ( len(raw_rest) - len(raw_rest.lstrip()) ) title_end: int | None = None if not raw_rest.strip(): parsed = (header[0], target_start, target_end, True) elif ( raw_rest[0].isspace() and title_position < len(next_content) and next_content[title_position] in "\"'(" ): title_end = consume_multiline_title( index + 1, next_content, title_position, quote_depth, ) if title_end is not None: parsed = (header[0], target_start, target_end, False) target_line_index = index + 1 target_prefix = next_prefix definition_end_index = ( title_end if title_end is not None else index + 1 ) if parsed is not None: label, target_start, target_end, title_may_follow = parsed end_index = definition_end_index if title_may_follow and end_index + 1 < len(lines): next_raw = lines[end_index + 1].rstrip("\r\n") next_content, _, next_quote_depth, next_list_item = container_content( next_raw ) next_leading = len(next_content) - len(next_content.lstrip(" ")) title_end = None if ( next_quote_depth == quote_depth and not next_list_item and next_leading <= 3 and next_leading < len(next_content) and next_content[next_leading] in "\"'(" ): title_end = consume_multiline_title( end_index + 1, next_content, next_leading, quote_depth, ) if title_end is not None: end_index = title_end absolute_start = offsets[index] absolute_target_start = ( offsets[target_line_index] + target_prefix + target_start ) absolute_target_end = ( offsets[target_line_index] + target_prefix + target_end ) absolute_end = offsets[end_index] + len(lines[end_index]) destination = text[absolute_target_start:absolute_target_end] destination = re.sub(r"\\(.)", r"\1", destination) definitions.append( ReferenceDefinition( normalize_reference_identifier(label), decode_html_entities_with_offsets(destination)[0], absolute_start, absolute_end, absolute_target_start, absolute_target_end, ) ) skip_through = end_index paragraph_open = False continue structural = bool( re.match( r"^[ ]{0,3}(?:#{1,6}(?:[ \t]+|$)|(?:=+|-+)[ \t]*$|" r"(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,}|(?:-[ \t]*){3,}))", content, ) or opening_fence(content + "\n") is not None or advance_html_block(content + "\n", None, paragraph_open=paragraph_open)[0] ) paragraph_open = bool(content.strip()) and not structural raw_html_structure = mask_raw_html_blocks(text) return [ item for item in definitions if not ( item.start < len(text) and not text[item.start].isspace() and raw_html_structure[item.start] == " " ) ] def scan_markdown_visibility( text: str, ) -> tuple[str, str, list[FencedBlock], list[int]]: """Return fence-masked, reader-visible text and structural defects. Markdown comments and fenced blocks are parsed in one pass so a fence-shaped line inside an HTML comment is not treated as code, while literal ``", cursor, line_end) if closing < 0: break comment_end = closing + 3 mask_range(reader_masked, comment_start, comment_end) comment_start = None cursor = comment_end list_context_indent = list_continuation_indent(line, list_context_indent) paragraph_open = bool(content.strip()) and not ( html_line or comment_start is not None or re.match( r"^[ ]{0,3}(?:#{1,6}(?:[ \t]+|$)|(?:=+|-+)[ \t]*$|" r"(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,}|(?:-[ \t]*){3,}))", content, ) ) offset += len(line) if indented_start is not None: blocks.append( FencedBlock( indented_start, len(text), indented_line, text[indented_start:], True, "indented_code", ) ) mask_range(fence_masked, indented_start, len(text)) mask_range(reader_masked, indented_start, len(text)) if open_start is not None: blocks.append( FencedBlock( open_start, len(text), open_line, text[open_start:], False, ) ) mask_range(fence_masked, open_start, len(text)) mask_range(reader_masked, open_start, len(text)) if comment_start is not None: unclosed_comments.append(comment_start) mask_range(reader_masked, comment_start, len(text)) return ( "".join(fence_masked), "".join(reader_masked), blocks, unclosed_comments, ) def mask_fenced_blocks(text: str) -> tuple[str, list[FencedBlock]]: fence_masked, _, blocks, _ = scan_markdown_visibility(text) return fence_masked, blocks def mask_raw_html_blocks(text: str) -> str: """Mask raw HTML block source for Markdown structural parsing only.""" masked = list(text) state: tuple[str, str] | None = None paragraph_open = False offset = 0 for line in text.splitlines(keepends=True): html_line, state = advance_html_block( line, state, paragraph_open=paragraph_open ) if html_line: for index in range(offset, offset + len(line)): if masked[index] not in "\r\n": masked[index] = " " paragraph_open = False else: content = line.rstrip("\r\n") paragraph_open = bool(content.strip()) and not re.match( r"^[ ]{0,3}(?:#{1,6}(?:[ \t]+|$)|(?:=+|-+)[ \t]*$|" r"(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,}|(?:-[ \t]*){3,}))", content, ) offset += len(line) return "".join(masked) def mask_raw_code_html_blocks(text: str) -> str: """Mask raw HTML containers whose contents are code/data, not prose.""" masked = list(text) active_tag: str | None = None offset = 0 opener = re.compile( r"^[ ]{0,3}<(?Ppre|script|style|textarea)(?:[ \t>]|$)", re.IGNORECASE, ) for line in text.splitlines(keepends=True): content = line.rstrip("\r\n") structural, _ = strip_blockquotes(content) list_item = re.match( r"^[ \t]*(?:[-+*]|[0-9]{1,9}[.)])[ \t]+", structural, ) if list_item: structural = structural[list_item.end() :] match = opener.match(structural) if active_tag is None else None if match is not None: active_tag = match.group("tag").casefold() if active_tag is not None: for index in range(offset, offset + len(line)): if masked[index] not in "\r\n": masked[index] = " " if re.search( rf"", structural, re.IGNORECASE, ): active_tag = None offset += len(line) return "".join(masked) def github_slug(value: str) -> str: # GitHub derives an anchor from the rendered heading label, not from link # destinations or HTML attributes that happen to occur in the source. value = rendered_markdown_label(value) value = unicodedata.normalize("NFKC", value).strip().lower() value = re.sub(r"<[^>]+>", "", value) value = value.replace("`", "") value = re.sub(r"[^\w\- ]+", "", value, flags=re.UNICODE) value = re.sub(r"\s+", "-", value).strip("-") return value def markdown_inline_links( text: str, *, include_images: bool = False, ) -> list[tuple[int, int, int, int, str]]: """Return link/open-close and balanced destination spans (images excluded).""" links: list[tuple[int, int, int, int, str]] = [] position = 0 def escaped(index: int) -> bool: count = 0 index -= 1 while index >= 0 and text[index] == "\\": count += 1 index -= 1 return count % 2 == 1 def skip_space(index: int) -> int | None: newlines = 0 while index < len(text) and text[index] in " \t\r\n": if text[index] == "\n": newlines += 1 if newlines > 1: return None index += 1 return index while position < len(text): label_start = text.find("[", position) if label_start < 0: break is_image = ( label_start > 0 and text[label_start - 1] == "!" and not escaped(label_start - 1) ) if escaped(label_start) or (is_image and not include_images): position = label_start + 1 continue depth = 1 cursor = label_start + 1 while cursor < len(text) and depth: if text[cursor] == "\\" and cursor + 1 < len(text): cursor += 2 continue if text[cursor] == "[": depth += 1 elif text[cursor] == "]": depth -= 1 cursor += 1 if depth or cursor >= len(text) or text[cursor] != "(": position = label_start + 1 continue label_end = cursor - 1 raw_label = text[label_start + 1 : label_end] if crosses_paragraph_boundary(text, label_start + 1, label_end): position = label_start + 1 continue cursor = skip_space(cursor + 1) if cursor is None or cursor >= len(text): position = label_start + 1 continue if text[cursor] == "<": destination_start = cursor + 1 destination_end = destination_start while destination_end < len(text) and text[destination_end] not in ">\r\n": if text[destination_end] == "\\" and destination_end + 1 < len(text): destination_end += 2 else: destination_end += 1 if destination_end >= len(text) or text[destination_end] != ">": position = label_start + 1 continue tail = destination_end + 1 else: destination_start = cursor depth = 0 while cursor < len(text): character = text[cursor] if character == "\\" and cursor + 1 < len(text): cursor += 2 continue if character == "(": depth += 1 elif character == ")": if depth == 0: destination_end = cursor links.append( ( label_start, cursor + 1, destination_start, destination_end, raw_label, ) ) position = cursor + 1 break depth -= 1 elif character in " \t\r\n" and depth == 0: destination_end = cursor tail = cursor break cursor += 1 else: position = label_start + 1 continue if links and links[-1][0] == label_start: continue if cursor >= len(text): position = label_start + 1 continue unspaced_tail = tail spaced_tail = skip_space(tail) if spaced_tail is None: position = label_start + 1 continue tail = spaced_tail if tail < len(text) and text[tail] in "\"'(": if tail == unspaced_tail: position = label_start + 1 continue opener = text[tail] quote = ")" if opener == "(" else opener tail += 1 title_start = tail while tail < len(text) and text[tail] != quote: if text[tail] == "\\" and tail + 1 < len(text): tail += 2 continue if opener == "(" and text[tail] == "(": break if re.match(r"\r?\n[ \t]*\r?\n", text[tail:]): break tail += 1 if tail >= len(text) or text[tail] != quote: position = label_start + 1 continue if re.search(r"\r?\n[ \t]*\r?\n", text[title_start:tail]): position = label_start + 1 continue tail += 1 while tail < len(text) and text[tail] in " \t": tail += 1 if tail < len(text) and text[tail] == ")": links.append( ( label_start, tail + 1, destination_start, destination_end, raw_label, ) ) position = tail + 1 else: position = label_start + 1 return links def markdown_reference_links( text: str, definitions: Iterable[ReferenceDefinition] | None = None, *, include_images: bool = False, ) -> list[ReferenceLink]: """Return full, collapsed, and resolvable shortcut reference links.""" parsed_definitions = list(definitions or reference_definitions(text)) known = {item.identifier for item in parsed_definitions} definition_spans = [(item.start, item.end) for item in parsed_definitions] links: list[ReferenceLink] = [] position = 0 while position < len(text): label_start = text.find("[", position) if label_start < 0: break is_image = ( label_start > 0 and text[label_start - 1] == "!" and not is_backslash_escaped(text, label_start - 1) ) if is_backslash_escaped(text, label_start) or ( is_image and not include_images ): position = label_start + 1 continue if any(start <= label_start < end for start, end in definition_spans): position = label_start + 1 continue depth = 1 cursor = label_start + 1 while cursor < len(text) and depth: if text[cursor] == "\\" and cursor + 1 < len(text): cursor += 2 continue if text[cursor] == "[": depth += 1 elif text[cursor] == "]": depth -= 1 cursor += 1 if depth: break label_end = cursor - 1 raw_label = text[label_start + 1 : label_end] if crosses_paragraph_boundary(text, label_start + 1, label_end): position = label_start + 1 continue if cursor < len(text) and text[cursor] == "(": position = cursor + 1 continue if cursor < len(text) and text[cursor] == "[": identifier_start = cursor + 1 identifier_end = identifier_start while identifier_end < len(text): if text[identifier_end] == "]" and not is_backslash_escaped( text, identifier_end ): break if text[identifier_end] in "\r\n": break identifier_end += 1 if identifier_end >= len(text) or text[identifier_end] != "]": position = label_start + 1 continue raw_identifier = text[identifier_start:identifier_end] or raw_label identifier = normalize_reference_identifier(raw_identifier) if identifier not in known: position = identifier_end + 1 continue links.append( ReferenceLink( label_start, identifier_end + 1, cursor, identifier_end + 1, raw_label, identifier, ) ) position = identifier_end + 1 continue identifier = normalize_reference_identifier(raw_label) if identifier in known: links.append( ReferenceLink( label_start, label_end + 1, label_end + 1, label_end + 1, raw_label, identifier, ) ) position = label_end + 1 return links def mask_markdown_metadata(text: str) -> str: """Mask non-rendered destinations/tags while preserving visible labels/code.""" masked = list(text) inline_spans = inline_code_spans(text) raw_html_structure = mask_raw_html_blocks(text) def inside_raw_html_block(position: int) -> bool: return ( 0 <= position < len(text) and not text[position].isspace() and raw_html_structure[position] == " " ) for link_start, link_end, target_start, _, _ in markdown_inline_links( text, include_images=True ): if inside_any_span(link_start, inline_spans) or inside_raw_html_block( link_start ): continue metadata_start = text.rfind("(", link_start, target_start) for index in range(metadata_start, link_end): if masked[index] not in "\r\n": masked[index] = " " definitions = reference_definitions(text) for definition in definitions: if inside_any_span(definition.start, inline_spans) or inside_raw_html_block( definition.start ): continue for index in range(definition.start, definition.end): if masked[index] not in "\r\n": masked[index] = " " for link in markdown_reference_links(text, definitions, include_images=True): if inside_any_span(link.start, inline_spans) or inside_raw_html_block( link.start ): continue for index in range(link.metadata_start, link.metadata_end): if masked[index] not in "\r\n": masked[index] = " " for start, end in html_tag_spans(text): if inside_any_span(start, inline_spans): continue for index in range(start, end): if masked[index] not in "\r\n": masked[index] = " " return "".join(masked) def rendered_search_projection( text: str, *, include_inline_code_text: bool = False, ) -> tuple[str, list[int]]: """Return reader-visible searchable prose plus source-offset mapping.""" projected = mask_markdown_metadata(text) removed: set[int] = set() inline_spans = inline_code_spans(text) def remove_label_delimiters(start: int, close: int, image: bool) -> None: removed.add(start) removed.add(close) if image and start > 0: removed.add(start - 1) for link_start, _, target_start, _, _ in markdown_inline_links( text, include_images=True ): if inside_any_span(link_start, inline_spans): continue close = text.rfind("]", link_start, target_start) if close > link_start: remove_label_delimiters( link_start, close, link_start > 0 and text[link_start - 1] == "!" and not is_backslash_escaped(text, link_start - 1), ) definitions = reference_definitions(text) for link in markdown_reference_links(text, definitions, include_images=True): if inside_any_span(link.start, inline_spans): continue close = text.find("]", link.start + 1, link.end) if close > link.start: remove_label_delimiters( link.start, close, link.start > 0 and text[link.start - 1] == "!" and not is_backslash_escaped(text, link.start - 1), ) for marker in ("***", "___", "**", "__", "~~", "*", "_"): pattern = re.compile( rf"{re.escape(marker)}(?=\S)" rf"(?P(?:(?!\r?\n[ \t]*\r?\n).)+?)" rf"(?<=\S){re.escape(marker)}", re.DOTALL, ) for match in pattern.finditer(text): close_start = match.end() - len(marker) if is_backslash_escaped(text, match.start()) or is_backslash_escaped( text, close_start ): continue if inside_any_span(match.start(), inline_spans) or crosses_paragraph_boundary( text, match.start() + len(marker), close_start ): continue if marker.startswith("_") and ( (match.start() > 0 and text[match.start() - 1].isalnum()) or (match.end() < len(text) and text[match.end()].isalnum()) ): continue removed.update(range(match.start(), match.start() + len(marker))) removed.update(range(close_start, match.end())) if include_inline_code_text: for start, end, _ in inline_spans: opening = re.match(r"`+", text[start:end]) closing = re.search(r"`+$", text[start:end]) if opening is None or closing is None: continue removed.update(range(start, start + opening.end())) removed.update(range(start + closing.start(), end)) stripped_chars: list[str] = [] stripped_offsets: list[int] = [] for index, character in enumerate(projected): if index not in removed: stripped_chars.append(character) stripped_offsets.append(index) decoded, decoded_offsets = decode_html_entities_with_offsets( "".join(stripped_chars) ) return decoded, [stripped_offsets[index] for index in decoded_offsets] def resolve_reference_labels(text: str, known: set[str]) -> str: """Replace resolvable reference syntax with its reader-visible label.""" def full(match: re.Match[str]) -> str: bracket = match.start() + (1 if match.group(0).startswith("!") else 0) if is_backslash_escaped(text, bracket): return match.group(0) label = match.group("label") identifier = match.group("identifier") or label return label if normalize_reference_identifier(identifier) in known else match.group(0) text = re.sub( r"!?\[(?P