resolves #320 read from stdin and write to stdout
This commit is contained in:
parent
92af90518c
commit
76fa38857f
@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
from dataclasses import InitVar, dataclass, field
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
@ -280,7 +280,7 @@ class Cable:
|
||||
self.gauge = g
|
||||
|
||||
if self.gauge_unit is not None:
|
||||
print(
|
||||
sys.stderr.write(
|
||||
f"Warning: Cable {self.name} gauge_unit={self.gauge_unit} is ignored because its gauge contains {u}"
|
||||
)
|
||||
if u.upper() == "AWG":
|
||||
@ -304,7 +304,7 @@ class Cable:
|
||||
)
|
||||
self.length = L
|
||||
if self.length_unit is not None:
|
||||
print(
|
||||
sys.stderr.write(
|
||||
f"Warning: Cable {self.name} length_unit={self.length_unit} is ignored because its length contains {u}"
|
||||
)
|
||||
self.length_unit = u
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import sys
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from itertools import zip_longest
|
||||
from pathlib import Path
|
||||
from typing import Any, List, Union
|
||||
from typing import Any, List, Union, Optional
|
||||
|
||||
from graphviz import Graph
|
||||
|
||||
@ -20,13 +21,12 @@ from wireviz.DataClasses import (
|
||||
Tweak,
|
||||
Side,
|
||||
)
|
||||
from wireviz.svgembed import embed_svg_images_file
|
||||
from wireviz.svgembed import embed_svg_images
|
||||
from wireviz.wv_bom import (
|
||||
HEADER_MPN,
|
||||
HEADER_PN,
|
||||
HEADER_SPN,
|
||||
bom_list,
|
||||
component_table_entry,
|
||||
generate_bom,
|
||||
get_additional_component_table,
|
||||
pn_info_string,
|
||||
@ -543,11 +543,11 @@ class Harness:
|
||||
f'( +)?{attr}=("[^"]*"|[^] ]*)(?(1)| *)', "", entry
|
||||
)
|
||||
if n_subs < 1:
|
||||
print(
|
||||
sys.stderr.write(
|
||||
f"Harness.create_graph() warning: {attr} not found in {keyword}!"
|
||||
)
|
||||
elif n_subs > 1:
|
||||
print(
|
||||
sys.stderr.write(
|
||||
f"Harness.create_graph() warning: {attr} removed {n_subs} times in {keyword}!"
|
||||
)
|
||||
continue
|
||||
@ -562,7 +562,7 @@ class Harness:
|
||||
# If attr not found, then append it
|
||||
entry = re.sub(r"\]$", f" {attr}={value}]", entry)
|
||||
elif n_subs > 1:
|
||||
print(
|
||||
sys.stderr.write(
|
||||
f"Harness.create_graph() warning: {attr} overridden {n_subs} times in {keyword}!"
|
||||
)
|
||||
|
||||
@ -650,55 +650,61 @@ class Harness:
|
||||
graph = self.graph
|
||||
return embed_svg_images(graph.pipe(format="svg").decode("utf-8"), Path.cwd())
|
||||
|
||||
|
||||
def output(
|
||||
self,
|
||||
filename: (str, Path),
|
||||
view: bool = False,
|
||||
cleanup: bool = True,
|
||||
fmt: tuple = ("html", "png", "svg", "tsv"),
|
||||
self,
|
||||
output_dir: Optional[Union[str, Path]],
|
||||
output_name: Optional[Union[str, Path]],
|
||||
formats: List[str] | tuple[str]
|
||||
) -> None:
|
||||
# graphical output
|
||||
graph = self.graph
|
||||
svg_already_exists = Path(
|
||||
f"{filename}.svg"
|
||||
).exists() # if SVG already exists, do not delete later
|
||||
# graphical output
|
||||
for f in fmt:
|
||||
if f in ("png", "svg", "html"):
|
||||
if f == "html": # if HTML format is specified,
|
||||
f = "svg" # generate SVG for embedding into HTML
|
||||
# SVG file will be renamed/deleted later
|
||||
_filename = f"{filename}.tmp" if f == "svg" else filename
|
||||
# TODO: prevent rendering SVG twice when both SVG and HTML are specified
|
||||
graph.format = f
|
||||
graph.render(filename=_filename, view=view, cleanup=cleanup)
|
||||
# embed images into SVG output
|
||||
if "svg" in fmt or "html" in fmt:
|
||||
embed_svg_images_file(f"{filename}.tmp.svg")
|
||||
# GraphViz output
|
||||
if "gv" in fmt:
|
||||
graph.save(filename=f"{filename}.gv")
|
||||
# BOM output
|
||||
bomlist = bom_list(self.bom())
|
||||
if "tsv" in fmt:
|
||||
open_file_write(f"{filename}.bom.tsv").write(tuplelist2tsv(bomlist))
|
||||
if "csv" in fmt:
|
||||
# TODO: implement CSV output (preferrably using CSV library)
|
||||
print("CSV output is not yet supported")
|
||||
# HTML output
|
||||
if "html" in fmt:
|
||||
generate_html_output(filename, bomlist, self.metadata, self.options)
|
||||
# PDF output
|
||||
if "pdf" in fmt:
|
||||
|
||||
if "csv" in formats:
|
||||
# TODO: implement CSV output (preferably using CSV library)
|
||||
sys.stderr.write("CSV output is not yet supported")
|
||||
if "pdf" in formats:
|
||||
# TODO: implement PDF output
|
||||
print("PDF output is not yet supported")
|
||||
# delete SVG if not needed
|
||||
if "html" in fmt and not "svg" in fmt:
|
||||
# SVG file was just needed to generate HTML
|
||||
Path(f"{filename}.tmp.svg").unlink()
|
||||
elif "svg" in fmt:
|
||||
Path(f"{filename}.tmp.svg").replace(f"{filename}.svg")
|
||||
sys.stderr.write("PDF output is not yet supported")
|
||||
|
||||
outputs = {}
|
||||
if "svg" in formats or "html" in formats:
|
||||
# embed images into SVG output
|
||||
outputs["svg"] = embed_svg_images(graph.pipe(format="svg", encoding="utf8"))
|
||||
|
||||
if "png" in formats:
|
||||
outputs["png"] = graph.pipe(format="png")
|
||||
|
||||
# GraphViz output
|
||||
if "gv" in formats:
|
||||
outputs["gv"] = graph.pipe(format="gv")
|
||||
|
||||
if "tsv" in formats or "html" in formats:
|
||||
bomlist = bom_list(self.bom())
|
||||
# BOM output
|
||||
if "tsv" in formats:
|
||||
outputs["tsv"] = tuplelist2tsv(bomlist)
|
||||
|
||||
# HTML output
|
||||
if "html" in formats and "svg" in outputs:
|
||||
outputs["html"] = generate_html_output(outputs["svg"], output_dir, bomlist, self.metadata, self.options)
|
||||
|
||||
# print to stdout or write files in order
|
||||
for f in formats:
|
||||
if f in outputs:
|
||||
output = outputs[f]
|
||||
if output_dir is None or output_name is None:
|
||||
if isinstance(output, (bytes, bytearray)):
|
||||
sys.stdout.buffer.write(output)
|
||||
else:
|
||||
sys.stdout.write(output)
|
||||
else:
|
||||
file = f"{output_dir}/{output_name}.{f}"
|
||||
if isinstance(output, (bytes, bytearray)):
|
||||
with open(file, "wb") as binary_file:
|
||||
binary_file.write(output)
|
||||
else:
|
||||
with open(file, "w") as binary_file:
|
||||
binary_file.write(output)
|
||||
|
||||
def bom(self):
|
||||
if not self._bom:
|
||||
|
||||
@ -4,5 +4,5 @@
|
||||
__version__ = "0.4-dev"
|
||||
|
||||
CMD_NAME = "wireviz" # Lower case command and module name
|
||||
APP_NAME = "WireViz" # Application name in texts meant to be human readable
|
||||
APP_URL = "https://github.com/formatc1702/WireViz"
|
||||
APP_NAME = "WireViz" # Application name in texts meant to be human-readable
|
||||
APP_URL = "https://github.com/wireviz/WireViz"
|
||||
|
||||
@ -38,15 +38,3 @@ def get_mime_subtype(filename: Union[str, Path]) -> str:
|
||||
if mime_subtype in mime_subtype_replacements:
|
||||
mime_subtype = mime_subtype_replacements[mime_subtype]
|
||||
return mime_subtype
|
||||
|
||||
|
||||
def embed_svg_images_file(
|
||||
filename_in: Union[str, Path], overwrite: bool = True
|
||||
) -> None:
|
||||
filename_in = Path(filename_in).resolve()
|
||||
filename_out = filename_in.with_suffix(".b64.svg")
|
||||
filename_out.write_text(
|
||||
embed_svg_images(filename_in.read_text(), filename_in.parent)
|
||||
)
|
||||
if overwrite:
|
||||
filename_out.replace(filename_in)
|
||||
|
||||
@ -58,7 +58,7 @@ def parse(
|
||||
return_types (optional):
|
||||
One of the supported return types (see above), or a tuple of multiple return types.
|
||||
If set to None, no output is returned by the function.
|
||||
output_formats (optional):
|
||||
output_formats (Tuple[str], optional):
|
||||
One of the supported output types (see above), or a tuple of multiple output formats.
|
||||
If set to None, no files are generated.
|
||||
output_dir (Path | str, optional):
|
||||
@ -87,15 +87,18 @@ def parse(
|
||||
|
||||
yaml_data, yaml_file = _get_yaml_data_and_path(inp)
|
||||
if output_formats:
|
||||
# need to write data to file, determine output directory and filename
|
||||
output_dir = _get_output_dir(yaml_file, output_dir)
|
||||
output_name = _get_output_name(yaml_file, output_name)
|
||||
output_file = output_dir / output_name
|
||||
if str(output_dir) == "-":
|
||||
# write to stdout
|
||||
output_dir = None
|
||||
else:
|
||||
# write to directory
|
||||
output_dir = _get_output_dir(yaml_file, output_dir)
|
||||
output_name = _get_output_name(yaml_file, output_name)
|
||||
|
||||
if yaml_file:
|
||||
# if reading from file, ensure that input file's parent directory is included in image_paths
|
||||
default_image_path = yaml_file.parent.resolve()
|
||||
if not default_image_path in [Path(x).resolve() for x in image_paths]:
|
||||
if default_image_path not in [Path(x).resolve() for x in image_paths]:
|
||||
image_paths.append(default_image_path)
|
||||
|
||||
# define variables =========================================================
|
||||
@ -362,11 +365,11 @@ def parse(
|
||||
harness.add_bom_item(line)
|
||||
|
||||
if output_formats:
|
||||
harness.output(filename=output_file, fmt=output_formats, view=False)
|
||||
harness.output(formats=output_formats, output_dir=output_dir, output_name=output_name)
|
||||
|
||||
if return_types:
|
||||
returns = []
|
||||
if isinstance(return_types, str): # only one return type speficied
|
||||
if isinstance(return_types, str): # only one return type specified
|
||||
return_types = [return_types]
|
||||
|
||||
return_types = [t.lower() for t in return_types]
|
||||
@ -390,10 +393,11 @@ def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> (Dict, Path):
|
||||
# if no FileNotFoundError exception happens, get file contents
|
||||
yaml_str = open_file_read(yaml_path).read()
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
# if inp is a long YAML string, Pathlib will raise OSError: [Errno 63]
|
||||
# if inp is a long YAML string, Pathlib will raise OSError: [Errno 63].
|
||||
# when trying to expand and resolve it as a path.
|
||||
# Catch this error, but raise any others
|
||||
if type(e) is OSError and e.errno != 63:
|
||||
# it can also raise OSError: [Errno 36] File name too long.
|
||||
# Catch these errors, but raise any others.
|
||||
if type(e) is OSError and e.errno != 63 and e.errno != 36:
|
||||
raise e
|
||||
# file does not exist; assume inp is a YAML string
|
||||
yaml_str = inp
|
||||
@ -417,7 +421,7 @@ def _get_output_dir(input_file: Path, default_output_dir: Path) -> Path:
|
||||
return output_dir.resolve()
|
||||
|
||||
|
||||
def _get_output_name(input_file: Path, default_output_name: Path) -> str:
|
||||
def _get_output_name(input_file: Path, default_output_name: Union[None, str]) -> str:
|
||||
if default_output_name: # user-specified output name
|
||||
output_name = default_output_name
|
||||
else: # auto-determine appropriate output name
|
||||
@ -429,7 +433,7 @@ def _get_output_name(input_file: Path, default_output_name: Path) -> str:
|
||||
|
||||
|
||||
def main():
|
||||
print("When running from the command line, please use wv_cli.py instead.")
|
||||
sys.stderr.write("When running from the command line, please use wv_cli.py instead.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import click
|
||||
|
||||
@ -67,12 +68,11 @@ epilog += ", ".join([f"{key} ({value.upper()})" for key, value in format_codes.i
|
||||
default=False,
|
||||
help=f"Output {APP_NAME} version and exit.",
|
||||
)
|
||||
def wireviz(file, format, prepend, output_dir, output_name, version):
|
||||
def wireviz(file, format, prepend, output_dir: Union[Path, None], output_name, version):
|
||||
"""
|
||||
Parses the provided FILE and generates the specified outputs.
|
||||
"""
|
||||
print()
|
||||
print(f"{APP_NAME} {__version__}")
|
||||
sys.stderr.write(f"{APP_NAME} {__version__}\n")
|
||||
if version:
|
||||
return # print version number only and exit
|
||||
|
||||
@ -88,10 +88,13 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
|
||||
output_formats = []
|
||||
for code in format:
|
||||
if code in format_codes:
|
||||
output_formats.append(format_codes[code])
|
||||
output_format: str = format_codes[code]
|
||||
# unique
|
||||
if output_format not in output_formats:
|
||||
output_formats.append(output_format)
|
||||
else:
|
||||
raise Exception(f"Unknown output format: {code}")
|
||||
output_formats = tuple(sorted(set(output_formats)))
|
||||
|
||||
output_formats_str = (
|
||||
f'[{"|".join(output_formats)}]'
|
||||
if len(output_formats) > 1
|
||||
@ -105,44 +108,63 @@ def wireviz(file, format, prepend, output_dir, output_name, version):
|
||||
prepend_file = Path(prepend_file)
|
||||
if not prepend_file.exists():
|
||||
raise Exception(f"File does not exist:\n{prepend_file}")
|
||||
print("Prepend file:", prepend_file)
|
||||
|
||||
sys.stderr.write(f"Prepend file: {prepend_file}")
|
||||
|
||||
prepend_input += open_file_read(prepend_file).read() + "\n"
|
||||
else:
|
||||
prepend_input = ""
|
||||
|
||||
# run WireVIz on each input file
|
||||
for file in filepaths:
|
||||
file = Path(file)
|
||||
if not file.exists():
|
||||
raise Exception(f"File does not exist:\n{file}")
|
||||
|
||||
# file_out = file.with_suffix("") if not output_file else output_file
|
||||
_output_dir = file.parent if not output_dir else output_dir
|
||||
_output_name = file.stem if not output_name else output_name
|
||||
|
||||
print("Input file: ", file)
|
||||
print(
|
||||
"Output file: ", f"{Path(_output_dir / _output_name)}.{output_formats_str}"
|
||||
output_stdout = str(output_dir) == "-" or output_name == "-"
|
||||
if output_stdout and len(filepaths) == 0:
|
||||
wv.parse(
|
||||
sys.stdin.read(),
|
||||
output_dir="-",
|
||||
output_formats=tuple(output_formats),
|
||||
)
|
||||
|
||||
yaml_input = open_file_read(file).read()
|
||||
file_dir = file.parent
|
||||
# run WireViz on each input file
|
||||
for file in filepaths:
|
||||
if file == "-":
|
||||
# read from stdin
|
||||
yaml_input = prepend_input + sys.stdin.read()
|
||||
image_paths = []
|
||||
else:
|
||||
file = Path(file)
|
||||
if not file.exists():
|
||||
raise Exception(f"File does not exist:\n{file}")
|
||||
|
||||
sys.stderr.write(f"Input file: {file}\n")
|
||||
yaml_input = open_file_read(file).read()
|
||||
file_dir = file.parent
|
||||
|
||||
yaml_input = prepend_input + yaml_input
|
||||
image_paths = {file_dir}
|
||||
|
||||
yaml_input = prepend_input + yaml_input
|
||||
image_paths = {file_dir}
|
||||
for p in prepend:
|
||||
image_paths.add(Path(p).parent)
|
||||
|
||||
# file_out = file.with_suffix("") if not output_file else output_file
|
||||
if output_stdout or file == "-":
|
||||
sys.stderr.write(f"Output: <stdout>.{output_formats_str}\n")
|
||||
_output_dir = "-"
|
||||
_output_name = None
|
||||
else:
|
||||
_output_dir = file.parent if not output_dir else output_dir
|
||||
_output_name = file.stem if not output_name else output_name
|
||||
sys.stderr.write(
|
||||
f"Output file: {Path(_output_dir / _output_name)}.{output_formats_str}"
|
||||
)
|
||||
|
||||
wv.parse(
|
||||
yaml_input,
|
||||
output_formats=output_formats,
|
||||
output_formats=tuple(output_formats),
|
||||
output_dir=_output_dir,
|
||||
output_name=_output_name,
|
||||
image_paths=list(image_paths),
|
||||
)
|
||||
|
||||
print()
|
||||
sys.stderr.write('')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
from typing import Dict, List
|
||||
|
||||
COLOR_CODES = {
|
||||
@ -138,7 +138,7 @@ def get_color_hex(input: Colors, pad: bool = False) -> List[str]:
|
||||
if c[0] != "#" or not all(d in _hex_digits for d in c[1:]):
|
||||
if c != input:
|
||||
c += f" in input: {input}"
|
||||
print(f"Invalid hex color: {c}")
|
||||
sys.stderr.write(f"Invalid hex color: {c}")
|
||||
output[i] = color_default
|
||||
else: # Color name(s)
|
||||
|
||||
@ -148,7 +148,7 @@ def get_color_hex(input: Colors, pad: bool = False) -> List[str]:
|
||||
except KeyError:
|
||||
if c != input:
|
||||
c += f" in input: {input}"
|
||||
print(f"Unknown color name: {c}")
|
||||
sys.stderr.write(f"Unknown color name: {c}")
|
||||
return color_default
|
||||
|
||||
output = [lookup(input[i : i + 2]) for i in range(0, len(input), 2)]
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
@ -147,10 +148,10 @@ def aspect_ratio(image_src):
|
||||
image = Image.open(image_src)
|
||||
if image.width > 0 and image.height > 0:
|
||||
return image.width / image.height
|
||||
print(f"aspect_ratio(): Invalid image size {image.width} x {image.height}")
|
||||
sys.stderr.write(f"aspect_ratio(): Invalid image size {image.width} x {image.height}")
|
||||
# ModuleNotFoundError and FileNotFoundError are the most expected, but all are handled equally.
|
||||
except Exception as error:
|
||||
print(f"aspect_ratio(): {type(error).__name__}: {error}")
|
||||
sys.stderr.write(f"aspect_ratio(): {type(error).__name__}: {error}")
|
||||
return 1 # Assume 1:1 when unable to read actual image size
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Union
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from wireviz import APP_NAME, APP_URL, __version__, wv_colors
|
||||
from wireviz.DataClasses import Metadata, Options
|
||||
@ -10,45 +10,46 @@ from wireviz.wv_gv_html import html_line_breaks
|
||||
from wireviz.wv_helper import (
|
||||
flatten2d,
|
||||
open_file_read,
|
||||
open_file_write,
|
||||
smart_file_resolve,
|
||||
)
|
||||
|
||||
|
||||
def generate_html_output(
|
||||
filename: Union[str, Path],
|
||||
svg_input: str,
|
||||
output_dir: Optional[Path],
|
||||
bom_list: List[List[str]],
|
||||
metadata: Metadata,
|
||||
options: Options,
|
||||
):
|
||||
|
||||
) -> str:
|
||||
# load HTML template
|
||||
templatename = metadata.get("template", {}).get("name")
|
||||
if templatename:
|
||||
# if relative path to template was provided, check directory of YAML file first, fall back to built-in template directory
|
||||
templatefile = smart_file_resolve(
|
||||
f"{templatename}.html",
|
||||
[Path(filename).parent, Path(__file__).parent / "templates"],
|
||||
)
|
||||
else:
|
||||
# fall back to built-in simple template if no template was provided
|
||||
templatefile = Path(__file__).parent / "templates/simple.html"
|
||||
template_name = metadata.get("template", {}).get("name")
|
||||
builtin_template_directory = Path(__file__).parent / "templates" # built-in template directory
|
||||
if template_name:
|
||||
possible_paths = []
|
||||
# if relative path to template was provided, check directory of YAML file first
|
||||
if output_dir is not None:
|
||||
possible_paths.append(output_dir)
|
||||
|
||||
html = open_file_read(templatefile).read()
|
||||
possible_paths.append(builtin_template_directory) # fallback
|
||||
template_file = smart_file_resolve(f"{template_name}.html", possible_paths)
|
||||
else:
|
||||
# fallback to built-in simple template if no template was provided
|
||||
template_file = builtin_template_directory / "simple.html"
|
||||
|
||||
html = open_file_read(template_file).read()
|
||||
|
||||
# embed SVG diagram
|
||||
with open_file_read(f"{filename}.tmp.svg") as file:
|
||||
svgdata = re.sub(
|
||||
"^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>",
|
||||
"<!-- XML and DOCTYPE declarations from SVG file removed -->",
|
||||
file.read(),
|
||||
1,
|
||||
)
|
||||
svg_data = re.sub(
|
||||
"^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>",
|
||||
"<!-- XML and DOCTYPE declarations from SVG file removed -->",
|
||||
svg_input,
|
||||
1,
|
||||
)
|
||||
|
||||
# generate BOM table
|
||||
bom = flatten2d(bom_list)
|
||||
|
||||
# generate BOM header (may be at the top or bottom of the table)
|
||||
# generate BOM header (might be at the top or bottom of the table)
|
||||
bom_header_html = " <tr>\n"
|
||||
for item in bom[0]:
|
||||
th_class = f"bom_col_{item.lower()}"
|
||||
@ -80,7 +81,7 @@ def generate_html_output(
|
||||
"<!-- %generator% -->": f"{APP_NAME} {__version__} - {APP_URL}",
|
||||
"<!-- %fontname% -->": options.fontname,
|
||||
"<!-- %bgcolor% -->": wv_colors.translate_color(options.bgcolor, "hex"),
|
||||
"<!-- %diagram% -->": svgdata,
|
||||
"<!-- %diagram% -->": svg_data,
|
||||
"<!-- %bom% -->": bom_html,
|
||||
"<!-- %bom_reversed% -->": bom_html_reversed,
|
||||
"<!-- %sheet_current% -->": "1", # TODO: handle multi-page documents
|
||||
@ -114,6 +115,4 @@ def generate_html_output(
|
||||
replacements_sorted = sorted(replacements, key=len, reverse=True)
|
||||
replacements_escaped = map(re.escape, replacements_sorted)
|
||||
pattern = re.compile("|".join(replacements_escaped))
|
||||
html = pattern.sub(lambda match: replacements[match.group(0)], html)
|
||||
|
||||
open_file_write(f"{filename}.html").write(html)
|
||||
return pattern.sub(lambda match: replacements[match.group(0)], html)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user