├── .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"=>"true", :class=>' 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. <span>, <cite>, or <del> — 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 <a> or <img> 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 | - © copyright 46 | - AT&T should render the same as AT&T 47 | - 4 < 5 should render the same as 4 < 5 48 | 49 | Note that GitHub Flavored Markdown has URL autolinking, which will *not* 50 | convert `&`. 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&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 | © 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 | - `—` is the decimal-encoded equivalent of `—` 389 | 390 | 391 | Images 392 | ------ 393 | 394 | -  395 | -  396 | - ![Alt text][img] 397 | - <img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" width="32" height="32" /> ← bigger & 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. <span>, <cite>, or <del> — 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 <a> or <img> 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 | - © copyright 46 | - AT&T should render the same as AT&T 47 | - 4 < 5 should render the same as 4 < 5 48 | 49 | Note that GitHub Flavored Markdown has URL autolinking, which will *not* 50 | convert `&`. 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&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 | © 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 | - `—` is the decimal-encoded equivalent of `—` 389 | 390 | 391 | Images 392 | ------ 393 | 394 | -  395 | -  396 | - ![Alt text][img] 397 | - <img src="https://raw.githubusercontent.com/joeyespo/grip/master/artwork/favicon.ico" width="32" height="32" /> ← bigger & 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> </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> </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. <span>, <cite>, or <del> — 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 <a> or <img> 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&T should render the same as AT&T</li> 35 | <li>4 < 5 should render the same as 4 < 5</li> 36 | </ul> 37 | <p>Note that GitHub Flavored Markdown has URL autolinking, which will <em>not</em><br> 38 | convert <code>&amp;</code>. So these two should yield different links:</p> 39 | <ul> 40 | <li><a href="http://images.google.com/images?num=30&q=larry+bird" rel="nofollow">http://images.google.com/images?num=30&q=larry+bird</a></li> 41 | <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> 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><br /></code>. <br><br> 48 | This should be on a new line, directly below.</p> 49 | <p>These two sentences are separated by two <code><br /></code> tags. <br><br><br> 50 | This should be two lines below.</p> 51 | <p>These two paragraphs are separated by two <code><br /></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 > 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><code goes here> 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><div class="footer"> 241 | &copy; 2004 Foo Corporation 242 | </div> 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><blink></code> tags.</li> 291 | <li><code>&#8212;</code> is the decimal-encoded equivalent of <code>&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 & 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. <span>, <cite>, or <del> — 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 <a> or <img> 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&T should render the same as AT&T</li> 35 | <li>4 < 5 should render the same as 4 < 5</li> 36 | </ul> 37 | <p>Note that GitHub Flavored Markdown has URL autolinking, which will <em>not</em><br> 38 | convert <code>&amp;</code>. So these two should yield different links:</p> 39 | <ul> 40 | <li><a href="http://images.google.com/images?num=30&q=larry+bird" rel="nofollow">http://images.google.com/images?num=30&q=larry+bird</a></li> 41 | <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> 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><br /></code>. <br><br> 48 | This should be on a new line, directly below.</p> 49 | <p>These two sentences are separated by two <code><br /></code> tags. <br><br><br> 50 | This should be two lines below.</p> 51 | <p>These two paragraphs are separated by two <code><br /></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 > 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><code goes here> 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><div class="footer"> 241 | &copy; 2004 Foo Corporation 242 | </div> 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><blink></code> tags.</li> 291 | <li><code>&#8212;</code> is the decimal-encoded equivalent of <code>&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 & 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 | --------------------------------------------------------------------------------