538 lines
22 KiB
Python
538 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2022-2023 Salvador E. Tropea
|
|
# Copyright (c) 2022-2023 Instituto Nacional de Tecnología Industrial
|
|
# License: GPL-3.0
|
|
# Project: KiBot (formerly KiPlot)
|
|
# The Assembly image is a composition from Pixlok and oNline Web Fonts
|
|
# The rest are KiCad icons
|
|
"""
|
|
Dependencies:
|
|
- from: RSVG
|
|
role: Create outputs preview
|
|
id: rsvg1
|
|
- from: RSVG
|
|
role: Create PNG icons
|
|
id: rsvg2
|
|
- from: Ghostscript
|
|
role: Create outputs preview
|
|
- from: ImageMagick
|
|
role: Create outputs preview
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import pprint
|
|
from shutil import copy2
|
|
from math import ceil
|
|
from struct import unpack
|
|
from tempfile import NamedTemporaryFile
|
|
from .gs import GS
|
|
from .optionable import BaseOptions
|
|
from .kiplot import config_output, get_output_dir
|
|
from .misc import W_NOTYET, W_MISSTOOL, W_NOOUTPUTS
|
|
from .registrable import RegOutput
|
|
from .macros import macros, document, output_class # noqa: F401
|
|
from . import log, __version__
|
|
|
|
logger = log.get_logger()
|
|
CAT_IMAGE = {'PCB': 'pcbnew',
|
|
'Schematic': 'eeschema',
|
|
'Compress': 'zip',
|
|
'fabrication': 'fabrication',
|
|
'export': 'export',
|
|
'assembly': 'assembly_simple',
|
|
'repair': 'repair',
|
|
'docs': 'project',
|
|
'BoM': 'bom',
|
|
'3D': '3d',
|
|
'gerber': 'gerber',
|
|
'drill': 'load_drill',
|
|
'Auxiliar': 'repair'}
|
|
EXT_IMAGE = {'gbr': 'file_gbr',
|
|
'gtl': 'file_gbr',
|
|
'gtp': 'file_gbr',
|
|
'gbo': 'file_gbr',
|
|
'gto': 'file_gbr',
|
|
'gbs': 'file_gbr',
|
|
'gbl': 'file_gbr',
|
|
'gts': 'file_gbr',
|
|
'gml': 'file_gbr',
|
|
'gm1': 'file_gbr',
|
|
'gbrjob': 'file_gerber_job',
|
|
'brd': 'file_brd',
|
|
'bz2': 'file_bz2',
|
|
'dxf': 'file_dxf',
|
|
'cad': 'file_cad',
|
|
'drl': 'file_drl',
|
|
'pdf': 'file_pdf',
|
|
'txt': 'file_txt',
|
|
'pos': 'file_pos',
|
|
'csv': 'file_csv',
|
|
'svg': 'file_svg',
|
|
'eps': 'file_eps',
|
|
'png': 'file_png',
|
|
'jpg': 'file_jpg',
|
|
'plt': 'file_plt',
|
|
'ps': 'file_ps',
|
|
'rar': 'file_rar',
|
|
'scad': 'file_scad',
|
|
'stl': 'file_stl',
|
|
'step': 'file_stp',
|
|
'stp': 'file_stp',
|
|
'wrl': 'file_wrl',
|
|
'html': 'file_html',
|
|
'css': 'file_css',
|
|
'xml': 'file_xml',
|
|
'tsv': 'file_tsv',
|
|
'xlsx': 'file_xlsx',
|
|
'xyrs': 'file_xyrs',
|
|
'xz': 'file_xz',
|
|
'gz': 'file_gz',
|
|
'tar': 'file_tar',
|
|
'zip': 'file_zip',
|
|
'kicad_pcb': 'pcbnew',
|
|
'sch': 'eeschema',
|
|
'kicad_sch': 'eeschema',
|
|
'blend': 'file_blend',
|
|
'pcb3d': 'file_pcb3d'}
|
|
for i in range(31):
|
|
n = str(i)
|
|
EXT_IMAGE['gl'+n] = 'file_gbr'
|
|
EXT_IMAGE['g'+n] = 'file_gbr'
|
|
EXT_IMAGE['gp'+n] = 'file_gbr'
|
|
CAT_REP = {'PCB': ['pdf_pcb_print', 'svg_pcb_print', 'pcb_print'],
|
|
'Schematic': ['pdf_sch_print', 'svg_sch_print']}
|
|
BIG_ICON = 256
|
|
MID_ICON = 64
|
|
OUT_COLS = 12
|
|
BIG_2_MID_REL = int(ceil(BIG_ICON/MID_ICON))
|
|
IMAGEABLES_SIMPLE = {'png', 'jpg'}
|
|
IMAGEABLES_GS = {'pdf', 'eps', 'ps'}
|
|
IMAGEABLES_SVG = {'svg'}
|
|
STYLE = """
|
|
.cat-table { margin-left: auto; margin-right: auto; }
|
|
.cat-table td { padding: 20px 24px; }
|
|
.nav-table { margin-left: auto; margin-right: auto; }
|
|
.nav-table td { padding: 20px 24px; }
|
|
.output-table {
|
|
width: 1280px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
border-collapse:
|
|
collapse;
|
|
margin-top: 5px;
|
|
margin-bottom: 4em;
|
|
font-size: 0.9em;
|
|
font-family: sans-serif;
|
|
min-width: 400px;
|
|
border-radius: 5px 5px 0 0;
|
|
overflow: hidden;
|
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
|
|
}
|
|
.output-table thead tr { background-color: #0e4e8e; color: #ffffff; text-align: left; }
|
|
.output-table th { padding: 10px 12px; }
|
|
.output-table td { padding: 5px 7px; }
|
|
.out-cell { width: 128px; text-align: center }
|
|
.out-img { text-align: center; margin-left: auto; margin-right: auto; }
|
|
.cat-img { text-align: center; margin-left: auto; margin-right: auto; }
|
|
.td-small { text-align: center; font-size: 0.6em; }
|
|
.td-normal { text-align: center; }
|
|
.generator { text-align: right; font-size: 0.6em; }
|
|
a:link, a:visited { text-decoration: none;}
|
|
a:hover, a:active { text-decoration: underline;}
|
|
"""
|
|
|
|
|
|
def _run_command(cmd):
|
|
logger.debug('- Executing: '+GS.pasteable_cmd(cmd))
|
|
try:
|
|
cmd_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error('Failed to run %s, error %d', cmd[0], e.returncode)
|
|
if e.output:
|
|
logger.debug('Output from command: '+e.output.decode())
|
|
return False
|
|
if cmd_output.strip():
|
|
logger.debug('- Output from command:\n'+cmd_output.decode())
|
|
return True
|
|
|
|
|
|
def get_png_size(file):
|
|
with open(file, 'rb') as f:
|
|
s = f.read()
|
|
if not (s[:8] == b'\x89PNG\r\n\x1a\n' and (s[12:16] == b'IHDR')):
|
|
return 0, 0
|
|
w, h = unpack('>LL', s[16:24])
|
|
return int(w), int(h)
|
|
|
|
|
|
class Navigate_ResultsOptions(BaseOptions):
|
|
def __init__(self):
|
|
with document:
|
|
self.output = GS.def_global_output
|
|
""" *Filename for the output (%i=html, %x=navigate) """
|
|
self.link_from_root = ''
|
|
""" *The name of a file to create at the main output directory linking to the home page """
|
|
self.skip_not_run = False
|
|
""" Skip outputs with `run_by_default: false` """
|
|
super().__init__()
|
|
self._expand_id = 'navigate'
|
|
self._expand_ext = 'html'
|
|
|
|
def add_to_tree(self, cat, out, o_tree):
|
|
# Add `out` to `o_tree` in the `cat` category
|
|
cat = cat.split('/')
|
|
node = o_tree
|
|
for c in cat:
|
|
if c not in node:
|
|
# New one
|
|
node[c] = {}
|
|
node = node[c]
|
|
node[out.name] = out
|
|
|
|
def svg_to_png(self, svg_file, png_file, width):
|
|
cmd = [self.rsvg_command, '-w', str(width), '-f', 'png', '-o', png_file, svg_file]
|
|
return _run_command(cmd)
|
|
|
|
def copy(self, img, width):
|
|
""" Copy an SVG icon to the images/ dir.
|
|
Tries to convert it to PNG. """
|
|
img_w = "{}_{}".format(img, width)
|
|
if img_w in self.copied_images:
|
|
# Already copied, just return its name
|
|
return self.copied_images[img_w]
|
|
src = os.path.join(self.img_src_dir, img+'.svg')
|
|
dst = os.path.join(self.out_dir, 'images', img_w)
|
|
id = img_w
|
|
if self.rsvg_command is not None and self.svg_to_png(src, dst+'.png', width):
|
|
img_w += '.png'
|
|
else:
|
|
copy2(src, dst+'.svg')
|
|
img_w += '.svg'
|
|
name = os.path.join('images', img_w)
|
|
self.copied_images[id] = name
|
|
return name
|
|
|
|
def can_be_converted(self, ext):
|
|
if ext in IMAGEABLES_SVG and self.rsvg_command is None:
|
|
logger.warning(W_MISSTOOL+"Missing SVG to PNG converter")
|
|
return False
|
|
if ext in IMAGEABLES_GS and not self.ps2img_avail:
|
|
logger.warning(W_MISSTOOL+"Missing PS/PDF to PNG converter")
|
|
return False
|
|
if ext in IMAGEABLES_SIMPLE and self.convert_command is None:
|
|
logger.warning(W_MISSTOOL+"Missing ImageMagick converter")
|
|
return False
|
|
return ext in IMAGEABLES_SVG or ext in IMAGEABLES_GS or ext in IMAGEABLES_SIMPLE
|
|
|
|
def get_image_for_cat(self, cat):
|
|
img = None
|
|
# Check if we have an output that can represent this category
|
|
if cat in CAT_REP and self.convert_command is not None:
|
|
outs_rep = CAT_REP[cat]
|
|
rep_file = None
|
|
# Look in all outputs
|
|
for o in RegOutput.get_outputs():
|
|
# Is this one that can be used to represent it?
|
|
if o.type in outs_rep:
|
|
out_dir = get_output_dir(o.dir, o, dry=True)
|
|
targets = o.get_targets(out_dir)
|
|
# Look the output targets
|
|
for tg in targets:
|
|
ext = os.path.splitext(tg)[1][1:].lower()
|
|
# Can be converted to an image?
|
|
if os.path.isfile(tg) and self.can_be_converted(ext):
|
|
rep_file = tg
|
|
break
|
|
if rep_file:
|
|
break
|
|
if rep_file:
|
|
cat, _ = self.get_image_for_file(rep_file, cat, no_icon=True)
|
|
return cat
|
|
if cat in CAT_IMAGE:
|
|
img = self.copy(CAT_IMAGE[cat], BIG_ICON)
|
|
cat_img = '<img src="{}" alt="{}" width="{}" height="{}">'.format(img, cat, BIG_ICON, BIG_ICON)
|
|
cat = ('<table class="cat-img"><tr><td>{}<br>{}</td></tr></table>'.
|
|
format(cat_img, cat))
|
|
return cat
|
|
|
|
def compose_image(self, file, ext, img, out_name, no_icon=False):
|
|
if not os.path.isfile(file):
|
|
logger.warning(W_NOTYET+"{} not yet generated, using an icon".format(os.path.relpath(file)))
|
|
return False, None, None
|
|
if self.convert_command is None:
|
|
return False, None, None
|
|
# Create a unique name using the output name and the generated file name
|
|
bfname = os.path.splitext(os.path.basename(file))[0]
|
|
fname = os.path.join(self.out_dir, 'images', out_name+'_'+bfname+'.png')
|
|
# Full path for the icon image
|
|
icon = os.path.join(self.out_dir, img)
|
|
if ext == 'pdf':
|
|
# Only page 1
|
|
file += '[0]'
|
|
if ext == 'svg':
|
|
with NamedTemporaryFile(mode='w', suffix='.png', delete=False) as f:
|
|
tmp_name = f.name
|
|
logger.debug('Temporal convert: {} -> {}'.format(file, tmp_name))
|
|
if not self.svg_to_png(file, tmp_name, BIG_ICON):
|
|
return False, None, None
|
|
file = tmp_name
|
|
cmd = [self.convert_command, file,
|
|
# Size for the big icons (width)
|
|
'-resize', str(BIG_ICON)+'x']
|
|
if not no_icon:
|
|
cmd.extend([ # Add the file type icon
|
|
icon,
|
|
# At the bottom right
|
|
'-gravity', 'south-east',
|
|
# This is a composition, not 2 images
|
|
'-composite'])
|
|
cmd.append(fname)
|
|
res = _run_command(cmd)
|
|
if ext == 'svg':
|
|
logger.debug('Removing temporal {}'.format(tmp_name))
|
|
os.remove(tmp_name)
|
|
return res, fname, os.path.relpath(fname, start=self.out_dir)
|
|
|
|
def get_image_for_file(self, file, out_name, no_icon=False):
|
|
ext = os.path.splitext(file)[1][1:].lower()
|
|
wide = False
|
|
# Copy the icon for this file extension
|
|
icon_name = 'folder' if os.path.isdir(file) else EXT_IMAGE.get(ext, 'unknown')
|
|
img = self.copy(icon_name, MID_ICON)
|
|
# Full name for the file
|
|
file_full = file
|
|
# Just the file, to display it
|
|
file = os.path.basename(file)
|
|
# The icon size
|
|
height = width = MID_ICON
|
|
# Check if this file can be represented by an image
|
|
if self.can_be_converted(ext):
|
|
# Try to compose the image of the file with the icon
|
|
ok, fimg, new_img = self.compose_image(file_full, ext, img, 'cat_'+out_name, no_icon)
|
|
if ok:
|
|
# It was converted, replace the icon by the composited image
|
|
img = new_img
|
|
# Compute its size
|
|
width, height = get_png_size(fimg)
|
|
# We are using the big size
|
|
wide = True
|
|
# Now add the image with its file name as caption
|
|
ext_img = '<img src="{}" alt="{}" width="{}" height="{}">'.format(img, file, width, height)
|
|
file = ('<table class="out-img"><tr><td>{}</td></tr><tr><td class="{}">{}</td></tr></table>'.
|
|
format(ext_img, 'td-normal' if no_icon else 'td-small', out_name if no_icon else file))
|
|
return file, wide
|
|
|
|
def add_back_home(self, f, prev):
|
|
if prev is not None:
|
|
prev += '.html'
|
|
f.write('<table class="nav-table">')
|
|
f.write(' <tr>')
|
|
f.write(' <td><a href="{}"><img src="{}" width="{}" height="{}" alt="go back"></a></td>'.
|
|
format(prev, self.back_img, MID_ICON, MID_ICON))
|
|
f.write(' <td><a href="{}"><img src="{}" width="{}" height="{}" alt="go home"></a></td>'.
|
|
format(self.home, self.home_img, MID_ICON, MID_ICON))
|
|
f.write(' </tr>')
|
|
f.write('</table>')
|
|
f.write('<p class="generator">Generated by <a href="https://github.com/INTI-CMNB/KiBot/">KiBot</a> v{}</p>'.
|
|
format(__version__))
|
|
|
|
def write_head(self, f, title):
|
|
f.write('<!DOCTYPE html>\n')
|
|
f.write('<html lang="en">\n')
|
|
f.write('<head>\n')
|
|
f.write(' <title>{}</title>\n'.format(title if title else 'Main page'))
|
|
f.write(' <meta charset="UTF-8">\n') # UTF-8 encoding for unicode support
|
|
f.write(' <link rel="stylesheet" href="styles.css">\n')
|
|
f.write(' <link rel="icon" href="favicon.ico">\n')
|
|
f.write('</head>\n')
|
|
f.write('<body>\n')
|
|
|
|
def generate_cat_page_for(self, name, node, prev, category):
|
|
logger.debug('- Categories: '+str(node.keys()))
|
|
with open(os.path.join(self.out_dir, name), 'wt') as f:
|
|
self.write_head(f, category)
|
|
name, ext = os.path.splitext(name)
|
|
# Limit to 5 categories by row
|
|
c_cats = len(node)
|
|
rows = ceil(c_cats/5.0)
|
|
by_row = c_cats/rows
|
|
acc = 0
|
|
f.write('<table class="cat-table">\n<tr>\n')
|
|
for cat, content in node.items():
|
|
if not isinstance(content, dict):
|
|
continue
|
|
if acc >= by_row:
|
|
# Flush the table and create another
|
|
acc = 0
|
|
f.write('</tr>\n</table>\n<table class="cat-table">\n<tr>\n')
|
|
pname = name+'_'+cat+ext
|
|
self.generate_page_for(content, pname, name, category+'/'+cat)
|
|
f.write(' <td><a href="{}">{}</a></td>\n'.format(pname, self.get_image_for_cat(cat)))
|
|
acc += 1
|
|
f.write('</tr>\n</table>\n')
|
|
self.generate_outputs(f, node)
|
|
self.add_back_home(f, prev)
|
|
f.write('</body>\n</html>\n')
|
|
|
|
def generate_outputs(self, f, node):
|
|
for oname, out in node.items():
|
|
if isinstance(out, dict):
|
|
continue
|
|
f.write('<table class="output-table">\n')
|
|
out_name = oname.replace(' ', '_')
|
|
oname = oname.replace('_', ' ')
|
|
oname = oname[0].upper()+oname[1:]
|
|
if out.comment:
|
|
oname += ': '+out.comment
|
|
f.write('<thead><tr><th colspan="{}">{}</th></tr></thead>\n'.format(OUT_COLS, oname))
|
|
out_dir = get_output_dir(out.dir, out, dry=True)
|
|
f.write('<tbody><tr>\n')
|
|
targets = out.get_targets(out_dir)
|
|
if len(targets) == 1:
|
|
tg_rel = os.path.relpath(os.path.abspath(targets[0]), start=self.out_dir)
|
|
img, _ = self.get_image_for_file(targets[0], out_name)
|
|
f.write('<td class="out-cell" colspan="{}"><a href="{}">{}</a></td>\n'.
|
|
format(OUT_COLS, tg_rel, img))
|
|
else:
|
|
c = 0
|
|
for tg in targets:
|
|
if c == OUT_COLS:
|
|
f.write('</tr>\n<tr>\n')
|
|
c = 0
|
|
tg_rel = os.path.relpath(os.path.abspath(tg), start=self.out_dir)
|
|
img, wide = self.get_image_for_file(tg, out_name)
|
|
# Check if we need to break this row
|
|
span = 1
|
|
if wide:
|
|
span = BIG_2_MID_REL
|
|
remain = OUT_COLS-c
|
|
if span > remain:
|
|
f.write('<td class="out-cell" colspan="{}"></td></tr>\n<tr>\n'.format(remain))
|
|
# Add a new cell
|
|
f.write('<td class="out-cell" colspan="{}"><a href="{}">{}</a></td>\n'.format(span, tg_rel, img))
|
|
c = c+span
|
|
if c < OUT_COLS:
|
|
f.write('<td class="out-cell" colspan="{}"></td>\n'.format(OUT_COLS-c))
|
|
f.write('</tr>\n')
|
|
# This row is just to ensure we have at least 1 cell in each column
|
|
f.write('<tr>\n')
|
|
for _ in range(OUT_COLS):
|
|
f.write('<td></td>\n')
|
|
f.write('</tr>\n')
|
|
f.write('</tbody>\n')
|
|
f.write('</table>\n')
|
|
|
|
def generate_end_page_for(self, name, node, prev, category):
|
|
logger.debug('- Outputs: '+str(node.keys()))
|
|
with open(os.path.join(self.out_dir, name), 'wt') as f:
|
|
self.write_head(f, category)
|
|
name, ext = os.path.splitext(name)
|
|
self.generate_outputs(f, node)
|
|
self.add_back_home(f, prev)
|
|
f.write('</body>\n</html>\n')
|
|
|
|
def generate_page_for(self, node, name, prev=None, category=''):
|
|
logger.debug('Generating page for '+name)
|
|
if isinstance(list(node.values())[0], dict):
|
|
self.generate_cat_page_for(name, node, prev, category)
|
|
else:
|
|
self.generate_end_page_for(name, node, prev, category)
|
|
|
|
def get_targets(self, out_dir):
|
|
# Listing all targets is too complex, we list the most relevant
|
|
# This is good enough to compress the result
|
|
name = self._parent.expand_filename(out_dir, self.output)
|
|
files = [os.path.join(out_dir, 'images'),
|
|
os.path.join(out_dir, 'styles.css'),
|
|
os.path.join(out_dir, 'favicon.ico')]
|
|
if self.link_from_root:
|
|
files.append(os.path.join(GS.out_dir, self.link_from_root))
|
|
self.out_dir = out_dir
|
|
self.get_html_names(self.create_tree(), name, files)
|
|
return files
|
|
|
|
def get_html_names_cat(self, name, node, prev, category, files):
|
|
files.append(os.path.join(self.out_dir, name))
|
|
name, ext = os.path.splitext(name)
|
|
for cat, content in node.items():
|
|
if not isinstance(content, dict):
|
|
continue
|
|
pname = name+'_'+cat+ext
|
|
self.get_html_names(content, pname, files, name, category+'/'+cat)
|
|
|
|
def get_html_names(self, node, name, files, prev=None, category=''):
|
|
if isinstance(list(node.values())[0], dict):
|
|
self.get_html_names_cat(name, node, prev, category, files)
|
|
else:
|
|
files.append(os.path.join(self.out_dir, name))
|
|
|
|
def create_tree(self):
|
|
o_tree = {}
|
|
for o in RegOutput.get_outputs():
|
|
if not o.run_by_default and self.skip_not_run:
|
|
# Skip outputs that aren't generated in a regular run
|
|
continue
|
|
config_output(o)
|
|
cat = o.category
|
|
if cat is None:
|
|
continue
|
|
if isinstance(cat, str):
|
|
cat = [cat]
|
|
for c in cat:
|
|
self.add_to_tree(c, o, o_tree)
|
|
return o_tree
|
|
|
|
def run(self, name):
|
|
self.out_dir = os.path.dirname(name)
|
|
self.img_src_dir = GS.get_resource_path('images')
|
|
self.img_dst_dir = os.path.join(self.out_dir, 'images')
|
|
os.makedirs(self.img_dst_dir, exist_ok=True)
|
|
self.copied_images = {}
|
|
name = os.path.basename(name)
|
|
# Create a tree with all the outputs
|
|
o_tree = self.create_tree()
|
|
logger.debug('Collected outputs:\n'+pprint.pformat(o_tree))
|
|
if not o_tree:
|
|
logger.warning(W_NOOUTPUTS+'No outputs for navigate results')
|
|
return
|
|
with open(os.path.join(self.out_dir, 'styles.css'), 'wt') as f:
|
|
f.write(STYLE)
|
|
self.rsvg_command = self.check_tool('rsvg1')
|
|
self.convert_command = self.check_tool('ImageMagick')
|
|
self.ps2img_avail = self.check_tool('Ghostscript')
|
|
# Create the pages
|
|
self.home = name
|
|
self.back_img = self.copy('back', MID_ICON)
|
|
self.home_img = self.copy('home', MID_ICON)
|
|
copy2(os.path.join(self.img_src_dir, 'favicon.ico'), os.path.join(self.out_dir, 'favicon.ico'))
|
|
self.generate_page_for(o_tree, name)
|
|
# Link it?
|
|
if self.link_from_root:
|
|
redir_file = os.path.join(GS.out_dir, self.link_from_root)
|
|
rel_start = os.path.relpath(os.path.join(self.out_dir, name), start=GS.out_dir)
|
|
logger.debug('Creating redirector: {} -> {}'.format(redir_file, rel_start))
|
|
with open(redir_file, 'wt') as f:
|
|
f.write('<html>\n<head>\n<meta http-equiv="refresh" content="0; {}"/>'.format(rel_start))
|
|
f.write('</head>\n</html>')
|
|
|
|
|
|
@output_class
|
|
class Navigate_Results(BaseOutput): # noqa: F821
|
|
""" Navigate Results
|
|
Generates a web page to navigate the generated outputs """
|
|
def __init__(self):
|
|
super().__init__()
|
|
# Make it low priority so it gets created after all the other outputs
|
|
self.priority = 10
|
|
with document:
|
|
self.options = Navigate_ResultsOptions
|
|
""" *[dict] Options for the `navigate_results` output """
|
|
# The help is inherited and already mentions the default priority
|
|
self.fix_priority_help()
|
|
|
|
@staticmethod
|
|
def get_conf_examples(name, layers):
|
|
outs = BaseOutput.simple_conf_examples(name, 'Web page to browse the results', 'Browse') # noqa: F821
|
|
outs[0]['options'] = {'link_from_root': 'index.html', 'skip_not_run': True}
|
|
return outs
|