| | 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]''' |