TeaCode Previewer

2018-07-08

In the slack channel for TeaCode, there is a channel #expanders where people can share there expanders. These come in .tcbundle files which look something like this:

  "expanders" : [
    {
      "name" : "Function",
      "is_enabled" : true,
      "description" : "Creates default function statement.\n> fn do arr\n\n\n> fn do",
      "supported_languages" : [
        "Python"
      ],
      "pattern" : "fn ${name:word}| ${params:text?}|",
      "output_template" : "def ${name.snakecase}(|${params.snakecase}|):\n\t#",
      "identifier" : "fn"
    },

It’s quite portable and easy to read and understand. It is however, not as easy to understand what the expander will actually do when used. This is where the idea create a Markdown version of the descriptions of TeaCode expanders came to mind. In TeaCode entering > {expander} will show the expander and it’s output.

Figuring it out

Originally, the plan was to write a simple regex/delimiter parser for the pattern and output_template but would have been incredibly complex and definitely seemed beyond what was necessary. On a whim, I’d decided to see how the developer had made the Sublime Text plugin (which is also in Python), it turned out that the returned output is received through an Applescript call.

Organization

Following the design of TeaCode bundles, my code is split into Bundles and Expanders.

The Bundle class takes in a .tcbundle file and creates a string with a title of the extracted name, subtitle of the description and then passes all expanders to be represented by the Expander class.

class Bundle:
    def __init__(self, file):
        self._content = json.load(file)
        self._name: str = self._content['name']
        self._description: str = self._content['description']
        self._expanders = self._content['expanders']

    def to_md(self) -> str:
        out = ''
        # H1 Header for Bundle name
        out += f'# {self._name}\n\n'
        # H2 header for description
        out += f'## {self._description}\n\n'
        # For every expander in this bundle
        for expander in self.__render_expanders():
            # If the expander is not empty, add formatted too
            if expander is not None:
                out += expander + '\n\n'
        return out

    def __render_expanders(self):
        result = []
        for expander in self._expanders:
            expand = Expander(expander)
            result.append(expand.to_md())
        return result

The Expander class then creates another string formatted for markdown including the name. The description is then checked for > {expander} and runs the Applescript call adding both to the output string as code snippets. The final string is then sent back and added to the Bundle’s output string.

class Expander:
    def __init__(self, dictionary):
        self.name: str = dictionary['name']
        self.description: str = dictionary['description']
        self.langs = dictionary['supported_languages']
        self.pattern = dictionary['pattern']
        self.output = dictionary['output_template']
        self.id = dictionary['identifier']

    def __render(self):
        # Split description on newline
        possibles = self.description.split('\n')
        # Get the first language this Expander works with
        try:
            typer = self.langs[0]
        except IndexError:
            typer = ''
        usable = []
        # For all lines in description
        for string in possibles:
            try:
                # Check that it would show output in TeaCode
                if string[0] is '>':
                    # Remove '>' (means something different in md)
                    string = string[1:]
                    # Run TeaCode Expander and add output to string
                    script = "Application('TeaCode').expandAsJson('{text}', {{ 'extension': '{extension}' }})".format(
                        text=string,
                        extension=typer)
                    command = ["osascript", "-l", "JavaScript", "-e", script]
                    session = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                                               universal_newlines=True)
                    stdout, stderr = session.communicate()
                    # print(stdout)
                    try:
                        temp = json.loads(stdout)
                    except json.JSONDecodeError:
                        continue
                    # Format as code and code blocks
                    usable.append(f'`{string}`\n\nwill render:\n')
                    usable.append(f'```{typer}\n{temp["text"]}\n```')
                else:
                    # If the line is not a expander example, make it a blockquote
                    if string is not '':
                        usable.append(f'> {string}')
            except IndexError:
                continue
        return usable

    def to_md(self):
        result = ''
        values = self.__render()
        print(f"\t{self.name} expander.")
        result += f"### {self.name}\n\nDescription:\n\n"
        for string in values:
            result += string + '\n\n'
        if len(self.langs) > 0:
            result += f'Languages: {self.langs}\n\n'
        return result

Do ‘em all

Using the glob module, this is run on every .tcbundle in the directory.

All bundles are stored in a file called bundles.tcbundle in the Application Support folder for TeaCode. Therefore, instead of exporting all Bundles and running this script, I decided to pull the bundles from that file.

if __name__ == '__main__':
    try:
        path = os.path.expanduser('~/Library/Application Support/com.apptorium.TeaCode-dm/bundles.tcbundles')
    except FileNotFoundError:
        path = os.path.expanduser('~/Library/Application Support/com.apptorium.TeaCode-setapp/bundles.tcbundles')
    with open(path, 'r') as f:
        stuff = json.load(f)
        for bund in stuff['bundles']:
            bundle = Bundle(bund)
            with open(f'./{bund["name"]}.tcbundle', 'w+') as tcout:
                json.dump(bund, tcout, indent=4)
            with open(f'./{bund["name"]}.md', 'w+') as mdout:
                mdout.write(bundle.to_md())
    print("Success!")

Update

After using this script every couple new expanders I've made, I realized it was incredibly unnecessary to recreate every single file. Instead, if there is a change detected, only that Bundle is re-rendered in Markdown. Additionally, with more and more expanders in a single Bundle, I realized a Table of Contents would be very convenient. So, it's there now. The link below will always have the most current version.

See the code