Django has textile, markdown and other markup languages as filters in the templates. But those filters can't do complex operations. For example we want such tags in our page content:
[art id="34"] [art id="11"]
And we want them to become in the browser:
- <a href="/link/to/article34/">article title34</a><br> - <a href="/link/to/article11/">article title11</a><br>
Something like BBcode but with the ability to respond do attributes passed in the tags. From some time I was using such tags in my PHP scripts. I called them “ContentBBcode". Now the time have come to djangoify the concept.
The Code – The Implementation
I've written the parser that finds and replaces ContentBBcode tags (CBC for short) with proper response, and I won't go to details about it. Put this code in a file and save it as cbcparser.py (I saved it in the application folder in which I want to use it):
import re import sys sys.path.append('wiki/cbcplugins/') def parse_cbc_tags(text): # double: [tag]something here[/tag] tags = re.findall( r'(?xs)\[\s*rk:([a-z]*)\s*(.*?)\](.*?)\[(?=\s*/rk)\s*/rk:(\1)\s*\]''', text, re.MULTILINE) parsed_double = {} for tag in tags: k = str(tag[0]).strip() v = tag[1] v = v.split(' ') vals = {} vals['attributes'] = {} for attr in v: attr = attr.split('=') val = attr[1] attr[1] = val[1:-1] vals['attributes'][attr[0]] = attr[1] vals['code'] = tag[2] vals['tag'] = '[rk:' + tag[0] + ' ' + tag[1] + ']' + tag[2] + '[/rk:' + tag[0] + ']' if k not in parsed_double: parsed_double[k] = list() parsed_double[k].append(vals) for plugin in parsed_double: try: exec 'from ' + plugin + ' import *' except: pass else: text = render(parsed_double[plugin], text) # single: [tag] tags = re.findall('\[rk:([a-z]*) ([a-zA-z0-9 =.,"\']*)\]', text) parsed = {} for tag in tags: k = str(tag[0]).strip() v = tag[1] v = v.split(' ') vals = {} vals['attributes'] = {} for attr in v: attr = attr.split('=') val = attr[1] attr[1] = val[1:-1] vals['attributes'][attr[0]] = attr[1] vals['tag'] = '[rk:' + tag[0] + ' ' + tag[1] + ']' if k not in parsed: parsed[k] = list() parsed[k].append(vals) for plugin in parsed: try: exec 'from ' + plugin + ' import *' except: pass else: text = render(parsed[plugin], text) return text
Note on the line:
sys.path.append('wiki/cbcplugins/')
each tag for this parser is a python file ([art...] would need art.py). If don't put your plugins in PYTHONPATH then you need to append location of the plugin folder to PYTHONPATH. In my case wiki/ was my application folder and wiki/cbcplugins/ folder with plugins. Change the path to fit your needs.
Now we will make a template filter out of it. Create templategas folder in your application folder and create empty init.py file and cbc.py file with the code:
from project.aplication.cbcparser import * from django import template register = template.Library() def cbc(value): # Only one argument. return parse_cbc_tags(value) register.filter('cbc', cbc)
Where from project.aplication.cbcparser import * is the cbcparser.py importing. In my case it is from diamanda.wiki.cbcparser import *
And we are done. In a template in which you want to use it put
{% load cbc %}
and then to parse a string you just use:
{{ mystring|cbc }}
Now about the plugins. The parser tries to load them and if it succeeds it will call render function passing two variables – a dictionary with parsed data from the tag and the string. The tag should look like:
[rk:tagname attr1="value1" attr2="value2"]
Where tagname is the name of the tag and name of the plugin filename (tagname.py).
A basic plugin code would look like this:
def render(dic, text): for i in dic: text = text.replace(i['tag'], '<h1>Article ID ' + i['attributes']['id'] + '</h1>') return text
for a CBC:
[rk:art id="value"]
dic is a dictionary which has few dictionaries in it. attributes has all the attributes (as dictionaries), tag is a string containing the tag code which we replace.
attributes[ 'attrname' ] gives you value from given attribute from the tag ( attributes[ 'attr1' ] would give you value1). In the end the “rk:art" tag would become:
<h1>Article ID *value*</h1>
The parser supports also double tags:
[rk:tagname attr="value"]code here[/rk:tagname]
They work the same as single-tags with one difference – the code between tags is also available in the plugin as dic[ 'code' ]. For example for a tag:
[rk:codder lang="python"]a code here[/rk:codder]
We would create codder.py plugin:
def render(dic, text): for i in dic: text = text.replace(i['tag'], '<B>'+ i['attributes']['lang'] +'</B><pre><code>' + i['code'] + '</code></pre>') return text
Usage
- as a wrappers for JavaScript and similar code/widgets
- as a markup tags that need to get data from somewhere (database etc.)
Real Example
Now we will make a plugin for dp.syntaxhighlighter – a javascript based code highlighter. We have our codder.py plugin but it doesn't do any usefull things.
- Download the script archive and extract it to an empty folder
- Copy Styles and Scripts folders to your django media folder (I've placed them in MEDIA_ROOT/syntax/)
- You can open in a text editor one of examples to see how it works...
It works like this:
- First we have call to the CSS
<link type="text/css" rel="stylesheet" href="Styles/SyntaxHighlighter.css"></link>
- Then we place code for highlighting in textareas:
<textarea name="code" class="LANGNAME"> code here </textarea>
The name="code" class="LANGNAME" part make the textarea to work.
- After all textareas we load JS files for languages we use and one main one (shCore.js):
<script class="javascript" src="Scripts/shCore.js"></script> <script class="javascript" src="Scripts/shBrushCSharp.js"></script> <script class="javascript" src="Scripts/shBrushPhp.js"></script> <script class="javascript" src="Scripts/shBrushJScript.js"></script> <script class="javascript" src="Scripts/shBrushJava.js"></script> <script class="javascript" src="Scripts/shBrushVb.js"></script> <script class="javascript" src="Scripts/shBrushSql.js"></script> <script class="javascript" src="Scripts/shBrushXml.js"></script> <script class="javascript" src="Scripts/shBrushDelphi.js"></script> <script class="javascript" src="Scripts/shBrushPython.js"></script> <script class="javascript" src="Scripts/shBrushRuby.js"></script> <script class="javascript" src="Scripts/shBrushCss.js"></script>
- At the end we initialize the script:
<script class="javascript"> dp.SyntaxHighlighter.HighlightAll('code'); </script>
And thats all. Now how to make a plugin out of it? Like this:
def render(dic, text): # w3c will kill us for this :) text = '<link type="text/css" rel="stylesheet" href="/site_media/syntax/Styles/SyntaxHighlighter.css"></link>' + text langs = {} for i in dic: text = text.replace(i['tag'], '<textarea name="code" class="'+ i['attributes']['lang'] +'" rows="15" cols="90">' + i['code'] + '</textarea>') # what langs are used? langs[i['attributes']['lang']] = True # add the core JS text = text + '<script class="javascript" src="/site_media/syntax/Scripts/shCore.js"></script>' # add only those lang-JS files that we realy need. For example i limit it to two if 'python' in langs: text = text + '<script class="javascript" src="/site_media/syntax/Scripts/shBrushPython.js"></script>' if 'xml' in langs: text = text + '<script class="javascript" src="/site_media/syntax/Scripts/shBrushXml.js"></script>' # the end, activate the code text = text + '<script class="javascript">dp.SyntaxHighlighter.HighlightAll(\'code\');</script>' return text
- We changed paths to static files (JS and CSS). If you intend to use it often you could move CSS “injection" to HEAD in your template.
- Next we replace all “codder" tags with the textarea with language we want to use
- We make a dictionary which gathers all languages we use
- Next we inject the main JS file and then those language specific JS files we really need
- And at the end we add the initialization code.
Now CBC like:
[rk:codder lang="python"] for foo in bar: print foo [/rk:codder]
Will get highlighted. See a screenshot here