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)