├── .gitignore ├── Makefile ├── README.md ├── TextMate └── Sparkup.tmbundle │ ├── Commands │ └── Sparkup expand.tmCommand │ ├── Support │ └── sparkup.py │ └── info.plist ├── ftdetect └── hsb.vim ├── ftplugin ├── mit-license.txt ├── sparkup-unittest.py ├── sparkup.py └── vim ├── README.txt └── ftplugin ├── html ├── sparkup.py └── sparkup.vim ├── htmldjango ├── smarty └── xml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .project 3 | doc 4 | distribution/ 5 | .sourcescribe_index 6 | *.swp 7 | *.swo 8 | *.pyc 9 | cscope.out 10 | *~ 11 | 12 | # symlinks created for pathogen 13 | /ftplugin 14 | /doc 15 | 16 | # Distribution files 17 | # */sparkup 18 | # */sparkup.py 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for sparkup distribution 2 | # TODO: this should use a separate build dir to copy SPARKUP_PY into. 3 | # SPARKUP_PY should not reside in the Vim runtime dir (getting not updated via Git!) 4 | SPARKUP_PY=sparkup.py 5 | VERSION=`date '+%Y%m%d'` 6 | README=README.md 7 | 8 | .PHONY: all textmate vim textmate-dist vim-dist plugins plugins-pre generic all-dist 9 | all: plugins 10 | 11 | plugins-pre: 12 | mkdir -p distribution 13 | 14 | plugins: plugins-pre all-dist 15 | 16 | textmate-dist: textmate 17 | cd TextMate && zip -9r ../distribution/sparkup-textmate-${VERSION}.zip . && cd .. 18 | 19 | vim-dist: vim 20 | cd vim && zip -9r ../distribution/sparkup-vim-${VERSION}.zip . && cd .. 21 | 22 | generic-dist: generic 23 | cd generic && zip -9r ../distribution/sparkup-generic-${VERSION}.zip . && cd .. 24 | 25 | all-dist: 26 | zip -9r distribution/sparkup-${VERSION}.zip generic vim textmate README.md -x */sparkup-readme.txt 27 | cp distribution/sparkup-${VERSION}.zip distribution/sparkup-latest.zip 28 | 29 | generic: 30 | cat ${SPARKUP_PY} > generic/sparkup 31 | chmod +x generic/sparkup 32 | #cp ${README} generic/sparkup-readme.txt 33 | 34 | textmate: 35 | #cp ${README} TextMate/sparkup-readme.txt 36 | 37 | vim: vim/doc/sparkup.txt 38 | 39 | # create pathogen friendly structure 40 | vim-pathogen: vim ftplugin doc 41 | 42 | ftplugin doc: 43 | ln -s vim/$@ 44 | 45 | # Add asterisks to title, so it gets matched by `:helptags` 46 | vim/doc/sparkup.txt: ${README} 47 | mkdir -p $(@D) 48 | sed '1s/.*/*\0*/' $< > $@ 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Sparkup 2 | ======= 3 | 4 | **Sparkup lets you write HTML code faster.** Don't believe us? 5 | [See it in action!](http://www.youtube.com/watch?v=Jw3jipcenKc) 6 | 7 | Fixed by Zhao: 8 | This is a fork of original version. This version support both python 2 and 3. 9 | 10 | You can write HTML in a CSS-like syntax, and have Sparkup handle the expansion to full HTML 11 | code. It is meant to help you write long HTML blocks in your text editor by letting you 12 | type less characters than needed. 13 | 14 | Sparkup is written in Python, and requires Python 2.5 or newer (2.5 is preinstalled in 15 | Mac OS X Leopard). Sparkup also offers integration into common text editors. Support for VIM 16 | and TextMate are currently included. 17 | 18 | A short screencast is available here: 19 | [http://www.youtube.com/watch?v=Jw3jipcenKc](http://www.youtube.com/watch?v=Jw3jipcenKc) 20 | 21 | Usage and installation 22 | ---------------------- 23 | You may download Sparkup from GitHub. [Download the latest version here](http://github.com/rstacruz/sparkup/downloads). 24 | 25 | - **TextMate**: Simply double-click on the `Sparkup.tmbundle` package in Finder. This 26 | will install it automatically. In TextMate, open an HTML file (or set the document type to 27 | HTML) type in something (e.g., `#header > h1`), then press `Ctrl` + `E`. Pressing `Tab` 28 | will cycle through empty elements. 29 | 30 | - **VIM**: See the `vim/README.txt` file for installation. In VIM, 31 | create or open an HTML file (or set the filetype to ``html``), type in something (e.g. 32 | `#header > h1`), then press `` whilst in **insert mode** to expand to HTML. 33 | Pressing `` will cycle through empty elements. Variables specified in 34 | `vim/README.txt` can be used to customise key mappings, and to add **normal mode** mappings 35 | as well. 36 | 37 | - **Others/command line use**: You may put `sparkup` in your `$PATH` somewhere. You may then 38 | invoke it by typing `echo "(input here)" | sparkup`, or `sparkup --help` for a list of commands. 39 | 40 | Credits 41 | ------- 42 | 43 | Sparkup is written by Rico Sta. Cruz and is released under the MIT license. 44 | 45 | This project is inspired by [Zen Coding](http://code.google.com/p/zen-coding/) of 46 | [Vadim Makeev](http://pepelsbey.net). The Zen HTML syntax is forward-compatible with Sparkup 47 | (anything that Zen HTML can parse, Sparkup can too). 48 | 49 | The following people have contributed code to the project: 50 | 51 | - Guillermo O. Freschi (Tordek @ GitHub) 52 | Bugfixes to the parsing system 53 | 54 | - Eric Van Dewoestine (ervandew @ GitHub) 55 | Improvements to the VIM plugin 56 | 57 | Examples 58 | -------- 59 | 60 | **`div`** expands to: 61 | 62 | ```html 63 |
64 | ``` 65 | 66 | **`div#header`** expands to: 67 | 68 | ```html 69 | 70 | ``` 71 | 72 | **`div.align-left#header`** expands to: 73 | 74 | ```html 75 | 76 | ``` 77 | 78 | **`div#header + div#footer`** expands to: 79 | 80 | ```html 81 | 82 | 83 | ``` 84 | 85 | **`#menu > ul`** expands to: 86 | 87 | ```html 88 | 91 | ``` 92 | 93 | **`#menu > h3 + ul`** expands to: 94 | 95 | ```html 96 | 100 | ``` 101 | 102 | **`#header > h1{Welcome to our site}`** expands to: 103 | 104 | ```html 105 | 108 | ``` 109 | 110 | **`a[href=index.html]{Home}`** expands to: 111 | 112 | ```html 113 | Home 114 | ``` 115 | 116 | **`ul > li*3`** expands to: 117 | 118 | ```html 119 | 124 | ``` 125 | 126 | **`ul > li.item-$*3`** expands to: 127 | 128 | ```html 129 | 134 | ``` 135 | 136 | **`ul > li.item-$*3 > strong`** expands to: 137 | 138 | ```html 139 | 144 | ``` 145 | 146 | **`table > tr*2 > td.name + td*3`** expands to: 147 | 148 | ```html 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 |
163 | ``` 164 | 165 | **`#header > ul > li < p{Footer}`** expands to: 166 | 167 | ```html 168 | 169 | 175 | ``` 176 | -------------------------------------------------------------------------------- /TextMate/Sparkup.tmbundle/Commands/Sparkup expand.tmCommand: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | beforeRunningCommand 6 | nop 7 | command 8 | #!/usr/bin/env python2 9 | import sys; import os; sys.path.append(os.getenv('TM_BUNDLE_SUPPORT')); import sparkup 10 | 11 | # You may change these options to your liking. 12 | # Those starting with # are comments (disabled). 13 | options = { 14 | 'textmate': True, 15 | 'no-last-newline': True, 16 | 'indent-tabs': False, 17 | #'start-guide-format': 'Begin %s', 18 | #'end-guide-format': 'End %s', 19 | } 20 | 21 | sparkup.Router().start(options=options) 22 | fallbackInput 23 | line 24 | input 25 | selection 26 | keyEquivalent 27 | ^e 28 | name 29 | Sparkup expand 30 | output 31 | insertAsSnippet 32 | uuid 33 | 73A48D2B-D843-42A1-A288-0D1A6380043B 34 | 35 | 36 | -------------------------------------------------------------------------------- /TextMate/Sparkup.tmbundle/Support/sparkup.py: -------------------------------------------------------------------------------- 1 | ../../../sparkup.py -------------------------------------------------------------------------------- /TextMate/Sparkup.tmbundle/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Sparkup 7 | ordering 8 | 9 | AA687F82-BF47-477C-A832-D3671736EA81 10 | 11 | uuid 12 | 0CDE6908-2EF0-4E86-A9C1-5AC12E320414 13 | 14 | 15 | -------------------------------------------------------------------------------- /ftdetect/hsb.vim: -------------------------------------------------------------------------------- 1 | au BufNewFile,BufRead *.hbs set filetype=html 2 | -------------------------------------------------------------------------------- /ftplugin: -------------------------------------------------------------------------------- 1 | vim/ftplugin -------------------------------------------------------------------------------- /mit-license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Rico Sta. Cruz. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /sparkup-unittest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | import sparkup 5 | 6 | 7 | class SparkupTest: 8 | options = { 9 | 'textmate': True, 10 | 'no-last-newline': True, 11 | 'post-tag-guides': True, 12 | } 13 | options = { 14 | 'default': {'textmate': True, 'no-last-newline': True, 'post-tag-guides': True}, 15 | 'guides': {'textmate': True, 'no-last-newline': True, 'post-tag-guides': True, 'start-guide-format': 'Begin %s'}, 16 | 'namespaced-elements': {'textmate': True, 'no-last-newline': True, 'post-tag-guides': True, 'namespaced-elements': True } 17 | } 18 | cases = { 19 | 'Simple test': { 20 | 'options': 'default', 21 | 'input': 'div', 22 | 'output': '
$1
$0' 23 | }, 24 | 'Class test': { 25 | 'input': 'div.lol', 26 | 'output': '
$1
$0' 27 | }, 28 | 'ID and class test': { 29 | 'input': 'div.class#id', 30 | 'output': '
$1
$0' 31 | }, 32 | 'ID and class test 2': { 33 | 'input': 'div#id.class', 34 | 'output': '
$1
$0' 35 | }, 36 | 'Attributes test': { 37 | 'input': 'div#id.class[style=color:blue]', 38 | 'output': '
$1
$0' 39 | }, 40 | 'Multiple attributes test': { 41 | 'input': 'div[align=center][style=color:blue][rel=none]', 42 | 'output': '
$1
$0' 43 | }, 44 | 'Multiple class test': { 45 | 'input': 'div.c1.c2.c3', 46 | 'output': '
$1
$0' 47 | }, 48 | 'Shortcut test': { 49 | 'input': 'input:button', 50 | 'output': '$0' 51 | }, 52 | 'Shortcut synonym test': { 53 | 'input': 'button', 54 | 'output': '$0', 55 | }, 56 | 'Child test': { 57 | 'input': 'div>ul>li', 58 | 'output': "
\n
    \n
  • $1
  • \n
\n
$0" 59 | }, 60 | 'Sibling test': { 61 | 'input': 'div#x + ul+ h3.class', 62 | 'output': '
$1
\n\n

$3

$0' 63 | }, 64 | 'Child + sibling test': { 65 | 'input': 'div > ul > li + span', 66 | 'output': '
\n
    \n
  • $1
  • \n $2\n
\n
$0' 67 | }, 68 | 'Multiplier test 1': { 69 | 'input': 'ul > li*3', 70 | 'output': '$0' 71 | }, 72 | 'Multiplier test 2': { 73 | 'input': 'ul > li.item-$*3', 74 | 'output': '$0' 75 | }, 76 | 'Multiplier test 3': { 77 | 'input': 'ul > li.item-$*3 > a', 78 | 'output': '$0' 79 | }, 80 | 'Ampersand test': { 81 | 'input': 'td > tr.row-$*3 > td.cell-&*2', 82 | 'output': '\n \n $1\n $2\n \n \n $3\n $4\n \n \n $5\n $6\n \n$0' 83 | }, 84 | 'Menu test': { 85 | 'input': 'ul#menu > li*3 > a > span', 86 | 'output': '$0' 87 | }, 88 | 'Back test': { 89 | 'input': 'ul#menu > li*3 > a < < div', 90 | 'output': '\n
$7
$0' 91 | }, 92 | 'Expand test': { 93 | 'input': 'p#menu > table+ + ul', 94 | 'output': '\n

$0' 95 | }, 96 | 'Text with dot test': { 97 | 'input': 'p { text.com }', 98 | 'output': '

text.com

$0' 99 | }, 100 | 'Attribute with dot test': { 101 | 'input': 'p [attrib=text.com]', 102 | 'output': '

$1

$0' 103 | }, 104 | 'PHP tag test': { 105 | 'input': 'php', 106 | 'output': '$0', 107 | }, 108 | 'Eruby tag test': { 109 | 'input': 'erb:p', 110 | 'output': '<%= %>$0', 111 | }, 112 | 'ERB block test': { 113 | 'input': 'erb:b', 114 | 'output': '<% $2 %>\n $1\n<% end %>$0' 115 | }, 116 | 'Tag name case (#49)': { 117 | 'input': 'groupId{foobar}', 118 | 'output': 'foobar$0' 119 | }, 120 | 'Nested curly braces test': { 121 | 'input': 'p{{{ title }}}', 122 | 'output': '

{{ title }}

$0' 123 | }, 124 | 'Nested curly braces test (#54)': { 125 | 'input': 'html>head>title{${title}}', 126 | 'output': '\n \n ${title}\n \n$0' 127 | }, 128 | 'HTML component element with dash test': { 129 | 'input': 'my-html-component', 130 | 'output': '$1$0' 131 | }, 132 | 'XML namespaced element': { 133 | 'options': 'namespaced-elements', 134 | 'input': 'namespaced-ul', 135 | 'output': '$1$0' 136 | }, 137 | # Add: text test, broken test, multi-attribute tests, indentation test, start and end comments test 138 | } 139 | 140 | def run(self): 141 | """Run Forrest run!""" 142 | failures = 0 143 | 144 | print("Test results:") 145 | for name, case in self.cases.iteritems(): 146 | try: 147 | options_key = case['options'] 148 | except: 149 | options_key = 'default' 150 | 151 | try: 152 | options = self.options[options_key] 153 | except: 154 | options = self.options['default'] 155 | 156 | # Output buffer 157 | r = sparkup.Router() 158 | input = case['input'] 159 | output = r.start(options=options, str=input, ret=True) 160 | del r 161 | 162 | # Did it work? 163 | result = output == case['output'] 164 | if result: 165 | result_str = " OK " 166 | else: 167 | result_str = "FAIL" 168 | 169 | print(" - %-30s [%s]" % (name, result_str)) 170 | if not result: 171 | failures += 1 172 | print("= %s" % input.replace("\n", "\n= ")) 173 | print("Actual output (condensed):") 174 | print(" | '%s'" % output.replace("\n", r"\n").replace('"', '\"')) 175 | print("Actual output:") 176 | print(" | %s" % output.replace("\n", "\n | ")) 177 | print("Expected:") 178 | print(" | %s" % case['output'].replace("\n", "\ n| ")) 179 | 180 | return failures 181 | 182 | if __name__ == '__main__': 183 | s = SparkupTest() 184 | sys.exit(s.run()) 185 | -------------------------------------------------------------------------------- /sparkup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | version = "0.1.4" 4 | 5 | import getopt 6 | import sys 7 | import re 8 | 9 | # ============================================================================= 10 | 11 | def iteritems(obj): 12 | """iteritems() in python2 and items() in python3""" 13 | if sys.version[0] == '2': 14 | return obj.iteritems() 15 | else: 16 | return obj.items() 17 | 18 | class Dialect: 19 | shortcuts = {} 20 | synonyms = {} 21 | required = {} 22 | short_tags = () 23 | 24 | class XmlDialect(Dialect): 25 | shortcuts = {} 26 | synonyms = {} 27 | short_tags = () 28 | required = {} 29 | 30 | class HtmlDialect(Dialect): 31 | # Constructor, accepts an indent which should come from an Options object 32 | # so it integrates well with their editing environment. 33 | def __init__(self, indent=4): 34 | self.indent = indent 35 | 36 | self.shortcuts = { 37 | 'cc:ie': { 38 | 'opening_tag': ''}, 40 | 'cc:ie8': { 41 | 'opening_tag': ''}, 43 | 'cc:ie9': { 44 | 'opening_tag': ''}, 46 | 'cc:noie': { 47 | 'opening_tag': '', 48 | 'closing_tag': ''}, 49 | 'php:t': { 50 | 'expand': True, 51 | 'opening_tag': '', 53 | }, 54 | 'erb:p': { 55 | 'opening_tag': '<%= ', 56 | 'closing_tag': ' %>', 57 | }, 58 | 'erb:c': { 59 | 'opening_tag': '%<# ', 60 | 'closing_tag': ' %>', 61 | }, 62 | 'erb:d': { 63 | 'opening_tag': '<% ', 64 | 'closing_tag': ' %>', 65 | }, 66 | 'erb:b': { 67 | 'expand': True, 68 | 'opening_tag' : '<% $2 %>', 69 | 'closing_tag' : '<% end %>', 70 | }, 71 | 'erb:bp': { 72 | 'expand': True, 73 | 'opening_tag' : '<%= $2 %>', 74 | 'closing_tag' : '<% end %>', 75 | }, 76 | 'html:4t': { 77 | 'expand': True, 78 | 'opening_tag': 79 | '\n' + 80 | '\n' + 81 | '\n' + 82 | (' ' * self.indent) + '\n' + 83 | (' ' * self.indent) + '\n' + 84 | '\n' + 85 | '', 86 | 'closing_tag': 87 | '\n' + 88 | ''}, 89 | 'html:4s': { 90 | 'expand': True, 91 | 'opening_tag': 92 | '\n' + 93 | '\n' + 94 | '\n' + 95 | (' ' * self.indent) + '\n' + 96 | (' ' * self.indent) + '\n' + 97 | '\n' + 98 | '', 99 | 'closing_tag': 100 | '\n' + 101 | ''}, 102 | 'html:xt': { 103 | 'expand': True, 104 | 'opening_tag': 105 | '\n' + 106 | '\n' + 107 | '\n' + 108 | (' ' * self.indent) + '\n' + 109 | (' ' * self.indent) + '\n' + 110 | '\n' + 111 | '', 112 | 'closing_tag': 113 | '\n' + 114 | ''}, 115 | 'html:xs': { 116 | 'expand': True, 117 | 'opening_tag': 118 | '\n' + 119 | '\n' + 120 | '\n' + 121 | (' ' * self.indent) + '\n' + 122 | (' ' * self.indent) + '\n' + 123 | '\n' + 124 | '', 125 | 'closing_tag': 126 | '\n' + 127 | ''}, 128 | 'html:xxs': { 129 | 'expand': True, 130 | 'opening_tag': 131 | '\n' + 132 | '\n' + 133 | '\n' + 134 | (' ' * self.indent) + '\n' + 135 | (' ' * self.indent) + '\n' + 136 | '\n' + 137 | '', 138 | 'closing_tag': 139 | '\n' + 140 | ''}, 141 | 'html:5': { 142 | 'expand': True, 143 | 'opening_tag': 144 | '\n' + 145 | '\n' + 146 | '\n' + 147 | (' ' * self.indent) + '\n' + 148 | (' ' * self.indent) + '\n' + 149 | '\n' + 150 | '', 151 | 'closing_tag': 152 | '\n' + 153 | ''}, 154 | 'input:button': { 155 | 'name': 'input', 156 | 'attributes': { 'class': 'button', 'type': 'button', 'name': '', 'value': '' } 157 | }, 158 | 'input:password': { 159 | 'name': 'input', 160 | 'attributes': { 'class': 'text password', 'type': 'password', 'name': '', 'value': '' } 161 | }, 162 | 'input:radio': { 163 | 'name': 'input', 164 | 'attributes': { 'class': 'radio', 'type': 'radio', 'name': '', 'value': '' } 165 | }, 166 | 'input:checkbox': { 167 | 'name': 'input', 168 | 'attributes': { 'class': 'checkbox', 'type': 'checkbox', 'name': '', 'value': '' } 169 | }, 170 | 'input:file': { 171 | 'name': 'input', 172 | 'attributes': { 'class': 'file', 'type': 'file', 'name': '', 'value': '' } 173 | }, 174 | 'input:text': { 175 | 'name': 'input', 176 | 'attributes': { 'class': 'text', 'type': 'text', 'name': '', 'value': '' } 177 | }, 178 | 'input:submit': { 179 | 'name': 'input', 180 | 'attributes': { 'class': 'submit', 'type': 'submit', 'value': '' } 181 | }, 182 | 'input:hidden': { 183 | 'name': 'input', 184 | 'attributes': { 'type': 'hidden', 'name': '', 'value': '' } 185 | }, 186 | 'script:src': { 187 | 'name': 'script', 188 | 'attributes': { 'src': '' } 189 | }, 190 | 'script:jquery': { 191 | 'name': 'script', 192 | 'attributes': { 'src': 'http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js' } 193 | }, 194 | 'script:jquery2': { 195 | 'name': 'script', 196 | 'attributes': { 'src': 'http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js' } 197 | }, 198 | 'script:jsapi': { 199 | 'name': 'script', 200 | 'attributes': { 'src': 'http://www.google.com/jsapi' } 201 | }, 202 | 'script:jsapix': { 203 | 'name': 'script', 204 | 'text': '\n google.load("jquery", "1.3.2");\n google.setOnLoadCallback(function() {\n \n });\n' 205 | }, 206 | 'link:css': { 207 | 'name': 'link', 208 | 'attributes': { 'rel': 'stylesheet', 'type': 'text/css', 'href': '', 'media': 'all' }, 209 | }, 210 | 'link:print': { 211 | 'name': 'link', 212 | 'attributes': { 'rel': 'stylesheet', 'type': 'text/css', 'href': '', 'media': 'print' }, 213 | }, 214 | 'link:favicon': { 215 | 'name': 'link', 216 | 'attributes': { 'rel': 'shortcut icon', 'type': 'image/x-icon', 'href': '' }, 217 | }, 218 | 'link:touch': { 219 | 'name': 'link', 220 | 'attributes': { 'rel': 'apple-touch-icon', 'href': '' }, 221 | }, 222 | 'link:rss': { 223 | 'name': 'link', 224 | 'attributes': { 'rel': 'alternate', 'type': 'application/rss+xml', 'title': 'RSS', 'href': '' }, 225 | }, 226 | 'link:atom': { 227 | 'name': 'link', 228 | 'attributes': { 'rel': 'alternate', 'type': 'application/atom+xml', 'title': 'Atom', 'href': '' }, 229 | }, 230 | 'meta:ieedge': { 231 | 'name': 'meta', 232 | 'attributes': { 'http-equiv': 'X-UA-Compatible', 'content': 'IE=edge' }, 233 | }, 234 | 'form:get': { 235 | 'name': 'form', 236 | 'attributes': { 'method': 'get' }, 237 | }, 238 | 'form:g': { 239 | 'name': 'form', 240 | 'attributes': { 'method': 'get' }, 241 | }, 242 | 'form:post': { 243 | 'name': 'form', 244 | 'attributes': { 'method': 'post' }, 245 | }, 246 | 'form:p': { 247 | 'name': 'form', 248 | 'attributes': { 'method': 'post' }, 249 | }, 250 | } 251 | self.synonyms = { 252 | 'php': 'php:t', 253 | 'checkbox': 'input:checkbox', 254 | 'check': 'input:checkbox', 255 | 'input:c': 'input:checkbox', 256 | 'input:b': 'input:button', 257 | 'input:h': 'input:hidden', 258 | 'hidden': 'input:hidden', 259 | 'submit': 'input:submit', 260 | 'input:s': 'input:submit', 261 | 'radio': 'input:radio', 262 | 'input:r': 'input:radio', 263 | 'text': 'input:text', 264 | 'pass': 'input:password', 265 | 'passwd': 'input:password', 266 | 'password': 'input:password', 267 | 'pw': 'input:password', 268 | 'input': 'input:text', 269 | 'input:t': 'input:text', 270 | 'linkcss': 'link:css', 271 | 'scriptsrc': 'script:src', 272 | 'jquery': 'script:jquery', 273 | 'jsapi': 'script:jsapi', 274 | 'html5': 'html:5', 275 | 'html4': 'html:4s', 276 | 'html4s': 'html:4s', 277 | 'html4t': 'html:4t', 278 | 'xhtml': 'html:xxs', 279 | 'xhtmlt': 'html:xt', 280 | 'xhtmls': 'html:xs', 281 | 'xhtml11': 'html:xxs', 282 | 'opt': 'option', 283 | 'st': 'strong', 284 | 'css': 'style', 285 | 'csss': 'link:css', 286 | 'css:src': 'link:css', 287 | 'csssrc': 'link:css', 288 | 'js': 'script', 289 | 'jss': 'script:src', 290 | 'js:src': 'script:src', 291 | 'jssrc': 'script:src', 292 | } 293 | self.short_tags = ( 294 | 'area', 'base', 'basefont', 'br', 'embed', 'hr', 295 | 'input', 'img', 'link', 'param', 'meta') 296 | self.required = { 297 | 'a': {'href':''}, 298 | 'base': {'href':''}, 299 | 'abbr': {'title': ''}, 300 | 'acronym':{'title': ''}, 301 | 'bdo': {'dir': ''}, 302 | 'link': {'rel': 'stylesheet', 'href': ''}, 303 | 'style': {'type': 'text/css'}, 304 | 'script': {'type': 'text/javascript'}, 305 | 'img': {'src':'', 'alt':''}, 306 | 'iframe': {'src': '', 'frameborder': '0'}, 307 | 'embed': {'src': '', 'type': ''}, 308 | 'object': {'data': '', 'type': ''}, 309 | 'param': {'name': '', 'value': ''}, 310 | 'form': {'action': '', 'method': 'post'}, 311 | 'input': {'type': '', 'name': '', 'value': ''}, 312 | 'area': {'shape': '', 'coords': '', 'href': '', 'alt': ''}, 313 | 'select': {'name': ''}, 314 | 'option': {'value': ''}, 315 | 'textarea':{'name': ''}, 316 | 'meta': {'content': ''}, 317 | } 318 | short_tags = ( 319 | 'area', 'base', 'basefont', 'br', 'embed', 'hr', 320 | 'input', 'img', 'link', 'param', 'meta') 321 | required = { 322 | 'a': {'href':''}, 323 | 'base': {'href':''}, 324 | 'abbr': {'title': ''}, 325 | 'acronym':{'title': ''}, 326 | 'bdo': {'dir': ''}, 327 | 'link': {'rel': 'stylesheet', 'href': ''}, 328 | 'style': {'type': 'text/css'}, 329 | 'script': {'type': 'text/javascript'}, 330 | 'img': {'src':'', 'alt':''}, 331 | 'iframe': {'src': '', 'frameborder': '0'}, 332 | 'embed': {'src': '', 'type': ''}, 333 | 'object': {'data': '', 'type': ''}, 334 | 'param': {'name': '', 'value': ''}, 335 | 'form': {'action': '', 'method': 'post'}, 336 | 'input': {'type': '', 'name': '', 'value': ''}, 337 | 'area': {'shape': '', 'coords': '', 'href': '', 'alt': ''}, 338 | 'select': {'name': ''}, 339 | 'option': {'value': ''}, 340 | 'textarea':{'name': ''}, 341 | 'meta': {'content': ''}, 342 | } 343 | 344 | class Parser: 345 | """The parser. 346 | """ 347 | 348 | # Constructor 349 | # ------------------------------------------------------------------------- 350 | 351 | def __init__(self, options=None, str=''): 352 | """Constructor. 353 | """ 354 | 355 | self.tokens = [] 356 | self.str = str 357 | self.options = options 358 | if self.options.has("xml"): 359 | self.dialect = XmlDialect() 360 | else: 361 | self.dialect = HtmlDialect(int(self.options.options['indent-spaces'])) 362 | self.root = Element(parser=self) 363 | self.caret = [] 364 | self.caret.append(self.root) 365 | self._last = [] 366 | 367 | # Methods 368 | # ------------------------------------------------------------------------- 369 | 370 | def load_string(self, str): 371 | """Loads a string to parse. 372 | """ 373 | 374 | self.str = str 375 | self._tokenize() 376 | self._parse() 377 | 378 | def render(self): 379 | """Renders. 380 | Called by [[Router]]. 381 | """ 382 | 383 | # Get the initial render of the root node 384 | output = self.root.render() 385 | 386 | # Indent by whatever the input is indented with 387 | indent = re.findall("^[\r\n]*(\s*)", self.str)[0] 388 | output = indent + output.replace("\n", "\n" + indent) 389 | 390 | # Strip newline if not needed 391 | if self.options.has("no-last-newline") \ 392 | or self.prefix or self.suffix: 393 | output = re.sub(r'\n\s*$', '', output) 394 | 395 | # TextMate mode 396 | if self.options.has("textmate"): 397 | output = self._textmatify(output) 398 | 399 | return output 400 | 401 | # Protected methods 402 | # ------------------------------------------------------------------------- 403 | 404 | def _textmatify(self, output): 405 | """Returns a version of the output with TextMate placeholders in it. 406 | """ 407 | 408 | matches = re.findall(r'(>$%i]+>\s*)", str) 436 | if match is None: break 437 | if self.prefix is None: self.prefix = '' 438 | self.prefix += match.group(0) 439 | str = str[len(match.group(0)):] 440 | 441 | while True: 442 | match = re.findall(r"(\s*<[^>]+>[\s\n\r]*)$", str) 443 | if not match: break 444 | if self.suffix is None: self.suffix = '' 445 | self.suffix = match[0] + self.suffix 446 | str = str[:-len(match[0])] 447 | 448 | # Split by the element separators 449 | for token in re.split('(<|>|\+(?!\\s*\+|$))', str): 450 | if token.strip() != '': 451 | self.tokens.append(Token(token, parser=self)) 452 | 453 | def _parse(self): 454 | """Takes the tokens and does its thing. 455 | Populates [[self.root]]. 456 | """ 457 | 458 | # Carry it over to the root node. 459 | if self.prefix or self.suffix: 460 | self.root.prefix = self.prefix 461 | self.root.suffix = self.suffix 462 | self.root.depth += 1 463 | 464 | for token in self.tokens: 465 | if token.type == Token.ELEMENT: 466 | # Reset the "last elements added" list. We will 467 | # repopulate this with the new elements added now. 468 | self._last[:] = [] 469 | 470 | # Create [[Element]]s from a [[Token]]. 471 | # They will be created as many as the multiplier specifies, 472 | # multiplied by how many carets we have 473 | count = 0 474 | for caret in self.caret: 475 | local_count = 0 476 | for i in range(token.multiplier): 477 | count += 1 478 | local_count += 1 479 | new = Element(token, caret, 480 | count = count, 481 | local_count = local_count, 482 | parser = self) 483 | self._last.append(new) 484 | caret.append(new) 485 | 486 | # For > 487 | elif token.type == Token.CHILD: 488 | # The last children added. 489 | self.caret[:] = self._last 490 | 491 | # For < 492 | elif token.type == Token.PARENT: 493 | # If we're the root node, don't do anything 494 | parent = self.caret[0].parent 495 | if parent is not None: 496 | self.caret[:] = [parent] 497 | return 498 | 499 | # Properties 500 | # ------------------------------------------------------------------------- 501 | 502 | # Property: dialect 503 | # The dialect of XML 504 | dialect = None 505 | 506 | # Property: str 507 | # The string 508 | str = '' 509 | 510 | # Property: tokens 511 | # The list of tokens 512 | tokens = [] 513 | 514 | # Property: options 515 | # Reference to the [[Options]] instance 516 | options = None 517 | 518 | # Property: root 519 | # The root [[Element]] node. 520 | root = None 521 | 522 | # Property: caret 523 | # The current insertion point. 524 | caret = None 525 | 526 | # Property: _last 527 | # List of the last appended stuff 528 | _last = None 529 | 530 | # Property: indent 531 | # Yeah 532 | indent = '' 533 | 534 | # Property: prefix 535 | # (String) The trailing tag in the beginning. 536 | # 537 | # Description: 538 | # For instance, in `
ul>li
`, the `prefix` is `
`. 539 | prefix = '' 540 | 541 | # Property: suffix 542 | # (string) The trailing tag at the end. 543 | suffix = '' 544 | pass 545 | 546 | # ============================================================================= 547 | 548 | class Element: 549 | """An element. 550 | """ 551 | 552 | def __init__(self, token=None, parent=None, count=None, local_count=None, 553 | parser=None, opening_tag=None, closing_tag=None, 554 | attributes=None, name=None, text=None): 555 | """Constructor. 556 | 557 | This is called by ???. 558 | 559 | Description: 560 | All parameters are optional. 561 | 562 | token - (Token) The token (required) 563 | parent - (Element) Parent element; `None` if root 564 | count - (Int) The number to substitute for `&` (e.g., in `li.item-$`) 565 | local_count - (Int) The number to substitute for `$` (e.g., in `li.item-&`) 566 | parser - (Parser) The parser 567 | 568 | attributes - ... 569 | name - ... 570 | text - ... 571 | """ 572 | 573 | self.children = [] 574 | self.attributes = {} 575 | self.parser = parser 576 | 577 | if token is not None: 578 | # Assumption is that token is of type [[Token]] and is 579 | # a [[Token.ELEMENT]]. 580 | self.name = token.name 581 | self.attributes = token.attributes.copy() 582 | self.text = token.text 583 | self.populate = token.populate 584 | self.expand = token.expand 585 | self.opening_tag = token.opening_tag 586 | self.closing_tag = token.closing_tag 587 | 588 | # `count` can be given. This will substitude & in classname and ID 589 | if count is not None: 590 | for key in self.attributes: 591 | attrib = self.attributes[key] 592 | attrib = attrib.replace('&', ("%i" % count)) 593 | if local_count is not None: 594 | attrib = attrib.replace('$', ("%i" % local_count)) 595 | self.attributes[key] = attrib 596 | 597 | # Copy over from parameters 598 | if attributes: self.attributes = attributes 599 | if name: self.name = name 600 | if text: self.text = text 601 | 602 | self._fill_attributes() 603 | 604 | self.parent = parent 605 | if parent is not None: 606 | self.depth = parent.depth + 1 607 | 608 | if self.populate: self._populate() 609 | 610 | def render(self): 611 | """Renders the element, along with it's subelements, into HTML code. 612 | 613 | [Grouped under "Rendering methods"] 614 | """ 615 | 616 | output = "" 617 | 618 | try: tabs = bool(self.parser.options.options['indent-tabs']) 619 | except: tabs = False 620 | 621 | if tabs: 622 | spaces = '\t' 623 | else: 624 | try: spaces_count = int(self.parser.options.options['indent-spaces']) 625 | except: spaces_count = 4 626 | spaces = ' ' * spaces_count 627 | 628 | indent = self.depth * spaces 629 | 630 | prefix, suffix = ('', '') 631 | if self.prefix: prefix = self.prefix + "\n" 632 | if self.suffix: suffix = self.suffix 633 | 634 | # Make the guide from the ID (/#header), or the class if there's no ID (/.item) 635 | # This is for the start-guide, end-guide and post-tag-guides 636 | guide_str = '' 637 | if 'id' in self.attributes: 638 | guide_str += "#%s" % self.attributes['id'] 639 | elif 'class' in self.attributes: 640 | guide_str += ".%s" % self.attributes['class'].replace(' ', '.') 641 | 642 | # Build the post-tag guide (e.g.,
), 643 | # the start guide, and the end guide. 644 | guide = '' 645 | start_guide = '' 646 | end_guide = '' 647 | if ((self.name == 'div') and 648 | (('id' in self.attributes) or ('class' in self.attributes))): 649 | 650 | if (self.parser.options.has('post-tag-guides')): 651 | guide = "" % guide_str 652 | 653 | if (self.parser.options.has('start-guide-format')): 654 | format = self.parser.options.get('start-guide-format') 655 | try: start_guide = format % guide_str 656 | except: start_guide = (format + " " + guide_str).strip() 657 | start_guide = "%s\n" % (indent, start_guide) 658 | 659 | if (self.parser.options.has('end-guide-format')): 660 | format = self.parser.options.get('end-guide-format') 661 | try: end_guide = format % guide_str 662 | except: end_guide = (format + " " + guide_str).strip() 663 | end_guide = "\n%s" % (indent, end_guide) 664 | 665 | # Short, self-closing tags (
) 666 | short_tags = self.parser.dialect.short_tags 667 | 668 | # When it should be expanded.. 669 | # (That is,
\n...\n
or similar -- wherein something must go 670 | # inside the opening/closing tags) 671 | if len(self.children) > 0 \ 672 | or self.expand \ 673 | or prefix or suffix \ 674 | or (self.parser.options.has('expand-divs') and self.name == 'div'): 675 | 676 | for child in self.children: 677 | output += child.render() 678 | 679 | # For expand divs: if there are no children (that is, `output` 680 | # is still blank despite above), fill it with a blank line. 681 | if (output == ''): output = indent + spaces + "\n" 682 | 683 | # If we're a root node and we have a prefix or suffix... 684 | # (Only the root node can have a prefix or suffix.) 685 | if prefix or suffix: 686 | output = "%s%s%s%s%s\n" % \ 687 | (indent, prefix, output, suffix, guide) 688 | 689 | # Uh.. 690 | elif self.name != '' or \ 691 | self.opening_tag is not None or \ 692 | self.closing_tag is not None: 693 | output = start_guide + \ 694 | indent + self.get_opening_tag() + "\n" + \ 695 | output + \ 696 | indent + self.get_closing_tag() + \ 697 | guide + end_guide + "\n" 698 | 699 | 700 | # Short, self-closing tags (
or
depending on configuration) 701 | elif self.name in short_tags: 702 | if self.parser.options.has('no-html5-self-closing'): 703 | output = "%s<%s />\n" % (indent, self.get_default_tag()) 704 | else: 705 | output = "%s<%s>\n" % (indent, self.get_default_tag()) 706 | 707 | # Tags with text, possibly 708 | elif self.name != '' or \ 709 | self.opening_tag is not None or \ 710 | self.closing_tag is not None: 711 | between_tags = self.text 712 | if self.parser.options.has('open-empty-tags'): 713 | between_tags += "\n\n" + indent 714 | output = "%s%s%s%s%s%s%s%s" % \ 715 | (start_guide, indent, self.get_opening_tag(), 716 | between_tags, 717 | self.get_closing_tag(), 718 | guide, end_guide, "\n") 719 | 720 | # Else, it's an empty-named element (like the root). Pass. 721 | else: 722 | pass 723 | 724 | return output 725 | 726 | def get_default_tag(self): 727 | """Returns the opening tag (without brackets). 728 | 729 | Usage: 730 | element.get_default_tag() 731 | 732 | [Grouped under "Rendering methods"] 733 | """ 734 | 735 | output = '%s' % (self.name) 736 | for key, value in iteritems(self.attributes): 737 | output += ' %s="%s"' % (key, value) 738 | return output 739 | 740 | def get_opening_tag(self): 741 | if self.opening_tag is None: 742 | return "<%s>" % self.get_default_tag() 743 | else: 744 | return self.opening_tag 745 | 746 | def get_closing_tag(self): 747 | if self.closing_tag is None: 748 | return "" % self.name 749 | else: 750 | return self.closing_tag 751 | 752 | def append(self, object): 753 | """Registers an element as a child of this element. 754 | 755 | Usage: 756 | element.append(child) 757 | 758 | Description: 759 | Adds a given element `child` to the children list of this element. It 760 | will be rendered when [[render()]] is called on the element. 761 | 762 | See also: 763 | - [[get_last_child()]] 764 | 765 | [Grouped under "Traversion methods"] 766 | """ 767 | 768 | self.children.append(object) 769 | 770 | def get_last_child(self): 771 | """Returns the last child element which was [[append()]]ed to this element. 772 | 773 | Usage: 774 | element.get_last_child() 775 | 776 | Description: 777 | This is the same as using `element.children[-1]`. 778 | 779 | [Grouped under "Traversion methods"] 780 | """ 781 | 782 | return self.children[-1] 783 | 784 | def _populate(self): 785 | """Expands with default items. 786 | 787 | This is called when the [[populate]] flag is turned on. 788 | """ 789 | 790 | if self.name == 'ul': 791 | elements = [Element(name='li', parent=self, parser=self.parser)] 792 | 793 | elif self.name == 'dl': 794 | elements = [ 795 | Element(name='dt', parent=self, parser=self.parser), 796 | Element(name='dd', parent=self, parser=self.parser)] 797 | 798 | elif self.name == 'table': 799 | tr = Element(name='tr', parent=self, parser=self.parser) 800 | td = Element(name='td', parent=tr, parser=self.parser) 801 | tr.children.append(td) 802 | elements = [tr] 803 | 804 | else: 805 | elements = [] 806 | 807 | for el in elements: 808 | self.children.append(el) 809 | 810 | def _fill_attributes(self): 811 | """Fills default attributes for certain elements. 812 | 813 | Description: 814 | This is called by the constructor. 815 | 816 | [Protected, grouped under "Protected methods"] 817 | """ 818 | 819 | # Make sure 's have a href, 's have an src, etc. 820 | required = self.parser.dialect.required 821 | 822 | for element, attribs in iteritems(required): 823 | if self.name == element: 824 | for attrib in attribs: 825 | if attrib not in self.attributes: 826 | self.attributes[attrib] = attribs[attrib] 827 | 828 | # ------------------------------------------------------------------------- 829 | 830 | # Property: last_child 831 | # [Read-only] 832 | last_child = property(get_last_child) 833 | 834 | # ------------------------------------------------------------------------- 835 | 836 | # Property: parent 837 | # (Element) The parent element. 838 | parent = None 839 | 840 | # Property: name 841 | # (String) The name of the element (e.g., `div`) 842 | name = '' 843 | 844 | # Property: attributes 845 | # (Dict) The dictionary of attributes (e.g., `{'src': 'image.jpg'}`) 846 | attributes = None 847 | 848 | # Property: children 849 | # (List of Elements) The children 850 | children = None 851 | 852 | # Property: opening_tag 853 | # (String or None) The opening tag. Optional; will use `name` and 854 | # `attributes` if this is not given. 855 | opening_tag = None 856 | 857 | # Property: closing_tag 858 | # (String or None) The closing tag 859 | closing_tag = None 860 | 861 | text = '' 862 | depth = -1 863 | expand = False 864 | populate = False 865 | parser = None 866 | 867 | # Property: prefix 868 | # Only the root note can have this. 869 | prefix = None 870 | suffix = None 871 | 872 | # ============================================================================= 873 | 874 | class Token: 875 | def __init__(self, str, parser=None): 876 | """Token. 877 | 878 | Description: 879 | str - The string to parse 880 | 881 | In the string `div > ul`, there are 3 tokens. (`div`, `>`, and `ul`) 882 | 883 | For `>`, it will be a `Token` with `type` set to `Token.CHILD` 884 | """ 885 | 886 | self.str = str.strip() 887 | self.attributes = {} 888 | self.parser = parser 889 | 890 | # Set the type. 891 | if self.str == '<': 892 | self.type = Token.PARENT 893 | elif self.str == '>': 894 | self.type = Token.CHILD 895 | elif self.str == '+': 896 | self.type = Token.SIBLING 897 | else: 898 | self.type = Token.ELEMENT 899 | self._init_element() 900 | 901 | def _init_element(self): 902 | """Initializes. Only called if the token is an element token. 903 | [Private] 904 | """ 905 | 906 | # Get the tag name. Default to DIV if none given. 907 | name = re.findall('^([\w\-:]*)', self.str)[0] 908 | if self.parser.options.options['namespaced-elements'] == True: 909 | name = name.replace('-', ':') 910 | 911 | # Find synonyms through this thesaurus 912 | synonyms = self.parser.dialect.synonyms 913 | if name in synonyms.keys(): 914 | name = synonyms[name] 915 | 916 | if ':' in name: 917 | shortcuts = self.parser.dialect.shortcuts 918 | if name in shortcuts.keys(): 919 | for key, value in iteritems(shortcuts[name]): 920 | setattr(self, key, value) 921 | if 'html' in name: 922 | return 923 | else: 924 | self.name = name 925 | 926 | elif (name == ''): self.name = 'div' 927 | else: self.name = name 928 | 929 | # Look for attributes 930 | attribs = [] 931 | for attrib in re.findall('\[([^\]]*)\]', self.str): 932 | attribs.append(attrib) 933 | self.str = self.str.replace("[" + attrib + "]", "") 934 | if len(attribs) > 0: 935 | for attrib in attribs: 936 | try: key, value = attrib.split('=', 1) 937 | except: key, value = attrib, '' 938 | self.attributes[key] = value 939 | 940 | # Try looking for text 941 | text = None 942 | for text in re.findall('\{(.*?)\}(?!\})', self.str): 943 | self.str = self.str.replace("{" + text + "}", "") 944 | if text is not None: 945 | self.text = text 946 | 947 | # Get the class names 948 | classes = [] 949 | for classname in re.findall('\.([\$a-zA-Z0-9_\-\&]+)', self.str): 950 | classes.append(classname) 951 | if len(classes) > 0: 952 | try: self.attributes['class'] 953 | except: self.attributes['class'] = '' 954 | self.attributes['class'] += ' ' + ' '.join(classes) 955 | self.attributes['class'] = self.attributes['class'].strip() 956 | 957 | # Get the ID 958 | id = None 959 | for id in re.findall('#([\$a-zA-Z0-9_\-\&]+)', self.str): pass 960 | if id is not None: 961 | self.attributes['id'] = id 962 | 963 | # See if there's a multiplier (e.g., "li*3") 964 | multiplier = None 965 | for multiplier in re.findall('\*\s*([0-9]+)', self.str): pass 966 | if multiplier is not None: 967 | self.multiplier = int(multiplier) 968 | 969 | # Populate flag (e.g., ul+) 970 | flags = None 971 | for flags in re.findall('[\+\!]+$', self.str): pass 972 | if flags is not None: 973 | if '+' in flags: self.populate = True 974 | if '!' in flags: self.expand = True 975 | 976 | def __str__(self): 977 | return self.str 978 | 979 | str = '' 980 | parser = None 981 | 982 | # For elements 983 | # See the properties of `Element` for description on these. 984 | name = '' 985 | attributes = None 986 | multiplier = 1 987 | expand = False 988 | populate = False 989 | text = '' 990 | opening_tag = None 991 | closing_tag = None 992 | 993 | # Type 994 | type = 0 995 | ELEMENT = 2 996 | CHILD = 4 997 | PARENT = 8 998 | SIBLING = 16 999 | 1000 | # ============================================================================= 1001 | 1002 | class Router: 1003 | """The router. 1004 | """ 1005 | 1006 | # Constructor 1007 | # ------------------------------------------------------------------------- 1008 | 1009 | def __init__(self): 1010 | pass 1011 | 1012 | # Methods 1013 | # ------------------------------------------------------------------------- 1014 | 1015 | def start(self, options=None, str=None, ret=None): 1016 | if (options): 1017 | self.options = Options(router=self, options=options, argv=None) 1018 | else: 1019 | self.options = Options(router=self, argv=sys.argv[1:], options=None) 1020 | 1021 | if (self.options.has('help')): 1022 | return self.help() 1023 | 1024 | elif (self.options.has('version')): 1025 | return self.version() 1026 | 1027 | else: 1028 | return self.parse(str=str, ret=ret) 1029 | 1030 | def help(self): 1031 | print("Usage: %s [OPTIONS]" % sys.argv[0]) 1032 | print("Expands input into HTML.") 1033 | print("") 1034 | for short, long, info in self.options.cmdline_keys: 1035 | if "Deprecated" in info: continue 1036 | if not short == '': short = '-%s,' % short 1037 | if not long == '': long = '--%s' % long.replace("=", "=XXX") 1038 | 1039 | print("%6s %-25s %s" % (short, long, info)) 1040 | print("") 1041 | print("\n".join(self.help_content)) 1042 | 1043 | def version(self): 1044 | print("Uhm, yeah.") 1045 | 1046 | def parse(self, str=None, ret=None): 1047 | self.parser = Parser(self.options) 1048 | 1049 | try: 1050 | # Read the files 1051 | if str is not None: 1052 | lines = str 1053 | else: 1054 | lines = sys.stdin.read() 1055 | 1056 | except KeyboardInterrupt: 1057 | pass 1058 | 1059 | except: 1060 | sys.stderr.write("Reading failed.\n") 1061 | return 1062 | 1063 | try: 1064 | self.parser.load_string(lines) 1065 | output = self.parser.render() 1066 | if ret: return output 1067 | sys.stdout.write(output) 1068 | 1069 | except: 1070 | sys.stderr.write("Parse error. Check your input.\n") 1071 | print(sys.exc_info()[0]) 1072 | print(sys.exc_info()[1]) 1073 | 1074 | def exit(self): 1075 | sys.exit() 1076 | 1077 | help_content = [ 1078 | "Please refer to the manual for more information.", 1079 | ] 1080 | 1081 | # ============================================================================= 1082 | 1083 | class Options: 1084 | def __init__(self, router, argv, options=None): 1085 | # Init self 1086 | self.router = router 1087 | 1088 | # `options` can be given as a dict of stuff to preload 1089 | if options: 1090 | for k, v in iteritems(options): 1091 | self.options[k] = v 1092 | return 1093 | 1094 | # Prepare for getopt() 1095 | short_keys, long_keys = "", [] 1096 | for short, long, info in self.cmdline_keys: # 'v', 'version' 1097 | short_keys += short 1098 | long_keys.append(long) 1099 | 1100 | try: 1101 | getoptions, arguments = getopt.getopt(argv, short_keys, long_keys) 1102 | 1103 | except getopt.GetoptError: 1104 | err = sys.exc_info()[1] 1105 | sys.stderr.write("Options error: %s\n" % err) 1106 | sys.stderr.write("Try --help for a list of arguments.\n") 1107 | return router.exit() 1108 | 1109 | # Sort them out into options 1110 | options = {} 1111 | for option in getoptions: 1112 | key, value = option # '--version', '' 1113 | if (value == ''): value = True 1114 | 1115 | # If the key is long, write it 1116 | if key[0:2] == '--': 1117 | clean_key = key[2:] 1118 | options[clean_key] = value 1119 | 1120 | # If the key is short, look for the long version of it 1121 | elif key[0:1] == '-': 1122 | for short, long, info in self.cmdline_keys: 1123 | if short == key[1:]: 1124 | print(long) 1125 | options[long] = True 1126 | 1127 | # Done 1128 | for k, v in iteritems(options): 1129 | self.options[k] = v 1130 | 1131 | def __getattr__(self, attr): 1132 | return self.get(attr) 1133 | 1134 | def get(self, attr): 1135 | try: return self.options[attr] 1136 | except: return None 1137 | 1138 | def has(self, attr): 1139 | try: return self.options.has_key(attr) 1140 | except: return False 1141 | 1142 | options = { 1143 | 'indent-spaces': 4, 1144 | 'namespaced-elements': False, 1145 | } 1146 | cmdline_keys = [ 1147 | ('h', 'help', 'Shows help'), 1148 | ('v', 'version', 'Shows the version'), 1149 | ('', 'no-guides', 'Deprecated'), 1150 | ('', 'post-tag-guides', 'Adds comments at the end of DIV tags'), 1151 | ('', 'textmate', 'Adds snippet info (textmate mode)'), 1152 | ('', 'indent-spaces=', 'Indent spaces'), 1153 | ('', 'indent-tabs', 'Indent with tabs'), 1154 | ('', 'expand-divs', 'Automatically expand divs'), 1155 | ('', 'no-last-newline', 'Skip the trailing newline'), 1156 | ('', 'start-guide-format=', 'To be documented'), # << TODO 1157 | ('', 'end-guide-format=', 'To be documented'), # << TODO 1158 | ('', 'xml', 'Skip html attribute fillings'), 1159 | ('', 'no-html5-self-closing', 'Use HTML4
instead of HTML5
'), 1160 | ('', 'open-empty-tags', 'leaves an empty space between empty tags instead of leaving them on one line'), 1161 | ] 1162 | 1163 | # Property: router 1164 | # Router 1165 | router = 1 1166 | 1167 | # ============================================================================= 1168 | 1169 | if __name__ == "__main__": 1170 | z = Router() 1171 | z.start() 1172 | -------------------------------------------------------------------------------- /vim/README.txt: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | With Pathogen 5 | ^^^^^^^^^^^^^ 6 | 7 | If you are using tpope's vim-pathogen, install as follows: 8 | 9 | cd ~/.vim/bundle ; git clone https://github.com/rstacruz/sparkup.git 10 | cd sparkup 11 | make vim-pathogen 12 | 13 | 14 | With Vundle 15 | ^^^^^^^^^^^ 16 | 17 | If using Vundle, you can specify Sparkup as a bundle and installation will happen 18 | automatically. Add this to your Vim configuration: 19 | 20 | Plugin 'rstacruz/sparkup', {'rtp': 'vim/'} 21 | 22 | and run the standard installation command for Vundle: 23 | 24 | :PluginInstall 25 | 26 | 27 | Manual installation 28 | ^^^^^^^^^^^^^^^^^^^ 29 | 30 | 1. Copy the contents of vim/ftplugin/ to your ~/.vim/ftplugin directory. 31 | 32 | (Assuming your current dir is sparkup/vim/) 33 | $ cp -R ftplugin ~/.vim/ 34 | 35 | 2. Copy the sparkup.py file to your ~/.vim directory 36 | 37 | (Assuming your current dir is sparkup/vim/) 38 | $ cp ../sparkup.py ~/.vim/ 39 | 40 | 41 | Configuration 42 | ------------- 43 | 44 | Customise the Sparkup's configuration within Vim by specifying some or all of the following 45 | as variables within your Vim configuration using the ``let`` directive. 46 | 47 | g:sparkup (Default: 'sparkup') - 48 | Location of the sparkup executable. You shouldn't need to change this 49 | setting if you used the install option above. 50 | 51 | g:sparkupArgs (Default: '--no-last-newline') - 52 | Additional args passed to sparkup. 53 | 54 | g:sparkupExecuteMapping (Default: '') - 55 | Mapping used to execute sparkup within insert mode. 56 | 57 | g:sparkupNextMapping (Default: '') - 58 | Mapping used to jump to the next empty tag/attribute within insert mode. 59 | 60 | g:sparkupMaps (Default: 1) - 61 | Set up automatic mappings for Sparkup. If set to 0, this can be 62 | used to disable creation of any mappings, which is useful if 63 | full customisation is required. 64 | 65 | g:sparkupMapsNormal (Default: 0) - 66 | Set up mappings for normal mode within Vim. The same execute and next 67 | mappings configured above will apply to normal mode if this option is 68 | set. 69 | 70 | -------------------------------------------------------------------------------- /vim/ftplugin/html/sparkup.py: -------------------------------------------------------------------------------- 1 | ../../../sparkup.py -------------------------------------------------------------------------------- /vim/ftplugin/html/sparkup.vim: -------------------------------------------------------------------------------- 1 | " Sparkup 2 | " Installation: 3 | " Copy the contents of vim/ftplugin/ to your ~/.vim/ftplugin directory: 4 | " 5 | " $ cp -R vim/ftplugin ~/.vim/ftplugin/ 6 | " 7 | " or use one of the automated methods specified in the README.txt file. 8 | " 9 | " Configuration: 10 | " g:sparkup (Default: 'sparkup') - 11 | " Location of the sparkup executable. You shouldn't need to change this 12 | " setting if you used the install option above. 13 | " 14 | " g:sparkupArgs (Default: '--no-last-newline') - 15 | " Additional args passed to sparkup. 16 | " 17 | " g:sparkupExecuteMapping (Default: '') - 18 | " Mapping used to execute sparkup within insert mode. 19 | " 20 | " g:sparkupNextMapping (Default: '') - 21 | " Mapping used to jump to the next empty tag/attribute within insert mode. 22 | " 23 | " g:sparkupMaps (Default: 1) - 24 | " Set up automatic mappings for Sparkup. If set to 0, this can be 25 | " used to disable creation of any mappings, which is useful if 26 | " full customisation is required. 27 | " 28 | " g:sparkupMapsNormal (Default: 0) - 29 | " Set up mappings for normal mode within Vim. The same execute and next 30 | " mappings configured above will apply to normal mode if this option is 31 | " set. 32 | 33 | if !exists('g:sparkupExecuteMapping') 34 | let g:sparkupExecuteMapping = '' 35 | endif 36 | 37 | if !exists('g:sparkupNextMapping') 38 | let g:sparkupNextMapping = '' 39 | endif 40 | 41 | if !exists('g:sparkupMaps') 42 | let g:sparkupMaps = 1 43 | endif 44 | 45 | if !exists('g:sparkupMapsNormal') 46 | let g:sparkupMapsNormal = 0 47 | endif 48 | 49 | inoremap SparkupExecute u:call Sparkup() 50 | inoremap SparkupNext u:call SparkupNext() 51 | 52 | if g:sparkupMaps 53 | if ! hasmapto('SparkupExecute', 'i') 54 | exec 'imap ' . g:sparkupExecuteMapping . ' SparkupExecute' 55 | endif 56 | if ! hasmapto('SparkupNext', 'i') 57 | exec 'imap ' . g:sparkupNextMapping . ' SparkupNext' 58 | endif 59 | if g:sparkupMapsNormal 60 | if ! hasmapto('SparkupExecute', 'n') 61 | exec 'nnoremap ' . g:sparkupExecuteMapping . ' :call Sparkup()' 62 | endif 63 | if ! hasmapto('SparkupNext', 'n') 64 | exec 'nnoremap ' . g:sparkupNextMapping . ' :call SparkupNext()' 65 | endif 66 | endif 67 | endif 68 | 69 | if exists('*s:Sparkup') 70 | finish 71 | endif 72 | 73 | function! s:Sparkup() 74 | if !exists('s:sparkup') 75 | let s:sparkup = exists('g:sparkup') ? g:sparkup : 'sparkup' 76 | 77 | if !executable(s:sparkup) 78 | " If g:sparkup is not configured (and/or not found in $PATH), 79 | " look for sparkup.vim in Vim's runtimepath. 80 | " XXX: quite expensive for a Pathogen-like environment (where &rtp is huge) 81 | let paths = substitute(escape(&runtimepath, ' '), '\(,\|$\)', '/**\1', 'g') 82 | let s:sparkup = fnamemodify(findfile('sparkup.py', paths), ':p') 83 | 84 | if !filereadable(s:sparkup) 85 | echohl WarningMsg 86 | echom 'Warning: could not find sparkup/sparkup.py on your path or in your vim runtime path.' 87 | echohl None 88 | unlet s:sparkup 89 | return 90 | endif 91 | endif 92 | let s:sparkup = '"' . s:sparkup . '"' 93 | " Workaround for windows, where the Python file cannot be executed via shebang 94 | if has('win32') || has('win64') 95 | let s:sparkup = 'python ' . s:sparkup 96 | endif 97 | endif 98 | 99 | " Build arguments list (not cached, g:sparkupArgs might change, also 100 | " &filetype, &expandtab etc) 101 | let sparkupArgs = exists('g:sparkupArgs') ? g:sparkupArgs : '--no-last-newline' 102 | " Pass '--xml' option, if 'xml' is used as filetype (default: none/'html') 103 | " NOTE: &filetype can contain multiple values, e.g. 'smarty.html' 104 | if index(split(&filetype, '\.'), 'xml') >= 0 105 | let sparkupArgs .= ' --xml' 106 | endif 107 | " If the user's settings are to indent with tabs, do so! 108 | " TODO textmate version of this functionality 109 | if !&expandtab 110 | let sparkupArgs .= ' --indent-tabs' 111 | endif 112 | 113 | let sparkupCmd = s:sparkup . printf(' %s --indent-spaces=%s', sparkupArgs, &shiftwidth) 114 | exec '.!' . sparkupCmd 115 | call s:SparkupNext() 116 | endfunction 117 | 118 | function! s:SparkupNext() 119 | " 1: empty tag, 2: empty attribute, 3: empty line 120 | let n = search('><\/\|\(""\)\|\(^\s*$\)', 'Wp') 121 | if n == 3 122 | startinsert! 123 | else 124 | let p = getpos(".") 125 | let p[2] = p[2] + 1 126 | call setpos(".", p) 127 | startinsert 128 | endif 129 | endfunction 130 | -------------------------------------------------------------------------------- /vim/ftplugin/htmldjango: -------------------------------------------------------------------------------- 1 | html -------------------------------------------------------------------------------- /vim/ftplugin/smarty: -------------------------------------------------------------------------------- 1 | html -------------------------------------------------------------------------------- /vim/ftplugin/xml: -------------------------------------------------------------------------------- 1 | html --------------------------------------------------------------------------------