├── .gitignore ├── MANIFEST ├── README.md ├── dist └── readmd-0.0.1.tar.gz ├── readmd.py ├── setup.py └── tests ├── __init__.py ├── files ├── atx.desired ├── atx.sample ├── block-complex.desired ├── block-complex.sample ├── block.desired ├── block.sample ├── hr.desired ├── hr.sample ├── line-break.desired ├── line-break.sample ├── mixed.desired ├── mixed.sample ├── ol-in-ul.desired ├── ol-in-ul.sample ├── ol.desired ├── ol.sample ├── p.desired ├── p.sample ├── setext-extra-line.desired ├── setext-extra-line.sample ├── setext.desired ├── setext.sample ├── ul.desired └── ul.sample └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | readmd.py 3 | setup.py 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Make Markdown Easier to Read as Plaintext 2 | ========================================= 3 | 4 | Install with `pip install readmd` 5 | 6 | 7 | Example - take this file: 8 | 9 | A markdown file 10 | === 11 | 12 | 1. This file is awesome 13 | 1. It has contents 14 | 1. Lorem ipsum dolor sit amet, vestibulum nulla nunc, enim pellentesque tempor, pretium proin in ligula, ac in. Suspendisse praesent pellentesque dis mollis maecenas, mattis vehicula. Morbi vestibulum morbi. Proin et facilisis aliquam lectus lorem, rutrum phasellus, duis semper sodales venenatis diam ad, sodales wisi wisi eget. Lobortis nonummy, blandit lectus elit egestas, id ut nullam, parturient minus lobortis. 15 | 16 | > how cool? 17 | > 18 | > 19 | > so cool. 20 | 21 | and convert it with this command `readmd file.md` to: 22 | 23 | A markdown file 24 | =============== 25 | 26 | 1. This file is awesome 27 | 2. It has contents 28 | 3. Lorem ipsum dolor sit amet, vestibulum nulla nunc, enim pellentesque tempor, 29 | pretium proin in ligula, ac in. Suspendisse praesent pellentesque dis mollis 30 | maecenas, mattis vehicula. Morbi vestibulum morbi. Proin et facilisis 31 | aliquam lectus lorem, rutrum phasellus, duis semper sodales venenatis diam 32 | ad, sodales wisi wisi eget. Lobortis nonummy, blandit lectus elit egestas, 33 | id ut nullam, parturient minus lobortis. 34 | 35 | > how cool? 36 | > 37 | > so cool. 38 | 39 | Furthermore, if you do not specify a width parameter, it will just default to 40 | the width of your current screen. 41 | 42 | ### Use Cases 43 | 44 | This script can be used to: 45 | 46 | * Easily read any markdown file in your terminal 47 | * Pretty-print your github README.md file for other peoples' pleasure 48 | 49 | ### Command-line Usage 50 | 51 | Read a README.md file from a github project (in your current directory) in your 52 | terminal: 53 | 54 | readmd | less 55 | 56 | Convert your own readme into a pretty-printed one width of 60 characters: 57 | 58 | readmd --width 60 README.md README.md.new 59 | mv README.md{.new,} 60 | 61 | Read something from standard in and save it as a file: 62 | 63 | cat some-file.md | readmd - some-pretty-file.md 64 | 65 | ### Details 66 | 67 | * Command-line Usage: readmd [-w width] [input_file [output_file]] 68 | * Defaults to trying README.md if no file is specified 69 | * Defaults to width of 80 characters if not specified 70 | * Specify a width of -1 to have the script bypass text wrapping 71 | * Specify a width of 0 to have the character width match the current console 72 | * Pretty-prints markdown that will generate the same markup as the original 73 | markdown 74 | * Handles all special elements in markdown (headers, lists, block quote, code 75 | blocks, horizontal rules) 76 | * Formats sub-elements, e.g., a list within a blockquote 77 | * Converts numbers in ordered lists to properly ascend from one 78 | * Idempotent 79 | 80 | --- 81 | 82 | ### TODO 83 | 84 | - add a simple way to configure stylistic preferences (or even add support for 85 | new elements)? 86 | - figure out if hr's should expand or not 87 | - figure out if sub-lists without extra line-break should be kept as sub-lists 88 | -------------------------------------------------------------------------------- /dist/readmd-0.0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrcoles/readmd/c95f24ff074845ee7b392d7639b84974496c45bc/dist/readmd-0.0.1.tar.gz -------------------------------------------------------------------------------- /readmd.py: -------------------------------------------------------------------------------- 1 | # 2 | # readmd 3 | # 4 | # magical code to parse your markdown and make it more readable from the 5 | # commandline this is lossless, i.e., the generated file should produce the 6 | # same HTML as the original 7 | # 8 | # potentially useful for reading someone's README.md file in the terminal -- 9 | # or for formatting your own. 10 | # 11 | 12 | from __future__ import print_function, division, unicode_literals 13 | 14 | import re 15 | import sys 16 | 17 | try: 18 | from io import StringIO 19 | except: 20 | try: import cStringIO as StringIO 21 | except: import StringIO 22 | 23 | DEFAULT_WIDTH = 80 24 | MIN_WIDTH = 10 25 | NUM_SPACES = 4 26 | SPACES = ' ' * NUM_SPACES 27 | 28 | # SPECIAL STATE TYPES 29 | TYPE_HR = 1 30 | TYPE_UL = 2 31 | TYPE_OL = 3 32 | TYPE_BLOCK = 4 33 | TYPE_CODE = 5 34 | 35 | # REGEX's for SPECIAL STATE TYPES 36 | _atx_header = re.compile('^(#+)\s*') 37 | _setext_header = re.compile('^(=+|-+)\s*$') 38 | _hr = re.compile('^(([*\-_])\s*){3,}$') 39 | _ul_indent = re.compile('^ {0,3}([*+\-*])\s\s*') 40 | _ol_indent = re.compile('^ {0,3}\d+\.\s\s*') 41 | _blockquote = re.compile('^>\s*') 42 | _code = re.compile('^ {4,}[^ ]') 43 | 44 | _indented = re.compile('^ {4,}') 45 | _end_space = re.compile(' +$') 46 | 47 | SPECIAL_TYPES = ( 48 | (TYPE_HR, _hr), # must be before UL (or make the UL regex smarter) 49 | (TYPE_UL, _ul_indent), 50 | (TYPE_OL, _ol_indent), 51 | (TYPE_BLOCK, _blockquote), 52 | (TYPE_CODE, _code), 53 | ) 54 | 55 | def _increment_ol_state(state, prev_state=None): 56 | if prev_state is None: prev_state = state 57 | 58 | number = prev_state.get('number', 0) + 1 59 | prefix_first = '%s. ' % number 60 | while len(prefix_first) < 4: prefix_first += ' ' 61 | state.update({ 62 | 'number': number, 63 | 'prefix_first': prefix_first, 64 | }) 65 | 66 | 67 | ## The goods. 68 | 69 | def readmd(f, width=None, out=None): 70 | ''' 71 | Make a markdown file more readable for humans. 72 | 73 | f - filelike object that supports .next() and StopIteration 74 | width - (optional) width to use, otherwise uses terminal width 75 | out - (optional) a file-like object to write output, otherwise output is returned 76 | ''' 77 | if not width: 78 | dims = _get_terminal_size() 79 | width, height = dims or (80, 24) 80 | 81 | if width < 0: 82 | width = -1 83 | 84 | out_was_none = out is None 85 | 86 | if out_was_none: 87 | out = StringIO.StringIO() 88 | 89 | _groupify(f, width, out) 90 | 91 | if out_was_none: 92 | return out.getvalue() 93 | 94 | 95 | def _groupify(f, width, out, indent=''): 96 | ''' 97 | groups lines into different elements and renders them to out 98 | ''' 99 | group = [] # to group sections into different elements 100 | has_break = BooleanClass(False) # to record line breaks 101 | forced_break = BooleanClass(False) # to handle headers auto-rendering 102 | 103 | # special for doing recursive rendering and doing `prefix_first` properly... 104 | first_render = BooleanClass(True) 105 | 106 | prev_state = {} 107 | state = {} 108 | 109 | # helper function to pass the right arguments into _render_group 110 | def _do_render_group(line_after=True): 111 | is_first_render = first_render 112 | _render_group( 113 | group, 114 | width, 115 | indent, 116 | is_first_render, 117 | prefix_first=state.get('prefix_first', ''), 118 | prefix_rest=state.get('prefix_rest', ''), 119 | line_after=line_after, 120 | is_pre=state.get('type') == TYPE_CODE, 121 | out=out, 122 | ) 123 | 124 | is_first_render and first_render.set_false() 125 | has_break.set_false() 126 | forced_break.set_false() 127 | while len(group): group.pop() 128 | 129 | # main loop that goes through the file and parses it 130 | for line in f: 131 | line = line.strip('\n\r').expandtabs(NUM_SPACES) # lawl - that's a function! replace('\t', SPACES) 132 | 133 | # deal with empty line 134 | if not line.strip(): 135 | if not forced_break and group: 136 | has_break.set_true() 137 | 138 | # deal with setext header - make sure group exists, to prevent hrs from getting matched 139 | elif not has_break and _setext_header.match(line) and group: 140 | m = _setext_header.match(line) 141 | underline = m.groups()[0][0] 142 | above_line = group.pop() if group else '' 143 | 144 | if len(group) > 1: 145 | group.pop() 146 | _do_render_group() 147 | 148 | prev_state, state = state, {} # clear state 149 | group.append(above_line) 150 | _do_render_group(line_after=False) 151 | 152 | group.append(underline * len(above_line)) 153 | _do_render_group() 154 | forced_break.set_true() 155 | 156 | # deal with atx header 157 | elif _atx_header.match(line): 158 | m = _atx_header.match(line) 159 | hashes = m.groups()[0].strip(' ') 160 | 161 | if group: 162 | _do_render_group() 163 | 164 | prev_state, state = state, {} # clear state 165 | group.append('%s %s' % (hashes, line.strip('#').strip(' '))) 166 | _do_render_group() 167 | forced_break.set_true() 168 | 169 | # deal with non-empty line 170 | else: 171 | 172 | # clean up forced_break if we get to some content! 173 | forced_break.set_false() 174 | 175 | # check for continuations of special types 176 | was_continued = False 177 | state_type = state.get('type') 178 | 179 | if state_type in (TYPE_UL, TYPE_OL, TYPE_BLOCK, TYPE_CODE): 180 | 181 | # any non-empty non-code line following code will break immediately 182 | if TYPE_CODE == state_type: 183 | if not _code.search(line): 184 | _do_render_group() 185 | 186 | # continuing an ol with a ul or vice versa will convert to prior type 187 | elif state_type in (TYPE_UL, TYPE_OL): 188 | ul_m = _ul_indent.search(line) 189 | if ul_m or _ol_indent.search(line): 190 | #TODO - maybe remember if first had break or not and do rest consistently? 191 | _do_render_group(line_after=has_break) 192 | 193 | line = (_ul_indent if ul_m else _ol_indent).sub('', line) 194 | if TYPE_OL == state.get('type'): _increment_ol_state(state) 195 | was_continued = True 196 | 197 | elif _indented.search(line): 198 | 199 | if has_break: 200 | group.append('\n') 201 | has_break.set_false() 202 | 203 | line = _indented.sub('', line) # remove indent for proper parsing 204 | 205 | if not line: has_break.set_true() 206 | was_continued = True 207 | 208 | 209 | # see if we can drop the blockquote symbol from the start of a 210 | # continuation of a blockquoted region 211 | elif TYPE_BLOCK == state_type: 212 | if _blockquote.search(line): 213 | if has_break: 214 | group.append('\n') 215 | has_break.set_false() 216 | line = _blockquote.sub('', line) 217 | if not line: has_break.set_true() 218 | was_continued = True 219 | 220 | 221 | # non-empty line after a break - group it! 222 | if not was_continued and has_break: 223 | if group: _do_render_group() 224 | 225 | group.append(line) 226 | 227 | 228 | # first non-empty line of a new group - identify it! 229 | if not was_continued and len(group) == 1: 230 | match = None 231 | for special_type, regex in SPECIAL_TYPES: 232 | match = regex.search(line) 233 | if match: 234 | break 235 | 236 | if match: 237 | prev_state, state = state, {'type': special_type} 238 | 239 | if TYPE_UL == special_type: 240 | group[-1] = _ul_indent.sub('', group[-1]) 241 | bullet = match.groups()[0] 242 | state.update({ 243 | 'prefix_first': '%s ' % bullet, 244 | 'prefix_rest': SPACES, 245 | }) 246 | 247 | elif TYPE_OL == special_type: 248 | group[-1] = _ol_indent.sub('', group[-1]) 249 | _increment_ol_state(state, prev_state) 250 | state['prefix_rest'] = SPACES 251 | 252 | elif TYPE_BLOCK == special_type: 253 | group[-1] = _blockquote.sub('', group[-1]) 254 | state.update({ 255 | 'prefix_first': '> ', 256 | 'prefix_rest': '> ', 257 | }) 258 | 259 | elif TYPE_HR == special_type: 260 | # this doesn't extend the line for now... 261 | state['character'] = match.groups()[0] 262 | has_break.set_true() 263 | 264 | elif TYPE_CODE == special_type: 265 | state.update({ 266 | 'prefix_first': SPACES, 267 | 'prefix_rest': SPACES, 268 | }) 269 | 270 | else: 271 | prev_state, state = state, {} 272 | 273 | if group: _do_render_group(line_after=False) 274 | 275 | 276 | def _render_group(group, width, indent, is_first_render, prefix_first, prefix_rest, line_after, is_pre, out): 277 | ''' 278 | Do the rendering of several lines that have been grouped together by 279 | a particular type of element, and recursively render sub-elements 280 | ''' 281 | sections = [] 282 | cur_section = '' 283 | relative_width = width if width == -1 else max(MIN_WIDTH, width - len(indent) - max(len(prefix_first), len(prefix_rest))) 284 | first_indent = '' if is_first_render else indent 285 | 286 | if is_pre: 287 | for i, line in enumerate(group): 288 | out.write('%s%s\n' % (first_indent if i == 0 else indent, line)) 289 | 290 | else: 291 | 292 | # recursive call to allow rendering of special types within special types 293 | if prefix_first and prefix_rest: 294 | out.write(first_indent + (prefix_first[:-1] if prefix_first.endswith(' ') else prefix_first) + ' ') 295 | _groupify(iter(group), width, out, indent=indent + prefix_rest) 296 | 297 | # render that! 298 | else: 299 | num_lines = len(group) 300 | for i, line in enumerate(group): 301 | cur_section += ('' if i == 0 else ' ') + line.strip() 302 | 303 | if line.endswith(' ') or line == '\n' or i + 1 == num_lines: 304 | sections.append(cur_section + (' ' if line.endswith(' ') else '')) 305 | cur_section = '' 306 | 307 | if line == '\n': 308 | # add a whole other line for special case with line breaks 309 | sections.append('') 310 | 311 | num_sections = len(sections) 312 | for i, section in enumerate(sections): 313 | fitted_text = _fit_text(section, relative_width, 314 | with_break=(i + 1 < num_sections)) 315 | for j, line in enumerate(fitted_text): 316 | out.write('%s%s%s\n' % (first_indent if 0 == i == j else indent, 317 | prefix_first if 0 == i == j else prefix_rest, 318 | line)) 319 | 320 | if line_after: 321 | out.write(_end_space.sub('', indent) + '\n') 322 | 323 | 324 | def _fit_text(section, width, with_break=False): 325 | '''fit text to a given width''' 326 | # returns an array of this section of text to fit the given width 327 | words = [x for x in section.split(' ') if x] 328 | 329 | if with_break and words: 330 | words[-1] += ' ' 331 | 332 | result = [] 333 | cur = '' 334 | 335 | for i, word in enumerate(words): 336 | if cur and width != -1 and (len(cur) + len(word) + 1 > width): 337 | result.append(cur) 338 | cur = word 339 | else: 340 | cur += ('' if i == 0 else ' ') + word 341 | 342 | result.append(cur) 343 | 344 | return result 345 | 346 | 347 | class BooleanClass(object): 348 | '''A mutable class that represents a boolean value. 349 | I can therefore manipulate them from inside a closure. 350 | 351 | ''' 352 | def __init__(self, condition): self.condition = condition 353 | def is_true(self): return bool(self.condition) 354 | def set_true(self): self.condition = True 355 | def set_false(self): self.condition = False 356 | def __bool__(self): return self.is_true() 357 | __nonzero__ = __bool__ 358 | def __unicode__(self): return unicode(str(self)) 359 | def __str__(self): return str(bool(self.condition)) 360 | def __repr__(self): return 'BooleanClass(%s)' % unicode(self) 361 | 362 | 363 | def _get_terminal_size(): 364 | ''' 365 | get the width of the terminal window, from: 366 | http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python 367 | ''' 368 | def ioctl_GWINSZ(fd): 369 | try: 370 | import fcntl, termios, struct, os 371 | cr = struct.unpack('hh'.encode('utf8'), 372 | fcntl.ioctl(fd, termios.TIOCGWINSZ, 373 | '1234'.encode('utf8'))) 374 | except Exception: 375 | return None 376 | return cr 377 | 378 | cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) 379 | if not cr: 380 | try: 381 | fd = os.open(os.ctermid(), os.O_RDONLY) 382 | cr = ioctl_GWINSZ(fd) 383 | os.close(fd) 384 | except Exception: 385 | pass 386 | if not cr: 387 | try: 388 | cr = (env['LINES'], env['COLUMNS']) 389 | except Exception: 390 | cr = (25, 80) 391 | return int(cr[1]), int(cr[0]) 392 | 393 | 394 | ## 395 | 396 | def command_line_runner(): 397 | import argparse 398 | import os 399 | parser = argparse.ArgumentParser(description=( 400 | 'Convert a markdown file into pretty-printed markdown. ' 401 | 'The output will be able to generate the same HTML output as ' 402 | 'the original markdown file, but it gains the ability of being ' 403 | 'more readable as plain-text. If no files are specified, then ' 404 | 'it tries to read from `README.md` in the current directory.' 405 | )) 406 | parser.add_argument('-w', '--width', type=int, default=DEFAULT_WIDTH, 407 | help=('number of characters per line for text, ' 408 | 'use -1 for infinite width,' 409 | 'use 0 to match the width of the current console,' 410 | 'defaults to %s' % (DEFAULT_WIDTH,))) 411 | parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), 412 | default=None) 413 | parser.add_argument('outfile', nargs='?', type=argparse.FileType('w'), 414 | default=sys.stdout) 415 | args = parser.parse_args() 416 | 417 | if args.infile is None: 418 | default_in = 'README.md' 419 | if os.path.exists(default_in): 420 | args.infile = open(default_in) 421 | else: 422 | parser.error('Unable to find a default %s file. ' 423 | 'Please specify a file to read.' % (default_in,)) 424 | 425 | readmd(args.infile, width=args.width, out=args.outfile) 426 | 427 | 428 | if __name__ == '__main__': 429 | command_line_runner() 430 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name='readmd', 5 | version='0.1.2', 6 | description='Make markdown easier to read as plaintext', 7 | author='Peter Coles', 8 | author_email='peter@mrcoles.com', 9 | url='https://github.com/mrcoles/readmd', 10 | py_modules=['readmd'], 11 | entry_points={ 12 | 'console_scripts': [ 13 | 'readmd = readmd:command_line_runner', 14 | ] 15 | }, 16 | classifiers=[ 17 | 'Programming Language :: Python', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Development Status :: 4 - Beta', 20 | 'Operating System :: OS Independent', 21 | 'Intended Audience :: Developers', 22 | 'Topic :: Text Processing', 23 | 'Topic :: Software Development :: Libraries :: Python Modules', 24 | 'Programming Language :: Python :: 2.7', 25 | 'Programming Language :: Python :: 3', 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrcoles/readmd/c95f24ff074845ee7b392d7639b84974496c45bc/tests/__init__.py -------------------------------------------------------------------------------- /tests/files/atx.desired: -------------------------------------------------------------------------------- 1 | # h1 2 | 3 | ## h2 4 | 5 | ### h3 6 | 7 | #### h4 8 | 9 | ##### h5 10 | 11 | -------------------------------------------------------------------------------- /tests/files/atx.sample: -------------------------------------------------------------------------------- 1 | # h1 2 | 3 | ## h2 4 | 5 | ### h3 6 | 7 | #### h4 #### 8 | 9 | ##### h5 ##### 10 | -------------------------------------------------------------------------------- /tests/files/block-complex.desired: -------------------------------------------------------------------------------- 1 | > this is a block 2 | > 3 | > - this is a 4 | > - list 5 | > - in a block 6 | -------------------------------------------------------------------------------- /tests/files/block-complex.sample: -------------------------------------------------------------------------------- 1 | 2 | > this is a block 3 | > 4 | > - this is a 5 | > - list 6 | > - in a block 7 | -------------------------------------------------------------------------------- /tests/files/block.desired: -------------------------------------------------------------------------------- 1 | > This is a bunch of block text, how cool is that? Can you believe how cool it 2 | > is? 3 | -------------------------------------------------------------------------------- /tests/files/block.sample: -------------------------------------------------------------------------------- 1 | > This is a bunch of block text, how cool is that? Can you believe how cool it is? -------------------------------------------------------------------------------- /tests/files/hr.desired: -------------------------------------------------------------------------------- 1 | ----- 2 | 3 | ______ 4 | 5 | - - - 6 | 7 | ----- 8 | 9 | *** 10 | -------------------------------------------------------------------------------- /tests/files/hr.sample: -------------------------------------------------------------------------------- 1 | ----- 2 | 3 | ______ 4 | - - - 5 | ----- 6 | *** 7 | -------------------------------------------------------------------------------- /tests/files/line-break.desired: -------------------------------------------------------------------------------- 1 | This is a 2 | line break. 3 | -------------------------------------------------------------------------------- /tests/files/line-break.sample: -------------------------------------------------------------------------------- 1 | This is a 2 | line break. 3 | -------------------------------------------------------------------------------- /tests/files/mixed.desired: -------------------------------------------------------------------------------- 1 | Mixed Sample 2 | ============ 3 | 4 | ## This has a few things in it 5 | 6 | > a block for example 7 | 8 | and some paragraphs 9 | 10 | ## More things 11 | 12 | - a list 13 | - more list 14 | - tricked list? 15 | 16 | - spaced list 17 | 18 | 1. an inner list 19 | 2. that is numbered 20 | 21 | - and a really long list item. and a really long list item. and a really long 22 | list item. and a really long list item. and a really long list item. 23 | 24 | ---- 25 | 26 | ___ 27 | 28 | And an h2 29 | --------- 30 | 31 | -------------------------------------------------------------------------------- /tests/files/mixed.sample: -------------------------------------------------------------------------------- 1 | Mixed Sample 2 | ============ 3 | 4 | ## This has a few things in it 5 | 6 | > a block 7 | > for example 8 | 9 | and some paragraphs 10 | 11 | ## More things 12 | 13 | - a list 14 | - more list 15 | 1. tricked list? 16 | 17 | - spaced list 18 | 19 | 1. an inner list 20 | 1. that is numbered 21 | 22 | - and a really long list item. and a really long list item. and a really long list item. and a really long list item. and a really long list item. 23 | 24 | ---- 25 | 26 | ___ 27 | 28 | And an h2 29 | --- 30 | -------------------------------------------------------------------------------- /tests/files/ol-in-ul.desired: -------------------------------------------------------------------------------- 1 | - spaced list 2 | 3 | 1. an inner list 4 | 2. that is numbered 5 | 6 | - and a really long list item. and a really long list item. and a really long 7 | list item. and a really long list item. and a really long list item. 8 | -------------------------------------------------------------------------------- /tests/files/ol-in-ul.sample: -------------------------------------------------------------------------------- 1 | 2 | - spaced list 3 | 4 | 1. an inner list 5 | 1. that is numbered 6 | 7 | - and a really long list item. and a really long list item. and a really long list item. and a really long list item. and a really long list item. 8 | -------------------------------------------------------------------------------- /tests/files/ol.desired: -------------------------------------------------------------------------------- 1 | 1. this 2 | 2. is 3 | 3. a 4 | 4. list 5 | -------------------------------------------------------------------------------- /tests/files/ol.sample: -------------------------------------------------------------------------------- 1 | 1. this 2 | 2. is 3 | 2. a 4 | 1. list 5 | -------------------------------------------------------------------------------- /tests/files/p.desired: -------------------------------------------------------------------------------- 1 | This is a paragraph. 2 | 3 | So is this. 4 | 5 | And this. 6 | -------------------------------------------------------------------------------- /tests/files/p.sample: -------------------------------------------------------------------------------- 1 | This is a 2 | paragraph. 3 | 4 | So is this. 5 | 6 | And this. 7 | -------------------------------------------------------------------------------- /tests/files/setext-extra-line.desired: -------------------------------------------------------------------------------- 1 | Extra line above 2 | ================ 3 | 4 | -------------------------------------------------------------------------------- /tests/files/setext-extra-line.sample: -------------------------------------------------------------------------------- 1 | 2 | Extra line above 3 | ======== 4 | -------------------------------------------------------------------------------- /tests/files/setext.desired: -------------------------------------------------------------------------------- 1 | This is an H1 2 | ============= 3 | 4 | this is an h2 5 | ------------- 6 | 7 | this is an h1 8 | ============= 9 | 10 | -------------------------------------------------------------------------------- /tests/files/setext.sample: -------------------------------------------------------------------------------- 1 | This is an H1 2 | ====== 3 | 4 | this is an h2 5 | ------------------- 6 | 7 | this is an h1 8 | ======== -------------------------------------------------------------------------------- /tests/files/ul.desired: -------------------------------------------------------------------------------- 1 | - this 2 | - is 3 | - a 4 | - list 5 | -------------------------------------------------------------------------------- /tests/files/ul.sample: -------------------------------------------------------------------------------- 1 | - this 2 | - is 3 | - a 4 | * list 5 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import re 4 | try: import cStringIO as StringIO 5 | except: import StringIO 6 | 7 | import readmd 8 | 9 | def _markup(text, width=None): 10 | f = StringIO.StringIO(text) 11 | out = StringIO.StringIO() 12 | if width is None: width = 80 13 | readmd.readmd(f, width, out) 14 | return out.getvalue() 15 | 16 | class SampleTestsMetaclass(type): 17 | def __new__(cls, name, bases, attrs): 18 | samples_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'files') 19 | 20 | # get all the test file names 21 | file_names = os.listdir(samples_dir) 22 | test_file_names = set() 23 | for name in file_names: 24 | if name.endswith('.sample') or name.endswith('.desired'): 25 | test_file_names.add('.'.join(name.split('.')[:-1])) 26 | 27 | # test logic function closure 28 | def get_fn(path): 29 | def fn(self): 30 | path = os.path.join(samples_dir, test_file_name) 31 | sample = open(path + '.sample') 32 | desired = open(path + '.desired') #TODO - IOError? 33 | sample_data = sample.read() 34 | desired_data = desired.read() 35 | sample.close() 36 | desired.close() 37 | self.assertEqual(_markup(sample_data), desired_data) 38 | # For nose, test name must match /[Tt]est/ 39 | fn.__name__ = 'test_{0}'.format(path) 40 | return fn 41 | 42 | # run test on all test files 43 | _r_file_fn = re.compile('^[A-z][A-z0-9_\-]*$') 44 | for test_file_name in test_file_names: 45 | if _r_file_fn.match(test_file_name): 46 | attrs['test_%s' % test_file_name.replace('-', '_')] = get_fn(test_file_name) 47 | 48 | return super(SampleTestsMetaclass, cls).__new__(cls, name, bases, attrs) 49 | 50 | 51 | class TestReadMd(unittest.TestCase): 52 | __metaclass__ = SampleTestsMetaclass 53 | def setUp(self): pass 54 | def tearDown(self): pass 55 | 56 | if __name__ == '__main__': 57 | unittest.main() 58 | --------------------------------------------------------------------------------