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"