├── .gitignore ├── requirements.in ├── pygmentizr ├── static │ ├── favicon.ico │ └── pygmentizr.css ├── __init__.py ├── forms.py ├── views.py ├── renderer.py └── templates │ └── index.html ├── uwsgi.ini ├── nginx.conf ├── supervisord.conf ├── requirements.txt ├── Dockerfile ├── config.py ├── LICENSE ├── manage.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | docopt 2 | Flask 3 | Flask-WTF 4 | Pygments -------------------------------------------------------------------------------- /pygmentizr/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/pygmentizr/master/pygmentizr/static/favicon.ico -------------------------------------------------------------------------------- /pygmentizr/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from flask import Flask 4 | 5 | __version__ = '0.1' 6 | 7 | app = Flask(__name__) 8 | app.config.from_object('config') 9 | 10 | from pygmentizr import views 11 | -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | socket = /tmp/uwsgi.sock 3 | chown-socket = nginx:nginx 4 | chmod-socket = 664 5 | cheaper = 2 6 | processes = 16 7 | plugins = /usr/lib/uwsgi/python3_plugin.so 8 | module = pygmentizr 9 | callable = app 10 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | location / { 3 | try_files $uri @app; 4 | } 5 | location @app { 6 | include uwsgi_params; 7 | uwsgi_pass unix:///tmp/uwsgi.sock; 8 | } 9 | location /static { 10 | alias /pygmentizr/static; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:uwsgi] 5 | command=/usr/sbin/uwsgi --ini /etc/uwsgi/uwsgi.ini 6 | stdout_logfile=/dev/stdout 7 | stdout_logfile_maxbytes=0 8 | stderr_logfile=/dev/stderr 9 | stderr_logfile_maxbytes=0 10 | 11 | [program:nginx] 12 | command=/usr/sbin/nginx 13 | stdout_logfile=/dev/stdout 14 | stdout_logfile_maxbytes=0 15 | stderr_logfile=/dev/stderr 16 | stderr_logfile_maxbytes=0 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file requirements.txt requirements.in 6 | # 7 | docopt==0.6.2 8 | Flask-WTF==0.11 9 | Flask==0.10.1 # via flask-wtf 10 | itsdangerous==0.24 # via flask 11 | Jinja2==2.7.3 # via flask 12 | MarkupSafe==0.23 # via jinja2 13 | Pygments==2.0.2 14 | Werkzeug==0.10.1 # via flask, flask-wtf 15 | WTForms==2.0.2 # via flask-wtf 16 | -------------------------------------------------------------------------------- /pygmentizr/static/pygmentizr.css: -------------------------------------------------------------------------------- 1 | body { font-size: 1.6em; } 2 | pre { font-size: 0.9em; } 3 | footer { font-size: 0.75em; margin-top: 10em; } 4 | 5 | textarea { 6 | width: 100%; 7 | font-family: Menlo, Monaco, monospace; 8 | padding: 5px; 9 | margin-top: 5px; 10 | } 11 | 12 | .container { 13 | max-width: 900px; 14 | } 15 | 16 | /* 17 | Without this style, the "Reset" and "Apply Pygments" buttons were touching. 18 | There's probably a way to do this with button groups, but I couldn't work it 19 | out. 20 | */ 21 | .pull-right { margin-left: 5px; } 22 | 23 | /* 24 | Needed to "fix" the link in the "Reset" button. 25 | */ 26 | .btn a { color: #fff; } 27 | .btn a:hover { text-decoration: none; } -------------------------------------------------------------------------------- /pygmentizr/forms.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from flask.ext.wtf import Form 4 | from wtforms import SelectField, TextAreaField 5 | from wtforms.validators import DataRequired 6 | 7 | from pygmentizr import app 8 | from pygmentizr.renderer import STYLE_OPTIONS 9 | 10 | 11 | class SnippetForm(Form): 12 | """ 13 | Form for handling code snippets. 14 | """ 15 | language = SelectField( 16 | 'language', 17 | validators=[DataRequired()], 18 | choices=[(k, k) for k in app.config['SELECTED_LEXERS']], 19 | ) 20 | code = TextAreaField('code_snippet', validators=[DataRequired()]) 21 | style = SelectField( 22 | 'style', 23 | validators=[DataRequired()], 24 | choices=[(k, k) for k in STYLE_OPTIONS] 25 | ) 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | LABEL maintainer "Alex Chan alex@alexwlchan.net" 3 | 4 | RUN apk update 5 | RUN apk add nginx python3 supervisor uwsgi uwsgi-python3 6 | RUN pip3 install --upgrade pip 7 | 8 | EXPOSE 80 9 | 10 | # NGINX config: run in the background with custom configuration 11 | RUN echo "daemon off;" >> /etc/nginx/nginx.conf 12 | RUN rm /etc/nginx/conf.d/default.conf 13 | 14 | COPY requirements.txt /requirements.txt 15 | RUN pip3 install -r /requirements.txt 16 | 17 | COPY config.py /config.py 18 | COPY pygmentizr /pygmentizr 19 | 20 | COPY nginx.conf /etc/nginx/conf.d/ 21 | RUN mkdir -p /run/nginx 22 | COPY uwsgi.ini /etc/uwsgi 23 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 24 | 25 | CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # -*- encoding, utf-8 -*- 2 | 3 | from collections import OrderedDict 4 | import os 5 | 6 | WTF_CSRF_ENABLED = True 7 | SECRET_KEY = os.environ.get( 8 | 'SECRET_KEY', 'Pygments is a generic syntax highlighter' 9 | ) 10 | 11 | # Select the lexers to be exposed in the interface 12 | from pygments.lexers import * 13 | 14 | SELECTED_LEXERS = OrderedDict([ 15 | ('Python', PythonLexer), 16 | ('Python console', PythonConsoleLexer), 17 | ('Plain text', TextLexer), 18 | ('C', CLexer), 19 | ('C++', CppLexer), 20 | ('Bash/Ksh', BashLexer), 21 | ('Bash console', BashSessionLexer), 22 | ('Go', GoLexer), 23 | ('Rust', RustLexer), 24 | ('JavaScript', JavascriptLexer), 25 | ('JSON', JsonLexer), 26 | ('XML', XmlLexer), 27 | ('Diff', DiffLexer), 28 | ]) 29 | 30 | # Select the style used by Pygments 31 | PYGMENTS_STYLE = 'default' 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alex Chan 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 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | Pygmentizr. A web app for highlighting code with HTML using the 5 | Pygments library. 6 | 7 | Usage: 8 | manage.py run [--debug] [--port=] [--host=] 9 | manage.py (-h | --help) 10 | manage.py --version 11 | 12 | Options: 13 | -h --help Show this screen. 14 | --version Show version. 15 | --debug Whether to enable debug mode. 16 | --port= The port of the webserver. Defaults to 5000. 17 | --host= Hostname to listen on. Set to 0.0.0.0 to make the server 18 | available externally. Defaults to '127.0.0.1'. 19 | """ 20 | 21 | import sys 22 | 23 | from docopt import docopt 24 | 25 | from pygmentizr import app, __version__ 26 | 27 | 28 | if __name__ == '__main__': 29 | args = docopt(__doc__, version=__version__) 30 | 31 | is_debug = args['--debug'] 32 | if args['--port'] is not None: 33 | try: 34 | port_num = args['--port'] 35 | except ValueError: 36 | sys.exit('%s is not a valid port number.' % args['--port']) 37 | else: 38 | port_num = 5000 39 | host = args.get('--host', '127.0.0.1') 40 | 41 | app.run(debug=is_debug, port=port_num, host=host) 42 | -------------------------------------------------------------------------------- /pygmentizr/views.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | from flask import render_template 4 | from pygments.formatters import HtmlFormatter 5 | 6 | from pygmentizr import app, forms 7 | from pygmentizr.renderer import metacom_code, render_code 8 | 9 | 10 | @app.route('/', methods=['GET', 'POST']) 11 | def index(): 12 | form = forms.SnippetForm() 13 | 14 | if form.validate_on_submit(): 15 | 16 | try: 17 | lexer = app.config['SELECTED_LEXERS'][form.language.data] 18 | except KeyError: 19 | raise ValueError('Unrecognised language %s' % form.language.data) 20 | 21 | html_to_display = render_code( 22 | code_str=form.code.data, 23 | lexer=lexer, 24 | style=form.style.data, 25 | ) 26 | if form.style.data == 'For Metacom': 27 | html_code = metacom_code(html_to_display) 28 | else: 29 | html_code = html_to_display 30 | 31 | # Get the CSS used by this style to include on the page 32 | css = HtmlFormatter( 33 | style=app.config['PYGMENTS_STYLE'] 34 | ).get_style_defs() 35 | 36 | return render_template( 37 | 'index.html', 38 | form=form, 39 | html_code=html_code, 40 | html_to_display=html_to_display, 41 | css=css 42 | ) 43 | 44 | return render_template( 45 | 'index.html', 46 | form=form 47 | ) 48 | -------------------------------------------------------------------------------- /pygmentizr/renderer.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | import pygments 4 | from pygments.formatters import HtmlFormatter 5 | 6 | from pygmentizr import app 7 | 8 | 9 | STYLE_OPTIONS = [ 10 | 'Use CSS classes', 11 | 'Apply inline CSS styles', 12 | 'For Metacom', 13 | ] 14 | 15 | 16 | def metacom_code(hilite_str): 17 | """ 18 | Apply special styling for rendering code on Metacom. 19 | """ 20 | # Remove the opening and closing
tags 21 | opening = '
' 22 | closing = '
' 23 | hilite_str = hilite_str[len(opening):-len(closing)-1].strip() 24 | 25 | # Temporarily remove the opening/closing
 tags
26 |     opening_pre = '
'
27 |     closing_pre = '
' 28 | hilite_str = hilite_str[len(opening_pre):-len(closing_pre)-1].strip() 29 | 30 | # Turn off that horrific green coloring from Jive. 31 | opening_pre = '
'
32 | 
33 |     # Go through and add  tags to each line
34 |     hilite_str = '
\n'.join( 35 | '%s' % line if line.strip() else '' 36 | for line in hilite_str.splitlines() 37 | ) 38 | 39 | # Re-add the opening/closing
 tags
40 |     hilite_str = opening_pre + hilite_str + closing_pre
41 | 
42 |     return hilite_str
43 | 
44 | 
45 | def render_code(code_str, lexer, style):
46 |     """
47 |     Takes a string of code and a lexer, and returns a rendered HTML string.
48 |     """
49 |     noclasses = (style in ('Apply inline CSS styles', 'For Metacom'))
50 | 
51 |     formatter = HtmlFormatter(
52 |         noclasses=noclasses,
53 |         style=app.config['PYGMENTS_STYLE']
54 |     )
55 |     highlighted_string = pygments.highlight(code_str, lexer(), formatter)
56 | 
57 |     return highlighted_string


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | # pygmentizr
 2 | 
 3 | Pygmentizr is a web app that takes code, and returns HTML with syntax colouring, using the [Pygments library][1].
 4 | 
 5 | For motivation, see the [accompanying blog post][2].
 6 | 
 7 | You can try a [live demo](https://alexwlchan.net/experiments/pygmentizr/) of the app.
 8 | 
 9 | ## Installation
10 | 
11 | Clone this repository, create a virtualenv, then install the requirements with pip:
12 | 
13 | ```console
14 | $ git clone git@github.com:alexwlchan/pygmentizr.git
15 | $ cd pygmentizr
16 | $ virtualenv env; source env/bin/activate
17 | $ pip install -r requirements.txt
18 | ```
19 | 
20 | ## Configuration
21 | 
22 | The list of lexers/language choices is specified in `config.py`. Here's an example:
23 | 
24 | ```python
25 | from pygments.lexers import *
26 | 
27 | SELECTED_LEXERS = OrderedDict([
28 |     ('Python',           PythonLexer),
29 |     ('Python console',   PythonConsoleLexer),
30 |     ('CoffeeScript',     CoffeeScriptLexer),
31 |     ('CSS',              CssLexer),
32 | ])
33 | ```
34 | 
35 | For each language you want to include, add a tuple to `SELECTED_LEXERS`.
36 | The first item should be the name you want to appear in the interface, the second the lexer object from Pygments.
37 | 
38 | ## Running the app
39 | 
40 | To run a local instance, invoke as follows:
41 | 
42 | ```console
43 | $ cd /path/to/pygmentizr
44 | $ source env/bin/activate
45 | $ python manage.py run
46 | ```
47 | 
48 | This starts the Flask instance which runs Pygmentizr. Point a web browser at  to use the app.
49 | 
50 | Alternatively, you can use the Docker implementation:
51 | 
52 | ```console
53 | $ docker build -t alexwlchan/pygmentizr .
54 | $ docker run -p 5000:80 alexwlchan/pygmentizr
55 | ```
56 | 
57 | ## Acknowledgements
58 | 
59 | The favicon is the [fa-code icon][3] from the [Font Awesome][4] toolkit.
60 | 
61 | [1]: http://pygments.org
62 | [2]: http://www.alexwlchan.net/2015/03/pygmentizr
63 | [3]: http://fortawesome.github.io/Font-Awesome/icon/code/
64 | [4]: http://fortawesome.github.io/Font-Awesome/
65 | 


--------------------------------------------------------------------------------
/pygmentizr/templates/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     
 7 | 
 8 |     Pygmentizr
 9 | 
10 |     
11 |     
12 | 
13 |     
14 |     
15 | 
16 |     
17 |   
18 | 
19 |   
20 | 
21 |     
22 | 23 | 26 | 27 |

Pygmentizr is an tool for highlighting code using the Pygments library. You can enter some code below, select the language, and it gives you the rendered HTML suitable to drop into an email, forum post or web page.

28 | 29 |
30 | 31 |
32 |
33 | {{ form.hidden_tag() }} 34 |

{{ form.code(rows="6", placeholder="Enter your code here", class="form-control") }}

35 |
36 |
37 |

{{ form.language() }}

38 |
39 |
40 |

{{ form.style() }}

41 |
42 |

43 | 44 | 45 |

46 |
47 |
48 |
49 | 50 | {% if html_code %} 51 |
52 |

This is your HTML:

53 | 56 |

and here's a preview:

57 | {{ html_to_display | safe }} 58 | {% endif %} 59 | 60 | 64 | 65 |
66 | 67 | 68 | 76 | 77 | 78 | 79 | 80 | --------------------------------------------------------------------------------