| 1 | 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: |
| 2 | {{{ |
| 3 | [art id="34"] |
| 4 | [art id="11"] |
| 5 | }}} |
| 6 | And we want them to become in the browser: |
| 7 | {{{ |
| 8 | - <a href="/link/to/article34/">article title34</a><br> |
| 9 | - <a href="/link/to/article11/">article title11</a><br> |
| 10 | }}} |
| 11 | 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. |
| 12 | |
| 13 | === The Code – The Implementation === |
| 14 | 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): |
| 15 | {{{ |
| 16 | import re |
| 17 | import sys |
| 18 | sys.path.append('wiki/cbcplugins/') |
| 19 | |
| 20 | def parse_cbc_tags(text): |
| 21 | # double: [tag]something here[/tag] |
| 22 | tags = re.findall( r'(?xs)\[\s*rk:([a-z]*)\s*(.*?)\](.*?)\[(?=\s*/rk)\s*/rk:(\1)\s*\]''', text, re.MULTILINE) |
| 23 | parsed_double = {} |
| 24 | for tag in tags: |
| 25 | k = str(tag[0]).strip() |
| 26 | v = tag[1] |
| 27 | v = v.split(' ') |
| 28 | vals = {} |
| 29 | vals['attributes'] = {} |
| 30 | for attr in v: |
| 31 | attr = attr.split('=') |
| 32 | val = attr[1] |
| 33 | attr[1] = val[1:-1] |
| 34 | vals['attributes'][attr[0]] = attr[1] |
| 35 | vals['code'] = tag[2] |
| 36 | vals['tag'] = '[rk:' + tag[0] + ' ' + tag[1] + ']' + tag[2] + '[/rk:' + tag[0] + ']' |
| 37 | if not parsed_double.has_key(k): |
| 38 | parsed_double[k] = list() |
| 39 | parsed_double[k].append(vals) |
| 40 | |
| 41 | for plugin in parsed_double: |
| 42 | try: |
| 43 | exec 'from ' + plugin + ' import *' |
| 44 | except: |
| 45 | pass |
| 46 | else: |
| 47 | text = render(parsed_double[plugin], text) |
| 48 | |
| 49 | # single: [tag] |
| 50 | tags = re.findall('\[rk:([a-z]*) ([a-zA-z0-9 =.,"\']*)\]', text) |
| 51 | parsed = {} |
| 52 | for tag in tags: |
| 53 | k = str(tag[0]).strip() |
| 54 | v = tag[1] |
| 55 | v = v.split(' ') |
| 56 | vals = {} |
| 57 | vals['attributes'] = {} |
| 58 | for attr in v: |
| 59 | attr = attr.split('=') |
| 60 | val = attr[1] |
| 61 | attr[1] = val[1:-1] |
| 62 | vals['attributes'][attr[0]] = attr[1] |
| 63 | vals['tag'] = '[rk:' + tag[0] + ' ' + tag[1] + ']' |
| 64 | if not parsed.has_key(k): |
| 65 | parsed[k] = list() |
| 66 | parsed[k].append(vals) |
| 67 | |
| 68 | for plugin in parsed: |
| 69 | try: |
| 70 | exec 'from ' + plugin + ' import *' |
| 71 | except: |
| 72 | pass |
| 73 | else: |
| 74 | text = render(parsed[plugin], text) |
| 75 | return text |
| 76 | }}} |
| 77 | Note on the line: |
| 78 | {{{ |
| 79 | sys.path.append('wiki/cbcplugins/') |
| 80 | }}} |
| 81 | 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. |
| 82 | |
| 83 | 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: |
| 84 | {{{ |
| 85 | from project.aplication.cbcparser import * |
| 86 | from django import template |
| 87 | |
| 88 | register = template.Library() |
| 89 | |
| 90 | def cbc(value): # Only one argument. |
| 91 | return parse_cbc_tags(value) |
| 92 | |
| 93 | register.filter('cbc', cbc) |
| 94 | }}} |
| 95 | Where '''from project.aplication.cbcparser import *''' is the cbcparser.py importing. In my case it is '''from diamanda.wiki.cbcparser import *''' |
| 96 | |
| 97 | And we are done. In a template in which you want to use it put |
| 98 | {{{ |
| 99 | {% load cbc %} |
| 100 | }}} |
| 101 | and then to parse a string you just use: |
| 102 | {{{ |
| 103 | {{ mystring|cbc }} |
| 104 | }}} |
| 105 | |
| 106 | |
| 107 | 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: |
| 108 | {{{ |
| 109 | [rk:tagname attr1="value1" attr2="value2"] |
| 110 | }}} |
| 111 | Where '''tagname''' is the name of the tag and name of the plugin filename (tagname.py). |
| 112 | |
| 113 | A basic plugin code would look like this: |
| 114 | {{{ |
| 115 | def render(dic, text): |
| 116 | for i in dic: |
| 117 | text = text.replace(i['tag'], '<h1>Article ID ' + i['attributes']['id'] + '</h1>') |
| 118 | return text |
| 119 | }}} |
| 120 | for a CBC: |
| 121 | {{{ |
| 122 | [rk:art id="value"] |
| 123 | }}} |
| 124 | '''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. |
| 125 | '''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: |
| 126 | {{{ |
| 127 | <h1>Article ID *value*</h1> |
| 128 | }}} |
| 129 | The parser supports also double tags: |
| 130 | {{{ |
| 131 | [rk:tagname attr="value"]code here[/rk:tagname] |
| 132 | }}} |
| 133 | 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: |
| 134 | {{{ |
| 135 | [rk:codder lang="python"]a code here[/rk:codder] |
| 136 | }}} |
| 137 | We would create codder.py plugin: |
| 138 | {{{ |
| 139 | def render(dic, text): |
| 140 | for i in dic: |
| 141 | text = text.replace(i['tag'], '<B>'+ i['attributes']['lang'] +'</B><pre><code>' + i['code'] + '</code></pre>') |
| 142 | return text |
| 143 | }}} |
| 144 | |
| 145 | |
| 146 | === Usage === |
| 147 | - as a wrappers for JavaScript and similar code/widgets |
| 148 | |
| 149 | - as a markup tags that need to get data from somewhere (database etc.) |
| 150 | |
| 151 | |
| 152 | === Real Example === |
| 153 | Now we will make a plugin for [http://www.dreamprojections.com/syntaxhighlighter/ dp.syntaxhighlighter] – a javascript based code highlighter. We have our '''codder.py''' plugin but it doesn't do any usefull things. |
| 154 | |
| 155 | * Download the script archive and extract it to an empty folder |
| 156 | * Copy '''Styles''' and '''Scripts''' folders to your django media folder (I've placed them in MEDIA_ROOT/syntax/) |
| 157 | * You can open in a text editor one of examples to see how it works... |
| 158 | |
| 159 | It works like this: |
| 160 | |
| 161 | 1. First we have call to the CSS |
| 162 | {{{ |
| 163 | <link type="text/css" rel="stylesheet" href="Styles/SyntaxHighlighter.css"></link> |
| 164 | }}} |
| 165 | 2. Then we place code for highlighting in textareas: |
| 166 | {{{ |
| 167 | <textarea name="code" class="LANGNAME"> code here </textarea> |
| 168 | }}} |
| 169 | The ''' name="code" class="LANGNAME"''' part make the textarea to work. |
| 170 | 3. After all textareas we load JS files for languages we use and one main one (shCore.js): |
| 171 | {{{ |
| 172 | <script class="javascript" src="Scripts/shCore.js"></script> |
| 173 | <script class="javascript" src="Scripts/shBrushCSharp.js"></script> |
| 174 | <script class="javascript" src="Scripts/shBrushPhp.js"></script> |
| 175 | <script class="javascript" src="Scripts/shBrushJScript.js"></script> |
| 176 | <script class="javascript" src="Scripts/shBrushJava.js"></script> |
| 177 | <script class="javascript" src="Scripts/shBrushVb.js"></script> |
| 178 | <script class="javascript" src="Scripts/shBrushSql.js"></script> |
| 179 | <script class="javascript" src="Scripts/shBrushXml.js"></script> |
| 180 | <script class="javascript" src="Scripts/shBrushDelphi.js"></script> |
| 181 | <script class="javascript" src="Scripts/shBrushPython.js"></script> |
| 182 | <script class="javascript" src="Scripts/shBrushRuby.js"></script> |
| 183 | <script class="javascript" src="Scripts/shBrushCss.js"></script> |
| 184 | }}} |
| 185 | 4. At the end we initialize the script: |
| 186 | {{{ |
| 187 | <script class="javascript"> |
| 188 | dp.SyntaxHighlighter.HighlightAll('code'); |
| 189 | </script> |
| 190 | }}} |
| 191 | And thats all. Now how to make a plugin out of it? Like this: |
| 192 | {{{ |
| 193 | def render(dic, text): |
| 194 | # w3c will kill us for this :) |
| 195 | text = '<link type="text/css" rel="stylesheet" href="/site_media/syntax/Styles/SyntaxHighlighter.css"></link>' + text |
| 196 | langs = {} |
| 197 | for i in dic: |
| 198 | text = text.replace(i['tag'], '<textarea name="code" class="'+ i['attributes']['lang'] +'" rows="15" cols="90">' + i['code'] + '</textarea>') |
| 199 | # what langs are used? |
| 200 | langs[i['attributes']['lang']] = True |
| 201 | |
| 202 | # add the core JS |
| 203 | text = text + '<script class="javascript" src="/site_media/syntax/Scripts/shCore.js"></script>' |
| 204 | # add only those lang-JS files that we realy need. For example i limit it to two |
| 205 | if langs.has_key('python'): |
| 206 | text = text + '<script class="javascript" src="/site_media/syntax/Scripts/shBrushPython.js"></script>' |
| 207 | if langs.has_key('xml'): |
| 208 | text = text + '<script class="javascript" src="/site_media/syntax/Scripts/shBrushXml.js"></script>' |
| 209 | # the end, activate the code |
| 210 | text = text + '<script class="javascript">dp.SyntaxHighlighter.HighlightAll(\'code\');</script>' |
| 211 | return text |
| 212 | }}} |
| 213 | * 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. |
| 214 | * Next we replace all “codder" tags with the textarea with language we want to use |
| 215 | * We make a dictionary which gathers all languages we use |
| 216 | * Next we inject the main JS file and then those language specific JS files we really need |
| 217 | * And at the end we add the initialization code. |
| 218 | |
| 219 | Now CBC like: |
| 220 | {{{ |
| 221 | [rk:codder lang="python"] |
| 222 | for foo in bar: |
| 223 | print foo |
| 224 | [/rk:codder] |
| 225 | }}} |
| 226 | Will get highlighted. '''See a screenshot [http://www.fotosik.pl/showFullSize.php?id=4a82e9c901329a06 here]''' |