import argparse import json import os import shlex import shutil import sys from typing import List, Dict, Any sys.path.append(os.path.join(os.path.dirname(__file__), '../../../scripts')) from common import config import literate.format import literate.parse import literate.refactor import literate.render def build_arg_parser() -> argparse.ArgumentParser: common = argparse.ArgumentParser(add_help=False) common.add_argument('--project-dir', default='.', help='path to the project directory') config.add_args(common) ap = argparse.ArgumentParser( description='Process literate refactoring scripts.') subparsers = ap.add_subparsers(dest='cmd') sp = subparsers.add_parser('extract', help='extract refactoring script from a Markdown file and print it', parents=[common]) sp.add_argument('input', metavar='INPUT.md') sp = subparsers.add_parser('exec', help='extract refactoring script and run it on the project directory', parents=[common]) sp.add_argument('input', metavar='INPUT.md') sp.add_argument('--work-dir', help='copy the project into a work directory before refactoring'), sp.add_argument('-f', '--force', default=False, action='store_true', help='remove the work directory if it already exists'), sp = subparsers.add_parser('render', help='generate rendered Markdown, including a diff for every ' 'refactoring step', parents=[common]) sp.add_argument('--playground-js', help='if set, the generated markdown will include this javascript ' 'URL and call `initRefactorPlaygroundButtons` to set up ' 'playground integration') sp.add_argument('input', metavar='INPUT.md') sp.add_argument('output', metavar='OUTPUT.md') sp = subparsers.add_parser('playground', help='run a refactoring script on some code, and render a diff', parents=[common]) sp.add_argument('code', metavar='CODE.rs') sp.add_argument('script', metavar='SCRIPT.txt') sp.add_argument('output', metavar='OUTPUT.html') sp = subparsers.add_parser('playground-styles', help='print CSS styles for rendering playground diffs') return ap def do_extract(args: argparse.Namespace): with open(args.input) as f: blocks = literate.parse.parse_blocks(f) for b in blocks: if not isinstance(b, literate.parse.Script): continue for l in b.lines: sys.stdout.write(l) if l.strip() == 'commit ;': sys.stdout.write('\n') def do_exec(args: argparse.Namespace): with open(args.input) as f: blocks = literate.parse.parse_blocks(f) if args.work_dir is not None: if os.path.exists(args.work_dir): if args.force: print('removing old work dir `%s`' % args.work_dir) shutil.rmtree(args.work_dir) else: print('error: work directory `%s` already exists' % args.work_dir) sys.exit(1) print('copy project `%s` to work dir `%s`' % (args.project_dir, args.work_dir)) shutil.copytree(args.project_dir, args.work_dir) work_dir = args.work_dir else: work_dir = args.project_dir literate.refactor.exec_refactor_scripts(args, blocks, work_dir) def build_result_json(blocks: List[literate.refactor.Block]) -> Dict[str, Any]: code = [] script = [] results = [] script_acc = [] for b in blocks: if not isinstance(b, literate.refactor.RefactorCode): continue if b.parsed_old: if len(b.old) == 1: f = next(iter(b.old.values())) code.append(f.text) else: code.append(None) script_acc = [] if len(script_acc) > 0: # If there are previous commands in this block, make sure they end with # a semicolon words = shlex.split('\n'.join(script_acc)) if len(words) > 0 and words[-1] != ';': for i in reversed(range(len(script_acc))): if script_acc[i].strip() != '': script_acc[i] = script_acc[i].rstrip('\n') + ' ;\n' break # Add a blank line between blocks script_acc.append('\n') script_acc.extend(b.lines) script.append(''.join(script_acc)) results.append({ 'code_idx': len(code) - 1, 'script_idx': len(script) - 1, }) return { 'code': code, 'script': script, 'results': results, } def do_render(args: argparse.Namespace): with open(args.input) as f: blocks = literate.parse.parse_blocks(f) blocks, all_files = literate.refactor.run_refactor_scripts(args, blocks) literate.format.format_files(all_files) literate.render.prepare_files(all_files) with open(args.output, 'w') as f: f.write('\n\n') diff_idx = 0 for b in blocks: if isinstance(b, literate.refactor.Text): for line in b.lines: f.write(line) elif isinstance(b, literate.refactor.Code): f.write('```%s\n' % ' '.join(b.attrs)) for line in b.lines: f.write(line) f.write('```\n') elif isinstance(b, literate.refactor.RefactorCode): if not b.opts['hide-code']: f.write('```sh %s\n' % ' '.join(b.attrs[1:])) for line in b.lines: f.write(line) f.write('```\n\n') # Unfortunately the `pulldown-cmark` package used by `mdbook` # provides no way to set the `id` of a code block. Instead we # use this hack: we place an empty, invisible tag with an # `id` just after the block, and in the Javascript code we use # `document.getElementById(...).previousElementSibling` to get # the actual code block. f.write('\n' % diff_idx) f.write('\n\n') print('rendering diff #%d' % (diff_idx + 1)) print(' diff options: %s' % (b.opts,)) diff_text = literate.render.render_diff(b.old, b.new, b.opts) if diff_text is not None: collapse = b.opts['collapse-diff'] if collapse: f.write('
Diff #%d\n' % (diff_idx + 1)) f.write(diff_text) if collapse: f.write('\n
\n\n') diff_idx += 1 else: raise TypeError('expected Text or ScriptDiff, got %s' % (type(b),)) if args.playground_js is not None: j = build_result_json(blocks) f.write('\n' % json.dumps(j, indent=2)) f.write('' % args.playground_js) f.write('') def do_playground(args: argparse.Namespace): # Stupid hack here, because Rust `Process` doesn't support merging stdout # and stderr. sys.stderr = None os.close(2) os.dup2(1, 2) sys.stderr = sys.stdout with open(args.script) as f: script = f.read() result, all_files = literate.refactor.run_refactor_for_playground( args, script) old = result.old new = result.new literate.format.format_files(all_files) literate.render.prepare_files(all_files) opts = literate.refactor.OPT_DEFAULTS.copy() opts['show-filename'] = False opts['highlight-mode'] = 'ace' diff_text = literate.render.render_diff(old, new, opts) with open(args.output, 'w') as f: f.write(diff_text) def do_playground_styles(args: argparse.Namespace): print(literate.render.get_styles()) print(literate.render.get_pygments_styles()) def main(argv: List[str]): ap = build_arg_parser() args = ap.parse_args(argv) config.update_args(args) if args.cmd == 'extract': do_extract(args) elif args.cmd == 'exec': do_exec(args) elif args.cmd == 'render': do_render(args) elif args.cmd == 'playground': do_playground(args) elif args.cmd == 'playground-styles': do_playground_styles(args) else: if args.cmd is not None: print('unknown subcommand `%s`' % args.cmd) ap.print_usage() sys.exit(1)