Skip to content

htmlfilters

Class info

🛈 DocStrings

ansi2html

ansi2html(ansi_string: str, styles: dict[int, dict[str, str]] | None = None) -> str

Convert ansi string to colored HTML.

Parameters:

Name Type Description Default
ansi_string str

text with ANSI color codes.

required
styles dict[int, dict[str, str]] | None

A mapping from ANSI codes to a dict with css

None

Returns:

Type Description
str

HTML string

Source code in src/jinjarope/htmlfilters.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
def ansi2html(ansi_string: str, styles: dict[int, dict[str, str]] | None = None) -> str:
    """Convert ansi string to colored HTML.

    Args:
        ansi_string: text with ANSI color codes.
        styles: A mapping from ANSI codes to a dict with css

    Returns:
        HTML string
    """
    styles = styles or ANSI_STYLES
    previous_end = 0
    in_span = False
    ansi_codes: list[int] = []
    ansi_finder = re.compile("\033\\[([\\d;]*)([a-zA-z])")
    parts = []
    for match in ansi_finder.finditer(ansi_string):
        parts.append(ansi_string[previous_end : match.start()])
        previous_end = match.end()
        params, command = match.groups()

        if command not in "mM":
            continue

        try:
            params = [int(p) for p in params.split(";")]
        except ValueError:
            params = [0]

        for i, v in enumerate(params):
            if v == 0:
                params = params[i + 1 :]
                if in_span:
                    in_span = False
                    parts.append("</span>")
                ansi_codes = []
                if not params:
                    continue

        ansi_codes.extend(params)
        if in_span:
            parts.append("</span>")
            in_span = False

        if not ansi_codes:
            continue

        style = [
            "; ".join([f"{k}: {v}" for k, v in styles[k].items()]).strip()
            for k in ansi_codes
            if k in styles
        ]
        parts.append(f'<span style="{"; ".join(style)}">')

        in_span = True

    parts.append(ansi_string[previous_end:])
    if in_span:
        parts.append("</span>")
        in_span = False
    return "".join(parts)

clean_svg

clean_svg(text: str) -> str

Strip off unwanted stuff from svg text which might be added by external libs.

Removes xml headers and doctype declarations.

Parameters:

Name Type Description Default
text str

The text to cleanup / filter

required
Source code in src/jinjarope/htmlfilters.py
132
133
134
135
136
137
138
139
140
141
142
def clean_svg(text: str) -> str:
    """Strip off unwanted stuff from svg text which might be added by external libs.

    Removes xml headers and doctype declarations.

    Args:
        text: The text to cleanup / filter
    """
    text = re.sub(r"<\?xml version.*\?>\s*", "", text, flags=re.DOTALL)
    text = re.sub(r"<!DOCTYPE svg.*?>", "", text, flags=re.DOTALL)
    return text.strip()

format_css_rule

format_css_rule(dct: Mapping) -> str

Format a nested dictionary as CSS rule.

Mapping must be of shape {".a": {"b": "c"}}

Parameters:

Name Type Description Default
dct Mapping

The mapping to convert to CSS text

required
Source code in src/jinjarope/htmlfilters.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def format_css_rule(dct: Mapping) -> str:
    """Format a nested dictionary as CSS rule.

    Mapping must be of shape {".a": {"b": "c"}}

    Args:
        dct: The mapping to convert to CSS text
    """
    data: dict[str, list] = {}

    def _parse(obj, selector: str = ""):
        for key, value in obj.items():
            if hasattr(value, "items"):
                rule = selector + " " + key
                data[rule] = []
                _parse(value, rule)

            else:
                prop = data[selector]
                prop.append(f"\t{key}: {value};\n")

    _parse(dct)
    string = ""
    for key, value in sorted(data.items()):
        if data[key]:
            string += key[1:] + " {\n" + "".join(value) + "}\n\n"
    return string

format_js_map

format_js_map(mapping: dict | str, indent: int = 4) -> str

Return JS map str for given dictionary.

Parameters:

Name Type Description Default
mapping dict | str

Dictionary to dump

required
indent int

The amount of indentation for the key-value pairs

4
Source code in src/jinjarope/htmlfilters.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def format_js_map(mapping: dict | str, indent: int = 4) -> str:
    """Return JS map str for given dictionary.

    Args:
        mapping: Dictionary to dump
        indent: The amount of indentation for the key-value pairs
    """
    dct = json.loads(mapping) if isinstance(mapping, str) else mapping
    rows: list[str] = []
    indent_str = " " * indent
    for k, v in dct.items():
        match v:
            case bool():
                rows.append(f"{indent_str}{k}: {str(v).lower()},")
            case dict():
                rows.append(f"{indent_str}{k}: {format_js_map(v)},")
            case None:
                rows.append(f"{indent_str}{k}: null,")
            case _:
                rows.append(f"{indent_str}{k}: {v!r},")
    row_str = "\n" + "\n".join(rows) + "\n"
    return f"{{{row_str}}}"

format_xml cached

format_xml(
    str_or_elem: str | Element,
    indent: str | int = "  ",
    level: int = 0,
    method: Literal["xml", "html", "text", "c14n"] = "html",
    short_empty_elements: bool = True,
    add_declaration: bool = False,
) -> str

(Pretty)print given XML.

Parameters:

Name Type Description Default
str_or_elem str | Element

XML to prettyprint

required
indent str | int

Amount of spaces to use for indentation

' '
level int

Initial indentation level

0
method Literal['xml', 'html', 'text', 'c14n']

Output method

'html'
short_empty_elements bool

Whether empty elements should get printed in short form (applies when mode is "xml")

True
add_declaration bool

whether a XML declaration should be printed (applies when mode is "xml")

False
Source code in src/jinjarope/htmlfilters.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
@functools.lru_cache
def format_xml(
    str_or_elem: str | ET.Element,
    indent: str | int = "  ",
    level: int = 0,
    method: Literal["xml", "html", "text", "c14n"] = "html",
    short_empty_elements: bool = True,
    add_declaration: bool = False,
) -> str:
    """(Pretty)print given XML.

    Args:
        str_or_elem: XML to prettyprint
        indent: Amount of spaces to use for indentation
        level: Initial indentation level
        method: Output method
        short_empty_elements: Whether empty elements should get printed in short form
                              (applies when mode is "xml")
        add_declaration: whether a XML declaration should be printed
                         (applies when mode is "xml")
    """
    if isinstance(str_or_elem, str):
        str_or_elem = ET.fromstring(str_or_elem)
    space = indent if isinstance(indent, str) else indent * " "
    ET.indent(str_or_elem, space=space, level=level)
    return ET.tostring(
        str_or_elem,
        encoding="unicode",
        method=method,
        xml_declaration=add_declaration,
        short_empty_elements=short_empty_elements,
    )
html_link(text: str | None = None, link: str | None = None, **kwargs: Any) -> str

Create a html link.

If link is empty string or None, just the text will get returned.

Parameters:

Name Type Description Default
text str | None

Text to show for the link

None
link str | None

Target url

None
kwargs Any

additional key-value pairs to be inserted as attributes. Key strings will have "_" stripped from the end to allow using keywords.

{}
Source code in src/jinjarope/htmlfilters.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def html_link(text: str | None = None, link: str | None = None, **kwargs: Any) -> str:
    """Create a html link.

    If link is empty string or None, just the text will get returned.

    Args:
        text: Text to show for the link
        link: Target url
        kwargs: additional key-value pairs to be inserted as attributes.
                Key strings will have "_" stripped from the end to allow using keywords.
    """
    if not link:
        return text or ""
    attrs = [f'{k.rstrip("_")}="{v}"' for k, v in kwargs.items()]
    attr_str = (" " + " ".join(attrs)) if attrs else ""
    return f"<a href={link!r}{attr_str}>{text or link}</a>"

inject_javascript

inject_javascript(
    html_content: ContentType, javascript: str, *, position: Position = "end_body"
) -> ContentType

Injects JavaScript code into an HTML string or bytes object.

Parameters:

Name Type Description Default
html_content ContentType

The HTML content to inject the JavaScript into

required
javascript str

The JavaScript code to inject

required
position Position

The position to inject the JavaScript ('body' by default)

'end_body'

Returns:

Type Description
ContentType

The modified HTML content with the same type as the input

Raises:

Type Description
ValueError

If the specified position tag is not found in the HTML content

TypeError

If the input type is neither str nor bytes

Source code in src/jinjarope/htmlfilters.py
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def inject_javascript(
    html_content: ContentType,
    javascript: str,
    *,
    position: Position = "end_body",
) -> ContentType:
    """Injects JavaScript code into an HTML string or bytes object.

    Args:
        html_content: The HTML content to inject the JavaScript into
        javascript: The JavaScript code to inject
        position: The position to inject the JavaScript ('body' by default)

    Returns:
        The modified HTML content with the same type as the input

    Raises:
        ValueError: If the specified position tag is not found in the HTML content
        TypeError: If the input type is neither str nor bytes
    """
    # Convert bytes to str if needed
    is_bytes = isinstance(html_content, bytes)
    working_content: str = html_content.decode() if is_bytes else html_content  # type: ignore[assignment, attr-defined]

    # Prepare the JavaScript tag
    script_tag = f"<script>{javascript}</script>"

    # Define the injection patterns
    patterns = {
        "body": (r"<body[^>]*>", lambda m: f"{m.group(0)}{script_tag}"),
        "head": (r"<head[^>]*>", lambda m: f"{m.group(0)}{script_tag}"),
        "end_head": (r"</head>", lambda m: f"{script_tag}{m.group(0)}"),
        "end_body": (r"</body>", lambda m: f"{script_tag}{m.group(0)}"),
    }

    if position not in patterns:
        msg = f"Invalid position: {position}. Must be one of {list(patterns.keys())}"
        raise ValueError(msg)

    pattern, replacement = patterns[position]
    modified_content = re.sub(pattern, replacement, working_content, count=1)

    # If no substitution was made, the tag wasn't found
    if modified_content == working_content:
        msg = f"Could not find {position} tag in HTML content"
        raise ValueError(msg)
    # Return the same type as input
    return modified_content.encode() if is_bytes else modified_content  # type: ignore[return-value]

normalize_url cached

normalize_url(path: str, url: str | None = None, base: str = '') -> str

Return a URL relative to the given url or using the base.

Parameters:

Name Type Description Default
path str

The path to normalize

required
url str | None

Optional relative url

None
base str

Base path

''
Source code in src/jinjarope/htmlfilters.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
@functools.lru_cache
def normalize_url(path: str, url: str | None = None, base: str = "") -> str:
    """Return a URL relative to the given url or using the base.

    Args:
        path: The path to normalize
        url: Optional relative url
        base: Base path
    """
    path, relative_level = _get_norm_url(path)
    if relative_level == -1:
        return path
    if url is None:
        return posixpath.join(base, path)
    result = relative_url(url, path)
    if relative_level > 0:
        result = "../" * relative_level + result
    return result

relative_url cached

relative_url(url_a: str, url_b: str) -> str

Compute the relative path from URL A to URL B.

Parameters:

Name Type Description Default
url_a str

URL A.

required
url_b str

URL B.

required

Returns:

Type Description
str

The relative URL to go from A to B.

Source code in src/jinjarope/htmlfilters.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
@functools.lru_cache
def relative_url(url_a: str, url_b: str) -> str:
    """Compute the relative path from URL A to URL B.

    Args:
        url_a: URL A.
        url_b: URL B.

    Returns:
        The relative URL to go from A to B.
    """
    parts_a = url_a.split("/")
    if "#" in url_b:
        url_b, anchor = url_b.split("#", 1)
    else:
        anchor = None
    parts_b = url_b.split("/")

    # remove common left parts
    while parts_a and parts_b and parts_a[0] == parts_b[0]:
        parts_a.pop(0)
        parts_b.pop(0)

    # go up as many times as remaining a parts' depth
    levels = len(parts_a) - 1
    parts_relative = [".."] * levels + parts_b
    relative = "/".join(parts_relative)
    return f"{relative}#{anchor}" if anchor else relative

relative_url_mkdocs

relative_url_mkdocs(url: str, other: str) -> str

Return given url relative to other (MkDocs implementation).

Both are operated as slash-separated paths, similarly to the 'path' part of a URL. The last component of other is skipped if it contains a dot (considered a file). Actual URLs (with schemas etc.) aren't supported. The leading slash is ignored. Paths are normalized ('..' works as parent directory), but going higher than the root has no effect ('foo/../../bar' ends up just as 'bar').

Parameters:

Name Type Description Default
url str

URL A.

required
other str

URL B.

required
Source code in src/jinjarope/htmlfilters.py
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
def relative_url_mkdocs(url: str, other: str) -> str:
    """Return given url relative to other (MkDocs implementation).

    Both are operated as slash-separated paths, similarly to the 'path' part of a URL.
    The last component of `other` is skipped if it contains a dot (considered a file).
    Actual URLs (with schemas etc.) aren't supported. The leading slash is ignored.
    Paths are normalized ('..' works as parent directory), but going higher than the
    root has no effect ('foo/../../bar' ends up just as 'bar').

    Args:
        url: URL A.
        other: URL B.
    """
    # Remove filename from other url if it has one.
    dirname, _, basename = other.rpartition("/")
    if "." in basename:
        other = dirname

    other_parts = _norm_parts(other)
    dest_parts = _norm_parts(url)
    common = 0
    for a, b in zip(other_parts, dest_parts):
        if a != b:
            break
        common += 1

    rel_parts = [".."] * (len(other_parts) - common) + dest_parts[common:]
    relurl = "/".join(rel_parts) or "."
    return relurl + "/" if url.endswith("/") else relurl

split_url cached

split_url(value: str, query: QueryStr | None = None) -> str | dict[str, str]

Split a URL into its parts (and optionally return a specific part).

Parameters:

Name Type Description Default
value str

The URL to split

required
query QueryStr | None

Optional URL part to extract

None
Source code in src/jinjarope/htmlfilters.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
@functools.lru_cache
def split_url(value: str, query: QueryStr | None = None) -> str | dict[str, str]:
    """Split a URL into its parts (and optionally return a specific part).

    Args:
        value: The URL to split
        query: Optional URL part to extract
    """
    from urllib.parse import urlsplit

    def object_to_dict(obj: Any, exclude: list[str] | None = None) -> dict[str, Any]:
        """Converts an object into a dict making the properties into keys.

        Allows excluding certain keys.
        """
        if exclude is None or not isinstance(exclude, list):
            exclude = []
        return {
            key: getattr(obj, key)
            for key in dir(obj)
            if not (key.startswith("_") or key in exclude)
        }

    to_exclude = ["count", "index", "geturl", "encode"]
    results = object_to_dict(urlsplit(value), exclude=to_exclude)

    # If a query is supplied, make sure it's valid then return the results.
    # If no option is supplied, return the entire dictionary.
    if not query:
        return results
    if query not in results:
        msg = "split_url: unknown URL component: %s"
        raise ValueError(msg, query)
    return results[query]

svg_to_data_uri

svg_to_data_uri(svg: str) -> str

Wrap svg as data URL.

Parameters:

Name Type Description Default
svg str

The svg to wrap into a data URL

required
Source code in src/jinjarope/htmlfilters.py
120
121
122
123
124
125
126
127
128
129
def svg_to_data_uri(svg: str) -> str:
    """Wrap svg as data URL.

    Args:
        svg: The svg to wrap into a data URL
    """
    if not isinstance(svg, str):
        msg = "Invalid type: %r"
        raise TypeError(msg, type(svg))
    return f"url('data:image/svg+xml;charset=utf-8,{svg}')"

url_to_b64

url_to_b64(image_url: str) -> str | None

Convert an image URL to a base64-encoded string.

Parameters:

Name Type Description Default
image_url str

The URL of the image to convert.

required

Returns:

Type Description
str | None

The base64-encoded string of the image.

Raises:

Type Description
RequestException

If there's an error downloading the image.

Source code in src/jinjarope/htmlfilters.py
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
def url_to_b64(image_url: str) -> str | None:
    """Convert an image URL to a base64-encoded string.

    Args:
        image_url: The URL of the image to convert.

    Returns:
        The base64-encoded string of the image.

    Raises:
        requests.RequestException: If there's an error downloading the image.
    """
    # Download the image
    response = requests.get(image_url)
    response.raise_for_status()
    image_data = response.content

    # Encode the image to base64
    return base64.b64encode(image_data).decode("utf-8")

wrap_in_elem

wrap_in_elem(
    text: str | None, tag: str, add_linebreaks: bool = False, **kwargs: Any
) -> str

Wrap given text in an HTML/XML tag (with attributes).

If text is empty, just return an empty string.

Parameters:

Name Type Description Default
text str | None

Text to wrap

required
tag str

Tag to wrap text in

required
add_linebreaks bool

Adds a linebreak before and after the text

False
kwargs Any

additional key-value pairs to be inserted as attributes for tag. Key strings will have "_" stripped from the end to allow using keywords.

{}
Source code in src/jinjarope/htmlfilters.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def wrap_in_elem(
    text: str | None,
    tag: str,
    add_linebreaks: bool = False,
    **kwargs: Any,
) -> str:
    """Wrap given text in an HTML/XML tag (with attributes).

    If text is empty, just return an empty string.

    Args:
        text: Text to wrap
        tag: Tag to wrap text in
        add_linebreaks: Adds a linebreak before and after the text
        kwargs: additional key-value pairs to be inserted as attributes for tag.
                Key strings will have "_" stripped from the end to allow using keywords.
    """
    if not text:
        return ""
    attrs = [f'{k.rstrip("_")}="{v}"' for k, v in kwargs.items()]
    attr_str = (" " + " ".join(attrs)) if attrs else ""
    nl = "\n" if add_linebreaks else ""
    return f"<{tag}{attr_str}>{nl}{text}{nl}</{tag}>"