├── src └── markdoc │ ├── cli │ ├── __init__.py │ ├── parser.py │ ├── main.py │ └── commands.py │ ├── static │ ├── default-templates │ │ ├── 404.html │ │ ├── base.html │ │ ├── listing.html │ │ ├── document.html │ │ ├── markdoc-default │ │ │ ├── document.html │ │ │ ├── 404.html │ │ │ ├── listing.html │ │ │ └── base.html │ │ └── macros │ │ │ ├── crumbs │ │ │ └── html │ └── default-static │ │ └── media │ │ ├── sass │ │ ├── style.sass │ │ ├── _typography.sass │ │ └── _layout.sass │ │ └── css │ │ ├── pygments.css │ │ └── style.css │ ├── exc.py │ ├── templates.py │ ├── server.py │ ├── directories.py │ ├── __init__.py │ ├── render.py │ ├── cache.py │ ├── config.py │ ├── wsgi.py │ └── builder.py ├── test ├── example │ ├── wiki │ │ ├── an_empty_file.md │ │ ├── file1.md │ │ ├── file2.md │ │ ├── file3.md │ │ └── subdir │ │ │ └── hello.md │ ├── static │ │ └── example.css │ ├── markdoc.yaml │ └── _templates │ │ ├── document.html │ │ └── listing.html ├── cache_fixture.py ├── cli_fixture.py ├── listing_fixture.py ├── config_fixture.py ├── builder_fixture.py ├── common.py ├── config.doctest ├── cache.doctest ├── cli.doctest ├── listing.doctest └── builder.doctest ├── REQUIREMENTS ├── nose.cfg ├── .gitignore ├── MANIFEST.in ├── doc ├── .markdoc.yaml ├── tips │ ├── apache.md │ ├── barebones.md │ └── syntax-highlighting.md ├── authoring.md ├── ref │ ├── layout.md │ ├── configuration.md │ └── markup.md ├── internals │ ├── development.md │ └── rendering.md ├── quickstart.md ├── index.md └── about.md ├── UNLICENSE ├── setup.py ├── README.md └── distribute_setup.py /src/markdoc/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/example/wiki/an_empty_file.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/example/wiki/file1.md: -------------------------------------------------------------------------------- 1 | # Hello 2 | -------------------------------------------------------------------------------- /test/example/wiki/file2.md: -------------------------------------------------------------------------------- 1 | # World 2 | -------------------------------------------------------------------------------- /test/example/wiki/file3.md: -------------------------------------------------------------------------------- 1 | # Foo 2 | -------------------------------------------------------------------------------- /test/example/wiki/subdir/hello.md: -------------------------------------------------------------------------------- 1 | # Hello again. 2 | -------------------------------------------------------------------------------- /test/example/static/example.css: -------------------------------------------------------------------------------- 1 | /* Nothing to see here. */ 2 | -------------------------------------------------------------------------------- /REQUIREMENTS: -------------------------------------------------------------------------------- 1 | argparse 2 | CherryPy 3 | Jinja2 4 | Markdown 5 | Pygments 6 | PyYAML 7 | webob -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'markdoc-default/404.html' %} 2 | -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'markdoc-default/base.html' %} 2 | -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/listing.html: -------------------------------------------------------------------------------- 1 | {% extends 'markdoc-default/listing.html' %} 2 | -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/document.html: -------------------------------------------------------------------------------- 1 | {% extends 'markdoc-default/document.html' %} 2 | -------------------------------------------------------------------------------- /src/markdoc/static/default-static/media/sass/style.sass: -------------------------------------------------------------------------------- 1 | @import _layout.sass 2 | @import _typography.sass 3 | -------------------------------------------------------------------------------- /test/cache_fixture.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from builder_fixture import setup_test, teardown_test 4 | -------------------------------------------------------------------------------- /test/cli_fixture.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from builder_fixture import setup_test, teardown_test 4 | -------------------------------------------------------------------------------- /test/listing_fixture.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from builder_fixture import setup_test, teardown_test 4 | -------------------------------------------------------------------------------- /nose.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-doctest = yes 3 | doctest-tests = yes 4 | doctest-extension = doctest 5 | doctest-fixtures = _fixture 6 | -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/markdoc-default/document.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %}{{ content }}{% endblock %} 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | MANIFEST 4 | .DS_Store 5 | .sass-cache 6 | *.egg-info 7 | *.pyc 8 | distribute-* 9 | doc/.tmp/ 10 | doc/.html/ 11 | -------------------------------------------------------------------------------- /test/example/markdoc.yaml: -------------------------------------------------------------------------------- 1 | hide-prefix: "_" 2 | use-default-templates: no 3 | use-default-static: no 4 | 9d03e32c6cb1455388a170e3bb0c2030: 7010d5d5871143d089cb662cb540cbd5 5 | -------------------------------------------------------------------------------- /test/config_fixture.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os.path as p 4 | 5 | 6 | def setup_test(test): 7 | test.globs['WIKI_ROOT'] = p.join(p.dirname(p.abspath(__file__)), 'example') 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/markdoc/static/ * 2 | include distribute_setup.py 3 | include REQUIREMENTS 4 | global-exclude .DS_Store 5 | prune src/markdoc/static/default-static/media/sass/.sass-cache 6 | -------------------------------------------------------------------------------- /doc/.markdoc.yaml: -------------------------------------------------------------------------------- 1 | wiki-name: Markdoc Documentation 2 | google-analytics: UA-9915287-4 3 | 4 | wiki-dir: "." 5 | static-dir: ".static" 6 | 7 | markdown: 8 | extensions: 9 | - codehilite 10 | - def_list 11 | - headerid 12 | - toc 13 | 14 | -------------------------------------------------------------------------------- /src/markdoc/exc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | class MarkdocError(Exception): 5 | """An error occurred whilst running the markdoc utility.""" 6 | pass 7 | 8 | 9 | class AbortError(MarkdocError): 10 | """An exception occurred which should cause Markdoc to abort.""" 11 | pass 12 | 13 | -------------------------------------------------------------------------------- /test/example/_templates/document.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | {{ content }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/builder_fixture.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os.path as p 4 | 5 | from common import get_temporary_config, clean_temporary_config 6 | 7 | 8 | def setup_test(test): 9 | test.globs['CONFIG'] = get_temporary_config() 10 | test.globs['WIKI_ROOT'] = p.join(test.globs['CONFIG']['meta.root'], '') 11 | 12 | 13 | def teardown_test(test): 14 | clean_temporary_config(test.globs['CONFIG']) 15 | -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/markdoc-default/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Not Found: {{ request.path_info|e }}{% endblock %} 4 | 5 | {% block content %} 6 |

Not Found: {{ request.path_info|e }}

7 |

8 | We couldn’t find what you were looking for. 9 | {% if not is_index %}You could try going home.{% endif %} 10 |

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/macros/crumbs: -------------------------------------------------------------------------------- 1 | {% macro crumbs(breadcrumbs) %} 2 | {% if breadcrumbs %} 3 | 14 | {% endif %} 15 | {% endmacro %} -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/macros/html: -------------------------------------------------------------------------------- 1 | {% macro cssimport(css_href, media="screen, projection") -%} 2 | 3 | {%- endmacro %} 4 | 5 | {% macro css() -%} 6 | 9 | {%- endmacro %} 10 | 11 | {% macro jsimport(js_href) -%} 12 | 13 | {%- endmacro %} 14 | 15 | {% macro js() -%} 16 | 19 | {%- endmacro %} 20 | -------------------------------------------------------------------------------- /doc/tips/apache.md: -------------------------------------------------------------------------------- 1 | # Serving Markdoc Builds With Apache 2 | 3 | By default, you should just serve your HTML root out of Apache; that will work very simply. You will need a few options to your `.htaccess` file get the same semantics as the built-in web server (the one which runs when you do `markdoc serve`): 4 | 5 | #!text 6 | Options +MultiViews 7 | 8 | 9 | ForceType 'application/xhtml+xml; charset=UTF-8' 10 | 11 | 12 | The first directive will cause requests for `/directory/filename` to look for `directory/filename.html` in your HTML root, allowing for more natural references between pages in your wiki. 13 | 14 | The second part will cause `.html` files to be served as valid UTF-8-encoded XHTML, which Markdoc assumes they are. 15 | -------------------------------------------------------------------------------- /test/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os.path as p 4 | import shutil 5 | import tempfile 6 | 7 | from markdoc.config import Config 8 | 9 | 10 | def get_temporary_config(): 11 | 12 | """ 13 | Return a temporary Markdoc configuration. 14 | 15 | The contents of the wiki will be copied from the example Markdoc wiki. After 16 | you're done with this, you should call `clean_temporary_config()` on the 17 | config object. 18 | """ 19 | 20 | own_config_dir = p.join(p.dirname(p.abspath(__file__)), 'example') + p.sep 21 | temp_config_dir = p.join(tempfile.mkdtemp(), 'example') 22 | shutil.copytree(own_config_dir, temp_config_dir) 23 | return Config.for_directory(temp_config_dir) 24 | 25 | 26 | def clean_temporary_config(config): 27 | """Delete a temporary configuration's wiki root.""" 28 | 29 | shutil.rmtree(p.dirname(config['meta.root'])) 30 | -------------------------------------------------------------------------------- /test/config.doctest: -------------------------------------------------------------------------------- 1 | >>> from markdoc.config import Config 2 | >>> EXAMPLE_KEY = '9d03e32c6cb1455388a170e3bb0c2030' 3 | >>> EXAMPLE_VALUE = '7010d5d5871143d089cb662cb540cbd5' 4 | 5 | You can make a config based on a directory (which will usually be a wiki root): 6 | 7 | >>> dir_config = Config.for_directory(WIKI_ROOT) 8 | >>> dir_config[EXAMPLE_KEY] == EXAMPLE_VALUE 9 | True 10 | 11 | You can also make one based on a YAML filename: 12 | 13 | >>> import os.path as p 14 | >>> file_config = Config.for_file(p.join(WIKI_ROOT, 'markdoc.yaml')) 15 | >>> file_config[EXAMPLE_KEY] == EXAMPLE_VALUE 16 | True 17 | 18 | And if you like, you can make a complete config manually: 19 | 20 | >>> config_dict = { 21 | ... 'meta': {'root': WIKI_ROOT}, 22 | ... EXAMPLE_KEY: EXAMPLE_VALUE 23 | ... } 24 | >>> manual_config = Config(p.join(WIKI_ROOT, 'markdoc.yaml'), config_dict) 25 | >>> manual_config[EXAMPLE_KEY] == EXAMPLE_VALUE 26 | True 27 | -------------------------------------------------------------------------------- /src/markdoc/templates.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os.path as p 4 | 5 | import jinja2 6 | import markdoc 7 | from markdoc.config import Config 8 | 9 | 10 | Config.register_default('use-default-templates', True) 11 | 12 | 13 | def build_template_env(config): 14 | """Build a Jinja2 template environment for a given config.""" 15 | 16 | load_path = [] 17 | 18 | if p.isdir(config.template_dir): 19 | load_path.append(config.template_dir) 20 | 21 | if config['use-default-templates']: 22 | load_path.append(markdoc.default_template_dir) 23 | 24 | environment = jinja2.Environment(loader=jinja2.FileSystemLoader(load_path)) 25 | environment.globals['config'] = config 26 | return environment 27 | 28 | 29 | def template_env(config): 30 | if not getattr(config, '_template_env', None): 31 | config._template_env = build_template_env(config) 32 | return config._template_env 33 | 34 | Config.template_env = property(template_env) 35 | -------------------------------------------------------------------------------- /test/example/_templates/listing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ls /{{ directory }} 6 | 7 | 8 | 9 | 10 |

Listing for /{{ directory }}

11 | 12 | {% if sub_directories %} 13 |

Sub-directories

14 | 19 | {% endif %} 20 | 21 | {% if pages %} 22 |

Pages

23 | 28 | {% endif %} 29 | 30 | {% if files %} 31 |

Files

32 | 37 | {% endif %} 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/markdoc/cli/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | import argparse 6 | 7 | import markdoc 8 | from markdoc.config import Config 9 | 10 | 11 | parser = argparse.ArgumentParser(**{ 12 | 'prog': 'markdoc', 13 | 'description': 'A lightweight Markdown-based wiki build tool.', 14 | }) 15 | 16 | parser.add_argument('-v', '--version', action='version', 17 | version=markdoc.__version__) 18 | 19 | config = parser.add_argument('--config', '-c', default=os.getcwd(), 20 | help="Use the specified Markdoc config (a YAML file or a directory " 21 | "containing markdoc.yaml)") 22 | 23 | log_level = parser.add_argument('--log-level', '-l', metavar='LEVEL', 24 | default='INFO', choices='DEBUG INFO WARN ERROR'.split(), 25 | help="Choose a log level from DEBUG, INFO (default), WARN or ERROR") 26 | 27 | quiet = parser.add_argument('--quiet', '-q', 28 | action='store_const', dest='log_level', const='ERROR', 29 | help="Alias for --log-level ERROR") 30 | 31 | verbose = parser.add_argument('--verbose', 32 | action='store_const', dest='log_level', const='DEBUG', 33 | help="Alias for --log-level DEBUG") 34 | 35 | subparsers = parser.add_subparsers(dest='command', title='commands', metavar='COMMAND') 36 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /src/markdoc/cli/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import os 5 | import argparse 6 | 7 | from markdoc.cli import commands 8 | from markdoc.cli.parser import parser 9 | from markdoc.config import Config, ConfigNotFound 10 | 11 | 12 | def main(cmd_args=None): 13 | """The main entry point for running the Markdoc CLI.""" 14 | 15 | if cmd_args is not None: 16 | args = parser.parse_args(cmd_args) 17 | else: 18 | args = parser.parse_args() 19 | 20 | if args.command != 'init': 21 | try: 22 | args.config = os.path.abspath(args.config) 23 | 24 | if os.path.isdir(args.config): 25 | config = Config.for_directory(args.config) 26 | elif os.path.isfile(args.config): 27 | config = Config.for_file(args.config) 28 | else: 29 | raise ConfigNotFound("Couldn't locate Markdoc config.") 30 | except ConfigNotFound, exc: 31 | parser.error(str(exc)) 32 | else: 33 | config = None 34 | 35 | logging.getLogger('markdoc').setLevel(getattr(logging, args.log_level)) 36 | 37 | command = getattr(commands, args.command.replace('-', '_')) 38 | return command(config, args) 39 | 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /src/markdoc/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from markdoc.config import Config 4 | 5 | 6 | Config.register_default('server.bind', '127.0.0.1') 7 | Config.register_default('server.port', 8008) 8 | Config.register_default('server.num-threads', 10) 9 | Config.register_default('server.name', None) 10 | Config.register_default('server.request-queue-size', 5) 11 | Config.register_default('server.timeout', 10) 12 | 13 | 14 | def server_maker(config, **extra_config): 15 | 16 | """ 17 | Return a server-making callable to create a CherryPy WSGI server. 18 | 19 | The server-making callable should be passed a WSGI application, and it 20 | will return an instance of `cherrypy.wsgiserver.CherryPyWSGIServer`. 21 | 22 | You can optionally override any of the hardwired configuration 23 | parameters by passing in keyword arguments which will be passed along to 24 | the `CherryPyWSGIServer` constructor. 25 | """ 26 | 27 | from cherrypy.wsgiserver import CherryPyWSGIServer 28 | 29 | bind_addr = (config['server.bind'], config['server.port']) 30 | kwargs = dict( 31 | numthreads=config['server.num-threads'], 32 | server_name=config['server.name'], 33 | request_queue_size=config['server.request-queue-size'], 34 | timeout=config['server.timeout']) 35 | kwargs.update(extra_config) 36 | 37 | return lambda wsgi_app: CherryPyWSGIServer(bind_addr, wsgi_app, **kwargs) 38 | 39 | Config.server_maker = server_maker 40 | -------------------------------------------------------------------------------- /test/cache.doctest: -------------------------------------------------------------------------------- 1 | Set up the document cache with a 'root' directory: 2 | 3 | >>> import os 4 | >>> from markdoc.cache import DocumentCache 5 | >>> cache = DocumentCache(base=CONFIG.wiki_dir) 6 | 7 | You can fetch files from the document cache: 8 | 9 | >>> cache.get('file1.md') 10 | u'# Hello\n' 11 | >>> cache.get('file2.md') 12 | u'# World\n' 13 | >>> cache.get('file3.md') 14 | u'# Foo\n' 15 | 16 | The contents of these files will then be cached: 17 | 18 | >>> print sorted(cache.relative(path) for path in cache.cache) 19 | ['file1.md', 'file2.md', 'file3.md'] 20 | 21 | You can also fetch them without using the cache: 22 | 23 | >>> cache.cache.clear() 24 | >>> cache.cache 25 | {} 26 | 27 | >>> cache.get('file1.md', cache=False) 28 | u'# Hello\n' 29 | >>> cache.get('file2.md', cache=False) 30 | u'# World\n' 31 | >>> cache.get('file3.md', cache=False) 32 | u'# Foo\n' 33 | 34 | >>> cache.cache 35 | {} 36 | 37 | You can force the document cache to refresh its cached content for each of these files: 38 | 39 | >>> cache.refresh_cache('file1.md') 40 | >>> cache.refresh_cache('file2.md') 41 | >>> cache.refresh_cache('file3.md') 42 | 43 | >>> len(cache.cache) 44 | 3 45 | 46 | And then make sure they actually *have* updated to the latest version: 47 | 48 | >>> cache.has_latest_version('file1.md') 49 | True 50 | >>> cache.has_latest_version('file2.md') 51 | True 52 | >>> cache.has_latest_version('file3.md') 53 | True 54 | -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/markdoc-default/listing.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}ls /{{ directory|e }}{% endblock %} 4 | 5 | {% block body_attrs %}class="listing"{% endblock %} 6 | 7 | {% block content %} 8 |

ls /{{ directory|e }}

9 | 10 | {% if sub_directories %} 11 |

Directories

12 | 13 | 14 | {% for subdir in sub_directories %} 15 | 16 | 21 | 22 | {% endfor %} 23 |
17 | 18 | {{ subdir.basename|e }}/ 19 | 20 |
24 | {% endif %} 25 | 26 | {% if pages %} 27 |

Pages

28 | 29 | 30 | {% for page in pages %} 31 | 32 | 33 | 38 | 39 | {% endfor %} 40 |
{{ page.humansize }} 34 | 35 | {{ page.title|e }} 36 | 37 |
41 | {% endif %} 42 | 43 | {% if files %} 44 |

Files

45 | 46 | 47 | {% for file in files %} 48 | 49 | 50 | 55 | 56 | {% endfor %} 57 |
{{ file.humansize }} 51 | 52 | {{ file.basename|e }} 53 | 54 |
58 | {% endif %} 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /src/markdoc/directories.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os.path as p 4 | 5 | from markdoc.config import Config 6 | 7 | 8 | def html_dir(config): 9 | return p.abspath(p.join(config['meta.root'], 10 | config.get('html-dir', config['hide-prefix'] + 'html'))) 11 | 12 | 13 | def static_dir(config): 14 | return p.abspath(p.join(config['meta.root'], config.get('static-dir', 'static'))) 15 | 16 | 17 | def wiki_dir(config): 18 | return p.abspath(p.join(config['meta.root'], config.get('wiki-dir', 'wiki'))) 19 | 20 | 21 | def temp_dir(config): 22 | return p.abspath(p.join(config['meta.root'], 23 | config.get('temp-dir', config['hide-prefix'] + 'tmp'))) 24 | 25 | 26 | def template_dir(config): 27 | return p.abspath(p.join(config['meta.root'], 28 | config.get('template-dir', config['hide-prefix'] + 'templates'))) 29 | 30 | 31 | Config.register_default('hide-prefix', '.') 32 | Config.register_default('use-default-static', True) 33 | Config.register_default('cvs-exclude', True) 34 | Config.register_func_default('html-dir', lambda cfg, key: html_dir(cfg)) 35 | Config.register_func_default('static-dir', lambda cfg, key: static_dir(cfg)) 36 | Config.register_func_default('wiki-dir', lambda cfg, key: wiki_dir(cfg)) 37 | Config.register_func_default('temp-dir', lambda cfg, key: temp_dir(cfg)) 38 | Config.register_func_default('template-dir', lambda cfg, key: template_dir(cfg)) 39 | 40 | Config.html_dir = property(html_dir) 41 | Config.static_dir = property(static_dir) 42 | Config.wiki_dir = property(wiki_dir) 43 | Config.temp_dir = property(temp_dir) 44 | Config.template_dir = property(template_dir) 45 | -------------------------------------------------------------------------------- /src/markdoc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import os 5 | import os.path as p 6 | 7 | 8 | __version__ = '0.6.6' 9 | 10 | 11 | static_dir = p.join(p.dirname(__file__), 'static') 12 | default_static_dir = p.join(static_dir, 'default-static') 13 | default_template_dir = p.join(static_dir, 'default-templates') 14 | 15 | 16 | if not hasattr(p, 'relpath'): 17 | def relpath(path, start=p.curdir): 18 | """Return a relative version of a path""" 19 | 20 | if not path: 21 | raise ValueError("no path specified") 22 | 23 | start_list = p.abspath(start).split(p.sep) 24 | path_list = p.abspath(path).split(p.sep) 25 | 26 | # Work out how much of the filepath is shared by start and path. 27 | i = len(p.commonprefix([start_list, path_list])) 28 | 29 | rel_list = [p.pardir] * (len(start_list)-i) + path_list[i:] 30 | if not rel_list: 31 | return p.curdir 32 | return p.join(*rel_list) 33 | p.relpath = relpath 34 | 35 | 36 | default_formatter = logging.Formatter( 37 | u'%(name)s: %(levelname)s: %(message)s') 38 | 39 | console_handler = logging.StreamHandler() # By default, outputs to stderr. 40 | console_handler.setFormatter(default_formatter) 41 | console_handler.setLevel(logging.DEBUG) 42 | 43 | logging.getLogger('markdoc').addHandler(console_handler) 44 | logging.getLogger('markdoc').setLevel(logging.INFO) # Default level. 45 | 46 | # These modules all initialize various default config values, so need to be 47 | # imported straight away. 48 | import markdoc.builder 49 | import markdoc.directories 50 | import markdoc.render 51 | import markdoc.server 52 | import markdoc.templates 53 | -------------------------------------------------------------------------------- /doc/tips/barebones.md: -------------------------------------------------------------------------------- 1 | # Barebones Wikis 2 | 3 | Out of the box, Markdoc supports relatively complex wikis with custom templates and static files. However, for many cases, the default media and templates are adequate, so why should a vanilla wiki require nesting like the following: 4 | 5 | :::text 6 | some-wiki/ 7 | |-- .templates/ 8 | |-- static/ 9 | |-- wiki/ 10 | | |-- files 11 | | |-- go 12 | | `-- here 13 | `-- markdoc.yaml 14 | 15 | Fortunately, for very simple cases where you just want to be able to render and serve a collection of Markdown-formatted files, you can do so. Begin by just creating and entering an empty directory: 16 | 17 | :::bash 18 | $ mkdir mywiki/ 19 | $ cd mywiki/ 20 | 21 | Now create a file called `.markdoc.yaml` containing the following YAML data: 22 | 23 | :::yaml 24 | wiki-name: My Wiki # Set this to whatever you want 25 | 26 | wiki-dir: "." 27 | static-dir: ".static" 28 | 29 | You can add some more configuration parameters if you like, but these are the basic ones you’ll need. So now your directory structure will look like this: 30 | 31 | :::text 32 | mywiki/ 33 | `-- .markdoc.yaml 34 | 35 | And you can just start creating pages in the directory. 36 | 37 | :::text 38 | mywiki/ 39 | |-- .markdoc.yaml 40 | |-- page1.md 41 | |-- page2.md 42 | `-- page3.md 43 | 44 | To run the build process, just do the usual: 45 | 46 | :::bash 47 | $ markdoc build && markdoc serve 48 | 49 | `markdoc` recognizes both `markdoc.yaml` *and* `.markdoc.yaml` implicitly. Because you’ve hidden everything except the actual wiki pages, to most file browsers (including `ls`) the wiki will just look like a directory with a number of text files. 50 | -------------------------------------------------------------------------------- /src/markdoc/static/default-static/media/sass/_typography.sass: -------------------------------------------------------------------------------- 1 | !monospace_fonts_block = "Menlo", "'DejaVu Sans Mono'", "'Bitstream Vera Sans Mono'", "Courier", "'Courier 10 Pitch'", "'Courier New'", "monospace" 2 | !monospace_fonts_inline = "Courier", "'Courier 10 Pitch'", "'Courier New'", "monospace" 3 | !sans_serif_fonts = "'Helvetica Neue'", "Helvetica", "Arial", "Geneva", "sans-serif" 4 | 5 | body 6 | :font-family= !sans_serif_fonts 7 | :line-height 21px 8 | 9 | #breadcrumbs 10 | li 11 | :color #aaa 12 | :font-size 13px 13 | :font-weight bold 14 | :line-height 28px 15 | a 16 | :text-decoration none 17 | .list-crumb 18 | :font-weight normal 19 | 20 | #footer 21 | :color #777 22 | :font-size 13px 23 | :text-transform lowercase 24 | 25 | &.listing 26 | table#pages, table#subdirs 27 | td.size 28 | :font-family= !monospace_fonts_block 29 | :text-align right 30 | table#subdirs 31 | td.name 32 | :font-family= !monospace_fonts_inline 33 | 34 | 35 | // Headers 36 | 37 | h1, h2, h3, h4, h5, h6 38 | :line-height 21px 39 | 40 | h1 41 | :font-size 21px 42 | 43 | h2 44 | :font-size 18px 45 | 46 | h3 47 | :font-size 15px 48 | 49 | h4, h5, h6 50 | :font-size 13px 51 | 52 | 53 | // Links 54 | 55 | !link_color = #900 56 | 57 | a 58 | :color= !link_color 59 | :text-decoration none 60 | &:hover 61 | :color= !link_color * 0.5 62 | &[href^="http:"] 63 | :text-decoration underline 64 | 65 | 66 | // Lists 67 | 68 | dl 69 | dt 70 | :font-weight bold 71 | 72 | 73 | // Code 74 | 75 | code 76 | :font-family= !monospace_fonts_inline 77 | :line-height 18px 78 | 79 | pre 80 | :font-family= !monospace_fonts_block 81 | :font-size 11px 82 | :line-height 18px 83 | -------------------------------------------------------------------------------- /doc/tips/syntax-highlighting.md: -------------------------------------------------------------------------------- 1 | # Syntax Highlighting with Pygments 2 | 3 | [Markdown for Python][] supports syntax highlighting in your documents using [Pygments][]. Here’s how to set up and use it from Markdoc. 4 | 5 | [markdown for python]: http://www.freewisdom.org/projects/python-markdown 6 | [pygments]: http://pygments.org/ 7 | 8 | First, install the extension in your `markdoc.yaml` file: 9 | 10 | :::yaml 11 | wiki-name: My Wiki 12 | ## ...other settings... 13 | markdown: 14 | extensions: 15 | - codehilite # the important bit 16 | 17 | Pygments should have been installed as a dependency when you installed Markdoc. 18 | 19 | Initially, syntax highlighting will be applied to every code block Markdown encounters. Pygments will guess which lexer to apply to each block. To specify the language of a block, add a `:::LANG` prefix to blocks of code. For example: 20 | 21 | :::text 22 | :::python 23 | def hello(): 24 | print "Hello, World!" 25 | 26 | Will render to: 27 | 28 | :::python 29 | def hello(): 30 | print "Hello, World!" 31 | 32 | To switch syntax highlighting off for a block, use `:::text`. 33 | 34 | If you want a block to have line numbers, use `#!LANG` instead. For example: 35 | 36 | :::text 37 | #!ruby 38 | class Foo 39 | def bar 40 | @baz 41 | end 42 | end 43 | 44 | Will render to: 45 | 46 | #!ruby 47 | class Foo 48 | def bar 49 | @baz 50 | end 51 | end 52 | 53 | If you add a shebang to a code block, like `#!/bin/bash` or `#!/usr/bin/env python`, the language will be guessed and the shebang included in the output. In the final text. So for example, the following: 54 | 55 | :::text 56 | #!/bin/bash 57 | echo "Hello, World" 58 | 59 | Will render to: 60 | 61 | #!/bin/bash 62 | echo "Hello, World" 63 | -------------------------------------------------------------------------------- /test/cli.doctest: -------------------------------------------------------------------------------- 1 | First we have to set up a Python function for running the command-line tool. Since the main entry point is the `markdoc.cli.main.main()` function, we can just call that directly, with some defaults for the current wiki root: 2 | 3 | >>> import os 4 | >>> import os.path as p 5 | >>> from markdoc.cli.main import main 6 | >>> def markdoc(*args): 7 | ... try: 8 | ... main(['-c', WIKI_ROOT] + list(args)) 9 | ... except SystemExit, exc: 10 | ... return exc.code 11 | ... return 0 12 | 13 | The `show-config` command will show the current Markdoc config: 14 | 15 | >>> exit_code = markdoc('show-config') # doctest: +ELLIPSIS 16 | {'9d03e32c6cb1455388a170e3bb0c2030': '7010d5d5871143d089cb662cb540cbd5', 17 | 'hide-prefix': '_', 18 | 'meta.config-file': '.../example/markdoc.yaml', 19 | 'meta.root': '.../example', 20 | 'use-default-static': False, 21 | 'use-default-templates': False} 22 | 23 | >>> exit_code 24 | 0 25 | 26 | `sync-static` will compile the wiki's static media into the HTML root: 27 | 28 | >>> markdoc('--quiet', 'sync-static') # doctest: +ELLIPSIS 29 | 0 30 | 31 | If you leave out the `--quiet` option, Markdoc will print some additional logging information, including the `rsync` command that's run in the background; the rsync output itself will also be written to the terminal. 32 | 33 | The static media will now be in the HTML root: 34 | 35 | >>> os.listdir(CONFIG.html_dir) 36 | ['example.css'] 37 | 38 | There are three other commands -- `clean-html`, `clean-temp`, and `sync-html` -- which are used in the background by the `build` task. `markdoc build` will compile all the HTML, and then synchronize both built HTML and static media into the HTML root (usually `WIKI_ROOT/.html`). 39 | 40 | >>> markdoc('--quiet', 'build') 41 | 0 42 | 43 | >>> print '\n'.join(sorted(os.listdir(CONFIG.html_dir))) 44 | _list.html 45 | an_empty_file.html 46 | example.css 47 | file1.html 48 | file2.html 49 | file3.html 50 | index.html 51 | subdir 52 | -------------------------------------------------------------------------------- /doc/authoring.md: -------------------------------------------------------------------------------- 1 | # Authoring 2 | 3 | A wiki would be nothing without pages. In Markdoc, pages are written in 4 | [Markdown][df-markdown], a plain-text formatting syntax designed by 5 | [John Gruber][df]. In his own words: 6 | 7 | [df]: http://daringfireball.net/ 8 | [df-markdown]: http://daringfireball.net/projects/markdown/ 9 | 10 | > Markdown allows you to write using an easy-to-read, easy-to-write plain text 11 | > format, then convert it to structurally valid XHTML (or HTML). 12 | > 13 | > [...] 14 | > 15 | > The overriding design goal for Markdown’s formatting syntax is to 16 | > make it as readable as possible. The idea is that a Markdown-formatted 17 | > document should be publishable as-is, as plain text, without looking 18 | > like it’s been marked up with tags or formatting instructions. 19 | 20 | For a comprehensive guide to the Markdown syntax, consult the 21 | [markup reference documentation](/ref/markup). The rest of this document will 22 | cover the Markdoc-specific conventions and constraints related to writing wiki 23 | pages. 24 | 25 | 26 | ## Linking 27 | 28 | Every page in your wiki will likely link to several other pages. Markdoc does 29 | not require any special syntax for internal links; the usual Markdown format 30 | will work. An example of a link from this very page follows: 31 | 32 | :::text 33 | For a comprehensive guide to the Markdown syntax, 34 | consult the [markup reference documentation](/ref/markup). 35 | 36 | As you can see, the link href is an absolute path to the document, without any 37 | extension. Markdoc will process this and convert it into a relative path when 38 | rendering the corresponding HTML. This means that you can host Markdoc wikis 39 | under sub-directories on a web server, and the links will still work properly. 40 | 41 | If you split your wiki up into sub-directories (for example, in this wiki, there 42 | is an `internals/` directory), the pattern remains the same. A link to the 43 | [internals/rendering](/internals/rendering) document looks like this: 44 | 45 | :::text 46 | A link to the [internals/rendering](/internals/rendering) document. 47 | 48 | Note that links will only be made relative if they begin with a `/` character; 49 | for example, a link to `http://www.google.com/` will be left untouched. 50 | -------------------------------------------------------------------------------- /doc/ref/layout.md: -------------------------------------------------------------------------------- 1 | # Layout 2 | 3 | Markdoc wikis have the following layout: 4 | 5 | :::text 6 | WIKI_ROOT/ 7 | |-- .html/ 8 | |-- .templates/ 9 | |-- .tmp/ 10 | |-- static/ 11 | |-- wiki/ 12 | `-- markdoc.yaml 13 | 14 | The `.html/` and `.tmp/` directories should be excluded from any VCS, since they 15 | contain temporary files. Here is a list of the roles of the various files and 16 | sub-directories, in descending order of significance: 17 | 18 | `WIKI_ROOT/` 19 | : The *wiki root*, containing all of the files required for the `markdoc` 20 | utility to build and serve the wiki. 21 | 22 | `WIKI_ROOT/markdoc.yaml` 23 | : The *wiki configuration*; the main configuration point for your wiki, in a 24 | YAML-formatted file. Consult the [configuration docs](/configuration) for 25 | more information on how to write this. 26 | 27 | `WIKI_ROOT/wiki/` 28 | : The *document root*, which contains the actual *text* of your wiki in 29 | Markdown-formatted text files. It is assumed they are UTF-8 encoded. Any 30 | files without a valid extension will be ignored (see the option 31 | `document-extensions` in [configuration](/configuration)). 32 | 33 | `WIKI_ROOT/static/` 34 | : The *static directory*: static media files (such as CSS and JavaScript) 35 | should be put here. They will be copied to `.html/` by rsync during the 36 | build operation. This comes with some default CSS for styling. 37 | 38 | `WIKI_ROOT/.templates/` 39 | : The *template directory*: it contains the Jinja2 templates for documents, 40 | error pages and directory listings. It comes with some nice defaults, but 41 | you can override these if you wish. 42 | 43 | `WIKI_ROOT/.html/` 44 | : The *HTML root* (sometimes abbreviated as ‘htroot’). It holds the compiled 45 | HTML and static files. It is directly served from by the Markdoc webserver. 46 | 47 | `WIKI_ROOT/.tmp/` 48 | : The *temporary directory*: a temporary build destination for rendered 49 | Markdown files. This directory is then rsync’d to the HTML root along with 50 | the static directory; the incremental nature of this operation means the 51 | Markdoc web server can keep running in one process whilst another runs 52 | `markdoc build`. 53 | 54 | Note that all of the default locations for these directories can be overridden 55 | in the `markdoc.yaml` file. For example, you may wish to use `WIKI_ROOT/pages/` 56 | instead of `WIKI_ROOT/wiki/`, or `WIKI_ROOT/.build/` instead of 57 | `WIKI_ROOT/.html/`. Consult the [configuration documentation](/configuration) 58 | for more information. 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import os.path as p 5 | import re 6 | 7 | from distribute_setup import use_setuptools; use_setuptools() 8 | from setuptools import setup, find_packages 9 | 10 | 11 | rel_file = lambda *args: p.join(p.dirname(p.abspath(__file__)), *args) 12 | 13 | def read_from(filename): 14 | fp = open(filename) 15 | try: 16 | return fp.read() 17 | finally: 18 | fp.close() 19 | 20 | 21 | if not hasattr(p, 'relpath'): 22 | def relpath(path, start=p.curdir): 23 | """Return a relative version of a path""" 24 | 25 | if not path: 26 | raise ValueError("no path specified") 27 | 28 | start_list = p.abspath(start).split(p.sep) 29 | path_list = p.abspath(path).split(p.sep) 30 | 31 | # Work out how much of the filepath is shared by start and path. 32 | i = len(p.commonprefix([start_list, path_list])) 33 | 34 | rel_list = [p.pardir] * (len(start_list)-i) + path_list[i:] 35 | if not rel_list: 36 | return p.curdir 37 | return p.join(*rel_list) 38 | p.relpath = relpath 39 | 40 | 41 | def get_version(): 42 | data = read_from(rel_file('src', 'markdoc', '__init__.py')) 43 | return re.search(r"__version__ = '([^']+)'", data).group(1) 44 | 45 | 46 | def get_requirements(): 47 | data = read_from(rel_file('REQUIREMENTS')) 48 | lines = map(lambda s: s.strip(), data.splitlines()) 49 | return filter(None, lines) 50 | 51 | 52 | def find_package_data(): 53 | files = [] 54 | src_root = p.join(p.dirname(__file__), 'src', 'markdoc') 55 | static_root = p.join(src_root, 'static') 56 | for dirpath, subdirs, filenames in os.walk(static_root): 57 | for filename in filenames: 58 | if not filename.startswith('.') or filename.startswith('_'): 59 | abs_path = p.join(dirpath, filename) 60 | files.append(p.relpath(abs_path, start=src_root)) 61 | return files 62 | 63 | setup( 64 | name = 'Markdoc', 65 | version = get_version(), 66 | author = "Zachary Voase", 67 | author_email = "zacharyvoase@me.com", 68 | url = 'http://github.com/zacharyvoase/markdoc', 69 | description = "A lightweight Markdown-based wiki build tool.", 70 | packages = find_packages(where='src'), 71 | package_dir = {'': 'src'}, 72 | package_data = {'markdoc': find_package_data()}, 73 | entry_points = {'console_scripts': ['markdoc = markdoc.cli.main:main']}, 74 | install_requires = get_requirements(), 75 | ) 76 | -------------------------------------------------------------------------------- /src/markdoc/static/default-static/media/sass/_layout.sass: -------------------------------------------------------------------------------- 1 | html 2 | :background-color #f6f6f6 3 | 4 | body 5 | :background-color white 6 | :margin 0 auto 7 | :width 650px 8 | 9 | #breadcrumbs, #content, #footer 10 | :background-color white 11 | :clear both 12 | :float left 13 | :overflow hidden 14 | :padding 0 20px 15 | :width= 650px - (20px + 20px) 16 | 17 | #breadcrumbs 18 | :border-bottom 2px solid #f6f6f6 19 | :height 28px 20 | :margin 0 21 | :padding 0 22 | :width 650px 23 | li 24 | :float left 25 | :list-style none 26 | :margin 0 27 | :padding 0 28 | a 29 | :display block 30 | :float left 31 | :padding 0 8px 32 | &.last 33 | :padding 0 8px 34 | &.not-last:after 35 | :content "»" 36 | :float right 37 | 38 | #footer 39 | :border-top 8px solid #f6f6f6 40 | :padding-top 13px 41 | 42 | .clear 43 | :clear both 44 | :border-width 0 45 | :margin 0 46 | :visibility hidden 47 | 48 | 49 | body.listing 50 | table#pages, table#subdirs, table#files 51 | tr 52 | :border-bottom 1px solid #777 53 | :border-top 1px solid #777 54 | td 55 | :border none 56 | &.size 57 | :background-color #f6f6f6 58 | &.name 59 | :padding 0 60 | a 61 | :display block 62 | :margin 0 63 | :padding 4px 8px 64 | 65 | 66 | // Common elements 67 | 68 | blockquote 69 | :background-color #f6f6f6 70 | :padding 13px 71 | :padding-bottom 1px 72 | 73 | hr 74 | :border-style solid 75 | :border none 76 | :border-top 1px solid #777 77 | :margin 28px 0 78 | 79 | 80 | // Lists 81 | 82 | dl 83 | :margin-left 0 84 | dd 85 | :margin-bottom 13px 86 | :margin-left 13px 87 | 88 | ul 89 | :margin-top 0 90 | li 91 | :list-style square outside 92 | ul 93 | :margin-bottom 0 94 | 95 | 96 | // Code blocks 97 | 98 | =codeblock 99 | :border-left 1px solid gray 100 | :margin-bottom 13px 101 | :margin-left 30px 102 | :padding-left 12px 103 | 104 | pre 105 | +codeblock 106 | 107 | .codehilite 108 | +codeblock 109 | pre 110 | :border none 111 | :margin 0 112 | :padding 0 113 | 114 | .codehilitetable 115 | :margin-left 0 116 | :padding-left 0 117 | tr td 118 | :border none 119 | :padding 3px 5px 0 5px 120 | &.linenos 121 | :background-color #f6f6f6 122 | :border-right 1px solid gray 123 | :margin 0 124 | :padding-right 6px 125 | :text-align right 126 | :width= 30px - (5px + 6px) 127 | .linenodiv pre 128 | :border none 129 | :margin 0 130 | :padding 0 131 | &.code 132 | :margin 0 133 | :padding-left 12px 134 | .codehilite 135 | :border none 136 | :margin 0 137 | :padding 0 138 | -------------------------------------------------------------------------------- /doc/internals/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Markdoc is actively developed via [GitHub][gh-markdoc]. 4 | 5 | [gh-markdoc]: http://github.com/zacharyvoase/markdoc 6 | 7 | ## Working with the Repository 8 | 9 | You’ll need to install [Git][git] first; check your OS’s package manager for a 10 | `git` or `git-core` package. 11 | 12 | [git]: http://git-scm.com/ 13 | 14 | You can check out a copy of the repository by cloning it: 15 | 16 | :::bash 17 | $ git clone git://github.com/zacharyvoase/markdoc.git 18 | $ cd markdoc/ 19 | 20 | ### Repo Structure 21 | 22 | There are several files and directories in the root of the repo: 23 | 24 | :::text 25 | markdoc/ 26 | |-- doc/ 27 | |-- src/ 28 | |-- test/ 29 | |-- UNLICENSE 30 | |-- MANIFEST.in 31 | |-- README 32 | |-- REQUIREMENTS 33 | |-- distribute_setup.py 34 | |-- nose.cfg 35 | `-- setup.py 36 | 37 | `doc/` 38 | : A Markdoc wiki containing Markdoc’s own documentation. How very meta. 39 | 40 | `src/` 41 | : The home of all of Markdoc’s Python code. 42 | 43 | `test/`, `nose.cfg` 44 | : Markdoc’s tests (Python + Doctests) and nose configuration. [Nose][] is a 45 | Python utility to automate and simplify running complex test suites. 46 | 47 | `UNLICENSE` 48 | : The text of the unlicense which designates Markdoc as public domain software. 49 | 50 | `MANIFEST.in`, `setup.py`, `distribute_setup.py` 51 | : The necessary Python packaging machinery, so you can run 52 | `easy_install Markdoc`. 53 | 54 | `README` 55 | : Doesn’t need an explanation. 56 | 57 | `REQUIREMENTS` 58 | : A text file listing all of Markdoc’s requirements, suitable for use with 59 | `pip install -r REQUIREMENTS`. [pip][] is a next-generation `easy_install` 60 | replacement for Python. 61 | 62 | 63 | [pip]: http://pip.openplans.org/ 64 | [nose]: http://somethingaboutorange.com/mrl/projects/nose/0.11.1/ 65 | 66 | ### Bug Reporting and Feature Requests 67 | 68 | All bugs and feature requests are handled on the [GitHub issues page](http://github.com/zacharyvoase/markdoc/issues). 69 | 70 | ### Contributing 71 | 72 | If you’re interested in implementing a feature or extension for Markdoc, just fork the GitHub repository, work on the feature, commit your changes, then send me a pull request. If I like what you’ve done, I’ll pull your changes into the official Markdoc release and give you author credit. 73 | 74 | Remember that you must be willing to release your changes to the public domain. If you are submitting a non-trivial patch, take a look at [unlicense.org][unlicensing contributions] for detailed instructions; for now, you just need to agree to the following statement: 75 | 76 | [unlicensing contributions]: http://unlicense.org/#unlicensing-contributions 77 | 78 | :::text 79 | I dedicate any and all copyright interest in this software to the 80 | public domain. I make this dedication for the benefit of the public at 81 | large and to the detriment of my heirs and successors. I intend this 82 | dedication to be an overt act of relinquishment in perpetuity of all 83 | present and future rights to this software under copyright law. 84 | -------------------------------------------------------------------------------- /src/markdoc/render.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os.path as p 4 | 5 | from markdoc.config import Config 6 | import markdown 7 | 8 | 9 | Config.register_default('markdown.extensions', ()) 10 | Config.register_func_default('markdown.extension-configs', lambda cfg, key: {}) 11 | Config.register_default('markdown.safe-mode', False) 12 | Config.register_default('markdown.output-format', 'xhtml1') 13 | Config.register_default('document-extensions', 14 | frozenset(['.md', '.mdown', '.markdown', '.wiki', '.text'])) 15 | 16 | 17 | class RelativeLinksTreeProcessor(markdown.treeprocessors.Treeprocessor): 18 | 19 | """A Markdown tree processor to relativize wiki links.""" 20 | 21 | def __init__(self, curr_path='/'): 22 | self.curr_path = curr_path 23 | 24 | def make_relative(self, href): 25 | return make_relative(self.curr_path, href) 26 | 27 | def run(self, tree): 28 | links = tree.getiterator('a') 29 | for link in links: 30 | if link.attrib['href'].startswith('/'): 31 | link.attrib['href'] = self.make_relative(link.attrib['href']) 32 | return tree 33 | 34 | 35 | def make_relative(curr_path, href): 36 | """Given a current path and a href, return an equivalent relative path.""" 37 | 38 | curr_list = curr_path.lstrip('/').split('/') 39 | href_list = href.lstrip('/').split('/') 40 | 41 | # How many path components are shared between the two paths? 42 | i = len(p.commonprefix([curr_list, href_list])) 43 | 44 | rel_list = (['..'] * (len(curr_list) - i - 1)) + href_list[i:] 45 | if not rel_list or rel_list == ['']: 46 | return './' 47 | return '/'.join(rel_list) 48 | 49 | 50 | def unflatten_extension_configs(config): 51 | """Unflatten the markdown extension configs from the config dictionary.""" 52 | 53 | configs = config['markdown.extension-configs'] 54 | 55 | for key, value in config.iteritems(): 56 | if not key.startswith('markdown.extension-configs.'): 57 | continue 58 | 59 | parts = key[len('markdown.extension-configs.'):].split('.') 60 | extension_config = configs 61 | for part in parts[:-1]: 62 | extension_config = extension_config.setdefault(part, {}) 63 | extension_config[parts[-1]] = value 64 | 65 | return configs 66 | 67 | 68 | def get_markdown_instance(config, curr_path='/', **extra_config): 69 | """Return a `markdown.Markdown` instance for a given configuration.""" 70 | 71 | mdconfig = dict( 72 | extensions=config['markdown.extensions'], 73 | extension_configs=unflatten_extension_configs(config), 74 | safe_mode=config['markdown.safe-mode'], 75 | output_format=config['markdown.output-format']) 76 | 77 | mdconfig.update(extra_config) # Include any extra kwargs. 78 | 79 | md_instance = markdown.Markdown(**mdconfig) 80 | md_instance.treeprocessors['relative_links'] = RelativeLinksTreeProcessor(curr_path=curr_path) 81 | return md_instance 82 | 83 | # Add it as a method to `markdoc.config.Config`. 84 | Config.markdown = get_markdown_instance 85 | -------------------------------------------------------------------------------- /test/listing.doctest: -------------------------------------------------------------------------------- 1 | To generate listings, we first need to build the wiki. 2 | 3 | >>> from markdoc.cli.main import main 4 | >>> try: 5 | ... main(['-c', WIKI_ROOT, '--quiet', 'build']) 6 | ... except SystemExit, exc: 7 | ... assert exc.code != 0, "An error occurred building the wiki" 8 | 9 | The wiki should now be built in the HTML root: 10 | 11 | >>> import os 12 | >>> import os.path as p 13 | >>> print '\n'.join(os.listdir(CONFIG.html_dir)) 14 | _list.html 15 | an_empty_file.html 16 | example.css 17 | file1.html 18 | file2.html 19 | file3.html 20 | index.html 21 | subdir 22 | 23 | Now we can try getting some listings. The bulk of the process occurs in the `Builder.listing_context()` method, which builds a template context dictionary for rendering the Jinja2 `listing.html` template. First we have to get a `Builder` instance: 24 | 25 | >>> from markdoc.builder import Builder 26 | >>> b = Builder(CONFIG) 27 | 28 | Now we can get the listing for the HTML root itself, just by passing the empty string: 29 | 30 | >>> import pprint 31 | >>> pprint.pprint(b.listing_context('')) # doctest: +ELLIPSIS 32 | {'directory': '', 33 | 'files': [{'basename': 'example.css', 34 | 'href': '/example.css', 35 | 'humansize': '27B', 36 | 'size': 27, 37 | 'slug': 'example'}], 38 | 'make_relative': at 0x...>, 39 | 'pages': [{'basename': 'an_empty_file.html', 40 | 'href': '/an_empty_file', 41 | 'humansize': '248B', 42 | 'size': 248, 43 | 'slug': 'an_empty_file', 44 | 'title': 'An Empty File'}, 45 | {'basename': 'file3.html', 46 | 'href': '/file3', 47 | 'humansize': '250B', 48 | 'size': 250, 49 | 'slug': 'file3', 50 | 'title': u'Foo'}, 51 | {'basename': 'file1.html', 52 | 'href': '/file1', 53 | 'humansize': '254B', 54 | 'size': 254, 55 | 'slug': 'file1', 56 | 'title': u'Hello'}, 57 | {'basename': 'file2.html', 58 | 'href': '/file2', 59 | 'humansize': '254B', 60 | 'size': 254, 61 | 'slug': 'file2', 62 | 'title': u'World'}], 63 | 'sub_directories': [{'basename': 'subdir', 'href': '/subdir/'}]} 64 | 65 | We can also get the listings for subdirectories, by passing in their paths, relative to the HTML root: 66 | 67 | >>> pprint.pprint(b.listing_context('/subdir')) # doctest: +ELLIPSIS 68 | {'directory': 'subdir', 69 | 'files': [], 70 | 'make_relative': at 0x...>, 71 | 'pages': [{'basename': 'hello.html', 72 | 'href': '/subdir/hello', 73 | 'humansize': '268B', 74 | 'size': 268, 75 | 'slug': 'hello', 76 | 'title': u'Hello again.'}], 77 | 'sub_directories': []} 78 | 79 | Note that these paths are always '/'-delimited, since they are taken to be URL paths and not filesystem paths. 80 | -------------------------------------------------------------------------------- /doc/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | The first step towards using Markdoc is to install it. Luckily, it uses 4 | setuptools, so you can install it with relative ease on any system with Python. 5 | Note that most modern UNIX distributions come with a sufficiently recent version 6 | of Python, including Mac OS X, Ubuntu (and derivatives) and Fedora. 7 | 8 | 9 | ## Requirements 10 | 11 | The minimum requirements to run the Markdoc utility are: 12 | 13 | * Python 2.4 or later (2.5+ highly recommended) 14 | * A UNIX (or at least POSIX-compliant) operating system 15 | * [rsync](http://www.samba.org/rsync/) — installed out of the box with most 16 | modern OSes, including Mac OS X and Ubuntu. In the future Markdoc may 17 | include a pure-Python implementation. 18 | 19 | 20 | ## Installation 21 | 22 | You can use either `easy_install` or [pip][] to install Markdoc: 23 | 24 | [pip]: http://pip.openplans.org/ 25 | 26 | :::bash 27 | $ easy_install Markdoc # OR 28 | $ pip install Markdoc 29 | 30 | Note that you are likely to see a lot of scary-looking output from both 31 | commands; nevertheless, you can tell whether installation was successful by 32 | looking at the last line of the output. With `easy_install`, this should be: 33 | 34 | :::text 35 | Finished processing dependencies for Markdoc 36 | 37 | And with `pip install`: 38 | 39 | :::text 40 | Successfully installed ... Markdoc ... 41 | 42 | `pip` will list all of the packages it installed, and `Markdoc` should be 43 | amongst them. 44 | 45 | 46 | ## Making a Wiki 47 | 48 | ### Initializing the Wiki 49 | 50 | The `markdoc init` command creates a new wiki. It also accepts a `--vcs-ignore` 51 | option which will automatically create the appropriate ignore file for your VCS. 52 | 53 | :::bash 54 | $ markdoc init my-wiki --vcs-ignore hg 55 | markdoc.vcs-ignore: INFO: Writing ignore file to .hgignore 56 | markdoc.init: INFO: Wiki initialization complete 57 | markdoc.init: INFO: Your new wiki is at: .../my-wiki 58 | 59 | If you’re using SVN, you have to take a few more steps to set the `svn:ignore` 60 | property on the directory: 61 | 62 | :::bash 63 | $ markdoc init my-wiki --vcs-ignore cvs 64 | markdoc.vcs-ignore: INFO: Writing ignore file to .cvsignore 65 | markdoc.init: INFO: Wiki initialization complete 66 | markdoc.init: INFO: Your new wiki is at: .../my-wiki 67 | $ cd my-wiki/ 68 | $ svn propset svn:ignore -F .cvsignore 69 | $ rm .cvsignore 70 | 71 | 72 | ### Editing Pages 73 | 74 | Documents in a Markdoc wiki are located under the `wiki/` subdirectory, and are 75 | plain Markdown files. Typically documents have a `.md` file extension, but in 76 | the [wiki configuration](/configuration#building) you can specify others. 77 | 78 | :::bash 79 | $ cd my-wiki/ 80 | $ vim wiki/somefile.md 81 | # ... write some documents ... 82 | 83 | 84 | ### Building 85 | 86 | Markdoc comes with a default set of templates and stylesheets, so you can build 87 | your wiki right away. Just run `markdoc build`, and all the HTML will be 88 | generated and output into the `.html/` sub-directory (known as the HTML root). 89 | 90 | :::bash 91 | $ markdoc build 92 | 93 | 94 | ### Serving 95 | 96 | You can view all the HTML in a browser easily by running the built-in server. 97 | `markdoc serve` accepts a large number of options, but on its own will serve 98 | your documentation on port 8008. 99 | 100 | :::bash 101 | $ markdoc serve 102 | markdoc.serve: INFO: Serving on http://127.0.0.1:8008 103 | 104 | Now just open in your browser, and see your new 105 | Markdoc-powered wiki! 106 | -------------------------------------------------------------------------------- /doc/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Markdoc 4 | 5 | Markdoc is a lightweight Markdown-based wiki system. It’s been designed to allow 6 | you to create and manage wikis as quickly and easily as possible. 7 | 8 | 9 | ## What is it good for? 10 | 11 | Potential use cases for Markdoc include, but aren’t limited to: 12 | 13 | Technical Documentation/Manuals 14 | : Markdoc can be used to write and render hand-written guides and manuals for 15 | software. Such documentation will normally be separate from 16 | automatically-generated API documentation, and might give a higher-level 17 | view than API docs alone. It might be used for client documentation for 18 | web/desktop applications, or even developer documentation for frameworks. 19 | 20 | Internal Project Wikis 21 | : Markdoc wikis consist of a single plain-text file per page. By combining a 22 | wiki with a DVCS (such as [Mercurial][] or [Git][]), you can collaborate 23 | with several other people. Use the DVCS to track, share and merge changes 24 | with one another, and view the wiki’s history. 25 | 26 | [Mercurial]: http://mercurial.selenic.com/ 27 | [Git]: http://git-scm.com/ 28 | 29 | Static Site Generation 30 | : Markdoc converts wikis into raw HTML files and media. This allows you to 31 | manage a blog, personal website or a collection of pages in a Markdoc wiki, 32 | perhaps with custom CSS styles, and publish the rendered HTML to a website. 33 | Markdoc need not be installed on the hosting site, since the resultant HTML 34 | is completely independent. 35 | 36 | 37 | ## Cool Features 38 | 39 | * Set up [Google Analytics][] tracking in one line of configuration. 40 | 41 | * [Barebones][] wikis that just look like directories with Markdown-formatted 42 | text files in them. 43 | 44 | * A built-in HTTP server and WSGI application to serve up a compiled wiki with 45 | a single command. 46 | 47 | * Continuous builds (via `rsync`) mean the server can keep running whilst 48 | Markdoc re-compiles the wiki. Just refresh your browser to see the changes. 49 | 50 | * Add [Pygments][]-powered syntax highlighting to your Markdoc wiki with a 51 | single [configuration parameter][syntax-highlighting]. 52 | 53 | * Markdoc is [public domain software][licensing]. It will always be completely 54 | free to use, and you can redistribute it (in part or in whole) under any 55 | circumstances (open-source, proprietary or otherwise) with no attribution or 56 | encumberances. 57 | 58 | [google analytics]: /ref/configuration#metadata 59 | [barebones]: /tips/barebones 60 | [pygments]: http://pygments.org/ 61 | [syntax-highlighting]: /tips/syntax-highlighting 62 | [licensing]: /about#license 63 | 64 | 65 | ## Where do I start? 66 | 67 | The [quickstart](/quickstart) document has all the information you need to put 68 | together a simple Markdoc wiki. The [authoring](/authoring) guide provides a 69 | quick introduction to writing Markdoc pages themselves, especially with regards 70 | to linking between pages. 71 | 72 | 73 | ## Reference 74 | 75 | See the [configuration](/ref/configuration) reference for in-depth knowledge on 76 | writing your `markdoc.yaml` file. The [layout](/ref/layout) reference describes 77 | the basic filesystem layout for a Markdoc wiki, and the [tips](/tips/) directory 78 | contains several handy recipes. 79 | 80 | The Markdoc project’s goals and history are described in the [about](/about) 81 | page. If you’d like to know more about how Markdoc works at a deeper level, take 82 | a look at the [internals directory](/internals/). Developers interested in 83 | hacking the utility will likely want the [development](/internals/development) 84 | page. 85 | 86 | To see the complete list of pages in this wiki, you can browse the 87 | [directory listing](/_list). 88 | -------------------------------------------------------------------------------- /src/markdoc/static/default-templates/markdoc-default/base.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 12 | {% import "macros/html" as html -%} 13 | {% import "macros/crumbs" as breadcrumbs with context -%} 14 | 15 | 16 | {% block head %} 17 | 18 | {% block meta %} 19 | 20 | {% endblock %} 21 | 22 | 23 | {% block title_prefix -%} 24 | {% if 'wiki-name' in config %}{{ config['wiki-name']|e }} » {% endif %} 25 | {%- endblock %} 26 | {% block title -%} 27 | {{ title }} 28 | {%- endblock %} 29 | 30 | 31 | {% block css %} 32 | 33 | {{ html.cssimport(("http://yui.yahooapis.com/combo?" + 34 | "3.0.0/build/cssreset/reset-min.css&" + 35 | "3.0.0/build/cssfonts/fonts-min.css&" + 36 | "3.0.0/build/cssbase/base-min.css") | e) }} 37 | 38 | {{ html.cssimport(make_relative("/media/css/style.css")) }} 39 | {{ html.cssimport(make_relative("/media/css/pygments.css")) }} 40 | {% endblock %} 41 | 42 | {% block js %}{% endblock %} 43 | 44 | {% block analytics %} 45 | {% if 'google-analytics' in config %} 46 | 47 | 57 | {% endif %} 58 | {% endblock analytics %} 59 | {% endblock head %} 60 | 61 | 62 | 63 | {% block body %} 64 | 65 | {% block body_header %} 66 | {% block crumbs %} 67 | {{ breadcrumbs.crumbs(crumbs) }} 68 | {% endblock crumbs %} 69 | {% endblock body_header %} 70 | 71 |
72 | {% block content_header %} 73 | {% endblock content_header %} 74 | 75 | {% block content %} 76 | {% endblock content %} 77 | 78 | {% block content_footer %} 79 | {% endblock content_footer %} 80 | 81 |
82 |
83 | 84 | {% block body_footer %} 85 | 93 | {% endblock body_footer %} 94 | 95 | {% endblock body %} 96 | 97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /test/builder.doctest: -------------------------------------------------------------------------------- 1 | >>> import os.path as p 2 | >>> from markdoc.builder import Builder, get_title 3 | >>> b = Builder(CONFIG) 4 | 5 | Filesystem Interaction 6 | ====================== 7 | 8 | You can generate breadcrumbs for a document, based on its absolute path (which will be relativized to the wiki root): 9 | 10 | >>> b.crumbs(p.join(WIKI_ROOT, 'wiki', 'index.md')) 11 | [('index', None)] 12 | 13 | >>> b.crumbs(p.join(WIKI_ROOT, 'wiki', 'somefile.md')) 14 | [('index', '/'), ('somefile', None)] 15 | 16 | >>> b.crumbs(p.join(WIKI_ROOT, 'wiki', 'somedir', 'index.md')) 17 | [('index', '/'), ('somedir', None)] 18 | 19 | >>> b.crumbs(p.join(WIKI_ROOT, 'wiki', 'somedir', 'something.md')) 20 | [('index', '/'), ('somedir', '/somedir/'), ('something', None)] 21 | 22 | You can also just use relative paths (relative to `WIKI_ROOT/wiki`): 23 | 24 | >>> b.crumbs('index.md') 25 | [('index', None)] 26 | 27 | >>> b.crumbs('somefile.md') 28 | [('index', '/'), ('somefile', None)] 29 | 30 | >>> b.crumbs(p.join('somedir', 'index.md')) 31 | [('index', '/'), ('somedir', None)] 32 | 33 | >>> b.crumbs(p.join('somedir', 'something.md')) 34 | [('index', '/'), ('somedir', '/somedir/'), ('something', None)] 35 | 36 | You can also walk through all files in the wiki, using the `walk()` method: 37 | 38 | >>> import os 39 | >>> for filepath in b.walk(): 40 | ... print filepath 41 | an_empty_file.md 42 | file1.md 43 | file2.md 44 | file3.md 45 | subdir/hello.md 46 | 47 | Rendering 48 | ========= 49 | 50 | You can render documents with the `Builder.render()` method: 51 | 52 | >>> b.render('file1.md') 53 | u'

Hello

' 54 | >>> b.render('file2.md') 55 | u'

World

' 56 | >>> b.render('file3.md') 57 | u'

Foo

' 58 | 59 | Titles 60 | ------ 61 | 62 | The `get_title()` function deals with retrieving titles from files. Initially, the file content itself is used to try to get a title. Either text in a specially formatted HTML comment, like this: 63 | 64 | >>> get_title(None, '') 65 | 'Some Title' 66 | 67 | Or the first `

` element: 68 | 69 | >>> get_title(None, '

Some Title

') 70 | 'Some Title' 71 | 72 | If those both fail, the filename is used to produce a title: 73 | 74 | >>> get_title('some_title.arbitraryextension', '') 75 | 'Some Title' 76 | 77 | Since this is linked to the documents themselves, you can use the `title()` method on the builder instance to get the title for a given document: 78 | 79 | >>> b.title('file1.md') 80 | u'Hello' 81 | >>> b.title('file2.md') 82 | u'World' 83 | >>> b.title('file3.md') 84 | u'Foo' 85 | >>> b.title('an_empty_file.md') 86 | 'An Empty File' 87 | 88 | Documents 89 | --------- 90 | 91 | You can render whole documents using `Builder.render_document()`: 92 | 93 | >>> print b.render_document('file1.md') 94 | 95 | 96 | 97 | 98 | Hello 99 | 100 | 101 | 102 |

Hello

103 | 104 | 105 | 106 | >>> print b.render_document('file2.md') 107 | 108 | 109 | 110 | 111 | World 112 | 113 | 114 | 115 |

World

116 | 117 | 118 | 119 | This uses the `document.html` Jinja2 template, by default located in `WIKI_ROOT/.templates/`, to produce the documents. 120 | -------------------------------------------------------------------------------- /src/markdoc/static/default-static/media/css/pygments.css: -------------------------------------------------------------------------------- 1 | .codehilite { background: #ffffff; } 2 | .codehilite .c { color: #999988; font-style: italic } /* Comment */ 3 | .codehilite .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | .codehilite .k { font-weight: bold } /* Keyword */ 5 | .codehilite .o { font-weight: bold } /* Operator */ 6 | .codehilite .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | .codehilite .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 8 | .codehilite .c1 { color: #999988; font-style: italic } /* Comment.Single */ 9 | .codehilite .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 10 | .codehilite .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 11 | .codehilite .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ 12 | .codehilite .ge { font-style: italic } /* Generic.Emph */ 13 | .codehilite .gr { color: #aa0000 } /* Generic.Error */ 14 | .codehilite .gh { color: #999999 } /* Generic.Heading */ 15 | .codehilite .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 16 | .codehilite .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ 17 | .codehilite .go { color: #888888 } /* Generic.Output */ 18 | .codehilite .gp { color: #555555 } /* Generic.Prompt */ 19 | .codehilite .gs { font-weight: bold } /* Generic.Strong */ 20 | .codehilite .gu { color: #aaaaaa } /* Generic.Subheading */ 21 | .codehilite .gt { color: #aa0000 } /* Generic.Traceback */ 22 | .codehilite .kc { font-weight: bold } /* Keyword.Constant */ 23 | .codehilite .kd { font-weight: bold } /* Keyword.Declaration */ 24 | .codehilite .kp { font-weight: bold } /* Keyword.Pseudo */ 25 | .codehilite .kr { font-weight: bold } /* Keyword.Reserved */ 26 | .codehilite .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 27 | .codehilite .m { color: #009999 } /* Literal.Number */ 28 | .codehilite .s { color: #d14 } /* Literal.String */ 29 | .codehilite .na { color: #008080 } /* Name.Attribute */ 30 | .codehilite .nb { color: #0086B3 } /* Name.Builtin */ 31 | .codehilite .nc { color: #445588; font-weight: bold } /* Name.Class */ 32 | .codehilite .no { color: #008080 } /* Name.Constant */ 33 | .codehilite .ni { color: #800080 } /* Name.Entity */ 34 | .codehilite .ne { color: #990000; font-weight: bold } /* Name.Exception */ 35 | .codehilite .nf { color: #990000; font-weight: bold } /* Name.Function */ 36 | .codehilite .nn { color: #555555 } /* Name.Namespace */ 37 | .codehilite .nt { color: #000080 } /* Name.Tag */ 38 | .codehilite .nv { color: #008080 } /* Name.Variable */ 39 | .codehilite .ow { font-weight: bold } /* Operator.Word */ 40 | .codehilite .w { color: #bbbbbb } /* Text.Whitespace */ 41 | .codehilite .mf { color: #009999 } /* Literal.Number.Float */ 42 | .codehilite .mh { color: #009999 } /* Literal.Number.Hex */ 43 | .codehilite .mi { color: #009999 } /* Literal.Number.Integer */ 44 | .codehilite .mo { color: #009999 } /* Literal.Number.Oct */ 45 | .codehilite .sb { color: #d14 } /* Literal.String.Backtick */ 46 | .codehilite .sc { color: #d14 } /* Literal.String.Char */ 47 | .codehilite .sd { color: #d14 } /* Literal.String.Doc */ 48 | .codehilite .s2 { color: #d14 } /* Literal.String.Double */ 49 | .codehilite .se { color: #d14 } /* Literal.String.Escape */ 50 | .codehilite .sh { color: #d14 } /* Literal.String.Heredoc */ 51 | .codehilite .si { color: #d14 } /* Literal.String.Interpol */ 52 | .codehilite .sx { color: #d14 } /* Literal.String.Other */ 53 | .codehilite .sr { color: #009926 } /* Literal.String.Regex */ 54 | .codehilite .s1 { color: #d14 } /* Literal.String.Single */ 55 | .codehilite .ss { color: #990073 } /* Literal.String.Symbol */ 56 | .codehilite .bp { color: #999999 } /* Name.Builtin.Pseudo */ 57 | .codehilite .vc { color: #008080 } /* Name.Variable.Class */ 58 | .codehilite .vg { color: #008080 } /* Name.Variable.Global */ 59 | .codehilite .vi { color: #008080 } /* Name.Variable.Instance */ 60 | .codehilite .il { color: #009999 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /doc/about.md: -------------------------------------------------------------------------------- 1 | # About Markdoc 2 | 3 | Markdoc is a project which aims to provide a lightweight alternative to large 4 | database-powered wiki systems. I’ve listed the main goals of the project below; 5 | I believe that, in its current state, it meets all of these. 6 | 7 | 8 | ## Goals & Philosophy 9 | 10 | ### Wikis 11 | 12 | * Wikis should be made up of plain-text files, without requiring a running 13 | instance of MySQL or even an SQLite database. 14 | 15 | * There should only be one simple-to-write plain-text configuration file. 16 | 17 | * Wikis should be VCS-friendly, yet VCS-agnostic. 18 | 19 | * It should be possible to compile a wiki to static HTML, and then to serve 20 | this HTML with no wiki-specific software. 21 | 22 | 23 | ### Markdown 24 | 25 | I chose Markdown as the format for this wiki system because of its simplicity, 26 | familiarity for many writers, and the extensibility of its Python 27 | implementation. For example, Pygments syntax highlighting is available through a 28 | single configuration option in the `markdoc.yaml` file. The ability to embed raw 29 | HTML in Markdown documents gives it power and flexibility. 30 | 31 | 32 | ### Command-Line Interface 33 | 34 | * The CLI should be intuitive and easy to use. 35 | 36 | * There should only be a few different sub-commands, each of which does what 37 | one would expect it to. 38 | 39 | * There should be a full web server included, in the case that setting up a 40 | large-scale HTTP server is impractical or impossible. 41 | 42 | * The CLI should be pure-Python, for portability and extensibility. 43 | 44 | 45 | ## License 46 | 47 | Markdoc is [public domain software](http://unlicense.org/). 48 | 49 | > This is free and unencumbered software released into the public domain. 50 | > 51 | > Anyone is free to copy, modify, publish, use, compile, sell, or distribute 52 | > this software, either in source code form or as a compiled binary, for any 53 | > purpose, commercial or non-commercial, and by any means. 54 | > 55 | > In jurisdictions that recognize copyright laws, the author or authors of this 56 | > software dedicate any and all copyright interest in the software to the public 57 | > domain. We make this dedication for the benefit of the public at large and to 58 | > the detriment of our heirs and successors. We intend this dedication to be an 59 | > overt act of relinquishment in perpetuity of all present and future rights to 60 | > this software under copyright law. 61 | > 62 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 63 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 64 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 65 | > AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 66 | > ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 67 | > WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 68 | > 69 | > For more information, please refer to 70 | 71 | 72 | The bundled Pygments style (it’s the [Github][] syntax highlighting theme) was 73 | created by [Tom Preston-Werner][]; it was sourced from [his blog][] and is 74 | licensed under the MIT License: 75 | 76 | [github]: http://github.com/ 77 | [tom preston-werner]: http://tom.preston-werner.com/ 78 | [his blog]: http://github.com/mojombo/tpw/ 79 | 80 | > Copyright © 2010 Tom Preston-Werner 81 | > 82 | > Permission is hereby granted, free of charge, to any person 83 | > obtaining a copy of this software and associated documentation 84 | > files (the "Software"), to deal in the Software without 85 | > restriction, including without limitation the rights to use, 86 | > copy, modify, merge, publish, distribute, sublicense, and/or sell 87 | > copies of the Software, and to permit persons to whom the 88 | > Software is furnished to do so, subject to the following 89 | > conditions: 90 | > 91 | > The above copyright notice and this permission notice shall be 92 | > included in all copies or substantial portions of the Software. 93 | > 94 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 95 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 96 | > OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 97 | > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 98 | > HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 99 | > WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 100 | > FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 101 | > OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/markdoc/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import codecs 4 | from functools import wraps 5 | import os 6 | import os.path as p 7 | import time 8 | 9 | 10 | class DocumentCache(object): 11 | 12 | """ 13 | A high-level document cache for caching the content of files. 14 | 15 | This is a read-only cache which uses the OS-reported modification timestamps 16 | for files (via `os.stat()`) to determine cache dirtiness, and then refreshes 17 | its cache behind the scenes when files are requested. 18 | 19 | You can access values via `.get()` (which supports several options) or via 20 | simple subscription syntax (i.e. `cache[path]`). The cache is configured 21 | with a 'root' on instantiation, by which all relative paths are resolved. 22 | """ 23 | 24 | def __init__(self, base=None, cache=None, encoding='utf-8'): 25 | if cache is None: 26 | cache = {} 27 | self.cache = cache 28 | 29 | if base is None: 30 | base = os.getcwd() 31 | self.base = base 32 | 33 | self.encoding = encoding 34 | 35 | absolute = lambda self, relpath: p.join(self.base, relpath) 36 | relative = lambda self, abspath: p.relpath(abspath, start=self.base) 37 | 38 | def has_latest_version(self, path): 39 | """Determine whether the cache for a path is up to date.""" 40 | 41 | # No-op for already-absolute paths. 42 | path = self.absolute(path) 43 | if path not in self.cache: 44 | return False 45 | cached_mtime = self.cache[path][0] 46 | return os.stat(path).st_mtime <= cached_mtime 47 | 48 | def refresh_cache(self, path, encoding=None): 49 | """Refresh the cache, no matter what, with an optional encoding.""" 50 | 51 | path = self.absolute(path) 52 | encoding = encoding or self.encoding 53 | data = read_from(path, encoding=encoding) 54 | mtime = os.stat(path).st_mtime 55 | self.cache[path] = (mtime, data) 56 | 57 | def update_to_latest_version(self, path): 58 | """If necessary, refresh the cache's copy of a file.""" 59 | 60 | if not self.has_latest_version(path): 61 | self.refresh_cache(path) 62 | 63 | def get(self, path, cache=True, encoding=None): 64 | """Retrieve the data for a given path, optionally using the cache.""" 65 | 66 | path = self.absolute(path) 67 | 68 | if cache: 69 | self.update_to_latest_version(path) 70 | return self.cache[path][1] # (mtime, data)[1] 71 | 72 | if not p.isfile(path): 73 | return None 74 | 75 | if encoding is None: 76 | encoding = self.encoding 77 | return read_from(path, encoding=encoding) 78 | 79 | def __getitem__(self, path): 80 | result = self.get(path) 81 | if result is None: 82 | raise KeyError(path) 83 | return result 84 | 85 | 86 | class RenderCache(object): 87 | 88 | def __init__(self, render_func, document_cache): 89 | self.render_func = render_func 90 | self.doc_cache = document_cache 91 | # The two-cache structure allows us to garbage collect rendered results 92 | # for old versions of documents. 93 | # pathname => document hash 94 | self.hash_cache = {} 95 | # document hash => rendered results 96 | self.result_cache = {} 97 | 98 | def render(self, path, cache=True): 99 | """Render the contents of a filename, optionally using the cache.""" 100 | 101 | document = self.doc_cache.get(path, cache=cache) 102 | 103 | if cache: 104 | doc_hash = (hash(path), hash(document)) 105 | if path in self.hash_cache and self.hash_cache[path] != doc_hash: 106 | self.result_cache.pop(self.hash_cache[path], None) 107 | self.hash_cache[path] = doc_hash 108 | 109 | if doc_hash not in self.result_cache: 110 | self.result_cache[doc_hash] = self.render_func(path, document) 111 | return self.result_cache[doc_hash] 112 | else: 113 | return self.render_func(document) 114 | 115 | get = render # For compatibility with the document cache. 116 | 117 | 118 | def read_from(filename, encoding='utf-8'): 119 | """Read data from a filename, optionally with an encoding.""" 120 | 121 | if encoding is None: 122 | fp = open(filename) 123 | else: 124 | fp = codecs.open(filename, encoding=encoding) 125 | 126 | try: 127 | return fp.read() 128 | finally: 129 | fp.close() 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdoc 2 | 3 | Markdoc is a lightweight Markdown-based wiki system. It’s been designed to allow 4 | you to create and manage wikis as quickly and easily as possible. 5 | 6 | 7 | ## What is it good for? 8 | 9 | Potential use cases for Markdoc include, but aren’t limited to: 10 | 11 | * **Technical Documentation/Manuals** 12 | Markdoc can be used to write and render hand-written guides and manuals for 13 | software. Such documentation will normally be separate from 14 | automatically-generated API documentation, and might give a higher-level 15 | view than API docs alone. It might be used for client documentation for 16 | web/desktop applications, or even developer documentation for frameworks. 17 | 18 | * **Internal Project Wikis** 19 | Markdoc wikis consist of a single plain-text file per page. By combining a 20 | wiki with a DVCS (such as [Mercurial][] or [Git][]), you can collaborate 21 | with several other people. Use the DVCS to track, share and merge changes 22 | with one another, and view the wiki’s history. 23 | 24 | [Mercurial]: http://mercurial.selenic.com/ 25 | [Git]: http://git-scm.com/ 26 | 27 | * **Static Site Generation** 28 | Markdoc converts wikis into raw HTML files and media. This allows you to 29 | manage a blog, personal website or a collection of pages in a Markdoc wiki, 30 | perhaps with custom CSS styles, and publish the rendered HTML to a website. 31 | Markdoc need not be installed on the hosting site, since the resultant HTML 32 | is completely independent. 33 | 34 | 35 | ## Cool Features 36 | 37 | * Set up [Google Analytics][] tracking in one line of configuration. 38 | 39 | * [Barebones][] wikis that just look like directories with Markdown-formatted 40 | text files in them. 41 | 42 | * A built-in HTTP server and WSGI application to serve up a compiled wiki with 43 | a single command. 44 | 45 | * Continuous builds (via `rsync`) mean the server can keep running whilst 46 | Markdoc re-compiles the wiki. Just refresh your browser to see the changes. 47 | 48 | * Add [Pygments][]-powered syntax highlighting to your Markdoc wiki with a 49 | single [configuration parameter][syntax-highlighting]. 50 | 51 | * Markdoc is [public domain software][licensing]. It will always be completely 52 | free to use, and you can redistribute it (in part or in whole) under any 53 | circumstances (open-source, proprietary or otherwise) with no attribution or 54 | encumberances. 55 | 56 | [google analytics]: http://markdoc.org/ref/configuration#metadata 57 | [barebones]: http://markdoc.org/tips/barebones 58 | [pygments]: http://pygments.org/ 59 | [syntax-highlighting]: http://markdoc.org/tips/syntax-highlighting 60 | [licensing]: http://markdoc.org/about#license 61 | 62 | 63 | ## Quickstart 64 | 65 | ### Requirements 66 | 67 | The minimum requirements to run the Markdoc utility are: 68 | 69 | * Python 2.4 or later (2.5+ highly recommended) 70 | * A UNIX (or at least POSIX-compliant) operating system 71 | * [rsync](http://www.samba.org/rsync/) -- installed out of the box with most 72 | modern OSes, including Mac OS X and Ubuntu. In the future Markdoc may 73 | include a pure-Python implementation. 74 | 75 | 76 | ### Installation 77 | 78 | $ easy_install Markdoc # OR 79 | $ pip install Markdoc 80 | 81 | 82 | ### Making a Wiki 83 | 84 | markdoc init my-wiki 85 | cd my-wiki/ 86 | vim wiki/somefile.md 87 | # ... write some documentation ... 88 | markdoc build 89 | markdoc serve 90 | # .. open http://localhost:8008/ in a browser ... 91 | 92 | 93 | ## (Un)license 94 | 95 | This is free and unencumbered software released into the public domain. 96 | 97 | Anyone is free to copy, modify, publish, use, compile, sell, or 98 | distribute this software, either in source code form or as a compiled 99 | binary, for any purpose, commercial or non-commercial, and by any 100 | means. 101 | 102 | In jurisdictions that recognize copyright laws, the author or authors 103 | of this software dedicate any and all copyright interest in the 104 | software to the public domain. We make this dedication for the benefit 105 | of the public at large and to the detriment of our heirs and 106 | successors. We intend this dedication to be an overt act of 107 | relinquishment in perpetuity of all present and future rights to this 108 | software under copyright law. 109 | 110 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 111 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 112 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 113 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 114 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 115 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 116 | OTHER DEALINGS IN THE SOFTWARE. 117 | 118 | For more information, please refer to 119 | -------------------------------------------------------------------------------- /src/markdoc/static/default-static/media/css/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #f6f6f6; } 3 | 4 | body { 5 | background-color: white; 6 | margin: 0 auto; 7 | width: 650px; } 8 | body #breadcrumbs, body #content, body #footer { 9 | background-color: white; 10 | clear: both; 11 | float: left; 12 | overflow: hidden; 13 | padding: 0 20px; 14 | width: 610px; } 15 | body #breadcrumbs { 16 | border-bottom: 2px solid #f6f6f6; 17 | height: 28px; 18 | margin: 0; 19 | padding: 0; 20 | width: 650px; } 21 | body #breadcrumbs li { 22 | float: left; 23 | list-style: none; 24 | margin: 0; 25 | padding: 0; } 26 | body #breadcrumbs li a { 27 | display: block; 28 | float: left; 29 | padding: 0 8px; } 30 | body #breadcrumbs li.last { 31 | padding: 0 8px; } 32 | body #breadcrumbs li.not-last:after { 33 | content: "»"; 34 | float: right; } 35 | body #footer { 36 | border-top: 8px solid #f6f6f6; 37 | padding-top: 13px; } 38 | body .clear { 39 | clear: both; 40 | border-width: 0; 41 | margin: 0; 42 | visibility: hidden; } 43 | 44 | body.listing table#pages tr, body.listing table#subdirs tr, body.listing table#files tr { 45 | border-bottom: 1px solid #777; 46 | border-top: 1px solid #777; } 47 | body.listing table#pages td, body.listing table#subdirs td, body.listing table#files td { 48 | border: none; } 49 | body.listing table#pages td.size, body.listing table#subdirs td.size, body.listing table#files td.size { 50 | background-color: #f6f6f6; } 51 | body.listing table#pages td.name, body.listing table#subdirs td.name, body.listing table#files td.name { 52 | padding: 0; } 53 | body.listing table#pages td.name a, body.listing table#subdirs td.name a, body.listing table#files td.name a { 54 | display: block; 55 | margin: 0; 56 | padding: 4px 8px; } 57 | 58 | blockquote { 59 | background-color: #f6f6f6; 60 | padding: 13px; 61 | padding-bottom: 1px; } 62 | 63 | hr { 64 | border-style: solid; 65 | border: none; 66 | border-top: 1px solid #777; 67 | margin: 28px 0; } 68 | 69 | dl { 70 | margin-left: 0; } 71 | dl dd { 72 | margin-bottom: 13px; 73 | margin-left: 13px; } 74 | 75 | ul { 76 | margin-top: 0; } 77 | ul li { 78 | list-style: square outside; } 79 | ul ul { 80 | margin-bottom: 0; } 81 | 82 | pre { 83 | border-left: 1px solid gray; 84 | margin-bottom: 13px; 85 | margin-left: 30px; 86 | padding-left: 12px; } 87 | 88 | .codehilite { 89 | border-left: 1px solid gray; 90 | margin-bottom: 13px; 91 | margin-left: 30px; 92 | padding-left: 12px; } 93 | .codehilite pre { 94 | border: none; 95 | margin: 0; 96 | padding: 0; } 97 | 98 | .codehilitetable { 99 | margin-left: 0; 100 | padding-left: 0; } 101 | .codehilitetable tr td { 102 | border: none; 103 | padding: 3px 5px 0 5px; } 104 | .codehilitetable tr td.linenos { 105 | background-color: #f6f6f6; 106 | border-right: 1px solid gray; 107 | margin: 0; 108 | padding-right: 6px; 109 | text-align: right; 110 | width: 19px; } 111 | .codehilitetable tr td.linenos .linenodiv pre { 112 | border: none; 113 | margin: 0; 114 | padding: 0; } 115 | .codehilitetable tr td.code { 116 | margin: 0; 117 | padding-left: 12px; } 118 | .codehilitetable tr td.code .codehilite { 119 | border: none; 120 | margin: 0; 121 | padding: 0; } 122 | 123 | body { 124 | font-family: 'Helvetica Neue', Helvetica, Arial, Geneva, sans-serif; 125 | line-height: 21px; } 126 | body #breadcrumbs li { 127 | color: #aaa; 128 | font-size: 13px; 129 | font-weight: bold; 130 | line-height: 28px; } 131 | body #breadcrumbs li a { 132 | text-decoration: none; } 133 | body #breadcrumbs li .list-crumb { 134 | font-weight: normal; } 135 | body #footer { 136 | color: #777; 137 | font-size: 13px; 138 | text-transform: lowercase; } 139 | body.listing table#pages td.size, body.listing table#subdirs td.size { 140 | font-family: Menlo, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', Courier, 'Courier 10 Pitch', 'Courier New', monospace; 141 | text-align: right; } 142 | body.listing table#subdirs td.name { 143 | font-family: Courier, 'Courier 10 Pitch', 'Courier New', monospace; } 144 | 145 | h1, h2, h3, h4, h5, h6 { 146 | line-height: 21px; } 147 | 148 | h1 { 149 | font-size: 21px; } 150 | 151 | h2 { 152 | font-size: 18px; } 153 | 154 | h3 { 155 | font-size: 15px; } 156 | 157 | h4, h5, h6 { 158 | font-size: 13px; } 159 | 160 | a { 161 | color: #990000; 162 | text-decoration: none; } 163 | a:hover { 164 | color: #4c0000; } 165 | a[href^="http:"] { 166 | text-decoration: underline; } 167 | 168 | dl dt { 169 | font-weight: bold; } 170 | 171 | code { 172 | font-family: Courier, 'Courier 10 Pitch', 'Courier New', monospace; 173 | line-height: 18px; } 174 | 175 | pre { 176 | font-family: Menlo, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', Courier, 'Courier 10 Pitch', 'Courier New', monospace; 177 | font-size: 11px; 178 | line-height: 18px; } 179 | -------------------------------------------------------------------------------- /src/markdoc/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Utilities for working with Markdoc configurations.""" 4 | 5 | import copy 6 | import os 7 | import os.path as p 8 | 9 | import markdown 10 | import yaml 11 | 12 | import markdoc.exc 13 | 14 | 15 | class ConfigNotFound(markdoc.exc.AbortError): 16 | """The configuration file was not found.""" 17 | pass 18 | 19 | 20 | class ConfigMeta(type): 21 | 22 | def __new__(mcls, name, bases, attrs): 23 | cls = type.__new__(mcls, name, bases, attrs) 24 | cls._defaults = {} 25 | cls._func_defaults = {} 26 | return cls 27 | 28 | def register_default(cls, key, default_value): 29 | """Register a default value for a given key.""" 30 | 31 | cls._defaults[key] = default_value 32 | 33 | def register_func_default(cls, key, function): 34 | """Register a callable as a functional default for a key.""" 35 | 36 | cls._func_defaults[key] = function 37 | 38 | def func_default_for(cls, key): 39 | """Decorator to define a functional default for a given key.""" 40 | 41 | return lambda function: [cls.register_func_default(key, function), 42 | function][1] 43 | 44 | 45 | class Config(dict): 46 | 47 | """ 48 | A dictionary which represents a single wiki's Markdoc configuration. 49 | 50 | When instantiating this dictionary, if you aren't using an actual 51 | configuration file, just remember to set `config['meta.root']` to the 52 | wiki root; you can use `None` as the value for config_file. For example: 53 | 54 | # With a filename: 55 | config = Config('filename.yaml', {...}) 56 | 57 | # Without a filename: 58 | config = Config(None, {'meta': {'root': '/path/to/wiki/root/'}, ...}) 59 | 60 | """ 61 | 62 | __metaclass__ = ConfigMeta 63 | 64 | def __init__(self, config_file, config): 65 | super(Config, self).__init__(flatten(config)) 66 | 67 | self['meta.config-file'] = config_file 68 | self['meta.root'] = p.dirname(config_file) 69 | 70 | def __getitem__(self, key): 71 | try: 72 | return dict.__getitem__(self, key) 73 | except KeyError: 74 | if key in self._defaults: 75 | self[key] = copy.copy(self._defaults[key]) 76 | elif key in self._func_defaults: 77 | self[key] = self._func_defaults[key](self, key) 78 | else: 79 | raise 80 | return dict.__getitem__(self, key) 81 | 82 | def __delitem__(self, key): 83 | if (key not in self): 84 | return # fail silently. 85 | return dict.__delitem__(self, key) 86 | 87 | @classmethod 88 | def for_directory(cls, directory=None): 89 | 90 | """ 91 | Get the configuration from the 'markdoc.yaml' file in a directory. 92 | 93 | If you do not specify a directory, this method will use the current 94 | working directory. 95 | """ 96 | 97 | if directory is None: 98 | directory = os.getcwd() 99 | 100 | if p.exists(p.join(directory, 'markdoc.yaml')): 101 | return cls.for_file(p.join(directory, 'markdoc.yaml')) 102 | elif p.exists(p.join(directory, '.markdoc.yaml')): 103 | return cls.for_file(p.join(directory, '.markdoc.yaml')) 104 | raise ConfigNotFound("A markdoc configuration could not be found.") 105 | 106 | @classmethod 107 | def for_file(cls, filename): 108 | """Get the configuration from a given YAML file.""" 109 | 110 | if not p.exists(filename): 111 | relpath = p.relpath(p.dirname(filename), start=os.getcwd()) 112 | basename = p.basename(filename) 113 | if relpath == '.': 114 | raise ConfigNotFound("%s was not found in the current directory" % basename) 115 | raise ConfigNotFound("%s was not found in %s" % (basename, relpath)) 116 | 117 | fp = open(filename) 118 | try: 119 | config = yaml.load(fp) or {} 120 | finally: 121 | fp.close() 122 | 123 | return cls(filename, config) 124 | 125 | 126 | def flatten(dictionary, prefix=''): 127 | 128 | """ 129 | Flatten nested dictionaries into dotted keys. 130 | 131 | >>> d = { 132 | ... 'a': { 133 | ... 'b': 1, 134 | ... 'c': { 135 | ... 'd': 2, 136 | ... 'e': { 137 | ... 'f': 3 138 | ... } 139 | ... } 140 | ... }, 141 | ... 'g': 4, 142 | ... } 143 | 144 | >>> sorted(flatten(d).items()) 145 | [('a.b', 1), ('a.c.d', 2), ('a.c.e.f', 3), ('g', 4)] 146 | """ 147 | 148 | for key in dictionary.keys(): 149 | value = dictionary.pop(key) 150 | if not isinstance(value, dict): 151 | dictionary[prefix + key] = value 152 | else: 153 | for key2 in value.keys(): 154 | value2 = value.pop(key2) 155 | if not isinstance(value2, dict): 156 | dictionary[prefix + key + '.' + key2] = value2 157 | else: 158 | dictionary.update(flatten(value2, 159 | prefix=(prefix + key + '.' + key2 + '.'))) 160 | return dictionary 161 | -------------------------------------------------------------------------------- /doc/internals/rendering.md: -------------------------------------------------------------------------------- 1 | # Rendering 2 | 3 | This page will describe the process which documents undergo on their way from Markdoc to XHTML. 4 | 5 | ## Step 1: Markdown Rendering 6 | 7 | Each page is converted from Markdown to XHTML. This uses the Markdown library for Python, which comes with a number of extensions that you can enable in your [configuration](/configuration). For example, I like to use the `codehilite`, `def_list` and `headerid` Markdown extensions, but by default Markdoc wikis will not use any. 8 | 9 | The Markdown conversion results in XHTML data which is not tied to a page, so it’s not enough to display to a browser. That’s where the templating comes in. 10 | 11 | ## Step 2: Template Rendering 12 | 13 | Markdoc uses [Jinja2][] to render this partial XHTML to full XHTML documents. Jinja2 is a fast and flexible templating system for Python; if you are interested in writing your own templates, it would be wise to first consult its official documentation. 14 | 15 | [jinja2]: http://jinja2.pocoo.org 16 | 17 | Markdoc expects only two templates to be defined: `document.html` and `listing.html`. The way it finds these is as follows: 18 | 19 | * It first looks for them in the `.templates/` directory inside your wiki. 20 | * If the `use-default-templates` setting is `true` for your configuration (which it is by default), then search in the `default-templates` directory bundled with Markdoc. 21 | 22 | If `use-default-templates` is `false` and the templates are not defined in your wiki’s template directory, Markdoc will eventually raise an error. 23 | 24 | ### Documents 25 | 26 | `document.html` is used to convert the partial XHTML for a Markdown document into full, browser-ready XHTML. It receives a context much like the following: 27 | 28 | :::python 29 | { 30 | "content": "

...", # The XHTML for the document. 31 | "title": "Some Document", # The extracted title of the document. 32 | "crumbs": [("index", "/"), ("some-document", None)] # Breadcrumbs 33 | } 34 | 35 | The `config` variable is also (globally) set to the configuration dictionary for the current wiki. 36 | 37 | Take a look inside the `src/markdoc/static/default-templates/markdoc-default/` directory for examples of complete templates. 38 | 39 | ### Listings 40 | 41 | `listing.html` is used to generate listings for directories. This will only be used if `generate-listing` is set to either `always` or `sometimes` in the configuration (by default it is `always`). 42 | 43 | Listings are a little more complex to do, so they are generated after the complete set of documents have been rendered and synced (along with static media) to the HTML root. This means you get complete listings for all of your directories, including those which came from static media. 44 | 45 | The `listing.html` template is passed a context like this: 46 | 47 | :::python 48 | {"directory": "somedir", 49 | "crumbs": [("index", "/"), 50 | ("somedir", "/somedir/"), 51 | (jinja2.Markup('list'), None)], 52 | "files": [{"basename": "example.css", 53 | "href": "/example.css", 54 | "humansize": "27B", 55 | "size": 27, 56 | "slug": "example"}], 57 | "pages": [{"basename": "hello.html", 58 | "href": "/subdir/hello", 59 | "humansize": "268B", 60 | "size": 268, 61 | "slug": "hello", 62 | "title": u"Hello again."}], 63 | "sub_directories": [{"basename": "subdir", "href": "/subdir/"}]} 64 | 65 | The distinction between *files* and *pages* is useful because you can display links to pages with their full titles; if you’re viewing this now in a browser, just head to [/_list](/_list) to see what I mean. The filename which the list is output to can be configured with the `listing-filename` setting; this defaults to `_list.html` (hence `/_list`). You can also try `/media/_list`, et cetera. 66 | 67 | The last crumb is special in that it displays the string `"list"` but with a class of `list-crumb`; in the default templates and media this is displayed in a light grey to indicate that it is a special page. 68 | 69 | The semantics of listing generation are determined by the `generate-listing` setting; `always` will always generate a listing (even if it clobbers an existing file called `_list.html`), `sometimes` will only generate a listing when there is no `index.html` file for a directory, and `never` will never generate listings. 70 | 71 | ## Relative Links 72 | 73 | For portability, all URLs pointing to files and pages within the current Markdoc wiki should be relative. This allows built wiki HTML to be hosted under a sub-directory and still maintain link integrity. 74 | 75 | In practice, this is achieved in two parts: 76 | 77 | * A Markdown extension which causes absolute path URLs in links (such as 78 | `/path/to/somefile`) to be converted to relative ones (like `../somefile`). 79 | 80 | * A callable passed to every template context which, when called with an 81 | absolute path URL, will convert it to a relative one. This variable is 82 | `make_relative()`, and an example of its use can be seen in this snippet 83 | from the default base template: 84 | 85 | :::text 86 | 87 | 88 | 89 | {% import "macros/html" as html -%} 90 | 91 | {{ html.cssimport(make_relative("/media/css/reset.css")) }} 92 | {{ html.cssimport(make_relative("/media/css/layout.css")) }} 93 | {{ html.cssimport(make_relative("/media/css/typography.css")) }} 94 | {{ html.cssimport(make_relative("/media/css/pygments.css")) }} 95 | 96 | 97 | If you override the default templates, make sure to use this callable to 98 | relativize the media URLs and breadcrumb links. 99 | -------------------------------------------------------------------------------- /src/markdoc/wsgi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import logging 4 | import mimetypes 5 | import os.path as p 6 | 7 | import webob 8 | 9 | from markdoc.render import make_relative 10 | 11 | 12 | if not mimetypes.inited: 13 | mimetypes.init() 14 | # Assume all HTML files are XHTML. 15 | mimetypes.types_map['.html'] = mimetypes.types_map['.xhtml'] 16 | 17 | 18 | class MarkdocWSGIApplication(object): 19 | 20 | """ 21 | A WSGI application which will serve up a Markdoc wiki. 22 | 23 | Note that this application is not specifically reserved for Markdoc wikis, 24 | but was designed especially for them. The handling of requests is simple, 25 | and is based on the request path: 26 | 27 | /[a/b/.../c/]filename 28 | * If the file exists relative to the docroot, serve it; else 29 | * If the filename with the extension 'html' exists relative to the 30 | docroot, serve it; else 31 | * If a directory exists with that name, return a redirect to it (with a 32 | trailing slash); else 33 | * Return a HTTP 404 ‘Not Found’. 34 | 35 | /[a/b/.../c/]directory/ (including the index, '/') 36 | * If the directory exists, look for an 'index.html' file inside it, and 37 | serve it if it exists; else 38 | * If a file of the same name exists in the parent directory, return a 39 | redirect to it (without the trailing slash); else 40 | * Return a HTTP 404 ‘Not Found’. 41 | 42 | In the context of Markdoc, if a directory does not contain an 'index.md' 43 | file, a listing will be generated and saved as the 'index.html' file for 44 | that directory. 45 | """ 46 | 47 | def __init__(self, config): 48 | self.config = config 49 | self.log = logging.getLogger('markdoc.wsgi') 50 | 51 | def __call__(self, environ, start_response): 52 | request = webob.Request(environ) 53 | response = self.get_response(request) 54 | self.log.info('%s %s - %d' % (request.method, request.path_info, response.status_int)) 55 | return response(environ, start_response) 56 | 57 | def is_safe(self, directory): 58 | """Make sure the given absolute path does not point above the htroot.""" 59 | 60 | return p.pardir not in p.relpath(directory, start=self.config.html_dir).split(p.sep) 61 | 62 | def get_response(self, request): 63 | if request.path_info.endswith('/'): 64 | return self.directory(request) 65 | return self.file(request) 66 | 67 | def directory(self, request): 68 | 69 | """ 70 | Serve a request which points to a directory. 71 | 72 | * If the directory exists, look for an 'index.html' file inside it, and 73 | serve it if it exists; else 74 | * If a file of the same name exists in the parent directory, return a 75 | redirect to it (without the trailing slash); else 76 | * If a file of the same name with a 'html' extension exists in the 77 | parent directory, redirect to it (without the trailing slash); else 78 | * Return a HTTP 404 ‘Not Found’. 79 | """ 80 | 81 | path_parts = request.path_info.strip('/').split('/') 82 | index_filename = p.join(self.config.html_dir, *(path_parts + ['index.html'])) 83 | if p.exists(index_filename) and self.is_safe(index_filename): 84 | return serve_file(index_filename) 85 | 86 | directory_filename = p.join(self.config.html_dir, *path_parts) 87 | if p.isfile(directory_filename) or p.isfile(directory_filename + p.extsep + 'html'): 88 | return temp_redirect(request.path_info.rstrip('/')) 89 | 90 | return self.not_found(request) 91 | 92 | def file(self, request): 93 | 94 | """ 95 | Serve a request which points to a file. 96 | 97 | * If the file exists relative to the docroot, serve it; else 98 | * If the filename with the extension 'html' exists relative to the 99 | docroot, serve it; else 100 | * If a directory exists with that name, return a redirect to it (with a 101 | trailing slash); else 102 | * Return a HTTP 404 ‘Not Found’. 103 | """ 104 | 105 | path_parts = request.path_info.strip('/').split('/') 106 | filename = p.abspath(p.join(self.config.html_dir, *path_parts)) 107 | if not self.is_safe(filename): 108 | return self.forbidden(request) 109 | 110 | if p.isfile(filename): 111 | pass 112 | elif p.isfile(filename + p.extsep + 'html'): 113 | filename = filename + p.extsep + 'html' 114 | else: 115 | if p.isdir(filename): 116 | return temp_redirect(request.path_info + '/') 117 | return self.not_found(request) 118 | 119 | return serve_file(filename) 120 | 121 | def error(self, request, status): 122 | 123 | """ 124 | Serve a page for a given HTTP error. 125 | 126 | This works by rendering a template based on the HTTP error code; so an 127 | error of '404 Not Found' will render the '404.html' template. The 128 | context passed to the template is as follows: 129 | 130 | `request` 131 | : The `webob.Request` object for this HTTP request. 132 | 133 | `is_index` 134 | : A boolean indicating whether or not this is the index page. This may 135 | be useful in error pages where you want to link back to the home page; 136 | such a link will be useless in the index. 137 | 138 | `status` 139 | : An integer representing the HTTP status code of this error. 140 | 141 | `reason` 142 | : A string of the HTTP status 'reason', such as 'Not Found' for 404. 143 | 144 | The template is assumed to be valid XHTML. 145 | 146 | Note that the templating machinery is only invoked when the browser is 147 | expecting HTML. This is determined by calling 148 | `request.accept.accept_html()`. If not, an empty response (i.e. one 149 | without a content body) is returned. 150 | """ 151 | 152 | response = webob.Response() 153 | response.status = status 154 | 155 | if request.accept.accept_html(): 156 | context = {} 157 | context['request'] = request 158 | context['is_index'] = request.path_info in ['/', '/index.html'] 159 | context['make_relative'] = lambda href: make_relative(request.path_info, href) 160 | context['status'] = status 161 | context['reason'] = webob.util.status_reasons[status] 162 | 163 | template = self.config.template_env.get_template('%d.html' % status) 164 | response.unicode_body = template.render(context) 165 | response.content_type = mimetypes.types_map['.xhtml'] 166 | else: 167 | del response.content_length 168 | del response.content_type 169 | 170 | return response 171 | 172 | forbidden = lambda self, request: self.error(request, 403) 173 | not_found = lambda self, request: self.error(request, 404) 174 | 175 | 176 | def redirect(location, permanent=False): 177 | """Issue an optionally-permanent redirect to another location.""" 178 | 179 | response = webob.Response() 180 | response.status = 301 if permanent else 302 181 | response.location = location 182 | 183 | del response.content_type 184 | del response.content_length 185 | 186 | return response 187 | 188 | temp_redirect = lambda location: redirect(location, permanent=False) 189 | perm_redirect = lambda location: redirect(location, permanent=True) 190 | 191 | 192 | def serve_file(filename, content_type=None, chunk_size=4096): 193 | 194 | """ 195 | Serve the specified file as a chunked response. 196 | 197 | Return a `webob.Response` instance which will serve up the file in chunks, 198 | as specified by the `chunk_size` parameter (default 4KB). 199 | 200 | You can also specify a content type with the `content_type` keyword 201 | argument. If you do not, the content type will be inferred from the 202 | filename; so 'index.html' will be interpreted as 'application/xhtml+xml', 203 | 'file.mp3' as 'audio/mpeg', et cetera. If none can be guessed, the content 204 | type will be reported as 'application/octet-stream'. 205 | """ 206 | 207 | if content_type is None: 208 | content_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' 209 | 210 | if content_type.startswith('text/html'): 211 | content_type = content_type.replace('text/html', 'application/xhtml+xml') 212 | 213 | def chunked_read(chunk_size=4096): 214 | fp = open(filename, 'rb') 215 | try: 216 | data = fp.read(chunk_size) 217 | while data: 218 | yield data 219 | data = fp.read(chunk_size) 220 | finally: 221 | fp.close() 222 | 223 | response = webob.Response(content_type=content_type) 224 | response.app_iter = chunked_read() 225 | response.content_length = p.getsize(filename) 226 | return response 227 | -------------------------------------------------------------------------------- /doc/ref/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | All Markdoc wikis are configured via a single `markdoc.yaml` file in the wiki 4 | root. This file is formatted with YAML (Yet Another Markup Language); you can 5 | find more information on that at [yaml.org](http://yaml.org/). When running the 6 | command-line interface, Markdoc will search for either a `markdoc.yaml` or a 7 | `.markdoc.yaml` file in the current directory. You can also explicitly specify a 8 | file to use with the `-c`/`--config` command-line option. 9 | 10 | ## Example 11 | 12 | Here we’ll go through an example and show how the various settings affect 13 | Markdoc’s behavior: 14 | 15 | #!yaml 16 | # Metadata 17 | wiki-name: "My Wiki" 18 | google-analytics: UA-XXXXXX-X 19 | 20 | # Directories 21 | hide-prefix: "." 22 | wiki-dir: "wiki" 23 | static-dir: "static" 24 | html-dir: ".html" 25 | template-dir: ".templates" 26 | temp-dir: ".tmp" 27 | cvs-exclude: true 28 | 29 | # Building 30 | document-extensions: [.md, .mdown, .markdown, .wiki, .text] 31 | generate-listing: always 32 | listing-filename: "_list.html" 33 | use-default-static: true 34 | use-default-templates: true 35 | 36 | # Rendering 37 | markdown: 38 | safe-mode: false 39 | output-format: xhtml1 40 | extensions: [codehilite, def_list] 41 | extension-configs: 42 | codehilite: 43 | force_linenos: true 44 | 45 | # Serving 46 | server: 47 | bind: '127.0.0.1' 48 | port: 8010 49 | num-threads: 10 50 | name: 'server.example.com' 51 | request-queue-size: 5 52 | timeout: 10 53 | 54 | Remember that Markdoc uses sensible defaults for *all* of these options, so it’s 55 | perfectly OK to have an empty markdoc.yaml file. You still need one though. 56 | 57 | * [Metadata](#metadata) 58 | * [Directories](#directories) 59 | * [Building](#building) 60 | * [Rendering](#rendering) 61 | * [Serving](#serving) 62 | 63 | ### Metadata 64 | 65 | This is information about your wiki itself. It is currently only used when 66 | rendering the default set of templates, but custom templates may also use these 67 | parameters. 68 | 69 | `wiki-name` (no default) 70 | : Specify the human-friendly name of your wiki. If defined, this will appear 71 | in the title and footer in the default Markdoc templates. 72 | 73 | `google-analytics` (no default) 74 | : Specify a [Google Analytics][] tracking key for your Markdoc site. If given, 75 | the GA tracking code will be included in every HTML document. Note that this 76 | should be the full key, in the format `UA-XXXXXX-X`. 77 | 78 | [google analytics]: http://google.com/analytics/ 79 | 80 | ### Directories 81 | 82 | These settings affect where Markdoc looks for some pieces of data, and where it 83 | puts others. You can get more information on the roles of various directories in 84 | the [layout documentation](/layout). Note that all `*-dir` parameters are 85 | resolved relative to the wiki root, and that `'.'` (i.e. the wiki root itself) 86 | is an acceptable value. 87 | 88 | `hide-prefix` (default `.`) 89 | : This determines how Markdoc finds and writes to hidden directories like 90 | `.tmp`, `.templates` and `.html`. You may wish to set this to `'_'` if your 91 | VCS or operating system doesn’t play nicely with period-prefixed filenames. 92 | If you specify `html-dir`, `temp-dir` and `template-dir`, this setting won’t 93 | have any effect. 94 | 95 | `wiki-dir` (default `"wiki"`) 96 | : This tells Markdoc where to find pages that should be rendered with Markdown 97 | and output as HTML. Only files in this directory will be rendered. 98 | 99 | `static-dir` (default `"static"`) 100 | : Any files in the static directory are synced over to the HTML root as-is 101 | when building. This tells Markdoc where to find all the static media for 102 | your wiki. 103 | 104 | `html-dir` (default `hide-prefix + "html"`) 105 | : This is where HTML and static media are eventually output during the 106 | building process. It is also the document root for the Markdoc server. The 107 | default value is auto-generated using the `hide-prefix` setting. 108 | 109 | `template-dir` (default `hide-prefix + "templates"`) 110 | : Markdoc will look in this directory first when searching for the Jinja2 111 | templates needed to produce HTML. 112 | 113 | `temp-dir` (default `hide-prefix + "tmp"`) 114 | : This directory is used as a temporary destination when building HTML. 115 | 116 | `cvs-exclude` (default `true`) 117 | : If this is `true`, Markdoc will pass the `--cvs-exclude` option to `rsync` 118 | when syncing static media and rendered HTML files. This causes `rsync` to 119 | skip some common backup/hidden files (e.g. `.git/`, `.svn/`, `*~` and `#*`). 120 | The full semantics of this option are specified in the 121 | [`rsync` documentation][rsync-docs]. 122 | 123 | [rsync-docs]: http://www.samba.org/ftp/rsync/rsync.html 124 | 125 | ### Building 126 | 127 | These settings affect Markdoc’s behavior during the build process. 128 | 129 | `document-extensions` (default `[.md, .mdown, .markdown, .wiki, .text]`) 130 | : Markdoc will only render files from the document root which have one of 131 | these extensions. If one of the extensions is an empty string (`''`), then 132 | all files (including those without an extension) will be considered pages. 133 | Setting this parameter to the empty list (`[]`) will behave as if it is 134 | actually `['']`. 135 | 136 | `generate-listing` (default `always`) 137 | : This affects how listings are generated for directories in your Markdoc wiki 138 | (including the top level). Set this to either `always`, `sometimes` or 139 | `never`. The semantics are as follows: 140 | 141 | * `never` never generates any listings. 142 | * `sometimes` only generates a listing when there is no `index` or 143 | `index.html` file in a directory. This listing is saved as both 144 | `index.html` and the value of the `listing-filename` setting. 145 | * `always` generates a listing for every directory, and saves it under the 146 | value of the `listing-filename` setting, and as `index.html` if an index 147 | file does not already exist. 148 | 149 | `listing-filename` (default `_list.html`) 150 | : This specifies the filename that directory listings are saved under; see the 151 | documentation for `generate-listing` just above for more information. 152 | 153 | `use-default-static` (default `true`) 154 | : If true, Markdoc’s default set of static media will be synchronized to the 155 | HTML root when building. 156 | 157 | `use-default-templates` (default `true`) 158 | : If true, Jinja2 will look in Markdoc’s set of default templates when 159 | rendering documents and listings. 160 | 161 | ### Rendering 162 | 163 | These settings determine how Markdoc converts Markdown text into XHTML. They are 164 | all defined as sub-parameters inside the `markdown` dictionary. These parameters 165 | correspond to keyword arguments to the `markdown.Markdown` constructor, although 166 | hyphens (`-`) are all converted to underscores (`_`) in the key strings. 167 | 168 | `extensions` (default `[]`) 169 | : A list of strings specifying extensions to be used by the Markdown renderer. 170 | The [Markdown library for Python][markdown-python-lib] comes with the 171 | following by default: 172 | 173 | * `abbr` 174 | * `codehilite` 175 | * `def_list` 176 | * `extra` 177 | * `fenced_code` 178 | * `footnotes` 179 | * `headerid` 180 | * `html_tidy` 181 | * `imagelinks` 182 | * `meta` 183 | * `rss` 184 | * `tables` 185 | * `toc` 186 | * `wikilinks` 187 | 188 | [markdown-python-lib]: http://www.freewisdom.org/projects/python-markdown 189 | 190 | `extension-configs` (default `{}`) 191 | : These are configuration parameters for the extensions — you’ll need to 192 | consult the official Markdown library documentation for more information. 193 | 194 | `safe-mode` (default `false`) 195 | : Disallow raw HTML in Markdown documents. This can be either `false`, 196 | `remove`, `replace` or `escape`. 197 | 198 | `output-format` (default `xhtml1`) 199 | : Switch between rendering XHTML or HTML. Can be either `xhtml1`, `xhtml`, 200 | `html4` or `html` (the general ones will always refer to the latest 201 | version). It is strongly suggested that you use XHTML. 202 | 203 | ### Serving 204 | 205 | All of the server configuration parameters exist in the `server` dictionary (as 206 | with `markdown` previously). 207 | 208 | `bind` (default `127.0.0.1`) 209 | : Bind to the specified interface. With the default value the server will only 210 | listen on the loopback interface (localhost). 211 | 212 | `port` (default `8008`) 213 | : Listen on the specified port. 214 | 215 | `num-threads` (default `10`) 216 | : Use this number of threads to handle requests. 217 | 218 | `name` (default is autodetected) 219 | : Specify a server name. The default will be automatically detected from the 220 | socket the server binds to. 221 | 222 | `request-queue-size` (default `5`) 223 | : Sets the number of queued connections to the server before it will start 224 | dropping TCP connections (the `backlog` argument to `socket.listen()`). 225 | 226 | `timeout` (default `10`) 227 | : The socket timeout (in seconds) for accepted TCP connections. 228 | -------------------------------------------------------------------------------- /src/markdoc/builder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import os.path as p 5 | import operator 6 | import re 7 | 8 | from markdoc.cache import DocumentCache, RenderCache, read_from 9 | from markdoc.config import Config 10 | from markdoc.render import make_relative 11 | 12 | 13 | Config.register_default('listing-filename', '_list.html') 14 | 15 | 16 | class Builder(object): 17 | 18 | """An object to handle all the parts of the wiki building process.""" 19 | 20 | def __init__(self, config): 21 | self.config = config 22 | 23 | self.doc_cache = DocumentCache(base=self.config.wiki_dir) 24 | 25 | def render_func(path, doc): 26 | level = len(path.lstrip('/').split('/')) - 1 27 | return self.config.markdown(curr_path=path).convert(doc) 28 | self.render_cache = RenderCache(render_func, self.doc_cache) 29 | 30 | render_doc_func = lambda path, doc: self.render_document(path, cache=False) 31 | self.document_render_cache = RenderCache(render_doc_func, self.render_cache) 32 | 33 | def crumbs(self, path): 34 | 35 | """ 36 | Produce a breadcrumbs list for the given filename. 37 | 38 | The crumbs are calculated based on the wiki root and the absolute path 39 | to the current file. 40 | 41 | Examples 42 | -------- 43 | 44 | Assuming a wiki root of `/a/b/c`: 45 | 46 | * `a/b/c/wiki/index.md` => `[('index', None)]` 47 | 48 | * `a/b/c/wiki/subdir/index.md` => 49 | `[('index', '/'), ('subdir', None)]` 50 | 51 | * `a/b/c/wiki/subdir/file.md` => 52 | `[('index', '/'), ('subdir', '/subdir/'), ('file', None)] 53 | 54 | """ 55 | 56 | if p.isabs(path): 57 | path = self.doc_cache.relative(path) 58 | 59 | rel_components = path.split(p.sep) 60 | terminus = p.splitext(rel_components.pop())[0] 61 | 62 | if not rel_components: 63 | if terminus == 'index': 64 | return [('index', None)] 65 | return [('index', '/'), (terminus, None)] 66 | elif terminus == 'index': 67 | terminus = p.splitext(rel_components.pop())[0] 68 | 69 | crumbs = [('index', '/')] 70 | for component in rel_components: 71 | path = '%s%s/' % (crumbs[-1][1], component) 72 | crumbs.append((component, path)) 73 | 74 | crumbs.append((terminus, None)) 75 | return crumbs 76 | 77 | def walk(self): 78 | 79 | """ 80 | Walk through the wiki, yielding info for each document. 81 | 82 | For each document encountered, a `(filename, crumbs)` tuple will be 83 | yielded. 84 | """ 85 | 86 | if not self.config['document-extensions']: 87 | self.config['document-extensions'].append('') 88 | 89 | def valid_extension(filename): 90 | return any(filename.endswith(valid_ext) 91 | for valid_ext in self.config['document-extensions']) 92 | 93 | for dirpath, subdirs, files in os.walk(self.config.wiki_dir): 94 | remove_hidden(subdirs); subdirs.sort() 95 | remove_hidden(files); files.sort() 96 | 97 | for filename in filter(valid_extension, files): 98 | full_filename = p.join(dirpath, filename) 99 | yield p.relpath(full_filename, start=self.config.wiki_dir) 100 | 101 | def listing_context(self, directory): 102 | 103 | """ 104 | Generate the template context for a directory listing. 105 | 106 | This method accepts a relative path, with the base assumed to be the 107 | HTML root. This means listings must be generated after the wiki is 108 | built, allowing them to list static media too. 109 | 110 | Directories should always be '/'-delimited when specified, since it is 111 | assumed that they are URL paths, not filesystem paths. 112 | 113 | For information on what the produced context will look like, consult the 114 | `listing` doctest. 115 | """ 116 | 117 | # Ensure the directory name ends with '/'. 118 | directory = directory.strip('/') 119 | 120 | # Resolve to filesystem paths. 121 | fs_rel_dir = p.sep.join(directory.split('/')) 122 | fs_abs_dir = p.join(self.config.html_dir, fs_rel_dir) 123 | skip_files = set([self.config['listing-filename'], 'index.html']) 124 | 125 | sub_directories, pages, files = [], [], [] 126 | for basename in os.listdir(fs_abs_dir): 127 | fs_abs_path = p.join(fs_abs_dir, basename) 128 | file_dict = { 129 | 'basename': basename, 130 | 'href': directory + '/' + basename} 131 | if not file_dict['href'].startswith('/'): 132 | file_dict['href'] = '/' + file_dict['href'] 133 | 134 | if p.isdir(fs_abs_path): 135 | file_dict['href'] += '/' 136 | sub_directories.append(file_dict) 137 | 138 | else: 139 | if (basename in skip_files or basename.startswith('.') or 140 | basename.startswith('_')): 141 | continue 142 | 143 | file_dict['slug'] = p.splitext(basename)[0] 144 | file_dict['size'] = p.getsize(fs_abs_path) 145 | file_dict['humansize'] = humansize(file_dict['size']) 146 | 147 | if p.splitext(basename)[1] == (p.extsep + 'html'): 148 | # Get the title from the file. 149 | contents = read_from(fs_abs_path) 150 | file_dict['title'] = get_title(file_dict['slug'], contents) 151 | # Remove .html from the end of the href. 152 | file_dict['href'] = p.splitext(file_dict['href'])[0] 153 | pages.append(file_dict) 154 | else: 155 | files.append(file_dict) 156 | 157 | sub_directories.sort(key=lambda directory: directory['basename']) 158 | pages.sort(key=lambda page: page['title']) 159 | files.sort(key=lambda file_: file_['basename']) 160 | 161 | return { 162 | 'directory': directory, 163 | 'sub_directories': sub_directories, 164 | 'pages': pages, 165 | 'files': files, 166 | 'make_relative': lambda href: make_relative(directory, href), 167 | } 168 | 169 | def render(self, path, cache=True): 170 | return self.render_cache.render(path, cache=cache) 171 | 172 | def title(self, path, cache=True): 173 | return get_title(path, self.render(path, cache=cache)) 174 | 175 | def render_document(self, path, cache=True): 176 | if cache: 177 | return self.document_render_cache.render(path) 178 | 179 | context = {} 180 | context['content'] = self.render(path) 181 | context['title'] = self.title(path) 182 | context['crumbs'] = self.crumbs(path) 183 | context['make_relative'] = lambda href: make_relative(path, href) 184 | 185 | template = self.config.template_env.get_template('document.html') 186 | return template.render(context) 187 | 188 | def render_listing(self, path): 189 | import jinja2 190 | 191 | context = self.listing_context(path) 192 | 193 | crumbs = [('index', '/')] 194 | if path not in ['', '/']: 195 | current_dir = '' 196 | for component in path.strip('/').split('/'): 197 | crumbs.append((component, '%s/%s/' % (current_dir, component))) 198 | current_dir += '/' + component 199 | crumbs.append((jinja2.Markup('list'), None)) 200 | 201 | context['crumbs'] = crumbs 202 | context['make_relative'] = lambda href: make_relative(path + '/', href) 203 | 204 | template = self.config.template_env.get_template('listing.html') 205 | return template.render(context) 206 | 207 | 208 | def remove_hidden(names): 209 | """Remove (in-place) all strings starting with a '.' in the given list.""" 210 | 211 | i = 0 212 | while i < len(names): 213 | if names[i].startswith('.'): 214 | names.pop(i) 215 | else: 216 | i += 1 217 | return names 218 | 219 | 220 | def get_title(filename, data): 221 | """Try to retrieve a title from a filename and its contents.""" 222 | 223 | match = re.search(r'', data, re.IGNORECASE) 224 | if match: 225 | return match.group(1).strip() 226 | 227 | match = re.search(r']*>([^<]+)

', data, re.IGNORECASE) 228 | if match: 229 | return match.group(1) 230 | 231 | name, extension = p.splitext(p.basename(filename)) 232 | return re.sub(r'[-_]+', ' ', name).title() 233 | 234 | 235 | def humansize(size, base=1024): 236 | import decimal 237 | import math 238 | 239 | if size == 0: 240 | return '0B' 241 | 242 | i = int(math.log(size, base)) 243 | prefix = 'BKMGTPEZY'[i] 244 | number = decimal.Decimal(size) / (base ** i) 245 | return str(number.to_integral()) + prefix 246 | -------------------------------------------------------------------------------- /src/markdoc/cli/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import codecs 4 | from functools import wraps 5 | import logging 6 | import os 7 | import os.path as p 8 | import pprint 9 | import re 10 | import shutil 11 | import subprocess 12 | import sys 13 | 14 | import markdoc 15 | from markdoc.builder import Builder 16 | from markdoc.cli.parser import subparsers 17 | 18 | 19 | def command(function): 20 | """Decorator/wrapper to declare a function as a Markdoc CLI task.""" 21 | 22 | cmd_name = function.__name__.replace('_', '-') 23 | help = (function.__doc__ or '').rstrip('.') or None 24 | parser = subparsers.add_parser(cmd_name, help=help) 25 | 26 | @wraps(function) 27 | def wrapper(config, args): 28 | logging.getLogger('markdoc').debug('Running markdoc.%s' % cmd_name) 29 | return function(config, args) 30 | wrapper.parser = parser 31 | 32 | return wrapper 33 | 34 | 35 | ## Utilities 36 | 37 | @command 38 | def show_config(config, args): 39 | """Pretty-print the current Markdoc configuration.""" 40 | 41 | pprint.pprint(config) 42 | 43 | 44 | @command 45 | def init(_, args): 46 | """Initialize a new Markdoc repository.""" 47 | 48 | log = logging.getLogger('markdoc.init') 49 | 50 | if not args.destination: 51 | log.info('No destination specified; using current directory') 52 | destination = os.getcwd() 53 | else: 54 | destination = p.abspath(args.destination) 55 | 56 | if p.exists(destination) and os.listdir(destination): 57 | init.parser.error("destination isn't empty") 58 | elif not p.exists(destination): 59 | log.debug('makedirs %s' % destination) 60 | os.makedirs(destination) 61 | elif not p.isdir(destination): 62 | init.parser.error("destination isn't a directory") 63 | 64 | log.debug('mkdir %s/.templates/' % destination) 65 | os.makedirs(p.join(destination, '.templates')) 66 | log.debug('mkdir %s/static/' % destination) 67 | os.makedirs(p.join(destination, 'static')) 68 | log.debug('mkdir %s/wiki/' % destination) 69 | os.makedirs(p.join(destination, 'wiki')) 70 | 71 | log.debug('Creating markdoc.yaml file') 72 | config_filename = p.join(destination, 'markdoc.yaml') 73 | fp = open(config_filename, 'w') 74 | try: 75 | fp.write('{}\n') 76 | finally: 77 | fp.close() 78 | 79 | if args.vcs_ignore: 80 | config = markdoc.config.Config.for_directory(destination) 81 | args = vcs_ignore.parser.parse_args([args.vcs_ignore]) 82 | vcs_ignore(config, args) 83 | 84 | log.info('Wiki initialization complete') 85 | log.info('Your new wiki is at: %s' % destination) 86 | 87 | init.parser.add_argument('destination', default=None, 88 | help="Create wiki here (if omitted, defaults to current directory)") 89 | init.parser.add_argument('--vcs-ignore', choices=['hg', 'git', 'cvs', 'bzr'], 90 | help="Create an ignore file for the specified VCS.") 91 | 92 | 93 | @command 94 | def vcs_ignore(config, args): 95 | """Create a VCS ignore file for a wiki.""" 96 | 97 | log = logging.getLogger('markdoc.vcs-ignore') 98 | log.debug('Creating ignore file for %s' % args.vcs) 99 | wiki_root = config['meta.root'] # shorter local alias. 100 | 101 | ignore_file_lines = [] 102 | ignore_file_lines.append(p.relpath(config.html_dir, start=wiki_root)) 103 | ignore_file_lines.append(p.relpath(config.temp_dir, start=wiki_root)) 104 | if args.vcs == 'hg': 105 | ignore_file_lines.insert(0, 'syntax: glob') 106 | ignore_file_lines.insert(1, '') 107 | 108 | if args.output == '-': 109 | log.debug('Writing ignore file to stdout') 110 | fp = sys.stdout 111 | else: 112 | if not args.output: 113 | filename = p.join(wiki_root, '.%signore' % args.vcs) 114 | else: 115 | filename = p.join(wiki_root, args.output) 116 | log.info('Writing ignore file to %s' % p.relpath(filename, start=wiki_root)) 117 | fp = open(filename, 'w') 118 | 119 | try: 120 | fp.write('\n'.join(ignore_file_lines) + '\n') 121 | finally: 122 | if fp is not sys.stdout: 123 | fp.close() 124 | 125 | log.debug('Ignore file written.') 126 | 127 | vcs_ignore.parser.add_argument('vcs', default='hg', nargs='?', 128 | choices=['hg', 'git', 'cvs', 'bzr'], 129 | help="Create ignore file for specified VCS (default 'hg')") 130 | vcs_ignore.parser.add_argument('-o', '--output', default=None, metavar='FILENAME', 131 | help="Write output to the specified filename, relative to the wiki root. " 132 | "Default is to generate the filename from the VCS. " 133 | "'-' will write to stdout.") 134 | 135 | 136 | ## Cleanup 137 | 138 | @command 139 | def clean_html(config, args): 140 | """Clean built HTML from the HTML root.""" 141 | 142 | log = logging.getLogger('markdoc.clean-html') 143 | 144 | if p.exists(config.html_dir): 145 | log.debug('rm -Rf %s' % config.html_dir) 146 | shutil.rmtree(config.html_dir) 147 | 148 | log.debug('makedirs %s' % config.html_dir) 149 | os.makedirs(config.html_dir) 150 | 151 | 152 | @command 153 | def clean_temp(config, args): 154 | """Clean built HTML from the temporary directory.""" 155 | 156 | log = logging.getLogger('markdoc.clean-temp') 157 | 158 | if p.exists(config.temp_dir): 159 | log.debug('rm -Rf %s' % config.temp_dir) 160 | shutil.rmtree(config.temp_dir) 161 | 162 | log.debug('makedirs %s' % config.temp_dir) 163 | os.makedirs(config.temp_dir) 164 | 165 | 166 | ## Synchronization 167 | 168 | @command 169 | def sync_static(config, args): 170 | """Sync static files into the HTML root.""" 171 | 172 | log = logging.getLogger('markdoc.sync-static') 173 | 174 | if not p.exists(config.html_dir): 175 | log.debug('makedirs %s' % config.html_dir) 176 | os.makedirs(config.html_dir) 177 | 178 | command = ('rsync -vaxq --cvs-exclude --ignore-errors --include=.htaccess --exclude=.* --exclude=_*').split() 179 | display_cmd = command[:] 180 | 181 | if config['use-default-static']: 182 | # rsync needs the paths to have trailing slashes to work correctly. 183 | command.append(p.join(markdoc.default_static_dir, '')) 184 | display_cmd.append(p.basename(markdoc.default_static_dir) + '/') 185 | 186 | if not config['cvs-exclude']: 187 | command.remove('--cvs-exclude') 188 | display_cmd.remove('--cvs-exclude') 189 | 190 | if p.isdir(config.static_dir): 191 | command.append(p.join(config.static_dir, '')) 192 | display_cmd.append(p.basename(config.static_dir) + '/') 193 | 194 | command.append(p.join(config.html_dir, '')) 195 | display_cmd.append(p.basename(config.html_dir) + '/') 196 | 197 | log.debug(subprocess.list2cmdline(display_cmd)) 198 | 199 | subprocess.check_call(command) 200 | 201 | log.debug('rsync completed') 202 | 203 | 204 | @command 205 | def sync_html(config, args): 206 | """Sync built HTML and static media into the HTML root.""" 207 | 208 | log = logging.getLogger('markdoc.sync-html') 209 | 210 | if not p.exists(config.html_dir): 211 | log.debug('makedirs %s' % config.html_dir) 212 | os.makedirs(config.html_dir) 213 | 214 | command = ('rsync -vaxq --cvs-exclude --delete --ignore-errors --include=.htaccess --exclude=.* --exclude=_*').split() 215 | display_cmd = command[:] 216 | 217 | # rsync needs the paths to have trailing slashes to work correctly. 218 | command.append(p.join(config.temp_dir, '')) 219 | display_cmd.append(p.basename(config.temp_dir) + '/') 220 | 221 | if config['use-default-static']: 222 | command.append(p.join(markdoc.default_static_dir, '')) 223 | display_cmd.append(p.basename(markdoc.default_static_dir) + '/') 224 | 225 | if not config['cvs-exclude']: 226 | command.remove('--cvs-exclude') 227 | display_cmd.remove('--cvs-exclude') 228 | 229 | if p.isdir(config.static_dir): 230 | command.append(p.join(config.static_dir, '')) 231 | display_cmd.append(p.basename(config.static_dir) + '/') 232 | 233 | command.append(p.join(config.html_dir, '')) 234 | display_cmd.append(p.basename(config.html_dir) + '/') 235 | 236 | log.debug(subprocess.list2cmdline(display_cmd)) 237 | 238 | subprocess.check_call(command) 239 | 240 | log.debug('rsync completed') 241 | 242 | 243 | ## Building 244 | 245 | @command 246 | def build(config, args): 247 | """Compile wiki to HTML and sync to the HTML root.""" 248 | 249 | log = logging.getLogger('markdoc.build') 250 | 251 | clean_temp(config, args) 252 | 253 | builder = Builder(config) 254 | for rel_filename in builder.walk(): 255 | html = builder.render_document(rel_filename) 256 | out_filename = p.join(config.temp_dir, 257 | p.splitext(rel_filename)[0] + p.extsep + 'html') 258 | 259 | if not p.exists(p.dirname(out_filename)): 260 | log.debug('makedirs %s' % p.dirname(out_filename)) 261 | os.makedirs(p.dirname(out_filename)) 262 | 263 | log.debug('Creating %s' % p.relpath(out_filename, start=config.temp_dir)) 264 | fp = codecs.open(out_filename, 'w', encoding='utf-8') 265 | try: 266 | fp.write(html) 267 | finally: 268 | fp.close() 269 | 270 | sync_html(config, args) 271 | build_listing(config, args) 272 | 273 | 274 | @command 275 | def build_listing(config, args): 276 | """Create listings for all directories in the HTML root (post-build).""" 277 | 278 | log = logging.getLogger('markdoc.build-listing') 279 | 280 | list_basename = config['listing-filename'] 281 | builder = Builder(config) 282 | generate_listing = config.get('generate-listing', 'always').lower() 283 | always_list = True 284 | if generate_listing == 'never': 285 | log.debug("No listing generated (generate-listing == never)") 286 | return # No need to continue. 287 | 288 | for fs_dir, _, _ in os.walk(config.html_dir): 289 | index_file_exists = any([ 290 | p.exists(p.join(fs_dir, 'index.html')), 291 | p.exists(p.join(fs_dir, 'index'))]) 292 | 293 | directory = '/' + '/'.join(p.relpath(fs_dir, start=config.html_dir).split(p.sep)) 294 | if directory == '/' + p.curdir: 295 | directory = '/' 296 | 297 | if (generate_listing == 'sometimes') and index_file_exists: 298 | log.debug("No listing generated for %s" % directory) 299 | continue 300 | 301 | log.debug("Generating listing for %s" % directory) 302 | listing = builder.render_listing(directory) 303 | list_filename = p.join(fs_dir, list_basename) 304 | 305 | fp = codecs.open(list_filename, 'w', encoding='utf-8') 306 | try: 307 | fp.write(listing) 308 | finally: 309 | fp.close() 310 | 311 | if not index_file_exists: 312 | log.debug("cp %s/%s %s/%s" % (directory, list_basename, directory, 'index.html')) 313 | shutil.copyfile(list_filename, p.join(fs_dir, 'index.html')) 314 | 315 | 316 | ## Serving 317 | 318 | IPV4_RE = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') 319 | 320 | @command 321 | def serve(config, args): 322 | """Serve the built HTML from the HTML root.""" 323 | 324 | # This should be a lazy import, otherwise it'll slow down the whole CLI. 325 | from markdoc.wsgi import MarkdocWSGIApplication 326 | 327 | log = logging.getLogger('markdoc.serve') 328 | app = MarkdocWSGIApplication(config) 329 | 330 | config['server.port'] = args.port 331 | config['server.num-threads'] = args.num_threads 332 | if args.server_name: 333 | config['server.name'] = args.server_name 334 | config['server.request-queue-size'] = args.queue_size 335 | config['server.timeout'] = args.timeout 336 | if args.interface: 337 | if not IPV4_RE.match(args.interface): 338 | serve.parser.error('invalid interface specifier: %r' % args.interface) 339 | config['server.bind'] = args.interface 340 | 341 | server = config.server_maker()(app) 342 | 343 | try: 344 | log.info('Serving on http://%s:%d' % server.bind_addr) 345 | server.start() 346 | except KeyboardInterrupt: 347 | log.debug('Interrupted') 348 | finally: 349 | log.info('Shutting down gracefully') 350 | server.stop() 351 | 352 | serve.parser.add_argument('-p', '--port', type=int, default=8008, 353 | help="Listen on specified port (default is 8008)") 354 | serve.parser.add_argument('-i', '--interface', default=None, 355 | help="Bind to specified interface (defaults to loopback only)") 356 | serve.parser.add_argument('-t', '--num-threads', type=int, default=10, metavar='N', 357 | help="Use N threads to handle requests (default is 10)") 358 | serve.parser.add_argument('-n', '--server-name', default=None, metavar='NAME', 359 | help="Use an explicit server name (default to an autodetected value)") 360 | serve.parser.add_argument('-q', '--queue-size', type=int, default=5, metavar='SIZE', 361 | help="Set request queue size (default is 5)") 362 | serve.parser.add_argument('--timeout', type=int, default=10, 363 | help="Set the socket timeout for connections (default is 10)") 364 | 365 | -------------------------------------------------------------------------------- /distribute_setup.py: -------------------------------------------------------------------------------- 1 | #!python 2 | """Bootstrap distribute installation 3 | 4 | If you want to use setuptools in your package's setup.py, just include this 5 | file in the same directory with it, and add this to the top of your setup.py:: 6 | 7 | from distribute_setup import use_setuptools 8 | use_setuptools() 9 | 10 | If you want to require a specific version of setuptools, set a download 11 | mirror, or use an alternate download directory, you can do so by supplying 12 | the appropriate options to ``use_setuptools()``. 13 | 14 | This file can also be run as a script to install or upgrade setuptools. 15 | """ 16 | import os 17 | import sys 18 | import time 19 | import fnmatch 20 | import tempfile 21 | import tarfile 22 | from distutils import log 23 | 24 | try: 25 | from site import USER_SITE 26 | except ImportError: 27 | USER_SITE = None 28 | 29 | try: 30 | import subprocess 31 | 32 | def _python_cmd(*args): 33 | args = (sys.executable,) + args 34 | return subprocess.call(args) == 0 35 | 36 | except ImportError: 37 | # will be used for python 2.3 38 | def _python_cmd(*args): 39 | args = (sys.executable,) + args 40 | # quoting arguments if windows 41 | if sys.platform == 'win32': 42 | def quote(arg): 43 | if ' ' in arg: 44 | return '"%s"' % arg 45 | return arg 46 | args = [quote(arg) for arg in args] 47 | return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 48 | 49 | DEFAULT_VERSION = "0.6.10" 50 | DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" 51 | SETUPTOOLS_FAKED_VERSION = "0.6c11" 52 | 53 | SETUPTOOLS_PKG_INFO = """\ 54 | Metadata-Version: 1.0 55 | Name: setuptools 56 | Version: %s 57 | Summary: xxxx 58 | Home-page: xxx 59 | Author: xxx 60 | Author-email: xxx 61 | License: xxx 62 | Description: xxx 63 | """ % SETUPTOOLS_FAKED_VERSION 64 | 65 | 66 | def _install(tarball): 67 | # extracting the tarball 68 | tmpdir = tempfile.mkdtemp() 69 | log.warn('Extracting in %s', tmpdir) 70 | old_wd = os.getcwd() 71 | try: 72 | os.chdir(tmpdir) 73 | tar = tarfile.open(tarball) 74 | _extractall(tar) 75 | tar.close() 76 | 77 | # going in the directory 78 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 79 | os.chdir(subdir) 80 | log.warn('Now working in %s', subdir) 81 | 82 | # installing 83 | log.warn('Installing Distribute') 84 | if not _python_cmd('setup.py', 'install'): 85 | log.warn('Something went wrong during the installation.') 86 | log.warn('See the error message above.') 87 | finally: 88 | os.chdir(old_wd) 89 | 90 | 91 | def _build_egg(egg, tarball, to_dir): 92 | # extracting the tarball 93 | tmpdir = tempfile.mkdtemp() 94 | log.warn('Extracting in %s', tmpdir) 95 | old_wd = os.getcwd() 96 | try: 97 | os.chdir(tmpdir) 98 | tar = tarfile.open(tarball) 99 | _extractall(tar) 100 | tar.close() 101 | 102 | # going in the directory 103 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) 104 | os.chdir(subdir) 105 | log.warn('Now working in %s', subdir) 106 | 107 | # building an egg 108 | log.warn('Building a Distribute egg in %s', to_dir) 109 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) 110 | 111 | finally: 112 | os.chdir(old_wd) 113 | # returning the result 114 | log.warn(egg) 115 | if not os.path.exists(egg): 116 | raise IOError('Could not build the egg.') 117 | 118 | 119 | def _do_download(version, download_base, to_dir, download_delay): 120 | egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' 121 | % (version, sys.version_info[0], sys.version_info[1])) 122 | if not os.path.exists(egg): 123 | tarball = download_setuptools(version, download_base, 124 | to_dir, download_delay) 125 | _build_egg(egg, tarball, to_dir) 126 | sys.path.insert(0, egg) 127 | import setuptools 128 | setuptools.bootstrap_install_from = egg 129 | 130 | 131 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 132 | to_dir=os.curdir, download_delay=15, no_fake=True): 133 | # making sure we use the absolute path 134 | to_dir = os.path.abspath(to_dir) 135 | was_imported = 'pkg_resources' in sys.modules or \ 136 | 'setuptools' in sys.modules 137 | try: 138 | try: 139 | import pkg_resources 140 | if not hasattr(pkg_resources, '_distribute'): 141 | if not no_fake: 142 | _fake_setuptools() 143 | raise ImportError 144 | except ImportError: 145 | return _do_download(version, download_base, to_dir, download_delay) 146 | try: 147 | pkg_resources.require("distribute>="+version) 148 | return 149 | except pkg_resources.VersionConflict: 150 | e = sys.exc_info()[1] 151 | if was_imported: 152 | sys.stderr.write( 153 | "The required version of distribute (>=%s) is not available,\n" 154 | "and can't be installed while this script is running. Please\n" 155 | "install a more recent version first, using\n" 156 | "'easy_install -U distribute'." 157 | "\n\n(Currently using %r)\n" % (version, e.args[0])) 158 | sys.exit(2) 159 | else: 160 | del pkg_resources, sys.modules['pkg_resources'] # reload ok 161 | return _do_download(version, download_base, to_dir, 162 | download_delay) 163 | except pkg_resources.DistributionNotFound: 164 | return _do_download(version, download_base, to_dir, 165 | download_delay) 166 | finally: 167 | if not no_fake: 168 | _create_fake_setuptools_pkg_info(to_dir) 169 | 170 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, 171 | to_dir=os.curdir, delay=15): 172 | """Download distribute from a specified location and return its filename 173 | 174 | `version` should be a valid distribute version number that is available 175 | as an egg for download under the `download_base` URL (which should end 176 | with a '/'). `to_dir` is the directory where the egg will be downloaded. 177 | `delay` is the number of seconds to pause before an actual download 178 | attempt. 179 | """ 180 | # making sure we use the absolute path 181 | to_dir = os.path.abspath(to_dir) 182 | try: 183 | from urllib.request import urlopen 184 | except ImportError: 185 | from urllib2 import urlopen 186 | tgz_name = "distribute-%s.tar.gz" % version 187 | url = download_base + tgz_name 188 | saveto = os.path.join(to_dir, tgz_name) 189 | src = dst = None 190 | if not os.path.exists(saveto): # Avoid repeated downloads 191 | try: 192 | log.warn("Downloading %s", url) 193 | src = urlopen(url) 194 | # Read/write all in one block, so we don't create a corrupt file 195 | # if the download is interrupted. 196 | data = src.read() 197 | dst = open(saveto, "wb") 198 | dst.write(data) 199 | finally: 200 | if src: 201 | src.close() 202 | if dst: 203 | dst.close() 204 | return os.path.realpath(saveto) 205 | 206 | 207 | def _patch_file(path, content): 208 | """Will backup the file then patch it""" 209 | existing_content = open(path).read() 210 | if existing_content == content: 211 | # already patched 212 | log.warn('Already patched.') 213 | return False 214 | log.warn('Patching...') 215 | _rename_path(path) 216 | f = open(path, 'w') 217 | try: 218 | f.write(content) 219 | finally: 220 | f.close() 221 | return True 222 | 223 | 224 | def _same_content(path, content): 225 | return open(path).read() == content 226 | 227 | def _no_sandbox(function): 228 | def __no_sandbox(*args, **kw): 229 | try: 230 | from setuptools.sandbox import DirectorySandbox 231 | def violation(*args): 232 | pass 233 | DirectorySandbox._old = DirectorySandbox._violation 234 | DirectorySandbox._violation = violation 235 | patched = True 236 | except ImportError: 237 | patched = False 238 | 239 | try: 240 | return function(*args, **kw) 241 | finally: 242 | if patched: 243 | DirectorySandbox._violation = DirectorySandbox._old 244 | del DirectorySandbox._old 245 | 246 | return __no_sandbox 247 | 248 | @_no_sandbox 249 | def _rename_path(path): 250 | new_name = path + '.OLD.%s' % time.time() 251 | log.warn('Renaming %s into %s', path, new_name) 252 | os.rename(path, new_name) 253 | return new_name 254 | 255 | def _remove_flat_installation(placeholder): 256 | if not os.path.isdir(placeholder): 257 | log.warn('Unkown installation at %s', placeholder) 258 | return False 259 | found = False 260 | for file in os.listdir(placeholder): 261 | if fnmatch.fnmatch(file, 'setuptools*.egg-info'): 262 | found = True 263 | break 264 | if not found: 265 | log.warn('Could not locate setuptools*.egg-info') 266 | return 267 | 268 | log.warn('Removing elements out of the way...') 269 | pkg_info = os.path.join(placeholder, file) 270 | if os.path.isdir(pkg_info): 271 | patched = _patch_egg_dir(pkg_info) 272 | else: 273 | patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) 274 | 275 | if not patched: 276 | log.warn('%s already patched.', pkg_info) 277 | return False 278 | # now let's move the files out of the way 279 | for element in ('setuptools', 'pkg_resources.py', 'site.py'): 280 | element = os.path.join(placeholder, element) 281 | if os.path.exists(element): 282 | _rename_path(element) 283 | else: 284 | log.warn('Could not find the %s element of the ' 285 | 'Setuptools distribution', element) 286 | return True 287 | 288 | 289 | def _after_install(dist): 290 | log.warn('After install bootstrap.') 291 | placeholder = dist.get_command_obj('install').install_purelib 292 | _create_fake_setuptools_pkg_info(placeholder) 293 | 294 | @_no_sandbox 295 | def _create_fake_setuptools_pkg_info(placeholder): 296 | if not placeholder or not os.path.exists(placeholder): 297 | log.warn('Could not find the install location') 298 | return 299 | pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) 300 | setuptools_file = 'setuptools-%s-py%s.egg-info' % \ 301 | (SETUPTOOLS_FAKED_VERSION, pyver) 302 | pkg_info = os.path.join(placeholder, setuptools_file) 303 | if os.path.exists(pkg_info): 304 | log.warn('%s already exists', pkg_info) 305 | return 306 | 307 | log.warn('Creating %s', pkg_info) 308 | f = open(pkg_info, 'w') 309 | try: 310 | f.write(SETUPTOOLS_PKG_INFO) 311 | finally: 312 | f.close() 313 | 314 | pth_file = os.path.join(placeholder, 'setuptools.pth') 315 | log.warn('Creating %s', pth_file) 316 | f = open(pth_file, 'w') 317 | try: 318 | f.write(os.path.join(os.curdir, setuptools_file)) 319 | finally: 320 | f.close() 321 | 322 | def _patch_egg_dir(path): 323 | # let's check if it's already patched 324 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 325 | if os.path.exists(pkg_info): 326 | if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): 327 | log.warn('%s already patched.', pkg_info) 328 | return False 329 | _rename_path(path) 330 | os.mkdir(path) 331 | os.mkdir(os.path.join(path, 'EGG-INFO')) 332 | pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') 333 | f = open(pkg_info, 'w') 334 | try: 335 | f.write(SETUPTOOLS_PKG_INFO) 336 | finally: 337 | f.close() 338 | return True 339 | 340 | 341 | def _before_install(): 342 | log.warn('Before install bootstrap.') 343 | _fake_setuptools() 344 | 345 | 346 | def _under_prefix(location): 347 | if 'install' not in sys.argv: 348 | return True 349 | args = sys.argv[sys.argv.index('install')+1:] 350 | for index, arg in enumerate(args): 351 | for option in ('--root', '--prefix'): 352 | if arg.startswith('%s=' % option): 353 | top_dir = arg.split('root=')[-1] 354 | return location.startswith(top_dir) 355 | elif arg == option: 356 | if len(args) > index: 357 | top_dir = args[index+1] 358 | return location.startswith(top_dir) 359 | elif option == '--user' and USER_SITE is not None: 360 | return location.startswith(USER_SITE) 361 | return True 362 | 363 | 364 | def _fake_setuptools(): 365 | log.warn('Scanning installed packages') 366 | try: 367 | import pkg_resources 368 | except ImportError: 369 | # we're cool 370 | log.warn('Setuptools or Distribute does not seem to be installed.') 371 | return 372 | ws = pkg_resources.working_set 373 | try: 374 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', 375 | replacement=False)) 376 | except TypeError: 377 | # old distribute API 378 | setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) 379 | 380 | if setuptools_dist is None: 381 | log.warn('No setuptools distribution found') 382 | return 383 | # detecting if it was already faked 384 | setuptools_location = setuptools_dist.location 385 | log.warn('Setuptools installation detected at %s', setuptools_location) 386 | 387 | # if --root or --preix was provided, and if 388 | # setuptools is not located in them, we don't patch it 389 | if not _under_prefix(setuptools_location): 390 | log.warn('Not patching, --root or --prefix is installing Distribute' 391 | ' in another location') 392 | return 393 | 394 | # let's see if its an egg 395 | if not setuptools_location.endswith('.egg'): 396 | log.warn('Non-egg installation') 397 | res = _remove_flat_installation(setuptools_location) 398 | if not res: 399 | return 400 | else: 401 | log.warn('Egg installation') 402 | pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') 403 | if (os.path.exists(pkg_info) and 404 | _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): 405 | log.warn('Already patched.') 406 | return 407 | log.warn('Patching...') 408 | # let's create a fake egg replacing setuptools one 409 | res = _patch_egg_dir(setuptools_location) 410 | if not res: 411 | return 412 | log.warn('Patched done.') 413 | _relaunch() 414 | 415 | 416 | def _relaunch(): 417 | log.warn('Relaunching...') 418 | # we have to relaunch the process 419 | args = [sys.executable] + sys.argv 420 | sys.exit(subprocess.call(args)) 421 | 422 | 423 | def _extractall(self, path=".", members=None): 424 | """Extract all members from the archive to the current working 425 | directory and set owner, modification time and permissions on 426 | directories afterwards. `path' specifies a different directory 427 | to extract to. `members' is optional and must be a subset of the 428 | list returned by getmembers(). 429 | """ 430 | import copy 431 | import operator 432 | from tarfile import ExtractError 433 | directories = [] 434 | 435 | if members is None: 436 | members = self 437 | 438 | for tarinfo in members: 439 | if tarinfo.isdir(): 440 | # Extract directories with a safe mode. 441 | directories.append(tarinfo) 442 | tarinfo = copy.copy(tarinfo) 443 | tarinfo.mode = 448 # decimal for oct 0700 444 | self.extract(tarinfo, path) 445 | 446 | # Reverse sort directories. 447 | if sys.version_info < (2, 4): 448 | def sorter(dir1, dir2): 449 | return cmp(dir1.name, dir2.name) 450 | directories.sort(sorter) 451 | directories.reverse() 452 | else: 453 | directories.sort(key=operator.attrgetter('name'), reverse=True) 454 | 455 | # Set correct owner, mtime and filemode on directories. 456 | for tarinfo in directories: 457 | dirpath = os.path.join(path, tarinfo.name) 458 | try: 459 | self.chown(tarinfo, dirpath) 460 | self.utime(tarinfo, dirpath) 461 | self.chmod(tarinfo, dirpath) 462 | except ExtractError: 463 | e = sys.exc_info()[1] 464 | if self.errorlevel > 1: 465 | raise 466 | else: 467 | self._dbg(1, "tarfile: %s" % e) 468 | 469 | 470 | def main(argv, version=DEFAULT_VERSION): 471 | """Install or upgrade setuptools and EasyInstall""" 472 | tarball = download_setuptools() 473 | _install(tarball) 474 | 475 | 476 | if __name__ == '__main__': 477 | main(sys.argv[1:]) 478 | -------------------------------------------------------------------------------- /doc/ref/markup.md: -------------------------------------------------------------------------------- 1 | # Markdown Syntax Guide 2 | 3 | Adapted from . 4 | 5 | [TOC] 6 | 7 | * * * 8 | 9 | ## Overview 10 | 11 | ### Philosophy 12 | 13 | Markdown is intended to be as easy-to-read and easy-to-write as is feasible. 14 | 15 | Readability, however, is emphasized above all else. A Markdown-formatted 16 | document should be publishable as-is, as plain text, without looking like it's 17 | been marked up with tags or formatting instructions. While Markdown's syntax has 18 | been influenced by several existing text-to-HTML filters -- including 19 | [Setext] [1], [atx] [2], [Textile] [3], [reStructuredText] [4], [Grutatext] [5], 20 | and [EtText] [6] -- the single biggest source of inspiration for Markdown's 21 | syntax is the format of plain text email. 22 | 23 | [1]: http://docutils.sourceforge.net/mirror/setext.html 24 | [2]: http://www.aaronsw.com/2002/atx/ 25 | [3]: http://textism.com/tools/textile/ 26 | [4]: http://docutils.sourceforge.net/rst.html 27 | [5]: http://www.triptico.com/software/grutatxt.html 28 | [6]: http://ettext.taint.org/doc/ 29 | 30 | To this end, Markdown's syntax is comprised entirely of punctuation characters, 31 | which punctuation characters have been carefully chosen so as to look like what 32 | they mean. E.g., asterisks around a word actually look like \*emphasis\*. 33 | Markdown lists look like, well, lists. Even blockquotes look like quoted 34 | passages of text, assuming you've ever used email. 35 | 36 | 37 | 38 | ### Inline HTML 39 | 40 | Markdown's syntax is intended for one purpose: to be used as a format for 41 | *writing* for the web. 42 | 43 | Markdown is not a replacement for HTML, or even close to it. Its syntax is very 44 | small, corresponding only to a very small subset of HTML tags. The idea is *not* 45 | to create a syntax that makes it easier to insert HTML tags. In my opinion, HTML 46 | tags are already easy to insert. The idea for Markdown is to make it easy to 47 | read, write, and edit prose. HTML is a *publishing* format; Markdown is a 48 | *writing* format. Thus, Markdown's formatting syntax only addresses issues that 49 | can be conveyed in plain text. 50 | 51 | For any markup that is not covered by Markdown's syntax, you simply use HTML 52 | itself. There's no need to preface it or delimit it to indicate that you're 53 | switching from Markdown to HTML; you just use the tags. 54 | 55 | The only restrictions are that block-level HTML elements -- e.g. `
`, 56 | ``, `
`, `

`, etc. -- must be separated from surrounding content by 57 | blank lines, and the start and end tags of the block should not be indented with 58 | tabs or spaces. Markdown is smart enough not to add extra (unwanted) `

` tags 59 | around HTML block-level tags. 60 | 61 | For example, to add an HTML table to a Markdown article: 62 | 63 | :::text 64 | This is a regular paragraph. 65 | 66 |

67 | 68 | 69 | 70 |
Foo
71 | 72 | This is another regular paragraph. 73 | 74 | Note that Markdown formatting syntax is not processed within block-level HTML 75 | tags. E.g., you can't use Markdown-style `*emphasis*` inside an HTML block. 76 | 77 | Span-level HTML tags -- e.g. ``, ``, or `` -- can be used 78 | anywhere in a Markdown paragraph, list item, or header. If you want, you can 79 | even use HTML tags instead of Markdown formatting; e.g. if you'd prefer to use 80 | HTML `` or `` tags instead of Markdown's link or image syntax, go right 81 | ahead. 82 | 83 | Unlike block-level HTML tags, Markdown syntax *is* processed within span-level 84 | tags. 85 | 86 | 87 | ### Automatic Escaping for Special Characters 88 | 89 | In HTML, there are two characters that demand special treatment: `<` and `&`. 90 | Left angle brackets are used to start tags; ampersands are used to denote HTML 91 | entities. If you want to use them as literal characters, you must escape them as 92 | entities, e.g. `<`, and `&`. 93 | 94 | Ampersands in particular are bedeviling for web writers. If you want to write 95 | about 'AT&T', you need to write '`AT&T`'. You even need to escape ampersands 96 | within URLs. Thus, if you want to link to: 97 | 98 | :::text 99 | http://images.google.com/images?num=30&q=larry+bird 100 | 101 | you need to encode the URL as: 102 | 103 | :::text 104 | http://images.google.com/images?num=30&q=larry+bird 105 | 106 | in your anchor tag `href` attribute. Needless to say, this is easy to forget, 107 | and is probably the single most common source of HTML validation errors in 108 | otherwise well-marked-up web sites. 109 | 110 | Markdown allows you to use these characters naturally, taking care of all the 111 | necessary escaping for you. If you use an ampersand as part of an HTML entity, 112 | it remains unchanged; otherwise it will be translated into `&`. 113 | 114 | So, if you want to include a copyright symbol in your article, you can write: 115 | 116 | :::text 117 | © 118 | 119 | and Markdown will leave it alone. But if you write: 120 | 121 | :::text 122 | AT&T 123 | 124 | Markdown will translate it to: 125 | 126 | :::text 127 | AT&T 128 | 129 | Similarly, because Markdown supports [inline HTML](#inline-html), if you use 130 | angle brackets as delimiters for HTML tags, Markdown will treat them as such. 131 | But if you write: 132 | 133 | :::text 134 | 4 < 5 135 | 136 | Markdown will translate it to: 137 | 138 | :::text 139 | 4 < 5 140 | 141 | However, inside Markdown code spans and blocks, angle brackets and ampersands 142 | are *always* encoded automatically. This makes it easy to use Markdown to write 143 | about HTML code. (As opposed to raw HTML, which is a terrible format for writing 144 | about HTML syntax, because every single `<` and `&` in your example code needs 145 | to be escaped.) 146 | 147 | 148 | * * * 149 | 150 | 151 | ## Block Elements 152 | 153 | 154 | ### Paragraphs and Line Breaks 155 | 156 | A paragraph is simply one or more consecutive lines of text, separated by one or 157 | more blank lines. (A blank line is any line that looks like a blank line -- a 158 | line containing nothing but spaces or tabs is considered blank.) Normal 159 | paragraphs should not be indented with spaces or tabs. 160 | 161 | The implication of the "one or more consecutive lines of text" rule is that 162 | Markdown supports "hard-wrapped" text paragraphs. This differs significantly 163 | from most other text-to-HTML formatters (including Movable Type's "Convert Line 164 | Breaks" option) which translate every line break character in a paragraph into a 165 | `
` tag. 166 | 167 | When you *do* want to insert a `
` break tag using Markdown, you end a line 168 | with two or more spaces, then type return. 169 | 170 | Yes, this takes a tad more effort to create a `
`, but a simplistic "every 171 | line break is a `
`" rule wouldn't work for Markdown. Markdown's 172 | email-style [blockquoting][bq] and multi-paragraph [list items][l] work best -- 173 | and look better -- when you format them with hard breaks. 174 | 175 | [bq]: #blockquote 176 | [l]: #list 177 | 178 | 179 | 180 | ### Headers 181 | 182 | Markdown supports two styles of headers, [Setext] [1] and [atx] [2]. 183 | 184 | Setext-style headers are "underlined" using equal signs (for first-level 185 | headers) and dashes (for second-level headers). For example: 186 | 187 | :::text 188 | This is an H1 189 | ============= 190 | 191 | This is an H2 192 | ------------- 193 | 194 | Any number of underlining `=`'s or `-`'s will work. 195 | 196 | Atx-style headers use 1-6 hash characters at the start of the line, 197 | corresponding to header levels 1-6. For example: 198 | 199 | :::text 200 | # This is an H1 201 | 202 | ## This is an H2 203 | 204 | ###### This is an H6 205 | 206 | Optionally, you may "close" atx-style headers. This is purely cosmetic -- you 207 | can use this if you think it looks better. The closing hashes don't even need to 208 | match the number of hashes used to open the header. (The number of opening 209 | hashes determines the header level.) : 210 | 211 | :::text 212 | # This is an H1 # 213 | 214 | ## This is an H2 ## 215 | 216 | ### This is an H3 ###### 217 | 218 | 219 | ### Blockquotes 220 | 221 | Markdown uses email-style `>` characters for blockquoting. If you're familiar 222 | with quoting passages of text in an email message, then you know how to create a 223 | blockquote in Markdown. It looks best if you hard wrap the text and put a `>` 224 | before every line: 225 | 226 | :::text 227 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, 228 | > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. 229 | > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. 230 | > 231 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse 232 | > id sem consectetuer libero luctus adipiscing. 233 | 234 | Markdown allows you to be lazy and only put the `>` before the first line of a 235 | hard-wrapped paragraph: 236 | 237 | :::text 238 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, 239 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. 240 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. 241 | 242 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse 243 | id sem consectetuer libero luctus adipiscing. 244 | 245 | Blockquotes can be nested (i.e. a blockquote-in-a-blockquote) by adding 246 | additional levels of `>`: 247 | 248 | :::text 249 | > This is the first level of quoting. 250 | > 251 | > > This is nested blockquote. 252 | > 253 | > Back to the first level. 254 | 255 | Blockquotes can contain other Markdown elements, including headers, lists, and 256 | code blocks: 257 | 258 | :::text 259 | > ## This is a header. 260 | > 261 | > 1. This is the first list item. 262 | > 2. This is the second list item. 263 | > 264 | > Here's some example code: 265 | > 266 | > return shell_exec("echo $input | $markdown_script"); 267 | 268 | Any decent text editor should make email-style quoting easy. For example, with 269 | BBEdit, you can make a selection and choose Increase Quote Level from the Text 270 | menu. 271 | 272 | 273 | ### Lists 274 | 275 | Markdown supports ordered (numbered) and unordered (bulleted) lists. 276 | 277 | Unordered lists use asterisks, pluses, and hyphens -- interchangably -- as list 278 | markers: 279 | 280 | :::text 281 | * Red 282 | * Green 283 | * Blue 284 | 285 | is equivalent to: 286 | 287 | :::text 288 | + Red 289 | + Green 290 | + Blue 291 | 292 | and: 293 | 294 | :::text 295 | - Red 296 | - Green 297 | - Blue 298 | 299 | Ordered lists use numbers followed by periods: 300 | 301 | :::text 302 | 1. Bird 303 | 2. McHale 304 | 3. Parish 305 | 306 | It's important to note that the actual numbers you use to mark the list have no 307 | effect on the HTML output Markdown produces. The HTML Markdown produces from the 308 | above list is: 309 | 310 | :::text 311 |
    312 |
  1. Bird
  2. 313 |
  3. McHale
  4. 314 |
  5. Parish
  6. 315 |
316 | 317 | If you instead wrote the list in Markdown like this: 318 | 319 | :::text 320 | 1. Bird 321 | 1. McHale 322 | 1. Parish 323 | 324 | or even: 325 | 326 | :::text 327 | 3. Bird 328 | 1. McHale 329 | 8. Parish 330 | 331 | you'd get the exact same HTML output. The point is, if you want to, you can use 332 | ordinal numbers in your ordered Markdown lists, so that the numbers in your 333 | source match the numbers in your published HTML. But if you want to be lazy, you 334 | don't have to. 335 | 336 | If you do use lazy list numbering, however, you should still start the list with 337 | the number 1. At some point in the future, Markdown may support starting ordered 338 | lists at an arbitrary number. 339 | 340 | List markers typically start at the left margin, but may be indented by up to 341 | three spaces. List markers must be followed by one or more spaces or a tab. 342 | 343 | To make lists look nice, you can wrap items with hanging indents: 344 | 345 | :::text 346 | * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. 347 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, 348 | viverra nec, fringilla in, laoreet vitae, risus. 349 | * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. 350 | Suspendisse id sem consectetuer libero luctus adipiscing. 351 | 352 | But if you want to be lazy, you don't have to: 353 | 354 | :::text 355 | * Lorem ipsum dolor sit amet, consectetuer adipiscing elit. 356 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi, 357 | viverra nec, fringilla in, laoreet vitae, risus. 358 | * Donec sit amet nisl. Aliquam semper ipsum sit amet velit. 359 | Suspendisse id sem consectetuer libero luctus adipiscing. 360 | 361 | If list items are separated by blank lines, Markdown will wrap the items in 362 | `

` tags in the HTML output. For example, this input: 363 | 364 | :::text 365 | * Bird 366 | * Magic 367 | 368 | will turn into: 369 | 370 | :::html 371 |

    372 |
  • Bird
  • 373 |
  • Magic
  • 374 |
375 | 376 | But this: 377 | 378 | :::text 379 | * Bird 380 | 381 | * Magic 382 | 383 | will turn into: 384 | 385 | :::html 386 |
    387 |
  • Bird

  • 388 |
  • Magic

  • 389 |
390 | 391 | List items may consist of multiple paragraphs. Each subsequent paragraph in a 392 | list item must be indented by either 4 spaces or one tab: 393 | 394 | :::text 395 | 1. This is a list item with two paragraphs. Lorem ipsum dolor 396 | sit amet, consectetuer adipiscing elit. Aliquam hendrerit 397 | mi posuere lectus. 398 | 399 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet 400 | vitae, risus. Donec sit amet nisl. Aliquam semper ipsum 401 | sit amet velit. 402 | 403 | 2. Suspendisse id sem consectetuer libero luctus adipiscing. 404 | 405 | It looks nice if you indent every line of the subsequent paragraphs, but here 406 | again, Markdown will allow you to be lazy: 407 | 408 | :::text 409 | * This is a list item with two paragraphs. 410 | 411 | This is the second paragraph in the list item. You're 412 | only required to indent the first line. Lorem ipsum dolor 413 | sit amet, consectetuer adipiscing elit. 414 | 415 | * Another item in the same list. 416 | 417 | To put a blockquote within a list item, the blockquote's `>` delimiters need to 418 | be indented: 419 | 420 | :::text 421 | * A list item with a blockquote: 422 | 423 | > This is a blockquote 424 | > inside a list item. 425 | 426 | To put a code block within a list item, the code block needs to be indented 427 | *twice* -- 8 spaces or two tabs: 428 | 429 | :::text 430 | * A list item with a code block: 431 | 432 | 433 | 434 | 435 | It's worth noting that it's possible to trigger an ordered list by accident, by 436 | writing something like this: 437 | 438 | :::text 439 | 1986. What a great season. 440 | 441 | In other words, a *number-period-space* sequence at the beginning of a line. To 442 | avoid this, you can backslash-escape the period: 443 | 444 | :::text 445 | 1986\. What a great season. 446 | 447 | 448 | 449 | ### Code Blocks 450 | 451 | Pre-formatted code blocks are used for writing about programming or markup 452 | source code. Rather than forming normal paragraphs, the lines of a code block 453 | are interpreted literally. Markdown wraps a code block in both `
` and
454 | `` tags.
455 | 
456 | To produce a code block in Markdown, simply indent every line of the block by at
457 | least 4 spaces or 1 tab. For example, given this input:
458 | 
459 |     :::text
460 |     This is a normal paragraph:
461 |     
462 |         This is a code block.
463 | 
464 | Markdown will generate:
465 | 
466 |     :::html
467 |     

This is a normal paragraph:

468 | 469 |
This is a code block.
470 |     
471 | 472 | One level of indentation -- 4 spaces or 1 tab -- is removed from each line of 473 | the code block. For example, this: 474 | 475 | :::text 476 | Here is an example of AppleScript: 477 | 478 | tell application "Foo" 479 | beep 480 | end tell 481 | 482 | will turn into: 483 | 484 | :::html 485 |

Here is an example of AppleScript:

486 | 487 |
tell application "Foo"
488 |         beep
489 |     end tell
490 |     
491 | 492 | A code block continues until it reaches a line that is not indented (or the end 493 | of the article). 494 | 495 | Within a code block, ampersands (`&`) and angle brackets (`<` and `>`) are 496 | automatically converted into HTML entities. This makes it very easy to include 497 | example HTML source code using Markdown -- just paste it and indent it, and 498 | Markdown will handle the hassle of encoding the ampersands and angle brackets. 499 | For example, this: 500 | 501 | :::text 502 | 505 | 506 | will turn into: 507 | 508 | :::html 509 |
<div class="footer">
510 |         &copy; 2004 Foo Corporation
511 |     </div>
512 |     
513 | 514 | Regular Markdown syntax is not processed within code blocks. E.g., asterisks are 515 | just literal asterisks within a code block. This means it's also easy to use 516 | Markdown to write about Markdown's own syntax. 517 | 518 | 519 | 520 | ### Horizontal Rules 521 | 522 | You can produce a horizontal rule tag (`
`) by placing three or more 523 | hyphens, asterisks, or underscores on a line by themselves. If you wish, you may 524 | use spaces between the hyphens or asterisks. Each of the following lines will 525 | produce a horizontal rule: 526 | 527 | :::text 528 | * * * 529 | 530 | *** 531 | 532 | ***** 533 | 534 | - - - 535 | 536 | --------------------------------------- 537 | 538 | 539 | * * * 540 | 541 | ## Span Elements 542 | 543 | ### Links 544 | 545 | Markdown supports two style of links: *inline* and *reference*. 546 | 547 | In both styles, the link text is delimited by [square brackets]. 548 | 549 | To create an inline link, use a set of regular parentheses immediately after the 550 | link text's closing square bracket. Inside the parentheses, put the URL where 551 | you want the link to point, along with an *optional* title for the link, 552 | surrounded in quotes. For example: 553 | 554 | :::text 555 | This is [an example](http://example.com/ "Title") inline link. 556 | 557 | [This link](http://example.net/) has no title attribute. 558 | 559 | Will produce: 560 | 561 | :::html 562 |

This is 563 | an example inline link.

564 | 565 |

This link has no 566 | title attribute.

567 | 568 | If you're referring to a local resource on the same server, you can use relative 569 | paths: 570 | 571 | :::text 572 | See my [About](/about/) page for details. 573 | 574 | Reference-style links use a second set of square brackets, inside which you 575 | place a label of your choosing to identify the link: 576 | 577 | :::text 578 | This is [an example][id] reference-style link. 579 | 580 | You can optionally use a space to separate the sets of brackets: 581 | 582 | :::text 583 | This is [an example] [id] reference-style link. 584 | 585 | Then, anywhere in the document, you define your link label like this, on a line 586 | by itself: 587 | 588 | :::text 589 | [id]: http://example.com/ "Optional Title Here" 590 | 591 | That is: 592 | 593 | * Square brackets containing the link identifier (optionally indented from the 594 | left margin using up to three spaces); 595 | * followed by a colon; 596 | * followed by one or more spaces (or tabs); 597 | * followed by the URL for the link; 598 | * optionally followed by a title attribute for the link, enclosed in double or 599 | single quotes, or enclosed in parentheses. 600 | 601 | The following three link definitions are equivalent: 602 | 603 | :::text 604 | [foo]: http://example.com/ "Optional Title Here" 605 | [foo]: http://example.com/ 'Optional Title Here' 606 | [foo]: http://example.com/ (Optional Title Here) 607 | 608 | The link URL may, optionally, be surrounded by angle brackets: 609 | 610 | :::text 611 | [id]: "Optional Title Here" 612 | 613 | You can put the title attribute on the next line and use extra spaces or tabs 614 | for padding, which tends to look better with longer URLs: 615 | 616 | :::text 617 | [id]: http://example.com/longish/path/to/resource/here 618 | "Optional Title Here" 619 | 620 | Link definitions are only used for creating links during Markdown processing, 621 | and are stripped from your document in the HTML output. 622 | 623 | Link definition names may consist of letters, numbers, spaces, and punctuation 624 | -- but they are *not* case sensitive. E.g. these two links: 625 | 626 | :::text 627 | [link text][a] 628 | [link text][A] 629 | 630 | are equivalent. 631 | 632 | The *implicit link name* shortcut allows you to omit the name of the link, in 633 | which case the link text itself is used as the name. Just use an empty set of 634 | square brackets -- e.g., to link the word "Google" to the google.com web site, 635 | you could simply write: 636 | 637 | :::text 638 | [Google][] 639 | 640 | And then define the link: 641 | 642 | :::text 643 | [Google]: http://google.com/ 644 | 645 | Because link names may contain spaces, this shortcut even works for multiple 646 | words in the link text: 647 | 648 | :::text 649 | Visit [Daring Fireball][] for more information. 650 | 651 | And then define the link: 652 | 653 | :::text 654 | [Daring Fireball]: http://daringfireball.net/ 655 | 656 | Link definitions can be placed anywhere in your Markdown document. I tend to put 657 | them immediately after each paragraph in which they're used, but if you want, 658 | you can put them all at the end of your document, sort of like footnotes. 659 | 660 | Here's an example of reference links in action: 661 | 662 | :::text 663 | I get 10 times more traffic from [Google] [1] than from 664 | [Yahoo] [2] or [MSN] [3]. 665 | 666 | [1]: http://google.com/ "Google" 667 | [2]: http://search.yahoo.com/ "Yahoo Search" 668 | [3]: http://search.msn.com/ "MSN Search" 669 | 670 | Using the implicit link name shortcut, you could instead write: 671 | 672 | :::text 673 | I get 10 times more traffic from [Google][] than from 674 | [Yahoo][] or [MSN][]. 675 | 676 | [google]: http://google.com/ "Google" 677 | [yahoo]: http://search.yahoo.com/ "Yahoo Search" 678 | [msn]: http://search.msn.com/ "MSN Search" 679 | 680 | Both of the above examples will produce the following HTML output: 681 | 682 | :::html 683 |

I get 10 times more traffic from Google than from 685 | Yahoo 686 | or MSN.

687 | 688 | For comparison, here is the same paragraph written using Markdown's inline link 689 | style: 690 | 691 | :::text 692 | I get 10 times more traffic from [Google](http://google.com/ "Google") 693 | than from [Yahoo](http://search.yahoo.com/ "Yahoo Search") or 694 | [MSN](http://search.msn.com/ "MSN Search"). 695 | 696 | The point of reference-style links is not that they're easier to write. The 697 | point is that with reference-style links, your document source is vastly more 698 | readable. Compare the above examples: using reference-style links, the paragraph 699 | itself is only 81 characters long; with inline-style links, it's 176 characters; 700 | and as raw HTML, it's 234 characters. In the raw HTML, there's more markup than 701 | there is text. 702 | 703 | With Markdown's reference-style links, a source document much more closely 704 | resembles the final output, as rendered in a browser. By allowing you to move 705 | the markup-related metadata out of the paragraph, you can add links without 706 | interrupting the narrative flow of your prose. 707 | 708 | 709 | ### Emphasis 710 | 711 | Markdown treats asterisks (`*`) and underscores (`_`) as indicators of emphasis. 712 | Text wrapped with one `*` or `_` will be wrapped with an HTML `` tag; double 713 | `*`'s or `_`'s will be wrapped with an HTML `` tag. E.g., this input: 714 | 715 | :::text 716 | *single asterisks* 717 | 718 | _single underscores_ 719 | 720 | **double asterisks** 721 | 722 | __double underscores__ 723 | 724 | will produce: 725 | 726 | :::html 727 | single asterisks 728 | 729 | single underscores 730 | 731 | double asterisks 732 | 733 | double underscores 734 | 735 | You can use whichever style you prefer; the lone restriction is that the same 736 | character must be used to open and close an emphasis span. 737 | 738 | Emphasis can be used in the middle of a word: 739 | 740 | :::text 741 | un*frigging*believable 742 | 743 | But if you surround an `*` or `_` with spaces, it'll be treated as a literal 744 | asterisk or underscore. 745 | 746 | To produce a literal asterisk or underscore at a position where it would 747 | otherwise be used as an emphasis delimiter, you can backslash escape it: 748 | 749 | :::text 750 | \*this text is surrounded by literal asterisks\* 751 | 752 | 753 | 754 | ### Code 755 | 756 | To indicate a span of code, wrap it with backtick quotes (`` ` ``). Unlike a 757 | pre-formatted code block, a code span indicates code within a normal paragraph. 758 | For example: 759 | 760 | :::text 761 | Use the `printf()` function. 762 | 763 | will produce: 764 | 765 | :::html 766 |

Use the printf() function.

767 | 768 | To include a literal backtick character within a code span, you can use multiple 769 | backticks as the opening and closing delimiters: 770 | 771 | :::text 772 | ``There is a literal backtick (`) here.`` 773 | 774 | which will produce this: 775 | 776 | :::html 777 |

There is a literal backtick (`) here.

778 | 779 | The backtick delimiters surrounding a code span may include spaces -- one after 780 | the opening, one before the closing. This allows you to place literal backtick 781 | characters at the beginning or end of a code span: 782 | 783 | :::text 784 | A single backtick in a code span: `` ` `` 785 | 786 | A backtick-delimited string in a code span: `` `foo` `` 787 | 788 | will produce: 789 | 790 | :::html 791 |

A single backtick in a code span: `

792 | 793 |

A backtick-delimited string in a code span: `foo`

794 | 795 | With a code span, ampersands and angle brackets are encoded as HTML entities 796 | automatically, which makes it easy to include example HTML tags. Markdown will 797 | turn this: 798 | 799 | :::text 800 | Please don't use any `` tags. 801 | 802 | into: 803 | 804 | :::html 805 |

Please don't use any <blink> tags.

806 | 807 | You can write this: 808 | 809 | :::text 810 | `—` is the decimal-encoded equivalent of `—`. 811 | 812 | to produce: 813 | 814 | :::html 815 |

&#8212; is the decimal-encoded 816 | equivalent of &mdash;.

817 | 818 | 819 | 820 | ### Images 821 | 822 | Admittedly, it's fairly difficult to devise a "natural" syntax for placing 823 | images into a plain text document format. 824 | 825 | Markdown uses an image syntax that is intended to resemble the syntax for links, 826 | allowing for two styles: *inline* and *reference*. 827 | 828 | Inline image syntax looks like this: 829 | 830 | :::text 831 | ![Alt text](/path/to/img.jpg) 832 | 833 | ![Alt text](/path/to/img.jpg "Optional title") 834 | 835 | That is: 836 | 837 | * An exclamation mark: `!`; 838 | * followed by a set of square brackets, containing the `alt` attribute text 839 | for the image; 840 | * followed by a set of parentheses, containing the URL or path to the image, 841 | and an optional `title` attribute enclosed in double or single quotes. 842 | 843 | Reference-style image syntax looks like this: 844 | 845 | :::text 846 | ![Alt text][id] 847 | 848 | Where "id" is the name of a defined image reference. Image references are 849 | defined using syntax identical to link references: 850 | 851 | :::text 852 | [id]: url/to/image "Optional title attribute" 853 | 854 | As of this writing, Markdown has no syntax for specifying the dimensions of an 855 | image; if this is important to you, you can simply use regular HTML `` 856 | tags. 857 | 858 | 859 | * * * 860 | 861 | 862 | ## Miscellaneous 863 | 864 | ### Automatic Links 865 | 866 | Markdown supports a shortcut style for creating "automatic" links for URLs and 867 | email addresses: simply surround the URL or email address with angle brackets. 868 | What this means is that if you want to show the actual text of a URL or email 869 | address, and also have it be a clickable link, you can do this: 870 | 871 | :::text 872 | 873 | 874 | Markdown will turn this into: 875 | 876 | :::html 877 | http://example.com/ 878 | 879 | Automatic links for email addresses work similarly, except that Markdown will 880 | also perform a bit of randomized decimal and hex entity-encoding to help obscure 881 | your address from address-harvesting spambots. For example, Markdown will turn 882 | this: 883 | 884 | :::text 885 | 886 | 887 | into something like this: 888 | 889 | :::html 890 | address@exa 893 | mple.com 894 | 895 | which will render in a browser as a clickable link to "address@example.com". 896 | 897 | (This sort of entity-encoding trick will indeed fool many, if not most, 898 | address-harvesting bots, but it definitely won't fool all of them. It's better 899 | than nothing, but an address published in this way will probably eventually 900 | start receiving spam.) 901 | 902 | 903 | 904 | ### Backslash Escapes 905 | 906 | Markdown allows you to use backslash escapes to generate literal characters 907 | which would otherwise have special meaning in Markdown's formatting syntax. For 908 | example, if you wanted to surround a word with literal asterisks (instead of an 909 | HTML `` tag), you can use backslashes before the asterisks, like this: 910 | 911 | :::text 912 | \*literal asterisks\* 913 | 914 | Markdown provides backslash escapes for the following characters: 915 | 916 | :::text 917 | \ backslash 918 | ` backtick 919 | * asterisk 920 | _ underscore 921 | {} curly braces 922 | [] square brackets 923 | () parentheses 924 | # hash mark 925 | + plus sign 926 | - minus sign (hyphen) 927 | . dot 928 | ! exclamation mark 929 | --------------------------------------------------------------------------------