diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index be76617..a82d624 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -43,9 +43,10 @@ class Look: fontsize: Optional[Points] = None def _2dict(self) -> dict: - """Return dict of strings with color values translated to hex.""" + """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 asdict(self).items() + k:translate_color(v, "hex") if 'color' in k else str(v) + for k,v in asdict(self).items() if v is not None } def graph_args(self) -> dict: @@ -111,13 +112,16 @@ class Image: width: Optional[Points] = None height: Optional[Points] = None fixedsize: Optional[bool] = None - bgcolor: Optional[Color] = None + box: Optional[Look] = None # Contents of the text cell just below the image cell: caption: Optional[MultilineHypertext] = None # See also HTML doc at https://graphviz.org/doc/info/shapes.html#html def __post_init__(self, gv_dir): + if isinstance(self.box, dict): + self.box = Look(**self.box) + if self.fixedsize is None: # Default True if any dimension specified unless self.scale also is specified. self.fixedsize = (self.width or self.height) and self.scale is None @@ -150,7 +154,11 @@ class AdditionalComponent: qty: float = 1 unit: Optional[str] = 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 def description(self) -> str: @@ -160,8 +168,8 @@ class AdditionalComponent: @dataclass class Connector: name: Designator - bgcolor: Optional[Color] = None - bgcolor_title: Optional[Color] = None + box: Optional[Look] = None + title: Optional[Look] = None manufacturer: Optional[MultilineHypertext] = None mpn: Optional[MultilineHypertext] = None supplier: Optional[MultilineHypertext] = None @@ -188,6 +196,10 @@ class Connector: 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): self.image = Image(**self.image) @@ -246,8 +258,8 @@ class Connector: @dataclass class Cable: name: Designator - bgcolor: Optional[Color] = None - bgcolor_title: Optional[Color] = None + box: Optional[Look] = None + title: Optional[Look] = None manufacturer: Union[MultilineHypertext, List[MultilineHypertext], None] = None mpn: Union[MultilineHypertext, List[MultilineHypertext], None] = None supplier: Union[MultilineHypertext, List[MultilineHypertext], None] = None @@ -276,6 +288,10 @@ class Cable: 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): self.image = Image(**self.image) diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index 3e63974..a147503 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -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.wv_colors import get_color_hex, translate_color 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 from wireviz.wv_bom import pn_info_string, component_table_entry, \ get_additional_component_table, bom_list, generate_bom, \ @@ -124,7 +124,7 @@ class Harness: 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], [pn_info_string(HEADER_PN, None, remove_links(connector.pn)), html_line_breaks(pn_info_string(HEADER_MPN, connector.manufacturer, connector.mpn)), @@ -139,7 +139,7 @@ class Harness: [html_caption(connector.image)]] rows.extend(get_additional_component_table(self, connector)) 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': pinhtml = [] @@ -210,7 +210,7 @@ class Harness: elif cable.gauge_unit.upper() == 'AWG': 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], [pn_info_string(HEADER_PN, 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.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.append('') # conductor table diff --git a/src/wireviz/wv_bom.py b/src/wireviz/wv_bom.py index c60ea0d..b0fa387 100644 --- a/src/wireviz/wv_bom.py +++ b/src/wireviz/wv_bom.py @@ -4,9 +4,9 @@ from dataclasses import asdict from itertools import groupby from typing import Any, Dict, List, Optional, Tuple, Union -from wireviz.DataClasses import AdditionalComponent, Cable, Connector +from wireviz.DataClasses import AdditionalComponent, Cable, Connector, Look 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 BOM_COLUMNS_ALWAYS = ('id', 'description', 'qty', 'unit', 'designators') @@ -35,7 +35,7 @@ def get_additional_component_table(harness: "Harness", component: Union[Connecto common_args = { 'qty': part.qty * component.get_qty_multiplier(part.qty_multiplier), 'unit': part.unit, - 'bgcolor': part.bgcolor, + 'box': part.box, } if harness.options.mini_bom_mode: id = get_bom_index(harness.bom(), bom_entry_key({**asdict(part), 'description': part.description})) @@ -158,7 +158,7 @@ def component_table_entry( type: str, qty: Union[int, float], unit: Optional[str] = None, - bgcolor: Optional[Color] = None, + box: Optional[Look] = None, pn: Optional[str] = None, manufacturer: 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]))) # 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 - return f'''
- + return f'''
{html_line_breaks(output)}
+
{font_tag(box, html_line_breaks(output))}
''' def pn_info_string(header: str, name: Optional[str], number: Optional[str]) -> Optional[str]: diff --git a/src/wireviz/wv_gv_html.py b/src/wireviz/wv_gv_html.py index 065dd1b..f1fc0f8 100644 --- a/src/wireviz/wv_gv_html.py +++ b/src/wireviz/wv_gv_html.py @@ -1,18 +1,24 @@ # -*- coding: utf-8 -*- from typing import List, Optional, Union -import re +from wireviz.DataClasses import Image, Look from wireviz.wv_colors import Color, translate_color from wireviz.wv_helper import remove_links -def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_attrs: str = '') -> str: - # input: list, each item may be scalar or list +GvHtml = str # Graphviz HTML-like label string +GvHtmlX = str # Graphviz HTML-like label string possibly including a leading 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 # purpose: create the appearance of one table, where cell widths are independent between rows # attributes in any leading inside a list are injected into to the preceeding tag html = [] - html.append(f'') + attr = font_attr(look) + font = f'' if attr else '' + html.append(f'{font}
') for row in rows: if isinstance(row, List): if len(row) > 0 and any(row): @@ -28,23 +34,36 @@ def nested_html_table(rows: List[Union[str, List[Optional[str]], None]], table_a html.append(' ') - html.append('
') html.append(f' {row}') html.append('
') + html.append(f'{"" if font else ""}') return html -def html_bgcolor_attr(color: Color) -> str: - """Return attributes for bgcolor or '' if no color.""" - return f' bgcolor="{translate_color(color, "HEX")}"' if color else '' +def table_attr(look: Optional[Look]) -> GvHtmlAttr: + """Return table tag attributes containing all non-empty table option values.""" + return '' if not look else ''.join({ + f' {k}="{v}"' for k,v in look._2dict().items() if v and 'font' not in k}) -def html_bgcolor(color: Color, _extra_attr: str = '') -> str: - """Return attributes prefix for bgcolor or '' if no color.""" - return f'' if color else '' +def font_attr(look: Optional[Look]) -> GvHtmlAttr: + """Return font tag attributes containing all non-empty font option values.""" + 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: - """Return attributes prefix for bgcolor and minimum width or None if no color.""" - return html_bgcolor(color, ' width="4"') if color else None +def font_tag(look: Optional[Look], text: GvHtml) -> GvHtml: + """Return text in Graphviz HTML font tag with all non-empty font option values.""" + attr = font_attr(look) + return f'{text}' if attr and text > '' else text -def html_image(image): - from wireviz.DataClasses import Image +def html_cell(look: Optional[Look], text: GvHtml = '', attr: GvHtmlAttr = '') -> GvHtmlX: + """Return cell to be included in the rows list for nested_html_table().""" + return f'{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: return None # The leading attributes belong to the preceeding tag. See where used below. @@ -57,16 +76,14 @@ def html_image(image): ''' - return f'''{html_line_breaks(image.caption)}' - if image and image.caption else None) +def html_caption(image: Optional[Image]) -> Optional[GvHtmlX]: + """Return image caption cell to be included just after the image cell or None if no caption.""" + return html_cell(image.box, html_line_breaks(image.caption), ' sides="BLR"') if image and image.caption else None -def html_size_attr(image): - from wireviz.DataClasses import Image - # Return Graphviz HTML attributes to specify minimum or fixed size of a TABLE or TD object +def html_size_attr(image: Optional[Image]) -> GvHtmlAttr: + """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 '') + (f' height="{image.height}"' if image.height else '') + ( ' fixedsize="true"' if image.fixedsize else '')) if image else ''