summaryrefslogtreecommitdiffstats
path: root/_filters/syntax_highlight.py
blob: 2471c430968da0158884ad47956ba4efe5be1452 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import re
import os

import pygments
from pygments import formatters, util, lexers
import blogofile_bf as bf
 
config = {"name": "Syntax Highlighter",
          "description": "Highlights blocks of code based on syntax",
          "author": "Ryan McGuire",
          "css_dir": "/css",
          "preload_styles": []}


def init():
    #This filter normally only loads pygments styles when needed.
    #This will force a particular style to get loaded at startup.
    for style in bf.config.filters.syntax_highlight.preload_styles:
        css_class = "pygments_{0}".format(style)
        formatter = pygments.formatters.HtmlFormatter(
            linenos=False, cssclass=css_class, style=style)
        write_pygments_css(style, formatter)
        

example = """

This is normal text.

The following is a python code block:

$$code(lang=python)
import this

prices = {'apple' : 0.50,    #Prices of fruit
          'orange' : 0.65,
          'pear' : 0.90}

def print_prices():
    for fruit, price in prices.items():
        print "An %s costs %s" % (fruit, price)
$$/code

This is a ruby code block:

$$code(lang=ruby)
class Person
  attr_reader :name, :age
  def initialize(name, age)
    @name, @age = name, age
  end
  def <=>(person) # Comparison operator for sorting
    @age <=> person.age
  end
  def to_s
    "#@name (#@age)"
  end
end
 
group = [
  Person.new("Bob", 33), 
  Person.new("Chris", 16), 
  Person.new("Ash", 23) 
]
 
puts group.sort.reverse
$$/code

This is normal text
"""

css_files_written = set()

code_block_re = re.compile(
    r"(?:^|\s)"                 # $$code Must start as a new word
    r"\$\$code"                 # $$code is the start of the block
    r"(?P<args>\([^\r\n]*\))?"  # optional arguments are passed in brackets
    r"[^\r\n]*\r?\n"            # ignore everything else on the 1st line
    r"(?P<code>.*?)\s\$\$/code" # code block continues until $$/code
    , re.DOTALL)

argument_re = re.compile(
    r"[ ]*" # eat spaces at the beginning
    "(?P<arg>" # start of argument
    ".*?" # the name of the argument
    "=" # the assignment
    r"""(?:(?:[^"']*?)""" # a non-quoted value
    r"""|(?:"[^"]*")""" # or, a double-quoted value
    r"""|(?:'[^']*')))""" # or, a single-quoted value
    "[ ]*" # eat spaces at the end
    "[,\r\n]" # ends in a comma or newline
    )


def highlight_code(code, language, formatter):
    try:
        lexer = pygments.lexers.get_lexer_by_name(language)
    except pygments.util.ClassNotFound:
        lexer = pygments.lexers.get_lexer_by_name("text")
    #Highlight with pygments and surround by blank lines
    #(blank lines required for markdown syntax)
    highlighted = "\n\n{0}\n\n".format(
            pygments.highlight(code, lexer, formatter))
    return highlighted


def parse_args(args):
    #Make sure the args are newline terminated (req'd by regex)
    opts = {}
    if args is None:
        return opts
    args = args.lstrip("(").rstrip(")")
    if args[-1] != "\n":
        args = args+"\n"
    for m in argument_re.finditer(args):
        arg = m.group('arg').split('=')
        opts[arg[0]] = arg[1]
    return opts


def write_pygments_css(style, formatter,
        location=bf.config.filters.syntax_highlight.css_dir):
    path = bf.util.path_join("_site", bf.util.fs_site_path_helper(location))
    bf.util.mkdir(path)
    css_file = "pygments_{0}.css".format(style)
    css_path = os.path.join(path, css_file)
    css_site_path = css_path.replace("_site", "")
    if css_site_path in css_files_written:
        return #already written, no need to overwrite it.
    f = open(css_path, "w")
    css_class = ".pygments_{0}".format(style)
    f.write(formatter.get_style_defs(css_class))
    f.close()
    css_files_written.add(css_site_path)


def run(src):
    substitutions = {}
    for m in code_block_re.finditer(src):
        args = parse_args(m.group('args'))
        #Make default args
        if args.has_key('lang'):
            lang = args['lang']
        elif args.has_key('language'):
            lang = args['language']
        else:
            lang = 'text'
        try:
            if args.has_key('linenums'):
                linenums = args['linenums']
            elif args.has_key("linenos"):
                linenums = args['linenos']
            if linenums.lower().strip() == "true":
                linenums = True
            else:
                linenums = False
        except:
            linenums = False
        try:
            style = args['style']
        except KeyError:
            style = bf.config.filters.syntax_highlight.style
        try:
            css_class = args['cssclass']
        except KeyError:
            css_class = "pygments_{0}".format(style)
        formatter = pygments.formatters.HtmlFormatter(
            linenos=linenums, cssclass=css_class, style=style)
        write_pygments_css(style, formatter)
        substitutions[m.group()] = highlight_code(
                m.group('code'), lang, formatter)
    if len(substitutions) > 0:
        p = re.compile('|'.join(map(re.escape, substitutions)))
        src = p.sub(lambda x: substitutions[x.group(0)], src)
        return src
    else:
        return src