Skip to content

Latest commit

 

History

History
222 lines (169 loc) · 6.08 KB

DOCS.md

File metadata and controls

222 lines (169 loc) · 6.08 KB

Documentation

This is the documentation, and source code, for md-tangle.

md-tangle is supported by both Python 2 and Python 3.

Since some still swear to Python 2, I had to do some adaptations.

I would prefer to use:

  • Typings (introduced in Python 3.5)
  • os.makedirs(dir, exist_ok=True) instead of Path(dir).mkdir(exist_ok=True)
  • f"{filename:50} {lines} lines" instead of "{0: <50} {1} lines".format(filename, lines)

If installed with Python 2, pathlib2 is a requirement. Since it's not part of the requirements for the Python 3 package, it's not listed in the requirements.

Packaging

__init__.py

__title__ = 'md-tangle'
__version__ = '1.3.1'
__author__ = 'Joakim Myrvoll Johansen'
__author_email__ = 'joakimmyrvoll@gmail.com'
__license__ = 'MIT'

__main__.py

from . import main
main.main()

Entry point

Imports

import argparse
import sys
import md_tangle
from md_tangle.save import save_to_file
from md_tangle.tangle import map_md_to_code_blocks

Argument parsing

Setup for all arguments

def __get_args():
    parser = argparse.ArgumentParser(description="Tangle code blocks from Markdown file.")
    parser.add_argument("filename", type=str, help="path to Markdown file", nargs='?')
    parser.add_argument("--version", action='store_true', help="print version")
    parser.add_argument("-v", "--verbose", action='store_true', help="show output")
    parser.add_argument("-f", "--force", action='store_true', help="force overwrite of files")
    parser.add_argument("-d", "--destination", type=str, help="overwrite output destination")
    parser.add_argument("-s", "--separator", type=str, help="separator for tangle destinations (default=',')", default=",")
    return parser.parse_args()

main.py

def main():
    """Main program entry point"""
    args = __get_args()

    if args.version:
        print(md_tangle.__version__)
        sys.exit(0)

    if args.filename is None:
        sys.stderr.write("The 'filename' argument is required.\n")
        sys.exit(1)

    blocks = map_md_to_code_blocks(args.filename, args.separator)
    save_to_file(blocks, args.verbose, args.force, args.destination)


if __name__ == '__main__':
    main()

Tangling

Imports

import re
from io import open

Regex to fetch the keywords

These are the different Regex's for finding code blocks, and the tangle keyword.

TANGLE_CMD = "tangle:"
TANGLE_REGEX = "tangle:+([^\s]+)"
BLOCK_REGEX = "~{4}|`{3}"
BLOCK_REGEX_START = "^(~{4}|`{3})"

Check if line contains code block separators

This function check if the line starts with one of the code block separators, and it checks that it is only one on that line. So this is not read as a code block.

def __contains_code_block_separators(line):
    line = line.lstrip(' ')
    tangle = re.search(BLOCK_REGEX_START, line)
    starts_with_separator = tangle is not None

    tangle = re.findall(BLOCK_REGEX, line)
    only_one_separator = len(tangle) == 1

    return starts_with_separator and only_one_separator

Get save location from keyword

If the line includes one code block separator, this function will try to extract the tangle keyword.

def __get_save_locations(line, separator):
    tangle = re.search(TANGLE_REGEX, line)

    if tangle is None:
        return None

    match = tangle.group(0)
    locations = match.replace(TANGLE_CMD, '')
    return locations.split(separator)

Map Markdown to code blocks

These functions simply add the lines in the code blocks to it's destinations. The format on this data model is:

code_blocks = {
    "path/filename": "text from code block",
    "path/filename2": "text from code block",
}

implementation

def __add_to_code_blocks(code_blocks, locations, line):
    for location in locations:
        code_blocks[location] = code_blocks.get(location, "") + line


def map_md_to_code_blocks(filename, separator):
    md_file = open(filename, "r", encoding="utf8")
    lines = md_file.readlines()
    locations = None
    code_blocks = {}

    for line in lines:
        if __contains_code_block_separators(line):
            locations = __get_save_locations(line, separator)
        elif locations is not None:
            __add_to_code_blocks(code_blocks, locations, line)

    md_file.close()
    return code_blocks

Saving

Imports

os.makedirs does not support creating paths if they already exists in Python 2. So we need use Path from pathlib/pathlib2 (backport for Python 2).

import os
from io import open

try:
    get_input = raw_input  # fix for Python 2
except NameError:
    get_input = input

try:
    from pathlib import Path
except ImportError:
    from pathlib2 import Path  # Python 2 backport

Create directory

Creates directory if not existing

def __create_dir(path):
    dir_name = os.path.dirname(path)

    if dir_name is not "":
        Path(dir_name).mkdir(exist_ok=True)

Saving to file

This function writes the code blocks to it's destinations.

def save_to_file(code_blocks, verbose=False, force=False, output_dest=None):
    for path, value in code_blocks.items():
        path = os.path.expanduser(path)

        if output_dest is not None:
            path = output_dest + "/" + os.path.basename(path)

        __create_dir(path)

        if os.path.isfile(path) and not force:
            overwrite = get_input("'{0}' already exists. Overwrite? (Y/n) ".format(path))
            if overwrite != "" and overwrite.lower() != "y":
                continue

        with open(path, "w", encoding="utf8") as f:
            f.write(value)
            f.close()

        if verbose:
            print("{0: <50} {1} lines".format(path, len(value.splitlines())))