Interactive Markdown Cells

My white 🐳 is literate markdown programming in the notebook.

I have made so many attempts at a literate interface to the notebook. Each of these experiments revealed important features of modern literate programming computing.

I’ve drawn the concept in packages & single notebooks. In retrospect, they contain too many opinions to be extensible.

Here is another go at it.

    import re, textwrap, tokenize, io, itertools, IPython; from toolz.curried import *
    __all__ = 'parse',

Use the mistune to parse block level markdown objects.

    class Lexer(__import__('mistune').BlockLexer):
        def parse(self, text, rules=None):
            text = ''.join(x if x.strip() else "\n" for x in text.splitlines(True))
            rules = rules or self.default_rules
            def manipulate(text):
                for key in rules:
                    m = getattr(self.rules, key).match(text)
                    if m: getattr(self, 'parse_%s' % key)(m); return m
                return False  
            while text:
                m = manipulate(text)
                if m: text = text[len(m.group(0)):]
                if not m and text: raise RuntimeError('Infinite loop at: %s' % text)
                if self.tokens: self.tokens[-1]['match'] = m
            return self.tokens
    def quote(str, prior="", tick='"""'):
        """wrap a block of text in quotes. """
        if not str.strip(): return str
        indent, outdent = len(str)-len(str.lstrip()), len(str.rstrip())
        if tick in str or str.endswith(tick[0]): tick = '"""'
        return str[:indent] + prior + tick + str[indent:outdent] + tick + ";" + str[outdent:]

parse markdown code into valid python code.

    class Parser:
        def parse_code(self, token, *, indent=-1):
            code = token['match'].group().rstrip(); stripped = code.strip()
            if stripped.startswith(('>>>',)) or (not stripped and 'match' in token): return # don't do anything for blank code.
            if code.startswith(('```',)): 
                code = ''.join(['\n'] + code.rstrip('`').splitlines(True)[1:])
                if textwrap.dedent(code) == code: code = textwrap.indent(code, ' '*max(indent, 4))
            return code
        
        def indent_text(self, body, text, code="", *, indent=-1, block_level=0):
            try: tokenized = list(tokenize.tokenize(io.BytesIO(textwrap.dedent(body).encode('utf-8')).readline)) if body else []
            except tokenize.TokenError as exception:
                if exception.args[0] == 'EOF in multi-line string': text = textwrap.indent(text, ' '*_indent)
                else: text = (text.strip() and quote or identity)(text, ' '*_indent)
            else: 
                while tokenized and not tokenized[-1].string: tokenized.pop()
                this_indent, line = indent, tokenized[-1].line if tokenized else ""
                this_indent += len(line) - len(line.lstrip())
                if body.rstrip().endswith(':'): 
                    for last in code.splitlines() or ['']:
                        if last.strip(): break
                    this_indent += max(len(last)-len(last.lstrip())-this_indent + block_level*2, 4)
                text = quote(textwrap.indent(text, ' '*this_indent)) 
            return body + text + code
        
        def parse(self, object, *, formatted="", indent=-1, block_level=0):
            tokens, attrs = Lexer()(object), set(dir(self))
            while tokens:
                token = tokens.pop(0)
                if token['type'] == 'list_start': block_level += 1
                if token['type'] == 'list_end': block_level -= 1
                if F"parse_{token['type']}" in attrs: 
                    code = getattr(self, F"parse_{token['type']}")(token, indent=indent)
                    if code is None: continue
                    if indent < 0: indent = max(len(code) - len(code.lstrip()), 4)
                    text, object = re.split(r'\s*'.join(re.escape(token['match'].group().rstrip()).splitlines(True)), object)
                    formatted = self.indent_text(formatted, text, code, indent=indent, block_level=block_level)
            return self.indent_text(formatted, object, indent=indent)
        
                    

Another notebook defines how the output should be displayed.

Create IPython extensions that can be reused.

    parser = Parser()
    def cleanup_transform(x): 
        global parser
        return textwrap.dedent(parser.parse(''.join(x))).splitlines(True)

    def unload_ipython_extension(shell):
        globals()['_transforms'] = globals().get('_transforms', shell.input_transformer_manager.cleanup_transforms)
        global _transforms
        shell.input_transformer_manager.cleanup_transforms = _transforms
    def load_ipython_extension(shell):
        unload_ipython_extension(shell)
        shell.input_transformer_manager.cleanup_transforms = [cleanup_transform]

    __name__ == '__main__' and load_ipython_extension(get_ipython())
Written on October 16, 2019