372 lines
14 KiB
Python
372 lines
14 KiB
Python
# Author: Jan Mrázek
|
|
# License: MIT
|
|
|
|
import codecs
|
|
import os
|
|
import re
|
|
import shlex
|
|
import sys
|
|
from copy import deepcopy
|
|
from itertools import chain
|
|
from typing import List, Optional, Any, Tuple, Dict
|
|
|
|
import mistune # type: ignore
|
|
# The following try-catch is used to support mistune 0.8.4 and 2.x
|
|
try:
|
|
from mistune.plugins.table import plugin_table # type: ignore
|
|
from mistune.plugins.footnotes import plugin_footnotes # type: ignore
|
|
InlineParser = mistune.inline_parser.InlineParser
|
|
HTMLRenderer = mistune.renderers.HTMLRenderer
|
|
except ModuleNotFoundError:
|
|
InlineParser = mistune.InlineLexer
|
|
HTMLRenderer = mistune.Renderer
|
|
from . import pybars # type: ignore
|
|
import yaml
|
|
|
|
from . import mdrenderer
|
|
from .plot import find_data_file, get_global_datapaths
|
|
|
|
PKG_BASE = os.path.dirname(__file__)
|
|
|
|
def parse_pcbdraw(lexer: Any, m: re.Match[str], state: Any=None) -> Any:
|
|
text = m.group(1)
|
|
side, components = text.split("|")
|
|
components = list(map(lambda x: x.strip(), components.split(",")))
|
|
return 'pcbdraw', side, components
|
|
|
|
class PcbDrawInlineLexer(InlineParser): # type: ignore
|
|
def __init__(self, renderer: Any, **kwargs: Any) -> None:
|
|
super(PcbDrawInlineLexer, self).__init__(renderer, **kwargs)
|
|
self.enable_pcbdraw()
|
|
|
|
def enable_pcbdraw(self) -> None:
|
|
pcbdraw_pattern = (
|
|
r"\[\[" # [[
|
|
r"([\s\S]+?\|[\s\S]+?)" # side| component
|
|
r"\]\](?!\])" # ]]
|
|
)
|
|
if hasattr(self, 'register_rule'):
|
|
# mistune v2 API
|
|
self.rules.insert(3, 'pcbdraw')
|
|
self.register_rule('pcbdraw', pcbdraw_pattern, parse_pcbdraw)
|
|
else:
|
|
# mistune v0.8.4
|
|
self.rules.pcbdraw = re.compile(pcbdraw_pattern)
|
|
self.default_rules.insert(3, 'pcbdraw')
|
|
|
|
# This method is invoked by the old mistune API (i.e. v0.8.4)
|
|
# For the new API we register `parse_pcbdraw`
|
|
def output_pcbdraw(self, m: re.Match[str]) -> Any:
|
|
_, side, components = parse_pcbdraw(self, m)
|
|
return self.renderer.pcbdraw(side, components)
|
|
|
|
|
|
def Renderer(BaseRenderer, initial_components: List[str]): # type: ignore
|
|
class Tmp(BaseRenderer): # type: ignore
|
|
def __init__(self, initial_components: List[str]) -> None:
|
|
super(Tmp, self).__init__(escape=False)
|
|
self.items: List[Dict[str, Any]]= []
|
|
self.current_item: Optional[Dict[str, Any]] = None
|
|
self.active_side: str = "front"
|
|
self.visited_components: List[str] = initial_components
|
|
self.active_components: List[str] = []
|
|
|
|
def append_comment(self, html: str) -> None:
|
|
if self.current_item is not None and self.current_item["type"] == "steps":
|
|
self.items.append(self.current_item)
|
|
if self.current_item is None or self.current_item["type"] == "steps":
|
|
self.current_item = {
|
|
"is_comment": True,
|
|
"type": "comment",
|
|
"content": ""
|
|
}
|
|
self.current_item["content"] += html
|
|
|
|
def append_step(self, step: Dict[str, Any]) -> None:
|
|
if self.current_item is not None and self.current_item["type"] == "comment":
|
|
self.items.append(self.current_item)
|
|
if self.current_item is None or self.current_item["type"] == "comment":
|
|
self.current_item = {
|
|
"is_step": True,
|
|
"type": "steps",
|
|
"steps": []
|
|
}
|
|
self.current_item["steps"].append(step)
|
|
|
|
def output(self) -> List[Dict[str, Any]]:
|
|
items = self.items
|
|
if self.current_item is not None:
|
|
items.append(self.current_item)
|
|
return items
|
|
|
|
def pcbdraw(self, side: str, components: List[str]) -> str:
|
|
self.active_side = side
|
|
self.visited_components += components
|
|
self.active_components = components
|
|
return ""
|
|
|
|
def block_code(self, children: str, info: Optional[str]=None) -> Any:
|
|
retval = super(Tmp, self).block_code(children, info)
|
|
self.append_comment(retval)
|
|
return retval
|
|
|
|
def block_quote(self, text: str) -> Any:
|
|
retval = super(Tmp, self).block_quote(text)
|
|
self.append_comment(retval)
|
|
return retval
|
|
|
|
def block_html(self, html: str) -> Any:
|
|
retval = super(Tmp, self).block_html(html)
|
|
self.append_comment(retval)
|
|
return retval
|
|
|
|
def heading(self, children: str, level: int) -> Any:
|
|
retval = super(Tmp, self).heading(children, level)
|
|
self.append_comment(retval)
|
|
return retval
|
|
|
|
# Mistune 0.8.4 API
|
|
def header(self, text: str, level: int, raw: Optional[str]=None) -> Any:
|
|
retval = super(Tmp, self).header(text, level, raw)
|
|
self.append_comment(retval)
|
|
return retval
|
|
|
|
# Mistune 0.8.4 API
|
|
def hrule(self) -> Any:
|
|
retval = super(Tmp, self).hrule()
|
|
self.append_comment(retval)
|
|
return retval
|
|
|
|
def thematic_break(self) -> Any:
|
|
retval = super(Tmp, self).thematic_break()
|
|
self.append_comment(retval)
|
|
return retval
|
|
|
|
def list(self, text: Any, ordered: bool, level: Any=None, start: Any=None) -> str:
|
|
return ""
|
|
|
|
def list_item(self, text: str, level: Any=None) -> str:
|
|
step = {
|
|
"side": self.active_side,
|
|
"components": self.visited_components,
|
|
"active_components": self.active_components,
|
|
"comment": text
|
|
}
|
|
self.append_step(deepcopy(step))
|
|
return ""
|
|
|
|
def paragraph(self, text: str) -> Any:
|
|
retval = super(Tmp, self).paragraph(text)
|
|
self.append_comment(retval)
|
|
return retval
|
|
|
|
def table(self, header: str, body: str) -> Any:
|
|
retval = super(Tmp, self).table(header, body)
|
|
self.append_comment(retval)
|
|
return retval
|
|
return Tmp(initial_components)
|
|
|
|
def load_content(filename: str) -> Tuple[Optional[Dict[str, Any]], str]:
|
|
header = None
|
|
with codecs.open(filename, encoding="utf-8") as f:
|
|
content = f.read()
|
|
if content.startswith("---"):
|
|
end = content.find("...")
|
|
if end != -1:
|
|
header = yaml.safe_load(content[3:end])
|
|
content = content[end+3:]
|
|
return header, content
|
|
|
|
def parse_content(renderer: Any, content: str) -> List[Dict[str, Any]]:
|
|
lexer = PcbDrawInlineLexer(renderer)
|
|
processor = mistune.Markdown(renderer=renderer, inline=lexer)
|
|
try:
|
|
plugin_table(processor)
|
|
plugin_footnotes(processor)
|
|
except NameError:
|
|
# Mistune v0.8.4 doesn't define the above functions
|
|
pass
|
|
processor(content)
|
|
return renderer.output() # type: ignore
|
|
|
|
def read_template(filename: str) -> str:
|
|
with codecs.open(filename, encoding="utf-8") as f:
|
|
return f.read()
|
|
|
|
def generate_html(template: str, input: List[Dict[str, Any]]) -> bytes:
|
|
input_dict = {
|
|
"items": input
|
|
}
|
|
template_fn = pybars.Compiler().compile(template)
|
|
return template_fn(input_dict).encode("utf-8") # type: ignore
|
|
|
|
def generate_markdown(input: List[Dict[str, Any]]) -> bytes:
|
|
output = ""
|
|
for item in input:
|
|
if item["type"] == "comment":
|
|
output += item["content"] + "\n"
|
|
else:
|
|
for x in item["steps"]:
|
|
output += "#### " + x["comment"] + "\n\n"
|
|
output += "\n\n"
|
|
return output.encode("utf-8")
|
|
|
|
|
|
def generate_images(content: List[Dict[str, Any]], boardfilename: str,
|
|
plot_args: List[str], name: str, outdir: str) -> List[Dict[str, Any]]:
|
|
dir = os.path.dirname(os.path.join(outdir, name))
|
|
if not os.path.exists(dir):
|
|
os.makedirs(dir)
|
|
counter = 0
|
|
for item in content:
|
|
if item["type"] != "steps":
|
|
continue
|
|
for x in item["steps"]:
|
|
counter += 1
|
|
filename = name.format(counter)
|
|
generate_image(boardfilename, x["side"], x["components"],
|
|
x["active_components"], plot_args, os.path.join(outdir, filename))
|
|
x["img"] = filename
|
|
return content
|
|
|
|
def generate_image(boardfilename: str, side: str, components: List[str],
|
|
active: List[str], plot_args: List[str], outputfile: str) -> None:
|
|
from copy import deepcopy
|
|
|
|
from .ui import plot
|
|
|
|
plot_args = deepcopy(plot_args)
|
|
|
|
if side.startswith("back"):
|
|
plot_args += ["--side", "back"]
|
|
plot_args += ["--filter", ",".join(components)]
|
|
plot_args += ["--highlight", ",".join(active)]
|
|
plot_args += [boardfilename, outputfile]
|
|
try:
|
|
plot.main(args=plot_args)
|
|
except SystemExit as e:
|
|
if e.code is not None and e.code != 0:
|
|
raise e from None
|
|
|
|
def get_data_path() -> List[str]:
|
|
paths: List[str] = []
|
|
paths += filter(lambda x: len(x) > 0, os.environ.get("PCBDRAW_LIB_PATH", "").split(":"))
|
|
paths += [os.path.join(PKG_BASE, "resources", "templates")]
|
|
paths += get_global_datapaths()
|
|
return paths
|
|
|
|
def prepare_params(params: List[str]) -> List[str]:
|
|
p = [shlex.split(x) for x in params]
|
|
return list(chain(*p))
|
|
|
|
|
|
# Added for Kibot
|
|
# The rendender selection, they are imported here
|
|
def create_renderer(format, initial_components):
|
|
if format == "html":
|
|
return Renderer(HTMLRenderer, initial_components) # type: ignore
|
|
return Renderer(mdrenderer.MdRenderer, initial_components) # type: ignore
|
|
|
|
|
|
# The helper to look for a file, to avoid pulling LXML from pcbdraw
|
|
def find_data_file(name: str, extension: str, data_paths: List[str], subdir: Optional[str]=None) -> Optional[str]:
|
|
if not name.endswith(extension):
|
|
name += extension
|
|
if os.path.isfile(name):
|
|
return name
|
|
for path in data_paths:
|
|
if subdir is not None:
|
|
fname = os.path.join(path, subdir, name)
|
|
if os.path.isfile(fname):
|
|
return fname
|
|
fname = os.path.join(path, name)
|
|
if os.path.isfile(fname):
|
|
return fname
|
|
return None
|
|
|
|
# @click.command()
|
|
# @click.argument("input", type=click.Path(exists=True, file_okay=True, dir_okay=False))
|
|
# @click.argument("output", type=click.Path(file_okay=False, dir_okay=True))
|
|
# @click.option("--board", "-b", type=click.Path(exists=True, file_okay=True, dir_okay=False),
|
|
# default=None, help="override input board")
|
|
# @click.option("--imgname", "-t", type=str, default=None,
|
|
# help="overide image name template, should contain exactly one {{}}")
|
|
# @click.option("--template", "-t", type=str, default=None,
|
|
# help="override handlebars template for HTML output")
|
|
# @click.option("--type", "-t", type=click.Choice(["md", "html"]), default=None,
|
|
# help="override output type: markdown or HTML")
|
|
# def populate(input: str, output: str, board: Optional[str], imgname: Optional[str],
|
|
# template: Optional[str], type: Optional[str]) -> None:
|
|
# """
|
|
# Create assembly step-by-step guides
|
|
# """
|
|
#
|
|
# app = fakeKiCADGui()
|
|
#
|
|
# data_path = get_data_path()
|
|
# try:
|
|
# header, content = load_content(input)
|
|
# except IOError:
|
|
# sys.exit("Cannot open source file " + input)
|
|
#
|
|
# # We change board and output paths to absolute; then we change working
|
|
# # directory to the input file so we resolve everything according to it
|
|
# if board is not None:
|
|
# board = os.path.realpath(board)
|
|
# outputpath = os.path.realpath(output)
|
|
# input_dir = os.path.dirname(input)
|
|
# if input_dir != '':
|
|
# os.chdir(input_dir)
|
|
#
|
|
# # If no overriding is specified, load it from the template
|
|
# try:
|
|
# if board is None:
|
|
# if header is None:
|
|
# raise KeyError("board")
|
|
# board = header["board"]
|
|
# if imgname is None:
|
|
# if header is None:
|
|
# raise KeyError("imgname")
|
|
# imgname = header["imgname"]
|
|
# if type is None:
|
|
# if header is None:
|
|
# raise KeyError("type")
|
|
# type = header["type"]
|
|
# if template is None and type == "html":
|
|
# if header is None:
|
|
# raise KeyError("template")
|
|
# template = header["template"]
|
|
# except KeyError as e:
|
|
# sys.exit(f"Missing parameter {e} either in template file or source header")
|
|
#
|
|
# if type == "html":
|
|
# renderer = Renderer(HTMLRenderer, header.get("initial_components", [])) # type: ignore
|
|
# outputfile = "index.html"
|
|
# try:
|
|
# assert template is not None
|
|
# template_file = find_data_file(template, '.handlebars', data_path)
|
|
# if template_file is None:
|
|
# raise RuntimeError(f"Cannot find template '{template}'")
|
|
# template = read_template(template_file)
|
|
# except IOError:
|
|
# sys.exit("Cannot open template file " + str(template))
|
|
# else:
|
|
# renderer = Renderer(pcbdraw.mdrenderer.MdRenderer, header.get("initial_components", [])) # type: ignore
|
|
# outputfile = "index.md"
|
|
# parsed_content = parse_content(renderer, content)
|
|
# if header is None:
|
|
# raise RuntimeError("Parameters were not specified in the template")
|
|
# parsed_content = generate_images(parsed_content, board, prepare_params(header["params"]),
|
|
# imgname, outputpath)
|
|
# if type == "html":
|
|
# assert template is not None
|
|
# output_content = generate_html(template, parsed_content)
|
|
# else:
|
|
# output_content = generate_markdown(parsed_content)
|
|
#
|
|
# with open(os.path.join(outputpath, outputfile), "wb") as f:
|
|
# f.write(output_content)
|
|
#
|
|
# if __name__ == '__main__':
|
|
# populate()
|