Implement template-based HTML output

This commit is contained in:
Daniel Rojas 2021-08-26 18:50:08 +02:00
parent 6b1e274d57
commit eae2694b5d
4 changed files with 424 additions and 44 deletions

284
src/wireviz/templates/din-6771.html generated Normal file
View File

@ -0,0 +1,284 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="generator" content="<!-- %generator% -->">
<title><!-- %title% --></title>
<style>
body {
font-family: Arial;
}
table {
border: 0.35mm solid black; /* line weight based on DIN 15 */
padding: 0;
border-bottom: 0;
border-right: 0;
border-spacing: 0mm;
}
td, th {
border: 0.25mm solid black; /* line weight based on DIN 15 */
border-top: 0;
border-left: 0;
overflow: hidden;
/* display: inline-block; */
white-space: nowrap;
font-size: 2.8mm;
}
/* Canvas size based on DIN 823 / DIN 6771 / EN ISO 5457 */
#frame {
position: relative;
border: 0.35mm solid black
}
.A4, .sheetsize_default { /* portrait */
width: 180mm;
height: 277mm;
}
.A3 { /* landscape */
width: 390mm;
height: 277mm;
}
.A2 { /* landscape */
width: 564mm;
height: 400mm;
}
#diagram {
position: relative;
top: 0;
left: 0;
max-width: 100%;
height: calc(100% - 13 * 4.25mm);
/* TODO: auto-adapt to height of title block + BOM table;
BOM table might be above (A4) or to the left (A3 and larger) of the title block */
text-align: center;
vertical-align: middle;
}
#diagram svg, #diagram img {
max-width: 95%;
max-height: 100%;
position: relative;
top: 50%;
transform: translateY(-50%);
}
#titleblock {
position: absolute;
bottom: 0mm;
right: -0mm;
}
#titleblock table {
width: 180mm;
height: 38.25mm;
}
#titleblock tr, #bom tr {
height: 4.25mm;
}
.A4 #bom { /* BOM on top of title block */
position: absolute;
bottom: 38.25mm;
right: 0;
}
.A3 #bom, .A2 #bom { /* BOM to the left of title block */
position: absolute;
bottom: 0mm;
left: 0mm;
}
#bom table {
width:180mm;
}
#bom th, td {
text-align: left;
}
#bom .bom_col_id {
text-align: center;
}
#bom .bom_col_qty {
text-align: right;
}
.name {
width: 16mm;
}
.date {
width: 10mm;
}
.revno {
text-align: center;
width: 6mm;
}
.changelog {
width: 22mm;
}
.process {
width: 18mm;
}
.title {
width: 82mm;
font-size: 5.6mm;
text-align: center;
white-space: normal;
}
.company {
font-size: 4mm;
text-align: center;
white-space: normal;
}
.partno {
font-size: 4mm;
text-align: center;
white-space: normal;
}
.sheetno {
width: 12.75mm;
text-align: center;
}
</style>
<style type="text/css" media="print">
@page {
size: auto;
margin: 0;
}
/* TODO: auto-adjust based on portrait (larger margin on left) or landscape (larger margin on top) */
#page {
margin: 10mm;
margin-left: 20mm;
}
</style>
</head>
<body>
<div id="page">
<div id="frame" class="sheetsize_default">
<div id="diagram">
<div id="description">
<!-- %description% -->
</div>
<!-- %diagram% -->
<div id="notes">
<!-- %notes% -->
</div>
</div>
<div id="bom">
<!-- %bom_reversed% -->
</div>
<div id="titleblock">
<table>
<tr>
<td class="revno"><!-- %revisions_8% --></td>
<td class="changelog"><!-- %revisions_8_changelog% --></td>
<td class="date"><!-- %revisions_8_date% --></td>
<td class="name"><!-- %revisions_8_name% --></td>
<td class="process"></td>
<td class="date">Date</td>
<td class="name">Name</td>
<td class="title" colspan="3" rowspan="5"><!-- %title% --></td>
</tr>
<tr>
<td class="revno"><!-- %revisions_7% --></td>
<td><!-- %revisions_7_changelog% --></td>
<td><!-- %revisions_7_date% --></td>
<td><!-- %revisions_7_name% --></td>
<td><!-- %authors_1% --></td>
<td><!-- %authors_1_date% --></td>
<td><!-- %authors_1_name% --></td>
</tr>
<tr>
<td class="revno"><!-- %revisions_6% --></td>
<td><!-- %revisions_6_changelog% --></td>
<td><!-- %revisions_6_date% --></td>
<td><!-- %revisions_6_name% --></td>
<td><!-- %authors_2% --></td>
<td><!-- %authors_2_date% --></td>
<td><!-- %authors_2_name% --></td>
</tr>
<tr>
<td class="revno"><!-- %revisions_5% --></td>
<td><!-- %revisions_5_changelog% --></td>
<td><!-- %revisions_5_date% --></td>
<td><!-- %revisions_5_name% --></td>
<td><!-- %authors_3% --></td>
<td><!-- %authors_3_date% --></td>
<td><!-- %authors_3_name% --></td>
</tr>
<tr>
<td class="revno"><!-- %revisions_4% --></td>
<td><!-- %revisions_4_changelog% --></td>
<td><!-- %revisions_4_date% --></td>
<td><!-- %revisions_4_name% --></td>
<td colspan="2"></td>
<td></td>
</tr>
<tr>
<td class="revno"><!-- %revisions_3% --></td>
<td><!-- %revisions_3_changelog% --></td>
<td><!-- %revisions_3_date% --></td>
<td><!-- %revisions_3_name% --></td>
<td class="company" colspan="3" rowspan="3"><!-- %company% --></td>
<td class="partno" colspan="2" rowspan="3"><!-- %pn% --></td>
<td class="sheetno" rowspan="2">Sheet<br /><!-- %sheet_current% --></td>
</tr>
<tr>
<td class="revno"><!-- %revisions_2% --></td>
<td><!-- %revisions_2_changelog% --></td>
<td><!-- %revisions_2_date% --></td>
<td><!-- %revisions_2_name% --></td>
</tr>
<tr>
<td class="revno"><!-- %revisions_1% --></td>
<td><!-- %revisions_1_changelog% --></td>
<td><!-- %revisions_1_date% --></td>
<td><!-- %revisions_1_name% --></td>
<td class="sheetno">of <!-- %sheet_total% --></td>
</tr>
<tr>
<td>Rev</td>
<td>Changelog</td>
<td>Date</td>
<td>Name</td>
<td colspan="3"></td>
<td></td>
<td colspan="2"></td>
</tr>
</table>
</div> <!-- /titleblock -->
</div> <!-- /frame -->
</div> <!-- /page -->
</body>
</html>

45
src/wireviz/templates/simple.html generated Normal file
View File

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8">
<meta name="generator" content="<!-- %generator% -->">
<title><!-- %title% --></title>
<style>
#bom table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
#bom th, td {
padding: 4px;
text-align: left;
}
.bom_col_qty {
text-align: right;
}
</style>
</head><body style="font-family:arial;background-color:#FFFFFF">
<h1><!-- %title% --></h1>
<h2>Diagram</h2>
<div id="description">
<!-- %description% -->
</div>
<div id="diagram">
<!-- %diagram% -->
</div>
<div id="notes">
<!-- %notes% -->
</div>
<h2>Bill of Materials</h2>
<div id="bom">
<!-- %bom% -->
</div>
</body></html>

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import List from typing import List
from pathlib import Path
import re import re
awg_equiv_table = { awg_equiv_table = {
@ -134,3 +135,21 @@ def aspect_ratio(image_src):
except Exception as error: except Exception as error:
print(f'aspect_ratio(): {type(error).__name__}: {error}') print(f'aspect_ratio(): {type(error).__name__}: {error}')
return 1 # Assume 1:1 when unable to read actual image size return 1 # Assume 1:1 when unable to read actual image size
def smart_file_resolve(filename, possible_paths):
filename = Path(filename)
if filename.is_absolute():
if filename.exists():
return filename
else:
raise Exception(f'{filename} does not exist.')
else: # search all possible paths in decreasing order of precedence
possible_paths = [Path(path).resolve() for path in possible_paths]
for possible_path in possible_paths:
resolved_path = (possible_path / filename).resolve()
if (resolved_path).exists():
return resolved_path
else:
raise Exception(f'{filename} was not found in any of the following locations: \n' +
'\n'.join([str(x) for x in possible_paths]))

View File

@ -1,54 +1,86 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pathlib import Path from pathlib import Path
from typing import List, Union from typing import Dict, List, Union
import re import re
from wireviz import __version__, APP_NAME, APP_URL, wv_colors from wireviz import __version__, APP_NAME, APP_URL, wv_colors
from wireviz.DataClasses import Metadata, Options from wireviz.DataClasses import Metadata, Options
from wireviz.wv_helper import flatten2d, open_file_read, open_file_write from wireviz.wv_helper import flatten2d, open_file_read, open_file_write, smart_file_resolve
from wireviz.wv_gv_html import html_line_breaks
def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], metadata: Metadata, options: Options): def generate_html_output(filename: Union[str, Path], bom_list: List[List[str]], metadata: Metadata, options: Options):
# load HTML template
if 'name' in metadata.get('template',{}):
# 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'{metadata["template"]["name"]}.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'
with open_file_read(templatefile) as file:
html = file.read()
# embed SVG diagram
with open_file_read(f'{filename}.svg') as file:
svgdata = file.read()
svgdata = re.sub(
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>',
'<!-- XML and DOCTYPE declarations from SVG file removed -->',
svgdata, 1)
html = html.replace('<!-- %diagram% -->', svgdata)
# 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>\n' + bom_header_html + ''.join(bom_contents) + '</table>\n'
bom_html_reversed = '<table>\n' + ''.join(list(reversed(bom_contents))) + bom_header_html + '</table>\n'
# insert BOM table
html = html.replace('<!-- %bom% -->', bom_html)
html = html.replace('<!-- %bom_reversed% -->', bom_html_reversed)
# insert generator
html = html.replace('<!-- %generator% -->', f'{APP_NAME} {__version__} - {APP_URL}')
# insert other metadata
if metadata:
html = html.replace(f'"sheetsize_default"', '"{}"'.format(metadata.get('template',{}).get('sheetsize', ''))) # include quotes so no replacement happens within <style> definition
# TODO: handle multi-page documents
html = html.replace('<!-- %sheet_current% -->', '1')
html = html.replace('<!-- %sheet_total% -->', '1')
# fill out other generic metadata
for item, contents in metadata.items():
if isinstance(contents, (str, int, float)):
html = html.replace(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):
html = html.replace(f'<!-- %{item}_{index+1}% -->', str(category))
for entry_key, entry_value in entry.items():
html = html.replace(f'<!-- %{item}_{index+1}_{entry_key}% -->', html_line_breaks(str(entry_value)))
with open_file_write(f'{filename}.html') as file: with open_file_write(f'{filename}.html') as file:
file.write('<!DOCTYPE html>\n') file.write(html)
file.write('<html lang="en"><head>\n')
file.write(' <meta charset="UTF-8">\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'</head><body style="font-family:{options.fontname};background-color:'
f'{wv_colors.translate_color(options.bgcolor, "HEX")}">\n')
file.write(f'<h1>{metadata["title"]}</h1>\n')
description = metadata.get('description')
if description:
file.write(f'<p>{description}</p>\n')
file.write('<h2>Diagram</h2>\n')
with open_file_read(f'{filename}.svg') as svg:
file.write(re.sub(
'^<[?]xml [^?>]*[?]>[^<]*<!DOCTYPE [^>]*>',
'<!-- XML and DOCTYPE declarations from SVG file removed -->',
svg.read(1024), 1))
for svgdata in svg:
file.write(svgdata)
file.write('<h2>Bill of Materials</h2>\n')
listy = flatten2d(bom_list)
file.write('<table style="border:1px solid #000000; font-size: 14pt; border-spacing: 0px">\n')
file.write(' <tr>\n')
for item in listy[0]:
file.write(f' <th style="text-align:left; border:1px solid #000000; padding: 8px">{item}</th>\n')
file.write(' </tr>\n')
for row in listy[1:]:
file.write(' <tr>\n')
for i, item in enumerate(row):
item_str = item.replace('\u00b2', '&sup2;')
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(' </tr>\n')
file.write('</table>\n')
notes = metadata.get('notes')
if notes:
file.write(f'<h2>Notes</h2>\n<p>{notes}</p>\n')
file.write('</body></html>\n')