The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .flake8
├── .gitattributes
├── .gitignore
├── .travis.yml
├── AUTHORS.md
├── CHANGES.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── artwork
    ├── favicon-combined.pdn
    ├── favicon.ico
    └── favicon.pdn
├── docs
    └── TODO
├── grip
    ├── __init__.py
    ├── __main__.py
    ├── _compat.py
    ├── api.py
    ├── app.py
    ├── assets.py
    ├── browser.py
    ├── command.py
    ├── constants.py
    ├── exceptions.py
    ├── patcher.py
    ├── readers.py
    ├── renderers.py
    ├── settings.py
    ├── static
    │   ├── favicon.ico
    │   └── octicons
    │   │   ├── octicons.css
    │   │   ├── octicons.eot
    │   │   ├── octicons.svg
    │   │   ├── octicons.ttf
    │   │   ├── octicons.woff
    │   │   └── octicons.woff2
    ├── templates
    │   ├── base.html
    │   ├── index.html
    │   └── limit.html
    └── vendor
    │   ├── __init__.py
    │   ├── mdx_urlize.py
    │   └── six.py
├── pytest.ini
├── requirements-test.txt
├── requirements.txt
├── setup.py
└── tests
    ├── conftest.py
    ├── helpers.py
    ├── input
        ├── default
        │   └── README.md
        ├── gfm-test.md
        ├── github.md
        ├── simple.md
        └── zero.md
    ├── mocks.py
    ├── output
        ├── app
        │   ├── gfm-test-user-content.html
        │   ├── gfm-test-user-context.html
        │   ├── gfm-test.html
        │   ├── simple-user-content.html
        │   ├── simple-user-context.html
        │   ├── simple.html
        │   ├── zero-user-content.html
        │   ├── zero-user-context.html
        │   └── zero.html
        ├── raw
        │   ├── gfm-test-user-content.html
        │   ├── gfm-test-user-context.html
        │   ├── gfm-test.html
        │   ├── simple-user-content.html
        │   ├── simple-user-context.html
        │   ├── simple.html
        │   ├── zero-user-content.html
        │   ├── zero-user-context.html
        │   └── zero.html
        └── renderer
        │   ├── gfm-test-user-content.html
        │   ├── gfm-test-user-context.html
        │   ├── gfm-test.html
        │   ├── simple-user-content.html
        │   ├── simple-user-context.html
        │   └── simple.html
    ├── regenerate.py
    ├── test_api.py
    ├── test_cli.py
    └── test_github.py


/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude = .git,__pycache__,.research,grip/vendor
3 | 


--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /tests/output/*/*.html linguist-generated=true
2 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Deployment files
 2 | *.egg-info
 3 | dist
 4 | 
 5 | # Environment files
 6 | site-packages
 7 | instance
 8 | build
 9 | env
10 | .venv
11 | .cache
12 | *.py[cod]
13 | settings_local.py
14 | 
15 | # Test files
16 | .pytest_cache
17 | 
18 | # OS-specific files
19 | .DS_Store
20 | Desktop.ini
21 | Thumbs.db
22 | 
23 | # IDE-specific files
24 | .idea
25 | 


--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
 1 | dist: xenial
 2 | language: python
 3 | python:
 4 |   - 3.7
 5 |   - 3.8
 6 |   - 3.9
 7 |   - 3.10
 8 |   - pypy3
 9 | install:
10 |   - pip install -e .[tests]
11 | script:
12 |   - flake8
13 |   - pytest -m "not assumption"
14 | 


--------------------------------------------------------------------------------
/AUTHORS.md:
--------------------------------------------------------------------------------
 1 | Authors
 2 | =======
 3 | 
 4 | [Grip][home] is written and maintained by Joe Esposito,
 5 | along with the following contributors:
 6 | 
 7 | - Vlad Wing ([@vladwing](https://github.com/vladwing))
 8 | - Ismail Badawi ([@isbadawi](https://github.com/isbadawi))
 9 | - Joe Littlejohn ([@joelittlejohn](https://github.com/joelittlejohn))
10 | - Brian Cappello ([@briancappello](https://github.com/briancappello))
11 | - John Gallagher ([@jgallagher](https://github.com/jgallagher))
12 | - Ilya Rumyantsev ([@iliggio](https://github.com/iliggio))
13 | - Jon Chen ([@fly](https://github.com/fly))
14 | - Silas Snider ([@swsnider](https://github.com/swsnider))
15 | - Dave James Miller ([@davejamesmiller](https://github.com/davejamesmiller))
16 | - Alexandre Magno ([@alexandre-mbm](https://github.com/alexandre-mbm))
17 | - [@madflow](https://github.com/madflow)
18 | - Zhiming Wang ([@zmwangx](https://github.com/zmwangx))
19 | - Dan Davison ([@dandavison](https://github.com/dandavison))
20 | - Sriram Sundarraj ([@ssundarraj](https://github.com/ssundarraj))
21 | - Jose Honorato ([@jlhonora](https://github.com/jlhonora))
22 | - Aka.Why ([@akawhy](https://github.com/akawhy))
23 | - Mark Thomas ([@markbt](https://github.com/markbt))
24 | - Gastón N. Charkiewicz ([@mekoda](https://github.com/mekoda))
25 | - Erik Hummel ([@ErikMHummel](https://github.com/ErikMHummel))
26 | - Matthew R. Tanudjaja ([@mrexmelle](https://github.com/mrexmelle))
27 | - Tom Dunlap ([@motevets](https://github.com/motevets))
28 | - Konstantin Baierer ([@kba](https://github.com/kba))
29 | - Jakub Wilk ([@jwilk](https://github.com/jwilk))
30 | - Devin Chen ([@xxd3vin](https://github.com/xxd3vin))
31 | - Jamie Davis ([@davisjam](https://github.com/davisjam))
32 | - JasonThomasData ([@JasonThomasData](https://github.com/JasonThomasData))
33 | - Andrej ([@4ndrej](https://github.com/4ndrej))
34 | - Karl Goffin ([@kagof](https://github.com/kagof))
35 | - Gideon Richter ([@Godron629](https://github.com/Godron629))
36 | - Tom Dupré la Tour ([@TomDLT](https://github.com/TomDLT))
37 | - Joshua Adelman ([@synapticarbors](https://github.com/synapticarbors))
38 | - Simeon Visser ([@svisser](https://github.com/svisser))
39 | - Jace Browning ([@jacebrowning](https://github.com/jacebrowning))
40 | - Daniel Shannon ([@phyllisstein](https://github.com/phyllisstein))
41 | - Aaron Sikes ([@courajs](https://github.com/courajs))
42 | - Winsley ([@wvspee](https://github.com/wvspee))
43 | - Methacrylon ([@Methacrylon](https://github.com/Methacrylon))
44 | - Bryce Carson ([@Methacrylon](https://github.com/bryce-carson))
45 | 
46 | 
47 | [home]: README.md
48 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | Copyright (c) 2014-2022 Joe Esposito <joe@joeyespo.com>
 2 | 
 3 | Permission is hereby granted, free of charge, to any person obtaining a copy
 4 | of this software and associated documentation files (the "Software"), to deal
 5 | in the Software without restriction, including without limitation the rights
 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 7 | copies of the Software, and to permit persons to whom the Software is
 8 | furnished to do so, subject to the following conditions:
 9 | 
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 | 
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 | 


--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include *.txt
3 | include *.md
4 | recursive-include static *
5 | recursive-include templates *
6 | 


--------------------------------------------------------------------------------
/artwork/favicon-combined.pdn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/artwork/favicon-combined.pdn


--------------------------------------------------------------------------------
/artwork/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/artwork/favicon.ico


--------------------------------------------------------------------------------
/artwork/favicon.pdn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/artwork/favicon.pdn


--------------------------------------------------------------------------------
/docs/TODO:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/docs/TODO


--------------------------------------------------------------------------------
/grip/__init__.py:
--------------------------------------------------------------------------------
 1 | """\
 2 | Grip
 3 | ----
 4 | 
 5 | Render local readme files before sending off to GitHub.
 6 | 
 7 | :copyright: (c) 2014-2022 by Joe Esposito.
 8 | :license: MIT, see LICENSE for more details.
 9 | """
10 | 
11 | __version__ = '4.6.2'  # noqa
12 | 
13 | import sys
14 | 
15 | # Patch for Flask 11.0+ on Python 3 (pypy3)
16 | if not hasattr(sys, 'exc_clear'):  # noqa
17 |     sys.exc_clear = lambda: None
18 | 
19 | from .api import (
20 |     clear_cache, create_app, export, render_content, render_page, serve)
21 | from .app import Grip
22 | from .assets import GitHubAssetManager, ReadmeAssetManager
23 | from .command import main
24 | from .constants import (
25 |     DEFAULT_API_URL, DEFAULT_FILENAMES, DEFAULT_FILENAME, DEFAULT_GRIPHOME,
26 |     DEFAULT_GRIPURL, STYLE_ASSET_URLS_INLINE_FORMAT, STYLE_ASSET_URLS_RE,
27 |     STYLE_ASSET_URLS_SUB_FORMAT, STYLE_URLS_RES, STYLE_URLS_SOURCE,
28 |     SUPPORTED_EXTENSIONS, SUPPORTED_TITLES)
29 | from .exceptions import AlreadyRunningError, ReadmeNotFoundError
30 | from .readers import ReadmeReader, DirectoryReader, StdinReader, TextReader
31 | from .renderers import ReadmeRenderer, GitHubRenderer, OfflineRenderer
32 | 
33 | 
34 | __all__ = [
35 |     '__version__',
36 | 
37 |     'DEFAULT_API_URL', 'DEFAULT_FILENAMES', 'DEFAULT_FILENAME',
38 |     'DEFAULT_GRIPHOME', 'DEFAULT_GRIPURL', 'STYLE_ASSET_URLS_INLINE_FORMAT',
39 |     'STYLE_ASSET_URLS_RE', 'STYLE_ASSET_URLS_SUB_FORMAT', 'STYLE_URLS_RES',
40 |     'STYLE_URLS_SOURCE', 'SUPPORTED_EXTENSIONS', 'SUPPORTED_TITLES',
41 | 
42 |     'AlreadyRunningError', 'DirectoryReader', 'GitHubAssetManager',
43 |     'GitHubRenderer', 'Grip', 'OfflineRenderer', 'ReadmeNotFoundError',
44 |     'ReadmeAssetManager', 'ReadmeReader', 'ReadmeRenderer', 'StdinReader',
45 |     'TextReader',
46 | 
47 |     'clear_cache', 'create_app', 'export', 'main', 'render_content',
48 |     'render_page', 'serve',
49 | ]
50 | 


--------------------------------------------------------------------------------
/grip/__main__.py:
--------------------------------------------------------------------------------
 1 | """\
 2 | Grip
 3 | ----
 4 | 
 5 | Render local readme files before sending off to GitHub.
 6 | 
 7 | :copyright: (c) 2014-2022 by Joe Esposito.
 8 | :license: MIT, see LICENSE for more details.
 9 | """
10 | 
11 | import os
12 | import sys
13 | 
14 | 
15 | if __name__ == '__main__':
16 |     sys.path.insert(1, os.path.dirname(os.path.dirname(
17 |         os.path.abspath(__file__))))
18 | 
19 |     from grip.command import main
20 |     main()
21 | 


--------------------------------------------------------------------------------
/grip/_compat.py:
--------------------------------------------------------------------------------
 1 | # TODO: Use Werkzeug's directly after dropping support for older Flask versions
 2 | try:
 3 |     # Use older Flask implementation directly to ensure backwards compatibility
 4 |     from flask import safe_join
 5 | except ImportError:
 6 |     import werkzeug.utils
 7 |     from werkzeug.exceptions import NotFound
 8 | 
 9 |     # Use port of Flask 2.0 safe_join to match behavior
10 |     def safe_join(directory, *pathnames):
11 |         """Safely join zero or more untrusted path components to a base
12 |         directory to avoid escaping the base directory.
13 | 
14 |         :param directory: The trusted base directory.
15 |         :param pathnames: The untrusted path components relative to the
16 |             base directory.
17 |         :return: A safe path.
18 |         """
19 |         path = werkzeug.utils.safe_join(directory, *pathnames)
20 | 
21 |         if path is None:
22 |             raise NotFound()
23 | 
24 |         return path
25 | 
26 | 
27 | __all__ = ['safe_join']
28 | 


--------------------------------------------------------------------------------
/grip/api.py:
--------------------------------------------------------------------------------
  1 | from __future__ import print_function, unicode_literals
  2 | 
  3 | import io
  4 | import os
  5 | import sys
  6 | import errno
  7 | 
  8 | from .app import Grip
  9 | from .readers import DirectoryReader, StdinReader, TextReader
 10 | from .renderers import GitHubRenderer, OfflineRenderer
 11 | 
 12 | 
 13 | def create_app(path=None, user_content=False, context=None, username=None,
 14 |                password=None, render_offline=False, render_wide=False,
 15 |                render_inline=False, api_url=None, title=None, text=None,
 16 |                autorefresh=None, quiet=None, theme='light', grip_class=None):
 17 |     """
 18 |     Creates a Grip application with the specified overrides.
 19 |     """
 20 |     # Customize the app
 21 |     if grip_class is None:
 22 |         grip_class = Grip
 23 | 
 24 |     # Customize the reader
 25 |     if text is not None:
 26 |         display_filename = DirectoryReader(path, True).filename_for(None)
 27 |         source = TextReader(text, display_filename)
 28 |     elif path == '-':
 29 |         source = StdinReader()
 30 |     else:
 31 |         source = DirectoryReader(path)
 32 | 
 33 |     # Customize the renderer
 34 |     if render_offline:
 35 |         renderer = OfflineRenderer(user_content, context)
 36 |     elif user_content or context or api_url:
 37 |         renderer = GitHubRenderer(user_content, context, api_url)
 38 |     else:
 39 |         renderer = None
 40 | 
 41 |     # Optional basic auth
 42 |     auth = (username, password) if username or password else None
 43 | 
 44 |     # Create the customized app with default asset manager
 45 |     return grip_class(source, auth, renderer, None, render_wide,
 46 |                       render_inline, title, autorefresh, quiet, theme)
 47 | 
 48 | 
 49 | def serve(path=None, host=None, port=None, user_content=False, context=None,
 50 |           username=None, password=None, render_offline=False,
 51 |           render_wide=False, render_inline=False, api_url=None, title=None,
 52 |           autorefresh=True, browser=False, quiet=None, theme='light', grip_class=None):
 53 |     """
 54 |     Starts a server to render the specified file or directory containing
 55 |     a README.
 56 |     """
 57 |     app = create_app(path, user_content, context, username, password,
 58 |                      render_offline, render_wide, render_inline, api_url,
 59 |                      title, None, autorefresh, quiet, theme, grip_class)
 60 |     app.run(host, port, open_browser=browser)
 61 | 
 62 | 
 63 | def clear_cache(grip_class=None):
 64 |     """
 65 |     Clears the cached styles and assets.
 66 |     """
 67 |     if grip_class is None:
 68 |         grip_class = Grip
 69 |     grip_class(StdinReader()).clear_cache()
 70 | 
 71 | 
 72 | def render_page(path=None, user_content=False, context=None,
 73 |                 username=None, password=None,
 74 |                 render_offline=False, render_wide=False, render_inline=False,
 75 |                 api_url=None, title=None, text=None, quiet=None, theme='light',
 76 |                 grip_class=None):
 77 |     """
 78 |     Renders the specified markup text to an HTML page and returns it.
 79 |     """
 80 |     return create_app(path, user_content, context, username, password,
 81 |                       render_offline, render_wide, render_inline, api_url,
 82 |                       title, text, False, quiet, theme, grip_class).render()
 83 | 
 84 | 
 85 | def render_content(text, user_content=False, context=None, username=None,
 86 |                    password=None, render_offline=False, api_url=None):
 87 |     """
 88 |     Renders the specified markup and returns the result.
 89 |     """
 90 |     renderer = (GitHubRenderer(user_content, context, api_url)
 91 |                 if not render_offline else
 92 |                 OfflineRenderer(user_content, context))
 93 |     auth = (username, password) if username or password else None
 94 |     return renderer.render(text, auth)
 95 | 
 96 | 
 97 | def export(path=None, user_content=False, context=None,
 98 |            username=None, password=None, render_offline=False,
 99 |            render_wide=False, render_inline=True, out_filename=None,
100 |            api_url=None, title=None, quiet=False, theme='light', grip_class=None):
101 |     """
102 |     Exports the rendered HTML to a file.
103 |     """
104 |     export_to_stdout = out_filename == '-'
105 |     if out_filename is None:
106 |         if path == '-':
107 |             export_to_stdout = True
108 |         else:
109 |             filetitle, _ = os.path.splitext(
110 |                 os.path.relpath(DirectoryReader(path).root_filename))
111 |             out_filename = '{0}.html'.format(filetitle)
112 | 
113 |     if not export_to_stdout and not quiet:
114 |         print('Exporting to', out_filename, file=sys.stderr)
115 | 
116 |     page = render_page(path, user_content, context, username, password,
117 |                        render_offline, render_wide, render_inline, api_url,
118 |                        title, None, quiet, theme, grip_class)
119 | 
120 |     if export_to_stdout:
121 |         try:
122 |             print(page)
123 |         except IOError as ex:
124 |             if ex.errno != 0 and ex.errno != errno.EPIPE:
125 |                 raise
126 |     else:
127 |         with io.open(out_filename, 'w', encoding='utf-8') as f:
128 |             f.write(page)
129 | 


--------------------------------------------------------------------------------
/grip/app.py:
--------------------------------------------------------------------------------
  1 | from __future__ import print_function, unicode_literals
  2 | 
  3 | import base64
  4 | import json
  5 | import mimetypes
  6 | import os
  7 | import posixpath
  8 | import re
  9 | import socket
 10 | import sys
 11 | import threading
 12 | import time
 13 | import errno
 14 | from traceback import format_exc
 15 | try:
 16 |     from urlparse import urlparse
 17 | except ImportError:
 18 |     from urllib.parse import urlparse
 19 | try:
 20 |     str_type = basestring
 21 | except NameError:
 22 |     str_type = str
 23 | 
 24 | import requests
 25 | from flask import (
 26 |     Flask, Response, abort, redirect, render_template, request,
 27 |     send_from_directory, url_for)
 28 | 
 29 | from . import __version__
 30 | from .assets import GitHubAssetManager, ReadmeAssetManager
 31 | from .browser import start_browser_when_ready
 32 | from .constants import (
 33 |     DEFAULT_GRIPHOME, DEFAULT_GRIPURL, STYLE_ASSET_URLS_INLINE_FORMAT)
 34 | from .exceptions import AlreadyRunningError, ReadmeNotFoundError
 35 | from .readers import DirectoryReader
 36 | from .renderers import GitHubRenderer, ReadmeRenderer
 37 | 
 38 | 
 39 | class Grip(Flask):
 40 |     """
 41 |     A Flask application that can serve the specified file or directory
 42 |     containing a README.
 43 |     """
 44 |     def __init__(self, source=None, auth=None, renderer=None,
 45 |                  assets=None, render_wide=None, render_inline=None, title=None,
 46 |                  autorefresh=None, quiet=None, theme='light', grip_url=None,
 47 |                  static_url_path=None, instance_path=None, **kwargs):
 48 |         # Defaults
 49 |         if source is None or isinstance(source, str_type):
 50 |             source = DirectoryReader(source)
 51 |         if render_wide is None:
 52 |             render_wide = False
 53 |         if render_inline is None:
 54 |             render_inline = False
 55 | 
 56 |         # Defaults from ENV
 57 |         if grip_url is None:
 58 |             grip_url = os.environ.get('GRIPURL')
 59 |             if grip_url is None:
 60 |                 grip_url = DEFAULT_GRIPURL
 61 |         grip_url = grip_url.rstrip('/')
 62 |         if static_url_path is None:
 63 |             static_url_path = posixpath.join(grip_url, 'static')
 64 |         if instance_path is None:
 65 |             instance_path = os.environ.get('GRIPHOME')
 66 |             if instance_path is None:
 67 |                 instance_path = DEFAULT_GRIPHOME
 68 |         instance_path = os.path.abspath(os.path.expanduser(instance_path))
 69 | 
 70 |         # Flask application
 71 |         super(Grip, self).__init__(
 72 |             __name__, static_url_path=static_url_path,
 73 |             instance_path=instance_path, **kwargs)
 74 |         self.config.from_object('grip.settings')
 75 | 
 76 |         try:
 77 |             self.config.from_pyfile('settings_local.py', silent=True)
 78 |             self.config.from_pyfile(
 79 |                 os.path.join(instance_path, 'settings.py'), silent=True)
 80 |         except IOError as ex:
 81 |             # Flask workaround for when ~/.grip exists but is not a directory
 82 |             if ex.errno != errno.ENOTDIR:
 83 |                 raise
 84 | 
 85 |         # Defaults from settings
 86 |         if autorefresh is None:
 87 |             autorefresh = self.config['AUTOREFRESH']
 88 |         if quiet is None:
 89 |             quiet = self.config['QUIET']
 90 |         if auth is None:
 91 |             username = self.config['USERNAME']
 92 |             password = self.config['PASSWORD']
 93 |             if username or password:
 94 |                 auth = (username or '', password or '')
 95 | 
 96 |         # Thread-safe event to signal to the polling threads to exit
 97 |         self._run_mutex = threading.Lock()
 98 |         self._shutdown_event = None
 99 | 
100 |         # Parameterized attributes
101 |         self.auth = auth
102 |         self.autorefresh = autorefresh
103 |         self.reader = source
104 |         self.renderer = renderer
105 |         self.assets = assets
106 |         self.render_wide = render_wide
107 |         self.render_inline = render_inline
108 |         self.title = title
109 |         self.quiet = quiet
110 |         if self.quiet:
111 |             import logging
112 |             log = logging.getLogger('werkzeug')
113 |             log.setLevel(logging.ERROR)
114 |         self.theme = theme
115 | 
116 |         # Overridable attributes
117 |         if self.renderer is None:
118 |             renderer = self.default_renderer()
119 |             if not isinstance(renderer, ReadmeRenderer):
120 |                 raise TypeError(
121 |                     'Expected Grip.default_renderer to return a '
122 |                     'ReadmeRenderer instance, got {0}.'.format(type(renderer)))
123 |             self.renderer = renderer
124 |         if self.assets is None:
125 |             assets = self.default_asset_manager()
126 |             if not isinstance(assets, ReadmeAssetManager):
127 |                 raise TypeError(
128 |                     'Expected Grip.default_asset_manager to return an '
129 |                     'ReadmeAssetManager instance, got {0}.'.format(
130 |                         type(assets)))
131 |             self.assets = assets
132 | 
133 |         # Add missing content types
134 |         self.add_content_types()
135 | 
136 |         # Construct routes
137 |         asset_route = posixpath.join(grip_url, 'asset', '')
138 |         asset_subpath = posixpath.join(asset_route, '<path:subpath>')
139 |         refresh_route = posixpath.join(grip_url, 'refresh', '')
140 |         refresh_subpath = posixpath.join(refresh_route, '<path:subpath>')
141 |         rate_limit_route = posixpath.join(grip_url, 'rate-limit-preview')
142 | 
143 |         # Initialize views
144 |         self._styles_retrieved = False
145 |         self.before_request(self._retrieve_styles)
146 |         self.add_url_rule(asset_route, 'asset', self._render_asset)
147 |         self.add_url_rule(asset_subpath, 'asset', self._render_asset)
148 |         self.add_url_rule('/', 'render', self._render_page)
149 |         self.add_url_rule('/<path:subpath>', 'render', self._render_page)
150 |         self.add_url_rule(refresh_route, 'refresh', self._render_refresh)
151 |         self.add_url_rule(refresh_subpath, 'refresh', self._render_refresh)
152 |         self.add_url_rule(rate_limit_route, 'rate_limit',
153 |                           self._render_rate_limit_page)
154 |         self.errorhandler(403)(self._render_rate_limit_page)
155 | 
156 |     def _redirect_to_subpath(self, subpath=None):
157 |         """
158 |         Redirects to the specified subpath, which is the relative path
159 |         part of the root location (i.e. the current working directory
160 |         or the path part of a URL excluding the initial '/').
161 |         """
162 |         route = posixpath.normpath('/' + (subpath or '').lstrip('/'))
163 |         return redirect(route)
164 | 
165 |     def _render_asset(self, subpath):
166 |         """
167 |         Renders the specified cache file.
168 |         """
169 |         return send_from_directory(
170 |             self.assets.cache_path, self.assets.cache_filename(subpath))
171 | 
172 |     def _render_page(self, subpath=None):
173 |         # Normalize the subpath
174 |         normalized = self.reader.normalize_subpath(subpath)
175 |         if normalized != subpath:
176 |             return self._redirect_to_subpath(normalized)
177 | 
178 |         # Read the Readme text or asset
179 |         try:
180 |             text = self.reader.read(subpath)
181 |         except ReadmeNotFoundError:
182 |             abort(404)
183 | 
184 |         # Return binary asset
185 |         if self.reader.is_binary(subpath):
186 |             mimetype = self.reader.mimetype_for(subpath)
187 |             return Response(text, mimetype=mimetype)
188 | 
189 |         # Render the Readme content
190 |         try:
191 |             content = self.renderer.render(text, self.auth)
192 |         except requests.HTTPError as ex:
193 |             if ex.response.status_code == 403:
194 |                 abort(403)
195 |             raise
196 |         except requests.exceptions.SSLError as ex:
197 |             if 'TLSV1_ALERT_PROTOCOL_VERSION' in str(ex):
198 |                 print('Error: GitHub has turned off TLS1.0 support. '
199 |                       'Please upgrade your version of Python or Homebrew '
200 |                       'to use a later version of openssl. '
201 |                       'For more information, see '
202 |                       'https://github.com/joeyespo/grip/issues/262')
203 |                 abort(500)
204 |             raise
205 | 
206 |         # Inline favicon asset
207 |         favicon = None
208 |         if self.render_inline:
209 |             favicon_url = url_for('static', filename='favicon.ico')
210 |             favicon = self._to_data_url(favicon_url, 'image/x-icon')
211 | 
212 |         autorefresh_url = (url_for('refresh', subpath=subpath)
213 |                            if self.autorefresh
214 |                            else None)
215 | 
216 |         if self.theme == 'dark':
217 |             data_color_mode = 'dark'
218 |             data_light_theme = 'light'
219 |             data_dark_theme = 'dark'
220 |         else:
221 |             data_color_mode = 'light'
222 |             data_light_theme = 'light'
223 |             data_dark_theme = 'dark'
224 | 
225 |         return render_template(
226 |             'index.html', filename=self.reader.filename_for(subpath),
227 |             title=self.title, content=content, favicon=favicon,
228 |             user_content=self.renderer.user_content,
229 |             wide_style=self.render_wide, style_urls=self.assets.style_urls,
230 |             styles=self.assets.styles, autorefresh_url=autorefresh_url,
231 |             data_color_mode=data_color_mode, data_light_theme=data_light_theme,
232 |             data_dark_theme=data_dark_theme)
233 | 
234 |     def _render_refresh(self, subpath=None):
235 |         if not self.autorefresh:
236 |             abort(404)
237 | 
238 |         # Normalize the subpath
239 |         normalized = self.reader.normalize_subpath(subpath)
240 |         if normalized != subpath:
241 |             return self._redirect_to_subpath(normalized)
242 | 
243 |         # Get the full filename for display
244 |         filename = self.reader.filename_for(subpath)
245 | 
246 |         # Check whether app is running
247 |         shutdown_event = self._shutdown_event
248 |         if not shutdown_event or shutdown_event.is_set():
249 |             return ''
250 | 
251 |         def gen():
252 |             last_updated = self.reader.last_updated(subpath)
253 |             try:
254 |                 while not shutdown_event.is_set():
255 |                     time.sleep(0.3)
256 | 
257 |                     # Check for update
258 |                     updated = self.reader.last_updated(subpath)
259 |                     if updated == last_updated:
260 |                         continue
261 |                     last_updated = updated
262 |                     # Notify user that a refresh is in progress
263 |                     if not self.quiet:
264 |                         print(' * Change detected in {0}, refreshing'
265 |                               .format(filename))
266 |                     yield 'data: {0}\r\n\r\n'.format(
267 |                         json.dumps({'updating': True}))
268 |                     # Binary assets not supported
269 |                     if self.reader.is_binary(subpath):
270 |                         return
271 |                     # Read the Readme text
272 |                     try:
273 |                         text = self.reader.read(subpath)
274 |                     except ReadmeNotFoundError:
275 |                         return
276 |                     # Render the Readme content
277 |                     try:
278 |                         content = self.renderer.render(text, self.auth)
279 |                     except requests.HTTPError as ex:
280 |                         if ex.response.status_code == 403:
281 |                             abort(403)
282 |                         raise
283 |                     # Return the Readme content
284 |                     yield 'data: {0}\r\n\r\n'.format(
285 |                         json.dumps({'content': content}))
286 |             except GeneratorExit:
287 |                 pass
288 | 
289 |         return Response(gen(), mimetype='text/event-stream')
290 | 
291 |     def _render_rate_limit_page(self, exception=None):
292 |         """
293 |         Renders the rate limit page.
294 |         """
295 |         auth = request.args.get('auth')
296 |         is_auth = auth == '1' if auth else bool(self.auth)
297 |         return render_template('limit.html', is_authenticated=is_auth), 403
298 | 
299 |     def _download(self, url, binary=False):
300 |         if urlparse(url).netloc:
301 |             r = requests.get(url)
302 |             return r.content if binary else r.text
303 | 
304 |         with self.test_client() as c:
305 |             r = c.get(url)
306 |             charset = r.mimetype_params.get('charset', 'utf-8')
307 |             data = c.get(url).data
308 |             return data if binary else data.decode(charset)
309 | 
310 |     def _to_data_url(self, url, content_type):
311 |         asset = self._download(url, binary=True)
312 |         asset64_bytes = base64.b64encode(asset)
313 |         asset64_string = asset64_bytes.decode('ascii')
314 |         return 'data:{0};base64,{1}'.format(content_type, asset64_string)
315 | 
316 |     def _match_asset(self, match):
317 |         url = match.group(1)
318 |         ext = os.path.splitext(url)[1][1:]
319 |         return 'url({0})'.format(
320 |             self._to_data_url(url, 'font/' + ext))
321 | 
322 |     def _get_styles(self, style_urls, asset_url_path):
323 |         """
324 |         Gets the content of the given list of style URLs and
325 |         inlines assets.
326 |         """
327 |         styles = []
328 |         for style_url in style_urls:
329 |             urls_inline = STYLE_ASSET_URLS_INLINE_FORMAT.format(
330 |                 asset_url_path.rstrip('/'))
331 |             asset_content = self._download(style_url)
332 |             content = re.sub(urls_inline, self._match_asset, asset_content)
333 |             styles.append(content)
334 | 
335 |         return styles
336 | 
337 |     def _inline_styles(self):
338 |         """
339 |         Downloads the assets from the style URL list, clears it, and adds
340 |         each style with its embedded asset to the literal style list.
341 |         """
342 |         styles = self._get_styles(self.assets.style_urls, url_for('asset'))
343 |         self.assets.styles.extend(styles)
344 |         self.assets.style_urls[:] = []
345 | 
346 |     def _retrieve_styles(self):
347 |         """
348 |         Retrieves the style URLs from the source and caches them. This
349 |         is called before the first request is dispatched.
350 |         """
351 |         if self._styles_retrieved:
352 |             return
353 |         self._styles_retrieved = True
354 | 
355 |         try:
356 |             self.assets.retrieve_styles(url_for('asset'))
357 |         except Exception as ex:
358 |             if self.debug:
359 |                 print(format_exc(), file=sys.stderr)
360 |             else:
361 |                 print(' * Error: could not retrieve styles:', ex,
362 |                       file=sys.stderr)
363 |         if self.render_inline:
364 |             self._inline_styles()
365 | 
366 |     def default_renderer(self):
367 |         """
368 |         Returns the default renderer using the current config.
369 | 
370 |         This is only used if renderer is set to None in the constructor.
371 |         """
372 |         return GitHubRenderer(api_url=self.config['API_URL'])
373 | 
374 |     def default_asset_manager(self):
375 |         """
376 |         Returns the default asset manager using the current config.
377 | 
378 |         This is only used if asset_manager is set to None in the constructor.
379 |         """
380 |         cache_path = None
381 |         cache_directory = self.config['CACHE_DIRECTORY']
382 |         if cache_directory:
383 |             cache_directory = cache_directory.format(version=__version__)
384 |             cache_path = os.path.join(self.instance_path, cache_directory)
385 |         return GitHubAssetManager(
386 |             cache_path, self.config['STYLE_URLS'], self.quiet)
387 | 
388 |     def add_content_types(self):
389 |         """
390 |         Adds the application/x-font-woff and application/octet-stream
391 |         content types if they are missing.
392 | 
393 |         Override to add additional content types on initialization.
394 |         """
395 |         mimetypes.add_type('application/x-font-woff', '.woff')
396 |         mimetypes.add_type('application/octet-stream', '.ttf')
397 | 
398 |     def clear_cache(self):
399 |         self.assets.clear()
400 |         if not self.quiet:
401 |             print('Cache cleared.')
402 | 
403 |     def render(self, route=None):
404 |         """
405 |         Renders the application and returns the HTML unicode that would
406 |         normally appear when visiting in the browser.
407 |         """
408 |         if route is None:
409 |             route = '/'
410 |         with self.test_client() as c:
411 |             response = c.get(route, follow_redirects=True)
412 |             encoding = getattr(response, 'charset', 'utf-8')
413 |             return response.data.decode(encoding)
414 | 
415 |     def run(self, host=None, port=None, debug=None, use_reloader=None,
416 |             open_browser=False):
417 |         """
418 |         Starts a server to render the README.
419 |         """
420 |         if host is None:
421 |             host = self.config['HOST']
422 |         if port is None:
423 |             port = self.config['PORT']
424 |         if debug is None:
425 |             debug = self.debug
426 |         if use_reloader is None:
427 |             use_reloader = self.config['DEBUG_GRIP']
428 | 
429 |         # Verify the server is not already running and start
430 |         with self._run_mutex:
431 |             if self._shutdown_event:
432 |                 raise AlreadyRunningError()
433 |             self._shutdown_event = threading.Event()
434 | 
435 |         # Authentication message
436 |         if self.auth and not self.quiet:
437 |             if isinstance(self.auth, tuple):
438 |                 username, password = self.auth
439 |                 auth_method = ('credentials: {0}'.format(username)
440 |                                if username
441 |                                else 'personal access token')
442 |             else:
443 |                 auth_method = type(self.auth).__name__
444 |             print(' * Using', auth_method, file=sys.stderr)
445 | 
446 |         # Get random port manually when needed ahead of time
447 |         if port == 0 and open_browser:
448 |             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
449 |             sock.bind(('localhost', 0))
450 |             port = sock.getsockname()[1]
451 |             sock.close()
452 | 
453 |         # Open browser
454 |         browser_thread = (
455 |             start_browser_when_ready(host, port, self._shutdown_event)
456 |             if open_browser else None)
457 | 
458 |         # Run local server
459 |         super(Grip, self).run(host, port, debug=debug,
460 |                               use_reloader=use_reloader,
461 |                               threaded=True)
462 | 
463 |         # Signal to the polling and browser threads that they should exit
464 |         if not self.quiet:
465 |             print(' * Shutting down...')
466 |         self._shutdown_event.set()
467 | 
468 |         # Wait for browser thread to finish
469 |         if browser_thread:
470 |             browser_thread.join()
471 | 
472 |         # Cleanup
473 |         self._shutdown_event = None
474 | 


--------------------------------------------------------------------------------
/grip/assets.py:
--------------------------------------------------------------------------------
  1 | from __future__ import print_function, unicode_literals
  2 | 
  3 | import errno
  4 | import os
  5 | import posixpath
  6 | import re
  7 | import sys
  8 | import shutil
  9 | from abc import ABCMeta, abstractmethod
 10 | try:
 11 |     from urlparse import urljoin
 12 | except ImportError:
 13 |     from urllib.parse import urljoin
 14 | 
 15 | import requests
 16 | 
 17 | from ._compat import safe_join
 18 | 
 19 | from .constants import (
 20 |     STYLE_URLS_SOURCE, STYLE_URLS_RES, STYLE_ASSET_URLS_RE,
 21 |     STYLE_ASSET_URLS_SUB_FORMAT)
 22 | from .vendor.six import add_metaclass
 23 | 
 24 | 
 25 | @add_metaclass(ABCMeta)
 26 | class ReadmeAssetManager(object):
 27 |     """
 28 |     Manages the style and font assets rendered with Readme pages.
 29 | 
 30 |     Set cache_path to None to disable caching.
 31 |     """
 32 |     def __init__(self, cache_path, style_urls=None, quiet=None):
 33 |         super(ReadmeAssetManager, self).__init__()
 34 |         self.cache_path = cache_path
 35 |         self.style_urls = list(style_urls) if style_urls else []
 36 |         self.styles = []
 37 |         self.quiet = quiet
 38 | 
 39 |     def _strip_url_params(self, url):
 40 |         return url.rsplit('?', 1)[0].rsplit('#', 1)[0]
 41 | 
 42 |     def clear(self):
 43 |         """
 44 |         Clears the asset cache.
 45 |         """
 46 |         if self.cache_path and os.path.exists(self.cache_path):
 47 |             shutil.rmtree(self.cache_path)
 48 | 
 49 |     def cache_filename(self, url):
 50 |         """
 51 |         Gets a suitable relative filename for the specified URL.
 52 |         """
 53 |         # FUTURE: Use url exactly instead of flattening it here
 54 |         url = posixpath.basename(url)
 55 |         return self._strip_url_params(url)
 56 | 
 57 |     @abstractmethod
 58 |     def retrieve_styles(self, asset_url_path):
 59 |         """
 60 |         Get style URLs from the source HTML page and specified cached asset
 61 |         URL path.
 62 |         """
 63 |         pass
 64 | 
 65 | 
 66 | class GitHubAssetManager(ReadmeAssetManager):
 67 |     """
 68 |     Reads the styles used for rendering Readme pages.
 69 | 
 70 |     Set cache_path to None to disable caching.
 71 |     """
 72 |     def __init__(self, cache_path, style_urls=None, quiet=None):
 73 |         super(GitHubAssetManager, self).__init__(cache_path, style_urls, quiet)
 74 | 
 75 |     def _get_style_urls(self, asset_url_path):
 76 |         """
 77 |         Gets the specified resource and parses all style URLs and their
 78 |         assets in the form of the specified patterns.
 79 |         """
 80 |         # Check cache
 81 |         if self.cache_path:
 82 |             cached = self._get_cached_style_urls(asset_url_path)
 83 |             # Skip fetching styles if there's any already cached
 84 |             if cached:
 85 |                 return cached
 86 | 
 87 |         # Find style URLs
 88 |         r = requests.get(STYLE_URLS_SOURCE)
 89 |         if not 200 <= r.status_code < 300:
 90 |             print('Warning: retrieving styles gave status code',
 91 |                   r.status_code, file=sys.stderr)
 92 |         urls = []
 93 |         content = r.text
 94 |         for style_urls_re in STYLE_URLS_RES:
 95 |             print(re.findall(style_urls_re, content))
 96 |             urls.extend(re.findall(style_urls_re, content))
 97 |         if not urls:
 98 |             print('Warning: no styles found - see https://github.com/joeyespo/'
 99 |                   'grip/issues/265', file=sys.stderr)
100 | 
101 |         # Cache the styles and their assets
102 |         if self.cache_path:
103 |             is_cached = self._cache_contents(urls, asset_url_path)
104 |             if is_cached:
105 |                 urls = self._get_cached_style_urls(asset_url_path)
106 | 
107 |         return urls
108 | 
109 |     def _get_cached_style_urls(self, asset_url_path):
110 |         """
111 |         Gets the URLs of the cached styles.
112 |         """
113 |         try:
114 |             cached_styles = os.listdir(self.cache_path)
115 |         except IOError as ex:
116 |             if ex.errno != errno.ENOENT and ex.errno != errno.ESRCH:
117 |                 raise
118 |             return []
119 |         except OSError:
120 |             return []
121 |         return [posixpath.join(asset_url_path, style)
122 |                 for style in cached_styles
123 |                 if style.endswith('.css')]
124 | 
125 |     def _cache_contents(self, style_urls, asset_url_path):
126 |         """
127 |         Fetches the given URLs and caches their contents
128 |         and their assets in the given directory.
129 |         """
130 |         files = {}
131 | 
132 |         asset_urls = []
133 |         for style_url in style_urls:
134 |             if not self.quiet:
135 |                 print(' * Downloading style', style_url, file=sys.stderr)
136 |             r = requests.get(style_url)
137 |             if not 200 <= r.status_code < 300:
138 |                 print(' -> Warning: Style request responded with',
139 |                       r.status_code, file=sys.stderr)
140 |                 files = None
141 |                 continue
142 |             asset_content = r.text
143 |             # Find assets and replace their base URLs with the cache directory
144 |             for url in re.findall(STYLE_ASSET_URLS_RE, asset_content):
145 |                 asset_urls.append(urljoin(style_url, url))
146 |             contents = re.sub(
147 |                 STYLE_ASSET_URLS_RE,
148 |                 STYLE_ASSET_URLS_SUB_FORMAT.format(asset_url_path.rstrip('/')),
149 |                 asset_content)
150 |             # Prepare cache
151 |             if files is not None:
152 |                 filename = self.cache_filename(style_url)
153 |                 files[filename] = contents.encode('utf-8')
154 | 
155 |         for asset_url in asset_urls:
156 |             if not self.quiet:
157 |                 print(' * Downloading asset', asset_url, file=sys.stderr)
158 |             # Retrieve binary file and show message
159 |             r = requests.get(asset_url, stream=True)
160 |             if not 200 <= r.status_code < 300:
161 |                 print(' -> Warning: Asset request responded with',
162 |                       r.status_code, file=sys.stderr)
163 |                 files = None
164 |                 continue
165 |             # Prepare cache
166 |             if files is not None:
167 |                 filename = self.cache_filename(asset_url)
168 |                 files[filename] = r.raw.read(decode_content=True)
169 | 
170 |         # Skip caching if something went wrong to try again next time
171 |         if not files:
172 |             return False
173 | 
174 |         # Cache files if all downloads were successful
175 |         cache = {}
176 |         for relname in files:
177 |             cache[safe_join(self.cache_path, relname)] = files[relname]
178 |         if not os.path.exists(self.cache_path):
179 |             os.makedirs(self.cache_path)
180 |         for filename in cache:
181 |             with open(filename, 'wb') as f:
182 |                 f.write(cache[filename])
183 |         if not self.quiet:
184 |             print(
185 |                 ' * Cached all downloads in', self.cache_path, file=sys.stderr)
186 |         return True
187 | 
188 |     def retrieve_styles(self, asset_url_path):
189 |         """
190 |         Get style URLs from the source HTML page and specified cached
191 |         asset base URL.
192 |         """
193 |         if not asset_url_path.endswith('/'):
194 |             asset_url_path += '/'
195 |         self.style_urls.extend(self._get_style_urls(asset_url_path))
196 | 


--------------------------------------------------------------------------------
/grip/browser.py:
--------------------------------------------------------------------------------
 1 | import socket
 2 | import time
 3 | import webbrowser
 4 | from threading import Thread
 5 | 
 6 | 
 7 | def is_server_running(host, port):
 8 |     """
 9 |     Checks whether a server is currently listening on the specified
10 |     host and port.
11 |     """
12 |     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
13 |     try:
14 |         return s.connect_ex((host, port)) == 0
15 |     finally:
16 |         s.close()
17 | 
18 | 
19 | def wait_for_server(host, port, cancel_event=None):
20 |     """
21 |     Blocks until a local server is listening on the specified
22 |     host and port. Set cancel_event to cancel the wait.
23 | 
24 |     This is intended to be used in conjunction with running
25 |     the Flask server.
26 |     """
27 |     while not is_server_running(host, port):
28 |         # Stop waiting if shutting down
29 |         if cancel_event and cancel_event.is_set():
30 |             return False
31 |         time.sleep(0.1)
32 |     return True
33 | 
34 | 
35 | def start_browser(url):
36 |     """
37 |     Opens the specified URL in a new browser window.
38 |     """
39 |     try:
40 |         webbrowser.open(url)
41 |     except Exception:
42 |         pass
43 | 
44 | 
45 | def wait_and_start_browser(host, port=None, cancel_event=None):
46 |     """
47 |     Waits for the server to run and then opens the specified address in
48 |     the browser. Set cancel_event to cancel the wait.
49 |     """
50 |     if host == '0.0.0.0':
51 |         host = 'localhost'
52 |     if port is None:
53 |         port = 80
54 | 
55 |     if wait_for_server(host, port, cancel_event):
56 |         start_browser('http://{0}:{1}/'.format(host, port))
57 | 
58 | 
59 | def start_browser_when_ready(host, port=None, cancel_event=None):
60 |     """
61 |     Starts a thread that waits for the server then opens the specified
62 |     address in the browser. Set cancel_event to cancel the wait. The
63 |     started thread object is returned.
64 |     """
65 |     browser_thread = Thread(
66 |         target=wait_and_start_browser, args=(host, port, cancel_event))
67 |     browser_thread.daemon = True
68 |     browser_thread.start()
69 |     return browser_thread
70 | 


--------------------------------------------------------------------------------
/grip/command.py:
--------------------------------------------------------------------------------
  1 | """\
  2 | grip.command
  3 | ~~~~~~~~~~~~
  4 | 
  5 | Implements the command-line interface for Grip.
  6 | 
  7 | 
  8 | Usage:
  9 |   grip [options] [<path>] [<address>]
 10 |   grip -V | --version
 11 |   grip -h | --help
 12 | 
 13 | Where:
 14 |   <path> is a file to render or a directory containing README.md (- for stdin)
 15 |   <address> is what to listen on, of the form <host>[:<port>], or just <port>
 16 | 
 17 | Options:
 18 |   --user-content    Render as user-content like comments or issues.
 19 |   --context=<repo>  The repository context, only taken into account
 20 |                     when using --user-content.
 21 |   --user=<username> A GitHub username for API authentication. If used
 22 |                     without the --pass option, an upcoming password
 23 |                     input will be necessary.
 24 |   --pass=<password> A GitHub password or auth token for API auth.
 25 |   --wide            Renders wide, i.e. when the side nav is collapsed.
 26 |                     This only takes effect when --user-content is used.
 27 |   --clear           Clears the cached styles and assets and exits.
 28 |   --export          Exports to <path>.html or README.md instead of
 29 |                     serving, optionally using [<address>] as the out
 30 |                     file (- for stdout).
 31 |   --no-inline       Link to styles instead inlining when using --export.
 32 |   -b --browser      Open a tab in the browser after the server starts.
 33 |   --api-url=<url>   Specify a different base URL for the github API,
 34 |                     for example that of a Github Enterprise instance.
 35 |                     Default is the public API: https://api.github.com
 36 |   --title=<title>   Manually sets the page's title.
 37 |                     The default is the filename.
 38 |   --norefresh       Do not automatically refresh the Readme content when
 39 |                     the file changes.
 40 |   --quiet           Do not print to the terminal.
 41 |   --theme=<theme>   Theme to view markdown file (light mode or dark mode).
 42 |                     Valid options ("light", "dark"). Default: "light"
 43 | """
 44 | 
 45 | from __future__ import print_function
 46 | 
 47 | import sys
 48 | import mimetypes
 49 | import socket
 50 | import errno
 51 | 
 52 | from docopt import docopt
 53 | from getpass import getpass
 54 | from path_and_address import resolve, split_address
 55 | 
 56 | from . import __version__
 57 | from .api import clear_cache, export, serve
 58 | from .exceptions import ReadmeNotFoundError
 59 | 
 60 | 
 61 | usage = '\n\n\n'.join(__doc__.split('\n\n\n')[1:])
 62 | version = 'Grip ' + __version__
 63 | 
 64 | # Note: GitHub supports more than light mode and dark mode (exp: light-high-constrast, dark-high-constrast).
 65 | VALID_THEME_OPTIONS = ['light', 'dark']
 66 | 
 67 | def main(argv=None, force_utf8=True, patch_svg=True):
 68 |     """
 69 |     The entry point of the application.
 70 |     """
 71 |     if force_utf8 and sys.version_info[0] == 2:
 72 |         reload(sys)  # noqa
 73 |         sys.setdefaultencoding('utf-8')
 74 |     if patch_svg and sys.version_info[0] == 2 and sys.version_info[1] <= 6:
 75 |         mimetypes.add_type('image/svg+xml', '.svg')
 76 | 
 77 |     if argv is None:
 78 |         argv = sys.argv[1:]
 79 | 
 80 |     # Show specific errors
 81 |     if '-a' in argv or '--address' in argv:
 82 |         print('Use grip [options] <path> <address> instead of -a')
 83 |         print('See grip -h for details')
 84 |         return 2
 85 |     if '-p' in argv or '--port' in argv:
 86 |         print('Use grip [options] [<path>] [<hostname>:]<port> instead of -p')
 87 |         print('See grip -h for details')
 88 |         return 2
 89 | 
 90 |     # Parse options
 91 |     args = docopt(usage, argv=argv, version=version)
 92 | 
 93 |     # Handle printing version with -V (docopt handles --version)
 94 |     if args['-V']:
 95 |         print(version)
 96 |         return 0
 97 | 
 98 |     # Clear the cache
 99 |     if args['--clear']:
100 |         clear_cache()
101 |         return 0
102 | 
103 |     # Get password from prompt if necessary
104 |     password = args['--pass']
105 |     if args['--user'] and not password:
106 |         password = getpass()
107 | 
108 |     # Parse theme argument
109 |     if args['--theme']:
110 |         if args['--theme'] in VALID_THEME_OPTIONS:
111 |             theme: str = args['--theme']
112 |         else:
113 |             print('Error: valid options for theme argument are "light", "dark"')
114 |             return 1
115 |     else:
116 |         theme = 'light'
117 | 
118 |     # Export to a file instead of running a server
119 |     if args['--export']:
120 |         try:
121 |             export(args['<path>'], args['--user-content'], args['--context'],
122 |                    args['--user'], password, False, args['--wide'],
123 |                    not args['--no-inline'], args['<address>'],
124 |                    args['--api-url'], args['--title'], args['--quiet'], theme)
125 |             return 0
126 |         except ReadmeNotFoundError as ex:
127 |             print('Error:', ex)
128 |             return 1
129 | 
130 |     # Parse arguments
131 |     path, address = resolve(args['<path>'], args['<address>'])
132 |     host, port = split_address(address)
133 | 
134 |     # Validate address
135 |     if address and not host and port is None:
136 |         print('Error: Invalid address', repr(address))
137 | 
138 |     # Run server
139 |     try:
140 |         serve(path, host, port, args['--user-content'], args['--context'],
141 |               args['--user'], password, False, args['--wide'], False,
142 |               args['--api-url'], args['--title'], not args['--norefresh'],
143 |               args['--browser'], args['--quiet'], theme, None)
144 |         return 0
145 |     except ReadmeNotFoundError as ex:
146 |         print('Error:', ex)
147 |         return 1
148 |     except socket.error as ex:
149 |         print('Error:', ex)
150 |         if ex.errno == errno.EADDRINUSE:
151 |             print('This port is in use. Is a grip server already running? '
152 |                   'Stop that instance or specify another port here.')
153 |         return 1
154 | 


--------------------------------------------------------------------------------
/grip/constants.py:
--------------------------------------------------------------------------------
 1 | # The common titles and supported extensions,
 2 | # as defined by https://github.com/github/markup
 3 | SUPPORTED_TITLES = ['README', 'Readme', 'readme', 'Home']
 4 | SUPPORTED_EXTENSIONS = ['.md', '.markdown']
 5 | 
 6 | 
 7 | # The default filenames when no file is provided
 8 | DEFAULT_FILENAMES = [title + ext
 9 |                      for title in SUPPORTED_TITLES
10 |                      for ext in SUPPORTED_EXTENSIONS]
11 | DEFAULT_FILENAME = DEFAULT_FILENAMES[0]
12 | 
13 | 
14 | # The default directory to load Grip settings from
15 | DEFAULT_GRIPHOME = '~/.grip'
16 | 
17 | 
18 | # The default URL of the Grip server
19 | DEFAULT_GRIPURL = '/__/grip'
20 | 
21 | 
22 | # The public GitHub API
23 | DEFAULT_API_URL = 'https://api.github.com'
24 | 
25 | 
26 | # Style parsing
27 | STYLE_URLS_SOURCE = 'https://github.com/joeyespo/grip'
28 | # Note: Using a list in case the implementation limitation is a problem
29 | # https://docs.python.org/3/library/re.html#re.findall
30 | STYLE_URLS_RES = [
31 |     r'''<link\b[^>]+\bhref=['"]?([^'" >]+)['"]?\brel=['"]?stylesheet['"]?[^>]+[^>]*(?=>)''',
32 |     r'''<link\b[^>]+\brel=['"]?stylesheet['"]?[^>]+\bhref=['"]?([^'" >]+)['"]?[^>]*(?=>)''',
33 | ]
34 | STYLE_ASSET_URLS_RE = (
35 |     r'''url\(['"]?(/static/fonts/octicons/[^'" \)]+)['"]?\)''')
36 | STYLE_ASSET_URLS_SUB_FORMAT = r'url("{0}\1")'
37 | STYLE_ASSET_URLS_INLINE_FORMAT = (
38 |     r'''url\(['"]?((?:/static|{0})/[^'" \)]+)['"]?\)''')
39 | 


--------------------------------------------------------------------------------
/grip/exceptions.py:
--------------------------------------------------------------------------------
 1 | import errno
 2 | try:
 3 |     NotFoundError = FileNotFoundError
 4 | except NameError:
 5 |     NotFoundError = IOError
 6 | 
 7 | 
 8 | class AlreadyRunningError(RuntimeError):
 9 |     pass
10 | 
11 | 
12 | class ReadmeNotFoundError(NotFoundError):
13 |     """
14 |     This class inherits from FileNotFoundError on Python 3 and above.
15 |     """
16 |     def __init__(self, path=None, message=None):
17 |         self.path = path
18 |         self.message = message
19 |         super(ReadmeNotFoundError, self).__init__(
20 |             errno.ENOENT, 'README not found', path)
21 | 
22 |     def __repr__(self):
23 |         return '{0}({!r}, {!r})'.format(
24 |             type(self).__name__, self.path, self.message)
25 | 
26 |     def __str__(self):
27 |         if self.message:
28 |             return self.message
29 | 
30 |         if self.path is not None:
31 |             return 'No README found at {0}'.format(self.path)
32 | 
33 |         return self.strerror
34 | 


--------------------------------------------------------------------------------
/grip/patcher.py:
--------------------------------------------------------------------------------
 1 | import re
 2 | 
 3 | 
 4 | INCOMPLETE_TASK_RE = re.compile(r'<li>\[ \] (.*?)(<ul.*?>|</li>)', re.DOTALL)
 5 | INCOMPLETE_TASK_SUB = (r'<li class="task-list-item">'
 6 |                        r'<input type="checkbox" '
 7 |                        r'class="task-list-item-checkbox" disabled=""> \1\2')
 8 | COMPLETE_TASK_RE = re.compile(r'<li>\[x\] (.*?)(<ul.*?>|</li>)', re.DOTALL)
 9 | COMPLETE_TASK_SUB = (r'<li class="task-list-item">'
10 |                      r'<input type="checkbox" class="task-list-item-checkbox" '
11 |                      r'checked="" disabled=""> \1\2')
12 | 
13 | 
14 | HEADER_PATCH_RE = re.compile(r'<span>{:"aria-hidden"=&gt;"true", :class=&gt;'
15 |                              r'"octicon octicon-link"}</span>', re.DOTALL)
16 | HEADER_PATCH_SUB = r'<span class="octicon octicon-link"></span>'
17 | 
18 | 
19 | def patch(html, user_content=False):
20 |     """
21 |     Processes the HTML rendered by the GitHub API, patching
22 |     any inconsistencies from the main site.
23 |     """
24 |     # FUTURE: Remove this once GitHub API renders task lists
25 |     # https://github.com/isaacs/github/issues/309
26 |     if not user_content:
27 |         html = INCOMPLETE_TASK_RE.sub(INCOMPLETE_TASK_SUB, html)
28 |         html = COMPLETE_TASK_RE.sub(COMPLETE_TASK_SUB, html)
29 | 
30 |     # FUTURE: Remove this once GitHub API fixes the header bug
31 |     # https://github.com/joeyespo/grip/issues/244
32 |     html = HEADER_PATCH_RE.sub(HEADER_PATCH_SUB, html)
33 | 
34 |     return html
35 | 


--------------------------------------------------------------------------------
/grip/readers.py:
--------------------------------------------------------------------------------
  1 | from __future__ import print_function, unicode_literals
  2 | 
  3 | import errno
  4 | import io
  5 | import mimetypes
  6 | import os
  7 | import posixpath
  8 | import sys
  9 | from abc import ABCMeta, abstractmethod
 10 | 
 11 | from ._compat import safe_join
 12 | 
 13 | from .constants import DEFAULT_FILENAMES, DEFAULT_FILENAME
 14 | from .exceptions import ReadmeNotFoundError
 15 | from .vendor.six import add_metaclass
 16 | 
 17 | 
 18 | @add_metaclass(ABCMeta)
 19 | class ReadmeReader(object):
 20 |     """
 21 |     Reads Readme content from a URL subpath.
 22 |     """
 23 |     def __init__(self):
 24 |         super(ReadmeReader, self).__init__()
 25 | 
 26 |     def normalize_subpath(self, subpath):
 27 |         """
 28 |         Returns the normalized subpath.
 29 | 
 30 |         This allows Readme files to be inferred from directories while
 31 |         still allowing relative paths to work properly.
 32 | 
 33 |         Override to change the default behavior of returning the
 34 |         specified subpath as-is.
 35 |         """
 36 |         if subpath is None:
 37 |             return None
 38 | 
 39 |         return posixpath.normpath(subpath)
 40 | 
 41 |     def filename_for(self, subpath):
 42 |         """
 43 |         Returns the relative filename for the specified subpath, or None
 44 |         if the file does not exist.
 45 |         """
 46 |         return None
 47 | 
 48 |     def mimetype_for(self, subpath=None):
 49 |         """
 50 |         Gets the mimetype for the specified subpath.
 51 |         """
 52 |         if subpath is None:
 53 |             subpath = DEFAULT_FILENAME
 54 |         mimetype, _ = mimetypes.guess_type(subpath)
 55 |         return mimetype
 56 | 
 57 |     def is_binary(self, subpath=None):
 58 |         """
 59 |         Gets whether the specified subpath is a supported binary file.
 60 |         """
 61 |         return False
 62 | 
 63 |     def last_updated(self, subpath=None):
 64 |         """
 65 |         Returns the time of the last modification of the Readme or
 66 |         specified subpath. None is returned if the reader doesn't
 67 |         support modification tracking.
 68 | 
 69 |         The format of return value is dependent on the implementing
 70 |         reader. It can be any object as long as equality indicates
 71 |         that the content was not updated.
 72 |         """
 73 |         return None
 74 | 
 75 |     @abstractmethod
 76 |     def read(self, subpath=None):
 77 |         """
 78 |         Returns the UTF-8 content of the specified subpath, or None if
 79 |         subpath does not exist.
 80 |         """
 81 |         pass
 82 | 
 83 | 
 84 | class DirectoryReader(ReadmeReader):
 85 |     """
 86 |     Reads Readme files from URL subpaths.
 87 |     """
 88 |     def __init__(self, path=None, silent=False):
 89 |         super(DirectoryReader, self).__init__()
 90 |         root_filename = os.path.abspath(self._resolve_readme(path, silent))
 91 |         self.root_filename = root_filename
 92 |         self.root_directory = os.path.dirname(root_filename)
 93 | 
 94 |     def _find_file(self, path, silent=False):
 95 |         """
 96 |         Gets the full path and extension, or None if a README file could not
 97 |         be found at the specified path.
 98 |         """
 99 |         for filename in DEFAULT_FILENAMES:
100 |             full_path = os.path.join(path, filename) if path else filename
101 |             if os.path.exists(full_path):
102 |                 return full_path
103 | 
104 |         # Return default filename if silent
105 |         if silent:
106 |             return os.path.join(path, DEFAULT_FILENAME)
107 | 
108 |         raise ReadmeNotFoundError(path)
109 | 
110 |     def _resolve_readme(self, path=None, silent=False):
111 |         """
112 |         Returns the path if it's a file; otherwise, looks for a compatible
113 |         README file in the directory specified by path.
114 | 
115 |         If path is None, the current working directory is used.
116 | 
117 |         If silent is set, the default relative filename will be returned
118 |         if path is a directory or None if it does not exist.
119 | 
120 |         Raises ReadmeNotFoundError if no compatible README file can be
121 |         found and silent is False.
122 |         """
123 |         # Default to current working directory
124 |         if path is None:
125 |             path = '.'
126 | 
127 |         # Normalize the path
128 |         path = os.path.normpath(path)
129 | 
130 |         # Resolve README file if path is a directory
131 |         if os.path.isdir(path):
132 |             return self._find_file(path, silent)
133 | 
134 |         # Return path if file exists or if silent
135 |         if silent or os.path.exists(path):
136 |             return path
137 | 
138 |         raise ReadmeNotFoundError(path, 'File not found: ' + path)
139 | 
140 |     def _read_text(self, filename):
141 |         """
142 |         Helper that reads the UTF-8 content of the specified file, or
143 |         None if the file doesn't exist. This returns a unicode string.
144 |         """
145 |         with io.open(filename, 'rt', encoding='utf-8') as f:
146 |             return f.read()
147 | 
148 |     def _read_binary(self, filename):
149 |         """
150 |         Helper that reads the binary content of the specified file, or
151 |         None if the file doesn't exist. This returns a byte string.
152 |         """
153 |         with io.open(filename, 'rb') as f:
154 |             return f.read()
155 | 
156 |     def normalize_subpath(self, subpath):
157 |         """
158 |         Normalizes the specified subpath, or None if subpath is None.
159 | 
160 |         This allows Readme files to be inferred from directories while
161 |         still allowing relative paths to work properly.
162 | 
163 |         Raises werkzeug.exceptions.NotFound if the resulting path
164 |         would fall out of the root directory.
165 |         """
166 |         if subpath is None:
167 |             return None
168 | 
169 |         # Normalize the subpath
170 |         subpath = posixpath.normpath(subpath)
171 | 
172 |         # Add or remove trailing slash to properly support relative links
173 |         filename = os.path.normpath(safe_join(self.root_directory, subpath))
174 |         if os.path.isdir(filename):
175 |             subpath += '/'
176 | 
177 |         return subpath
178 | 
179 |     def readme_for(self, subpath):
180 |         """
181 |         Returns the full path for the README file for the specified
182 |         subpath, or the root filename if subpath is None.
183 | 
184 |         Raises ReadmeNotFoundError if a README for the specified subpath
185 |         does not exist.
186 | 
187 |         Raises werkzeug.exceptions.NotFound if the resulting path
188 |         would fall out of the root directory.
189 |         """
190 |         if subpath is None:
191 |             return self.root_filename
192 | 
193 |         # Join for safety and to convert subpath to normalized OS-specific path
194 |         filename = os.path.normpath(safe_join(self.root_directory, subpath))
195 | 
196 |         # Check for existence
197 |         if not os.path.exists(filename):
198 |             raise ReadmeNotFoundError(filename)
199 | 
200 |         # Resolve README file if path is a directory
201 |         if os.path.isdir(filename):
202 |             return self._find_file(filename)
203 | 
204 |         return filename
205 | 
206 |     def filename_for(self, subpath):
207 |         """
208 |         Returns the relative filename for the specified subpath, or the
209 |         root filename if subpath is None.
210 | 
211 |         Raises werkzeug.exceptions.NotFound if the resulting path
212 |         would fall out of the root directory.
213 |         """
214 |         try:
215 |             filename = self.readme_for(subpath)
216 |             return os.path.relpath(filename, self.root_directory)
217 |         except ReadmeNotFoundError:
218 |             return None
219 | 
220 |     def is_binary(self, subpath=None):
221 |         """
222 |         Gets whether the specified subpath is a supported binary file.
223 |         """
224 |         mimetype = self.mimetype_for(subpath)
225 |         return mimetype and not mimetype.startswith('text/')
226 | 
227 |     def last_updated(self, subpath=None):
228 |         """
229 |         Returns the time of the last modification of the Readme or
230 |         specified subpath, or None if the file does not exist.
231 | 
232 |         The return value is a number giving the number of seconds since
233 |         the epoch (see the time module).
234 | 
235 |         Raises werkzeug.exceptions.NotFound if the resulting path
236 |         would fall out of the root directory.
237 |         """
238 |         try:
239 |             return os.path.getmtime(self.readme_for(subpath))
240 |         except ReadmeNotFoundError:
241 |             return None
242 |         # OSError for Python 3 base class, EnvironmentError for Python 2
243 |         except (OSError, EnvironmentError) as ex:
244 |             if ex.errno == errno.ENOENT:
245 |                 return None
246 |             raise
247 | 
248 |     def read(self, subpath=None):
249 |         """
250 |         Returns the UTF-8 content of the specified subpath.
251 | 
252 |         subpath is expected to already have been normalized.
253 | 
254 |         Raises ReadmeNotFoundError if a README for the specified subpath
255 |         does not exist.
256 | 
257 |         Raises werkzeug.exceptions.NotFound if the resulting path
258 |         would fall out of the root directory.
259 |         """
260 |         is_binary = self.is_binary(subpath)
261 |         filename = self.readme_for(subpath)
262 |         try:
263 |             if is_binary:
264 |                 return self._read_binary(filename)
265 |             return self._read_text(filename)
266 |         # OSError for Python 3 base class, EnvironmentError for Python 2
267 |         except (OSError, EnvironmentError) as ex:
268 |             if ex.errno == errno.ENOENT:
269 |                 raise ReadmeNotFoundError(filename)
270 |             raise
271 | 
272 | 
273 | class TextReader(ReadmeReader):
274 |     """
275 |     Reads Readme content from the provided unicode string.
276 |     """
277 |     def __init__(self, text, display_filename=None):
278 |         super(TextReader, self).__init__()
279 |         self.text = text
280 |         self.display_filename = display_filename
281 | 
282 |     def filename_for(self, subpath):
283 |         """
284 |         Returns the display filename, or None if subpath is specified
285 |         since subpaths are not supported for text readers.
286 |         """
287 |         if subpath is not None:
288 |             return None
289 | 
290 |         return self.display_filename
291 | 
292 |     def read(self, subpath=None):
293 |         """
294 |         Returns the UTF-8 Readme content.
295 | 
296 |         Raises ReadmeNotFoundError if subpath is specified since
297 |         subpaths are not supported for text readers.
298 |         """
299 |         if subpath is not None:
300 |             raise ReadmeNotFoundError(subpath)
301 | 
302 |         return self.text
303 | 
304 | 
305 | class StdinReader(TextReader):
306 |     """
307 |     Reads Readme text from STDIN.
308 |     """
309 |     def __init__(self, display_filename=None):
310 |         super(StdinReader, self).__init__(None, display_filename)
311 | 
312 |     def read(self, subpath=None):
313 |         """
314 |         Returns the UTF-8 Readme content.
315 | 
316 |         Raises ReadmeNotFoundError if subpath is specified since
317 |         subpaths are not supported for text readers.
318 |         """
319 |         # Lazily read STDIN
320 |         if self.text is None and subpath is None:
321 |             self.text = self.read_stdin()
322 | 
323 |         return super(StdinReader, self).read(subpath)
324 | 
325 |     def read_stdin(self):
326 |         """
327 |         Reads STDIN until the end of input and returns a unicode string.
328 |         """
329 |         text = sys.stdin.read()
330 | 
331 |         # Decode the bytes returned from earlier Python STDIN implementations
332 |         if sys.version_info[0] < 3 and text is not None:
333 |             text = text.decode(sys.stdin.encoding or 'utf-8')
334 | 
335 |         return text
336 | 


--------------------------------------------------------------------------------
/grip/renderers.py:
--------------------------------------------------------------------------------
  1 | from __future__ import print_function, unicode_literals
  2 | 
  3 | import json
  4 | import sys
  5 | from abc import ABCMeta, abstractmethod
  6 | 
  7 | import requests
  8 | 
  9 | try:
 10 |     import markdown
 11 |     from .vendor.mdx_urlize import UrlizeExtension
 12 | except ImportError:
 13 |     markdown = None
 14 |     UrlizeExtension = None
 15 | 
 16 | from .constants import DEFAULT_API_URL
 17 | from .patcher import patch
 18 | from .vendor.six import add_metaclass
 19 | 
 20 | 
 21 | @add_metaclass(ABCMeta)
 22 | class ReadmeRenderer(object):
 23 |     """
 24 |     Renders the Readme.
 25 |     """
 26 |     def __init__(self, user_content=None, context=None):
 27 |         if user_content is None:
 28 |             user_content = False
 29 |         super(ReadmeRenderer, self).__init__()
 30 |         self.user_content = user_content
 31 |         self.context = context
 32 | 
 33 |     @abstractmethod
 34 |     def render(self, text, auth=None):
 35 |         """
 36 |         Renders the specified markdown content and embedded styles.
 37 |         """
 38 |         pass
 39 | 
 40 | 
 41 | class GitHubRenderer(ReadmeRenderer):
 42 |     """
 43 |     Renders the specified Readme using the GitHub Markdown API.
 44 |     """
 45 |     def __init__(self, user_content=None, context=None, api_url=None,
 46 |                  raw=None):
 47 |         if api_url is None:
 48 |             api_url = DEFAULT_API_URL
 49 |         super(GitHubRenderer, self).__init__(user_content, context)
 50 |         self.api_url = api_url
 51 |         self.raw = raw
 52 | 
 53 |     def render(self, text, auth=None):
 54 |         """
 55 |         Renders the specified markdown content and embedded styles.
 56 | 
 57 |         Raises TypeError if text is not a Unicode string.
 58 |         Raises requests.HTTPError if the request fails.
 59 |         """
 60 |         # Ensure text is Unicode
 61 |         expected = str if sys.version_info[0] >= 3 else unicode  # noqa
 62 |         if not isinstance(text, expected):
 63 |             raise TypeError(
 64 |                 'Expected a Unicode string, got {!r}.'.format(text))
 65 | 
 66 |         if self.user_content:
 67 |             url = '{0}/markdown'.format(self.api_url)
 68 |             data = {'text': text, 'mode': 'gfm'}
 69 |             if self.context:
 70 |                 data['context'] = self.context
 71 |             data = json.dumps(data, ensure_ascii=False).encode('utf-8')
 72 |             headers = {'content-type': 'application/json; charset=UTF-8'}
 73 |         else:
 74 |             url = '{0}/markdown/raw'.format(self.api_url)
 75 |             data = text.encode('utf-8')
 76 |             headers = {'content-type': 'text/x-markdown; charset=UTF-8'}
 77 | 
 78 |         r = requests.post(url, headers=headers, data=data, auth=auth)
 79 |         r.raise_for_status()
 80 | 
 81 |         # FUTURE: Remove this once GitHub API properly handles Unicode markdown
 82 |         r.encoding = 'utf-8'
 83 | 
 84 |         return r.text if self.raw else patch(r.text)
 85 | 
 86 | 
 87 | class OfflineRenderer(ReadmeRenderer):
 88 |     """
 89 |     Renders the specified Readme locally using pure Python.
 90 | 
 91 |     Note: This is currently an incomplete feature.
 92 |     """
 93 |     def __init__(self, user_content=None, context=None):
 94 |         super(OfflineRenderer, self).__init__(user_content, context)
 95 | 
 96 |     def render(self, text, auth=None):
 97 |         """
 98 |         Renders the specified markdown content and embedded styles.
 99 |         """
100 |         if markdown is None:
101 |             import markdown
102 |         if UrlizeExtension is None:
103 |             from .mdx_urlize import UrlizeExtension
104 |         return markdown.markdown(text, extensions=[
105 |             'fenced_code',
106 |             'codehilite(css_class=highlight)',
107 |             'toc',
108 |             'tables',
109 |             'sane_lists',
110 |             UrlizeExtension(),
111 |         ])
112 | 


--------------------------------------------------------------------------------
/grip/settings.py:
--------------------------------------------------------------------------------
 1 | """\
 2 | Default Configuration
 3 | 
 4 | Do NOT change the values here for risk of accidentally committing them.
 5 | Override them using command-line arguments or with a settings_local.py in
 6 | this directory or in ~/.grip/settings.py instead.
 7 | """
 8 | 
 9 | 
10 | HOST = 'localhost'
11 | PORT = 6419
12 | DEBUG = False
13 | DEBUG_GRIP = False
14 | CACHE_DIRECTORY = 'cache-{version}'
15 | AUTOREFRESH = True
16 | QUIET = False
17 | 
18 | 
19 | # Note: For security concerns, please don't save your GitHub password in your
20 | # local settings.py. Use a personal access token instead:
21 | # https://github.com/settings/tokens/new?scopes=
22 | USERNAME = None
23 | PASSWORD = None
24 | 
25 | 
26 | # Custom GitHub API
27 | API_URL = None
28 | 
29 | 
30 | # Custom styles
31 | STYLE_URLS = []
32 | 


--------------------------------------------------------------------------------
/grip/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/grip/static/favicon.ico


--------------------------------------------------------------------------------
/grip/static/octicons/octicons.css:
--------------------------------------------------------------------------------
  1 | 
  2 | @font-face {
  3 |   font-family:"Octicons";
  4 |   src:url("octicons.eot?ef21c39f0ca9b1b5116e5eb7ac5eabe6");
  5 |   src:url("octicons.eot?#iefix") format("embedded-opentype"),
  6 | 		url("octicons.woff2?ef21c39f0ca9b1b5116e5eb7ac5eabe6") format("woff2"),
  7 | 		url("octicons.woff?ef21c39f0ca9b1b5116e5eb7ac5eabe6") format("woff"),
  8 | 		url("octicons.ttf?ef21c39f0ca9b1b5116e5eb7ac5eabe6") format("truetype"),
  9 | 		url("octicons.svg?ef21c39f0ca9b1b5116e5eb7ac5eabe6#octicons") format("svg");
 10 |   font-weight:normal;
 11 |   font-style:normal;
 12 | }
 13 | 
 14 | 
 15 | /*
 16 | 
 17 | .octicon is optimized for 16px.
 18 | .mega-octicon is optimized for 32px but can be used larger.
 19 | 
 20 | */
 21 | .octicon, .mega-octicon {
 22 |   font: normal normal normal 16px/1 Octicons;
 23 |   display: inline-block;
 24 |   text-decoration: none;
 25 |   text-rendering: auto;
 26 |   -webkit-font-smoothing: antialiased;
 27 |   -moz-osx-font-smoothing: grayscale;
 28 |   -webkit-user-select: none;
 29 |   -ms-user-select: none;
 30 |   user-select: none;
 31 |   speak: none;
 32 | }
 33 | .mega-octicon { font-size: 32px; }
 34 | 
 35 | .octicon-alert:before { content:"\f02d"; }
 36 | 
 37 | .octicon-arrow-down:before { content:"\f03f"; }
 38 | 
 39 | .octicon-arrow-left:before { content:"\f040"; }
 40 | 
 41 | .octicon-arrow-right:before { content:"\f03e"; }
 42 | 
 43 | .octicon-arrow-small-down:before { content:"\f0a0"; }
 44 | 
 45 | .octicon-arrow-small-left:before { content:"\f0a1"; }
 46 | 
 47 | .octicon-arrow-small-right:before { content:"\f071"; }
 48 | 
 49 | .octicon-arrow-small-up:before { content:"\f09f"; }
 50 | 
 51 | .octicon-arrow-up:before { content:"\f03d"; }
 52 | 
 53 | .octicon-beaker:before { content:"\f0dd"; }
 54 | 
 55 | .octicon-bell:before { content:"\f0de"; }
 56 | 
 57 | .octicon-bold:before { content:"\f0e2"; }
 58 | 
 59 | .octicon-book:before { content:"\f007"; }
 60 | 
 61 | .octicon-bookmark:before { content:"\f07b"; }
 62 | 
 63 | .octicon-briefcase:before { content:"\f0d3"; }
 64 | 
 65 | .octicon-broadcast:before { content:"\f048"; }
 66 | 
 67 | .octicon-browser:before { content:"\f0c5"; }
 68 | 
 69 | .octicon-bug:before { content:"\f091"; }
 70 | 
 71 | .octicon-calendar:before { content:"\f068"; }
 72 | 
 73 | .octicon-check:before { content:"\f03a"; }
 74 | 
 75 | .octicon-checklist:before { content:"\f076"; }
 76 | 
 77 | .octicon-chevron-down:before { content:"\f0a3"; }
 78 | 
 79 | .octicon-chevron-left:before { content:"\f0a4"; }
 80 | 
 81 | .octicon-chevron-right:before { content:"\f078"; }
 82 | 
 83 | .octicon-chevron-up:before { content:"\f0a2"; }
 84 | 
 85 | .octicon-circle-slash:before { content:"\f084"; }
 86 | 
 87 | .octicon-circuit-board:before { content:"\f0d6"; }
 88 | 
 89 | .octicon-clippy:before { content:"\f035"; }
 90 | 
 91 | .octicon-clock:before { content:"\f046"; }
 92 | 
 93 | .octicon-cloud-download:before { content:"\f00b"; }
 94 | 
 95 | .octicon-cloud-upload:before { content:"\f00c"; }
 96 | 
 97 | .octicon-code:before { content:"\f05f"; }
 98 | 
 99 | .octicon-comment-discussion:before { content:"\f04f"; }
100 | 
101 | .octicon-comment:before { content:"\f02b"; }
102 | 
103 | .octicon-credit-card:before { content:"\f045"; }
104 | 
105 | .octicon-dash:before { content:"\f0ca"; }
106 | 
107 | .octicon-dashboard:before { content:"\f07d"; }
108 | 
109 | .octicon-database:before { content:"\f096"; }
110 | 
111 | .octicon-desktop-download:before { content:"\f0dc"; }
112 | 
113 | .octicon-device-camera-video:before { content:"\f057"; }
114 | 
115 | .octicon-device-camera:before { content:"\f056"; }
116 | 
117 | .octicon-device-desktop:before { content:"\f27c"; }
118 | 
119 | .octicon-device-mobile:before { content:"\f038"; }
120 | 
121 | .octicon-diff-added:before { content:"\f06b"; }
122 | 
123 | .octicon-diff-ignored:before { content:"\f099"; }
124 | 
125 | .octicon-diff-modified:before { content:"\f06d"; }
126 | 
127 | .octicon-diff-removed:before { content:"\f06c"; }
128 | 
129 | .octicon-diff-renamed:before { content:"\f06e"; }
130 | 
131 | .octicon-diff:before { content:"\f04d"; }
132 | 
133 | .octicon-ellipses:before { content:"\f101"; }
134 | 
135 | .octicon-ellipsis:before { content:"\f09a"; }
136 | 
137 | .octicon-eye:before { content:"\f04e"; }
138 | 
139 | .octicon-file-binary:before { content:"\f094"; }
140 | 
141 | .octicon-file-code:before { content:"\f010"; }
142 | 
143 | .octicon-file-directory:before { content:"\f016"; }
144 | 
145 | .octicon-file-media:before { content:"\f012"; }
146 | 
147 | .octicon-file-pdf:before { content:"\f014"; }
148 | 
149 | .octicon-file-submodule:before { content:"\f017"; }
150 | 
151 | .octicon-file-symlink-directory:before { content:"\f0b1"; }
152 | 
153 | .octicon-file-symlink-file:before { content:"\f0b0"; }
154 | 
155 | .octicon-file-text:before { content:"\f011"; }
156 | 
157 | .octicon-file-zip:before { content:"\f013"; }
158 | 
159 | .octicon-file:before { content:"\f102"; }
160 | 
161 | .octicon-flame:before { content:"\f0d2"; }
162 | 
163 | .octicon-fold:before { content:"\f0cc"; }
164 | 
165 | .octicon-gear:before { content:"\f02f"; }
166 | 
167 | .octicon-gift:before { content:"\f042"; }
168 | 
169 | .octicon-gist-secret:before { content:"\f08c"; }
170 | 
171 | .octicon-gist:before { content:"\f00e"; }
172 | 
173 | .octicon-git-branch:before { content:"\f020"; }
174 | 
175 | .octicon-git-commit:before { content:"\f01f"; }
176 | 
177 | .octicon-git-compare:before { content:"\f0ac"; }
178 | 
179 | .octicon-git-merge:before { content:"\f023"; }
180 | 
181 | .octicon-git-pull-request:before { content:"\f009"; }
182 | 
183 | .octicon-globe:before { content:"\f0b6"; }
184 | 
185 | .octicon-grabber:before { content:"\f103"; }
186 | 
187 | .octicon-graph:before { content:"\f043"; }
188 | 
189 | .octicon-heart:before { content:"\2665"; }
190 | 
191 | .octicon-history:before { content:"\f07e"; }
192 | 
193 | .octicon-home:before { content:"\f08d"; }
194 | 
195 | .octicon-horizontal-rule:before { content:"\f070"; }
196 | 
197 | .octicon-hubot:before { content:"\f09d"; }
198 | 
199 | .octicon-inbox:before { content:"\f0cf"; }
200 | 
201 | .octicon-info:before { content:"\f059"; }
202 | 
203 | .octicon-issue-closed:before { content:"\f028"; }
204 | 
205 | .octicon-issue-opened:before { content:"\f026"; }
206 | 
207 | .octicon-issue-reopened:before { content:"\f027"; }
208 | 
209 | .octicon-italic:before { content:"\f0e4"; }
210 | 
211 | .octicon-jersey:before { content:"\f019"; }
212 | 
213 | .octicon-key:before { content:"\f049"; }
214 | 
215 | .octicon-keyboard:before { content:"\f00d"; }
216 | 
217 | .octicon-law:before { content:"\f0d8"; }
218 | 
219 | .octicon-light-bulb:before { content:"\f000"; }
220 | 
221 | .octicon-link-external:before { content:"\f07f"; }
222 | 
223 | .octicon-link:before { content:"\f05c"; }
224 | 
225 | .octicon-list-ordered:before { content:"\f062"; }
226 | 
227 | .octicon-list-unordered:before { content:"\f061"; }
228 | 
229 | .octicon-location:before { content:"\f060"; }
230 | 
231 | .octicon-lock:before { content:"\f06a"; }
232 | 
233 | .octicon-logo-gist:before { content:"\f0ad"; }
234 | 
235 | .octicon-logo-github:before { content:"\f092"; }
236 | 
237 | .octicon-mail-read:before { content:"\f03c"; }
238 | 
239 | .octicon-mail-reply:before { content:"\f051"; }
240 | 
241 | .octicon-mail:before { content:"\f03b"; }
242 | 
243 | .octicon-mark-github:before { content:"\f00a"; }
244 | 
245 | .octicon-markdown:before { content:"\f0c9"; }
246 | 
247 | .octicon-megaphone:before { content:"\f077"; }
248 | 
249 | .octicon-mention:before { content:"\f0be"; }
250 | 
251 | .octicon-milestone:before { content:"\f075"; }
252 | 
253 | .octicon-mirror:before { content:"\f024"; }
254 | 
255 | .octicon-mortar-board:before { content:"\f0d7"; }
256 | 
257 | .octicon-mute:before { content:"\f080"; }
258 | 
259 | .octicon-no-newline:before { content:"\f09c"; }
260 | 
261 | .octicon-octoface:before { content:"\f008"; }
262 | 
263 | .octicon-organization:before { content:"\f037"; }
264 | 
265 | .octicon-package:before { content:"\f0c4"; }
266 | 
267 | .octicon-paintcan:before { content:"\f0d1"; }
268 | 
269 | .octicon-pencil:before { content:"\f058"; }
270 | 
271 | .octicon-person:before { content:"\f018"; }
272 | 
273 | .octicon-pin:before { content:"\f041"; }
274 | 
275 | .octicon-plug:before { content:"\f0d4"; }
276 | 
277 | .octicon-plus-small:before { content:"\f104"; }
278 | 
279 | .octicon-plus:before { content:"\f05d"; }
280 | 
281 | .octicon-primitive-dot:before { content:"\f052"; }
282 | 
283 | .octicon-primitive-square:before { content:"\f053"; }
284 | 
285 | .octicon-pulse:before { content:"\f085"; }
286 | 
287 | .octicon-question:before { content:"\f02c"; }
288 | 
289 | .octicon-quote:before { content:"\f063"; }
290 | 
291 | .octicon-radio-tower:before { content:"\f030"; }
292 | 
293 | .octicon-reply:before { content:"\f105"; }
294 | 
295 | .octicon-repo-clone:before { content:"\f04c"; }
296 | 
297 | .octicon-repo-force-push:before { content:"\f04a"; }
298 | 
299 | .octicon-repo-forked:before { content:"\f002"; }
300 | 
301 | .octicon-repo-pull:before { content:"\f006"; }
302 | 
303 | .octicon-repo-push:before { content:"\f005"; }
304 | 
305 | .octicon-repo:before { content:"\f001"; }
306 | 
307 | .octicon-rocket:before { content:"\f033"; }
308 | 
309 | .octicon-rss:before { content:"\f034"; }
310 | 
311 | .octicon-ruby:before { content:"\f047"; }
312 | 
313 | .octicon-search:before { content:"\f02e"; }
314 | 
315 | .octicon-server:before { content:"\f097"; }
316 | 
317 | .octicon-settings:before { content:"\f07c"; }
318 | 
319 | .octicon-shield:before { content:"\f0e1"; }
320 | 
321 | .octicon-sign-in:before { content:"\f036"; }
322 | 
323 | .octicon-sign-out:before { content:"\f032"; }
324 | 
325 | .octicon-smiley:before { content:"\f0e7"; }
326 | 
327 | .octicon-squirrel:before { content:"\f0b2"; }
328 | 
329 | .octicon-star:before { content:"\f02a"; }
330 | 
331 | .octicon-stop:before { content:"\f08f"; }
332 | 
333 | .octicon-sync:before { content:"\f087"; }
334 | 
335 | .octicon-tag:before { content:"\f015"; }
336 | 
337 | .octicon-tasklist:before { content:"\f0e5"; }
338 | 
339 | .octicon-telescope:before { content:"\f088"; }
340 | 
341 | .octicon-terminal:before { content:"\f0c8"; }
342 | 
343 | .octicon-text-size:before { content:"\f0e3"; }
344 | 
345 | .octicon-three-bars:before { content:"\f05e"; }
346 | 
347 | .octicon-thumbsdown:before { content:"\f0db"; }
348 | 
349 | .octicon-thumbsup:before { content:"\f0da"; }
350 | 
351 | .octicon-tools:before { content:"\f031"; }
352 | 
353 | .octicon-trashcan:before { content:"\f0d0"; }
354 | 
355 | .octicon-triangle-down:before { content:"\f05b"; }
356 | 
357 | .octicon-triangle-left:before { content:"\f044"; }
358 | 
359 | .octicon-triangle-right:before { content:"\f05a"; }
360 | 
361 | .octicon-triangle-up:before { content:"\f0aa"; }
362 | 
363 | .octicon-unfold:before { content:"\f039"; }
364 | 
365 | .octicon-unmute:before { content:"\f0ba"; }
366 | 
367 | .octicon-unverified:before { content:"\f0e8"; }
368 | 
369 | .octicon-verified:before { content:"\f0e6"; }
370 | 
371 | .octicon-versions:before { content:"\f064"; }
372 | 
373 | .octicon-watch:before { content:"\f0e0"; }
374 | 
375 | .octicon-x:before { content:"\f081"; }
376 | 
377 | .octicon-zap:before { content:"\26a1"; }
378 | 
379 | 


--------------------------------------------------------------------------------
/grip/static/octicons/octicons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/grip/static/octicons/octicons.eot


--------------------------------------------------------------------------------
/grip/static/octicons/octicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/grip/static/octicons/octicons.ttf


--------------------------------------------------------------------------------
/grip/static/octicons/octicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/grip/static/octicons/octicons.woff


--------------------------------------------------------------------------------
/grip/static/octicons/octicons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/grip/static/octicons/octicons.woff2


--------------------------------------------------------------------------------
/grip/templates/base.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html lang="en" data-color-mode={{ data_color_mode }} data-light-theme={{ data_light_theme }} data-dark-theme={{ data_dark_theme }}>
 3 | <head>
 4 |   <meta charset="utf-8" />
 5 |   <title>{% block title %}Grip{% endblock %}</title>
 6 |   <link rel="icon" href="{{ favicon or url_for('static', filename='favicon.ico') }}" />
 7 |   {%- block styles %}{% endblock %}
 8 | </head>
 9 | <body>
10 |   <div class="page">
11 |     {% block page %}{% endblock %}
12 |   </div>
13 |   {%- block scripts %}{% endblock %}
14 | </body>
15 | </html>
16 | 


--------------------------------------------------------------------------------
/grip/templates/index.html:
--------------------------------------------------------------------------------
  1 | {% extends "base.html" %}
  2 | 
  3 | {% block title %}{% if title %}{{ title }}{% else %}{{ filename or '' }} - Grip{% endif %}{% endblock %}
  4 | 
  5 | {%- block styles -%}
  6 |   {%- for style_url in style_urls %}
  7 |   <link rel="stylesheet" href="{{ style_url }}" />
  8 |   {%- endfor %}
  9 |   <link rel="stylesheet" href="{{ url_for('static', filename='octicons/octicons.css') }}" />
 10 |   {%- if styles %}
 11 |   <style>
 12 |     {%- for style in styles %}
 13 |     {{ style|safe }}
 14 |     {%- endfor %}
 15 |   </style>
 16 |   {%- endif %}
 17 |   <style>
 18 |     /* Page tweaks */
 19 |     .preview-page {
 20 |       margin-top: 64px;
 21 |       margin-bottom: 21px;
 22 |     }
 23 |     /* User-content tweaks */
 24 |     .timeline-comment-wrapper > .timeline-comment:after,
 25 |     .timeline-comment-wrapper > .timeline-comment:before {
 26 |       content: none;
 27 |     }
 28 |     /* User-content overrides */
 29 |     .discussion-timeline.wide {
 30 |       width: 920px;
 31 |     }
 32 |   </style>
 33 | {%- endblock -%}
 34 | 
 35 | {%- block scripts -%}
 36 |   <script>
 37 |     function showCanonicalImages() {
 38 |       var images = document.getElementsByTagName('img');
 39 |       if (!images) {
 40 |         return;
 41 |       }
 42 |       for (var index = 0; index < images.length; index++) {
 43 |         var image = images[index];
 44 |         if (image.getAttribute('data-canonical-src') && image.src !== image.getAttribute('data-canonical-src')) {
 45 |           image.src = image.getAttribute('data-canonical-src');
 46 |         }
 47 |       }
 48 |     }
 49 | 
 50 |     function scrollToHash() {
 51 |       if (location.hash && !document.querySelector(':target')) {
 52 |         var element = document.getElementById('user-content-' + location.hash.slice(1));
 53 |         if (element) {
 54 |            element.scrollIntoView();
 55 |         }
 56 |       }
 57 |     }
 58 | 
 59 |     function autorefreshContent(eventSourceUrl) {
 60 |       var initialTitle = document.title;
 61 |       var contentElement = document.getElementById('grip-content');
 62 |       var source = new EventSource(eventSourceUrl);
 63 |       var isRendering = false;
 64 | 
 65 |       source.onmessage = function(ev) {
 66 |         var msg = JSON.parse(ev.data);
 67 |         if (msg.updating) {
 68 |           isRendering = true;
 69 |           document.title = '(Rendering) ' + document.title;
 70 |         } else {
 71 |           isRendering = false;
 72 |           document.title = initialTitle;
 73 |           contentElement.innerHTML = msg.content;
 74 |           showCanonicalImages();
 75 |         }
 76 |       }
 77 | 
 78 |       source.onerror = function(e) {
 79 |         if (e.readyState === EventSource.CLOSED && isRendering) {
 80 |           isRendering = false;
 81 |           document.title = initialTitle;
 82 |         }
 83 |       }
 84 |     }
 85 | 
 86 |     window.onhashchange = function() {
 87 |       scrollToHash();
 88 |     }
 89 | 
 90 |     window.onload = function() {
 91 |       scrollToHash();
 92 |     }
 93 | 
 94 |     showCanonicalImages();
 95 | 
 96 |     var autorefreshUrl = document.getElementById('preview-page').getAttribute('data-autorefresh-url');
 97 |     if (autorefreshUrl) {
 98 |       autorefreshContent(autorefreshUrl);
 99 |     }
100 |   </script>
101 | {%- endblock -%}
102 | 
103 | {%- block page -%}
104 |   <div id="preview-page" class="preview-page" data-autorefresh-url="{{ autorefresh_url if autorefresh_url }}">
105 |     <main id="js-repo-pjax-container">
106 |       <div class="clearfix new-discussion-timeline container-xl px-3 px-md-4 px-lg-5">
107 |         <div class="repository-content">
108 |           <div class="clearfix">
109 |             <div class="Layout Layout--flowRow-until-md Layout--sidebarPosition-end Layout--sidebarPosition-flowRow-end">
110 |               <div class="Layout-main">
111 | 
112 |                 {% if not user_content %}
113 | 
114 |                   <div id="readme" class="Box md Box--responsive">
115 |                     {% if title or filename %}
116 |                       <div class="Box-header d-flex border-bottom-0 flex-items-center flex-justify-between color-bg-default rounded-top-2">
117 |                         <div class="d-flex flex-items-center">
118 |                           <h2 class="Box-title">
119 |                             {{ title or filename }}
120 |                           </h2>
121 |                         </div>
122 |                       </div>
123 |                     {% endif %}
124 |                     <div class="Box-body px-5 pb-5">
125 |                       <article id="grip-content" class="markdown-body entry-content container-lg">
126 |                         {{ content|safe }}
127 |                       </article>
128 |                     </div>
129 |                   </div>
130 | 
131 |                 {% else %}
132 | 
133 |                   <div class="pull-discussion-timeline">
134 |                     <div class="ml-0 pl-0 ml-md-6 pl-md-3">
135 |                       <div class="TimelineItem pt-0">
136 |                         <div class="timeline-comment-group TimelineItem-body my-0">
137 |                           <div class="ml-n3 timeline-comment unminimized-comment comment previewable-edit editable-comment timeline-comment--caret reorderable-task-lists">
138 |                             {% if title %}
139 |                               <div class="timeline-comment-header clearfix d-block d-sm-flex">
140 |                                 <h3 class="timeline-comment-header-text f5 text-normal">
141 |                                   <strong class="css-truncate expandable"><span class="author text-inherit css-truncate-target">{{ title }}</span></strong>
142 |                                 </h3>
143 |                               </div>
144 |                             {% endif %}
145 |                             <div class="edit-comment-hide">
146 |                               <table class="d-block">
147 |                                 <tbody class="d-block">
148 |                                   <tr class="d-block">
149 |                                     <td class="d-block comment-body markdown-body" id="grip-content">
150 |                                       {{ content|safe }}
151 |                                     </td>
152 |                                   </tr>
153 |                                 </tbody>
154 |                               </table>
155 |                             </div>
156 |                           </div>
157 |                         </div>
158 |                       </div>
159 |                     </div>
160 |                   </div>
161 | 
162 |                 {% endif %}
163 | 
164 |               </div>
165 |             </div>
166 |           </div>
167 |         </div>
168 |       </div>
169 |     </main>
170 |   </div>
171 | {%- endblock -%}
172 | 


--------------------------------------------------------------------------------
/grip/templates/limit.html:
--------------------------------------------------------------------------------
  1 | {% extends "base.html" %}
  2 | 
  3 | {% block title %}GitHub rate limit reached - Grip{% endblock %}
  4 | 
  5 | 
  6 | {%- block styles -%}
  7 |   <style>
  8 |     body {
  9 |       font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
 10 |       font-size: 14px;
 11 |       line-height: 1.42em;
 12 |     }
 13 |     code {
 14 |       font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
 15 |       background-color: #f8f8f8;
 16 |       border: 1px solid #ddd;
 17 |       border-radius: 3px;
 18 |       font-size: 12px;
 19 |       line-height: 19px;
 20 |       overflow: auto;
 21 |       padding: 2px 10px;
 22 |     }
 23 |     .error-page {
 24 |       margin: 32px auto;
 25 |       width: 500px;
 26 |     }
 27 |     .error-page.authenticated {
 28 |       margin: 64px auto;
 29 |       width: 600px;
 30 |     }
 31 |     .error-block {
 32 |       background: #f5f5f5;
 33 |       border: 1px solid #ddd;
 34 |       border-radius: 3px;
 35 |       padding: 16px 32px;
 36 |     }
 37 |     .error-description {
 38 |       padding: 16px 32px;
 39 |     }
 40 |   </style>
 41 | {%- endblock -%}
 42 | 
 43 | 
 44 | {%- block scripts -%}
 45 |   <script>
 46 |     function scrollToHash() {
 47 |       if (location.hash && !document.querySelector(":target")) {
 48 |         var elements = document.getElementsByName('user-content-' + location.hash.slice(1));
 49 |         if (elements.length > 0) {
 50 |           elements[elements.length - 1].scrollIntoView();
 51 |         }
 52 |       }
 53 |     }
 54 |     window.onhashchange = function() {
 55 |       scrollToHash();
 56 |     }
 57 |     window.onload = function() {
 58 |       scrollToHash();
 59 |     }
 60 |   </script>
 61 | {%- endblock -%}
 62 | 
 63 | {%- block page -%}
 64 |   <div class="error-page {{ 'authenticated' if is_authenticated }}">
 65 |     <div class="error-block">
 66 |       <h1>GitHub Rate Limit Reached</h1>
 67 |       <p>
 68 |         The <a href="https://developer.github.com/v3/#rate-limiting">GitHub API rate limit</a>
 69 |         {% if is_authenticated %} for basic auth {% endif %}
 70 |         has been reached for the hour.
 71 |       </p>
 72 |       {% if is_authenticated %}
 73 |         <p>
 74 |           Sorry for the inconvenience.
 75 |         </p>
 76 |       {% endif %}
 77 |     </div>
 78 | 
 79 |     {% if not is_authenticated %}
 80 |       <div class="error-description">
 81 |         <h2>What?</h2>
 82 |         <p>
 83 |           GitHub imposes a limit of <strong>60 requests/hour</strong> when using their API without authentication.
 84 |         </p>
 85 | 
 86 |         <h2>Why?</h2>
 87 |         <p>
 88 |           This prevents people from anonymously abusing GitHub's system.
 89 |         </p>
 90 |         <p>
 91 |           As for Grip, it's built to appear as close to GitHub as possible. Using
 92 |           GitHub's API allows Grip to immediately and accurately reflect any updates
 93 |           from GitHub, without the delay of busy maintainers or requiring you to upgrade.
 94 |         </p>
 95 | 
 96 |         <h2>Ok, fine. Where do I go from here?</h2>
 97 |         <p>
 98 |           Until the <a href="https://github.com/joeyespo/grip/issues/35">offline renderer</a> is complete, you can run Grip using <br />
 99 |           the <code>--user</code> and <code>--pass</code> arguments to use basic auth, <br />
100 |           giving you <strong>5,000 requests/hour</strong>. Run <code>grip -h</code> for details.
101 |         </p>
102 |         <p>
103 |           I do apologize for the inconvenience. If you need help, or have ideas on improving this
104 |           experience, please reach out <a href="mailto:{{ email }}?subject=I+just+hit+GitHub's+rate+limit+with+grip" target="_blank">joe@joeyespo.com</a>
105 |         </p>
106 |       </div>
107 |     {% endif %}
108 |   </div>
109 | {%- endblock -%}
110 | 


--------------------------------------------------------------------------------
/grip/vendor/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/grip/vendor/__init__.py


--------------------------------------------------------------------------------
/grip/vendor/mdx_urlize.py:
--------------------------------------------------------------------------------
 1 | """
 2 | A more liberal autolinker
 3 | 
 4 | Inspired by Django's urlize function.
 5 | 
 6 | Positive examples:
 7 | 
 8 | >>> import markdown
 9 | >>> md = markdown.Markdown(extensions=['urlize'])
10 | 
11 | >>> md.convert('http://example.com/')
12 | u'<p><a href="http://example.com/">http://example.com/</a></p>'
13 | 
14 | >>> md.convert('go to http://example.com')
15 | u'<p>go to <a href="http://example.com">http://example.com</a></p>'
16 | 
17 | >>> md.convert('example.com')
18 | u'<p><a href="http://example.com">example.com</a></p>'
19 | 
20 | >>> md.convert('example.net')
21 | u'<p><a href="http://example.net">example.net</a></p>'
22 | 
23 | >>> md.convert('www.example.us')
24 | u'<p><a href="http://www.example.us">www.example.us</a></p>'
25 | 
26 | >>> md.convert('(www.example.us/path/?name=val)')
27 | u'<p>(<a href="http://www.example.us/path/?name=val">www.example.us/path/?name=val</a>)</p>'
28 | 
29 | >>> md.convert('go to <http://example.com> now!')
30 | u'<p>go to <a href="http://example.com">http://example.com</a> now!</p>'
31 | 
32 | Negative examples:
33 | 
34 | >>> md.convert('del.icio.us')
35 | u'<p>del.icio.us</p>'
36 | """
37 | 
38 | import markdown
39 | 
40 | 
41 | URLIZE_RE = '(%s)' % '|'.join([
42 |     r'<(?:f|ht)tps?://[^>]*>',
43 |     r'\b(?:f|ht)tps?://[^)<>\s]+[^.,)<>\s]',
44 |     r'\bwww\.[^)<>\s]+[^.,)<>\s]',
45 |     r'[^(<\s]+\.(?:com|net|org)\b',
46 | ])
47 | 
48 | 
49 | class UrlizePattern(markdown.inlinepatterns.Pattern):
50 |     """
51 |     Return a link Element given an autolink (`http://example/com`).
52 |     """
53 |     def handleMatch(self, m):
54 |         url = m.group(2)
55 | 
56 |         if url.startswith('<'):
57 |             url = url[1:-1]
58 | 
59 |         text = url
60 | 
61 |         if not url.split('://')[0] in ['http', 'https', 'ftp']:
62 |             if '@' in url and not '/' in url:
63 |                 url = 'mailto:' + url
64 |             else:
65 |                 url = 'http://' + url
66 | 
67 |         el = markdown.util.etree.Element('a')
68 |         el.set('href', url)
69 |         el.text = markdown.util.AtomicString(text)
70 |         return el
71 | 
72 | 
73 | class UrlizeExtension(markdown.Extension):
74 |     """
75 |     Urlize Extension for Python-Markdown.
76 |     """
77 |     def extendMarkdown(self, md, md_globals):
78 |         """
79 |         Replace autolink with UrlizePattern
80 |         """
81 |         md.inlinePatterns['autolink'] = UrlizePattern(URLIZE_RE, md)
82 | 
83 | 
84 | def makeExtension(configs=None):
85 |     return UrlizeExtension(configs=configs)
86 | 
87 | 
88 | if __name__ == '__main__':
89 |     import doctest
90 |     doctest.testmod()
91 | 


--------------------------------------------------------------------------------
/grip/vendor/six.py:
--------------------------------------------------------------------------------
 1 | """Utilities for writing code that runs on Python 2 and 3"""
 2 | 
 3 | # Copyright (c) 2010-2015 Benjamin Peterson
 4 | #
 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | # of this software and associated documentation files (the "Software"), to deal
 7 | # in the Software without restriction, including without limitation the rights
 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all
13 | # copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 | 
23 | from __future__ import absolute_import
24 | 
25 | # [...]
26 | 
27 | __author__ = "Benjamin Peterson <benjamin@python.org>"
28 | __version__ = "1.10.0"
29 | 
30 | 
31 | # [...]
32 | 
33 | 
34 | def add_metaclass(metaclass):
35 |     """Class decorator for creating a class with a metaclass."""
36 |     def wrapper(cls):
37 |         orig_vars = cls.__dict__.copy()
38 |         slots = orig_vars.get('__slots__')
39 |         if slots is not None:
40 |             if isinstance(slots, str):
41 |                 slots = [slots]
42 |             for slots_var in slots:
43 |                 orig_vars.pop(slots_var)
44 |         orig_vars.pop('__dict__', None)
45 |         orig_vars.pop('__weakref__', None)
46 |         return metaclass(cls.__name__, cls.__bases__, orig_vars)
47 |     return wrapper
48 | 
49 | 
50 | # [...]
51 | 


--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | norecursedirs = venv site-packages .research
3 | markers =
4 |     assumption: mark a test as an external assumption.
5 | 


--------------------------------------------------------------------------------
/requirements-test.txt:
--------------------------------------------------------------------------------
1 | responses>=0.5.0
2 | flake8>=3.0.0
3 | pytest>=4.4.1
4 | 


--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | docopt>=0.4.0
2 | Flask>=0.10.1
3 | Markdown>=2.5.1
4 | path-and-address>=2.0.1
5 | Pygments>=1.6
6 | requests>=2.4.1
7 | Werkzeug>=0.7
8 | 


--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
 1 | """
 2 | Grip
 3 | ----
 4 | 
 5 | Render local readme files before sending off to GitHub.
 6 | 
 7 | 
 8 | Grip is easy to set up
 9 | ``````````````````````
10 | 
11 | ::
12 | 
13 |     $ pip install grip
14 |     $ cd myproject
15 |     $ grip
16 | 
17 | 
18 | Links
19 | `````
20 | 
21 | * `Website <http://github.com/joeyespo/grip>`_
22 | 
23 | """
24 | 
25 | import os
26 | from setuptools import setup, find_packages
27 | 
28 | 
29 | def read(filename):
30 |     with open(os.path.join(os.path.dirname(__file__), filename)) as f:
31 |         return f.read()
32 | 
33 | 
34 | setup(
35 |     name='grip',
36 |     version='4.6.2',
37 |     description='Render local readme files before sending off to GitHub.',
38 |     long_description=__doc__,
39 |     author='Joe Esposito',
40 |     author_email='joe@joeyespo.com',
41 |     url='http://github.com/joeyespo/grip',
42 |     license='MIT',
43 |     platforms='any',
44 |     packages=find_packages(),
45 |     package_data={'grip': ['static/*.*', 'static/octicons/*', 'templates/*']},
46 |     install_requires=read('requirements.txt').splitlines(),
47 |     extras_require={'tests': read('requirements-test.txt').splitlines()},
48 |     zip_safe=False,
49 |     entry_points={'console_scripts': ['grip = grip:main']},
50 | )
51 | 


--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals
2 | 
3 | import os
4 | import sys
5 | 
6 | 
7 | DIRNAME = os.path.dirname(os.path.abspath(__file__))
8 | sys.path.insert(1, os.path.dirname(DIRNAME))
9 | 


--------------------------------------------------------------------------------
/tests/helpers.py:
--------------------------------------------------------------------------------
 1 | from __future__ import print_function, unicode_literals
 2 | 
 3 | import io
 4 | import os
 5 | 
 6 | 
 7 | DIRNAME = os.path.dirname(os.path.abspath(__file__))
 8 | USER_CONTEXT = 'joeyespo/grip'
 9 | 
10 | 
11 | def input_filename(*parts):
12 |     return os.path.join(DIRNAME, 'input', *parts)
13 | 
14 | 
15 | def output_filename(*parts):
16 |     return os.path.join(DIRNAME, 'output', *parts)
17 | 
18 | 
19 | def input_file(*parts, **kwargs):
20 |     encoding = kwargs.pop('encoding', 'utf-8')
21 |     with io.open(input_filename(*parts), 'rt', encoding=encoding) as f:
22 |         return f.read()
23 | 
24 | 
25 | def output_file(*parts, **kwargs):
26 |     encoding = kwargs.pop('encoding', 'utf-8')
27 |     with io.open(output_filename(*parts), 'rt', encoding=encoding) as f:
28 |         return f.read()
29 | 


--------------------------------------------------------------------------------
/tests/input/default/README.md:
--------------------------------------------------------------------------------
1 | README Test
2 | ===========
3 | 
4 | A directory with a single `readme.md` file.
5 | 


--------------------------------------------------------------------------------
/tests/input/gfm-test.md:
--------------------------------------------------------------------------------
  1 | GitHub Flavored Markdown Test
  2 | =============================
  3 | 
  4 | This Markdown file contains all the features of **GitHub Flavored Markdown** for
  5 | testing a renderer with.
  6 | 
  7 | The features are taken directly from [Daring Fireball](https://daringfireball.net/projects/markdown/syntax)
  8 | and [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/),
  9 | followed by a list of one-offs.
 10 | 
 11 | 
 12 | Inline HTML
 13 | -----------
 14 | 
 15 | <table>
 16 |   <tr>
 17 |     <td>Foo</td>
 18 |   </tr>
 19 |   <tr>
 20 |     <td>
 21 |       Note that Markdown formatting syntax is not processed within block-level HTML tags.
 22 |       E.g., you can’t use Markdown-style *emphasis* inside an HTML block.
 23 |     </td>
 24 |   </tr>
 25 | </table>
 26 | 
 27 | Span-level HTML tags — e.g. &lt;span&gt;, &lt;cite&gt;, or &lt;del&gt; — can be used anywhere
 28 | in a Markdown paragraph, list item, or header. If you want, you can even use HTML tags instead
 29 | of Markdown formatting; e.g. if you’d prefer to use HTML &lt;a&gt; or &lt;img&gt; tags instead
 30 | of Markdown’s link or image syntax, go right ahead.
 31 | 
 32 | #### Testing <span>span</span>, <cite>cite</cite>, and <del>del</del> tags
 33 | 
 34 | Testing <span>span</span>, <cite>cite</cite>, and <del>del</del> tags in a paragraph.
 35 | 
 36 | And each element in its own list item:
 37 | - <span>This is within a span tag.</span>
 38 | - <cite>This is within a cite tag.</cite>
 39 | - <del>This is within a del tag.</del>
 40 | 
 41 | 
 42 | Automatic escaping for special characters
 43 | -----------------------------------------
 44 | 
 45 | - &copy; copyright
 46 | - AT&T should render the same as AT&amp;T
 47 | - 4 < 5 should render the same as 4 &lt; 5
 48 | 
 49 | Note that GitHub Flavored Markdown has URL autolinking, which will *not*
 50 | convert `&amp;`. So these two should yield different links:
 51 | 
 52 | - http://images.google.com/images?num=30&q=larry+bird
 53 | - http://images.google.com/images?num=30&amp;q=larry+bird
 54 | 
 55 | 
 56 | Paragraphs and line breaks
 57 | --------------------------
 58 | 
 59 | This is a normal paragraph.
 60 | 
 61 | This and the next sentence is separated by a single newline.
 62 | This should be on the same line.
 63 | 
 64 | This and the next sentence is joined by a single `<br />`. <br />
 65 | This should be on a new line, directly below.
 66 | 
 67 | These two sentences are separated by two `<br />` tags. <br /><br />
 68 | This should be two lines below.
 69 | 
 70 | These two paragraphs are separated by two `<br />` tags. <br /><br />
 71 | 
 72 | This should be three lines below.
 73 | 
 74 | 
 75 | Headers
 76 | -------
 77 | 
 78 | Here are some headers followed by [Lorem Ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum).
 79 | 
 80 | 
 81 | This is an H1 (Setext-style)
 82 | ============================
 83 | 
 84 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
 85 | 
 86 | 
 87 | This is an H2 (Setext-style)
 88 | ----------------------------
 89 | 
 90 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
 91 | 
 92 | The following are atx-style.
 93 | 
 94 | 
 95 | # This is an H1
 96 | 
 97 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
 98 | 
 99 | 
100 | ## This is an H2
101 | 
102 | Mauris feugiat, augue vitae sollicitudin vulputate, neque arcu dapibus eros, eget semper lorem ex rhoncus nulla.
103 | 
104 | 
105 | ### This is an H3
106 | 
107 | Etiam sit amet orci sit amet dui mollis molestie.
108 | 
109 | 
110 | #### This is an H4
111 | 
112 | Cras et elit egestas, lacinia est eu, vestibulum enim.
113 | 
114 | 
115 | ##### This is an H5
116 | 
117 | Phasellus sed suscipit quam.
118 | 
119 | 
120 | ###### This is an H√36
121 | 
122 | Nam rutrum imperdiet purus, sit amet porttitor augue tempor quis.
123 | 
124 | 
125 | Blockquotes
126 | -----------
127 | 
128 | Email-style blockquotes:
129 | 
130 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
131 | > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
132 | > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
133 | >
134 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
135 | > id sem consectetuer libero luctus adipiscing.
136 | 
137 | Putting the > before the first line of a hard-wrapped paragraph:
138 | 
139 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
140 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
141 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
142 | 
143 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
144 | id sem consectetuer libero luctus adipiscing.
145 | 
146 | Nested blockquotes (i.e. a blockquote-in-a-blockquote):
147 | 
148 | > This is the first level of quoting.
149 | >
150 | > > This is nested blockquote.
151 | >
152 | > Back to the first level.
153 | 
154 | Blockquotes containing other Markdown elements:
155 | 
156 | > ## This is a header.
157 | >
158 | > 1.   This is the first list item.
159 | > 2.   This is the second list item.
160 | >
161 | > Here's some example code:
162 | >
163 | >     return shell_exec("echo $input | $markdown_script");
164 | 
165 | 
166 | Lists
167 | -----
168 | 
169 | ##### These three lists should be equivalent
170 | 
171 | First:
172 | 
173 | *   Red
174 | *   Green
175 | *   Blue
176 | 
177 | Second:
178 | 
179 | +   Red
180 | +   Green
181 | +   Blue
182 | 
183 | Third:
184 | 
185 | -   Red
186 | -   Green
187 | -   Blue
188 | 
189 | 
190 | #### These three ordered lists should be equivalent
191 | 
192 | First:
193 | 
194 | 1.  Bird
195 | 2.  McHale
196 | 3.  Parish
197 | 
198 | Second:
199 | 
200 | 1.  Bird
201 | 1.  McHale
202 | 1.  Parish
203 | 
204 | Third:
205 | 
206 | 3. Bird
207 | 1. McHale
208 | 8. Parish
209 | 
210 | 
211 | #### These two lists should be equivalent
212 | 
213 | First:
214 | 
215 | *   Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
216 |     Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
217 |     viverra nec, fringilla in, laoreet vitae, risus.
218 | *   Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
219 |     Suspendisse id sem consectetuer libero luctus adipiscing.
220 | 
221 | Second:
222 | 
223 | *   Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
224 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
225 | viverra nec, fringilla in, laoreet vitae, risus.
226 | *   Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
227 | Suspendisse id sem consectetuer libero luctus adipiscing.
228 | 
229 | 
230 | #### Paragraphs and lists
231 | 
232 | *   Bird
233 | *   Magic
234 | 
235 | Blank line separated:
236 | 
237 | *   Bird
238 | 
239 | *   Magic
240 | 
241 | 
242 | #### Paragraphs within lists
243 | 
244 | First:
245 | 
246 | 1.  This is a list item with two paragraphs. Lorem ipsum dolor
247 |     sit amet, consectetuer adipiscing elit. Aliquam hendrerit
248 |     mi posuere lectus.
249 | 
250 |     Vestibulum enim wisi, viverra nec, fringilla in, laoreet
251 |     vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
252 |     sit amet velit.
253 | 
254 | 2.  Suspendisse id sem consectetuer libero luctus adipiscing.
255 | 
256 | Second:
257 | 
258 | *   This is a list item with two paragraphs.
259 | 
260 |     This is the second paragraph in the list item. You're
261 | only required to indent the first line. Lorem ipsum dolor
262 | sit amet, consectetuer adipiscing elit.
263 | 
264 | *   Another item in the same list.
265 | 
266 | 
267 | #### Blockquote within a list
268 | 
269 | *   A list item with a blockquote:
270 | 
271 |     > This is a blockquote
272 |     > inside a list item.
273 | 
274 | 
275 | #### Code within a list
276 | 
277 | *   A list item with a code block:
278 | 
279 |         <code goes here>
280 | 
281 | 
282 | #### Accidental lists
283 | 
284 | 1986. What a great season. (oops, I wanted a year, not a list)
285 | 
286 | 1986\. What a great season. (*whew!* there we go)
287 | 
288 | 
289 | Code blocks
290 | -----------
291 | 
292 | This is a normal paragraph:
293 | 
294 |     This is a code block.
295 | 
296 | 
297 | Here is an example of AppleScript:
298 | 
299 |     tell application "Foo"
300 |         beep
301 |     end tell
302 | 
303 | Markdown will handle the hassle of encoding the ampersands and angle brackets:
304 | 
305 |     <div class="footer">
306 |         &copy; 2004 Foo Corporation
307 |     </div>
308 | 
309 | 
310 |     def this_is
311 |       puts "some #{4-space-indent} code"
312 |     end
313 | 
314 | <code>
315 | print('Code block')
316 | </code>
317 | 
318 | <pre>
319 | print('Pre block')
320 | </pre>
321 | 
322 | 
323 | Horizontal rules
324 | ----------------
325 | 
326 | * * *
327 | 
328 | ***
329 | 
330 | *****
331 | 
332 | - - -
333 | 
334 | ---------------------------------------
335 | 
336 | 
337 | Links
338 | -----
339 | 
340 | Markdown supports two style of links: inline and reference.
341 | 
342 | This is [an example](http://joeyespo.com/ "Title") inline link.
343 | 
344 | [This link](http://joeyespo.com/) has no title attribute.
345 | 
346 | See my [About](/about/) page for some awesome people (*note: broken link*).
347 | 
348 | This is [an example][id] reference-style link.
349 | This is [an example] [id] reference-style link with a space separating the brackets.
350 | 
351 | These should all be equivalent:
352 | 
353 | - [foo 1][]
354 | - [foo 2][]
355 | - [foo 3][]
356 | - [foo 4][]
357 | - [foo 5][FOO 5]
358 | 
359 | 
360 | [id]: http://joeyespo.com/  "Optional Title Here"
361 | [foo 1]: http://joeyespo.com/  "Optional Title Here"
362 | [foo 2]: http://joeyespo.com/  'Optional Title Here'
363 | [foo 3]: http://joeyespo.com/  (Optional Title Here)
364 | [foo 4]: <http://joeyespo.com/>  "Optional Title Here"
365 | [foo 5]: http://joeyespo.com/
366 |     "Optional Title Here"
367 | 
368 | 
369 | Emphasis
370 | --------
371 | 
372 | - *single asterisks*
373 | - _single underscores_
374 | - **double asterisks**
375 | - __double underscores__
376 | - un*frigging*believable
377 | - \*this text is surrounded by literal asterisks\*
378 | 
379 | 
380 | Code
381 | ----
382 | 
383 | - Use the `printf()` function.
384 | - ``There is a literal backtick (`) here.``
385 | - A single backtick in a code span: `` ` ``
386 | - A backtick-delimited string in a code span: `` `foo` ``
387 | - Please don't use any `<blink>` tags.
388 | - `&#8212;` is the decimal-encoded equivalent of `&mdash;`
389 | 
390 | 
391 | Images
392 | ------
393 | 
394 | - ![Alt text](https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico)
395 | - ![Alt text](https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico "Optional title")
396 | - ![Alt text][img]
397 | - <img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" width="32" height="32" /> &nbsp; &larr; bigger &amp; blurrier
398 | 
399 | 
400 | [img]: https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico   "Optional title attribute"
401 | 
402 | 
403 | Automatic links
404 | ---------------
405 | 
406 | - <http://joeyespo.com/>
407 | - <joe@joeyespo.com>
408 | 
409 | 
410 | Backslash escapes
411 | -----------------
412 | 
413 | - \*literal asterisks\*
414 | - \\   backslash
415 | - \`   backtick
416 | - \*   asterisk
417 | - \_   underscore
418 | - \{\}  curly braces
419 | - \[\]  square brackets
420 | - \(\)  parentheses
421 | - \#   hash mark
422 | - \+   plus sign
423 | - \-   minus sign (hyphen)
424 | - \.   dot
425 | - \!   exclamation mark
426 | 
427 | 
428 | GitHub Flavored Markdown
429 | ------------------------
430 | 
431 | See [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/) for details.
432 | 
433 | #### Multiple underscores in words
434 | 
435 | - wow_great_stuff
436 | 
437 | 
438 | #### URL autolinking
439 | 
440 | http://joeyespo.com
441 | 
442 | 
443 | #### Strikethrough
444 | 
445 | ~~Mistaken text.~~
446 | 
447 | 
448 | #### Fenced code blocks
449 | 
450 | ```
451 | function test() {
452 |   console.log("notice the blank line before this function?");
453 | }
454 | ```
455 | 
456 | 
457 | #### Syntax highlighting
458 | 
459 | ```python
460 | print('Hello!')
461 | ```
462 | 
463 | ```javascript
464 | console.log('JavaScript!');
465 | ```
466 | 
467 | ```js
468 | console.log('JavaScript (with js)!');
469 | ```
470 | 
471 | ```unmatched_language
472 | console.log('No matching language, but looks like JavaScript.');
473 | ```
474 | 
475 | 
476 | #### Tables
477 | 
478 | Simple:
479 | 
480 | First Header  | Second Header
481 | ------------- | -------------
482 | Content Cell  | Content Cell
483 | Content Cell  | Content Cell
484 | 
485 | Pipes:
486 | 
487 | | First Header  | Second Header |
488 | | ------------- | ------------- |
489 | | Content Cell  | Content Cell  |
490 | | Content Cell  | Content Cell  |
491 | 
492 | Unmatched:
493 | 
494 | | Name | Description          |
495 | | ------------- | ----------- |
496 | | Help      | Display the help window.|
497 | | Close     | Closes a window     |
498 | 
499 | Inner Markdown:
500 | 
501 | | Name | Description          |
502 | | ------------- | ----------- |
503 | | Help      | ~~Display the~~ help window.|
504 | | Close     | _Closes_ a window     |
505 | 
506 | Alignment:
507 | 
508 | | Left-Aligned  | Center Aligned  | Right Aligned |
509 | | :------------ |:---------------:| -----:|
510 | | col 3 is      | some wordy text | $1600 |
511 | | col 2 is      | centered        |   $12 |
512 | | zebra stripes | are neat        |    $1 |
513 | Text right below a table.
514 | 
515 | 
516 | #### HTML
517 | 
518 | *TODO: Test all allowed HTML tags.*
519 | 
520 | 
521 | Writing on GitHub
522 | -----------------
523 | 
524 | See [this article](https://help.github.com/articles/writing-on-github/) for details.
525 | 
526 | 
527 | ####  Newlines
528 | 
529 | Roses are red
530 | Violets are Blue
531 | 
532 | 
533 | #### Task lists
534 | 
535 | - [x] @mentions, #refs, [links](), **formatting**, and <del>tags</del> are supported
536 | - [x] list syntax is required (any unordered or ordered list supported)
537 | - [x] this is a complete item
538 | - [ ] this is an incomplete item
539 | 
540 | Task lists can be nested to better structure your tasks:
541 | 
542 | - [ ] a bigger project
543 |   - [x] first subtask #1234
544 |   - [ ] follow up subtask #4321
545 |   - [ ] final subtask cc @mention
546 | - [x] a separate task
547 | 
548 | 
549 | #### References
550 | 
551 | * SHA: dbcd7a410ee7489acf92f40641a135fbcf52a768
552 | * User@SHA: joeyespo@dbcd7a410ee7489acf92f40641a135fbcf52a768
553 | * User/Repository@SHA: joeyespo/grip@dbcd7a410ee7489acf92f40641a135fbcf52a768
554 | * #Num: #135
555 | * GH-Num: GH-135
556 | * User#Num: joeyespo#135
557 | * User/Repository#Num: joeyespo/grip#135
558 | 


--------------------------------------------------------------------------------
/tests/input/github.md:
--------------------------------------------------------------------------------
  1 | GitHub Flavored Markdown Test
  2 | =============================
  3 | 
  4 | This Markdown file contains all the features of **GitHub Flavored Markdown** for
  5 | testing a renderer with.
  6 | 
  7 | The features are taken directly from [Daring Fireball](https://daringfireball.net/projects/markdown/syntax)
  8 | and [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/),
  9 | followed by a list of one-offs.
 10 | 
 11 | 
 12 | Inline HTML
 13 | -----------
 14 | 
 15 | <table>
 16 |   <tr>
 17 |     <td>Foo</td>
 18 |   </tr>
 19 |   <tr>
 20 |     <td>
 21 |       Note that Markdown formatting syntax is not processed within block-level HTML tags.
 22 |       E.g., you can’t use Markdown-style *emphasis* inside an HTML block.
 23 |     </td>
 24 |   </tr>
 25 | </table>
 26 | 
 27 | Span-level HTML tags — e.g. &lt;span&gt;, &lt;cite&gt;, or &lt;del&gt; — can be used anywhere
 28 | in a Markdown paragraph, list item, or header. If you want, you can even use HTML tags instead
 29 | of Markdown formatting; e.g. if you’d prefer to use HTML &lt;a&gt; or &lt;img&gt; tags instead
 30 | of Markdown’s link or image syntax, go right ahead.
 31 | 
 32 | #### Testing <span>span</span>, <cite>cite</cite>, and <del>del</del> tags
 33 | 
 34 | Testing <span>span</span>, <cite>cite</cite>, and <del>del</del> tags in a paragraph.
 35 | 
 36 | And each element in its own list item:
 37 | - <span>This is within a span tag.</span>
 38 | - <cite>This is within a cite tag.</cite>
 39 | - <del>This is within a del tag.</del>
 40 | 
 41 | 
 42 | Automatic escaping for special characters
 43 | -----------------------------------------
 44 | 
 45 | - &copy; copyright
 46 | - AT&T should render the same as AT&amp;T
 47 | - 4 < 5 should render the same as 4 &lt; 5
 48 | 
 49 | Note that GitHub Flavored Markdown has URL autolinking, which will *not*
 50 | convert `&amp;`. So these two should yield different links:
 51 | 
 52 | - http://images.google.com/images?num=30&q=larry+bird
 53 | - http://images.google.com/images?num=30&amp;q=larry+bird
 54 | 
 55 | 
 56 | Paragraphs and line breaks
 57 | --------------------------
 58 | 
 59 | This is a normal paragraph.
 60 | 
 61 | This and the next sentence is separated by a single newline.
 62 | This should be on the same line.
 63 | 
 64 | This and the next sentence is joined by a single `<br />`. <br />
 65 | This should be on a new line, directly below.
 66 | 
 67 | These two sentences are separated by two `<br />` tags. <br /><br />
 68 | This should be two lines below.
 69 | 
 70 | These two paragraphs are separated by two `<br />` tags. <br /><br />
 71 | 
 72 | This should be three lines below.
 73 | 
 74 | 
 75 | Headers
 76 | -------
 77 | 
 78 | Here are some headers followed by [Lorem Ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum).
 79 | 
 80 | 
 81 | This is an H1 (Setext-style)
 82 | ============================
 83 | 
 84 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
 85 | 
 86 | 
 87 | This is an H2 (Setext-style)
 88 | ----------------------------
 89 | 
 90 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
 91 | 
 92 | The following are atx-style.
 93 | 
 94 | 
 95 | # This is an H1
 96 | 
 97 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.
 98 | 
 99 | 
100 | ## This is an H2
101 | 
102 | Mauris feugiat, augue vitae sollicitudin vulputate, neque arcu dapibus eros, eget semper lorem ex rhoncus nulla.
103 | 
104 | 
105 | ### This is an H3
106 | 
107 | Etiam sit amet orci sit amet dui mollis molestie.
108 | 
109 | 
110 | #### This is an H4
111 | 
112 | Cras et elit egestas, lacinia est eu, vestibulum enim.
113 | 
114 | 
115 | ##### This is an H5
116 | 
117 | Phasellus sed suscipit quam.
118 | 
119 | 
120 | ###### This is an H√36
121 | 
122 | Nam rutrum imperdiet purus, sit amet porttitor augue tempor quis.
123 | 
124 | 
125 | Blockquotes
126 | -----------
127 | 
128 | Email-style blockquotes:
129 | 
130 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
131 | > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
132 | > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
133 | >
134 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
135 | > id sem consectetuer libero luctus adipiscing.
136 | 
137 | Putting the > before the first line of a hard-wrapped paragraph:
138 | 
139 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
140 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
141 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
142 | 
143 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
144 | id sem consectetuer libero luctus adipiscing.
145 | 
146 | Nested blockquotes (i.e. a blockquote-in-a-blockquote):
147 | 
148 | > This is the first level of quoting.
149 | >
150 | > > This is nested blockquote.
151 | >
152 | > Back to the first level.
153 | 
154 | Blockquotes containing other Markdown elements:
155 | 
156 | > ## This is a header.
157 | >
158 | > 1.   This is the first list item.
159 | > 2.   This is the second list item.
160 | >
161 | > Here's some example code:
162 | >
163 | >     return shell_exec("echo $input | $markdown_script");
164 | 
165 | 
166 | Lists
167 | -----
168 | 
169 | ##### These three lists should be equivalent
170 | 
171 | First:
172 | 
173 | *   Red
174 | *   Green
175 | *   Blue
176 | 
177 | Second:
178 | 
179 | +   Red
180 | +   Green
181 | +   Blue
182 | 
183 | Third:
184 | 
185 | -   Red
186 | -   Green
187 | -   Blue
188 | 
189 | 
190 | #### These three ordered lists should be equivalent
191 | 
192 | First:
193 | 
194 | 1.  Bird
195 | 2.  McHale
196 | 3.  Parish
197 | 
198 | Second:
199 | 
200 | 1.  Bird
201 | 1.  McHale
202 | 1.  Parish
203 | 
204 | Third:
205 | 
206 | 3. Bird
207 | 1. McHale
208 | 8. Parish
209 | 
210 | 
211 | #### These two lists should be equivalent
212 | 
213 | First:
214 | 
215 | *   Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
216 |     Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
217 |     viverra nec, fringilla in, laoreet vitae, risus.
218 | *   Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
219 |     Suspendisse id sem consectetuer libero luctus adipiscing.
220 | 
221 | Second:
222 | 
223 | *   Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
224 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,
225 | viverra nec, fringilla in, laoreet vitae, risus.
226 | *   Donec sit amet nisl. Aliquam semper ipsum sit amet velit.
227 | Suspendisse id sem consectetuer libero luctus adipiscing.
228 | 
229 | 
230 | #### Paragraphs and lists
231 | 
232 | *   Bird
233 | *   Magic
234 | 
235 | Blank line separated:
236 | 
237 | *   Bird
238 | 
239 | *   Magic
240 | 
241 | 
242 | #### Paragraphs within lists
243 | 
244 | First:
245 | 
246 | 1.  This is a list item with two paragraphs. Lorem ipsum dolor
247 |     sit amet, consectetuer adipiscing elit. Aliquam hendrerit
248 |     mi posuere lectus.
249 | 
250 |     Vestibulum enim wisi, viverra nec, fringilla in, laoreet
251 |     vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
252 |     sit amet velit.
253 | 
254 | 2.  Suspendisse id sem consectetuer libero luctus adipiscing.
255 | 
256 | Second:
257 | 
258 | *   This is a list item with two paragraphs.
259 | 
260 |     This is the second paragraph in the list item. You're
261 | only required to indent the first line. Lorem ipsum dolor
262 | sit amet, consectetuer adipiscing elit.
263 | 
264 | *   Another item in the same list.
265 | 
266 | 
267 | #### Blockquote within a list
268 | 
269 | *   A list item with a blockquote:
270 | 
271 |     > This is a blockquote
272 |     > inside a list item.
273 | 
274 | 
275 | #### Code within a list
276 | 
277 | *   A list item with a code block:
278 | 
279 |         <code goes here>
280 | 
281 | 
282 | #### Accidental lists
283 | 
284 | 1986. What a great season. (oops, I wanted a year, not a list)
285 | 
286 | 1986\. What a great season. (*whew!* there we go)
287 | 
288 | 
289 | Code blocks
290 | -----------
291 | 
292 | This is a normal paragraph:
293 | 
294 |     This is a code block.
295 | 
296 | 
297 | Here is an example of AppleScript:
298 | 
299 |     tell application "Foo"
300 |         beep
301 |     end tell
302 | 
303 | Markdown will handle the hassle of encoding the ampersands and angle brackets:
304 | 
305 |     <div class="footer">
306 |         &copy; 2004 Foo Corporation
307 |     </div>
308 | 
309 | 
310 |     def this_is
311 |       puts "some #{4-space-indent} code"
312 |     end
313 | 
314 | <code>
315 | print('Code block')
316 | </code>
317 | 
318 | <pre>
319 | print('Pre block')
320 | </pre>
321 | 
322 | 
323 | Horizontal rules
324 | ----------------
325 | 
326 | * * *
327 | 
328 | ***
329 | 
330 | *****
331 | 
332 | - - -
333 | 
334 | ---------------------------------------
335 | 
336 | 
337 | Links
338 | -----
339 | 
340 | Markdown supports two style of links: inline and reference.
341 | 
342 | This is [an example](http://joeyespo.com/ "Title") inline link.
343 | 
344 | [This link](http://joeyespo.com/) has no title attribute.
345 | 
346 | See my [About](/about/) page for some awesome people (*note: broken link*).
347 | 
348 | This is [an example][id] reference-style link.
349 | This is [an example] [id] reference-style link with a space separating the brackets.
350 | 
351 | These should all be equivalent:
352 | 
353 | - [foo 1][]
354 | - [foo 2][]
355 | - [foo 3][]
356 | - [foo 4][]
357 | - [foo 5][FOO 5]
358 | 
359 | 
360 | [id]: http://joeyespo.com/  "Optional Title Here"
361 | [foo 1]: http://joeyespo.com/  "Optional Title Here"
362 | [foo 2]: http://joeyespo.com/  'Optional Title Here'
363 | [foo 3]: http://joeyespo.com/  (Optional Title Here)
364 | [foo 4]: <http://joeyespo.com/>  "Optional Title Here"
365 | [foo 5]: http://joeyespo.com/
366 |     "Optional Title Here"
367 | 
368 | 
369 | Emphasis
370 | --------
371 | 
372 | - *single asterisks*
373 | - _single underscores_
374 | - **double asterisks**
375 | - __double underscores__
376 | - un*frigging*believable
377 | - \*this text is surrounded by literal asterisks\*
378 | 
379 | 
380 | Code
381 | ----
382 | 
383 | - Use the `printf()` function.
384 | - ``There is a literal backtick (`) here.``
385 | - A single backtick in a code span: `` ` ``
386 | - A backtick-delimited string in a code span: `` `foo` ``
387 | - Please don't use any `<blink>` tags.
388 | - `&#8212;` is the decimal-encoded equivalent of `&mdash;`
389 | 
390 | 
391 | Images
392 | ------
393 | 
394 | - ![Alt text](https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico)
395 | - ![Alt text](https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico "Optional title")
396 | - ![Alt text][img]
397 | - <img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" width="32" height="32" /> &nbsp; &larr; bigger &amp; blurrier
398 | 
399 | 
400 | [img]: https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico   "Optional title attribute"
401 | 
402 | 
403 | Automatic links
404 | ---------------
405 | 
406 | - <http://joeyespo.com/>
407 | - <joe@joeyespo.com>
408 | 
409 | 
410 | Backslash escapes
411 | -----------------
412 | 
413 | - \*literal asterisks\*
414 | - \\   backslash
415 | - \`   backtick
416 | - \*   asterisk
417 | - \_   underscore
418 | - \{\}  curly braces
419 | - \[\]  square brackets
420 | - \(\)  parentheses
421 | - \#   hash mark
422 | - \+   plus sign
423 | - \-   minus sign (hyphen)
424 | - \.   dot
425 | - \!   exclamation mark
426 | 
427 | 
428 | GitHub Flavored Markdown
429 | ------------------------
430 | 
431 | See [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/) for details.
432 | 
433 | #### Multiple underscores in words
434 | 
435 | - wow_great_stuff
436 | 
437 | 
438 | #### URL autolinking
439 | 
440 | http://joeyespo.com
441 | 
442 | 
443 | #### Strikethrough
444 | 
445 | ~~Mistaken text.~~
446 | 
447 | 
448 | #### Fenced code blocks
449 | 
450 | ```
451 | function test() {
452 |   console.log("notice the blank line before this function?");
453 | }
454 | ```
455 | 
456 | 
457 | #### Syntax highlighting
458 | 
459 | ```python
460 | print('Hello!')
461 | ```
462 | 
463 | ```javascript
464 | console.log('JavaScript!');
465 | ```
466 | 
467 | ```js
468 | console.log('JavaScript (with js)!');
469 | ```
470 | 
471 | ```unmatched_language
472 | console.log('No matching language, but looks like JavaScript.');
473 | ```
474 | 
475 | 
476 | #### Tables
477 | 
478 | Simple:
479 | 
480 | First Header  | Second Header
481 | ------------- | -------------
482 | Content Cell  | Content Cell
483 | Content Cell  | Content Cell
484 | 
485 | Pipes:
486 | 
487 | | First Header  | Second Header |
488 | | ------------- | ------------- |
489 | | Content Cell  | Content Cell  |
490 | | Content Cell  | Content Cell  |
491 | 
492 | Unmatched:
493 | 
494 | | Name | Description          |
495 | | ------------- | ----------- |
496 | | Help      | Display the help window.|
497 | | Close     | Closes a window     |
498 | 
499 | Inner Markdown:
500 | 
501 | | Name | Description          |
502 | | ------------- | ----------- |
503 | | Help      | ~~Display the~~ help window.|
504 | | Close     | _Closes_ a window     |
505 | 
506 | Alignment:
507 | 
508 | | Left-Aligned  | Center Aligned  | Right Aligned |
509 | | :------------ |:---------------:| -----:|
510 | | col 3 is      | some wordy text | $1600 |
511 | | col 2 is      | centered        |   $12 |
512 | | zebra stripes | are neat        |    $1 |
513 | Text right below a table.
514 | 
515 | 
516 | #### HTML
517 | 
518 | *TODO: Test all allowed HTML tags.*
519 | 
520 | 
521 | Writing on GitHub
522 | -----------------
523 | 
524 | See [this article](https://help.github.com/articles/writing-on-github/) for details.
525 | 
526 | 
527 | ####  Newlines
528 | 
529 | Roses are red
530 | Violets are Blue
531 | 
532 | 
533 | #### Task lists
534 | 
535 | - [x] @mentions, #refs, [links](), **formatting**, and <del>tags</del> are supported
536 | - [x] list syntax is required (any unordered or ordered list supported)
537 | - [x] this is a complete item
538 | - [ ] this is an incomplete item
539 | 
540 | Task lists can be nested to better structure your tasks:
541 | 
542 | - [ ] a bigger project
543 |   - [x] first subtask #1234
544 |   - [ ] follow up subtask #4321
545 |   - [ ] final subtask cc @mention
546 | - [x] a separate task
547 | 
548 | 
549 | #### References
550 | 
551 | * SHA: dbcd7a410ee7489acf92f40641a135fbcf52a768
552 | * User@SHA: joeyespo@dbcd7a410ee7489acf92f40641a135fbcf52a768
553 | * User/Repository@SHA: joeyespo/grip@dbcd7a410ee7489acf92f40641a135fbcf52a768
554 | * #Num: #135
555 | * GH-Num: GH-135
556 | * User#Num: joeyespo#135
557 | * User/Repository#Num: joeyespo/grip#135
558 | 


--------------------------------------------------------------------------------
/tests/input/simple.md:
--------------------------------------------------------------------------------
1 | Simple Test ✓
2 | =============
3 | 
4 | This is just a simple Unicode Markdown test.
5 | 


--------------------------------------------------------------------------------
/tests/input/zero.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/tests/input/zero.md


--------------------------------------------------------------------------------
/tests/mocks.py:
--------------------------------------------------------------------------------
  1 | from __future__ import print_function, unicode_literals
  2 | 
  3 | import json
  4 | 
  5 | import requests
  6 | import responses
  7 | from grip import DEFAULT_API_URL, GitHubAssetManager, Grip, StdinReader
  8 | 
  9 | from helpers import USER_CONTEXT, input_file, output_file
 10 | 
 11 | 
 12 | class GitHubRequestsMock(responses.RequestsMock):
 13 |     auth = ('test-username', 'test-password')
 14 |     bad_auth = ('bad-username', 'bad-password')
 15 | 
 16 |     def __init__(self, assert_all_requests_are_fired=False):
 17 |         super(GitHubRequestsMock, self).__init__(
 18 |             assert_all_requests_are_fired=assert_all_requests_are_fired)
 19 |         self._response_map = {
 20 |             input_file('zero.md'): {
 21 |                 'markdown': output_file('raw', 'zero.html'),
 22 |                 'user-content': output_file('raw', 'zero-user-content.html'),
 23 |                 'user-context': output_file('raw', 'zero-user-context.html'),
 24 |             },
 25 |             input_file('simple.md'): {
 26 |                 'markdown': output_file('raw', 'simple.html'),
 27 |                 'user-content': output_file('raw', 'simple-user-content.html'),
 28 |                 'user-context': output_file('raw', 'simple-user-context.html'),
 29 |             },
 30 |             input_file('gfm-test.md'): {
 31 |                 'markdown': output_file('raw', 'gfm-test.html'),
 32 |                 'user-content': output_file(
 33 |                     'raw', 'gfm-test-user-content.html'),
 34 |                 'user-context': output_file(
 35 |                     'raw', 'gfm-test-user-context.html'),
 36 |             },
 37 |         }
 38 |         self.add_callback(
 39 |             responses.POST, '{0}/markdown'.format(DEFAULT_API_URL),
 40 |             callback=self._markdown_request)
 41 |         self.add_callback(
 42 |             responses.POST, '{0}/markdown/raw'.format(DEFAULT_API_URL),
 43 |             callback=self._markdown_raw_request)
 44 | 
 45 |     def _authenticate(self, request):
 46 |         if 'Authorization' not in request.headers:
 47 |             return None
 48 |         dummy = requests.Request()
 49 |         requests.auth.HTTPBasicAuth(*self.auth)(dummy)
 50 |         if request.headers['Authorization'] != dummy.headers['Authorization']:
 51 |             return (401, {'content-type': 'application/json; charset=utf-8'},
 52 |                     '{"message":"Bad credentials"}')
 53 |         return None
 54 | 
 55 |     def _output_for(self, content, mode=None, context=None):
 56 |         for request_content in self._response_map:
 57 |             if request_content != content:
 58 |                 continue
 59 |             responses = self._response_map[request_content]
 60 |             if mode is None or mode == 'markdown':
 61 |                 return responses['markdown']
 62 |             elif context is None:
 63 |                 return responses['user-content']
 64 |             elif context == USER_CONTEXT:
 65 |                 return responses['user-context']
 66 |             else:
 67 |                 raise ValueError(
 68 |                     'Markdown group not found for user context: {0}'.format(
 69 |                         USER_CONTEXT))
 70 |         raise ValueError('Markdown group not found for: {!r}'.format(content))
 71 | 
 72 |     def _decode_body(self, request):
 73 |         if 'charset=UTF-8' not in request.headers['content-type']:
 74 |             raise ValueError('Expected UTF-8 charset, got: {!r}'.format(
 75 |                 request.headers['content-type']))
 76 |         return request.body.decode('utf-8') if request.body else ''
 77 | 
 78 |     def _markdown_request(self, request):
 79 |         r = self._authenticate(request)
 80 |         if r:
 81 |             return r
 82 |         payload = json.loads(self._decode_body(request))
 83 |         return (200, {'content-type': 'text/html'}, self._output_for(
 84 |             payload['text'], payload['mode'], payload.get('context', None)))
 85 | 
 86 |     def _markdown_raw_request(self, request):
 87 |         r = self._authenticate(request)
 88 |         if r:
 89 |             return r
 90 |         return (200, {'content-type': 'text/html'}, self._output_for(
 91 |             self._decode_body(request)))
 92 | 
 93 | 
 94 | class StdinReaderMock(StdinReader):
 95 |     def __init__(self, mock_stdin, *args, **kwargs):
 96 |         super(StdinReaderMock, self).__init__(*args, **kwargs)
 97 |         self._mock_stdin = mock_stdin
 98 | 
 99 |     def read_stdin(self):
100 |         return self._mock_stdin
101 | 
102 | 
103 | class GitHubAssetManagerMock(GitHubAssetManager):
104 |     def __init__(self, cache_path=None, style_urls=None):
105 |         if cache_path is None:
106 |             cache_path = 'dummy-path'
107 |         super(GitHubAssetManagerMock, self).__init__(cache_path, style_urls)
108 |         self.clear_calls = 0
109 |         self.cache_filename_calls = 0
110 |         self.retrieve_styles_calls = 0
111 | 
112 |     def clear(self):
113 |         self.clear_calls += 1
114 | 
115 |     def cache_filename(self, url):
116 |         self.cache_filename_calls += 1
117 |         return super(GitHubAssetManagerMock, self).cache_filename(url)
118 | 
119 |     def retrieve_styles(self, asset_url_path):
120 |         self.retrieve_styles_calls += 1
121 | 
122 | 
123 | class GripMock(Grip):
124 |     def default_asset_manager(self):
125 |         return GitHubAssetManagerMock()
126 | 


--------------------------------------------------------------------------------
/tests/output/app/simple-user-content.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en" data-color-mode=light data-light-theme=light data-dark-theme=dark>
  3 | <head>
  4 |   <meta charset="utf-8" />
  5 |   <title>README.md - Grip</title>
  6 |   <link rel="icon" href="/__/grip/static/favicon.ico" />
  7 |   <style>
  8 |     /* Page tweaks */
  9 |     .preview-page {
 10 |       margin-top: 64px;
 11 |     }
 12 |     /* Discussion tweaks */
 13 |     .discussion-timeline.wide {
 14 |       width: 920px;
 15 |     }
 16 |     .timeline-comment-wrapper > .timeline-comment:after,
 17 |     .timeline-comment-wrapper > .timeline-comment:before {
 18 |       content: none;
 19 |     }
 20 |   </style>
 21 | </head>
 22 | <body>
 23 |   <div class="page">
 24 |     <div id="preview-page" class="preview-page" data-autorefresh-url="">
 25 |     <div class="container">
 26 |       <div class="repository-with-sidebar repo-container with-full-navigation">
 27 | 
 28 |         
 29 |           <div class="discussion-timeline ">
 30 |             <div class="timeline-comment-wrapper">
 31 |               <div class="timeline-comment">
 32 |                 <div class="comment">
 33 |                   <div class="comment-content">
 34 |                     <div id="grip-content" class="comment-body markdown-body markdown-format">
 35 |                       <h1>Simple Test ✓</h1>
 36 | 
 37 | <p>This is just a simple Unicode Markdown test.</p>
 38 |                     </div>
 39 |                   </div>
 40 |                 </div>
 41 |               </div>
 42 |             </div>
 43 |           </div>
 44 |         
 45 | 
 46 |       </div>
 47 |     </div>
 48 |   </div>
 49 |   <div>&nbsp;</div>
 50 |   </div><script>
 51 |     function showCanonicalImages() {
 52 |       var images = document.getElementsByTagName('img');
 53 |       if (!images) {
 54 |         return;
 55 |       }
 56 |       for (var index = 0; index < images.length; index++) {
 57 |         var image = images[index];
 58 |         if (image.getAttribute('data-canonical-src') && image.src !== image.getAttribute('data-canonical-src')) {
 59 |           image.src = image.getAttribute('data-canonical-src');
 60 |         }
 61 |       }
 62 |     }
 63 | 
 64 |     function scrollToHash() {
 65 |       if (location.hash && !document.querySelector(':target')) {
 66 |         var element = document.getElementById('user-content-' + location.hash.slice(1));
 67 |         if (element) {
 68 |            element.scrollIntoView();
 69 |         }
 70 |       }
 71 |     }
 72 | 
 73 |     function autorefreshContent(eventSourceUrl) {
 74 |       var initialTitle = document.title;
 75 |       var contentElement = document.getElementById('grip-content');
 76 |       var source = new EventSource(eventSourceUrl);
 77 |       var isRendering = false;
 78 | 
 79 |       source.onmessage = function(ev) {
 80 |         var msg = JSON.parse(ev.data);
 81 |         if (msg.updating) {
 82 |           isRendering = true;
 83 |           document.title = '(Rendering) ' + document.title;
 84 |         } else {
 85 |           isRendering = false;
 86 |           document.title = initialTitle;
 87 |           contentElement.innerHTML = msg.content;
 88 |           showCanonicalImages();
 89 |         }
 90 |       }
 91 | 
 92 |       source.onerror = function(e) {
 93 |         if (e.readyState === EventSource.CLOSED && isRendering) {
 94 |           isRendering = false;
 95 |           document.title = initialTitle;
 96 |         }
 97 |       }
 98 |     }
 99 | 
100 |     window.onhashchange = function() {
101 |       scrollToHash();
102 |     }
103 | 
104 |     window.onload = function() {
105 |       scrollToHash();
106 |     }
107 | 
108 |     showCanonicalImages();
109 | 
110 |     var autorefreshUrl = document.getElementById('preview-page').getAttribute('data-autorefresh-url');
111 |     if (autorefreshUrl) {
112 |       autorefreshContent(autorefreshUrl);
113 |     }
114 |   </script>
115 | </body>
116 | </html>


--------------------------------------------------------------------------------
/tests/output/app/simple-user-context.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en" data-color-mode=light data-light-theme=light data-dark-theme=dark>
  3 | <head>
  4 |   <meta charset="utf-8" />
  5 |   <title>simple.md - Grip</title>
  6 |   <link rel="icon" href="/__/grip/static/favicon.ico" />
  7 |   <link rel="stylesheet" href="/__/grip/static/octicons/octicons.css" />
  8 |   <style>
  9 |     /* Page tweaks */
 10 |     .preview-page {
 11 |       margin-top: 64px;
 12 |       margin-bottom: 21px;
 13 |     }
 14 |     /* User-content tweaks */
 15 |     .timeline-comment-wrapper > .timeline-comment:after,
 16 |     .timeline-comment-wrapper > .timeline-comment:before {
 17 |       content: none;
 18 |     }
 19 |     /* User-content overrides */
 20 |     .discussion-timeline.wide {
 21 |       width: 920px;
 22 |     }
 23 |   </style>
 24 | </head>
 25 | <body>
 26 |   <div class="page">
 27 |     <div id="preview-page" class="preview-page" data-autorefresh-url="/__/grip/refresh/">
 28 |     <main id="js-repo-pjax-container">
 29 |       <div class="clearfix new-discussion-timeline container-xl px-3 px-md-4 px-lg-5">
 30 |         <div class="repository-content">
 31 |           <div class="clearfix">
 32 |             <div class="Layout Layout--flowRow-until-md Layout--sidebarPosition-end Layout--sidebarPosition-flowRow-end">
 33 |               <div class="Layout-main">
 34 | 
 35 |                 
 36 | 
 37 |                   <div class="pull-discussion-timeline">
 38 |                     <div class="ml-0 pl-0 ml-md-6 pl-md-3">
 39 |                       <div class="TimelineItem pt-0">
 40 |                         <div class="timeline-comment-group TimelineItem-body my-0">
 41 |                           <div class="ml-n3 timeline-comment unminimized-comment comment previewable-edit editable-comment timeline-comment--caret reorderable-task-lists">
 42 |                             
 43 |                             <div class="edit-comment-hide">
 44 |                               <table class="d-block">
 45 |                                 <tbody class="d-block">
 46 |                                   <tr class="d-block">
 47 |                                     <td class="d-block comment-body markdown-body" id="grip-content">
 48 |                                       <h1 dir="auto">Simple Test ✓</h1>
 49 | <p dir="auto">This is just a simple Unicode Markdown test.</p>
 50 |                                     </td>
 51 |                                   </tr>
 52 |                                 </tbody>
 53 |                               </table>
 54 |                             </div>
 55 |                           </div>
 56 |                         </div>
 57 |                       </div>
 58 |                     </div>
 59 |                   </div>
 60 | 
 61 |                 
 62 | 
 63 |               </div>
 64 |             </div>
 65 |           </div>
 66 |         </div>
 67 |       </div>
 68 |     </main>
 69 |   </div>
 70 |   </div><script>
 71 |     function showCanonicalImages() {
 72 |       var images = document.getElementsByTagName('img');
 73 |       if (!images) {
 74 |         return;
 75 |       }
 76 |       for (var index = 0; index < images.length; index++) {
 77 |         var image = images[index];
 78 |         if (image.getAttribute('data-canonical-src') && image.src !== image.getAttribute('data-canonical-src')) {
 79 |           image.src = image.getAttribute('data-canonical-src');
 80 |         }
 81 |       }
 82 |     }
 83 | 
 84 |     function scrollToHash() {
 85 |       if (location.hash && !document.querySelector(':target')) {
 86 |         var element = document.getElementById('user-content-' + location.hash.slice(1));
 87 |         if (element) {
 88 |            element.scrollIntoView();
 89 |         }
 90 |       }
 91 |     }
 92 | 
 93 |     function autorefreshContent(eventSourceUrl) {
 94 |       var initialTitle = document.title;
 95 |       var contentElement = document.getElementById('grip-content');
 96 |       var source = new EventSource(eventSourceUrl);
 97 |       var isRendering = false;
 98 | 
 99 |       source.onmessage = function(ev) {
100 |         var msg = JSON.parse(ev.data);
101 |         if (msg.updating) {
102 |           isRendering = true;
103 |           document.title = '(Rendering) ' + document.title;
104 |         } else {
105 |           isRendering = false;
106 |           document.title = initialTitle;
107 |           contentElement.innerHTML = msg.content;
108 |           showCanonicalImages();
109 |         }
110 |       }
111 | 
112 |       source.onerror = function(e) {
113 |         if (e.readyState === EventSource.CLOSED && isRendering) {
114 |           isRendering = false;
115 |           document.title = initialTitle;
116 |         }
117 |       }
118 |     }
119 | 
120 |     window.onhashchange = function() {
121 |       scrollToHash();
122 |     }
123 | 
124 |     window.onload = function() {
125 |       scrollToHash();
126 |     }
127 | 
128 |     showCanonicalImages();
129 | 
130 |     var autorefreshUrl = document.getElementById('preview-page').getAttribute('data-autorefresh-url');
131 |     if (autorefreshUrl) {
132 |       autorefreshContent(autorefreshUrl);
133 |     }
134 |   </script>
135 | </body>
136 | </html>


--------------------------------------------------------------------------------
/tests/output/app/simple.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en" data-color-mode=light data-light-theme=light data-dark-theme=dark>
  3 | <head>
  4 |   <meta charset="utf-8" />
  5 |   <title>simple.md - Grip</title>
  6 |   <link rel="icon" href="/__/grip/static/favicon.ico" />
  7 |   <link rel="stylesheet" href="/__/grip/static/octicons/octicons.css" />
  8 |   <style>
  9 |     /* Page tweaks */
 10 |     .preview-page {
 11 |       margin-top: 64px;
 12 |       margin-bottom: 21px;
 13 |     }
 14 |     /* User-content tweaks */
 15 |     .timeline-comment-wrapper > .timeline-comment:after,
 16 |     .timeline-comment-wrapper > .timeline-comment:before {
 17 |       content: none;
 18 |     }
 19 |     /* User-content overrides */
 20 |     .discussion-timeline.wide {
 21 |       width: 920px;
 22 |     }
 23 |   </style>
 24 | </head>
 25 | <body>
 26 |   <div class="page">
 27 |     <div id="preview-page" class="preview-page" data-autorefresh-url="/__/grip/refresh/">
 28 |     <main id="js-repo-pjax-container">
 29 |       <div class="clearfix new-discussion-timeline container-xl px-3 px-md-4 px-lg-5">
 30 |         <div class="repository-content">
 31 |           <div class="clearfix">
 32 |             <div class="Layout Layout--flowRow-until-md Layout--sidebarPosition-end Layout--sidebarPosition-flowRow-end">
 33 |               <div class="Layout-main">
 34 | 
 35 |                 
 36 | 
 37 |                   <div id="readme" class="Box md Box--responsive">
 38 |                     
 39 |                       <div class="Box-header d-flex border-bottom-0 flex-items-center flex-justify-between color-bg-default rounded-top-2">
 40 |                         <div class="d-flex flex-items-center">
 41 |                           <h2 class="Box-title">
 42 |                             simple.md
 43 |                           </h2>
 44 |                         </div>
 45 |                       </div>
 46 |                     
 47 |                     <div class="Box-body px-5 pb-5">
 48 |                       <article id="grip-content" class="markdown-body entry-content container-lg">
 49 |                         <h1>
 50 | <a id="user-content-simple-test-" class="anchor" href="#simple-test-" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Simple Test ✓</h1>
 51 | <p>This is just a simple Unicode Markdown test.</p>
 52 | 
 53 |                       </article>
 54 |                     </div>
 55 |                   </div>
 56 | 
 57 |                 
 58 | 
 59 |               </div>
 60 |             </div>
 61 |           </div>
 62 |         </div>
 63 |       </div>
 64 |     </main>
 65 |   </div>
 66 |   </div><script>
 67 |     function showCanonicalImages() {
 68 |       var images = document.getElementsByTagName('img');
 69 |       if (!images) {
 70 |         return;
 71 |       }
 72 |       for (var index = 0; index < images.length; index++) {
 73 |         var image = images[index];
 74 |         if (image.getAttribute('data-canonical-src') && image.src !== image.getAttribute('data-canonical-src')) {
 75 |           image.src = image.getAttribute('data-canonical-src');
 76 |         }
 77 |       }
 78 |     }
 79 | 
 80 |     function scrollToHash() {
 81 |       if (location.hash && !document.querySelector(':target')) {
 82 |         var element = document.getElementById('user-content-' + location.hash.slice(1));
 83 |         if (element) {
 84 |            element.scrollIntoView();
 85 |         }
 86 |       }
 87 |     }
 88 | 
 89 |     function autorefreshContent(eventSourceUrl) {
 90 |       var initialTitle = document.title;
 91 |       var contentElement = document.getElementById('grip-content');
 92 |       var source = new EventSource(eventSourceUrl);
 93 |       var isRendering = false;
 94 | 
 95 |       source.onmessage = function(ev) {
 96 |         var msg = JSON.parse(ev.data);
 97 |         if (msg.updating) {
 98 |           isRendering = true;
 99 |           document.title = '(Rendering) ' + document.title;
100 |         } else {
101 |           isRendering = false;
102 |           document.title = initialTitle;
103 |           contentElement.innerHTML = msg.content;
104 |           showCanonicalImages();
105 |         }
106 |       }
107 | 
108 |       source.onerror = function(e) {
109 |         if (e.readyState === EventSource.CLOSED && isRendering) {
110 |           isRendering = false;
111 |           document.title = initialTitle;
112 |         }
113 |       }
114 |     }
115 | 
116 |     window.onhashchange = function() {
117 |       scrollToHash();
118 |     }
119 | 
120 |     window.onload = function() {
121 |       scrollToHash();
122 |     }
123 | 
124 |     showCanonicalImages();
125 | 
126 |     var autorefreshUrl = document.getElementById('preview-page').getAttribute('data-autorefresh-url');
127 |     if (autorefreshUrl) {
128 |       autorefreshContent(autorefreshUrl);
129 |     }
130 |   </script>
131 | </body>
132 | </html>


--------------------------------------------------------------------------------
/tests/output/app/zero-user-content.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en" data-color-mode=light data-light-theme=light data-dark-theme=dark>
  3 | <head>
  4 |   <meta charset="utf-8" />
  5 |   <title>zero.md - Grip</title>
  6 |   <link rel="icon" href="/__/grip/static/favicon.ico" />
  7 |   <style>
  8 |     /* Page tweaks */
  9 |     .preview-page {
 10 |       margin-top: 64px;
 11 |     }
 12 |     /* Discussion tweaks */
 13 |     .discussion-timeline.wide {
 14 |       width: 920px;
 15 |     }
 16 |     .timeline-comment-wrapper > .timeline-comment:after,
 17 |     .timeline-comment-wrapper > .timeline-comment:before {
 18 |       content: none;
 19 |     }
 20 |   </style>
 21 | </head>
 22 | <body>
 23 |   <div class="page">
 24 |     <div id="preview-page" class="preview-page" data-autorefresh-url="">
 25 |     <div class="container">
 26 |       <div class="repository-with-sidebar repo-container with-full-navigation">
 27 | 
 28 |         
 29 |           <div class="discussion-timeline ">
 30 |             <div class="timeline-comment-wrapper">
 31 |               <div class="timeline-comment">
 32 |                 <div class="comment">
 33 |                   <div class="comment-content">
 34 |                     <div id="grip-content" class="comment-body markdown-body markdown-format">
 35 |                       
 36 |                     </div>
 37 |                   </div>
 38 |                 </div>
 39 |               </div>
 40 |             </div>
 41 |           </div>
 42 |         
 43 | 
 44 |       </div>
 45 |     </div>
 46 |   </div>
 47 |   <div>&nbsp;</div>
 48 |   </div><script>
 49 |     function showCanonicalImages() {
 50 |       var images = document.getElementsByTagName('img');
 51 |       if (!images) {
 52 |         return;
 53 |       }
 54 |       for (var index = 0; index < images.length; index++) {
 55 |         var image = images[index];
 56 |         if (image.getAttribute('data-canonical-src') && image.src !== image.getAttribute('data-canonical-src')) {
 57 |           image.src = image.getAttribute('data-canonical-src');
 58 |         }
 59 |       }
 60 |     }
 61 | 
 62 |     function scrollToHash() {
 63 |       if (location.hash && !document.querySelector(':target')) {
 64 |         var element = document.getElementById('user-content-' + location.hash.slice(1));
 65 |         if (element) {
 66 |            element.scrollIntoView();
 67 |         }
 68 |       }
 69 |     }
 70 | 
 71 |     function autorefreshContent(eventSourceUrl) {
 72 |       var initialTitle = document.title;
 73 |       var contentElement = document.getElementById('grip-content');
 74 |       var source = new EventSource(eventSourceUrl);
 75 |       var isRendering = false;
 76 | 
 77 |       source.onmessage = function(ev) {
 78 |         var msg = JSON.parse(ev.data);
 79 |         if (msg.updating) {
 80 |           isRendering = true;
 81 |           document.title = '(Rendering) ' + document.title;
 82 |         } else {
 83 |           isRendering = false;
 84 |           document.title = initialTitle;
 85 |           contentElement.innerHTML = msg.content;
 86 |           showCanonicalImages();
 87 |         }
 88 |       }
 89 | 
 90 |       source.onerror = function(e) {
 91 |         if (e.readyState === EventSource.CLOSED && isRendering) {
 92 |           isRendering = false;
 93 |           document.title = initialTitle;
 94 |         }
 95 |       }
 96 |     }
 97 | 
 98 |     window.onhashchange = function() {
 99 |       scrollToHash();
100 |     }
101 | 
102 |     window.onload = function() {
103 |       scrollToHash();
104 |     }
105 | 
106 |     showCanonicalImages();
107 | 
108 |     var autorefreshUrl = document.getElementById('preview-page').getAttribute('data-autorefresh-url');
109 |     if (autorefreshUrl) {
110 |       autorefreshContent(autorefreshUrl);
111 |     }
112 |   </script>
113 | </body>
114 | </html>


--------------------------------------------------------------------------------
/tests/output/app/zero-user-context.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en" data-color-mode=light data-light-theme=light data-dark-theme=dark>
  3 | <head>
  4 |   <meta charset="utf-8" />
  5 |   <title>zero.md - Grip</title>
  6 |   <link rel="icon" href="/__/grip/static/favicon.ico" />
  7 |   <link rel="stylesheet" href="/__/grip/static/octicons/octicons.css" />
  8 |   <style>
  9 |     /* Page tweaks */
 10 |     .preview-page {
 11 |       margin-top: 64px;
 12 |       margin-bottom: 21px;
 13 |     }
 14 |     /* User-content tweaks */
 15 |     .timeline-comment-wrapper > .timeline-comment:after,
 16 |     .timeline-comment-wrapper > .timeline-comment:before {
 17 |       content: none;
 18 |     }
 19 |     /* User-content overrides */
 20 |     .discussion-timeline.wide {
 21 |       width: 920px;
 22 |     }
 23 |   </style>
 24 | </head>
 25 | <body>
 26 |   <div class="page">
 27 |     <div id="preview-page" class="preview-page" data-autorefresh-url="/__/grip/refresh/">
 28 |     <main id="js-repo-pjax-container">
 29 |       <div class="clearfix new-discussion-timeline container-xl px-3 px-md-4 px-lg-5">
 30 |         <div class="repository-content">
 31 |           <div class="clearfix">
 32 |             <div class="Layout Layout--flowRow-until-md Layout--sidebarPosition-end Layout--sidebarPosition-flowRow-end">
 33 |               <div class="Layout-main">
 34 | 
 35 |                 
 36 | 
 37 |                   <div class="pull-discussion-timeline">
 38 |                     <div class="ml-0 pl-0 ml-md-6 pl-md-3">
 39 |                       <div class="TimelineItem pt-0">
 40 |                         <div class="timeline-comment-group TimelineItem-body my-0">
 41 |                           <div class="ml-n3 timeline-comment unminimized-comment comment previewable-edit editable-comment timeline-comment--caret reorderable-task-lists">
 42 |                             
 43 |                             <div class="edit-comment-hide">
 44 |                               <table class="d-block">
 45 |                                 <tbody class="d-block">
 46 |                                   <tr class="d-block">
 47 |                                     <td class="d-block comment-body markdown-body" id="grip-content">
 48 |                                       
 49 |                                     </td>
 50 |                                   </tr>
 51 |                                 </tbody>
 52 |                               </table>
 53 |                             </div>
 54 |                           </div>
 55 |                         </div>
 56 |                       </div>
 57 |                     </div>
 58 |                   </div>
 59 | 
 60 |                 
 61 | 
 62 |               </div>
 63 |             </div>
 64 |           </div>
 65 |         </div>
 66 |       </div>
 67 |     </main>
 68 |   </div>
 69 |   </div><script>
 70 |     function showCanonicalImages() {
 71 |       var images = document.getElementsByTagName('img');
 72 |       if (!images) {
 73 |         return;
 74 |       }
 75 |       for (var index = 0; index < images.length; index++) {
 76 |         var image = images[index];
 77 |         if (image.getAttribute('data-canonical-src') && image.src !== image.getAttribute('data-canonical-src')) {
 78 |           image.src = image.getAttribute('data-canonical-src');
 79 |         }
 80 |       }
 81 |     }
 82 | 
 83 |     function scrollToHash() {
 84 |       if (location.hash && !document.querySelector(':target')) {
 85 |         var element = document.getElementById('user-content-' + location.hash.slice(1));
 86 |         if (element) {
 87 |            element.scrollIntoView();
 88 |         }
 89 |       }
 90 |     }
 91 | 
 92 |     function autorefreshContent(eventSourceUrl) {
 93 |       var initialTitle = document.title;
 94 |       var contentElement = document.getElementById('grip-content');
 95 |       var source = new EventSource(eventSourceUrl);
 96 |       var isRendering = false;
 97 | 
 98 |       source.onmessage = function(ev) {
 99 |         var msg = JSON.parse(ev.data);
100 |         if (msg.updating) {
101 |           isRendering = true;
102 |           document.title = '(Rendering) ' + document.title;
103 |         } else {
104 |           isRendering = false;
105 |           document.title = initialTitle;
106 |           contentElement.innerHTML = msg.content;
107 |           showCanonicalImages();
108 |         }
109 |       }
110 | 
111 |       source.onerror = function(e) {
112 |         if (e.readyState === EventSource.CLOSED && isRendering) {
113 |           isRendering = false;
114 |           document.title = initialTitle;
115 |         }
116 |       }
117 |     }
118 | 
119 |     window.onhashchange = function() {
120 |       scrollToHash();
121 |     }
122 | 
123 |     window.onload = function() {
124 |       scrollToHash();
125 |     }
126 | 
127 |     showCanonicalImages();
128 | 
129 |     var autorefreshUrl = document.getElementById('preview-page').getAttribute('data-autorefresh-url');
130 |     if (autorefreshUrl) {
131 |       autorefreshContent(autorefreshUrl);
132 |     }
133 |   </script>
134 | </body>
135 | </html>


--------------------------------------------------------------------------------
/tests/output/app/zero.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en" data-color-mode=light data-light-theme=light data-dark-theme=dark>
  3 | <head>
  4 |   <meta charset="utf-8" />
  5 |   <title>zero.md - Grip</title>
  6 |   <link rel="icon" href="/__/grip/static/favicon.ico" />
  7 |   <link rel="stylesheet" href="/__/grip/static/octicons/octicons.css" />
  8 |   <style>
  9 |     /* Page tweaks */
 10 |     .preview-page {
 11 |       margin-top: 64px;
 12 |       margin-bottom: 21px;
 13 |     }
 14 |     /* User-content tweaks */
 15 |     .timeline-comment-wrapper > .timeline-comment:after,
 16 |     .timeline-comment-wrapper > .timeline-comment:before {
 17 |       content: none;
 18 |     }
 19 |     /* User-content overrides */
 20 |     .discussion-timeline.wide {
 21 |       width: 920px;
 22 |     }
 23 |   </style>
 24 | </head>
 25 | <body>
 26 |   <div class="page">
 27 |     <div id="preview-page" class="preview-page" data-autorefresh-url="/__/grip/refresh/">
 28 |     <main id="js-repo-pjax-container">
 29 |       <div class="clearfix new-discussion-timeline container-xl px-3 px-md-4 px-lg-5">
 30 |         <div class="repository-content">
 31 |           <div class="clearfix">
 32 |             <div class="Layout Layout--flowRow-until-md Layout--sidebarPosition-end Layout--sidebarPosition-flowRow-end">
 33 |               <div class="Layout-main">
 34 | 
 35 |                 
 36 | 
 37 |                   <div id="readme" class="Box md Box--responsive">
 38 |                     
 39 |                       <div class="Box-header d-flex border-bottom-0 flex-items-center flex-justify-between color-bg-default rounded-top-2">
 40 |                         <div class="d-flex flex-items-center">
 41 |                           <h2 class="Box-title">
 42 |                             zero.md
 43 |                           </h2>
 44 |                         </div>
 45 |                       </div>
 46 |                     
 47 |                     <div class="Box-body px-5 pb-5">
 48 |                       <article id="grip-content" class="markdown-body entry-content container-lg">
 49 |                         
 50 |                       </article>
 51 |                     </div>
 52 |                   </div>
 53 | 
 54 |                 
 55 | 
 56 |               </div>
 57 |             </div>
 58 |           </div>
 59 |         </div>
 60 |       </div>
 61 |     </main>
 62 |   </div>
 63 |   </div><script>
 64 |     function showCanonicalImages() {
 65 |       var images = document.getElementsByTagName('img');
 66 |       if (!images) {
 67 |         return;
 68 |       }
 69 |       for (var index = 0; index < images.length; index++) {
 70 |         var image = images[index];
 71 |         if (image.getAttribute('data-canonical-src') && image.src !== image.getAttribute('data-canonical-src')) {
 72 |           image.src = image.getAttribute('data-canonical-src');
 73 |         }
 74 |       }
 75 |     }
 76 | 
 77 |     function scrollToHash() {
 78 |       if (location.hash && !document.querySelector(':target')) {
 79 |         var element = document.getElementById('user-content-' + location.hash.slice(1));
 80 |         if (element) {
 81 |            element.scrollIntoView();
 82 |         }
 83 |       }
 84 |     }
 85 | 
 86 |     function autorefreshContent(eventSourceUrl) {
 87 |       var initialTitle = document.title;
 88 |       var contentElement = document.getElementById('grip-content');
 89 |       var source = new EventSource(eventSourceUrl);
 90 |       var isRendering = false;
 91 | 
 92 |       source.onmessage = function(ev) {
 93 |         var msg = JSON.parse(ev.data);
 94 |         if (msg.updating) {
 95 |           isRendering = true;
 96 |           document.title = '(Rendering) ' + document.title;
 97 |         } else {
 98 |           isRendering = false;
 99 |           document.title = initialTitle;
100 |           contentElement.innerHTML = msg.content;
101 |           showCanonicalImages();
102 |         }
103 |       }
104 | 
105 |       source.onerror = function(e) {
106 |         if (e.readyState === EventSource.CLOSED && isRendering) {
107 |           isRendering = false;
108 |           document.title = initialTitle;
109 |         }
110 |       }
111 |     }
112 | 
113 |     window.onhashchange = function() {
114 |       scrollToHash();
115 |     }
116 | 
117 |     window.onload = function() {
118 |       scrollToHash();
119 |     }
120 | 
121 |     showCanonicalImages();
122 | 
123 |     var autorefreshUrl = document.getElementById('preview-page').getAttribute('data-autorefresh-url');
124 |     if (autorefreshUrl) {
125 |       autorefreshContent(autorefreshUrl);
126 |     }
127 |   </script>
128 | </body>
129 | </html>


--------------------------------------------------------------------------------
/tests/output/raw/gfm-test-user-content.html:
--------------------------------------------------------------------------------
  1 | <h1>GitHub Flavored Markdown Test</h1>
  2 | <p>This Markdown file contains all the features of <strong>GitHub Flavored Markdown</strong> for<br>
  3 | testing a renderer with.</p>
  4 | <p>The features are taken directly from <a href="https://daringfireball.net/projects/markdown/syntax" rel="nofollow">Daring Fireball</a><br>
  5 | and <a href="https://help.github.com/articles/github-flavored-markdown/">GitHub Flavored Markdown</a>,<br>
  6 | followed by a list of one-offs.</p>
  7 | <h2>Inline HTML</h2>
  8 | <table role="table">
  9 |   <tbody><tr>
 10 |     <td>Foo</td>
 11 |   </tr>
 12 |   <tr>
 13 |     <td>
 14 |       Note that Markdown formatting syntax is not processed within block-level HTML tags.
 15 |       E.g., you can’t use Markdown-style *emphasis* inside an HTML block.
 16 |     </td>
 17 |   </tr>
 18 | </tbody></table>
 19 | <p>Span-level HTML tags — e.g. &lt;span&gt;, &lt;cite&gt;, or &lt;del&gt; — can be used anywhere<br>
 20 | in a Markdown paragraph, list item, or header. If you want, you can even use HTML tags instead<br>
 21 | of Markdown formatting; e.g. if you’d prefer to use HTML &lt;a&gt; or &lt;img&gt; tags instead<br>
 22 | of Markdown’s link or image syntax, go right ahead.</p>
 23 | <h4>Testing <span>span</span>, cite, and <del>del</del> tags</h4>
 24 | <p>Testing <span>span</span>, cite, and <del>del</del> tags in a paragraph.</p>
 25 | <p>And each element in its own list item:</p>
 26 | <ul>
 27 | <li><span>This is within a span tag.</span></li>
 28 | <li>This is within a cite tag.</li>
 29 | <li><del>This is within a del tag.</del></li>
 30 | </ul>
 31 | <h2>Automatic escaping for special characters</h2>
 32 | <ul>
 33 | <li>© copyright</li>
 34 | <li>AT&amp;T should render the same as AT&amp;T</li>
 35 | <li>4 &lt; 5 should render the same as 4 &lt; 5</li>
 36 | </ul>
 37 | <p>Note that GitHub Flavored Markdown has URL autolinking, which will <em>not</em><br>
 38 | convert <code>&amp;amp;</code>. So these two should yield different links:</p>
 39 | <ul>
 40 | <li><a href="http://images.google.com/images?num=30&amp;q=larry+bird" rel="nofollow">http://images.google.com/images?num=30&amp;q=larry+bird</a></li>
 41 | <li><a href="http://images.google.com/images?num=30&amp;amp;q=larry+bird" rel="nofollow">http://images.google.com/images?num=30&amp;amp;q=larry+bird</a></li>
 42 | </ul>
 43 | <h2>Paragraphs and line breaks</h2>
 44 | <p>This is a normal paragraph.</p>
 45 | <p>This and the next sentence is separated by a single newline.<br>
 46 | This should be on the same line.</p>
 47 | <p>This and the next sentence is joined by a single <code>&lt;br /&gt;</code>. <br><br>
 48 | This should be on a new line, directly below.</p>
 49 | <p>These two sentences are separated by two <code>&lt;br /&gt;</code> tags. <br><br><br>
 50 | This should be two lines below.</p>
 51 | <p>These two paragraphs are separated by two <code>&lt;br /&gt;</code> tags. <br><br></p>
 52 | <p>This should be three lines below.</p>
 53 | <h2>Headers</h2>
 54 | <p>Here are some headers followed by <a href="https://en.wikipedia.org/wiki/Lorem_ipsum" rel="nofollow">Lorem Ipsum</a>.</p>
 55 | <h1>This is an H1 (Setext-style)</h1>
 56 | <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
 57 | <h2>This is an H2 (Setext-style)</h2>
 58 | <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
 59 | <p>The following are atx-style.</p>
 60 | <h1>This is an H1</h1>
 61 | <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
 62 | <h2>This is an H2</h2>
 63 | <p>Mauris feugiat, augue vitae sollicitudin vulputate, neque arcu dapibus eros, eget semper lorem ex rhoncus nulla.</p>
 64 | <h3>This is an H3</h3>
 65 | <p>Etiam sit amet orci sit amet dui mollis molestie.</p>
 66 | <h4>This is an H4</h4>
 67 | <p>Cras et elit egestas, lacinia est eu, vestibulum enim.</p>
 68 | <h5>This is an H5</h5>
 69 | <p>Phasellus sed suscipit quam.</p>
 70 | <h6>This is an H√36</h6>
 71 | <p>Nam rutrum imperdiet purus, sit amet porttitor augue tempor quis.</p>
 72 | <h2>Blockquotes</h2>
 73 | <p>Email-style blockquotes:</p>
 74 | <blockquote>
 75 | <p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,<br>
 76 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.<br>
 77 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
 78 | <p>Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse<br>
 79 | id sem consectetuer libero luctus adipiscing.</p>
 80 | </blockquote>
 81 | <p>Putting the &gt; before the first line of a hard-wrapped paragraph:</p>
 82 | <blockquote>
 83 | <p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,<br>
 84 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.<br>
 85 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
 86 | </blockquote>
 87 | <blockquote>
 88 | <p>Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse<br>
 89 | id sem consectetuer libero luctus adipiscing.</p>
 90 | </blockquote>
 91 | <p>Nested blockquotes (i.e. a blockquote-in-a-blockquote):</p>
 92 | <blockquote>
 93 | <p>This is the first level of quoting.</p>
 94 | <blockquote>
 95 | <p>This is nested blockquote.</p>
 96 | </blockquote>
 97 | <p>Back to the first level.</p>
 98 | </blockquote>
 99 | <p>Blockquotes containing other Markdown elements:</p>
100 | <blockquote>
101 | <h2>This is a header.</h2>
102 | <ol>
103 | <li>This is the first list item.</li>
104 | <li>This is the second list item.</li>
105 | </ol>
106 | <p>Here's some example code:</p>
107 | <pre><code>return shell_exec("echo $input | $markdown_script");
108 | </code></pre>
109 | </blockquote>
110 | <h2>Lists</h2>
111 | <h5>These three lists should be equivalent</h5>
112 | <p>First:</p>
113 | <ul>
114 | <li>Red</li>
115 | <li>Green</li>
116 | <li>Blue</li>
117 | </ul>
118 | <p>Second:</p>
119 | <ul>
120 | <li>Red</li>
121 | <li>Green</li>
122 | <li>Blue</li>
123 | </ul>
124 | <p>Third:</p>
125 | <ul>
126 | <li>Red</li>
127 | <li>Green</li>
128 | <li>Blue</li>
129 | </ul>
130 | <h4>These three ordered lists should be equivalent</h4>
131 | <p>First:</p>
132 | <ol>
133 | <li>Bird</li>
134 | <li>McHale</li>
135 | <li>Parish</li>
136 | </ol>
137 | <p>Second:</p>
138 | <ol>
139 | <li>Bird</li>
140 | <li>McHale</li>
141 | <li>Parish</li>
142 | </ol>
143 | <p>Third:</p>
144 | <ol start="3">
145 | <li>Bird</li>
146 | <li>McHale</li>
147 | <li>Parish</li>
148 | </ol>
149 | <h4>These two lists should be equivalent</h4>
150 | <p>First:</p>
151 | <ul>
152 | <li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.<br>
153 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,<br>
154 | viverra nec, fringilla in, laoreet vitae, risus.</li>
155 | <li>Donec sit amet nisl. Aliquam semper ipsum sit amet velit.<br>
156 | Suspendisse id sem consectetuer libero luctus adipiscing.</li>
157 | </ul>
158 | <p>Second:</p>
159 | <ul>
160 | <li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.<br>
161 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,<br>
162 | viverra nec, fringilla in, laoreet vitae, risus.</li>
163 | <li>Donec sit amet nisl. Aliquam semper ipsum sit amet velit.<br>
164 | Suspendisse id sem consectetuer libero luctus adipiscing.</li>
165 | </ul>
166 | <h4>Paragraphs and lists</h4>
167 | <ul>
168 | <li>Bird</li>
169 | <li>Magic</li>
170 | </ul>
171 | <p>Blank line separated:</p>
172 | <ul>
173 | <li>
174 | <p>Bird</p>
175 | </li>
176 | <li>
177 | <p>Magic</p>
178 | </li>
179 | </ul>
180 | <h4>Paragraphs within lists</h4>
181 | <p>First:</p>
182 | <ol>
183 | <li>
184 | <p>This is a list item with two paragraphs. Lorem ipsum dolor<br>
185 | sit amet, consectetuer adipiscing elit. Aliquam hendrerit<br>
186 | mi posuere lectus.</p>
187 | <p>Vestibulum enim wisi, viverra nec, fringilla in, laoreet<br>
188 | vitae, risus. Donec sit amet nisl. Aliquam semper ipsum<br>
189 | sit amet velit.</p>
190 | </li>
191 | <li>
192 | <p>Suspendisse id sem consectetuer libero luctus adipiscing.</p>
193 | </li>
194 | </ol>
195 | <p>Second:</p>
196 | <ul>
197 | <li>
198 | <p>This is a list item with two paragraphs.</p>
199 | <p>This is the second paragraph in the list item. You're<br>
200 | only required to indent the first line. Lorem ipsum dolor<br>
201 | sit amet, consectetuer adipiscing elit.</p>
202 | </li>
203 | <li>
204 | <p>Another item in the same list.</p>
205 | </li>
206 | </ul>
207 | <h4>Blockquote within a list</h4>
208 | <ul>
209 | <li>
210 | <p>A list item with a blockquote:</p>
211 | <blockquote>
212 | <p>This is a blockquote<br>
213 | inside a list item.</p>
214 | </blockquote>
215 | </li>
216 | </ul>
217 | <h4>Code within a list</h4>
218 | <ul>
219 | <li>
220 | <p>A list item with a code block:</p>
221 | <pre><code>&lt;code goes here&gt;
222 | </code></pre>
223 | </li>
224 | </ul>
225 | <h4>Accidental lists</h4>
226 | <ol start="1986">
227 | <li>What a great season. (oops, I wanted a year, not a list)</li>
228 | </ol>
229 | <p>1986. What a great season. (<em>whew!</em> there we go)</p>
230 | <h2>Code blocks</h2>
231 | <p>This is a normal paragraph:</p>
232 | <pre><code>This is a code block.
233 | </code></pre>
234 | <p>Here is an example of AppleScript:</p>
235 | <pre><code>tell application "Foo"
236 |     beep
237 | end tell
238 | </code></pre>
239 | <p>Markdown will handle the hassle of encoding the ampersands and angle brackets:</p>
240 | <pre><code>&lt;div class="footer"&gt;
241 |     &amp;copy; 2004 Foo Corporation
242 | &lt;/div&gt;
243 | 
244 | 
245 | def this_is
246 |   puts "some #{4-space-indent} code"
247 | end
248 | </code></pre>
249 | <code>
250 | print('Code block')
251 | </code>
252 | <pre>print('Pre block')
253 | </pre>
254 | <h2>Horizontal rules</h2>
255 | <hr>
256 | <hr>
257 | <hr>
258 | <hr>
259 | <hr>
260 | <h2>Links</h2>
261 | <p>Markdown supports two style of links: inline and reference.</p>
262 | <p>This is <a href="http://joeyespo.com/" title="Title" rel="nofollow">an example</a> inline link.</p>
263 | <p><a href="http://joeyespo.com/" rel="nofollow">This link</a> has no title attribute.</p>
264 | <p>See my <a href="/about/">About</a> page for some awesome people (<em>note: broken link</em>).</p>
265 | <p>This is <a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">an example</a> reference-style link.<br>
266 | This is [an example] <a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">id</a> reference-style link with a space separating the brackets.</p>
267 | <p>These should all be equivalent:</p>
268 | <ul>
269 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 1</a></li>
270 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 2</a></li>
271 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 3</a></li>
272 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 4</a></li>
273 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 5</a></li>
274 | </ul>
275 | <h2>Emphasis</h2>
276 | <ul>
277 | <li><em>single asterisks</em></li>
278 | <li><em>single underscores</em></li>
279 | <li><strong>double asterisks</strong></li>
280 | <li><strong>double underscores</strong></li>
281 | <li>un<em>frigging</em>believable</li>
282 | <li>*this text is surrounded by literal asterisks*</li>
283 | </ul>
284 | <h2>Code</h2>
285 | <ul>
286 | <li>Use the <code>printf()</code> function.</li>
287 | <li><code>There is a literal backtick (`) here.</code></li>
288 | <li>A single backtick in a code span: <code>`</code></li>
289 | <li>A backtick-delimited string in a code span: <code>`foo`</code></li>
290 | <li>Please don't use any <code>&lt;blink&gt;</code> tags.</li>
291 | <li><code>&amp;#8212;</code> is the decimal-encoded equivalent of <code>&amp;mdash;</code></li>
292 | </ul>
293 | <h2>Images</h2>
294 | <ul>
295 | <li><a target="_blank" rel="noopener noreferrer" href="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico"><img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" alt="Alt text" style="max-width: 100%;"></a></li>
296 | <li><a target="_blank" rel="noopener noreferrer" href="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico"><img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" alt="Alt text" title="Optional title" style="max-width: 100%;"></a></li>
297 | <li><a target="_blank" rel="noopener noreferrer" href="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico"><img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" alt="Alt text" title="Optional title attribute" style="max-width: 100%;"></a></li>
298 | <li><a target="_blank" rel="noopener noreferrer" href="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico"><img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" width="32" height="32" style="max-width: 100%;"></a>   ← bigger &amp; blurrier</li>
299 | </ul>
300 | <h2>Automatic links</h2>
301 | <ul>
302 | <li><a href="http://joeyespo.com/" rel="nofollow">http://joeyespo.com/</a></li>
303 | <li><a href="mailto:joe@joeyespo.com">joe@joeyespo.com</a></li>
304 | </ul>
305 | <h2>Backslash escapes</h2>
306 | <ul>
307 | <li>*literal asterisks*</li>
308 | <li>\   backslash</li>
309 | <li>`   backtick</li>
310 | <li>*   asterisk</li>
311 | <li>_   underscore</li>
312 | <li>{}  curly braces</li>
313 | <li>[]  square brackets</li>
314 | <li>()  parentheses</li>
315 | <li>#   hash mark</li>
316 | <li>+   plus sign</li>
317 | <li>-   minus sign (hyphen)</li>
318 | <li>.   dot</li>
319 | <li>!   exclamation mark</li>
320 | </ul>
321 | <h2>GitHub Flavored Markdown</h2>
322 | <p>See <a href="https://help.github.com/articles/github-flavored-markdown/">GitHub Flavored Markdown</a> for details.</p>
323 | <h4>Multiple underscores in words</h4>
324 | <ul>
325 | <li>wow_great_stuff</li>
326 | </ul>
327 | <h4>URL autolinking</h4>
328 | <p><a href="http://joeyespo.com" rel="nofollow">http://joeyespo.com</a></p>
329 | <h4>Strikethrough</h4>
330 | <p><del>Mistaken text.</del></p>
331 | <h4>Fenced code blocks</h4>
332 | <pre><code>function test() {
333 |   console.log("notice the blank line before this function?");
334 | }
335 | </code></pre>
336 | <h4>Syntax highlighting</h4>
337 | <div class="highlight highlight-source-python"><pre><span class="pl-en">print</span>(<span class="pl-s">'Hello!'</span>)</pre></div>
338 | <div class="highlight highlight-source-js"><pre><span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'JavaScript!'</span><span class="pl-kos">)</span><span class="pl-kos">;</span></pre></div>
339 | <div class="highlight highlight-source-js"><pre><span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'JavaScript (with js)!'</span><span class="pl-kos">)</span><span class="pl-kos">;</span></pre></div>
340 | <pre lang="unmatched_language"><code>console.log('No matching language, but looks like JavaScript.');
341 | </code></pre>
342 | <h4>Tables</h4>
343 | <p>Simple:</p>
344 | <table role="table">
345 | <thead>
346 | <tr>
347 | <th>First Header</th>
348 | <th>Second Header</th>
349 | </tr>
350 | </thead>
351 | <tbody>
352 | <tr>
353 | <td>Content Cell</td>
354 | <td>Content Cell</td>
355 | </tr>
356 | <tr>
357 | <td>Content Cell</td>
358 | <td>Content Cell</td>
359 | </tr>
360 | </tbody>
361 | </table>
362 | <p>Pipes:</p>
363 | <table role="table">
364 | <thead>
365 | <tr>
366 | <th>First Header</th>
367 | <th>Second Header</th>
368 | </tr>
369 | </thead>
370 | <tbody>
371 | <tr>
372 | <td>Content Cell</td>
373 | <td>Content Cell</td>
374 | </tr>
375 | <tr>
376 | <td>Content Cell</td>
377 | <td>Content Cell</td>
378 | </tr>
379 | </tbody>
380 | </table>
381 | <p>Unmatched:</p>
382 | <table role="table">
383 | <thead>
384 | <tr>
385 | <th>Name</th>
386 | <th>Description</th>
387 | </tr>
388 | </thead>
389 | <tbody>
390 | <tr>
391 | <td>Help</td>
392 | <td>Display the help window.</td>
393 | </tr>
394 | <tr>
395 | <td>Close</td>
396 | <td>Closes a window</td>
397 | </tr>
398 | </tbody>
399 | </table>
400 | <p>Inner Markdown:</p>
401 | <table role="table">
402 | <thead>
403 | <tr>
404 | <th>Name</th>
405 | <th>Description</th>
406 | </tr>
407 | </thead>
408 | <tbody>
409 | <tr>
410 | <td>Help</td>
411 | <td><del>Display the</del> help window.</td>
412 | </tr>
413 | <tr>
414 | <td>Close</td>
415 | <td><em>Closes</em> a window</td>
416 | </tr>
417 | </tbody>
418 | </table>
419 | <p>Alignment:</p>
420 | <table role="table">
421 | <thead>
422 | <tr>
423 | <th align="left">Left-Aligned</th>
424 | <th align="center">Center Aligned</th>
425 | <th align="right">Right Aligned</th>
426 | </tr>
427 | </thead>
428 | <tbody>
429 | <tr>
430 | <td align="left">col 3 is</td>
431 | <td align="center">some wordy text</td>
432 | <td align="right">$1600</td>
433 | </tr>
434 | <tr>
435 | <td align="left">col 2 is</td>
436 | <td align="center">centered</td>
437 | <td align="right">$12</td>
438 | </tr>
439 | <tr>
440 | <td align="left">zebra stripes</td>
441 | <td align="center">are neat</td>
442 | <td align="right">$1</td>
443 | </tr>
444 | <tr>
445 | <td align="left">Text right below a table.</td>
446 | <td align="center"></td>
447 | <td align="right"></td>
448 | </tr>
449 | </tbody>
450 | </table>
451 | <h4>HTML</h4>
452 | <p><em>TODO: Test all allowed HTML tags.</em></p>
453 | <h2>Writing on GitHub</h2>
454 | <p>See <a href="https://help.github.com/articles/writing-on-github/">this article</a> for details.</p>
455 | <h4>Newlines</h4>
456 | <p>Roses are red<br>
457 | Violets are Blue</p>
458 | <h4>Task lists</h4>
459 | <ul class="contains-task-list">
460 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> @mentions, #refs, <a href="">links</a>, <strong>formatting</strong>, and <del>tags</del> are supported</li>
461 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> list syntax is required (any unordered or ordered list supported)</li>
462 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> this is a complete item</li>
463 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> this is an incomplete item</li>
464 | </ul>
465 | <p>Task lists can be nested to better structure your tasks:</p>
466 | <ul class="contains-task-list">
467 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> a bigger project
468 | <ul class="contains-task-list">
469 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> first subtask #1234</li>
470 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> follow up subtask #4321</li>
471 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> final subtask cc @mention</li>
472 | </ul>
473 | </li>
474 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> a separate task</li>
475 | </ul>
476 | <h4>References</h4>
477 | <ul>
478 | <li>SHA: dbcd7a410ee7489acf92f40641a135fbcf52a768</li>
479 | <li>User@SHA: joeyespo@dbcd7a410ee7489acf92f40641a135fbcf52a768</li>
480 | <li>User/Repository@SHA: <a class="commit-link" data-hovercard-type="commit" data-hovercard-url="https://github.com/joeyespo/grip/commit/dbcd7a410ee7489acf92f40641a135fbcf52a768/hovercard" href="https://github.com/joeyespo/grip/commit/dbcd7a410ee7489acf92f40641a135fbcf52a768">joeyespo/grip@<tt>dbcd7a4</tt></a></li>
481 | <li>#Num: #135</li>
482 | <li>GH-Num: GH-135</li>
483 | <li>User#Num: joeyespo#135</li>
484 | <li>User/Repository#Num: <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="93030887" data-permission-text="Title is private" data-url="https://github.com/joeyespo/grip/issues/135" data-hovercard-type="pull_request" data-hovercard-url="/joeyespo/grip/pull/135/hovercard" href="https://github.com/joeyespo/grip/pull/135">joeyespo/grip#135</a></li>
485 | </ul>


--------------------------------------------------------------------------------
/tests/output/raw/simple-user-content.html:
--------------------------------------------------------------------------------
1 | <h1>Simple Test ✓</h1>
2 | <p>This is just a simple Unicode Markdown test.</p>


--------------------------------------------------------------------------------
/tests/output/raw/simple-user-context.html:
--------------------------------------------------------------------------------
1 | <h1 dir="auto">Simple Test ✓</h1>
2 | <p dir="auto">This is just a simple Unicode Markdown test.</p>


--------------------------------------------------------------------------------
/tests/output/raw/simple.html:
--------------------------------------------------------------------------------
1 | <h1>
2 | <a id="user-content-simple-test-" class="anchor" href="#simple-test-" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Simple Test ✓</h1>
3 | <p>This is just a simple Unicode Markdown test.</p>
4 | 


--------------------------------------------------------------------------------
/tests/output/raw/zero-user-content.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/tests/output/raw/zero-user-content.html


--------------------------------------------------------------------------------
/tests/output/raw/zero-user-context.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/tests/output/raw/zero-user-context.html


--------------------------------------------------------------------------------
/tests/output/raw/zero.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joeyespo/grip/a3f0ab5c8942ac15cf68f790b80c0a43b3a4c71e/tests/output/raw/zero.html


--------------------------------------------------------------------------------
/tests/output/renderer/gfm-test-user-content.html:
--------------------------------------------------------------------------------
  1 | <h1>GitHub Flavored Markdown Test</h1>
  2 | <p>This Markdown file contains all the features of <strong>GitHub Flavored Markdown</strong> for<br>
  3 | testing a renderer with.</p>
  4 | <p>The features are taken directly from <a href="https://daringfireball.net/projects/markdown/syntax" rel="nofollow">Daring Fireball</a><br>
  5 | and <a href="https://help.github.com/articles/github-flavored-markdown/">GitHub Flavored Markdown</a>,<br>
  6 | followed by a list of one-offs.</p>
  7 | <h2>Inline HTML</h2>
  8 | <table role="table">
  9 |   <tbody><tr>
 10 |     <td>Foo</td>
 11 |   </tr>
 12 |   <tr>
 13 |     <td>
 14 |       Note that Markdown formatting syntax is not processed within block-level HTML tags.
 15 |       E.g., you can’t use Markdown-style *emphasis* inside an HTML block.
 16 |     </td>
 17 |   </tr>
 18 | </tbody></table>
 19 | <p>Span-level HTML tags — e.g. &lt;span&gt;, &lt;cite&gt;, or &lt;del&gt; — can be used anywhere<br>
 20 | in a Markdown paragraph, list item, or header. If you want, you can even use HTML tags instead<br>
 21 | of Markdown formatting; e.g. if you’d prefer to use HTML &lt;a&gt; or &lt;img&gt; tags instead<br>
 22 | of Markdown’s link or image syntax, go right ahead.</p>
 23 | <h4>Testing <span>span</span>, cite, and <del>del</del> tags</h4>
 24 | <p>Testing <span>span</span>, cite, and <del>del</del> tags in a paragraph.</p>
 25 | <p>And each element in its own list item:</p>
 26 | <ul>
 27 | <li><span>This is within a span tag.</span></li>
 28 | <li>This is within a cite tag.</li>
 29 | <li><del>This is within a del tag.</del></li>
 30 | </ul>
 31 | <h2>Automatic escaping for special characters</h2>
 32 | <ul>
 33 | <li>© copyright</li>
 34 | <li>AT&amp;T should render the same as AT&amp;T</li>
 35 | <li>4 &lt; 5 should render the same as 4 &lt; 5</li>
 36 | </ul>
 37 | <p>Note that GitHub Flavored Markdown has URL autolinking, which will <em>not</em><br>
 38 | convert <code>&amp;amp;</code>. So these two should yield different links:</p>
 39 | <ul>
 40 | <li><a href="http://images.google.com/images?num=30&amp;q=larry+bird" rel="nofollow">http://images.google.com/images?num=30&amp;q=larry+bird</a></li>
 41 | <li><a href="http://images.google.com/images?num=30&amp;amp;q=larry+bird" rel="nofollow">http://images.google.com/images?num=30&amp;amp;q=larry+bird</a></li>
 42 | </ul>
 43 | <h2>Paragraphs and line breaks</h2>
 44 | <p>This is a normal paragraph.</p>
 45 | <p>This and the next sentence is separated by a single newline.<br>
 46 | This should be on the same line.</p>
 47 | <p>This and the next sentence is joined by a single <code>&lt;br /&gt;</code>. <br><br>
 48 | This should be on a new line, directly below.</p>
 49 | <p>These two sentences are separated by two <code>&lt;br /&gt;</code> tags. <br><br><br>
 50 | This should be two lines below.</p>
 51 | <p>These two paragraphs are separated by two <code>&lt;br /&gt;</code> tags. <br><br></p>
 52 | <p>This should be three lines below.</p>
 53 | <h2>Headers</h2>
 54 | <p>Here are some headers followed by <a href="https://en.wikipedia.org/wiki/Lorem_ipsum" rel="nofollow">Lorem Ipsum</a>.</p>
 55 | <h1>This is an H1 (Setext-style)</h1>
 56 | <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
 57 | <h2>This is an H2 (Setext-style)</h2>
 58 | <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
 59 | <p>The following are atx-style.</p>
 60 | <h1>This is an H1</h1>
 61 | <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
 62 | <h2>This is an H2</h2>
 63 | <p>Mauris feugiat, augue vitae sollicitudin vulputate, neque arcu dapibus eros, eget semper lorem ex rhoncus nulla.</p>
 64 | <h3>This is an H3</h3>
 65 | <p>Etiam sit amet orci sit amet dui mollis molestie.</p>
 66 | <h4>This is an H4</h4>
 67 | <p>Cras et elit egestas, lacinia est eu, vestibulum enim.</p>
 68 | <h5>This is an H5</h5>
 69 | <p>Phasellus sed suscipit quam.</p>
 70 | <h6>This is an H√36</h6>
 71 | <p>Nam rutrum imperdiet purus, sit amet porttitor augue tempor quis.</p>
 72 | <h2>Blockquotes</h2>
 73 | <p>Email-style blockquotes:</p>
 74 | <blockquote>
 75 | <p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,<br>
 76 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.<br>
 77 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
 78 | <p>Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse<br>
 79 | id sem consectetuer libero luctus adipiscing.</p>
 80 | </blockquote>
 81 | <p>Putting the &gt; before the first line of a hard-wrapped paragraph:</p>
 82 | <blockquote>
 83 | <p>This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,<br>
 84 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.<br>
 85 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.</p>
 86 | </blockquote>
 87 | <blockquote>
 88 | <p>Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse<br>
 89 | id sem consectetuer libero luctus adipiscing.</p>
 90 | </blockquote>
 91 | <p>Nested blockquotes (i.e. a blockquote-in-a-blockquote):</p>
 92 | <blockquote>
 93 | <p>This is the first level of quoting.</p>
 94 | <blockquote>
 95 | <p>This is nested blockquote.</p>
 96 | </blockquote>
 97 | <p>Back to the first level.</p>
 98 | </blockquote>
 99 | <p>Blockquotes containing other Markdown elements:</p>
100 | <blockquote>
101 | <h2>This is a header.</h2>
102 | <ol>
103 | <li>This is the first list item.</li>
104 | <li>This is the second list item.</li>
105 | </ol>
106 | <p>Here's some example code:</p>
107 | <pre><code>return shell_exec("echo $input | $markdown_script");
108 | </code></pre>
109 | </blockquote>
110 | <h2>Lists</h2>
111 | <h5>These three lists should be equivalent</h5>
112 | <p>First:</p>
113 | <ul>
114 | <li>Red</li>
115 | <li>Green</li>
116 | <li>Blue</li>
117 | </ul>
118 | <p>Second:</p>
119 | <ul>
120 | <li>Red</li>
121 | <li>Green</li>
122 | <li>Blue</li>
123 | </ul>
124 | <p>Third:</p>
125 | <ul>
126 | <li>Red</li>
127 | <li>Green</li>
128 | <li>Blue</li>
129 | </ul>
130 | <h4>These three ordered lists should be equivalent</h4>
131 | <p>First:</p>
132 | <ol>
133 | <li>Bird</li>
134 | <li>McHale</li>
135 | <li>Parish</li>
136 | </ol>
137 | <p>Second:</p>
138 | <ol>
139 | <li>Bird</li>
140 | <li>McHale</li>
141 | <li>Parish</li>
142 | </ol>
143 | <p>Third:</p>
144 | <ol start="3">
145 | <li>Bird</li>
146 | <li>McHale</li>
147 | <li>Parish</li>
148 | </ol>
149 | <h4>These two lists should be equivalent</h4>
150 | <p>First:</p>
151 | <ul>
152 | <li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.<br>
153 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,<br>
154 | viverra nec, fringilla in, laoreet vitae, risus.</li>
155 | <li>Donec sit amet nisl. Aliquam semper ipsum sit amet velit.<br>
156 | Suspendisse id sem consectetuer libero luctus adipiscing.</li>
157 | </ul>
158 | <p>Second:</p>
159 | <ul>
160 | <li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.<br>
161 | Aliquam hendrerit mi posuere lectus. Vestibulum enim wisi,<br>
162 | viverra nec, fringilla in, laoreet vitae, risus.</li>
163 | <li>Donec sit amet nisl. Aliquam semper ipsum sit amet velit.<br>
164 | Suspendisse id sem consectetuer libero luctus adipiscing.</li>
165 | </ul>
166 | <h4>Paragraphs and lists</h4>
167 | <ul>
168 | <li>Bird</li>
169 | <li>Magic</li>
170 | </ul>
171 | <p>Blank line separated:</p>
172 | <ul>
173 | <li>
174 | <p>Bird</p>
175 | </li>
176 | <li>
177 | <p>Magic</p>
178 | </li>
179 | </ul>
180 | <h4>Paragraphs within lists</h4>
181 | <p>First:</p>
182 | <ol>
183 | <li>
184 | <p>This is a list item with two paragraphs. Lorem ipsum dolor<br>
185 | sit amet, consectetuer adipiscing elit. Aliquam hendrerit<br>
186 | mi posuere lectus.</p>
187 | <p>Vestibulum enim wisi, viverra nec, fringilla in, laoreet<br>
188 | vitae, risus. Donec sit amet nisl. Aliquam semper ipsum<br>
189 | sit amet velit.</p>
190 | </li>
191 | <li>
192 | <p>Suspendisse id sem consectetuer libero luctus adipiscing.</p>
193 | </li>
194 | </ol>
195 | <p>Second:</p>
196 | <ul>
197 | <li>
198 | <p>This is a list item with two paragraphs.</p>
199 | <p>This is the second paragraph in the list item. You're<br>
200 | only required to indent the first line. Lorem ipsum dolor<br>
201 | sit amet, consectetuer adipiscing elit.</p>
202 | </li>
203 | <li>
204 | <p>Another item in the same list.</p>
205 | </li>
206 | </ul>
207 | <h4>Blockquote within a list</h4>
208 | <ul>
209 | <li>
210 | <p>A list item with a blockquote:</p>
211 | <blockquote>
212 | <p>This is a blockquote<br>
213 | inside a list item.</p>
214 | </blockquote>
215 | </li>
216 | </ul>
217 | <h4>Code within a list</h4>
218 | <ul>
219 | <li>
220 | <p>A list item with a code block:</p>
221 | <pre><code>&lt;code goes here&gt;
222 | </code></pre>
223 | </li>
224 | </ul>
225 | <h4>Accidental lists</h4>
226 | <ol start="1986">
227 | <li>What a great season. (oops, I wanted a year, not a list)</li>
228 | </ol>
229 | <p>1986. What a great season. (<em>whew!</em> there we go)</p>
230 | <h2>Code blocks</h2>
231 | <p>This is a normal paragraph:</p>
232 | <pre><code>This is a code block.
233 | </code></pre>
234 | <p>Here is an example of AppleScript:</p>
235 | <pre><code>tell application "Foo"
236 |     beep
237 | end tell
238 | </code></pre>
239 | <p>Markdown will handle the hassle of encoding the ampersands and angle brackets:</p>
240 | <pre><code>&lt;div class="footer"&gt;
241 |     &amp;copy; 2004 Foo Corporation
242 | &lt;/div&gt;
243 | 
244 | 
245 | def this_is
246 |   puts "some #{4-space-indent} code"
247 | end
248 | </code></pre>
249 | <code>
250 | print('Code block')
251 | </code>
252 | <pre>print('Pre block')
253 | </pre>
254 | <h2>Horizontal rules</h2>
255 | <hr>
256 | <hr>
257 | <hr>
258 | <hr>
259 | <hr>
260 | <h2>Links</h2>
261 | <p>Markdown supports two style of links: inline and reference.</p>
262 | <p>This is <a href="http://joeyespo.com/" title="Title" rel="nofollow">an example</a> inline link.</p>
263 | <p><a href="http://joeyespo.com/" rel="nofollow">This link</a> has no title attribute.</p>
264 | <p>See my <a href="/about/">About</a> page for some awesome people (<em>note: broken link</em>).</p>
265 | <p>This is <a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">an example</a> reference-style link.<br>
266 | This is [an example] <a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">id</a> reference-style link with a space separating the brackets.</p>
267 | <p>These should all be equivalent:</p>
268 | <ul>
269 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 1</a></li>
270 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 2</a></li>
271 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 3</a></li>
272 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 4</a></li>
273 | <li><a href="http://joeyespo.com/" title="Optional Title Here" rel="nofollow">foo 5</a></li>
274 | </ul>
275 | <h2>Emphasis</h2>
276 | <ul>
277 | <li><em>single asterisks</em></li>
278 | <li><em>single underscores</em></li>
279 | <li><strong>double asterisks</strong></li>
280 | <li><strong>double underscores</strong></li>
281 | <li>un<em>frigging</em>believable</li>
282 | <li>*this text is surrounded by literal asterisks*</li>
283 | </ul>
284 | <h2>Code</h2>
285 | <ul>
286 | <li>Use the <code>printf()</code> function.</li>
287 | <li><code>There is a literal backtick (`) here.</code></li>
288 | <li>A single backtick in a code span: <code>`</code></li>
289 | <li>A backtick-delimited string in a code span: <code>`foo`</code></li>
290 | <li>Please don't use any <code>&lt;blink&gt;</code> tags.</li>
291 | <li><code>&amp;#8212;</code> is the decimal-encoded equivalent of <code>&amp;mdash;</code></li>
292 | </ul>
293 | <h2>Images</h2>
294 | <ul>
295 | <li><a target="_blank" rel="noopener noreferrer" href="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico"><img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" alt="Alt text" style="max-width: 100%;"></a></li>
296 | <li><a target="_blank" rel="noopener noreferrer" href="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico"><img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" alt="Alt text" title="Optional title" style="max-width: 100%;"></a></li>
297 | <li><a target="_blank" rel="noopener noreferrer" href="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico"><img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" alt="Alt text" title="Optional title attribute" style="max-width: 100%;"></a></li>
298 | <li><a target="_blank" rel="noopener noreferrer" href="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico"><img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" width="32" height="32" style="max-width: 100%;"></a>   ← bigger &amp; blurrier</li>
299 | </ul>
300 | <h2>Automatic links</h2>
301 | <ul>
302 | <li><a href="http://joeyespo.com/" rel="nofollow">http://joeyespo.com/</a></li>
303 | <li><a href="mailto:joe@joeyespo.com">joe@joeyespo.com</a></li>
304 | </ul>
305 | <h2>Backslash escapes</h2>
306 | <ul>
307 | <li>*literal asterisks*</li>
308 | <li>\   backslash</li>
309 | <li>`   backtick</li>
310 | <li>*   asterisk</li>
311 | <li>_   underscore</li>
312 | <li>{}  curly braces</li>
313 | <li>[]  square brackets</li>
314 | <li>()  parentheses</li>
315 | <li>#   hash mark</li>
316 | <li>+   plus sign</li>
317 | <li>-   minus sign (hyphen)</li>
318 | <li>.   dot</li>
319 | <li>!   exclamation mark</li>
320 | </ul>
321 | <h2>GitHub Flavored Markdown</h2>
322 | <p>See <a href="https://help.github.com/articles/github-flavored-markdown/">GitHub Flavored Markdown</a> for details.</p>
323 | <h4>Multiple underscores in words</h4>
324 | <ul>
325 | <li>wow_great_stuff</li>
326 | </ul>
327 | <h4>URL autolinking</h4>
328 | <p><a href="http://joeyespo.com" rel="nofollow">http://joeyespo.com</a></p>
329 | <h4>Strikethrough</h4>
330 | <p><del>Mistaken text.</del></p>
331 | <h4>Fenced code blocks</h4>
332 | <pre><code>function test() {
333 |   console.log("notice the blank line before this function?");
334 | }
335 | </code></pre>
336 | <h4>Syntax highlighting</h4>
337 | <div class="highlight highlight-source-python"><pre><span class="pl-en">print</span>(<span class="pl-s">'Hello!'</span>)</pre></div>
338 | <div class="highlight highlight-source-js"><pre><span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'JavaScript!'</span><span class="pl-kos">)</span><span class="pl-kos">;</span></pre></div>
339 | <div class="highlight highlight-source-js"><pre><span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'JavaScript (with js)!'</span><span class="pl-kos">)</span><span class="pl-kos">;</span></pre></div>
340 | <pre lang="unmatched_language"><code>console.log('No matching language, but looks like JavaScript.');
341 | </code></pre>
342 | <h4>Tables</h4>
343 | <p>Simple:</p>
344 | <table role="table">
345 | <thead>
346 | <tr>
347 | <th>First Header</th>
348 | <th>Second Header</th>
349 | </tr>
350 | </thead>
351 | <tbody>
352 | <tr>
353 | <td>Content Cell</td>
354 | <td>Content Cell</td>
355 | </tr>
356 | <tr>
357 | <td>Content Cell</td>
358 | <td>Content Cell</td>
359 | </tr>
360 | </tbody>
361 | </table>
362 | <p>Pipes:</p>
363 | <table role="table">
364 | <thead>
365 | <tr>
366 | <th>First Header</th>
367 | <th>Second Header</th>
368 | </tr>
369 | </thead>
370 | <tbody>
371 | <tr>
372 | <td>Content Cell</td>
373 | <td>Content Cell</td>
374 | </tr>
375 | <tr>
376 | <td>Content Cell</td>
377 | <td>Content Cell</td>
378 | </tr>
379 | </tbody>
380 | </table>
381 | <p>Unmatched:</p>
382 | <table role="table">
383 | <thead>
384 | <tr>
385 | <th>Name</th>
386 | <th>Description</th>
387 | </tr>
388 | </thead>
389 | <tbody>
390 | <tr>
391 | <td>Help</td>
392 | <td>Display the help window.</td>
393 | </tr>
394 | <tr>
395 | <td>Close</td>
396 | <td>Closes a window</td>
397 | </tr>
398 | </tbody>
399 | </table>
400 | <p>Inner Markdown:</p>
401 | <table role="table">
402 | <thead>
403 | <tr>
404 | <th>Name</th>
405 | <th>Description</th>
406 | </tr>
407 | </thead>
408 | <tbody>
409 | <tr>
410 | <td>Help</td>
411 | <td><del>Display the</del> help window.</td>
412 | </tr>
413 | <tr>
414 | <td>Close</td>
415 | <td><em>Closes</em> a window</td>
416 | </tr>
417 | </tbody>
418 | </table>
419 | <p>Alignment:</p>
420 | <table role="table">
421 | <thead>
422 | <tr>
423 | <th align="left">Left-Aligned</th>
424 | <th align="center">Center Aligned</th>
425 | <th align="right">Right Aligned</th>
426 | </tr>
427 | </thead>
428 | <tbody>
429 | <tr>
430 | <td align="left">col 3 is</td>
431 | <td align="center">some wordy text</td>
432 | <td align="right">$1600</td>
433 | </tr>
434 | <tr>
435 | <td align="left">col 2 is</td>
436 | <td align="center">centered</td>
437 | <td align="right">$12</td>
438 | </tr>
439 | <tr>
440 | <td align="left">zebra stripes</td>
441 | <td align="center">are neat</td>
442 | <td align="right">$1</td>
443 | </tr>
444 | <tr>
445 | <td align="left">Text right below a table.</td>
446 | <td align="center"></td>
447 | <td align="right"></td>
448 | </tr>
449 | </tbody>
450 | </table>
451 | <h4>HTML</h4>
452 | <p><em>TODO: Test all allowed HTML tags.</em></p>
453 | <h2>Writing on GitHub</h2>
454 | <p>See <a href="https://help.github.com/articles/writing-on-github/">this article</a> for details.</p>
455 | <h4>Newlines</h4>
456 | <p>Roses are red<br>
457 | Violets are Blue</p>
458 | <h4>Task lists</h4>
459 | <ul class="contains-task-list">
460 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> @mentions, #refs, <a href="">links</a>, <strong>formatting</strong>, and <del>tags</del> are supported</li>
461 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> list syntax is required (any unordered or ordered list supported)</li>
462 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> this is a complete item</li>
463 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> this is an incomplete item</li>
464 | </ul>
465 | <p>Task lists can be nested to better structure your tasks:</p>
466 | <ul class="contains-task-list">
467 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> a bigger project
468 | <ul class="contains-task-list">
469 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> first subtask #1234</li>
470 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> follow up subtask #4321</li>
471 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> final subtask cc @mention</li>
472 | </ul>
473 | </li>
474 | <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> a separate task</li>
475 | </ul>
476 | <h4>References</h4>
477 | <ul>
478 | <li>SHA: dbcd7a410ee7489acf92f40641a135fbcf52a768</li>
479 | <li>User@SHA: joeyespo@dbcd7a410ee7489acf92f40641a135fbcf52a768</li>
480 | <li>User/Repository@SHA: <a class="commit-link" data-hovercard-type="commit" data-hovercard-url="https://github.com/joeyespo/grip/commit/dbcd7a410ee7489acf92f40641a135fbcf52a768/hovercard" href="https://github.com/joeyespo/grip/commit/dbcd7a410ee7489acf92f40641a135fbcf52a768">joeyespo/grip@<tt>dbcd7a4</tt></a></li>
481 | <li>#Num: #135</li>
482 | <li>GH-Num: GH-135</li>
483 | <li>User#Num: joeyespo#135</li>
484 | <li>User/Repository#Num: <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="93030887" data-permission-text="Title is private" data-url="https://github.com/joeyespo/grip/issues/135" data-hovercard-type="pull_request" data-hovercard-url="/joeyespo/grip/pull/135/hovercard" href="https://github.com/joeyespo/grip/pull/135">joeyespo/grip#135</a></li>
485 | </ul>


--------------------------------------------------------------------------------
/tests/output/renderer/simple-user-content.html:
--------------------------------------------------------------------------------
1 | <h1>Simple Test ✓</h1>
2 | <p>This is just a simple Unicode Markdown test.</p>


--------------------------------------------------------------------------------
/tests/output/renderer/simple-user-context.html:
--------------------------------------------------------------------------------
1 | <h1 dir="auto">Simple Test ✓</h1>
2 | <p dir="auto">This is just a simple Unicode Markdown test.</p>


--------------------------------------------------------------------------------
/tests/output/renderer/simple.html:
--------------------------------------------------------------------------------
1 | <h1>
2 | <a id="user-content-simple-test-" class="anchor" href="#simple-test-" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Simple Test ✓</h1>
3 | <p>This is just a simple Unicode Markdown test.</p>
4 | 


--------------------------------------------------------------------------------
/tests/regenerate.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Regenerates all the rendered Markdown files in the output/ directory.
  3 | """
  4 | 
  5 | from __future__ import print_function, unicode_literals
  6 | 
  7 | import sys
  8 | import io
  9 | import os
 10 | 
 11 | from grip import Grip, GitHubRenderer, TextReader
 12 | from helpers import USER_CONTEXT, input_file, input_filename
 13 | from mocks import GripMock
 14 | 
 15 | DIRNAME = os.path.dirname(os.path.abspath(__file__))
 16 | sys.path.insert(1, os.path.dirname(DIRNAME))
 17 | 
 18 | 
 19 | # Use auth from user settings
 20 | AUTH = Grip(TextReader('')).auth
 21 | 
 22 | 
 23 | def write(text, *parts):
 24 |     filename = os.path.join(DIRNAME, 'output', *parts)
 25 |     return io.open(filename, 'wt', encoding='utf-8').write(text)
 26 | 
 27 | 
 28 | def regenerate_app():
 29 |     zero = input_filename('zero.md')
 30 |     simple = input_filename('simple.md')
 31 |     gfm_test = input_filename('gfm-test.md')
 32 | 
 33 |     write(GripMock(zero, AUTH).render(), 'app', 'zero.html')
 34 |     write(GripMock(zero, AUTH, GitHubRenderer(True)).render(),
 35 |           'app', 'zero-user-context.html')
 36 |     write(GripMock(zero, AUTH, GitHubRenderer(True, USER_CONTEXT)).render(),
 37 |           'app', 'zero-user-context.html')
 38 | 
 39 |     write(GripMock(simple, AUTH).render(), 'app', 'simple.html')
 40 |     write(GripMock(simple, AUTH, GitHubRenderer(True)).render(),
 41 |           'app', 'simple-user-context.html')
 42 |     write(GripMock(simple, AUTH, GitHubRenderer(True, USER_CONTEXT)).render(),
 43 |           'app', 'simple-user-context.html')
 44 | 
 45 |     write(GripMock(gfm_test, AUTH).render(), 'app', 'gfm-test.html')
 46 |     write(GripMock(gfm_test, AUTH, GitHubRenderer(True)).render(),
 47 |           'app', 'gfm-test-user-context.html')
 48 |     write(GripMock(gfm_test, AUTH, GitHubRenderer(True, USER_CONTEXT))
 49 |           .render(), 'app', 'gfm-test-user-context.html')
 50 | 
 51 | 
 52 | def regenerate_exporter():
 53 |     # TODO: Implement
 54 |     # TODO: Strip out inlined CSS specifics?
 55 |     pass
 56 | 
 57 | 
 58 | def regenerate_renderer():
 59 |     simple = input_file('simple.md')
 60 |     gfm_test = input_file('gfm-test.md')
 61 | 
 62 |     write(GitHubRenderer().render(simple, AUTH),
 63 |           'renderer', 'simple.html')
 64 |     write(GitHubRenderer(True).render(simple, AUTH),
 65 |           'renderer', 'simple-user-content.html')
 66 |     write(GitHubRenderer(True, USER_CONTEXT).render(simple, AUTH),
 67 |           'renderer', 'simple-user-context.html')
 68 | 
 69 |     write(GitHubRenderer().render(gfm_test, AUTH),
 70 |           'renderer', 'gfm-test.html')
 71 |     write(GitHubRenderer(True).render(gfm_test, AUTH),
 72 |           'renderer', 'gfm-test-user-content.html')
 73 |     write(GitHubRenderer(True, USER_CONTEXT).render(gfm_test, AUTH),
 74 |           'renderer', 'gfm-test-user-context.html')
 75 | 
 76 | 
 77 | def regenerate_raw():
 78 |     zero = input_file('zero.md')
 79 |     simple = input_file('simple.md')
 80 |     gfm_test = input_file('gfm-test.md')
 81 | 
 82 |     write(GitHubRenderer(raw=True).render(zero, AUTH),
 83 |           'raw', 'zero.html')
 84 |     write(GitHubRenderer(True, raw=True).render(zero, AUTH),
 85 |           'raw', 'zero-user-content.html')
 86 |     write(GitHubRenderer(True, USER_CONTEXT, raw=True).render(zero, AUTH),
 87 |           'raw', 'zero-user-context.html')
 88 | 
 89 |     write(GitHubRenderer(raw=True).render(simple, AUTH),
 90 |           'raw', 'simple.html')
 91 |     write(GitHubRenderer(True, raw=True).render(simple, AUTH),
 92 |           'raw', 'simple-user-content.html')
 93 |     write(GitHubRenderer(True, USER_CONTEXT, raw=True).render(simple, AUTH),
 94 |           'raw', 'simple-user-context.html')
 95 | 
 96 |     write(GitHubRenderer(raw=True).render(gfm_test, AUTH),
 97 |           'raw', 'gfm-test.html')
 98 |     write(GitHubRenderer(True, raw=True).render(gfm_test, AUTH),
 99 |           'raw', 'gfm-test-user-content.html')
100 |     write(GitHubRenderer(True, USER_CONTEXT, raw=True)
101 |           .render(gfm_test, AUTH), 'raw', 'gfm-test-user-context.html')
102 | 
103 | 
104 | def regenerate():
105 |     print('Regenerating output files...')
106 |     regenerate_app()
107 |     regenerate_exporter()
108 |     regenerate_renderer()
109 |     regenerate_raw()
110 | 
111 | 
112 | if __name__ == '__main__':
113 |     regenerate()
114 | 


--------------------------------------------------------------------------------
/tests/test_api.py:
--------------------------------------------------------------------------------
  1 | """
  2 | Tests Grip's public API, i.e. everything you can access from `import grip`.
  3 | 
  4 | This doesn't send any requests to GitHub (see test_github.py for that), and
  5 | this doesn't run a server (see test_cli.py for that). Instead, this creates
  6 | fake objects with subclasses and tests the basic expected behavior of Grip.
  7 | """
  8 | 
  9 | from __future__ import print_function, unicode_literals
 10 | 
 11 | import os
 12 | import posixpath
 13 | 
 14 | import pytest
 15 | from requests.exceptions import HTTPError
 16 | from werkzeug.exceptions import NotFound
 17 | 
 18 | from helpers import USER_CONTEXT, input_file, input_filename, output_file
 19 | from mocks import (
 20 |     GitHubAssetManagerMock, GripMock, GitHubRequestsMock, StdinReaderMock)
 21 | 
 22 | from grip import (
 23 |     DEFAULT_FILENAME, DirectoryReader, GitHubAssetManager, GitHubRenderer,
 24 |     Grip, ReadmeNotFoundError, ReadmeReader, ReadmeRenderer, TextReader,
 25 |     create_app)
 26 | 
 27 | 
 28 | # TODO: Test DEFAULT_API_URL, DEFAULT_FILENAMES, DEFAULT_GRIPHOME,
 29 | #       DEFAULT_GRIPURL, STYLE_ASSET_URLS_INLINE_FORMAT, STYLE_ASSET_URLS_RE,
 30 | #       STYLE_ASSET_URLS_SUB_FORMAT, STYLE_URLS_RES, STYLE_URLS_SOURCE,
 31 | #       SUPPORTED_EXTENSIONS, SUPPORTED_TITLES, ReadmeAssetManager,
 32 | #       OfflineRenderer, clear_cache, create_app, export, main, render_content,
 33 | #       render_page, serve
 34 | 
 35 | 
 36 | DIRNAME = os.path.dirname(os.path.abspath(__file__))
 37 | 
 38 | 
 39 | def test_exceptions():
 40 |     """
 41 |     Test that ReadmeNotFoundError behaves like FileNotFoundError on
 42 |     Python 3 and IOError on Python 2.
 43 |     """
 44 |     assert str(ReadmeNotFoundError()) == 'README not found'
 45 |     assert (str(ReadmeNotFoundError('.')) == 'No README found at .')
 46 |     assert str(ReadmeNotFoundError('some/path', 'Overridden')) == 'Overridden'
 47 |     assert ReadmeNotFoundError().filename is None
 48 |     assert ReadmeNotFoundError(DEFAULT_FILENAME).filename == DEFAULT_FILENAME
 49 | 
 50 | 
 51 | def test_readme_reader():
 52 |     with pytest.raises(TypeError):
 53 |         ReadmeReader()
 54 | 
 55 | 
 56 | def test_directory_reader():
 57 |     input_path = 'input'
 58 |     markdown_path = posixpath.join(input_path, 'gfm-test.md')
 59 |     default_path = posixpath.join(input_path, 'default')
 60 |     input_img_path = posixpath.join(input_path, 'img.png')
 61 | 
 62 |     input_dir = os.path.join(DIRNAME, 'input')
 63 |     markdown_file = os.path.join(input_dir, 'gfm-test.md')
 64 |     default_dir = os.path.join(input_dir, 'default')
 65 |     default_file = os.path.join(default_dir, DEFAULT_FILENAME)
 66 | 
 67 |     DirectoryReader(input_filename('default'))
 68 |     DirectoryReader(input_filename(default_file))
 69 |     DirectoryReader(input_filename(default_file), silent=True)
 70 |     DirectoryReader(input_filename('empty'), silent=True)
 71 |     with pytest.raises(ReadmeNotFoundError):
 72 |         DirectoryReader(input_filename('empty'))
 73 |     with pytest.raises(ReadmeNotFoundError):
 74 |         DirectoryReader(input_filename('empty', DEFAULT_FILENAME))
 75 | 
 76 |     reader = DirectoryReader(DIRNAME, silent=True)
 77 |     assert reader.root_filename == os.path.join(DIRNAME, DEFAULT_FILENAME)
 78 |     assert reader.root_directory == DIRNAME
 79 | 
 80 |     assert reader.normalize_subpath(None) is None
 81 |     assert reader.normalize_subpath('.') == './'
 82 |     assert reader.normalize_subpath('./././') == './'
 83 |     assert reader.normalize_subpath('non-existent/.././') == './'
 84 |     assert reader.normalize_subpath('non-existent/') == 'non-existent'
 85 |     assert reader.normalize_subpath('non-existent') == 'non-existent'
 86 |     with pytest.raises(NotFound):
 87 |         reader.normalize_subpath('../unsafe')
 88 |     with pytest.raises(NotFound):
 89 |         reader.normalize_subpath('/unsafe')
 90 |     assert reader.normalize_subpath(input_path) == input_path + '/'
 91 |     assert reader.normalize_subpath(markdown_path) == markdown_path
 92 |     assert reader.normalize_subpath(markdown_path + '/') == markdown_path
 93 | 
 94 |     assert reader.readme_for(None) == os.path.join(DIRNAME, DEFAULT_FILENAME)
 95 |     with pytest.raises(ReadmeNotFoundError):
 96 |         reader.readme_for('non-existent')
 97 |     with pytest.raises(ReadmeNotFoundError):
 98 |         reader.readme_for(input_path)
 99 |     assert reader.readme_for(markdown_path) == os.path.abspath(markdown_file)
100 |     assert reader.readme_for(default_path) == os.path.abspath(default_file)
101 | 
102 |     # TODO: 'README.md' vs 'readme.md'
103 | 
104 |     assert reader.filename_for(None) == DEFAULT_FILENAME
105 |     assert reader.filename_for(input_path) is None
106 |     assert reader.filename_for(default_path) == os.path.relpath(
107 |         default_file, reader.root_directory)
108 | 
109 |     assert not reader.is_binary()
110 |     assert not reader.is_binary(input_path)
111 |     assert not reader.is_binary(markdown_path)
112 |     assert reader.is_binary(input_img_path)
113 | 
114 |     assert reader.last_updated() is None
115 |     assert reader.last_updated(input_path) is None
116 |     assert reader.last_updated(markdown_path) is not None
117 |     assert reader.last_updated(default_path) is not None
118 |     assert DirectoryReader(default_dir).last_updated is not None
119 | 
120 |     with pytest.raises(ReadmeNotFoundError):
121 |         assert reader.read(input_path) is not None
122 |     assert reader.read(markdown_path)
123 |     assert reader.read(default_path)
124 |     with pytest.raises(ReadmeNotFoundError):
125 |         assert reader.read()
126 |     assert DirectoryReader(default_dir).read() is not None
127 | 
128 | 
129 | def test_text_reader():
130 |     text = 'Test *Text*'
131 |     filename = DEFAULT_FILENAME
132 | 
133 |     assert TextReader(text).normalize_subpath(None) is None
134 |     assert TextReader(text).normalize_subpath('././.') == '.'
135 |     assert TextReader(text).normalize_subpath(filename) == filename
136 | 
137 |     assert TextReader(text).filename_for(None) is None
138 |     assert TextReader(text, filename).filename_for(None) == filename
139 |     assert TextReader(text, filename).filename_for('.') is None
140 | 
141 |     assert TextReader(text).last_updated() is None
142 |     assert TextReader(text, filename).last_updated() is None
143 |     assert TextReader(text, filename).last_updated('.') is None
144 |     assert TextReader(text, filename).last_updated(filename) is None
145 | 
146 |     assert TextReader(text).read() == text
147 |     assert TextReader(text, filename).read() == text
148 |     with pytest.raises(ReadmeNotFoundError):
149 |         TextReader(text).read('.')
150 |     with pytest.raises(ReadmeNotFoundError):
151 |         TextReader(text, filename).read('.')
152 |     with pytest.raises(ReadmeNotFoundError):
153 |         TextReader(text, filename).read(filename)
154 | 
155 | 
156 | def test_stdin_reader():
157 |     text = 'Test *STDIN*'
158 |     filename = DEFAULT_FILENAME
159 | 
160 |     assert StdinReaderMock(text).normalize_subpath(None) is None
161 |     assert StdinReaderMock(text).normalize_subpath('././.') == '.'
162 |     assert StdinReaderMock(text).normalize_subpath(filename) == filename
163 | 
164 |     assert StdinReaderMock(text).filename_for(None) is None
165 |     assert StdinReaderMock(text, filename).filename_for(None) == filename
166 |     assert StdinReaderMock(text, filename).filename_for('.') is None
167 | 
168 |     assert StdinReaderMock(text).last_updated() is None
169 |     assert StdinReaderMock(text, filename).last_updated() is None
170 |     assert StdinReaderMock(text, filename).last_updated('.') is None
171 |     assert StdinReaderMock(text, filename).last_updated(filename) is None
172 | 
173 |     assert StdinReaderMock(text).read() == text
174 |     assert StdinReaderMock(text, filename).read() == text
175 |     with pytest.raises(ReadmeNotFoundError):
176 |         StdinReaderMock(text).read('.')
177 |     with pytest.raises(ReadmeNotFoundError):
178 |         StdinReaderMock(text, filename).read('.')
179 |     with pytest.raises(ReadmeNotFoundError):
180 |         StdinReaderMock(text, filename).read(filename)
181 | 
182 | 
183 | def test_readme_renderer():
184 |     with pytest.raises(TypeError):
185 |         ReadmeRenderer()
186 | 
187 | 
188 | def test_github_renderer():
189 |     simple_input = input_file('simple.md')
190 |     gfm_test_input = input_file('gfm-test.md')
191 | 
192 |     with GitHubRequestsMock() as responses:
193 |         assert (GitHubRenderer().render(simple_input) ==
194 |                 output_file('renderer', 'simple.html'))
195 |         assert (GitHubRenderer(True).render(simple_input) ==
196 |                 output_file('renderer', 'simple-user-content.html'))
197 |         assert (GitHubRenderer(True, USER_CONTEXT).render(simple_input) ==
198 |                 output_file('renderer', 'simple-user-context.html'))
199 |         assert len(responses.calls) == 3
200 | 
201 |     assert (output_file('renderer', 'gfm-test-user-content.html') !=
202 |             output_file('renderer', 'gfm-test-user-context.html'))
203 | 
204 |     with GitHubRequestsMock() as responses:
205 |         assert (GitHubRenderer().render(gfm_test_input) ==
206 |                 output_file('renderer', 'gfm-test.html'))
207 |         assert (GitHubRenderer(True).render(gfm_test_input) ==
208 |                 output_file('renderer', 'gfm-test-user-content.html'))
209 |         assert (GitHubRenderer(True, USER_CONTEXT).render(gfm_test_input) ==
210 |                 output_file('renderer', 'gfm-test-user-context.html'))
211 |         assert len(responses.calls) == 3
212 | 
213 |     with GitHubRequestsMock() as responses:
214 |         assert (
215 |             GitHubRenderer().render(simple_input, GitHubRequestsMock.auth) ==
216 |             output_file('renderer', 'simple.html'))
217 |         with pytest.raises(HTTPError):
218 |             GitHubRenderer().render(simple_input, GitHubRequestsMock.bad_auth)
219 |         assert len(responses.calls) == 2
220 | 
221 | 
222 | def test_offline_renderer():
223 |     # TODO: Test all GitHub rendering features and get the renderer to pass
224 |     # FUTURE: Expose OfflineRenderer once all Markdown features are tested
225 |     pass
226 | 
227 | 
228 | def test_readme_asset_manager():
229 |     with pytest.raises(TypeError):
230 |         ReadmeRenderer()
231 | 
232 | 
233 | def test_github_asset_manager(tmpdir):
234 |     cache_dir = tmpdir.mkdir('cache-dummy')
235 |     assets = GitHubAssetManager(str(cache_dir))
236 | 
237 |     cache_dir.join('dummy1.css').write_text('', 'utf-8')
238 |     cache_dir.join('dummy2.css').write_text('', 'utf-8')
239 |     assert len(cache_dir.listdir()) == 2
240 |     assets.clear()
241 |     assert not cache_dir.check()
242 | 
243 |     # TODO: Test style retrieval on a fresh cache
244 |     # TODO: Test that an existing cache is used when styles are requested
245 |     # TODO: Test the upgrade case (cache-x.y.z should be fresh)
246 | 
247 | 
248 | # TODO: test_browser?
249 | 
250 | 
251 | def test_app(monkeypatch, tmpdir):
252 |     monkeypatch.setenv('GRIPHOME', str(tmpdir))
253 |     zero_path = input_filename('zero.md')
254 |     zero_output = output_file('app', 'zero.html')
255 |     gfm_test_path = input_filename('gfm-test.md')
256 |     gfm_test_output = output_file('app', 'gfm-test.html')
257 |     assets = GitHubAssetManagerMock()
258 | 
259 |     with GitHubRequestsMock() as responses:
260 |         assert Grip(zero_path, assets=assets).render() == zero_output
261 |         assert Grip(zero_path, assets=assets).render('/') == zero_output
262 |         assert Grip(zero_path, assets=assets).render('/x/../') == zero_output
263 |         with Grip(zero_path, assets=assets).test_client() as client:
264 |             assert client.get('/').data.decode('utf-8') == zero_output
265 |         assert len(responses.calls) == 4
266 | 
267 |     with GitHubRequestsMock() as responses:
268 |         app = Grip(gfm_test_path, assets=assets)
269 |         assert app.render() == gfm_test_output
270 |         assert app.render('/') == gfm_test_output
271 |         assert len(responses.calls) == 2
272 | 
273 |     # TODO: Test all constructor parameters
274 |     # TODO: Test other methods
275 |     # TODO: cd('input', 'default') and run on cwd
276 |     # TODO: Test 403 responses
277 |     # TODO: Test behaviors? -> anchor tags, autorefresh
278 | 
279 | 
280 | def test_api():
281 |     assert isinstance(create_app(grip_class=GripMock), GripMock)
282 | 
283 |     # TODO: Test all API functions and argument combinations
284 | 
285 | 
286 | def test_command():
287 |     # TODO: Test main(argv) with all command and argument combinations
288 |     # TODO: Test autorefresh by mimicking the browser with a manually GET
289 |     # TODO: Test browser opening using monkey patching?
290 |     pass
291 | 


--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
 1 | """
 2 | Tests the Grip command-line interface.
 3 | """
 4 | 
 5 | from __future__ import print_function, unicode_literals
 6 | 
 7 | import sys
 8 | from subprocess import PIPE, STDOUT, CalledProcessError, Popen
 9 | 
10 | import pytest
11 | 
12 | from grip.command import usage, version
13 | 
14 | 
15 | if sys.version_info[0] == 2 and sys.version_info[1] < 7:
16 |     class CalledProcessError(CalledProcessError):
17 |         def __init__(self, returncode, cmd, output):
18 |             super(CalledProcessError, self).__init__(returncode, cmd)
19 |             self.output = output
20 | 
21 | 
22 | def run(*args, **kwargs):
23 |     command = kwargs.pop('command', 'grip')
24 |     stdin = kwargs.pop('stdin', None)
25 | 
26 |     cmd = [command] + list(args)
27 |     p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT,
28 |               universal_newlines=True)
29 |     # Sent input as STDIN then close it
30 |     output, _ = p.communicate(input=stdin)
31 |     p.stdin.close()
32 |     # Wait for process to terminate
33 |     returncode = p.wait()
34 |     # Raise exception on failed process calls
35 |     if returncode != 0:
36 |         raise CalledProcessError(returncode, cmd, output=output)
37 |     return output
38 | 
39 | 
40 | def test_help():
41 |     assert run('-h') == usage
42 |     assert run('--help') == usage
43 | 
44 | 
45 | def test_version():
46 |     assert run('-V') == version + '\n'
47 |     assert run('--version') == version + '\n'
48 | 
49 | 
50 | def test_bad_command():
51 |     simple_usage = '\n\n'.join(usage.split('\n\n')[:1])
52 |     with pytest.raises(CalledProcessError) as excinfo:
53 |         run('--does-not-exist')
54 |     assert excinfo.value.output == simple_usage + '\n'
55 | 
56 | 
57 | # TODO: Figure out how to run the CLI and still capture requests
58 | # TODO: Test all Grip CLI commands and arguments
59 | # TODO: Test settings wire-up (settings.py, settings_local.py, ~/.grip)
60 | 
61 | # TODO: Test `cat README.md | ~/.local/bin/grip - --export -` (#152)
62 | 


--------------------------------------------------------------------------------
/tests/test_github.py:
--------------------------------------------------------------------------------
 1 | """
 2 | Tests GitHub's public API to verify that all assumptions made by Grip
 3 | are still in place.
 4 | 
 5 | Note that these tests assume a reliable internet connection, so they
 6 | may fail despite being correct. To run the tests without making any
 7 | external calls, use `py.test -m "not assumption"`
 8 | """
 9 | 
10 | from __future__ import print_function, unicode_literals
11 | 
12 | import os
13 | 
14 | import pytest
15 | import requests
16 | from grip import DEFAULT_API_URL, GitHubAssetManager, GitHubRenderer
17 | 
18 | from helpers import USER_CONTEXT, input_file, output_file
19 | 
20 | 
21 | @pytest.fixture
22 | def input_markdown():
23 |     return input_file('gfm-test.md')
24 | 
25 | 
26 | @pytest.fixture
27 | def output_readme():
28 |     return output_file('raw', 'gfm-test.html')
29 | 
30 | 
31 | @pytest.fixture
32 | def output_user_content():
33 |     return output_file('raw', 'gfm-test-user-content.html')
34 | 
35 | 
36 | @pytest.fixture
37 | def output_user_context():
38 |     return output_file('raw', 'gfm-test-user-context.html')
39 | 
40 | 
41 | @pytest.mark.assumption
42 | def test_github():
43 |     requests.get(DEFAULT_API_URL).raise_for_status()
44 | 
45 | 
46 | @pytest.mark.assumption
47 | def test_github_api():
48 |     assert GitHubRenderer(raw=True).render('') == ''
49 |     assert GitHubRenderer(user_content=True, raw=True).render('') == ''
50 | 
51 | 
52 | @pytest.mark.assumption
53 | def test_github_readme(input_markdown, output_readme):
54 |     assert GitHubRenderer(raw=True).render(input_markdown) == output_readme
55 | 
56 | 
57 | @pytest.mark.assumption
58 | def test_github_user_content(input_markdown, output_user_content):
59 |     renderer = GitHubRenderer(True, raw=True)
60 |     assert renderer.render(input_markdown) == output_user_content
61 | 
62 | 
63 | @pytest.mark.assumption
64 | def test_github_user_context(input_markdown, output_user_context):
65 |     renderer = GitHubRenderer(True, USER_CONTEXT, raw=True)
66 |     assert renderer.render(input_markdown) == output_user_context
67 | 
68 | 
69 | @pytest.mark.assumption
70 | def test_styles_exist(tmpdir):
71 |     GitHubAssetManager(str(tmpdir)).retrieve_styles('http://dummy/')
72 |     assert len(tmpdir.listdir()) > 2
73 | 
74 |     files = list(map(lambda f: os.path.basename(str(f)), tmpdir.listdir()))
75 |     assert any(f.startswith('github-') and f.endswith('.css') for f in files)
76 |     assert any(
77 |         f.startswith('frameworks-') and f.endswith('.css') for f in files)
78 | 
79 |     # TODO: Test that style retrieval actually parsed CSS with regex
80 | 
81 | 
82 | # TODO: Test that local images show up in the browser
83 | # TODO: Test that web images show up in the browser
84 | # TODO: Test that octicons show up in the browser
85 | # TODO: Test that anchor tags still work
86 | 


--------------------------------------------------------------------------------