Source code for klea_utils.ui.linkify
#!/usr/bin/env python3
"""
Bare-URL linkification for rendered text
Two variants: :func:`linkify_md` wraps bare URLs as Markdown links for a
Markdown renderer, and :func:`linkify_html` escapes the text and wraps bare
URLs in safe anchors for plain (non-Markdown) rendering.
File: klea_utils/ui/linkify.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import html
import logging
import re
from linkify_it import LinkifyIt
logger = logging.getLogger(__name__)
#: Existing ``[text](url)`` markdown links are left untouched; only bare
#: URLs between them are linkified. ``[^)]*`` is greedy up to the first
#: ``)``, which also tolerates balanced parentheses inside the URL.
_MARKDOWN_LINK_RE = re.compile(r"\[[^\]]*\]\([^)]*\)")
#: A fenced-code delimiter line (`` ` `` or ``~``), optionally indented up to
#: three spaces per CommonMark. Content between an opening and matching
#: closing fence is code and must not be linkified.
_FENCE_RE = re.compile(r"^( {0,3})(`{3,}|~{3,})", re.MULTILINE)
#: URL detection engine. Defaults enable "fuzzy" matching of bare domains
#: and emails in addition to explicit http(s):// URLs.
_LINKIFY = LinkifyIt()
[docs]
def linkify_md(text: str) -> str:
"""Convert bare URLs in markdown *text* to ``[url](url)`` links.
Existing ``[text](url)`` links are returned verbatim so they are not
re-wrapped, and fenced code blocks (triple-backtick or tilde fences) are
left untouched: a URL in code would otherwise be wrapped and the literal
``[url](url)`` shown inside the code span (fenced blocks are not parsed
as markdown links). Callers pass the result to a markdown renderer,
which turns the wrapped URLs into clickable anchors.
:param text: Markdown source text
:returns: Text with bare URLs wrapped as ``[url](url)``
"""
parts: list[str] = []
last = 0
fence: str | None = None
for match in _FENCE_RE.finditer(text):
# Text before this delimiter is code when a fence is open, otherwise
# prose that may be linkified.
segment = text[last : match.start()]
parts.append(segment if fence is not None else _linkify_segment(segment))
# The delimiter line itself is always verbatim.
line_end = text.find("\n", match.end())
line_end = len(text) if line_end == -1 else line_end + 1
parts.append(text[match.start() : line_end])
marker = match.group(2)
if fence is None:
fence = marker[0] # ``` or ~~~
elif marker[0] == fence:
fence = None
last = line_end
tail = text[last:]
parts.append(tail if fence is not None else _linkify_segment(tail))
return "".join(parts)
def _linkify_segment(text: str) -> str:
"""Wrap bare URLs in a code-FREE markdown segment."""
if not text:
return text
result: list[str] = []
last = 0
for match in _MARKDOWN_LINK_RE.finditer(text):
result.append(_linkify_plain(text[last : match.start()]))
result.append(match.group(0))
last = match.end()
result.append(_linkify_plain(text[last:]))
return "".join(result)
def _linkify_plain(text: str) -> str:
"""Wrap bare URLs in *text* (which contains no markdown links)."""
if not text:
return text
matches = _LINKIFY.match(text)
if not matches:
return text
result: list[str] = []
last = 0
for match in matches:
result.append(text[last : match.index])
result.append(f"[{match.text}]({match.url})")
last = match.last_index
result.append(text[last:])
return "".join(result)
[docs]
def linkify_html(text: str) -> str:
"""HTML-escape *text* and wrap bare URLs in safe ``<a>`` anchors.
For plain-text (non-markdown) rendering, e.g. user input: the text is
escaped so it is shown verbatim (no Markdown decoration), while bare URLs
become clickable links. The result is safe to pass to
``ui.html(sanitize=True)``.
:param text: The literal text to render.
:returns: HTML with the text escaped and bare URLs wrapped in anchors.
"""
matches = _LINKIFY.match(text)
if not matches:
return html.escape(text)
parts: list[str] = []
last = 0
for match in matches:
parts.append(html.escape(text[last : match.index]))
parts.append(
f'<a href="{html.escape(match.url, quote=True)}" '
f'target="_blank" rel="noopener noreferrer">'
f"{html.escape(match.text)}</a>"
)
last = match.last_index
parts.append(html.escape(text[last:]))
return "".join(parts)