├── .gitignore ├── CHANGES.md ├── LICENSE.txt ├── MANIFEST.in ├── Makefile.py ├── README.md ├── TODO.txt ├── lib └── markdown_deux │ ├── __init__.py │ ├── conf │ ├── __init__.py │ └── settings.py │ ├── templates │ └── markdown_deux │ │ └── markdown_cheatsheet.html │ └── templatetags │ ├── __init__.py │ └── markdown_deux_tags.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | tmp/ 4 | build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # django-markdown-deux change log 2 | 3 | 4 | ## django-markdown-deux 1.0.6 5 | 6 | - [pull #34] Fix support for modern Django (by github.com/melkon98) 7 | - [pull #12] Fix typo in rendered html (by github.com/mlissner) 8 | - [pull #30] Fix typo in README (by github.com/martinleblanc) 9 | 10 | ## django-markdown-deux 1.0.5 11 | 12 | - [pull #6] A fix for python3. 13 | 14 | 15 | ## django-markdown-deux 1.0.4 16 | 17 | - [pull #3] Make the "open markdown syntax" link open in a new tab by default. 18 | (by github.com/mrooney). 19 | 20 | 21 | ## django-markdown-deux 1.0.3 22 | 23 | - Add markdown2 dependency in setup.py. 24 | 25 | 26 | ## django-markdown-deux 1.0.2 27 | 28 | - Ensure all necessary files includes in setup.py sdist package. 29 | 30 | 31 | ## django-markdown-deux 1.0.1 32 | 33 | - Fix overriding of `settings.MARKDOWN_DEUX_HELP_URL` in the project "settings.py". 34 | 35 | 36 | ## django-markdown-deux 1.0.0 37 | 38 | (Started maintaining this changelog 11 June 2010.) 39 | 40 | 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | # This is the MIT license 2 | 3 | Copyright (c) 2014 Trent Mick. 4 | Copyright (c) 2010 ActiveState Software Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included 15 | in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include CHANGES.md 3 | include LICENSE.txt 4 | recursive-include lib *.py 5 | recursive-include lib *.html 6 | -------------------------------------------------------------------------------- /Makefile.py: -------------------------------------------------------------------------------- 1 | 2 | # This is a Makefile for the `mk` tool. (Limited) details for that here: 3 | # 4 | 5 | import sys 6 | import os 7 | from os.path import join, dirname, normpath, abspath, exists, basename 8 | import re 9 | from glob import glob 10 | import codecs 11 | import webbrowser 12 | 13 | import mklib 14 | assert mklib.__version_info__ >= (0,7,2) # for `mklib.mk` 15 | from mklib.common import MkError 16 | from mklib import Task, mk 17 | from mklib import sh 18 | 19 | 20 | class bugs(Task): 21 | """open bug/issues page""" 22 | def make(self): 23 | webbrowser.open("http://github.com/trentm/django-markdown-deux/issues") 24 | 25 | class site(Task): 26 | """open project page""" 27 | def make(self): 28 | webbrowser.open("http://github.com/trentm/django-markdown-deux") 29 | 30 | class pypi(Task): 31 | """open project page""" 32 | def make(self): 33 | webbrowser.open("http://pypi.python.org/pypi/django-markdown-deux/") 34 | 35 | 36 | class cut_a_release(Task): 37 | """automate the steps for cutting a release 38 | 39 | See 'docs/devguide.md' in for details. 40 | """ 41 | proj_name = "django-markdown-deux" 42 | version_py_path = "lib/markdown_deux/__init__.py" 43 | version_module = "markdown_deux" 44 | _changes_parser = re.compile(r'^## %s (?P[\d\.abc]+)' 45 | r'(?P\s+\(not yet released\))?' 46 | r'(?P.*?)(?=^##|\Z)' % proj_name, re.M | re.S) 47 | 48 | def make(self): 49 | DRY_RUN = False 50 | version = self._get_version() 51 | 52 | # Confirm 53 | if not DRY_RUN: 54 | answer = query_yes_no("* * *\n" 55 | "Are you sure you want cut a %s release?\n" 56 | "This will involved commits and a release to pypi." % version, 57 | default="no") 58 | if answer != "yes": 59 | self.log.info("user abort") 60 | return 61 | print "* * *" 62 | self.log.info("cutting a %s release", version) 63 | 64 | # Checks: Ensure there is a section in changes for this version. 65 | changes_path = join(self.dir, "CHANGES.md") 66 | changes_txt = changes_txt_before = codecs.open(changes_path, 'r', 'utf-8').read() 67 | changes_sections = self._changes_parser.findall(changes_txt) 68 | top_ver = changes_sections[0][0] 69 | if top_ver != version: 70 | raise MkError("top section in `CHANGES.md' is for " 71 | "version %r, expected version %r: aborting" 72 | % (top_ver, version)) 73 | top_nyr = changes_sections[0][1] 74 | if not top_nyr: 75 | answer = query_yes_no("\n* * *\n" 76 | "The top section in `CHANGES.md' doesn't have the expected\n" 77 | "'(not yet released)' marker. Has this been released already?", 78 | default="yes") 79 | if answer != "no": 80 | self.log.info("abort") 81 | return 82 | print "* * *" 83 | top_body = changes_sections[0][2] 84 | if top_body.strip() == "(nothing yet)": 85 | raise MkError("top section body is `(nothing yet)': it looks like " 86 | "nothing has been added to this release") 87 | 88 | # Commits to prepare release. 89 | changes_txt = changes_txt.replace(" (not yet released)", "", 1) 90 | if not DRY_RUN and changes_txt != changes_txt_before: 91 | self.log.info("prepare `CHANGES.md' for release") 92 | f = codecs.open(changes_path, 'w', 'utf-8') 93 | f.write(changes_txt) 94 | f.close() 95 | sh.run('git commit %s -m "prepare for %s release"' 96 | % (changes_path, version), self.log.debug) 97 | 98 | # Tag version and push. 99 | curr_tags = set(t for t in _capture_stdout(["git", "tag", "-l"]).split('\n') if t) 100 | if not DRY_RUN and version not in curr_tags: 101 | self.log.info("tag the release") 102 | sh.run('git tag -a "%s" -m "version %s"' % (version, version), 103 | self.log.debug) 104 | sh.run('git push --tags', self.log.debug) 105 | 106 | # Release to PyPI. 107 | self.log.info("release to pypi") 108 | if not DRY_RUN: 109 | mk("pypi_upload") 110 | 111 | # Commits to prepare for future dev and push. 112 | next_version = self._get_next_version(version) 113 | self.log.info("prepare for future dev (version %s)", next_version) 114 | marker = "## %s %s\n" % (self.proj_name, version) 115 | if marker not in changes_txt: 116 | raise MkError("couldn't find `%s' marker in `%s' " 117 | "content: can't prep for subsequent dev" % (marker, changes_path)) 118 | changes_txt = changes_txt.replace("## %s %s\n" % (self.proj_name, version), 119 | "## %s %s (not yet released)\n\n(nothing yet)\n\n## %s %s\n" % ( 120 | self.proj_name, next_version, self.proj_name, version)) 121 | if not DRY_RUN: 122 | f = codecs.open(changes_path, 'w', 'utf-8') 123 | f.write(changes_txt) 124 | f.close() 125 | 126 | ver_path = join(self.dir, normpath(self.version_py_path)) 127 | ver_content = codecs.open(ver_path, 'r', 'utf-8').read() 128 | version_tuple = self._tuple_from_version(version) 129 | next_version_tuple = self._tuple_from_version(next_version) 130 | marker = "__version_info__ = %r" % (version_tuple,) 131 | if marker not in ver_content: 132 | raise MkError("couldn't find `%s' version marker in `%s' " 133 | "content: can't prep for subsequent dev" % (marker, ver_path)) 134 | ver_content = ver_content.replace(marker, 135 | "__version_info__ = %r" % (next_version_tuple,)) 136 | if not DRY_RUN: 137 | f = codecs.open(ver_path, 'w', 'utf-8') 138 | f.write(ver_content) 139 | f.close() 140 | 141 | if not DRY_RUN: 142 | sh.run('git commit %s %s -m "prep for future dev"' % ( 143 | changes_path, ver_path)) 144 | sh.run('git push') 145 | 146 | def _tuple_from_version(self, version): 147 | def _intify(s): 148 | try: 149 | return int(s) 150 | except ValueError: 151 | return s 152 | return tuple(_intify(b) for b in version.split('.')) 153 | 154 | def _get_next_version(self, version): 155 | last_bit = version.rsplit('.', 1)[-1] 156 | try: 157 | last_bit = int(last_bit) 158 | except ValueError: # e.g. "1a2" 159 | last_bit = int(re.split('[abc]', last_bit, 1)[-1]) 160 | return version[:-len(str(last_bit))] + str(last_bit + 1) 161 | 162 | def _get_version(self): 163 | lib_dir = join(dirname(abspath(__file__)), "lib") 164 | sys.path.insert(0, lib_dir) 165 | try: 166 | mod = __import__(self.version_module) 167 | return mod.__version__ 168 | finally: 169 | del sys.path[0] 170 | 171 | 172 | class clean(Task): 173 | """Clean generated files and dirs.""" 174 | def make(self): 175 | patterns = [ 176 | "dist", 177 | "build", 178 | "MANIFEST", 179 | "*.pyc", 180 | "lib/*.pyc", 181 | ] 182 | for pattern in patterns: 183 | p = join(self.dir, pattern) 184 | for path in glob(p): 185 | sh.rm(path, log=self.log) 186 | 187 | class sdist(Task): 188 | """python setup.py sdist""" 189 | def make(self): 190 | sh.run_in_dir("%spython setup.py sdist --formats zip" 191 | % _setup_command_prefix(), 192 | self.dir, self.log.debug) 193 | 194 | class pypi_upload(Task): 195 | """Upload release to pypi.""" 196 | def make(self): 197 | sh.run_in_dir("%spython setup.py sdist --formats zip upload" 198 | % _setup_command_prefix(), 199 | self.dir, self.log.debug) 200 | 201 | sys.path.insert(0, join(self.dir, "lib")) 202 | url = "http://pypi.python.org/pypi/django-markdown-deux/" 203 | import webbrowser 204 | webbrowser.open_new(url) 205 | 206 | 207 | class todo(Task): 208 | """Print out todo's and xxx's in the docs area.""" 209 | def make(self): 210 | for path in _paths_from_path_patterns(['.'], 211 | excludes=[".svn", "*.pyc", "TO""DO.txt", "Makefile.py", 212 | "*.png", "*.gif", "*.pprint", "*.prof", 213 | "tmp*"]): 214 | self._dump_pattern_in_path("TO\DO\\|XX\X", path) 215 | 216 | def _dump_pattern_in_path(self, pattern, path): 217 | os.system("grep -nH '%s' '%s'" % (pattern, path)) 218 | 219 | 220 | 221 | #---- internal support stuff 222 | 223 | ## {{{ http://code.activestate.com/recipes/577058/ (r2) 224 | def query_yes_no(question, default="yes"): 225 | """Ask a yes/no question via raw_input() and return their answer. 226 | 227 | "question" is a string that is presented to the user. 228 | "default" is the presumed answer if the user just hits . 229 | It must be "yes" (the default), "no" or None (meaning 230 | an answer is required of the user). 231 | 232 | The "answer" return value is one of "yes" or "no". 233 | """ 234 | valid = {"yes":"yes", "y":"yes", "ye":"yes", 235 | "no":"no", "n":"no"} 236 | if default == None: 237 | prompt = " [y/n] " 238 | elif default == "yes": 239 | prompt = " [Y/n] " 240 | elif default == "no": 241 | prompt = " [y/N] " 242 | else: 243 | raise ValueError("invalid default answer: '%s'" % default) 244 | 245 | while 1: 246 | sys.stdout.write(question + prompt) 247 | choice = raw_input().lower() 248 | if default is not None and choice == '': 249 | return default 250 | elif choice in valid.keys(): 251 | return valid[choice] 252 | else: 253 | sys.stdout.write("Please respond with 'yes' or 'no' "\ 254 | "(or 'y' or 'n').\n") 255 | ## end of http://code.activestate.com/recipes/577058/ }}} 256 | 257 | 258 | ## {{{ http://code.activestate.com/recipes/577230/ (r2) 259 | def _should_include_path(path, includes, excludes): 260 | """Return True iff the given path should be included.""" 261 | from os.path import basename 262 | from fnmatch import fnmatch 263 | 264 | base = basename(path) 265 | if includes: 266 | for include in includes: 267 | if fnmatch(base, include): 268 | try: 269 | log.debug("include `%s' (matches `%s')", path, include) 270 | except (NameError, AttributeError): 271 | pass 272 | break 273 | else: 274 | try: 275 | log.debug("exclude `%s' (matches no includes)", path) 276 | except (NameError, AttributeError): 277 | pass 278 | return False 279 | for exclude in excludes: 280 | if fnmatch(base, exclude): 281 | try: 282 | log.debug("exclude `%s' (matches `%s')", path, exclude) 283 | except (NameError, AttributeError): 284 | pass 285 | return False 286 | return True 287 | 288 | def _walk(top, topdown=True, onerror=None, follow_symlinks=False): 289 | """A version of `os.walk()` with a couple differences regarding symlinks. 290 | 291 | 1. follow_symlinks=False (the default): A symlink to a dir is 292 | returned as a *non*-dir. In `os.walk()`, a symlink to a dir is 293 | returned in the *dirs* list, but it is not recursed into. 294 | 2. follow_symlinks=True: A symlink to a dir is returned in the 295 | *dirs* list (as with `os.walk()`) but it *is conditionally* 296 | recursed into (unlike `os.walk()`). 297 | 298 | A symlinked dir is only recursed into if it is to a deeper dir 299 | within the same tree. This is my understanding of how `find -L 300 | DIR` works. 301 | 302 | TODO: put as a separate recipe 303 | """ 304 | import os 305 | from os.path import join, isdir, islink, abspath 306 | 307 | # We may not have read permission for top, in which case we can't 308 | # get a list of the files the directory contains. os.path.walk 309 | # always suppressed the exception then, rather than blow up for a 310 | # minor reason when (say) a thousand readable directories are still 311 | # left to visit. That logic is copied here. 312 | try: 313 | names = os.listdir(top) 314 | except OSError, err: 315 | if onerror is not None: 316 | onerror(err) 317 | return 318 | 319 | dirs, nondirs = [], [] 320 | if follow_symlinks: 321 | for name in names: 322 | if isdir(join(top, name)): 323 | dirs.append(name) 324 | else: 325 | nondirs.append(name) 326 | else: 327 | for name in names: 328 | path = join(top, name) 329 | if islink(path): 330 | nondirs.append(name) 331 | elif isdir(path): 332 | dirs.append(name) 333 | else: 334 | nondirs.append(name) 335 | 336 | if topdown: 337 | yield top, dirs, nondirs 338 | for name in dirs: 339 | path = join(top, name) 340 | if follow_symlinks and islink(path): 341 | # Only walk this path if it links deeper in the same tree. 342 | top_abs = abspath(top) 343 | link_abs = abspath(join(top, os.readlink(path))) 344 | if not link_abs.startswith(top_abs + os.sep): 345 | continue 346 | for x in _walk(path, topdown, onerror, follow_symlinks=follow_symlinks): 347 | yield x 348 | if not topdown: 349 | yield top, dirs, nondirs 350 | 351 | _NOT_SPECIFIED = ("NOT", "SPECIFIED") 352 | def _paths_from_path_patterns(path_patterns, files=True, dirs="never", 353 | recursive=True, includes=[], excludes=[], 354 | skip_dupe_dirs=False, 355 | follow_symlinks=False, 356 | on_error=_NOT_SPECIFIED): 357 | """_paths_from_path_patterns([, ...]) -> file paths 358 | 359 | Generate a list of paths (files and/or dirs) represented by the given path 360 | patterns. 361 | 362 | "path_patterns" is a list of paths optionally using the '*', '?' and 363 | '[seq]' glob patterns. 364 | "files" is boolean (default True) indicating if file paths 365 | should be yielded 366 | "dirs" is string indicating under what conditions dirs are 367 | yielded. It must be one of: 368 | never (default) never yield dirs 369 | always yield all dirs matching given patterns 370 | if-not-recursive only yield dirs for invocations when 371 | recursive=False 372 | See use cases below for more details. 373 | "recursive" is boolean (default True) indicating if paths should 374 | be recursively yielded under given dirs. 375 | "includes" is a list of file patterns to include in recursive 376 | searches. 377 | "excludes" is a list of file and dir patterns to exclude. 378 | (Note: This is slightly different than GNU grep's --exclude 379 | option which only excludes *files*. I.e. you cannot exclude 380 | a ".svn" dir.) 381 | "skip_dupe_dirs" can be set True to watch for and skip 382 | descending into a dir that has already been yielded. Note 383 | that this currently does not dereference symlinks. 384 | "follow_symlinks" is a boolean indicating whether to follow 385 | symlinks (default False). To guard against infinite loops 386 | with circular dir symlinks, only dir symlinks to *deeper* 387 | dirs are followed. 388 | "on_error" is an error callback called when a given path pattern 389 | matches nothing: 390 | on_error(PATH_PATTERN) 391 | If not specified, the default is look for a "log" global and 392 | call: 393 | log.error("`%s': No such file or directory") 394 | Specify None to do nothing. 395 | 396 | Typically this is useful for a command-line tool that takes a list 397 | of paths as arguments. (For Unix-heads: the shell on Windows does 398 | NOT expand glob chars, that is left to the app.) 399 | 400 | Use case #1: like `grep -r` 401 | {files=True, dirs='never', recursive=(if '-r' in opts)} 402 | script FILE # yield FILE, else call on_error(FILE) 403 | script DIR # yield nothing 404 | script PATH* # yield all files matching PATH*; if none, 405 | # call on_error(PATH*) callback 406 | script -r DIR # yield files (not dirs) recursively under DIR 407 | script -r PATH* # yield files matching PATH* and files recursively 408 | # under dirs matching PATH*; if none, call 409 | # on_error(PATH*) callback 410 | 411 | Use case #2: like `file -r` (if it had a recursive option) 412 | {files=True, dirs='if-not-recursive', recursive=(if '-r' in opts)} 413 | script FILE # yield FILE, else call on_error(FILE) 414 | script DIR # yield DIR, else call on_error(DIR) 415 | script PATH* # yield all files and dirs matching PATH*; if none, 416 | # call on_error(PATH*) callback 417 | script -r DIR # yield files (not dirs) recursively under DIR 418 | script -r PATH* # yield files matching PATH* and files recursively 419 | # under dirs matching PATH*; if none, call 420 | # on_error(PATH*) callback 421 | 422 | Use case #3: kind of like `find .` 423 | {files=True, dirs='always', recursive=(if '-r' in opts)} 424 | script FILE # yield FILE, else call on_error(FILE) 425 | script DIR # yield DIR, else call on_error(DIR) 426 | script PATH* # yield all files and dirs matching PATH*; if none, 427 | # call on_error(PATH*) callback 428 | script -r DIR # yield files and dirs recursively under DIR 429 | # (including DIR) 430 | script -r PATH* # yield files and dirs matching PATH* and recursively 431 | # under dirs; if none, call on_error(PATH*) 432 | # callback 433 | 434 | TODO: perf improvements (profile, stat just once) 435 | """ 436 | from os.path import basename, exists, isdir, join, normpath, abspath, \ 437 | lexists, islink, realpath 438 | from glob import glob 439 | 440 | assert not isinstance(path_patterns, basestring), \ 441 | "'path_patterns' must be a sequence, not a string: %r" % path_patterns 442 | GLOB_CHARS = '*?[' 443 | 444 | if skip_dupe_dirs: 445 | searched_dirs = set() 446 | 447 | for path_pattern in path_patterns: 448 | # Determine the set of paths matching this path_pattern. 449 | for glob_char in GLOB_CHARS: 450 | if glob_char in path_pattern: 451 | paths = glob(path_pattern) 452 | break 453 | else: 454 | if follow_symlinks: 455 | paths = exists(path_pattern) and [path_pattern] or [] 456 | else: 457 | paths = lexists(path_pattern) and [path_pattern] or [] 458 | if not paths: 459 | if on_error is None: 460 | pass 461 | elif on_error is _NOT_SPECIFIED: 462 | try: 463 | log.error("`%s': No such file or directory", path_pattern) 464 | except (NameError, AttributeError): 465 | pass 466 | else: 467 | on_error(path_pattern) 468 | 469 | for path in paths: 470 | if (follow_symlinks or not islink(path)) and isdir(path): 471 | if skip_dupe_dirs: 472 | canon_path = normpath(abspath(path)) 473 | if follow_symlinks: 474 | canon_path = realpath(canon_path) 475 | if canon_path in searched_dirs: 476 | continue 477 | else: 478 | searched_dirs.add(canon_path) 479 | 480 | # 'includes' SHOULD affect whether a dir is yielded. 481 | if (dirs == "always" 482 | or (dirs == "if-not-recursive" and not recursive) 483 | ) and _should_include_path(path, includes, excludes): 484 | yield path 485 | 486 | # However, if recursive, 'includes' should NOT affect 487 | # whether a dir is recursed into. Otherwise you could 488 | # not: 489 | # script -r --include="*.py" DIR 490 | if recursive and _should_include_path(path, [], excludes): 491 | for dirpath, dirnames, filenames in _walk(path, 492 | follow_symlinks=follow_symlinks): 493 | dir_indeces_to_remove = [] 494 | for i, dirname in enumerate(dirnames): 495 | d = join(dirpath, dirname) 496 | if skip_dupe_dirs: 497 | canon_d = normpath(abspath(d)) 498 | if follow_symlinks: 499 | canon_d = realpath(canon_d) 500 | if canon_d in searched_dirs: 501 | dir_indeces_to_remove.append(i) 502 | continue 503 | else: 504 | searched_dirs.add(canon_d) 505 | if dirs == "always" \ 506 | and _should_include_path(d, includes, excludes): 507 | yield d 508 | if not _should_include_path(d, [], excludes): 509 | dir_indeces_to_remove.append(i) 510 | for i in reversed(dir_indeces_to_remove): 511 | del dirnames[i] 512 | if files: 513 | for filename in sorted(filenames): 514 | f = join(dirpath, filename) 515 | if _should_include_path(f, includes, excludes): 516 | yield f 517 | 518 | elif files and _should_include_path(path, includes, excludes): 519 | yield path 520 | ## end of http://code.activestate.com/recipes/577230/ }}} 521 | 522 | 523 | _g_version = None 524 | def _get_version(): 525 | global _g_version 526 | if _g_version is None: 527 | sys.path.insert(0, join(dirname(__file__), "lib")) 528 | try: 529 | import cmdln 530 | _g_version = cmdln.__version__ 531 | finally: 532 | del sys.path[0] 533 | return _g_version 534 | 535 | def _setup_command_prefix(): 536 | prefix = "" 537 | if sys.platform == "darwin": 538 | # http://forums.macosxhints.com/archive/index.php/t-43243.html 539 | # This is an Apple customization to `tar` to avoid creating 540 | # '._foo' files for extended-attributes for archived files. 541 | prefix = "COPY_EXTENDED_ATTRIBUTES_DISABLE=1 " 542 | return prefix 543 | 544 | def _capture_stdout(argv): 545 | import subprocess 546 | p = subprocess.Popen(argv, stdout=subprocess.PIPE) 547 | return p.communicate()[0] 548 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A small Django app that provides template tags for using 2 | [Markdown](http://daringfireball.net/projects/markdown/) using the 3 | [python-markdown2](https://github.com/trentm/python-markdown2) library. 4 | 5 | # What's with the "deux" in the name? 6 | 7 | The obvious name for this project is `django-markdown2`. However, there 8 | [already is one!](http://github.com/svetlyak40wt/django-markdown2) and name 9 | confusion doesn't help anybody. Plus, I took French immersion in school for 12 10 | years: might as well put it to use. 11 | 12 | # So why another project then? 13 | 14 | Because I wanted to do something slightly different. Django-markdown2's 15 | `markdown` filter takes 16 | ["extras"](https://github.com/trentm/python-markdown2/wiki/Extras) as arguments 17 | -- with the one exception that "safe" is transformed to python-markdown2's 18 | `safe_mode` argument. This is handy for quick usage. My use case is more 19 | commonly: lots of `markdown` filter and block usage in my Django templates with 20 | the same set of python-markdown2 options. 21 | 22 | 23 | # Installation 24 | 25 | Choose the *one* of the following that works best for you: 26 | 27 | - Install the latest release from PyPI: 28 | 29 | pip install django-markdown-deux 30 | 31 | or, if you use [ActivePython](http://www.activestate.com/activepython): 32 | 33 | pypm install django-markdown-deux 34 | 35 | These should install the dependent `python-markdown2` package. 36 | 37 | - Get a git clone of the source tree: 38 | 39 | git clone git://github.com/trentm/django-markdown-deux.git 40 | 41 | You might want a particular tag: 42 | 43 | cd django-markdown-deux 44 | git tag -l # list available tags 45 | git checkout $tagname 46 | 47 | Then you'll need the "lib" subdir on your PYTHONPATH: 48 | 49 | python setup.py install # or 'export PYTHONPATH=`pwd`/lib:$PYTHONPATH' 50 | 51 | You'll also need the [python-markdown2 52 | library](https://github.com/trentm/python-markdown2): 53 | 54 | git clone git@github.com:trentm/python-markdown2.git 55 | cd python-markdown2 56 | python setup.py install # or 'export PYTHONPATH=`pwd`/python-markdown2/lib' 57 | 58 | 59 | # Django project setup 60 | 61 | 1. Add `markdown_deux` to `INSTALLED_APPS` in your project's "settings.py". 62 | 63 | 2. Optionally set some of the `MARKDOWN_DEUX_*` settings. See the "Settings" 64 | section below. 65 | 66 | 67 | # Usage 68 | 69 | The `markdown_deux` facilities typically take an optional "style" argument. This 70 | is a name for a set of options to the `python-markdown2` processor. There is 71 | a "default" style that is used if no argument is given. See the 72 | `MARKDOWN_DEUX_STYLES` setting below for more. 73 | 74 | ## `markdown` template filter 75 | 76 | {% load markdown_deux_tags %} 77 | ... 78 | {{ myvar|markdown:"STYLE" }} {# convert `myvar` to HTML using the "STYLE" style #} 79 | {{ myvar|markdown }} {# same as `{{ myvar|markdown:"default"}}` #} 80 | 81 | ## `markdown` template block tag 82 | 83 | {% load markdown_deux_tags %} 84 | ... 85 | {% markdown STYLE %} {# can omit "STYLE" to use the "default" style #} 86 | This is some **cool** 87 | [Markdown](http://daringfireball.net/projects/markdown/) 88 | text here. 89 | {% endmarkdown %} 90 | 91 | ## `markdown_allowed` template tag 92 | 93 | In a template: 94 | 95 | {% markdown_allowed %} 96 | 97 | will emit a short HTML blurb that says Markdown syntax is allowed. This can be 98 | handy for placing under form elements that accept markdown syntax. You can also 99 | use it as the `help_text` for a form field something like: 100 | 101 | # myapp/forms.py 102 | from markdown_deux.templatetags.markdown_deux_tags import markdown_allowed 103 | class MyForm(forms.Form): 104 | #... 105 | description = forms.CharField( 106 | label="Description (required)", 107 | widget=forms.Textarea(attrs={"rows": 5}), 108 | help_text=_secondary_span("A brief description of your thing.
" 109 | + markdown_allowed()), 110 | required=True) 111 | 112 | 113 | ## `markdown_cheatsheet` tag 114 | 115 | {% markdown_cheatsheet %} 116 | 117 | This outputs HTML giving a narrow (appropriate for, e.g., a sidebar) listing of 118 | some of the more common Markdown features. 119 | 120 | 121 | ## `markdown_deux.markdown(TEXT, STYLE)` in your Python code 122 | 123 | The `markdown` filter and block tags above ultimately use this 124 | `markdown_deux.markdown(...)` function. You might find it useful to do Markdown 125 | processing in your Python code (e.g. in a view, in a model `.save()` method). 126 | 127 | 128 | # Settings 129 | 130 | All settings for this app are optional. 131 | 132 | ## `MARKDOWN_DEUX_STYLES` setting 133 | 134 | A mapping of style name to a dict of keyword arguments for python-markdown2's 135 | `markdown2.markdown(text, **kwargs)`. For example the default setting is 136 | effectively: 137 | 138 | MARKDOWN_DEUX_STYLES = { 139 | "default": { 140 | "extras": { 141 | "code-friendly": None, 142 | }, 143 | "safe_mode": "escape", 144 | }, 145 | } 146 | 147 | I.e. only the "default" style is defined and it just uses the [code-friendly 148 | extra](https://github.com/trentm/python-markdown2/wiki/code-friendly) and escapes 149 | raw HTML in the given Markdown (for safety). 150 | 151 | Here is how you might add styles of your own, and preserve the default style: 152 | 153 | # settings.py 154 | from markdown_deux.conf.settings import MARKDOWN_DEUX_DEFAULT_STYLE 155 | 156 | MARKDOWN_DEUX_STYLES = { 157 | "default": MARKDOWN_DEUX_DEFAULT_STYLE, 158 | "trusted": { 159 | "extras": { 160 | "code-friendly": None, 161 | }, 162 | # Allow raw HTML (WARNING: don't use this for user-generated 163 | # Markdown for your site!). 164 | "safe_mode": False, 165 | }, 166 | # Here is what http://code.activestate.com/recipes/ currently uses. 167 | "recipe": { 168 | "extras": { 169 | "code-friendly": None, 170 | }, 171 | "safe_mode": "escape", 172 | "link_patterns": [ 173 | # Transform "Recipe 123" in a link. 174 | (re.compile(r"recipe\s+#?(\d+)\b", re.I), 175 | r"http://code.activestate.com/recipes/\1/"), 176 | ], 177 | "extras": { 178 | "code-friendly": None, 179 | "pyshell": None, 180 | "demote-headers": 3, 181 | "link-patterns": None, 182 | # `class` attribute put on `pre` tags to enable using 183 | # for syntax 184 | # highlighting. 185 | "html-classes": {"pre": "prettyprint"}, 186 | "cuddled-lists": None, 187 | "footnotes": None, 188 | "header-ids": None, 189 | }, 190 | "safe_mode": "escape", 191 | } 192 | } 193 | 194 | 195 | ## `MARKDOWN_DEUX_HELP_URL` setting 196 | 197 | A URL for to which to link for full markdown syntax default. This link is 198 | only in the output of the `markdown_allowed` and `markdown_cheatsheet` 199 | template tags. 200 | 201 | The default is , the 202 | canonical Markdown syntax reference. However, if your site uses Markdown with 203 | specific tweaks, you may prefer to have your own override. For example, 204 | [ActiveState Code](http://code.activestate.com) uses: 205 | 206 | MARKDOWN_DEUX_HELP_URL = "/help/markdown/" 207 | 208 | To link to [its own Markdown syntax notes 209 | URL](http://code.activestate.com/help/markdown/). 210 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - setup.py and release to pypi 2 | - TODOs in README 3 | - announce elsewhere? django-users? django-hotapps? 4 | - TODO: move Makefile.py's parameterized `cut_a_release` back to eol's Makefile 5 | -------------------------------------------------------------------------------- /lib/markdown_deux/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2008-2010 ActiveState Corp. 3 | # License: MIT (http://www.opensource.org/licenses/mit-license.php) 4 | 5 | r"""A small Django app that provides template tags for Markdown using the 6 | python-markdown2 library. 7 | 8 | See for more info. 9 | """ 10 | 11 | __version_info__ = (1, 0, 6) 12 | __version__ = '.'.join(map(str, __version_info__)) 13 | __author__ = "Trent Mick" 14 | 15 | 16 | def markdown(text, style="default"): 17 | if not text: 18 | return "" 19 | import markdown2 20 | return markdown2.markdown(text, **get_style(style)) 21 | 22 | def get_style(style): 23 | from markdown_deux.conf import settings 24 | try: 25 | return settings.MARKDOWN_DEUX_STYLES[style] 26 | except KeyError: 27 | return settings.MARKDOWN_DEUX_STYLES.get("default", 28 | settings.MARKDOWN_DEUX_DEFAULT_STYLE) 29 | -------------------------------------------------------------------------------- /lib/markdown_deux/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trentm/django-markdown-deux/7383192c671749a2c7aa02bb12acc5d2d648ae14/lib/markdown_deux/conf/__init__.py -------------------------------------------------------------------------------- /lib/markdown_deux/conf/settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 ActiveState Software Inc. 2 | 3 | from django.conf import settings 4 | 5 | MARKDOWN_DEUX_HELP_URL = getattr(settings, "MARKDOWN_DEUX_HELP_URL", 6 | "http://daringfireball.net/projects/markdown/syntax") 7 | 8 | MARKDOWN_DEUX_DEFAULT_STYLE = { 9 | "extras": { 10 | "code-friendly": None, 11 | }, 12 | "safe_mode": "escape", 13 | } 14 | 15 | MARKDOWN_DEUX_STYLES = getattr(settings, "MARKDOWN_DEUX_STYLES", 16 | {"default": MARKDOWN_DEUX_DEFAULT_STYLE}) 17 | 18 | DEBUG = settings.DEBUG 19 | -------------------------------------------------------------------------------- /lib/markdown_deux/templates/markdown_deux/markdown_cheatsheet.html: -------------------------------------------------------------------------------- 1 |
2 |

Markdown Cheatsheet

3 | 4 |

Phrase Emphasis

5 | 6 |
*italic*   **bold**
7 | 8 |

Links

9 | 10 |

Inline:

11 | 12 |
A [link](http://url.com/)
13 | A [link](http://url.com/
14 | "with a title")
15 | 
16 | 17 |

Reference-style labels:

18 | 19 |
A [link][id]. Then, anywhere
20 | else, define the link:
21 | 
22 | [id]: http://url.com/ "title"
23 | 
24 | 25 |

Code Spans

26 | 27 |
`<code>` spans are delimited
28 | by backticks.
29 | 
30 | You can include literal
31 | backticks like `` `this` ``.
32 | 
33 | 34 |

Preformatted Code Blocks

35 | 36 |

Indent every line of a code block by at least 4 spaces.

37 | 38 |
This is a normal paragraph.
39 | 
40 |     This is a preformatted
41 |     code block.
42 | 
43 | 44 |

Lists

45 | 46 |
1. an ordered
47 | 2. list
48 | 
49 | * an unordered list
50 | * '-' bullets work too
51 | 
52 | 53 |

Headers

54 | 55 |
# Header 1
56 | 
57 | ## Header 2
58 | 
59 | ###### Header 6
60 | 
61 | 62 |

Blockquotes

63 | 64 |
> Email-style angle brackets
65 | > are used for blockquotes.
66 | 
67 | > > And, they can be nested.
68 | 
69 | > ## Headers in blockquotes
70 | > 
71 | > * You can quote a list.
72 | > * Etc.
73 | 
74 | 75 |

Manual Line Breaks

76 | 77 |

End a line with two or more spaces:

78 | 79 |
Roses are red,   
80 | Violets are blue.
81 | 
82 | 83 |

More markdown syntax help here.

84 | 85 |
86 | -------------------------------------------------------------------------------- /lib/markdown_deux/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trentm/django-markdown-deux/7383192c671749a2c7aa02bb12acc5d2d648ae14/lib/markdown_deux/templatetags/__init__.py -------------------------------------------------------------------------------- /lib/markdown_deux/templatetags/markdown_deux_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.safestring import mark_safe 3 | try: 4 | from django.utils.encoding import force_text 5 | except ImportError: 6 | from django.utils.encoding import force_str as force_text 7 | 8 | import markdown_deux 9 | from markdown_deux.conf import settings 10 | 11 | 12 | 13 | register = template.Library() 14 | 15 | 16 | @register.filter(name="markdown") 17 | def markdown_filter(value, style="default"): 18 | """Processes the given value as Markdown, optionally using a particular 19 | Markdown style/config 20 | 21 | Syntax:: 22 | 23 | {{ value|markdown }} {# uses the "default" style #} 24 | {{ value|markdown:"mystyle" }} 25 | 26 | Markdown "styles" are defined by the `MARKDOWN_DEUX_STYLES` setting. 27 | """ 28 | try: 29 | return mark_safe(markdown_deux.markdown(value, style)) 30 | except ImportError: 31 | if settings.DEBUG: 32 | raise template.TemplateSyntaxError("Error in `markdown` filter: " 33 | "The python-markdown2 library isn't installed.") 34 | return force_text(value) 35 | markdown_filter.is_safe = True 36 | 37 | 38 | @register.tag(name="markdown") 39 | def markdown_tag(parser, token): 40 | nodelist = parser.parse(('endmarkdown',)) 41 | bits = token.split_contents() 42 | if len(bits) == 1: 43 | style = "default" 44 | elif len(bits) == 2: 45 | style = bits[1] 46 | else: 47 | raise template.TemplateSyntaxError("`markdown` tag requires exactly " 48 | "zero or one arguments") 49 | parser.delete_first_token() # consume '{% endmarkdown %}' 50 | return MarkdownNode(style, nodelist) 51 | 52 | class MarkdownNode(template.Node): 53 | def __init__(self, style, nodelist): 54 | self.style = style 55 | self.nodelist = nodelist 56 | def render(self, context): 57 | value = self.nodelist.render(context) 58 | try: 59 | return mark_safe(markdown_deux.markdown(value, self.style)) 60 | except ImportError: 61 | if settings.DEBUG: 62 | raise template.TemplateSyntaxError("Error in `markdown` tag: " 63 | "The python-markdown2 library isn't installed.") 64 | return force_text(value) 65 | 66 | 67 | @register.inclusion_tag("markdown_deux/markdown_cheatsheet.html") 68 | def markdown_cheatsheet(): 69 | return {"help_url": settings.MARKDOWN_DEUX_HELP_URL} 70 | 71 | 72 | @register.simple_tag 73 | def markdown_allowed(): 74 | return ('Markdown syntax allowed, but no raw HTML. ' 75 | 'Examples: **bold**, *italic*, indent 4 spaces for a code block.' 76 | % settings.MARKDOWN_DEUX_HELP_URL) 77 | 78 | 79 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os 5 | from setuptools import setup, find_packages 6 | 7 | 8 | 9 | _top_dir = os.path.dirname(os.path.abspath(__file__)) 10 | sys.path.insert(0, os.path.join(_top_dir, "lib")) 11 | try: 12 | import markdown_deux 13 | finally: 14 | del sys.path[0] 15 | README = open(os.path.join(_top_dir, 'README.md')).read() 16 | 17 | setup(name='django-markdown-deux', 18 | version=markdown_deux.__version__, 19 | description="a Django app that provides template tags for using Markdown (using the python-markdown2 processor)", 20 | long_description=README, 21 | classifiers=[c.strip() for c in """ 22 | Development Status :: 5 - Production/Stable 23 | Environment :: Web Environment 24 | Framework :: Django 25 | Intended Audience :: Developers 26 | License :: OSI Approved :: MIT License 27 | Operating System :: OS Independent 28 | Programming Language :: Python :: 3 29 | Topic :: Internet :: WWW/HTTP 30 | """.split('\n') if c.strip()], 31 | keywords='django markdown markdown2 text markup html', 32 | author='Trent Mick', 33 | author_email='trentm@gmail.com', 34 | maintainer='Riccardo Magliocchetti', 35 | maintainer_email='riccardo.magliocchetti@gmail.com', 36 | url='http://github.com/trentm/django-markdown-deux', 37 | license='MIT', 38 | install_requires = ['markdown2'], 39 | packages=["markdown_deux"], 40 | package_dir={"": "lib"}, 41 | include_package_data=True, 42 | zip_safe=False, 43 | ) 44 | 45 | --------------------------------------------------------------------------------