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