Rename modules, adjust imports, move build_examples.py
This commit is contained in:
parent
7111a3375f
commit
fb91be402a
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install .
|
||||
- name: Create Examples
|
||||
run: PYTHONPATH=$(pwd)/src:$PYTHONPATH cd src/wireviz/ && python build_examples.py
|
||||
run: PYTHONPATH=$(pwd)/src:$PYTHONPATH && python src/wireviz/tools/build_examples.py
|
||||
- name: Upload examples, demos, and tutorials
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base64
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
mime_subtype_replacements = {"jpg": "jpeg", "tif": "tiff"}
|
||||
|
||||
|
||||
def embed_svg_images(svg_in: str, base_path: Union[str, Path] = Path.cwd()) -> str:
|
||||
images_b64 = {} # cache of base64-encoded images
|
||||
|
||||
def image_tag(pre: str, url: str, post: str) -> str:
|
||||
return f'<image{pre} xlink:href="{url}"{post}>'
|
||||
|
||||
def replace(match: re.Match) -> str:
|
||||
imgurl = match["URL"]
|
||||
if not imgurl in images_b64: # only encode/cache every unique URL once
|
||||
imgurl_abs = (Path(base_path) / imgurl).resolve()
|
||||
image = imgurl_abs.read_bytes()
|
||||
images_b64[imgurl] = base64.b64encode(image).decode("utf-8")
|
||||
return image_tag(
|
||||
match["PRE"] or "",
|
||||
f"data:image/{get_mime_subtype(imgurl)};base64, {images_b64[imgurl]}",
|
||||
match["POST"] or "",
|
||||
)
|
||||
|
||||
pattern = re.compile(
|
||||
image_tag(r"(?P<PRE> [^>]*?)?", r'(?P<URL>[^"]*?)', r"(?P<POST> [^>]*?)?"),
|
||||
re.IGNORECASE,
|
||||
)
|
||||
return pattern.sub(replace, svg_in)
|
||||
|
||||
|
||||
def get_mime_subtype(filename: Union[str, Path]) -> str:
|
||||
mime_subtype = Path(filename).suffix.lstrip(".").lower()
|
||||
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)
|
||||
@ -7,13 +7,12 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
script_path = Path(__file__).absolute()
|
||||
|
||||
sys.path.insert(0, str(script_path.parent.parent)) # to find wireviz module
|
||||
from wv_helper import open_file_append, open_file_read, open_file_write
|
||||
sys.path.insert(0, str(script_path.parent.parent.parent)) # to find wireviz module
|
||||
|
||||
from wireviz import APP_NAME, __version__, wireviz
|
||||
from wireviz.wv_utils import open_file_append, open_file_read, open_file_write
|
||||
|
||||
dir = script_path.parent.parent.parent
|
||||
dir = script_path.parent.parent.parent.parent
|
||||
readme = "readme.md"
|
||||
groups = {
|
||||
"examples": {
|
||||
@ -10,9 +10,9 @@ import yaml
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent)) # add src/wireviz to PATH
|
||||
|
||||
from wireviz.DataClasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
|
||||
from wireviz.Harness import Harness
|
||||
from wireviz.wv_helper import (
|
||||
from wireviz.wv_dataclasses import AUTOGENERATED_PREFIX, Metadata, Options, Tweak
|
||||
from wireviz.wv_harness import Harness
|
||||
from wireviz.wv_utils import (
|
||||
expand,
|
||||
get_single_key_and_value,
|
||||
is_arrow,
|
||||
@ -396,20 +396,12 @@ def parse(
|
||||
def _get_yaml_data_and_path(inp: Union[str, Path, Dict]) -> (Dict, Path):
|
||||
# determine whether inp is a file path, a YAML string, or a Dict
|
||||
if not isinstance(inp, Dict): # received a str or a Path
|
||||
try:
|
||||
if isinstance(inp, Path) or (isinstance(inp, str) and not "\n" in inp):
|
||||
yaml_path = Path(inp).expanduser().resolve(strict=True)
|
||||
# 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.ENAMETOOLONG]
|
||||
# when trying to expand and resolve it as a path.
|
||||
# Catch this error, but raise any others
|
||||
from errno import ENAMETOOLONG
|
||||
if type(e) is OSError and e.errno != ENAMETOOLONG:
|
||||
raise e
|
||||
# file does not exist; assume inp is a YAML string
|
||||
yaml_str = inp
|
||||
else:
|
||||
yaml_path = None
|
||||
yaml_str = inp
|
||||
yaml_data = yaml.safe_load(yaml_str)
|
||||
else:
|
||||
# received a Dict, use as-is
|
||||
|
||||
@ -5,7 +5,7 @@ from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from wireviz.wv_helper import html_line_breaks
|
||||
from wireviz.wv_utils import html_line_breaks
|
||||
|
||||
BOM_HASH_FIELDS = "description unit partnumbers"
|
||||
BomHash = namedtuple("BomHash", BOM_HASH_FIELDS)
|
||||
|
||||
@ -11,7 +11,7 @@ if __name__ == "__main__":
|
||||
|
||||
import wireviz.wireviz as wv
|
||||
from wireviz import APP_NAME, __version__
|
||||
from wireviz.wv_helper import open_file_read
|
||||
from wireviz.wv_utils import open_file_read
|
||||
|
||||
format_codes = {
|
||||
"c": "csv",
|
||||
@ -23,10 +23,11 @@ format_codes = {
|
||||
"t": "tsv",
|
||||
}
|
||||
|
||||
|
||||
epilog = (
|
||||
"The -f or --format option accepts a string containing one or more of the "
|
||||
"following characters to specify which file types to output:\n"
|
||||
f", ".join([f"{key} ({value.upper()})" for key, value in format_codes.items()])
|
||||
+ f", ".join([f"{key} ({value.upper()})" for key, value in format_codes.items()])
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ from wireviz.wv_colors import (
|
||||
SingleColor,
|
||||
get_color_by_colorcode_index,
|
||||
)
|
||||
from wireviz.wv_helper import aspect_ratio, awg_equiv, mm2_equiv, remove_links
|
||||
from wireviz.wv_utils import aspect_ratio, awg_equiv, mm2_equiv, remove_links
|
||||
|
||||
# Each type alias have their legal values described in comments
|
||||
# - validation might be implemented in the future
|
||||
@ -5,7 +5,8 @@ from itertools import zip_longest
|
||||
from typing import Any, List, Union
|
||||
|
||||
from wireviz import APP_NAME, APP_URL, __version__
|
||||
from wireviz.DataClasses import (
|
||||
from wireviz.wv_bom import partnumbers_to_list
|
||||
from wireviz.wv_dataclasses import (
|
||||
ArrowDirection,
|
||||
ArrowWeight,
|
||||
Cable,
|
||||
@ -18,9 +19,8 @@ from wireviz.DataClasses import (
|
||||
ShieldClass,
|
||||
WireClass,
|
||||
)
|
||||
from wireviz.wv_bom import partnumbers_to_list
|
||||
from wireviz.wv_helper import html_line_breaks, remove_links
|
||||
from wireviz.wv_table_util import * # TODO: explicitly import each needed tag later
|
||||
from wireviz.wv_html import Img, Table, Td, Tr
|
||||
from wireviz.wv_utils import html_line_breaks, remove_links
|
||||
|
||||
|
||||
def gv_node_component(component: Component) -> Table:
|
||||
@ -8,7 +8,7 @@ from typing import List
|
||||
from graphviz import Graph
|
||||
|
||||
import wireviz.wv_colors
|
||||
from wireviz.DataClasses import (
|
||||
from wireviz.wv_dataclasses import (
|
||||
AUTOGENERATED_PREFIX,
|
||||
AdditionalComponent,
|
||||
Arrow,
|
||||
@ -23,8 +23,7 @@ from wireviz.DataClasses import (
|
||||
Side,
|
||||
Tweak,
|
||||
)
|
||||
from wireviz.svgembed import embed_svg_images_file
|
||||
from wireviz.wv_gv_html import (
|
||||
from wireviz.wv_graphviz import (
|
||||
apply_dot_tweaks,
|
||||
calculate_node_bgcolor,
|
||||
gv_connector_loops,
|
||||
@ -34,8 +33,8 @@ from wireviz.wv_gv_html import (
|
||||
parse_arrow_str,
|
||||
set_dot_basics,
|
||||
)
|
||||
from wireviz.wv_helper import open_file_write, tuplelist2tsv
|
||||
from wireviz.wv_html import generate_html_output
|
||||
from wireviz.wv_output import embed_svg_images_file, generate_html_output
|
||||
from wireviz.wv_utils import open_file_write, tuplelist2tsv
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -1,120 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Union
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict
|
||||
|
||||
from wireviz import APP_NAME, APP_URL, __version__, wv_colors
|
||||
from wireviz.DataClasses import Metadata, Options
|
||||
from wireviz.wv_helper import (
|
||||
flatten2d,
|
||||
html_line_breaks,
|
||||
open_file_read,
|
||||
open_file_write,
|
||||
smart_file_resolve,
|
||||
)
|
||||
indent_count = 1
|
||||
|
||||
|
||||
def generate_html_output(
|
||||
filename: Union[str, Path],
|
||||
bom_list: List[List[str]],
|
||||
metadata: Metadata,
|
||||
options: Options,
|
||||
):
|
||||
class Attribs(Dict):
|
||||
def __repr__(self):
|
||||
if len(self) == 0:
|
||||
return ""
|
||||
|
||||
# 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"
|
||||
html = []
|
||||
for k, v in self.items():
|
||||
if v is not None:
|
||||
html.append(f' {k}="{v}"')
|
||||
# else:
|
||||
# html.append(f" {k}")
|
||||
return "".join(html)
|
||||
|
||||
html = open_file_read(templatefile).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,
|
||||
)
|
||||
@dataclass
|
||||
class Tag:
|
||||
contents = None
|
||||
attribs: Attribs = field(default_factory=Attribs)
|
||||
flat: bool = None
|
||||
delete_if_empty: bool = False
|
||||
|
||||
# generate BOM table
|
||||
bom = flatten2d(bom_list)
|
||||
def __init__(self, contents, flat=None, delete_if_empty=False, **kwargs):
|
||||
self.contents = contents
|
||||
self.flat = flat
|
||||
self.delete_if_empty = delete_if_empty
|
||||
self.attribs = Attribs({**kwargs})
|
||||
|
||||
# generate BOM header (may 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()}"
|
||||
bom_header_html = f'{bom_header_html} <th class="{th_class}">{item}</th>\n'
|
||||
bom_header_html = f"{bom_header_html} </tr>\n"
|
||||
def update_attribs(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
self.attribs[k] = v
|
||||
|
||||
# generate BOM contents
|
||||
bom_contents = []
|
||||
for row in bom[1:]:
|
||||
row_html = " <tr>\n"
|
||||
for i, item in enumerate(row):
|
||||
td_class = f"bom_col_{bom[0][i].lower()}"
|
||||
row_html = f'{row_html} <td class="{td_class}">{item}</td>\n'
|
||||
row_html = f"{row_html} </tr>\n"
|
||||
bom_contents.append(row_html)
|
||||
@property
|
||||
def tagname(self):
|
||||
return type(self).__name__.lower()
|
||||
|
||||
bom_html = (
|
||||
'<table class="bom">\n' + bom_header_html + "".join(bom_contents) + "</table>\n"
|
||||
)
|
||||
bom_html_reversed = (
|
||||
'<table class="bom">\n'
|
||||
+ "".join(list(reversed(bom_contents)))
|
||||
+ bom_header_html
|
||||
+ "</table>\n"
|
||||
)
|
||||
@property
|
||||
def auto_flat(self):
|
||||
if self.flat is not None: # user specified
|
||||
return self.flat
|
||||
if not _is_iterable_not_str(self.contents): # catch str, int, float, ...
|
||||
if not isinstance(self.contents, Tag): # avoid recursion
|
||||
return not "\n" in str(self.contents) # flatten if single line
|
||||
|
||||
# prepare simple replacements
|
||||
replacements = {
|
||||
"<!-- %generator% -->": f"{APP_NAME} {__version__} - {APP_URL}",
|
||||
"<!-- %fontname% -->": options.fontname,
|
||||
"<!-- %bgcolor% -->": options.bgcolor.html,
|
||||
"<!-- %diagram% -->": svgdata,
|
||||
"<!-- %bom% -->": bom_html,
|
||||
"<!-- %bom_reversed% -->": bom_html_reversed,
|
||||
"<!-- %sheet_current% -->": "1", # TODO: handle multi-page documents
|
||||
"<!-- %sheet_total% -->": "1", # TODO: handle multi-page documents
|
||||
}
|
||||
@property
|
||||
def is_empty(self):
|
||||
return self.get_contents(force_flat=True) == ""
|
||||
|
||||
# prepare metadata replacements
|
||||
if metadata:
|
||||
for item, contents in metadata.items():
|
||||
if isinstance(contents, (str, int, float)):
|
||||
replacements[f"<!-- %{item}% -->"] = html_line_breaks(str(contents))
|
||||
elif isinstance(contents, Dict): # useful for authors, revisions
|
||||
for index, (category, entry) in enumerate(contents.items()):
|
||||
if isinstance(entry, Dict):
|
||||
replacements[f"<!-- %{item}_{index+1}% -->"] = str(category)
|
||||
for entry_key, entry_value in entry.items():
|
||||
replacements[
|
||||
f"<!-- %{item}_{index+1}_{entry_key}% -->"
|
||||
] = html_line_breaks(str(entry_value))
|
||||
def indent_lines(self, lines, force_flat=False):
|
||||
if self.auto_flat or force_flat:
|
||||
return lines
|
||||
else:
|
||||
indenter = " " * indent_count
|
||||
return "\n".join(f"{indenter}{line}" for line in lines.split("\n"))
|
||||
|
||||
replacements['"sheetsize_default"'] = '"{}"'.format(
|
||||
metadata.get("template", {}).get("sheetsize", "")
|
||||
)
|
||||
# include quotes so no replacement happens within <style> definition
|
||||
def get_contents(self, force_flat=False):
|
||||
separator = "" if self.auto_flat or force_flat else "\n"
|
||||
if _is_iterable_not_str(self.contents):
|
||||
return separator.join(
|
||||
[
|
||||
self.indent_lines(str(c), force_flat)
|
||||
for c in self.contents
|
||||
if c is not None
|
||||
]
|
||||
)
|
||||
elif self.contents is None:
|
||||
return ""
|
||||
else: # str, int, float, etc.
|
||||
return self.indent_lines(str(self.contents), force_flat)
|
||||
|
||||
# perform replacements
|
||||
# regex replacement adapted from:
|
||||
# https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729
|
||||
def __repr__(self):
|
||||
separator = "" if self.auto_flat else "\n"
|
||||
if self.delete_if_empty and self.is_empty:
|
||||
return ""
|
||||
else:
|
||||
html = [
|
||||
f"<{self.tagname}{str(self.attribs)}>",
|
||||
f"{self.get_contents()}",
|
||||
f"</{self.tagname}>",
|
||||
]
|
||||
html_joined = separator.join(html)
|
||||
return html_joined
|
||||
|
||||
# longer replacements first, just in case
|
||||
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)
|
||||
@dataclass
|
||||
class TagSingleton(Tag):
|
||||
def __init__(self, **kwargs):
|
||||
self.attribs = Attribs({**kwargs})
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.tagname}{str(self.attribs)} />"
|
||||
|
||||
|
||||
def _is_iterable_not_str(inp):
|
||||
# str is iterable, but should be treated as not iterable
|
||||
return isinstance(inp, Iterable) and not isinstance(inp, str)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Br(TagSingleton):
|
||||
pass
|
||||
|
||||
|
||||
class Img(TagSingleton):
|
||||
pass
|
||||
|
||||
|
||||
class Td(Tag):
|
||||
pass
|
||||
|
||||
|
||||
class Tr(Tag):
|
||||
pass
|
||||
|
||||
|
||||
class Table(Tag):
|
||||
pass
|
||||
|
||||
168
src/wireviz/wv_output.py
Normal file
168
src/wireviz/wv_output.py
Normal file
@ -0,0 +1,168 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base64
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Union
|
||||
|
||||
import wireviz # for doing wireviz.__file__
|
||||
from wireviz import APP_NAME, APP_URL, __version__
|
||||
from wireviz.wv_dataclasses import Metadata, Options
|
||||
from wireviz.wv_utils import (
|
||||
flatten2d,
|
||||
html_line_breaks,
|
||||
open_file_read,
|
||||
open_file_write,
|
||||
smart_file_resolve,
|
||||
)
|
||||
|
||||
mime_subtype_replacements = {"jpg": "jpeg", "tif": "tiff"}
|
||||
|
||||
|
||||
def embed_svg_images(svg_in: str, base_path: Union[str, Path] = Path.cwd()) -> str:
|
||||
images_b64 = {} # cache of base64-encoded images
|
||||
|
||||
def image_tag(pre: str, url: str, post: str) -> str:
|
||||
return f'<image{pre} xlink:href="{url}"{post}>'
|
||||
|
||||
def replace(match: re.Match) -> str:
|
||||
imgurl = match["URL"]
|
||||
if not imgurl in images_b64: # only encode/cache every unique URL once
|
||||
imgurl_abs = (Path(base_path) / imgurl).resolve()
|
||||
image = imgurl_abs.read_bytes()
|
||||
images_b64[imgurl] = base64.b64encode(image).decode("utf-8")
|
||||
return image_tag(
|
||||
match["PRE"] or "",
|
||||
f"data:image/{get_mime_subtype(imgurl)};base64, {images_b64[imgurl]}",
|
||||
match["POST"] or "",
|
||||
)
|
||||
|
||||
pattern = re.compile(
|
||||
image_tag(r"(?P<PRE> [^>]*?)?", r'(?P<URL>[^"]*?)', r"(?P<POST> [^>]*?)?"),
|
||||
re.IGNORECASE,
|
||||
)
|
||||
return pattern.sub(replace, svg_in)
|
||||
|
||||
|
||||
def get_mime_subtype(filename: Union[str, Path]) -> str:
|
||||
mime_subtype = Path(filename).suffix.lstrip(".").lower()
|
||||
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)
|
||||
|
||||
|
||||
def generate_html_output(
|
||||
filename: Union[str, Path],
|
||||
bom_list: List[List[str]],
|
||||
metadata: Metadata,
|
||||
options: Options,
|
||||
):
|
||||
|
||||
# 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(wireviz.__file__).parent / "templates/simple.html"
|
||||
|
||||
html = open_file_read(templatefile).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,
|
||||
)
|
||||
|
||||
# generate BOM table
|
||||
bom = flatten2d(bom_list)
|
||||
|
||||
# generate BOM header (may 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()}"
|
||||
bom_header_html = f'{bom_header_html} <th class="{th_class}">{item}</th>\n'
|
||||
bom_header_html = f"{bom_header_html} </tr>\n"
|
||||
|
||||
# generate BOM contents
|
||||
bom_contents = []
|
||||
for row in bom[1:]:
|
||||
row_html = " <tr>\n"
|
||||
for i, item in enumerate(row):
|
||||
td_class = f"bom_col_{bom[0][i].lower()}"
|
||||
row_html = f'{row_html} <td class="{td_class}">{item}</td>\n'
|
||||
row_html = f"{row_html} </tr>\n"
|
||||
bom_contents.append(row_html)
|
||||
|
||||
bom_html = (
|
||||
'<table class="bom">\n' + bom_header_html + "".join(bom_contents) + "</table>\n"
|
||||
)
|
||||
bom_html_reversed = (
|
||||
'<table class="bom">\n'
|
||||
+ "".join(list(reversed(bom_contents)))
|
||||
+ bom_header_html
|
||||
+ "</table>\n"
|
||||
)
|
||||
|
||||
# prepare simple replacements
|
||||
replacements = {
|
||||
"<!-- %generator% -->": f"{APP_NAME} {__version__} - {APP_URL}",
|
||||
"<!-- %fontname% -->": options.fontname,
|
||||
"<!-- %bgcolor% -->": options.bgcolor.html,
|
||||
"<!-- %diagram% -->": svgdata,
|
||||
"<!-- %bom% -->": bom_html,
|
||||
"<!-- %bom_reversed% -->": bom_html_reversed,
|
||||
"<!-- %sheet_current% -->": "1", # TODO: handle multi-page documents
|
||||
"<!-- %sheet_total% -->": "1", # TODO: handle multi-page documents
|
||||
}
|
||||
|
||||
# prepare metadata replacements
|
||||
if metadata:
|
||||
for item, contents in metadata.items():
|
||||
if isinstance(contents, (str, int, float)):
|
||||
replacements[f"<!-- %{item}% -->"] = html_line_breaks(str(contents))
|
||||
elif isinstance(contents, Dict): # useful for authors, revisions
|
||||
for index, (category, entry) in enumerate(contents.items()):
|
||||
if isinstance(entry, Dict):
|
||||
replacements[f"<!-- %{item}_{index+1}% -->"] = str(category)
|
||||
for entry_key, entry_value in entry.items():
|
||||
replacements[
|
||||
f"<!-- %{item}_{index+1}_{entry_key}% -->"
|
||||
] = html_line_breaks(str(entry_value))
|
||||
|
||||
replacements['"sheetsize_default"'] = '"{}"'.format(
|
||||
metadata.get("template", {}).get("sheetsize", "")
|
||||
)
|
||||
# include quotes so no replacement happens within <style> definition
|
||||
|
||||
# perform replacements
|
||||
# regex replacement adapted from:
|
||||
# https://gist.github.com/bgusach/a967e0587d6e01e889fd1d776c5f3729
|
||||
|
||||
# longer replacements first, just in case
|
||||
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)
|
||||
@ -1,125 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict
|
||||
|
||||
indent_count = 1
|
||||
|
||||
|
||||
class Attribs(Dict):
|
||||
def __repr__(self):
|
||||
if len(self) == 0:
|
||||
return ""
|
||||
|
||||
html = []
|
||||
for k, v in self.items():
|
||||
if v is not None:
|
||||
html.append(f' {k}="{v}"')
|
||||
# else:
|
||||
# html.append(f" {k}")
|
||||
return "".join(html)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Tag:
|
||||
contents = None
|
||||
attribs: Attribs = field(default_factory=Attribs)
|
||||
flat: bool = None
|
||||
delete_if_empty: bool = False
|
||||
|
||||
def __init__(self, contents, flat=None, delete_if_empty=False, **kwargs):
|
||||
self.contents = contents
|
||||
self.flat = flat
|
||||
self.delete_if_empty = delete_if_empty
|
||||
self.attribs = Attribs({**kwargs})
|
||||
|
||||
def update_attribs(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
self.attribs[k] = v
|
||||
|
||||
@property
|
||||
def tagname(self):
|
||||
return type(self).__name__.lower()
|
||||
|
||||
@property
|
||||
def auto_flat(self):
|
||||
if self.flat is not None: # user specified
|
||||
return self.flat
|
||||
if not _is_iterable_not_str(self.contents): # catch str, int, float, ...
|
||||
if not isinstance(self.contents, Tag): # avoid recursion
|
||||
return not "\n" in str(self.contents) # flatten if single line
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return self.get_contents(force_flat=True) == ""
|
||||
|
||||
def indent_lines(self, lines, force_flat=False):
|
||||
if self.auto_flat or force_flat:
|
||||
return lines
|
||||
else:
|
||||
indenter = " " * indent_count
|
||||
return "\n".join(f"{indenter}{line}" for line in lines.split("\n"))
|
||||
|
||||
def get_contents(self, force_flat=False):
|
||||
separator = "" if self.auto_flat or force_flat else "\n"
|
||||
if _is_iterable_not_str(self.contents):
|
||||
return separator.join(
|
||||
[
|
||||
self.indent_lines(str(c), force_flat)
|
||||
for c in self.contents
|
||||
if c is not None
|
||||
]
|
||||
)
|
||||
elif self.contents is None:
|
||||
return ""
|
||||
else: # str, int, float, etc.
|
||||
return self.indent_lines(str(self.contents), force_flat)
|
||||
|
||||
def __repr__(self):
|
||||
separator = "" if self.auto_flat else "\n"
|
||||
if self.delete_if_empty and self.is_empty:
|
||||
return ""
|
||||
else:
|
||||
html = [
|
||||
f"<{self.tagname}{str(self.attribs)}>",
|
||||
f"{self.get_contents()}",
|
||||
f"</{self.tagname}>",
|
||||
]
|
||||
html_joined = separator.join(html)
|
||||
return html_joined
|
||||
|
||||
|
||||
@dataclass
|
||||
class TagSingleton(Tag):
|
||||
def __init__(self, **kwargs):
|
||||
self.attribs = Attribs({**kwargs})
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.tagname}{str(self.attribs)} />"
|
||||
|
||||
|
||||
def _is_iterable_not_str(inp):
|
||||
# str is iterable, but should be treated as not iterable
|
||||
return isinstance(inp, Iterable) and not isinstance(inp, str)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Br(TagSingleton):
|
||||
pass
|
||||
|
||||
|
||||
class Img(TagSingleton):
|
||||
pass
|
||||
|
||||
|
||||
class Td(Tag):
|
||||
pass
|
||||
|
||||
|
||||
class Tr(Tag):
|
||||
pass
|
||||
|
||||
|
||||
class Table(Tag):
|
||||
pass
|
||||
Loading…
x
Reference in New Issue
Block a user