Code

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:

  1. First we have call to the CSS
    <link type="text/css" rel="stylesheet" href="Styles/SyntaxHighlighter.css"></link>
    
  2. 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.

  1. 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>
    
  2. 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

Last modified 5 years ago Last modified on 10/22/09 15:01:35