File: update_theme_docs.py

package info (click to toggle)
accessible-pygments 0.0.5-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,076 kB
  • sloc: python: 1,451; sh: 48; javascript: 33; makefile: 3
file content (233 lines) | stat: -rw-r--r-- 7,862 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
172
173
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
206
207
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
# This script generates READMEs for themes from their style.py definitions

import logging
import string
import sys

from argparse import ArgumentParser
from functools import lru_cache
from importlib import import_module
from inspect import getdoc
from pathlib import Path
from typing import Set, Tuple, Type

from playwright.sync_api import sync_playwright
from pygments.styles import get_style_by_name

from a11y_pygments.utils.utils import find_all_themes_packages
from a11y_pygments.utils.wcag_contrast import (
    contrast_ratio,
    get_wcag_level_large_text,
    get_wcag_level_normal_text,
    hex_to_rgb01,
    hexstr_without_hash,
)


HERE = Path(__file__).parent
REPO = HERE.parent

# TODO fix this hack later when restructuring the repo
sys.path.append(str(REPO / "test"))
from render_html import outdir as html_outdir  # noqa: E402
from render_html import render_html  # noqa: E402


def markdown_table(rows: list[list[str]]) -> str:
    """Pretty format a table as Markdown

    Formatting features: header, separator, body and equal-spaced columns.

    >>> print(markdown_table([["a","b"],["foo","bar"]]))
    | a   | b   |
    | --- | --- |
    | foo | bar |
    """

    # Calculate the maximum width of each column
    column_widths = [max(len(cell) for cell in column) for column in zip(*rows)]

    # Create lines of the Markdown table from each data row
    lines = []
    for row in rows:
        lines.append(
            "| "
            # Pad each cell to the column width
            + " | ".join(cell.ljust(width) for cell, width in zip(row, column_widths))
            + " |"
        )

    # Create the separator row line, matching each cell to the column width
    separator = "| " + " | ".join("-" * width for width in column_widths) + " |"

    # Insert the separator row between the header and body rows
    lines.insert(1, separator)

    # Join together the lines as a single string with newline characters between them
    return "\n".join(lines)


def hexstr_to_rgb(hex_string: str) -> Tuple[int, int, int]:
    hex_string = hex_string.lstrip("#")
    r = int(hex_string[0:2], 16)
    g = int(hex_string[2:4], 16)
    b = int(hex_string[4:6], 16)
    return (r, g, b)


@lru_cache()  # just to not create the same png twice in the same run.
def make_square_png(hex_color: str, path_tpl):
    assert hex_color.startswith("#")
    from PIL import Image

    width, height = 20, 20

    rgb_color = hexstr_to_rgb(hex_color)

    image = Image.new("RGB", (width, height), rgb_color)
    filename = path_tpl.format(rrggbb=hex_color[1:])
    if Path(filename).exists():
        print("Updating", filename)
    else:
        print("Creating", filename)

    image.save(filename)


def contrast_markdown_table(
    color_cls: Type,
    background_color: str,
    img_tpl="../../a11y_pygments/assets/{rrggbb}.png",
) -> Tuple[str, Set[str]]:
    """Create Markdown table of contrast ratios and WCAG ratings for foreground colors against background color

    Args:
        color_cls (class object): A mapping of color names to six-value hex CSS color
            strings. The mapping comes from a class named Colors defined in
            <theme-name>/style.py.
        background_color (str): Theme default background color for code blocks
            in six-value #RRGGBB hex format. For example, white would be "#ffffff"
            (case insensitive).
    """

    # Start table
    rows = [["Color", "Hex", "Ratio", "Normal text", "Large text"]]

    # Keep track of colors already seen so we do not duplicate rows in the table
    unique_colors = set()

    # Iterate through Color class properties
    for key in vars(color_cls):
        value = getattr(color_cls, key)
        if (
            not callable(value)
            and not key.startswith("__")
            and value not in unique_colors
        ):
            # Keep track of color values we have already seen
            unique_colors.add(value)

            # Calculate contrast
            contrast = contrast_ratio(
                hex_to_rgb01(value), hex_to_rgb01(background_color)
            )
            rrggbb = hexstr_without_hash(value).lower()
            img_url = img_tpl.format(rrggbb=rrggbb)

            # Add row to table
            rows.append(
                [
                    # Color
                    f"![#{rrggbb}]({img_url})",
                    # Hex
                    f"`#{rrggbb}`",
                    # Ratio
                    f"{round(contrast, 1)} : 1",
                    # Normal text
                    get_wcag_level_normal_text(contrast),
                    # Large text
                    get_wcag_level_large_text(contrast),
                ]
            )

    # Format table as Markdown
    return markdown_table(rows), unique_colors


def update_readme(theme: str):
    """Given a theme module name, update that theme's README file on disk"""

    theme_kebab_case = theme.replace("_", "-")
    outdir = REPO / "a11y_pygments" / theme

    # Take a screenshot of the theme applied to a sample bash script
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page(viewport={"width": 620, "height": 720})
        bash_sample_rendered_html = html_outdir / theme_kebab_case / "bash.html"
        page.goto(bash_sample_rendered_html.absolute().as_uri())
        page.screenshot(path=outdir / "images" / f"{theme_kebab_case}.png")
        browser.close()

    # Get the theme colors
    theme_style_module = import_module(f"a11y_pygments.{theme}.style")
    color_cls = theme_style_module.Colors  # access to foreground colors
    theme_cls = theme_style_module.Theme  # access to docstring
    style = get_style_by_name(
        theme_kebab_case
    )  # access to background and highlight colors

    # Render the README template with the contrast info
    with open(HERE / "templates" / "theme_readme.md", "r") as file:
        template_str = file.read()
    template = string.Template(template_str)
    img_tpl = "a11y_pygments/assets/{rrggbb}.png"
    table, colors = contrast_markdown_table(
        color_cls, style.background_color, img_tpl="../../" + img_tpl
    )
    for c in colors:
        rrggbb = hexstr_without_hash(c).lower()
        make_square_png("#" + rrggbb, path_tpl=img_tpl)

    result = template.substitute(
        theme=theme_kebab_case,
        theme_title=theme.replace("_", " ").title(),
        theme_docstring=getdoc(theme_cls),
        background_hex=hexstr_without_hash(style.background_color),
        highlight_hex=hexstr_without_hash(style.highlight_color),
        contrast_table=table,
    )

    # Save the new README file
    out = REPO / "a11y_pygments" / theme / "README.md"
    with open(out, "w") as f:
        logging.info("Updating %s", out)
        f.write(result)


# You can either update one theme README at a time or all of them at once
if __name__ == "__main__":
    parser = ArgumentParser(
        prog="Update theme readme",
        description="Updates theme README.md files in the accessible-pygments repo",
    )
    parser.add_argument(
        "themes", nargs="*", help="Themes to update (example: a11y_dark)"
    )
    args = parser.parse_args()

    # Check that each theme provided on the command line is a known theme. If
    # not, error and exit immediately
    all_themes = find_all_themes_packages()
    for i, theme in enumerate(args.themes):
        if "-" in theme:
            logging.info("Converting to snake_case: %s", theme)
            args.themes[i] = theme = theme.replace("-", "_")
        assert theme in all_themes, f"Theme {theme} not found"

    # Update themes provided or, if none provided, update all themes in the repo
    themes = args.themes or all_themes
    render_html(theme.replace("_", "-") for theme in themes)

    for theme in themes:
        update_readme(theme)