README.md (7053B) - raw


      1 # Markion
      2 
      3 Markion is a Python script to allow writing literate programs using Markdown.
      4 Markion retrieves the tangled code in a file and written it to the specified
      5 files in the output directory. This README file is an example of a file that
      6 Markion can process, indeed, this file will give you Markion itself!
      7 
      8 ## Using Markion
      9 
     10 ### Creating the input file
     11 
     12 To use Markion, create a Mardown file normally, and insert code snippets as you
     13 would typically with Markdown. If you want to use that code for your output
     14 files, you should use the following syntax:
     15 
     16 <pre>
     17 ```[language] block|file blockid|filename
     18 code snippet
     19 ```
     20 </pre>
     21 
     22 Specifing the language is optional (but you should put a space between the
     23 <code>```</code> and either "block" or "file"). The next word specifies wether
     24 the code is a block or it is the content of a file, and the last word represents
     25 the block ID (to include it in other snippets) or the output file name (where
     26 the code will be written) respectively. You may add comments if desired at the
     27 end of the line, both Markdown and Markion will ignore them.
     28 
     29 ### Prerequisites
     30 
     31 In order to run the program, you will need Python version 3.6 or later.
     32 
     33 ### Running the program
     34 
     35 To run the program, execute the file `markion.py` followed by the input file.
     36 
     37 ```
     38 python3 markion.py file
     39 ```
     40 
     41 Run the program with `--help` (or `-h`) to get help about the program's usage
     42 and options.
     43 
     44 ## Explanation of the program
     45 
     46 First of all, we will add the shebang, license notice, import all the required
     47 libraries and set the version.
     48 
     49 ```python file markion.py
     50 #!/usr/bin/env python3
     51 [[ include license ]]
     52 import os, sys, re, argparse
     53 __version__ = "1.0.0"
     54 ```
     55 
     56 ### Program arguments
     57 
     58 We will use Python's `argparse` package to deal with our program's arguments. We
     59 initialize the parser with a brief description of the program's utility.
     60 
     61 ```python file markion.py
     62 parser = argparse.ArgumentParser(prog='Markion', description='Markion is a simple scripts that retrieves tangled code from Markdown.')
     63 ```
     64 
     65 One of the arguments is the name of the input file.
     66 
     67 ```python file markion.py
     68 parser.add_argument('file', metavar='file', type=str, nargs=1, help='Input file.')
     69 ```
     70 
     71 Another optional argument lets the user specify the output directory.
     72 
     73 ```python file markion.py
     74 parser.add_argument('-d', '--output-directory', dest='out_dir', type=str, default=os.getcwd(), help='Change the output directory.')
     75 ```
     76 
     77 The user can also let the program automatically detect the output directory
     78 (based on the file's directory). This option will override the
     79 `--output-directory` option.
     80 
     81 ```python file markion.py
     82 parser.add_argument('-D', '--auto-directory', dest='auto_dir', action='store_true', help='Auto detect output directory.')
     83 ```
     84 
     85 To calculate the directory automatically, we simply check the input file's
     86 directory.
     87 
     88 ```python block auto_dir
     89 if args.auto_dir:
     90     args.out_dir = os.path.dirname(args.file[0])
     91 ```
     92 
     93 Finally, there is an option to retrieve the current version.
     94 
     95 ```python file markion.py
     96 parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__)
     97 ```
     98 
     99 Assign the arguments' values to the `args` variable to use it later on.
    100 
    101 ```python file markion.py
    102 args = parser.parse_args()
    103 ```
    104 
    105 ### Reading the input file
    106 
    107 Read the input file and copy the contents to a variable `inp`.
    108 
    109 ```python file markion.py
    110 with open(args.file[0], 'r') as f:
    111     inp = f.read()
    112 ```
    113 
    114 ### Extracting the tangled code
    115 
    116 Extract the important pieces of code from the `inp` variable. To do so there are
    117 two regular expressions, one that matches the blocks and one that matches the
    118 content to output in the files. We get all the snippets and save them into the
    119 variables `blocks` and `files`.
    120 
    121 ```python file markion.py
    122 r_block = '```[\w\-.]*\s+block\s+([\w.-]+).*?\n(.*?)\n```\s*?\n'
    123 r_file = '```[\w\-.]*\s+file\s+([\w.-]+).*?\n(.*?\n)```\s*?\n'
    124 blocks = re.findall(r_block, inp, flags = re.DOTALL)
    125 files = re.findall(r_file, inp, flags = re.DOTALL)
    126 ```
    127 
    128 ### Resolving includes in the tangled code
    129 
    130 For each file specified in the input, we resolve all the blocks that are
    131 included (recursively). To do so we use the function `resolve`.
    132 
    133 ```python file markion.py
    134 [[ include resolve ]]
    135 block_content = { b[0] : [False, b[1]] for b in blocks }
    136 file_content = dict()
    137 for f in files:
    138     if f[0] not in file_content:
    139         file_content[f[0]] = ''
    140     file_content[f[0]] += resolve(f[1], block_content)
    141 ```
    142 
    143 The following code is the function resolve included in the last code fragment,
    144 it won't be directly written on the file, but be included when the
    145 `[[ include resolve ]]` is called. As you can see it indents the whole block.
    146 
    147 ```python block resolve
    148 r_include = re.compile('([ \t]*)\[\[\s*include\s+([\w\-.]+)\s*\]\]', flags = re.DOTALL)
    149 def resolve(content, blocks):
    150     it = r_include.finditer(content)
    151     for include in it:
    152         block_name = include[2]
    153         if blocks[block_name][0]:
    154             raise Exception('Circular dependency in block ' + block_name)
    155         blocks[block_name][0] = True
    156         s = resolve(blocks[block_name][1], blocks)
    157         blocks[block_name][0] = False
    158         blocks[block_name][1] = s
    159         s = include[1] + s.replace('\n', '\n' + include[1])
    160         content = r_include.sub(repr(s)[1:-1], content, count = 1)
    161     return content
    162 ```
    163 
    164 ### Writing the output to the corresponding files
    165 
    166 Finally, if there weren't any errors, we write the output code into the
    167 respective files. To do so, we assign the directory automatically if the option
    168 has been delcared, otherwise, we create the output directory if not already
    169 created:
    170 
    171 ```python file markion.py
    172 [[ include auto_dir ]]
    173 if not os.path.exists(args.out_dir):
    174     os.mkdirs(args.out_dir)
    175 ```
    176 
    177 And we write the output.
    178 
    179 ```python file markion.py
    180 for fn, fc in file_content.items():
    181     with open(os.path.join(args.out_dir, fn), 'w') as f:
    182         f.write(fc)
    183 ```
    184 
    185 ## License
    186 
    187 The program is licensed under the GNU Affero General Public License version 3 or
    188 later (available [here][agpl]).
    189 
    190 In order to make sure there is no missunderstanding, we will include the
    191 following license notice on our file.
    192 
    193 ```python block license
    194 # Markion
    195 # Copyright (C) 2019-2020 Oscar Benedito <oscar@oscarbenedito.com>
    196 #
    197 # This program is free software: you can redistribute it and/or modify
    198 # it under the terms of the GNU Affero General Public License as
    199 # published by the Free Software Foundation, either version 3 of the
    200 # License, or (at your option) any later version.
    201 #
    202 # This program is distributed in the hope that it will be useful,
    203 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    204 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    205 # GNU Affero General Public License for more details.
    206 #
    207 # You should have received a copy of the GNU Affero General Public License
    208 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
    209 ```
    210 
    211 ## Author
    212 
    213 - **Oscar Benedito** - oscar@oscarbenedito.com
    214 
    215 [agpl]: <https://www.gnu.org/licenses/agpl-3.0.en.html> "GNU Affero General Public License v3.0"