# 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 from . 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, 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) -> Any: _, side, components = parse_pcbdraw(self, m) return self.renderer.pcbdraw(side, components) def Renderer(BaseRenderer, initial_components: List[str], format: str): # type: ignore class Tmp(BaseRenderer): # type: ignore def __init__(self, initial_components: List[str], format: 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] = [] self.found_step = False self.format = format 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 # We found a step, this isn't a regular item self.found_step = True 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 name_head(self, name: str, code: str): if format != "html": return code # Add a name to this heading so it can be referenced name = name.lower().replace(' ', '-') return f'{code}' def heading(self, children: str, level: int) -> Any: retval = super(Tmp, self).heading(children, level) self.append_comment(self.name_head(children, 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(self.name_head(text, 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: if not text: return "" retval = super(Tmp, self).list(text, ordered, level, start) if level == 1: # Add the result from the list when finished and only if not empty self.append_comment(retval) return retval def list_item(self, text: str, level: Any=None) -> str: if not self.found_step: # If we don't have an image for this step assume this is a regular list return super(Tmp, self).list_item(text, level) # Add a step step = { "side": self.active_side, "components": self.visited_components, "active_components": self.active_components, "comment": text } self.append_step(deepcopy(step)) # Reset the flag self.found_step = False 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, format) 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 += "![step](" + x["img"] + ")\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): return Renderer(HTMLRenderer if format == "html" else mdrenderer.MdRenderer, initial_components, format) # 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()