]> git.sur5r.net Git - i3/i3.github.io/blob - _filters/syntax_highlight.py
2471c430968da0158884ad47956ba4efe5be1452
[i3/i3.github.io] / _filters / syntax_highlight.py
1 import re
2 import os
3
4 import pygments
5 from pygments import formatters, util, lexers
6 import blogofile_bf as bf
7  
8 config = {"name": "Syntax Highlighter",
9           "description": "Highlights blocks of code based on syntax",
10           "author": "Ryan McGuire",
11           "css_dir": "/css",
12           "preload_styles": []}
13
14
15 def init():
16     #This filter normally only loads pygments styles when needed.
17     #This will force a particular style to get loaded at startup.
18     for style in bf.config.filters.syntax_highlight.preload_styles:
19         css_class = "pygments_{0}".format(style)
20         formatter = pygments.formatters.HtmlFormatter(
21             linenos=False, cssclass=css_class, style=style)
22         write_pygments_css(style, formatter)
23         
24
25 example = """
26
27 This is normal text.
28
29 The following is a python code block:
30
31 $$code(lang=python)
32 import this
33
34 prices = {'apple' : 0.50,    #Prices of fruit
35           'orange' : 0.65,
36           'pear' : 0.90}
37
38 def print_prices():
39     for fruit, price in prices.items():
40         print "An %s costs %s" % (fruit, price)
41 $$/code
42
43 This is a ruby code block:
44
45 $$code(lang=ruby)
46 class Person
47   attr_reader :name, :age
48   def initialize(name, age)
49     @name, @age = name, age
50   end
51   def <=>(person) # Comparison operator for sorting
52     @age <=> person.age
53   end
54   def to_s
55     "#@name (#@age)"
56   end
57 end
58  
59 group = [
60   Person.new("Bob", 33), 
61   Person.new("Chris", 16), 
62   Person.new("Ash", 23) 
63 ]
64  
65 puts group.sort.reverse
66 $$/code
67
68 This is normal text
69 """
70
71 css_files_written = set()
72
73 code_block_re = re.compile(
74     r"(?:^|\s)"                 # $$code Must start as a new word
75     r"\$\$code"                 # $$code is the start of the block
76     r"(?P<args>\([^\r\n]*\))?"  # optional arguments are passed in brackets
77     r"[^\r\n]*\r?\n"            # ignore everything else on the 1st line
78     r"(?P<code>.*?)\s\$\$/code" # code block continues until $$/code
79     , re.DOTALL)
80
81 argument_re = re.compile(
82     r"[ ]*" # eat spaces at the beginning
83     "(?P<arg>" # start of argument
84     ".*?" # the name of the argument
85     "=" # the assignment
86     r"""(?:(?:[^"']*?)""" # a non-quoted value
87     r"""|(?:"[^"]*")""" # or, a double-quoted value
88     r"""|(?:'[^']*')))""" # or, a single-quoted value
89     "[ ]*" # eat spaces at the end
90     "[,\r\n]" # ends in a comma or newline
91     )
92
93
94 def highlight_code(code, language, formatter):
95     try:
96         lexer = pygments.lexers.get_lexer_by_name(language)
97     except pygments.util.ClassNotFound:
98         lexer = pygments.lexers.get_lexer_by_name("text")
99     #Highlight with pygments and surround by blank lines
100     #(blank lines required for markdown syntax)
101     highlighted = "\n\n{0}\n\n".format(
102             pygments.highlight(code, lexer, formatter))
103     return highlighted
104
105
106 def parse_args(args):
107     #Make sure the args are newline terminated (req'd by regex)
108     opts = {}
109     if args is None:
110         return opts
111     args = args.lstrip("(").rstrip(")")
112     if args[-1] != "\n":
113         args = args+"\n"
114     for m in argument_re.finditer(args):
115         arg = m.group('arg').split('=')
116         opts[arg[0]] = arg[1]
117     return opts
118
119
120 def write_pygments_css(style, formatter,
121         location=bf.config.filters.syntax_highlight.css_dir):
122     path = bf.util.path_join("_site", bf.util.fs_site_path_helper(location))
123     bf.util.mkdir(path)
124     css_file = "pygments_{0}.css".format(style)
125     css_path = os.path.join(path, css_file)
126     css_site_path = css_path.replace("_site", "")
127     if css_site_path in css_files_written:
128         return #already written, no need to overwrite it.
129     f = open(css_path, "w")
130     css_class = ".pygments_{0}".format(style)
131     f.write(formatter.get_style_defs(css_class))
132     f.close()
133     css_files_written.add(css_site_path)
134
135
136 def run(src):
137     substitutions = {}
138     for m in code_block_re.finditer(src):
139         args = parse_args(m.group('args'))
140         #Make default args
141         if args.has_key('lang'):
142             lang = args['lang']
143         elif args.has_key('language'):
144             lang = args['language']
145         else:
146             lang = 'text'
147         try:
148             if args.has_key('linenums'):
149                 linenums = args['linenums']
150             elif args.has_key("linenos"):
151                 linenums = args['linenos']
152             if linenums.lower().strip() == "true":
153                 linenums = True
154             else:
155                 linenums = False
156         except:
157             linenums = False
158         try:
159             style = args['style']
160         except KeyError:
161             style = bf.config.filters.syntax_highlight.style
162         try:
163             css_class = args['cssclass']
164         except KeyError:
165             css_class = "pygments_{0}".format(style)
166         formatter = pygments.formatters.HtmlFormatter(
167             linenos=linenums, cssclass=css_class, style=style)
168         write_pygments_css(style, formatter)
169         substitutions[m.group()] = highlight_code(
170                 m.group('code'), lang, formatter)
171     if len(substitutions) > 0:
172         p = re.compile('|'.join(map(re.escape, substitutions)))
173         src = p.sub(lambda x: substitutions[x.group(0)], src)
174         return src
175     else:
176         return src