import argparse from docxtpl import DocxTemplate, RichText import markdown from novel_stats.novel_stats import count_words import tempfile import MarkdownPP import json from lxml import etree from importlib.resources import path TITLE_MARKER = '# ' AUTHOR_MARKER = '### ' CHAPTER_MARKER = '## ' STATUS_MARKER = '[status]: # ' ACT_MARKER = '[act]: # ' COMMENT_MARKER = '[//]: # ' # Strandard markdown comment marker, supported by pandoc and calibre's ebook-convert class Chapter: def __init__(self, heading): self.heading = heading self.paragraphs = [] class ParseTarget: TAGS = {'em':'italic', 'strong':'bold'} def __init__(self): self.cur = {key: False for key in self.TAGS} self.par = RichText() def start(self, tag, attrib): if tag in self.TAGS: self.cur[tag] = True def end(self, tag): if tag in self.TAGS: self.cur[tag] = False def data(self, data): tags = {self.TAGS[tag]:self.cur[tag] for tag in self.TAGS} self.par.add(data, **tags) def close(self): return self.par def md_re_parser(md_paragraph, break_mark): if md_paragraph == break_mark: return None html = markdown.markdown(md_paragraph) target = ParseTarget() parser = etree.XMLParser(target=target) par = etree.XML(html, parser) return par def novel_parser(source_file, context = None): if not context: context = {'author_address': 'Street\nTown, State ZIP\nCountry', 'author_email': 'name@email.com', 'author_phone': 'PhoneNumber(s)', 'author_website': 'https://www.author.com', 'md_break_mark': '-*-', 'docx_break_mark': '#'} context['chapters'] = [] wc = 0 chapter = Chapter('') for line in source_file: if line.startswith(TITLE_MARKER): title = line[len(TITLE_MARKER):].strip('()\n') context['project_title'] = title wc += count_words(title) elif line.startswith(AUTHOR_MARKER): author_name = line[len(AUTHOR_MARKER):].strip('()\n') if 'author_name' not in context: context['author_name'] = author_name context['penname'] = author_name elif line.startswith(CHAPTER_MARKER): if chapter.heading or chapter.paragraphs: context['chapters'].append(chapter) chapter = Chapter(line[len(CHAPTER_MARKER):].strip('()\n')) wc += count_words(chapter.heading) elif line.startswith(STATUS_MARKER) or line.startswith(ACT_MARKER) or line.startswith(COMMENT_MARKER): pass else: stripped = line.strip() if stripped: wc += count_words(stripped) chapter.paragraphs.append(md_re_parser(stripped, context['md_break_mark'])) context['chapters'].append(chapter) source_file.close() context['wc_1000'] = f'{(wc//1000)*1000:,}' if 'header' not in context: context['header'] = context['author_name'].split()[-1] + ' - ' + context['project_title'] + ' - ' return context def main(): parser = argparse.ArgumentParser() parser.add_argument( 'markdown_file', type=argparse.FileType('r'), help='The markdown file for the novel, main file if a multi-file novel', ) parser.add_argument( '-pp', action='store_true', help='run markdown pre-processor, this allows for a multi-file input (e.g. each chapter in its own file), but requires the MarkdownPP python library', ) parser.add_argument( '--settings', '-s', type=argparse.FileType('r'), help='setting json file', ) parser.add_argument( '--template', '-t', type=argparse.FileType('r'), help='template docx file', ) parser.add_argument( '--output', '-o', type=argparse.FileType('w'), help='output docx file', ) arguments = parser.parse_args() if arguments.template: arguments.template.close() doc = DocxTemplate(arguments.template.name) else: with path('novel_compiler.templates', 'novel.docx') as novel_template: doc = DocxTemplate(novel_template) if arguments.pp: mdfile = tempfile.TemporaryFile(mode='w+') MarkdownPP.MarkdownPP( input=arguments.markdown_file, output=mdfile, modules=list(MarkdownPP.modules) ) mdfile.seek(0) else: mdfile = arguments.markdown_file if arguments.settings: context = json.load(arguments.settings) else: context = None context = novel_parser(mdfile, context = context) doc.render(context) arguments.output.close() doc.save(arguments.output.name) if __name__ == '__main__': main()