Merge c87f0733b05c1f7ae7b0b1722f74c99056ff02f9 into a6efd281248975144f3396f233df84814d8fc911

This commit is contained in:
kvid 2021-10-23 02:12:26 +00:00 committed by GitHub
commit 901a0deae9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 165 additions and 90 deletions

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Dict, List, Optional, Tuple, Union from typing import Dict, List, Optional, Tuple, Union
from dataclasses import dataclass, field, InitVar from dataclasses import asdict, dataclass, field, InitVar
from pathlib import Path from pathlib import Path
from wireviz.wv_helper import int2tuple, aspect_ratio from wireviz.wv_helper import int2tuple, aspect_ratio
from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES from wireviz.wv_colors import Color, Colors, ColorMode, ColorScheme, COLOR_CODES, translate_color
# Each type alias have their legal values described in comments - validation might be implemented in the future # Each type alias have their legal values described in comments - validation might be implemented in the future
@ -13,6 +13,7 @@ PlainText = str # Text not containing HTML tags nor newlines
Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output Hypertext = str # Text possibly including HTML hyperlinks that are removed in all outputs except HTML output
MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output MultilineHypertext = str # Hypertext possibly also including newlines to break lines in diagram output
Designator = PlainText # Case insensitive unique name of connector or cable Designator = PlainText # Case insensitive unique name of connector or cable
Points = float # Size in points = 1/72 inch
# Literal type aliases below are commented to avoid requiring python 3.8 # Literal type aliases below are commented to avoid requiring python 3.8
ConnectorMultiplier = PlainText # = Literal['pincount', 'populated'] ConnectorMultiplier = PlainText # = Literal['pincount', 'populated']
@ -33,25 +34,68 @@ class Metadata(dict):
@dataclass @dataclass
class Options: class Look:
fontname: PlainText = 'arial' """Colors and font that defines how an element should look like."""
bgcolor: Color = 'WH' bordercolor: Optional[Color] = None
bgcolor_node: Optional[Color] = 'WH' bgcolor: Optional[Color] = None
bgcolor_connector: Optional[Color] = None fontcolor: Optional[Color] = None
bgcolor_cable: Optional[Color] = None fontname: Optional[PlainText] = None
bgcolor_bundle: Optional[Color] = None fontsize: Optional[Points] = None
def lookdict(self) -> dict:
"""Return Look attributes as dict."""
return {k:v for k,v in asdict(self).items() if k in asdict(DEFAULT_LOOK).keys()}
def _2dict(self) -> dict:
"""Return dict of non-None strings with color values translated to hex."""
return {
k:translate_color(v, "hex") if 'color' in k else str(v)
for k,v in self.lookdict().items() if v is not None
}
def graph_args(self) -> dict:
"""Return dict with arguments to a dot graph."""
return {k:v for k,v in self._2dict().items() if k != 'bordercolor'}
def node_args(self) -> dict:
"""Return dict with arguments to a dot node with filled style."""
return {k.replace('border', '').replace('bg', 'fill'):v for k,v in self._2dict().items()}
def html_style(self, color_prefix: Optional[str] = None, include_all: bool = True) -> str:
"""Return HTML style value containing all non-empty option values."""
translated = Look(**self._2dict())
return ' '.join(value for value in (
f'{color_prefix} {translated.bordercolor};' if self.bordercolor and color_prefix else None,
f'background-color: {translated.bgcolor};' if self.bgcolor and include_all else None,
f'color: {translated.fontcolor};' if self.fontcolor and include_all else None,
f'font-family: {self.fontname};' if self.fontname and include_all else None,
f'font-size: {self.fontsize}pt;' if self.fontsize and include_all else None,
) if value)
DEFAULT_LOOK = Look(
bordercolor = 'BK',
bgcolor = 'WH',
fontcolor = 'BK',
fontname = 'arial',
fontsize = 14,
)
@dataclass
class Options(Look):
node: Look = field(default_factory=dict)
connector: Look = field(default_factory=dict)
cable: Look = field(default_factory=dict)
bundle: Look = field(default_factory=dict)
color_mode: ColorMode = 'SHORT' color_mode: ColorMode = 'SHORT'
mini_bom_mode: bool = True mini_bom_mode: bool = True
def __post_init__(self): def __post_init__(self):
if not self.bgcolor_node: # Build initialization dicts with default values followed by dict entries from YAML input.
self.bgcolor_node = self.bgcolor self.node = Look(**{**self.lookdict(), **self.node})
if not self.bgcolor_connector: self.connector = Look(**{**asdict(self.node), **self.connector})
self.bgcolor_connector = self.bgcolor_node self.cable = Look(**{**asdict(self.node), **self.cable})
if not self.bgcolor_cable: self.bundle = Look(**{**asdict(self.cable), **self.bundle})
self.bgcolor_cable = self.bgcolor_node
if not self.bgcolor_bundle:
self.bgcolor_bundle = self.bgcolor_cable
@dataclass @dataclass
@ -67,16 +111,19 @@ class Image:
src: str src: str
scale: Optional[ImageScale] = None scale: Optional[ImageScale] = None
# Attributes of the image cell <td> containing the image: # Attributes of the image cell <td> containing the image:
width: Optional[int] = None width: Optional[Points] = None
height: Optional[int] = None height: Optional[Points] = None
fixedsize: Optional[bool] = None fixedsize: Optional[bool] = None
bgcolor: Optional[Color] = None box: Optional[Look] = None
# Contents of the text cell <td> just below the image cell: # Contents of the text cell <td> just below the image cell:
caption: Optional[MultilineHypertext] = None caption: Optional[MultilineHypertext] = None
# See also HTML doc at https://graphviz.org/doc/info/shapes.html#html # See also HTML doc at https://graphviz.org/doc/info/shapes.html#html
def __post_init__(self, gv_dir): def __post_init__(self, gv_dir):
if isinstance(self.box, dict):
self.box = Look(**self.box)
if self.fixedsize is None: if self.fixedsize is None:
# Default True if any dimension specified unless self.scale also is specified. # Default True if any dimension specified unless self.scale also is specified.
self.fixedsize = (self.width or self.height) and self.scale is None self.fixedsize = (self.width or self.height) and self.scale is None
@ -109,7 +156,11 @@ class AdditionalComponent:
qty: float = 1 qty: float = 1
unit: Optional[str] = None unit: Optional[str] = None
qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None qty_multiplier: Union[ConnectorMultiplier, CableMultiplier, None] = None
bgcolor: Optional[Color] = None box: Optional[Look] = None
def __post_init__(self) -> None:
if isinstance(self.box, dict):
self.box = Look(**self.box)
@property @property
def description(self) -> str: def description(self) -> str:
@ -119,8 +170,8 @@ class AdditionalComponent:
@dataclass @dataclass
class Connector: class Connector:
name: Designator name: Designator
bgcolor: Optional[Color] = None box: Optional[Look] = None
bgcolor_title: Optional[Color] = None title: Optional[Look] = None
manufacturer: Optional[MultilineHypertext] = None manufacturer: Optional[MultilineHypertext] = None
mpn: Optional[MultilineHypertext] = None mpn: Optional[MultilineHypertext] = None
supplier: Optional[MultilineHypertext] = None supplier: Optional[MultilineHypertext] = None
@ -147,6 +198,10 @@ class Connector:
def __post_init__(self) -> None: def __post_init__(self) -> None:
if isinstance(self.box, dict):
self.box = Look(**self.box)
if isinstance(self.title, dict):
self.title = Look(**self.title)
if isinstance(self.image, dict): if isinstance(self.image, dict):
self.image = Image(**self.image) self.image = Image(**self.image)
@ -205,8 +260,8 @@ class Connector:
@dataclass @dataclass
class Cable: class Cable:
name: Designator name: Designator
bgcolor: Optional[Color] = None box: Optional[Look] = None
bgcolor_title: Optional[Color] = None title: Optional[Look] = None
manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None
mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None
supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None
@ -235,6 +290,10 @@ class Cable:
def __post_init__(self) -> None: def __post_init__(self) -> None:
if isinstance(self.box, dict):
self.box = Look(**self.box)
if isinstance(self.title, dict):
self.title = Look(**self.title)
if isinstance(self.image, dict): if isinstance(self.image, dict):
self.image = Image(**self.image) self.image = Image(**self.image)

View File

@ -12,7 +12,7 @@ from wireviz import wv_colors, __version__, APP_NAME, APP_URL
from wireviz.DataClasses import Metadata, Options, Tweak, Connector, Cable from wireviz.DataClasses import Metadata, Options, Tweak, Connector, Cable
from wireviz.wv_colors import get_color_hex, translate_color from wireviz.wv_colors import get_color_hex, translate_color
from wireviz.wv_gv_html import nested_html_table, \ from wireviz.wv_gv_html import nested_html_table, \
html_bgcolor_attr, html_bgcolor, html_colorbar, \ html_cell, html_colorbar, \
html_image, html_caption, remove_links, html_line_breaks html_image, html_caption, remove_links, html_line_breaks
from wireviz.wv_bom import pn_info_string, component_table_entry, \ from wireviz.wv_bom import pn_info_string, component_table_entry, \
get_additional_component_table, bom_list, generate_bom, \ get_additional_component_table, bom_list, generate_bom, \
@ -97,17 +97,16 @@ class Harness:
dot.body.append(f'// {APP_URL}') dot.body.append(f'// {APP_URL}')
dot.attr('graph', rankdir='LR', dot.attr('graph', rankdir='LR',
ranksep='2', ranksep='2',
bgcolor=wv_colors.translate_color(self.options.bgcolor, "HEX"),
nodesep='0.33', nodesep='0.33',
fontname=self.options.fontname) **self.options.graph_args())
dot.attr('node', dot.attr('node', shape='none',
shape='none',
width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
style='filled', style='filled',
fillcolor=wv_colors.translate_color(self.options.bgcolor_node, "HEX"), width='0', height='0', margin='0', # Actual size of the node is entirely determined by the label.
fontname=self.options.fontname) **self.options.node.node_args())
dot.attr('edge', style='bold', dot.attr('edge', style='bold',
fontname=self.options.fontname) **self.options.node_args())
wire_border_hex = wv_colors.get_color_hex(self.options.bordercolor)[0]
# prepare ports on connectors depending on which side they will connect # prepare ports on connectors depending on which side they will connect
for _, cable in self.cables.items(): for _, cable in self.cables.items():
@ -125,7 +124,7 @@ class Harness:
html = [] html = []
rows = [[f'{html_bgcolor(connector.bgcolor_title)}{remove_links(connector.name)}' rows = [[html_cell(connector.title, remove_links(connector.name))
if connector.show_name else None], if connector.show_name else None],
[pn_info_string(HEADER_PN, None, remove_links(connector.pn)), [pn_info_string(HEADER_PN, None, remove_links(connector.pn)),
html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)), html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)),
@ -140,7 +139,7 @@ class Harness:
[html_caption(connector.image)]] [html_caption(connector.image)]]
rows.extend(get_additional_component_table(self, connector)) rows.extend(get_additional_component_table(self, connector))
rows.append([html_line_breaks(connector.notes)]) rows.append([html_line_breaks(connector.notes)])
html.extend(nested_html_table(rows, html_bgcolor_attr(connector.bgcolor))) html.extend(nested_html_table(rows, connector.box))
if connector.style != 'simple': if connector.style != 'simple':
pinhtml = [] pinhtml = []
@ -175,10 +174,11 @@ class Harness:
html = '\n'.join(html) html = '\n'.join(html)
dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled', dot.node(connector.name, label=f'<\n{html}\n>', shape='box', style='filled',
fillcolor=translate_color(self.options.bgcolor_connector, "HEX")) **self.options.connector.node_args())
if len(connector.loops) > 0: if len(connector.loops) > 0:
dot.attr('edge', color='#000000:#ffffff:#000000') # TODO: Use self.options.wire.color and self.options.wire.bgcolor here?
dot.attr('edge', color=f'{wire_border_hex}:#ffffff:{wire_border_hex}')
if connector.ports_left: if connector.ports_left:
loop_side = 'l' loop_side = 'l'
loop_dir = 'w' loop_dir = 'w'
@ -210,7 +210,7 @@ class Harness:
elif cable.gauge_unit.upper() == 'AWG': elif cable.gauge_unit.upper() == 'AWG':
awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)' awg_fmt = f' ({mm2_equiv(cable.gauge)} mm\u00B2)'
rows = [[f'{html_bgcolor(cable.bgcolor_title)}{remove_links(cable.name)}' rows = [[html_cell(cable.title, remove_links(cable.name))
if cable.show_name else None], if cable.show_name else None],
[pn_info_string(HEADER_PN, None, [pn_info_string(HEADER_PN, None,
remove_links(cable.pn)) if not isinstance(cable.pn, list) else None, remove_links(cable.pn)) if not isinstance(cable.pn, list) else None,
@ -233,7 +233,7 @@ class Harness:
rows.extend(get_additional_component_table(self, cable)) rows.extend(get_additional_component_table(self, cable))
rows.append([html_line_breaks(cable.notes)]) rows.append([html_line_breaks(cable.notes)])
html.extend(nested_html_table(rows, html_bgcolor_attr(cable.bgcolor))) html.extend(nested_html_table(rows, cable.box))
wirehtml = [] wirehtml = []
wirehtml.append('<table border="0" cellspacing="0" cellborder="0">') # conductor table wirehtml.append('<table border="0" cellspacing="0" cellborder="0">') # conductor table
@ -258,10 +258,11 @@ class Harness:
wirehtml.append(f' <td><!-- {i}_out --></td>') wirehtml.append(f' <td><!-- {i}_out --></td>')
wirehtml.append(' </tr>') wirehtml.append(' </tr>')
bgcolors = ['#000000'] + get_color_hex(connection_color, pad=pad) + ['#000000'] bgcolors = [wire_border_hex] + get_color_hex(connection_color, pad=pad) + [wire_border_hex]
wirehtml.append(f' <tr>') wirehtml.append(f' <tr>')
wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">') wirehtml.append(f' <td colspan="3" border="0" cellspacing="0" cellpadding="0" port="w{i}" height="{(2 * len(bgcolors))}">')
wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">') wirehtml.append(' <table cellspacing="0" cellborder="0" border="0">')
# TODO: Reverse curved wire colors instead? Test also with empty wire colors! wv_colors.default_color ??
for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors for j, bgcolor in enumerate(bgcolors[::-1]): # Reverse to match the curved wires when more than 2 colors
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>') wirehtml.append(f' <tr><td colspan="3" cellpadding="0" height="2" bgcolor="{bgcolor if bgcolor != "" else wv_colors.default_color}" border="0"></td></tr>')
wirehtml.append(' </table>') wirehtml.append(' </table>')
@ -301,10 +302,10 @@ class Harness:
if isinstance(cable.shield, str): if isinstance(cable.shield, str):
# shield is shown with specified color and black borders # shield is shown with specified color and black borders
shield_color_hex = wv_colors.get_color_hex(cable.shield)[0] shield_color_hex = wv_colors.get_color_hex(cable.shield)[0]
attributes = f'height="6" bgcolor="{shield_color_hex}" border="2" sides="tb"' attributes = f'height="6" bgcolor="{shield_color_hex}" color="{wire_border_hex}" border="2" sides="tb"'
else: else:
# shield is shown as a thin black wire # shield is shown as a thin black wire
attributes = f'height="2" bgcolor="#000000" border="0"' attributes = f'height="2" bgcolor="{wire_border_hex}" border="0"'
wirehtml.append(f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>') wirehtml.append(f' <tr><td colspan="3" cellpadding="0" {attributes} port="ws"></td></tr>')
wirehtml.append(' <tr><td>&nbsp;</td></tr>') wirehtml.append(' <tr><td>&nbsp;</td></tr>')
@ -315,10 +316,10 @@ class Harness:
# connections # connections
for connection in cable.connections: for connection in cable.connections:
if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield if isinstance(connection.via_port, int): # check if it's an actual wire and not a shield
dot.attr('edge', color=':'.join(['#000000'] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + ['#000000'])) dot.attr('edge', color=':'.join([wire_border_hex] + wv_colors.get_color_hex(cable.colors[connection.via_port - 1], pad=pad) + [wire_border_hex]))
else: # it's a shield connection else: # it's a shield connection
# shield is shown with specified color and black borders, or as a thin black wire otherwise # shield is shown with specified color and black borders, or as a thin black wire otherwise
dot.attr('edge', color=':'.join(['#000000', shield_color_hex, '#000000']) if isinstance(cable.shield, str) else '#000000') dot.attr('edge', color=':'.join([wire_border_hex, shield_color_hex, wire_border_hex]) if isinstance(cable.shield, str) else wire_border_hex)
if connection.from_port is not None: # connect to left if connection.from_port is not None: # connect to left
from_connector = self.connectors[connection.from_name] from_connector = self.connectors[connection.from_name]
from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else '' from_port = f':p{connection.from_port+1}r' if from_connector.style != 'simple' else ''
@ -352,11 +353,10 @@ class Harness:
to_string = '' to_string = ''
html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html] html = [row.replace(f'<!-- {connection.via_port}_out -->', to_string) for row in html]
style, bgcolor = ('filled,dashed', self.options.bgcolor_bundle) if cable.category == 'bundle' else \ style, options = ('filled,dashed', self.options.bundle) if cable.category == 'bundle' else \
('filled', self.options.bgcolor_cable) ('filled', self.options.cable)
html = '\n'.join(html) html = '\n'.join(html)
dot.node(cable.name, label=f'<\n{html}\n>', shape='box', dot.node(cable.name, label=f'<\n{html}\n>', shape='box', style=style, **options.node_args())
style=style, fillcolor=translate_color(bgcolor, "HEX"))
def typecheck(name: str, value: Any, expect: type) -> None: def typecheck(name: str, value: Any, expect: type) -> None:
if not isinstance(value, expect): if not isinstance(value, expect):

View File

@ -2,6 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import argparse import argparse
from dataclasses import asdict
import os import os
from pathlib import Path from pathlib import Path
import sys import sys
@ -13,7 +14,7 @@ if __name__ == '__main__':
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from wireviz import __version__ from wireviz import __version__
from wireviz.DataClasses import Metadata, Options, Tweak from wireviz.DataClasses import DEFAULT_LOOK, Metadata, Options, Tweak
from wireviz.Harness import Harness from wireviz.Harness import Harness
from wireviz.wv_helper import expand, open_file_read from wireviz.wv_helper import expand, open_file_read
@ -37,7 +38,7 @@ def parse(yaml_input: str, file_out: (str, Path) = None, return_types: (None, st
harness = Harness( harness = Harness(
metadata = Metadata(**yaml_data.get('metadata', {})), metadata = Metadata(**yaml_data.get('metadata', {})),
options = Options(**yaml_data.get('options', {})), options = Options(**asdict(DEFAULT_LOOK), **yaml_data.get('options', {})),
tweak = Tweak(**yaml_data.get('tweak', {})), tweak = Tweak(**yaml_data.get('tweak', {})),
) )
if 'title' not in harness.metadata: if 'title' not in harness.metadata:

View File

@ -4,9 +4,9 @@ from dataclasses import asdict
from itertools import groupby from itertools import groupby
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from wireviz.DataClasses import AdditionalComponent, Cable, Color, Connector from wireviz.DataClasses import AdditionalComponent, Cable, Connector, Look
from wireviz.wv_colors import translate_color from wireviz.wv_colors import Color, translate_color
from wireviz.wv_gv_html import html_bgcolor_attr, html_line_breaks from wireviz.wv_gv_html import font_tag, html_line_breaks, table_attr
from wireviz.wv_helper import clean_whitespace from wireviz.wv_helper import clean_whitespace
BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators') BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators')
@ -35,7 +35,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto
common_args = { common_args = {
'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier),
'unit': part.unit, 'unit': part.unit,
'bgcolor': part.bgcolor, 'box': part.box,
} }
if harness.options.mini_bom_mode: if harness.options.mini_bom_mode:
id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description})) id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description}))
@ -158,7 +158,7 @@ def component_table_entry(
type: str, type: str,
qty: Union[int, float], qty: Union[int, float],
unit: Optional[str] = None, unit: Optional[str] = None,
bgcolor: Optional[Color] = None, box: Optional[Look] = None,
pn: Optional[str] = None, pn: Optional[str] = None,
manufacturer: Optional[str] = None, manufacturer: Optional[str] = None,
mpn: Optional[str] = None, mpn: Optional[str] = None,
@ -178,8 +178,8 @@ def component_table_entry(
+ (', '.join([pn for pn in part_number_list if pn]))) + (', '.join([pn for pn in part_number_list if pn])))
# format the above output as left aligned text in a single visible cell # format the above output as left aligned text in a single visible cell
# indent is set to two to match the indent in the generated html table # indent is set to two to match the indent in the generated html table
return f'''<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{html_bgcolor_attr(bgcolor)}><tr> return f'''<table border="0" cellspacing="0" cellpadding="3" cellborder="1"{table_attr(box)}><tr>
<td align="left" balign="left">{html_line_breaks(output)}</td> <td align="left" balign="left">{font_tag(box, html_line_breaks(output))}</td>
</tr></table>''' </tr></table>'''
def pn_info_string(header: str, name: Optional[str], number: Optional[str]) -> Optional[str]: def pn_info_string(header: str, name: Optional[str], number: Optional[str]) -> Optional[str]:

View File

@ -1,19 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import List, Optional, Union from typing import List, Optional, Union
import re
from wireviz.DataClasses import Color from wireviz.DataClasses import Image, Look
from wireviz.wv_colors import translate_color from wireviz.wv_colors import Color, translate_color
from wireviz.wv_helper import remove_links from wireviz.wv_helper import remove_links
def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_attrs: str = '') -> str: GvHtml = str # Graphviz HTML-like label string
# input: list, each item may be scalar or list GvHtmlX = str # Graphviz HTML-like label string possibly including a leading <tdX> tag
GvHtmlAttr = str # Attributes part of Graphviz HTML-like tag (including a leading space)
def nested_html_table(rows: List[Union[GvHtml, List[Optional[GvHtmlX]], None]], look: Optional[Look]) -> GvHtml:
# input: list, each item may be scalar or list, and look with optional table look attributes
# output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar # output: a parent table with one child table per parent item that is list, and one cell per parent item that is scalar
# purpose: create the appearance of one table, where cell widths are independent between rows # purpose: create the appearance of one table, where cell widths are independent between rows
# attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag # attributes in any leading <tdX> inside a list are injected into to the preceeding <td> tag
html = [] html = []
html.append(f'<table border="0" cellspacing="0" cellpadding="0"{table_attrs or ""}>') attr = font_attr(look)
font = f'<font{attr}>' if attr else ''
html.append(f'{font}<table border="0" cellspacing="0" cellpadding="0"{table_attr(look)}>')
for row in rows: for row in rows:
if isinstance(row, List): if isinstance(row, List):
if len(row) > 0 and any(row): if len(row) > 0 and any(row):
@ -29,23 +34,36 @@ def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_a
html.append(' <tr><td>') html.append(' <tr><td>')
html.append(f' {row}') html.append(f' {row}')
html.append(' </td></tr>') html.append(' </td></tr>')
html.append('</table>') html.append(f'</table>{"</font>" if font else ""}')
return html return html
def html_bgcolor_attr(color: Color) -> str: def table_attr(look: Optional[Look]) -> GvHtmlAttr:
"""Return attributes for bgcolor or '' if no color.""" """Return table tag attributes containing all non-empty table option values."""
return f' bgcolor="{translate_color(color, "HEX")}"' if color else '' return '' if not look else ''.join({
f' {k.replace("border", "")}="{v}"' for k,v in look._2dict().items() if v and 'font' not in k})
def html_bgcolor(color: Color, _extra_attr: str = '') -> str: def font_attr(look: Optional[Look]) -> GvHtmlAttr:
"""Return <td> attributes prefix for bgcolor or '' if no color.""" """Return font tag attributes containing all non-empty font option values."""
return f'<tdX{html_bgcolor_attr(color)}{_extra_attr}>' if color else '' attr = {k:v for k,v in look._2dict().items() if v and 'font' in k} if look else {}
return ((f' color="{attr["fontcolor"]}"' if attr.get('fontcolor') else '')
+ (f' face="{attr["fontname"]}"' if attr.get('fontname') else '')
+ (f' point-size="{attr["fontsize"]}"' if attr.get('fontsize') else ''))
def html_colorbar(color: Color) -> str: def font_tag(look: Optional[Look], text: GvHtml) -> GvHtml:
"""Return <tdX> attributes prefix for bgcolor and minimum width or None if no color.""" """Return text in Graphviz HTML font tag with all non-empty font option values."""
return html_bgcolor(color, ' width="4"') if color else None attr = font_attr(look)
return f'<font{attr}>{text}</font>' if attr and text > '' else text
def html_image(image): def html_cell(look: Optional[Look], text: GvHtml = '', attr: GvHtmlAttr = '') -> GvHtmlX:
from wireviz.DataClasses import Image """Return cell to be included in the rows list for nested_html_table()."""
return f'<tdX{attr}{table_attr(look)}>{font_tag(look, text)}'
def html_colorbar(color: Optional[Color]) -> Optional[GvHtmlX]:
"""Return colored cell to be included in the rows list for nested_html_table() or None if no color."""
return html_cell(Look(bgcolor=color), attr=' width="4"') if color else None
def html_image(image: Optional[Image]) -> Optional[GvHtmlX]:
"""Return image cell to be included in the rows list for nested_html_table() or None if no image."""
if not image: if not image:
return None return None
# The leading attributes belong to the preceeding tag. See where used below. # The leading attributes belong to the preceeding tag. See where used below.
@ -58,16 +76,14 @@ def html_image(image):
<td{html}</td> <td{html}</td>
</tr></table> </tr></table>
''' '''
return f'''<tdX{' sides="TLR"' if image.caption else ''}{html_bgcolor_attr(image.bgcolor)}{html}''' return f'''<tdX{' sides="TLR"' if image.caption else ''}{table_attr(image.box)}{html}'''
def html_caption(image): def html_caption(image: Optional[Image]) -> Optional[GvHtmlX]:
from wireviz.DataClasses import Image """Return image caption cell to be included just after the image cell or None if no caption."""
return (f'<tdX sides="BLR"{html_bgcolor_attr(image.bgcolor)}>{html_line_breaks(image.caption)}' return html_cell(image.box, html_line_breaks(image.caption), ' sides="BLR"') if image and image.caption else None
if image and image.caption else None)
def html_size_attr(image): def html_size_attr(image: Optional[Image]) -> GvHtmlAttr:
from wireviz.DataClasses import Image """Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object."""
# Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object
return ((f' width="{image.width}"' if image.width else '') return ((f' width="{image.width}"' if image.width else '')
+ (f' height="{image.height}"' if image.height else '') + (f' height="{image.height}"' if image.height else '')
+ ( ' fixedsize="true"' if image.fixedsize else '')) if image else '' + ( ' fixedsize="true"' if image.fixedsize else '')) if image else ''

View File

@ -15,9 +15,7 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]],
file.write(' <meta charset="UTF-8">\n') file.write(' <meta charset="UTF-8">\n')
file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n') file.write(f' <meta name="generator" content="{APP_NAME} {__version__} - {APP_URL}">\n')
file.write(f' <title>{metadata["title"]}</title>\n') file.write(f' <title>{metadata["title"]}</title>\n')
file.write(f'</head><body style="font-family:{options.fontname};background-color:' file.write(f'</head><body style="{options.html_style()}">\n')
f'{wv_colors.translate_color(options.bgcolor, "HEX")}">\n')
file.write(f'<h1>{metadata["title"]}</h1>\n') file.write(f'<h1>{metadata["title"]}</h1>\n')
description = metadata.get('description') description = metadata.get('description')
if description: if description:
@ -33,17 +31,18 @@ def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]],
file.write('<h2>Bill of Materials</h2>\n') file.write('<h2>Bill of Materials</h2>\n')
listy = flatten2d(bom_list) listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">\n') border = options.html_style(color_prefix="border: 1px solid", include_all=False)
file.write(f'<table style="{border} border-spacing: 0px;">\n')
file.write(' <tr>\n') file.write(' <tr>\n')
for item in listy[0]: for item in listy[0]:
file.write(f' <th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>\n') file.write(f' <th style="{border} padding: 8px; text-align:left;">{item}</th>\n')
file.write(' </tr>\n') file.write(' </tr>\n')
for row in listy[1:]: for row in listy[1:]:
file.write(' <tr>\n') file.write(' <tr>\n')
for i, item in enumerate(row): for i, item in enumerate(row):
item_str = item.replace('\u00b2', '&sup2;') item_str = item.replace('\u00b2', '&sup2;')
align = '; text-align:right' if listy[0][i] == 'Qty' else '' align = '; text-align:right' if listy[0][i] == 'Qty' else ''
file.write(f' <td style="border:1px solid #000000; padding: 4px{align}">{item_str}</td>\n') file.write(f' <td style="{border} padding: 4px{align};">{item_str}</td>\n')
file.write(' </tr>\n') file.write(' </tr>\n')
file.write('</table>\n') file.write('</table>\n')