docs for muutils v0.8.8
View Source on GitHub

muutils.web.inline_html

Inline local CSS/JS files into an HTML document


  1"Inline local CSS/JS files into an HTML document"
  2
  3from __future__ import annotations
  4
  5from typing import Literal
  6from pathlib import Path
  7import warnings
  8
  9AssetType = Literal["script", "style"]
 10
 11
 12def inline_html_assets(
 13    html: str,
 14    assets: list[tuple[AssetType, Path]],
 15    base_path: Path,
 16    include_filename_comments: bool = True,
 17    prettify: bool = False,
 18) -> str:
 19    """Inline specified local CSS/JS files into the text of an HTML document.
 20
 21    Each entry in `assets` should be a tuple like `("script", "app.js")` or `("style", "style.css")`.
 22
 23    # Parameters:
 24    - `html : str`
 25            input HTML content.
 26    - `assets : list[tuple[AssetType, Path]]`
 27            List of (tag_type, filename) tuples to inline.
 28
 29    # Returns:
 30    `str` : Modified HTML content with inlined assets.
 31    """
 32    for tag_type, filename in assets:
 33        fname_str: str = filename.as_posix()
 34        if tag_type not in AssetType.__args__:  # type: ignore[attr-defined]
 35            err_msg: str = f"Unsupported tag type: {tag_type}"
 36            raise ValueError(err_msg)
 37
 38        # Dynamically create the pattern for the given tag and filename
 39        pattern: str
 40        if tag_type == "script":
 41            pattern = rf'<script src="{fname_str}"></script>'
 42        elif tag_type == "style":
 43            pattern = rf'<link rel="stylesheet" href="{fname_str}">'
 44        # assert it's in the text exactly once
 45        assert (
 46            html.count(pattern) == 1
 47        ), f"Pattern {pattern} should be in the html exactly once, found {html.count(pattern) = }"
 48        # figure out the indentation level of the pattern  in the html
 49        indentation: str = html.split(pattern)[0].splitlines()[-1]
 50        assert (
 51            indentation.strip() == ""
 52        ), f"Pattern '{pattern}' should be alone in its line, found {indentation = }"
 53        # read the content and create the replacement
 54        content: str = (base_path / filename).read_text()
 55        replacement: str = f"<{tag_type}>\n{content}\n</{tag_type}>"
 56        if include_filename_comments:
 57            replacement = f"<!-- begin '{fname_str}' -->\n{replacement}\n<!-- end '{fname_str}' -->"
 58        # indent the replacement
 59        replacement = "\n".join(
 60            [f"{indentation}\t{line}" for line in replacement.splitlines()]
 61        )
 62        # perform the replacement
 63        html = html.replace(pattern, replacement)
 64
 65    if prettify:
 66        try:
 67            from bs4 import BeautifulSoup
 68
 69            soup: BeautifulSoup = BeautifulSoup(html, "html.parser")
 70            # TYPING: .prettify() might return a str or bytes, but we want str?
 71            html = str(soup.prettify())
 72            print(BeautifulSoup)
 73        except ImportError:
 74            warnings.warn(
 75                "BeautifulSoup is not installed, skipping prettification of HTML."
 76            )
 77
 78    return html
 79
 80
 81def inline_html_file(
 82    html_path: Path,
 83    output_path: Path,
 84    include_filename_comments: bool = True,
 85    prettify: bool = False,
 86) -> None:
 87    "given a path to an HTML file, inline the local CSS/JS files into it and save it to output_path"
 88    base_path: Path = html_path.parent
 89    # read the HTML file
 90    html: str = html_path.read_text()
 91    # read the assets
 92    assets: list[tuple[AssetType, Path]] = []
 93    for asset in base_path.glob("*.js"):
 94        assets.append(("script", Path(asset.name)))
 95    for asset in base_path.glob("*.css"):
 96        assets.append(("style", Path(asset.name)))
 97    # inline the assets
 98    html_new: str = inline_html_assets(
 99        html,
100        assets,
101        base_path,
102        include_filename_comments=include_filename_comments,
103        prettify=prettify,
104    )
105    # write the new HTML file
106    output_path.write_text(html_new)
107
108
109if __name__ == "__main__":
110    import argparse
111
112    parser: argparse.ArgumentParser = argparse.ArgumentParser(
113        description="Inline local CSS/JS files into an HTML document."
114    )
115    parser.add_argument(
116        "-i",
117        "--input-path",
118        type=Path,
119        help="Path to the HTML file to process.",
120    )
121    parser.add_argument(
122        "-o",
123        "--output-path",
124        type=str,
125        help="Path to save the modified HTML file.",
126    )
127
128    parser.add_argument(
129        "-c",
130        "--no-filename-comments",
131        action="store_true",
132        help="don't include comments with the filename in the inlined assets",
133    )
134
135    parser.add_argument(
136        "-p",
137        "--no-prettify",
138        action="store_true",
139        help="don't prettify the HTML file",
140    )
141
142    args: argparse.Namespace = parser.parse_args()
143
144    inline_html_file(
145        html_path=Path(args.input_path),
146        output_path=Path(args.output_path),
147        include_filename_comments=not args.no_filename_comments,
148        prettify=not args.no_prettify,
149    )

AssetType = typing.Literal['script', 'style']
def inline_html_assets( html: str, assets: list[tuple[typing.Literal['script', 'style'], pathlib.Path]], base_path: pathlib.Path, include_filename_comments: bool = True, prettify: bool = False) -> str:
13def inline_html_assets(
14    html: str,
15    assets: list[tuple[AssetType, Path]],
16    base_path: Path,
17    include_filename_comments: bool = True,
18    prettify: bool = False,
19) -> str:
20    """Inline specified local CSS/JS files into the text of an HTML document.
21
22    Each entry in `assets` should be a tuple like `("script", "app.js")` or `("style", "style.css")`.
23
24    # Parameters:
25    - `html : str`
26            input HTML content.
27    - `assets : list[tuple[AssetType, Path]]`
28            List of (tag_type, filename) tuples to inline.
29
30    # Returns:
31    `str` : Modified HTML content with inlined assets.
32    """
33    for tag_type, filename in assets:
34        fname_str: str = filename.as_posix()
35        if tag_type not in AssetType.__args__:  # type: ignore[attr-defined]
36            err_msg: str = f"Unsupported tag type: {tag_type}"
37            raise ValueError(err_msg)
38
39        # Dynamically create the pattern for the given tag and filename
40        pattern: str
41        if tag_type == "script":
42            pattern = rf'<script src="{fname_str}"></script>'
43        elif tag_type == "style":
44            pattern = rf'<link rel="stylesheet" href="{fname_str}">'
45        # assert it's in the text exactly once
46        assert (
47            html.count(pattern) == 1
48        ), f"Pattern {pattern} should be in the html exactly once, found {html.count(pattern) = }"
49        # figure out the indentation level of the pattern  in the html
50        indentation: str = html.split(pattern)[0].splitlines()[-1]
51        assert (
52            indentation.strip() == ""
53        ), f"Pattern '{pattern}' should be alone in its line, found {indentation = }"
54        # read the content and create the replacement
55        content: str = (base_path / filename).read_text()
56        replacement: str = f"<{tag_type}>\n{content}\n</{tag_type}>"
57        if include_filename_comments:
58            replacement = f"<!-- begin '{fname_str}' -->\n{replacement}\n<!-- end '{fname_str}' -->"
59        # indent the replacement
60        replacement = "\n".join(
61            [f"{indentation}\t{line}" for line in replacement.splitlines()]
62        )
63        # perform the replacement
64        html = html.replace(pattern, replacement)
65
66    if prettify:
67        try:
68            from bs4 import BeautifulSoup
69
70            soup: BeautifulSoup = BeautifulSoup(html, "html.parser")
71            # TYPING: .prettify() might return a str or bytes, but we want str?
72            html = str(soup.prettify())
73            print(BeautifulSoup)
74        except ImportError:
75            warnings.warn(
76                "BeautifulSoup is not installed, skipping prettification of HTML."
77            )
78
79    return html

Inline specified local CSS/JS files into the text of an HTML document.

Each entry in assets should be a tuple like ("script", "app.js") or ("style", "style.css").

Parameters:

  • html : str input HTML content.
  • assets : list[tuple[AssetType, Path]] List of (tag_type, filename) tuples to inline.

Returns:

str : Modified HTML content with inlined assets.

def inline_html_file( html_path: pathlib.Path, output_path: pathlib.Path, include_filename_comments: bool = True, prettify: bool = False) -> None:
 82def inline_html_file(
 83    html_path: Path,
 84    output_path: Path,
 85    include_filename_comments: bool = True,
 86    prettify: bool = False,
 87) -> None:
 88    "given a path to an HTML file, inline the local CSS/JS files into it and save it to output_path"
 89    base_path: Path = html_path.parent
 90    # read the HTML file
 91    html: str = html_path.read_text()
 92    # read the assets
 93    assets: list[tuple[AssetType, Path]] = []
 94    for asset in base_path.glob("*.js"):
 95        assets.append(("script", Path(asset.name)))
 96    for asset in base_path.glob("*.css"):
 97        assets.append(("style", Path(asset.name)))
 98    # inline the assets
 99    html_new: str = inline_html_assets(
100        html,
101        assets,
102        base_path,
103        include_filename_comments=include_filename_comments,
104        prettify=prettify,
105    )
106    # write the new HTML file
107    output_path.write_text(html_new)

given a path to an HTML file, inline the local CSS/JS files into it and save it to output_path