#!/usr/bin/env python3 import argparse import copy import os import re import subprocess import sys import tempfile import CommonMark_bkrs as CommonMark import yaml def format_keyword(line): words = line.split(' ') keyword = words[0] return '*{}* '.format(keyword) + line[len(keyword):] def indent(line): prefix = ' ' * 4 return '>{}{}'.format(prefix, line) def format_match(keyword, line, m): n = len(m.groups()) if n > 0: end = 0 parts = [] for i in range(1, n+1): parts.append(line[end:m.start(i)]) thispart = line[m.start(i) : m.end(i)] parts.append('*{}*'.format(thispart)) end = m.end(i) line = ''.join(parts) + line[m.end(n):] line = '{} {}'.format(keyword, line) return line def format_backtick(c): if c == "`": return "`" return c def format_chars(s): return ''.join(format_backtick(c) for c in s) def format_scenario_step(bind, line, prev_keyword): debug('line: %r' % line) words = line.split() if not words: return line.strip(), prev_keyword keyword = words[0] real_keyword = keyword if keyword.lower() == 'and': if prev_keyword is None: sys.exit('AND may not be used on first step in snippet') real_keyword = prev_keyword debug('keyword: %r' % keyword) line = line[len(keyword):].lstrip() debug('line: %r' % line) for b in bind: debug('consider binding %r' % b) if real_keyword not in b: debug('keyword %r not in binding' % real_keyword) continue m = re.match(b[real_keyword.lower()], line, re.I | re.M) debug('m: %r' % m) if m and m.end() == len(line): debug('match: %r' % line) debug(' : %r' % m.groupdict()) line = format_match(keyword, line, m) break else: line = '{} {}'.format(keyword, line) if not line.strip(): return line line = format_chars(line) debug('pre-indent: %r' % line) lines = line.splitlines() if len(lines) > 1: line = '\n'.join([lines[0] + ' '] + [indent(x) + ' ' for x in lines[1:]]) debug('post-indent: %r' % line) return format_keyword(line), real_keyword def count_leading_whitespace(s): n = 0 while s and s[0].isspace(): n += 1 s = s[1:] return n def skip_indent(s, indent): while indent > 0 and s and s[0].isspace(): s = s[1:] indent -= 1 return s def get_steps(lines): step = [] indent = None for line in lines: if not line.strip(): yield '\n'.join(step) step = [] indent = None elif line[0].isspace(): if indent is None: indent = count_leading_whitespace(line) line = skip_indent(line, indent) step.append(line) else: yield '\n'.join(step) step = [line] indent = None if step: yield '\n'.join(step) def format_fable_snippet(bind, lines): # debug('snippet: %r' % lines) prev_keyword = None output = [] for step in get_steps(lines): debug('step: %r' % step) ln, prev_keyword = format_scenario_step(bind, step, prev_keyword) output.append(ln) return output def is_fable_snippet(o): prefix = "```fable\n" return o.t == 'FencedCode' and o.info == 'fable' def is_heading(o): return o.t =='ATXHeader' def write_document(bind, f, o): pass def write_atxheader(bind, f, o): f.write('{} {}\n\n'.format('#' * o.level, ' '.join(o.strings))) def write_setextheader(bind, f, o): chars = { 1: '=', 2: '-', } c = chars[o.level] f.write('{}\n{}\n\n'.format(' '.join(o.strings), c * 72)) def write_paragraph(bind, f, o): for s in o.strings: f.write('{}\n'.format(s)) f.write('\n') def write_fable_snippet(bind, f, o): for line in format_fable_snippet(bind, o.strings[1:]): f.write('> {} \n'.format(line)) f.write('\n') def write_not_fable_snippet(bind, f, o): fence = o.fence_char * o.fence_length lang = o.strings[0] f.write('{}{}\n'.format(fence, lang)) for line in o.strings[1:]: f.write('{}\n'.format(line)) f.write('{}\n'.format(fence)) f.write('\n') def write_fencedcode(bind, f, o): if is_fable_snippet(o): write_fable_snippet(bind, f, o) else: write_not_fable_snippet(bind, f, o) def write_indentedcode(bind, f, o): for s in o.strings: f.write(' {}\n'.format(s)) f.write('\n') def write_horizontalrule(bind, f, o): f.write('---\n') def write_list(bind, f, o): pass def write_listitem(bind, f, o): bullet = o.list_data['bullet_char'] offset = o.list_data['marker_offset'] padding = o.list_data['padding'] prefix = '{}{} '.format(' ' * offset, bullet) cont = ' ' * padding for c in o.children: prepend = prefix for s in c.strings: f.write('{}{}\n'.format(prepend, s)) prepend = cont if o.last_line_blank: f.write('\n') return True def write_referencedef(bind, f, o): for s in o.strings: f.write('{}\n'.format(s)) writers = { 'Document': write_document, 'ATXHeader': write_atxheader, 'SetextHeader': write_setextheader, 'Paragraph': write_paragraph, 'FencedCode': write_fencedcode, 'IndentedCode': write_indentedcode, 'HorizontalRule': write_horizontalrule, 'List': write_list, 'ListItem': write_listitem, 'ReferenceDef': write_referencedef, } def write(bind, f, o): if o.t not in writers: debug('{} not known'.format(repr(o.t))) return writer = writers[o.t] return writer(bind, f, o) def walk(o, func): done = func(o) if not done: for c in o.children: walk(c, func) def infer_basename(markdowns): root, ext = os.path.splitext(markdowns[0]) if ext not in ['.md', '.mdwn']: sys.exit('Input filenames must end in .md or .mdwn') return root # This only allows one markdown filename. This is crap. But I can't # get it work with -- otherwise. What I want is for the following to # work: # ftt-docgetn --pdf foo.md bar.md -- --pandoc-arg --other-pandoc-arg # but I can't make it work. def parse_cli(): p = argparse.ArgumentParser() p.add_argument('--pdf', action='store_true') p.add_argument('--html', action='store_true') p.add_argument('markdown', nargs=1) args, pandoc_args = p.parse_known_args() return args, pandoc_args def debug(msg): if False: sys.stderr.write('DEBUG: {}\n'.format(msg)) sys.stderr.flush() def pandoc(args): argv = ['ftt-pandoc'] + args subprocess.check_call(argv) args, pandoc_args = parse_cli() text = ''.join(open(filename).read() for filename in args.markdown) basename = infer_basename(args.markdown) dirname = os.path.dirname(basename) with open(basename + '.yaml') as f: bindings = yaml.safe_load(f) start = '---\n' end = '\n...\n' if text.startswith(start): meta, text = text.split(end, 1) meta += end + '\n' else: meta = '' parser = CommonMark.DocParser() ast = parser.parse(text) if args.pdf or args.html: with tempfile.NamedTemporaryFile(mode='w', dir=dirname) as f: f.write(meta) walk(ast, lambda o: write(bindings, f, o)) f.flush() if args.pdf: pandoc(['-o', basename + '.pdf', f.name] + pandoc_args) elif args.html: pandoc(['-o', basename + '.html', f.name] + pandoc_args) else: sys.stdout.write(meta) walk(ast, lambda o: write(bindings, sys.stdout, o))