├── .gitattributes
├── .gitignore
├── License.md
├── MANIFEST.in
├── README.md
├── htmltree
├── __init__.py
├── doc
│ └── img
│ │ └── quickstart.png
├── htmltree.py
└── tests
│ ├── client.py
│ └── sanitycheck.py
├── setup.cfg
└── setup.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text=auto
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 | *.c text
7 | *.css text
8 | *.example text
9 | *.h text text
10 | *.html text
11 | *.ics text
12 | *.ini text
13 | *.js text
14 | *.json text
15 | *.jsonp text
16 | *.load text
17 | *.md text
18 | *.map text
19 | *.markdown text
20 | *.py text
21 | *.local text
22 | *.sh text
23 | *.xml text
24 | rc.local text
25 |
26 | # Declare files that will always have CRLF line endings on checkout.
27 | # *.sln text eol=crlf
28 |
29 | # Denote all files that are truly binary and should not be modified.
30 | *.png binary
31 | *.jpg binary
32 | *.ico
33 | *.pdf binary
34 | *.woff binary
35 | *.woff2 binary
36 | *.svg binary
37 | *.sqlite binary
38 | *.ttf binary
39 | *.eot binary
40 | *.table binary
41 |
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## File types that should not be version controlled.
2 |
3 | # Compiled source #
4 | ###################
5 | *.com
6 | *.class
7 | *.dll
8 | *.exe
9 | *.o
10 | *.so
11 |
12 | # Packages #
13 | ############
14 | # it's better to unpack these files and commit the raw source
15 | # git has its own built in compression methods
16 | *.7z
17 | *.dmg
18 | *.gz
19 | *.iso
20 | *.jar
21 | *.rar
22 | *.tar
23 | *.zip
24 |
25 | # Logs and databases #
26 | ######################
27 | *.db
28 | *.log
29 | *.log.*
30 | *.sql
31 | *.sqlite
32 | *.out
33 |
34 |
35 | # OS generated files #
36 | ######################
37 | .DS_Store
38 | .DS_Store?
39 | ._*
40 | .Spotlight-V100
41 | .Trashes
42 | ehthumbs.db
43 | Thumbs.db
44 |
45 | # Editor temporary files #
46 | ##########################
47 | *.swo
48 | *.swp
49 | *.bak
50 |
51 |
52 | # UltiSnips snippet files #
53 | ###########################
54 | *.snippets
55 |
56 | ## Python specific files
57 |
58 | # Byte-compiled / optimized / DLL files
59 | __pycache__/
60 | *.py[cod]
61 | *$py.class
62 |
63 | # C extensions
64 | *.so
65 |
66 | # Distribution / packaging
67 | .Python
68 | env/
69 | build/
70 | develop-eggs/
71 | dist/
72 | downloads/
73 | eggs/
74 | .eggs/
75 | lib/
76 | lib64/
77 | parts/
78 | sdist/
79 | var/
80 | *.egg-info/
81 | .installed.cfg
82 | *.egg
83 |
84 | # PyInstaller
85 | # Usually these files are written by a python script from a template
86 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
87 | *.manifest
88 | *.spec
89 |
90 | # Installer logs
91 | pip-log.txt
92 | pip-delete-this-directory.txt
93 |
94 | # Unit test / coverage reports
95 | htmlcov/
96 | .tox/
97 | .coverage
98 | .coverage.*
99 | .cache
100 | nosetests.xml
101 | coverage.xml
102 | *,cover
103 | .hypothesis/
104 |
105 | # Translations
106 | *.mo
107 | *.pot
108 |
109 | # Django stuff:
110 | *.log
111 | local_settings.py
112 |
113 | # Flask stuff:
114 | instance/
115 | .webassets-cache
116 |
117 | # Scrapy stuff:
118 | .scrapy
119 |
120 | # Sphinx documentation
121 | docs/_build/
122 |
123 | # PyBuilder
124 | target/
125 |
126 | # IPython Notebook
127 | .ipynb_checkpoints
128 |
129 | # pyenv
130 | .python-version
131 |
132 | # celery beat schedule file
133 | celerybeat-schedule
134 |
135 | # dotenv
136 | .env
137 |
138 | # virtualenv
139 | venv/
140 | ENV/
141 |
142 | # Spyder project settings
143 | .spyderproject
144 |
145 | # Rope project settings
146 | .ropeproject
147 |
148 | # directories for generated files
149 | __javascript__/
150 | __pycache__/
151 | __html__/
152 |
153 |
154 |
--------------------------------------------------------------------------------
/License.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include tests/*.py
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Table of Contents** *generated with [DocToc](http://doctoc.herokuapp.com/)*
2 |
3 | - [Python htmltree project](#python-htmltree-project)
4 | - [Create and manipulate HTML and CSS from the comfort of Python](#create-and-manipulate-html-and-css-from-the-comfort-of-python)
5 | - [Installation](#installation)
6 | - [Quick Start](#quick-start)
7 | - [Open a Python interpreter and type or paste the following](#open-a-python-interpreter-and-type-or-paste-the-following)
8 | - [Render and print the HTML](#render-and-print-the-html)
9 | - [Now add some styling and text ...](#now-add-some-styling-and-text)
10 | - [and print the result.](#and-print-the-result)
11 | - [Reserved words and hyphenated attributes](#reserved-words-and-hyphenated-attributes)
12 | - [Viewing your work](#viewing-your-work)
13 | - [Discussion](#discussion)
14 | - [Public members](#public-members)
15 | - [Rendering](#rendering)
16 | - [Usage tips](#usage-tips)
17 | - [Rolling your own](#rolling-your-own)
18 | - [Bundling](#bundling)
19 | - [Looping](#looping)
20 | - [Using htmltree with Transcrypt™](#using-htmltree-with-transcrypt)
21 | - [Writing CSS](#writing-css)
22 | - [List of wrapper functions](#list-of-wrapper-functions)
23 | # Python htmltree project
24 |
25 | ## Create and manipulate HTML and CSS from the comfort of Python
26 | * Easy to learn. Consistent, simple syntax.
27 | * 85 predefined tag functions.
28 | * Create HTML on the fly or save as static files.
29 | * Flexible usage and easy extension.
30 | * Run locally with CPython or as Javascript in the browser using Jacques De Hooge's [*Transcrypt™*](https://transcrypt.org/) Python to JS transpiler
31 | * Dependencies: Python 3.x
32 |
33 | ### Installation
34 | `pip install htmltree`
35 |
36 | ### Quick Start
37 |
38 | #### Open a Python interpreter and type or paste the following
39 | ```
40 | from htmltree import *
41 | head = Head()
42 | body = Body()
43 | doc = Html(head, body)
44 | ```
45 | #### Render and print the HTML
46 | ```
47 | >>> print(doc.render(0))
48 |
49 |
50 |
51 |
52 |
53 |
54 | ```
55 | #### Now add some styling and text
56 | ```
57 | who = Meta(name="author",content="Your Name Here")
58 | head.C.append(who)
59 | body.A.update(dict(style={'background-color':'black'}))
60 | banner = H1("Hello, htmltree!", _class='banner', style={'color':'green'})
61 | body.C.append(banner)
62 | ```
63 | #### and print the result.
64 | ```
65 | >>> print(doc.render(0))
66 |
67 |
68 |
69 |
70 |
71 |
72 | Hello, htmltree!
73 |
74 |
75 |
76 | ```
77 | In the examples above, we created elements and assigned them to variables so we could alter their content later. However, we could also have written it out all at once.
78 |
79 | ```
80 | doc = Html(
81 | Head(
82 | Meta(name="author",content="Your Name Here")),
83 | Body(
84 | H1("Hello, htmltree!", _class='banner', style={'color':'green'}),
85 | style={'background-color':'black'}))
86 | ```
87 |
88 | That's short and clean and renders exactly the same html. It also mimics the page structure but sacrifices ease of alteration later in the execution. Your choices should come down to whether you're creating static html or dynamic content based on information that's not available until run time.
89 |
90 | ### Reserved words and hyphenated attributes
91 | Did you notice the underscore in `H1("Hello, htmltree!", _class='banner', ...)`? It's written that way because `class` is a Python keyword. Trying to use it as an identifier will raise a syntax error.
92 |
93 | As a convenience, all the wrapper functions strip off leading and trailing underscores in attribute names, so `class_` would also work. Normal HTML doesn't use underscores in attribute names so this fix is safe to use. I think `for` as a `` attribute is the only other conflict in standard HTML.
94 |
95 | The wrapper functions also replace internal underscores in attribute names with dashes. That avoids the problem of Python trying to interpret `data-role="magic"` as a subtraction expression. Use `data_role="magic"` instead. If you need to style with vendor-specific attributes that begin with a '-', add a trailing underscore, e.g. `_moz_style_` is converted to `-moz-style`.
96 |
97 | *The conversion happens when the element is created, not when it is rendered.* If you add, update or replace an element attribute after it is created, use the attribute's true name, e.g. `mybutton.A.update({'class': 'super-button'})` rather than `mybutton.A.update(dict(_class='super-button'))`.
98 |
99 |
100 |
101 | ### Viewing your work
102 | Use htmltree's `renderToFile` method and Python's standard `webbrowser` module.
103 | ```
104 | import webbrowser
105 | fileurl = doc.renderToFile('path/to/somefile.html')
106 | webbrowser.open(fileurl)
107 | ```
108 |
109 | The Quick Start example should look like this:
110 |
111 | 
112 |
113 | ## Discussion
114 | Importing * from htmltree.py provides 85 wrapper functions (as of this writing) that cover the most of the common non-obsolete HTML5 tags. To see the most up-to-date list you can do `help(htmltree)` from the command line of a Python interactive session or look futher down on this page for a listing. The function names and arguments follow simple and consistent conventions that make use of Python's `*args, **kwargs`features.
115 |
116 | - Functions are named by tag with initial caps, e.g. `Html()`
117 | - The signature for non-empty tags is `Tagname(*content, **attrs)`
118 | - The signature for empty tags is `Tagname(**attrs)` (Empty refers to elements that enclose no content and need no closing tag -- like ` `, ` `, etc.)
119 |
120 | To create, say, a div with two empty paragraphs separated by a horizontal rule element, you'd write
121 |
122 | ```mydiv = Div(P(), Hr(), P(), id=42, name='puddintane')```
123 |
124 | Because the first three args are unnamed Python knows they belong, in order, `to *content`. The last two arguments are named and therefore belong to `**attrs`, the attributes of the div.
125 |
126 | Python's rules about not mixing list and keyword arguments apply. In every element, put all the *content args first, followed by all the **attrs arguments.
127 |
128 | The '
135 | ```
136 | - See [Writing CSS](#writing-css) later in this document for more dicussion of `Style()`.
137 |
138 | The design pattern for `htmltree` is "as simple as possible but not simpler." Using built-in Python objects, dicts and lists, means that all the familiar methods of those objects are available when manipulating trees of Elements. Notice, for instance, the use of `update` and `append` in the Quick start examples.
139 | ```
140 | body.A.update(dict(style={'background-color':'black'}))
141 | body.C.append(H1("Hello, htmltree!", _class='myclass', id='myid'))
142 | ```
143 | But wait a minute! What are `body.A` and `body.C`? Read on ...
144 |
145 | ### Public members
146 | You can access and modify the attributes and content of an element `el` as `el.A` and `el.C` respectively. The tagname is also available as `el.T` though this is generally not so useful as the other two.
147 |
148 | The attribute member, `el.A` is an ordinary Python dictionary containing whatever keyword arguments were passed when the element was created. You can modify it with `update()` as shown in the Quick Start example or use any of the other dictionary methods on it. You can also replace it entirely with any dict-like object that has an `items()` method that behaves like dict.items()
149 |
150 | The content member, `el.C` is normally a Python list. It contains all the stuff that gets rendered between the closing and ending tags of an element. The list may hold an arbitrary mix of strings, ints, float, and objects. In normal usage, the objects are of type `htmltree.Element`. This is the element type returned by all the functions in htmltree.py. You can use all the normal Python list methods (append, insert, etc) to manipulate the list.
151 |
152 | (If you insert objects (other than those listed above), they should have a `render(indent=-1)` method that returns valid HTML with the same indentation conventions as the `htmltree.Element.render` method described in the next section.)
153 |
154 | ### Rendering
155 | The render method emits HTML. In the examples above, we've called it as doc.render(0) to display the entire document tree in indented form. Calling it with no arguments emits the HTML as a single line with no breaks or spaces. Values > 0 increase the indentations by 2 spaces * the value.
156 | ```
157 | >>> print(head.render())
158 |
159 |
160 | >>> print(head.render(0))
161 |
162 |
163 |
164 |
165 |
166 | >>> print(head.render(1))
167 |
168 |
169 |
170 |
171 | ```
172 |
173 | To include the `` declaration and the beginning of the document, you can use the flag, `doctype_declaration`.
174 | ```
175 | >>> print(doc.render(doctype_declaration=True))
176 |
177 |
178 | >>> print(doc.render(0, doctype_declaration=True))
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | >>> print(doc.render(1, doctype_declaration=True))
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 | ```
198 |
199 | The `renderToFile()` method also accepts the `indent` and `doctype_declaration` arguments.
200 |
201 | ## Usage tips
202 |
203 | ### Rolling your own
204 | The simplest possible extension is wrapping a frequently used tag to save a little typing. This is already done for you for all the wrapper functions in htmltree.py. But if you need something that's not defined it only takes two lines of code (not counting the import).
205 | ```
206 | from htmltree import KWElement
207 | def Foo(*content, **attrs):
208 | return KWElement('foo', *content, **wrappers)
209 | ```
210 | For an empty tag element, omit the content arg and pass None to KWElement().
211 | ```
212 | def Bar(**attrs):
213 | return KWElement('bar', None, **attrs)
214 | ```
215 |
216 | ### Bundling
217 | Wrapping commonly used fragments in a function is easy and useful, e.g.
218 | ```
219 | def docheadbody():
220 | head = Head()
221 | body = Body()
222 | doc = Html(head, body)
223 | return doc, head, body
224 |
225 | >>> doc, head, body = docheadbody()
226 | ```
227 |
228 | ### Looping
229 | Use loops to simplify the creation of many similar elements.
230 | ```
231 | for id in ('one', 'two', 'three'):
232 | content = "Help! I'm trapped in div {}.".format(id)
233 | body.C.append(Div(content, id=id))
234 |
235 | >>> print(body.render(0))
236 |
237 |
238 | Help! I'm trapped in div one.
239 |
240 |
241 | Help! I'm trapped in div two.
242 |
243 |
244 | Help! I'm trapped in div three.
245 |
246 |
247 | ```
248 | ### Using *htmltree* with [*Transcrypt*](https://transcrypt.org/)
249 | This project was designed from the ground up to be compatible with Transcrypt to create a pure Python development environment for HTML/CSS/JS on both sides of the client/server divide.
250 |
251 | If you've installed *htmltree* with `pip`, Transcrypt will find it when transpiling your Python files to JavaScript if you import it as `htmltree`. If you have a need to install and access *htmltree* by other means, see
252 | * http://www.transcrypt.org/docs/html/special_facilities.html for information about Transcrypt's module mechanism and
253 | * https://github.com/Michael-F-Ellis/htmltree/issues/3 for a discussion of some specific ways to locate htmltree at compile time.
254 |
255 | Also, look at the modules `sanitycheck.py` and `client.py` in the `tests/` directory as a template for developing and testing with htmltree and Transcrypt. For a more elaborate template with a built-in server, AJAX/JSON data updates and automatic rebuild/reload when source files change, see [NearlyPurePythonWebAppDemo](https://github.com/Michael-F-Ellis/NearlyPurePythonWebAppDemo)
256 |
257 | All the functions should work the same as under CPython. If not, please submit an issue on GitHub so I can fix it!
258 |
259 | ### Writing CSS
260 |
261 | Use the Style() element to create CSS rulesets for your HTML, for example
262 | ```
263 | >>> mystyle = Style(h2=dict(margin_top='4px', color='red'))
264 | >>> doc = Html(Head(mystyle), Body(H2("Hello!")))
265 | >>> print(doc.render(0))
266 |
267 |
268 |
269 |
272 |
273 |
274 |
275 | Hello!
276 |
277 |
278 |
279 | ```
280 |
281 | #### Interlude: dict() vs {}
282 | In python, `{}` and `dict()` both define dictionaries. The difference is that dict treats keyword arguments as strings, so
283 | ```
284 | >>> {'foo':1, 'bar':2} == dict(foo=1, bar=2)
285 | True
286 | ```
287 | Using `dict()` with keyword arguments saves having to quote keywords and `htmltree` tries to help by converting underscores in keywords to hyphens (as discussed earlier in this document). On the other hand, the CSS selector syntax permits strings that are not valid Python keywords, so the best practice for non-trivial rulesets is to create a dictionary of dictionaries, as shown below, and supply that as the argument to Style(). *Don't forget the two asterisks in front!*
288 | ```
289 | mycss = {
290 | 'a.someclass' : {'color':'red', 'margin-top':'0.5em !important'},
291 | 'div p #id:first-line' : {'background-style': 'none'}
292 | }
293 | mystyle = Style(**mycss) ## Don't forget the two asterisks in front
294 |
295 | >>> print(mystyle.render(0))
296 |
300 | ```
301 |
302 | If you compare the dictionary definition of `mycss` with the rendered css
303 | output, you'll see that the differences are as follows:
304 |
305 | 1. Quote every item,
306 | 2. A colon between each selector and its property/value pairs,
307 | 3. A comma after each rule definition,
308 | 4. Separate property:value pairs with commas instead of semicolons.
309 | #### Caveat: dictionaries are unordered
310 | When two CSS selectors apply with equal specificity to the same property of an element, [the last one wins](http://monc.se/kitchen/38/cascading-order-and-inheritance-in-css). You should not rely on the Style() object to render rules in any particular order because Python dictionaries are, by definition, unordered collections.
311 | ## List of wrapper functions
312 | ```
313 | Html(*content, **attrs):
314 | Head(*content, **attrs):
315 | Body(*content, **attrs):
316 | Link(**attrs):
317 | Meta(**attrs):
318 | Title(*content, **attrs):
319 | Style(**content):
320 | Address(*content, **attrs):
321 | Article(*content, **attrs):
322 | Aside(*content, **attrs):
323 | Footer(*content, **attrs):
324 | Header(*content, **attrs):
325 | H1(*content, **attrs):
326 | H2(*content, **attrs):
327 | H3(*content, **attrs):
328 | H4(*content, **attrs):
329 | H5(*content, **attrs):
330 | H6(*content, **attrs):
331 | Nav(*content, **attrs):
332 | Section(*content, **attrs):
333 | Blockquote(*content, **attrs):
334 | Dd(*content, **attrs):
335 | Div(*content, **attrs):
336 | Dl(*content, **attrs):
337 | Dt(*content, **attrs):
338 | Figcaption(*content, **attrs):
339 | Figure(*content, **attrs):
340 | Hr(**attrs):
341 | Li(*content, **attrs):
342 | Main(*content, **attrs):
343 | Ol(*content, **attrs):
344 | P(*content, **attrs):
345 | Pre(*content, **attrs):
346 | Ul(*content, **attrs):
347 | A(*content, **attrs):
348 | B(*content, **attrs):
349 | Br(**attrs):
350 | Cite(*content, **attrs):
351 | Code(*content, **attrs):
352 | Em(*content, **attrs):
353 | I(*content, **attrs):
354 | S(*content, **attrs):
355 | Samp(*content, **attrs):
356 | Small(*content, **attrs):
357 | Span(*content, **attrs):
358 | Strong(*content, **attrs):
359 | Sub(*content, **attrs):
360 | Sup(*content, **attrs):
361 | U(*content, **attrs):
362 | Area(**attrs):
363 | Audio(*content, **attrs):
364 | Img(**attrs):
365 | Map(*content, **attrs):
366 | Track(**attrs):
367 | Video(*content, **attrs):
368 | Embed(**attrs):
369 | Object(*content, **attrs):
370 | Param(**attrs):
371 | Source(**attrs):
372 | Canvas(*content, **attrs):
373 | Noscript(*content, **attrs):
374 | Script(*content, **attrs):
375 | Caption(*content, **attrs):
376 | Col(**attrs):
377 | Table(*content, **attrs):
378 | Tbody(*content, **attrs):
379 | Td(*content, **attrs):
380 | Tfoot(*content, **attrs):
381 | Th(*content, **attrs):
382 | Thead(*content, **attrs):
383 | Tr(*content, **attrs):
384 | Button(*content, **attrs):
385 | Datalist(*content, **attrs):
386 | Fieldset(*content, **attrs):
387 | Form(*content, **attrs):
388 | Input(**attrs):
389 | Label(*content, **attrs):
390 | Legend(*content, **attrs):
391 | Meter(*content, **attrs):
392 | Optgroup(*content, **attrs):
393 | Option(*content, **attrs):
394 | Output(*content, **attrs):
395 | Progress(*content, **attrs):
396 | Select(*content, **attrs):
397 | Textarea(*content, **attrs):
398 | ```
399 |
400 |
401 |
--------------------------------------------------------------------------------
/htmltree/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from htmltree.htmltree import *
3 |
--------------------------------------------------------------------------------
/htmltree/doc/img/quickstart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Michael-F-Ellis/htmltree/026f107b644f75ad49504bd45a72e4922292d380/htmltree/doc/img/quickstart.png
--------------------------------------------------------------------------------
/htmltree/htmltree.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Description: Provides a general html tree class, HtmlElement and wrapper
4 | functions for most standard non-obsolete HTML tags.
5 |
6 | This file is part of htmltree
7 | https://github.com/Michael-F-Ellis/htmltree
8 |
9 | Compatibilty with Transcrypt Python to JS transpiler:
10 | By design, this module is intended for use with both CPython and
11 | Transcrypt. The latter implies certain constraints on the Python
12 | constructs that may be used. Compatibility with Transcrypt also requires
13 | the use of two __pragma__ calls to achieve compilation. These are contained
14 | in line comments and hence have no effect when running under CPython.
15 |
16 | The benefit of Transcrypt compatibility is complete freedom to use Python
17 | to define HTML/CSS at run time on the client side, i.e. in transpiled JS
18 | running in a browser.
19 |
20 |
21 | Author: Michael Ellis
22 | Copyright 2017 Ellis & Grant, Inc.
23 | License: MIT License
24 | """
25 | # __pragma__('kwargs')
26 |
27 | def KWElement(tag, *content, **attrs):
28 | """
29 | It's recommended to import this function rather than the HtmlElement class it wraps.
30 | This function's signature reduces the need to quote attribute names.
31 | >>> KWElement('p', "hello", style=dict(_moz_style_="foo")).render()
32 | 'hello
'
33 | """
34 | if len(content) == 1 and content[0] is None:
35 | content = None
36 | else:
37 | content = list(content)
38 | return HtmlElement(tag, convertAttrKeys(attrs), content)
39 |
40 | def convertAttrKeys(attrdict):
41 | """
42 | Convert underscores to minus signs ('-'). Remove one trailing minus sign if present.
43 | If no trailing minus signs, remove one leading minus sign.
44 | >>> convertAttrKeys({'_class':'foo'})
45 | {'class': 'foo'}
46 | >>> convertAttrKeys({'class_':'foo'})
47 | {'class': 'foo'}
48 | >>> convertAttrKeys({'data_role':'foo'})
49 | {'data-role': 'foo'}
50 | >>> convertAttrKeys({'_moz_style_':'foo'})
51 | {'-moz-style': 'foo'}
52 | """
53 | newdict = {}
54 | for k, v in attrdict.items():
55 | ## Replace all underscores with '-'
56 | k = k.replace('_', '-')
57 | if k.endswith('-'):
58 | ## Drop trailing '-'
59 | k = k[:-1].replace('_','-')
60 | elif k.startswith('-'):
61 | ## Drop leading '-'
62 | k = k[1:]
63 |
64 | if k == 'style':
65 | newdict[k] = convertAttrKeys(v)
66 | else:
67 | newdict[k] = v
68 | return newdict
69 |
70 | class HtmlElement:
71 | """
72 | General nested html element tree with recursive rendering
73 |
74 | Note: Directly importing this class is deprecated (or at least not
75 | recommended) because the arguments to this classes constructor are not in
76 | the most convenient order. Import KWElement defined above instead.
77 |
78 | Constructor arguments:
79 | tagname : valid html tag name (string)
80 |
81 | attrs : attributes (dict | dict-like object | None)
82 | keys must be valid attribute names (string)
83 | values must be (string | list of strings | dict of styles)
84 |
85 | content : (None | string | int | float | list of (strings/ints/floats and/or elements)
86 | elements must have a 'render' method that returns valid html.
87 |
88 | None has a special meaning. It denotes a singleton tag, e.g. or
89 |
90 | The '
132 |
133 | >>> comment = E('!--', None, "This is out!")
134 | >>> comment.render()
135 | ''
136 |
137 | >>> comment.C = [body]
138 | >>> comment.render()
139 | ''
140 |
141 | """
142 | def __init__(self, tagname, attrs, content):
143 | ## Validate arguments
144 | assert isinstance(tagname, str)
145 | self.T = tagname.lower()
146 | if self.T == '!--': ## HTML comment
147 | attrs = None
148 | self.endtag = " -->"
149 | else:
150 | self.endtag = "{}>".format(self.T)
151 |
152 | if attrs is not None:
153 | ## hasattr isn't quite right in Transcrypt,
154 | ## hence we need to test with a try clause.
155 | try:
156 | _ = attrs.items()
157 | except AttributeError:
158 | msg = "attrs must be a dict-like object or None"
159 | raise ValueError(msg)
160 |
161 | self.A = attrs
162 | self.C = content
163 |
164 | def render(self, indent=-1, doctype_declaration=False):
165 | """ Recursively generate html """
166 | rlist = []
167 | ## Render the tag with attributes
168 | opentag = "<{}".format(self.T)
169 | rlist.append(indented(opentag, indent))
170 |
171 | ## Render the attributes
172 | if self.A is not None:
173 | for a, v in self.A.items():
174 | if isinstance(v, str):
175 | rlist.append(' {}="{}"'.format(a,v))
176 | elif v is None:
177 | rlist.append(' {}'.format(a)) # bare attribute, e.g. 'disabled'
178 | elif isinstance(v,list):
179 | _ = ' '.join(v) ## must be list of strings
180 | rlist.append(' {}="{}"'.format(a, _))
181 | elif a == 'style':
182 | rlist.append(' {}="{}"'.format(a,renderInlineStyle(v)))
183 | else:
184 | msg="Don't know what to with attribute {}={}".format(a,v)
185 | raise ValueError(msg)
186 |
187 | if self.C is None and self.T != "!--":
188 | ## It's a singleton tag. Close it accordingly.
189 | closing = ">"
190 | else:
191 | ## Close the tag
192 | if self.T == "!--":
193 | rlist.append(" ")
194 | else:
195 | rlist.append('>')
196 |
197 | ## Render the content
198 | if isinstance(self.C, str):
199 | rlist.append(indented(self.C, indent))
200 | elif self.T == "style":
201 | rlist.append(renderCss(self.C, indent))
202 | else:
203 | cindent = indent + 1 if indent >= 0 else indent
204 | for c in self.C:
205 | if isinstance(c, (str,int,float)):
206 | rlist.append(indented(str(c), cindent))
207 | else:
208 | rlist.append(c.render(cindent)) ## here's the recursion!
209 |
210 | closing = indented(self.endtag, indent)
211 | rlist.append(closing)
212 |
213 | if not doctype_declaration:
214 | return ''.join(rlist)
215 | else:
216 | return indented("", indent) + ''.join(rlist)
217 |
218 | #__pragma__('skip')
219 | def renderToFile(self, filepath, indent=-1, doctype_declaration=False):
220 | """
221 | Render to a local file and return a "file://" url for convenient display.
222 | Note: This method has no meaning under Transcrypt and is not compiled.
223 | """
224 | import os
225 | with open(filepath, 'w') as f:
226 | print(self.render(indent=indent, doctype_declaration=doctype_declaration), file=f)
227 | return "file://" + os.path.abspath(filepath)
228 | #__pragma__('noskip')
229 |
230 | def indented(contentstring, indent=-1):
231 | """
232 | Return indented content.
233 | indent >= 0 prefixes content with newline + 2 * indent spaces.
234 | indent < 0 returns content unchanged
235 |
236 | Docstrings:
237 | >>> indented("foo bar", -1)
238 | 'foo bar'
239 |
240 | >>> indented("foo bar", 0)
241 | '\\nfoo bar'
242 |
243 | >>> indented("foo bar", 1)
244 | '\\n foo bar'
245 |
246 | """
247 | if not indent >= 0:
248 | return contentstring
249 | else:
250 | return "\n{}{}".format(" " * indent, contentstring)
251 |
252 |
253 | def renderInlineStyle(d):
254 | """If d is a dict of styles, return a proper style string """
255 | if isinstance(d, (str, int, float)):
256 | result = str(d)
257 | else:
258 | style=[]
259 | for k,v in d.items():
260 | style.append("{}:{};".format(k, v))
261 | separator = ' '
262 | result = separator.join(style)
263 | return result
264 |
265 | def renderCss(d, indent=-1):
266 | """
267 | Render a string of CSS rulesets from d, a dict (or dict-like object) of
268 | rulesets,.
269 | """
270 | rulesetlist = []
271 | for selector, declaration in d.items():
272 | ruleset = " ".join([selector,
273 | '{',
274 | renderInlineStyle(declaration),
275 | '}'
276 | ])
277 | rulesetlist.append(indented(ruleset, indent))
278 | return ' '.join(rulesetlist)
279 |
280 |
281 |
282 | #######################################################################
283 | ## Wrappers for html element tags.
284 | ##
285 | ## Functions are grouped in the categories given at
286 | ## https://developer.mozilla.org/en-US/docs/Web/HTML/Element and
287 | ## are alphabetical within groups.
288 | ##
289 | ## Conventions:
290 | ## Functions are named by tag with initial caps, e.g. Html()
291 | ##
292 | ## The signature for non-empty tags is Tagname(*content, **attrs)
293 | ## The signature for empty tags is Tagname(**attrs)
294 | ##
295 | ## Empty refers to elements that enclose no content and need no closing tag.
296 | ##
297 | ## '
380 | >>> b = ''
381 | >>> html in (a,b) # order is indeterminate, so test both ways
382 | True
383 | >>> Style(body=dict(_moz_style_='foo')).render()
384 | ''
385 | """
386 | ## convert selectors
387 | _ = convertAttrKeys(content)
388 | newcontent = {}
389 | for k, v in _.items():
390 | newcontent[k] = convertAttrKeys(v)
391 | return HtmlElement('style', {}, newcontent)
392 |
393 | #######################################################################
394 | ## Content Sectioning
395 | ## TODO hgroup
396 | #######################################################################
397 |
398 | def Address(*content, **attrs):
399 | """
400 | Wrapper for address tag
401 | >>> Address().render()
402 | ' '
403 | """
404 | return KWElement('address', *content, **attrs)
405 |
406 | def Article(*content, **attrs):
407 | """
408 | Wrapper for article tag
409 | >>> Article().render()
410 | ' '
411 | """
412 | return KWElement('article', *content, **attrs)
413 |
414 | def Aside(*content, **attrs):
415 | """
416 | Wrapper for aside tag
417 | >>> Aside().render()
418 | ''
419 | """
420 | return KWElement('aside', *content, **attrs)
421 |
422 | def Footer(*content, **attrs):
423 | """
424 | Wrapper for footer tag
425 | >>> Footer().render()
426 | ''
427 | """
428 | return KWElement('footer', *content, **attrs)
429 |
430 | def Header(*content, **attrs):
431 | """
432 | Wrapper for header tag
433 | >>> Header().render()
434 | ''
435 | """
436 | return KWElement('header', *content, **attrs)
437 |
438 |
439 | def H1(*content, **attrs):
440 | """
441 | Wrapper for h1 tag
442 | >>> H1().render()
443 | ' '
444 | """
445 | return KWElement('h1', *content, **attrs)
446 |
447 | def H2(*content, **attrs):
448 | """
449 | Wrapper for h2 tag
450 | >>> H2().render()
451 | ' '
452 | """
453 | return KWElement('h2', *content, **attrs)
454 |
455 | def H3(*content, **attrs):
456 | """
457 | Wrapper for h3 tag
458 | >>> H3().render()
459 | ' '
460 | """
461 | return KWElement('h3', *content, **attrs)
462 |
463 | def H4(*content, **attrs):
464 | """
465 | Wrapper for h4 tag
466 | >>> H4().render()
467 | ' '
468 | """
469 | return KWElement('h4', *content, **attrs)
470 |
471 | def H5(*content, **attrs):
472 | """
473 | Wrapper for h5 tag
474 | >>> H5().render()
475 | ' '
476 | """
477 | return KWElement('h5', *content, **attrs)
478 |
479 | def H6(*content, **attrs):
480 | """
481 | Wrapper for h6 tag
482 | >>> H6().render()
483 | ' '
484 | """
485 | return KWElement('h6', *content, **attrs)
486 |
487 | def Nav(*content, **attrs):
488 | """
489 | Wrapper for nav tag
490 | >>> Nav().render()
491 | ' '
492 | """
493 | return KWElement('nav', *content, **attrs)
494 |
495 | def Section(*content, **attrs):
496 | """
497 | Wrapper for section tag
498 | >>> Section().render()
499 | ''
500 | """
501 | return KWElement('section', *content, **attrs)
502 |
503 | #######################################################################
504 | ## Text Content
505 | #######################################################################
506 |
507 | def Blockquote(*content, **attrs):
508 | """
509 | Wrapper for blockquote tag
510 | >>> Blockquote().render()
511 | ' '
512 | """
513 | return KWElement('blockquote', *content, **attrs)
514 |
515 | def Dd(*content, **attrs):
516 | """
517 | Wrapper for dd tag
518 | >>> Dd().render()
519 | ' '
520 | """
521 | return KWElement('dd', *content, **attrs)
522 |
523 | def Div(*content, **attrs):
524 | """
525 | Wrapper for div tag
526 | >>> Div().render()
527 | '
'
528 | """
529 | return KWElement('div', *content, **attrs)
530 |
531 | def Dl(*content, **attrs):
532 | """
533 | Wrapper for dl tag
534 | >>> Dl().render()
535 | ' '
536 | """
537 | return KWElement('dl', *content, **attrs)
538 |
539 | def Dt(*content, **attrs):
540 | """
541 | Wrapper for dt tag
542 | >>> Dt().render()
543 | ' '
544 | """
545 | return KWElement('dt', *content, **attrs)
546 |
547 | def Figcaption(*content, **attrs):
548 | """
549 | Wrapper for figcaption tag
550 | >>> Figcaption().render()
551 | ' '
552 | """
553 | return KWElement('figcaption', *content, **attrs)
554 |
555 | def Figure(*content, **attrs):
556 | """
557 | Wrapper for figure tag
558 | >>> Figure().render()
559 | ' '
560 | """
561 | return KWElement('figure', *content, **attrs)
562 |
563 | def Hr(**attrs):
564 | """
565 | Wrapper for hr tag
566 | >>> Hr().render()
567 | ' '
568 | """
569 | return KWElement('hr', None, **attrs)
570 |
571 | def Li(*content, **attrs):
572 | """
573 | Wrapper for li tag
574 | >>> Li().render()
575 | ' '
576 | """
577 | return KWElement('li', *content, **attrs)
578 |
579 | def Main(*content, **attrs):
580 | """
581 | Wrapper for main tag
582 | >>> Main().render()
583 | ' '
584 | """
585 | return KWElement('main', *content, **attrs)
586 |
587 | def Ol(*content, **attrs):
588 | """
589 | Wrapper for ol tag
590 | >>> Ol().render()
591 | ' '
592 | """
593 | return KWElement('ol', *content, **attrs)
594 |
595 | def P(*content, **attrs):
596 | """
597 | Wrapper for p tag
598 | >>> P().render()
599 | '
'
600 | """
601 | return KWElement('p', *content, **attrs)
602 |
603 | def Pre(*content, **attrs):
604 | """
605 | Wrapper for pre tag
606 | >>> Pre().render()
607 | ' '
608 | """
609 | return KWElement('pre', *content, **attrs)
610 |
611 | def Ul(*content, **attrs):
612 | """
613 | Wrapper for ul tag
614 | >>> Ul().render()
615 | ''
616 | """
617 | return KWElement('ul', *content, **attrs)
618 |
619 | #######################################################################
620 | ## Inline Text Semantics
621 | ## TODO abbr, bdi, bdo, data, dfn, kbd, mark, q, rp, rt, rtc, ruby,
622 | ## time, var, wbr
623 | #######################################################################
624 |
625 | def A(*content, **attrs):
626 | """
627 | Wrapper for a tag
628 | >>> A("Example", href="https://example.com").render()
629 | 'Example '
630 | """
631 | return KWElement('a', *content, **attrs)
632 |
633 | def B(*content, **attrs):
634 | """
635 | Wrapper for b tag
636 | >>> B().render()
637 | ' '
638 | """
639 | return KWElement('b', *content, **attrs)
640 |
641 | def Br(**attrs):
642 | """
643 | Wrapper for br tag
644 | >>> Br().render()
645 | ' '
646 | """
647 | return KWElement('br', None, **attrs)
648 |
649 | def Cite(*content, **attrs):
650 | """
651 | Wrapper for cite tag
652 | >>> Cite().render()
653 | ' '
654 | """
655 | return KWElement('cite', *content, **attrs)
656 |
657 | def Code(*content, **attrs):
658 | """
659 | Wrapper for code tag
660 | >>> Code().render()
661 | '
'
662 | """
663 | return KWElement('code', *content, **attrs)
664 |
665 | def Em(*content, **attrs):
666 | """
667 | Wrapper for em tag
668 | >>> Em().render()
669 | ' '
670 | """
671 | return KWElement('em', *content, **attrs)
672 |
673 | def I(*content, **attrs):
674 | """
675 | Wrapper for i tag
676 | >>> I().render()
677 | ' '
678 | """
679 | return KWElement('i', *content, **attrs)
680 |
681 | def S(*content, **attrs):
682 | """
683 | Wrapper for s tag
684 | >>> S().render()
685 | ' '
686 | """
687 | return KWElement('s', *content, **attrs)
688 |
689 | def Samp(*content, **attrs):
690 | """
691 | Wrapper for samp tag
692 | >>> Samp().render()
693 | ' '
694 | """
695 | return KWElement('samp', *content, **attrs)
696 |
697 | def Small(*content, **attrs):
698 | """
699 | Wrapper for small tag
700 | >>> Small().render()
701 | ' '
702 | """
703 | return KWElement('small', *content, **attrs)
704 |
705 | def Span(*content, **attrs):
706 | """
707 | Wrapper for span tag
708 | >>> Span().render()
709 | ' '
710 | """
711 | return KWElement('span', *content, **attrs)
712 |
713 | def Strong(*content, **attrs):
714 | """
715 | Wrapper for strong tag
716 | >>> Strong().render()
717 | ' '
718 | """
719 | return KWElement('strong', *content, **attrs)
720 |
721 | def Sub(*content, **attrs):
722 | """
723 | Wrapper for sub tag
724 | >>> Sub().render()
725 | ' '
726 | """
727 | return KWElement('sub', *content, **attrs)
728 |
729 | def Sup(*content, **attrs):
730 | """
731 | Wrapper for sup tag
732 | >>> Sup().render()
733 | ' '
734 | """
735 | return KWElement('sup', *content, **attrs)
736 |
737 | def U(*content, **attrs):
738 | """
739 | Wrapper for u tag
740 | >>> U().render()
741 | ' '
742 | """
743 | return KWElement('u', *content, **attrs)
744 |
745 | #######################################################################
746 | ## Image and Multimedia
747 | #######################################################################
748 |
749 | def Area(**attrs):
750 | """
751 | Wrapper for area tag
752 | >>> Area().render()
753 | ' '
754 | """
755 | return KWElement('area', None, **attrs)
756 |
757 | def Audio(*content, **attrs):
758 | """
759 | Wrapper for audio tag
760 | >>> Audio().render()
761 | ' '
762 | """
763 | return KWElement('audio', *content, **attrs)
764 |
765 | def Img(**attrs):
766 | """
767 | Wrapper for img tag
768 | >>> Img().render()
769 | ' '
770 | """
771 | return KWElement('img', None, **attrs)
772 |
773 | def Map(*content, **attrs):
774 | """
775 | Wrapper for map tag
776 | >>> Map().render()
777 | ' '
778 | """
779 | return KWElement('map', *content, **attrs)
780 |
781 | def Track(**attrs):
782 | """
783 | Wrapper for track tag
784 | >>> Track().render()
785 | ''
786 | """
787 | return KWElement('track', None, **attrs)
788 |
789 | def Video(*content, **attrs):
790 | """
791 | Wrapper for video tag
792 | >>> Video().render()
793 | ' '
794 | """
795 | return KWElement('video', *content, **attrs)
796 |
797 | #######################################################################
798 | ## Embedded Content
799 | #######################################################################
800 |
801 | def Embed(**attrs):
802 | """
803 | Wrapper for embed tag
804 | >>> Embed().render()
805 | ''
806 | """
807 | return KWElement('embed', None, **attrs)
808 |
809 | def Object(*content, **attrs):
810 | """
811 | Wrapper for object tag
812 | >>> Object().render()
813 | ' '
814 | """
815 | return KWElement('object', *content, **attrs)
816 |
817 | def Param(**attrs):
818 | """
819 | Wrapper for param tag
820 | >>> Param().render()
821 | ' '
822 | """
823 | return KWElement('param', None, **attrs)
824 |
825 | def Source(**attrs):
826 | """
827 | Wrapper for source tag
828 | >>> Source().render()
829 | ''
830 | """
831 | return KWElement('source', None, **attrs)
832 |
833 | #######################################################################
834 | ## Scripting
835 | #######################################################################
836 |
837 | def Canvas(*content, **attrs):
838 | """
839 | Wrapper for canvas tag
840 | >>> Canvas().render()
841 | ' '
842 | """
843 | return KWElement('canvas', *content, **attrs)
844 |
845 | def Noscript(*content, **attrs):
846 | """
847 | Wrapper for noscript tag
848 | >>> Noscript().render()
849 | ' '
850 | """
851 | return KWElement('noscript', *content, **attrs)
852 |
853 | def Script(*content, **attrs):
854 | """
855 | Wrapper for script tag
856 | >>> Script().render()
857 | ''
858 | """
859 | return KWElement('script', *content, **attrs)
860 |
861 | #######################################################################
862 | ## Demarcating Edits
863 | ## TODO del, ins
864 | #######################################################################
865 |
866 | #######################################################################
867 | ## Table Content
868 | ## TODO colgroup (maybe. It's poorly supported.)
869 | #######################################################################
870 |
871 | def Caption(*content, **attrs):
872 | """
873 | Wrapper for caption tag
874 | >>> Caption().render()
875 | ' '
876 | """
877 | return KWElement('caption', *content, **attrs)
878 |
879 | def Col(**attrs):
880 | """
881 | Wrapper for col tag
882 | >>> Col().render()
883 | ' '
884 | """
885 | return KWElement('col', None, **attrs)
886 |
887 | def Table(*content, **attrs):
888 | """
889 | Wrapper for table tag
890 | >>> Table().render()
891 | ''
892 | """
893 | return KWElement('table', *content, **attrs)
894 |
895 | def Tbody(*content, **attrs):
896 | """
897 | Wrapper for tbody tag
898 | >>> Tbody().render()
899 | ' '
900 | """
901 | return KWElement('tbody', *content, **attrs)
902 |
903 | def Td(*content, **attrs):
904 | """
905 | Wrapper for td tag
906 | >>> Td().render()
907 | ' '
908 | """
909 | return KWElement('td', *content, **attrs)
910 |
911 | def Tfoot(*content, **attrs):
912 | """
913 | Wrapper for tfoot tag
914 | >>> Tfoot().render()
915 | ' '
916 | """
917 | return KWElement('tfoot', *content, **attrs)
918 |
919 | def Th(*content, **attrs):
920 | """
921 | Wrapper for th tag
922 | >>> Th().render()
923 | ' '
924 | """
925 | return KWElement('th', *content, **attrs)
926 |
927 | def Thead(*content, **attrs):
928 | """
929 | Wrapper for thead tag
930 | >>> Thead().render()
931 | ' '
932 | """
933 | return KWElement('thead', *content, **attrs)
934 |
935 | def Tr(*content, **attrs):
936 | """
937 | Wrapper for tr tag
938 | >>> Tr().render()
939 | ' '
940 | """
941 | return KWElement('tr', *content, **attrs)
942 |
943 | #######################################################################
944 | ## Forms
945 | #######################################################################
946 |
947 | def Button(*content, **attrs):
948 | """
949 | Wrapper for button tag
950 | >>> Button().render()
951 | ' '
952 | """
953 | return KWElement('button', *content, **attrs)
954 |
955 | def Datalist(*content, **attrs):
956 | """
957 | Wrapper for datalist tag
958 | >>> Datalist().render()
959 | ' '
960 | """
961 | return KWElement('datalist', *content, **attrs)
962 |
963 | def Fieldset(*content, **attrs):
964 | """
965 | Wrapper for fieldset tag
966 | >>> Fieldset().render()
967 | ' '
968 | """
969 | return KWElement('fieldset', *content, **attrs)
970 |
971 | def Form(*content, **attrs):
972 | """
973 | Wrapper for form tag
974 | >>> Form().render()
975 | ''
976 | """
977 | return KWElement('form', *content, **attrs)
978 |
979 | def Input(**attrs):
980 | """
981 | Wrapper for input tag
982 | >>> Input().render()
983 | ' '
984 | """
985 | return KWElement('input', None, **attrs)
986 |
987 | def Label(*content, **attrs):
988 | """
989 | Wrapper for label tag
990 | >>> Label().render()
991 | ' '
992 | """
993 | return KWElement('label', *content, **attrs)
994 |
995 | def Legend(*content, **attrs):
996 | """
997 | Wrapper for legend tag
998 | >>> Legend().render()
999 | ' '
1000 | """
1001 | return KWElement('legend', *content, **attrs)
1002 |
1003 | def Meter(*content, **attrs):
1004 | """
1005 | Wrapper for meter tag
1006 | >>> Meter().render()
1007 | ' '
1008 | """
1009 | return KWElement('meter', *content, **attrs)
1010 |
1011 | def Optgroup(*content, **attrs):
1012 | """
1013 | Wrapper for optgroup tag
1014 | >>> Optgroup().render()
1015 | ' '
1016 | """
1017 | return KWElement('optgroup', *content, **attrs)
1018 |
1019 | def Option(*content, **attrs):
1020 | """
1021 | Wrapper for option tag
1022 | >>> Option().render()
1023 | ' '
1024 | """
1025 | return KWElement('option', *content, **attrs)
1026 |
1027 | def Output(*content, **attrs):
1028 | """
1029 | Wrapper for output tag
1030 | >>> Output().render()
1031 | ' '
1032 | """
1033 | return KWElement('output', *content, **attrs)
1034 |
1035 | def Progress(*content, **attrs):
1036 | """
1037 | Wrapper for progress tag
1038 | >>> Progress().render()
1039 | ' '
1040 | """
1041 | return KWElement('progress', *content, **attrs)
1042 |
1043 | def Select(*content, **attrs):
1044 | """
1045 | Wrapper for select tag
1046 | >>> Select().render()
1047 | ' '
1048 | """
1049 | return KWElement('select', *content, **attrs)
1050 |
1051 | def Textarea(*content, **attrs):
1052 | """
1053 | Wrapper for textarea tag
1054 | >>> Textarea().render()
1055 | ''
1056 | """
1057 | return KWElement('textarea', *content, **attrs)
1058 |
1059 | #######################################################################
1060 | ## Interactive Elememts (Experimental. Omitted for now.)
1061 | #######################################################################
1062 |
1063 | #######################################################################
1064 | ## Web Components (Experimental. Omitted for now.)
1065 | #######################################################################
1066 |
1067 | # __pragma__('nokwargs')
1068 |
1069 | ## The 'skip' pragma tells the Transcrypt Python to JS transpiler to
1070 | ## ignore a section of code. It's needed here because the 'run as script'
1071 | ## idiom causes an error in Transcrypt and has no meaning in that context.
1072 | ## Putting the pragmas in comments means they'll be ignored and cause no
1073 | ## problems in a real python interpreter.
1074 |
1075 | # __pragma__ ('skip')
1076 | if __name__ == '__main__':
1077 | import doctest
1078 | doctest.testmod()
1079 | # __pragma__ ('noskip')
1080 |
--------------------------------------------------------------------------------
/htmltree/tests/client.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Description: Client side of sanity check
4 | Uses JS functions insertAdjacentHTML, innerHTML and addEventListener.
5 | See https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML
6 | https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
7 | https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
8 |
9 | Author: Mike Ellis
10 | Copyright 2017 Owner
11 | """
12 | from htmltree import *
13 | def start():
14 | console.log("Starting")
15 | ## insert a style element at the end of the content
21 | newcontent = Div(H1("Sanity check PASS", _class='test'))
22 | document.body.innerHTML = newcontent.render()
23 | console.log("Finished")
24 |
25 | ## JS is event driven.
26 | ## Wait for DOM load to complete before firing
27 | ## our start() function.
28 | document.addEventListener('DOMContentLoaded', start)
29 |
30 |
--------------------------------------------------------------------------------
/htmltree/tests/sanitycheck.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Description: Very basic test of htmltree under Cpython and Transcrypt.
4 | Runs doctests first. Proceeds only if all pass.
5 |
6 | Then creates a rudimentary html file that loads a JS file which replaces the
7 | body element with new content. Opens the html file in the default browser.
8 |
9 | Requires Transcrypt.
10 |
11 | Author: Mike Ellis
12 | Copyright 2017 Ellis & Grant, Inc.
13 | """
14 | import doctest
15 |
16 | if __name__ == '__main__':
17 | doctest.testmod()
18 | import subprocess
19 | import webbrowser
20 | import os
21 | import doctest
22 | import htmltree
23 | print("Running doctests ...")
24 | doctest_result = doctest.testmod(htmltree)
25 | if doctest_result.failed > 0:
26 | print("Failed {} doctests! Aborting.",format(doctest_result.failed))
27 | import sys
28 | sys.exit()
29 | else:
30 | print("Passed all of {} doctests.".format(doctest_result.attempted))
31 |
32 | from htmltree import *
33 |
34 | head = Head(Script(src="../__javascript__/client.js", charset="UTF-8"))
35 | body = Body(H1("Sanity check FAIL if this remains visible"))
36 | doc = Html(head, body)
37 |
38 | print("Writing sanitycheck.html ...")
39 | if not os.path.exists("__html__"):
40 | os.makedirs("__html__")
41 | fileurl = doc.renderToFile('__html__/sanitycheck.html')
42 |
43 | print("Building client.js")
44 | proc = subprocess.Popen('transcrypt -b -n -m -v client.py', shell=True)
45 | if proc.wait() != 0:
46 | raise Exception("Failed trying to build client.js")
47 |
48 | print("Build complete. Opening sanitycheck.html in browser ...")
49 | path = os.path.abspath("__html__/sanitycheck.html")
50 | webbrowser.open(fileurl)
51 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from distutils.core import setup
3 | with open('README.md', encoding="utf8") as file:
4 | long_description = file.read()
5 | setup(
6 | name = 'htmltree',
7 | packages = ['htmltree'], # this must be the same as the name above
8 | version = '0.7.6',
9 | description = 'Generalized nested html element tree with recursive rendering',
10 | long_description = long_description,
11 | author = 'Michael F. Ellis',
12 | author_email = 'michael.f.ellis@gmail.com',
13 | url = 'https://github.com/Michael-F-Ellis/htmltree', # use the URL to the github repo
14 | download_url = 'https://github.com/Michael-F-Ellis/htmltree/archive/0.7.5.tar.gz',
15 | keywords = ['html', 'css', 'Transcrypt', 'web app development'],
16 | classifiers = [],
17 | )
18 |
--------------------------------------------------------------------------------