from IPython.display import Markdown
from pprint import pprint
from fastcore.test import test_eq, test_neXML
ft(tag, *children, **attrs) builds an FT tree for HTML generation. to_xml renders the tree. FastHTML uses these functions to generate HTML.
Common tags have constructors such as Div, P and A. Pass attributes as keywords, using klass for class and fr for for. Underscores in attribute names become hyphens. You can also get and set attributes on the element.
To supply children after attributes, call the element: Div(id='x')(child1, child2).
FT functions
FT
def FT(
tag:str, cs:tuple, attrs:dict=None, void_:bool=False, **kwargs
):A ‘Fast Tag’ structure, containing tag,children,and attrs
ft
def ft(
tag:str, *c, void_:bool=False, attrmap:<built-in function callable>=attrmap,
valmap:<built-in function callable>=valmap, ft_cls:type=FT, **kw
):The main HTML tags are exported as ft partials.
Attributes are passed as keywords. Use ‘klass’ and ‘fr’ instead of ‘class’ and ‘for’, to avoid Python reserved word clashes.
Html
def Html(
*c, doctype:bool=True, **kwargs
)->__main__.FT:An HTML tag, optionally preceeded by !DOCTYPE HTML
samp = Html(
Head(Title('Some page')),
Body(Div('Some text\nanother line', (Input(name="jph's"), Img(src="filename", data=1)),
cls=['myclass', 'another'],
style={'padding':1, 'margin':2}))
)
pprint(samp)(!doctype((),{'html': True}),
html((head((title(('Some page',),{}),),{}), body((div(('Some text\nanother line', input((),{'name': "jph's"}), img((),{'src': 'filename', 'data': 1})),{'class': 'myclass another', 'style': 'padding:1; margin:2'}),),{})),{}))
elem = P('Some text', id="myid")
print(elem.tag)
print(elem.children)
print(elem.attrs)p
('Some text',)
{'id': 'myid'}
You can get and set attrs directly:
elem.id = 'newid'
print(elem.id, elem.get('id'), elem.get('foo', 'missing'))
elemnewid newid missing
p(('Some text',),{'id': 'newid'})
Safe
def Safe(
*args, **kwargs
):str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’.
Conversion to XML/HTML
to_xml
def to_xml(
*elms, lvl:int=0, indent:bool=True, do_escape:bool=True
):Convert ft element tree into an XML string
t = Div(P('hi', klass='a'), hx_get='/get')
test_eq(to_xml(t), '<div hx-get="/get">\n <p class="a">hi</p>\n</div>\n')
test_eq(to_xml(Div(id='x')('child')), '<div id="x">child</div>\n')test_eq(to_xml(B('Bold Text'), Div(P('Paragraph Text'))),
'<b>Bold Text</b>\n<div>\n <p>Paragraph Text</p>\n</div>\n')to_xml(Div("<script>alert('XSS')</script>"), do_escape=True)"<div><script>alert('XSS')</script></div>\n"
h = to_xml(samp, do_escape=False)
print(h)<!doctype html>
<html>
<head>
<title>Some page</title>
</head>
<body>
<div class="myclass another" style="padding:1; margin:2">
Some text
another line <input name="jph's">
<img src="filename" data="1"> </div>
</body>
</html>
c = I('hello')
print(c)<i>hello</i>
cclass PageTitle:
def __ft__(self): return H1("Hello")
class HomePage:
def __ft__(self): return Div(PageTitle(), Div('hello'))
h = to_xml(Div(HomePage()))
expected_output = """<div>
<div>
<h1>Hello</h1>
<div>hello</div>
</div>
</div>
"""
assert h == expected_outputprint(h)<div>
<div>
<h1>Hello</h1>
<div>hello</div>
</div>
</div>
h = to_xml(samp, indent=False)
print(h)<!doctype html><html><head><title>Some page</title></head><body><div class="myclass another" style="padding:1; margin:2">Some text
another line<input name="jph's"><img src="filename" data="1"></div></body></html>
Interoperability both directions with Django and Jinja using the html() protocol:
def _esc(s): return s.__html__() if hasattr(s, '__html__') else Safe(escape(s))
r = Safe('<b>Hello from Django</b>')
print(to_xml(Div(r)))
print(_esc(Div(P('Hello from fastcore <3'))))<div><b>Hello from Django</b></div>
<div><p>Hello from fastcore <3</p></div>
Attribute values can be FT elements. to_xml renders those values as markup:
print(to_xml(P('hi', value=Div('ho'))))<p value="<div>ho</div>">hi</p>
FT components also stringify with to_xml:
print(Div('ho'))<div>ho</div>
FT object equality and hashing is based on tag, attrs, and children.
test_eq(Div('hello', id='x'), Div('hello', id='x'))
test_ne(Div('hello'), Div('goodbye'))
test_ne(Div('hello', id='a'), Div('hello', id='b'))
test_ne(P('hello'), Div('hello'))
test_eq(hash(Div('hello', id='x')), hash(Div('hello', id='x')))
assert hash(Div('hello')), hash(Div('goodbye'))dict2xml
def dict2xml(
d, do_escape:bool=False, unwrap:NoneType=None
):Convert d to XML tags, one per key/value pair, unwrapping if only single key in unwrap exists
dict2xml converts dictionary keys to tags and values to text content:
d = {'name': 'Jeremy', 'lang': 'Python'}
print(dict2xml(d))<name>Jeremy</name>
<lang>Python</lang>
dict2xml returns the value without a tag when the dictionary has one key and that key matches unwrap:
d = {'name': 'Jeremy'}
print(dict2xml(d, unwrap='name'))Jeremy
Display
highlight
def highlight(
s, lang:str='html'
):Markdown to syntax-highlight s in language lang
Call an element to add children after its attributes:
hl_md(
Body(klass='myclass')(
Div(style='padding:3px')(
'Some text 1<2',
I(spurious=True)('in italics'),
Input(name='me'),
Img(src="filename", data=1)
)
))<body class="myclass"><div style="padding:3px">Some text 1<2<i spurious>in italics</i><input name="me"><img src="filename" data="1"></div></body>mk_getattr
def mk_getattr(
f
):Create a module __getattr__ for mapping undefined attributes to kebab-case HTML tags via factory f
from fastcore.xml import Foo_bar,Foo_Bar,BarBaz,BarBAZImport an otherwise undefined capitalized name to create a tag constructor. Underscores in that name become hyphens:
print(Foo_bar())
print(Foo_Bar())<foo-bar></foo-bar>
<foo-bar></foo-bar>
CamelCase names become lowercase tags with hyphens between words:
print(BarBaz())<bar-baz></bar-baz>
Namespace-aware XML
E creates XML elements from Python calls. Its ns argument maps namespace prefixes to URIs. The prefix and attr_ns arguments set the default prefixes for element and attribute names.
For example, e = E('w', attr_ns='w', ns={'w': uri}) uses w for both defaults. Calling e.tcW(type='dxa', w=2400) creates a w:tcW element with w:type and w:w attributes.
E preserves the case of tag and attribute names. Use prefix__name for an explicitly prefixed attribute, or attrs_ for literal attribute names.
Nesting an element under a different parent does not change its namespace bindings.
XML serialization escapes text and writes booleans as true or false. Override attr_value for other conventions. .bytes() returns UTF-8. ft and to_xml retain their HTML behavior.
from xml.etree.ElementTree import fromstring
from fastcore.test import expect_failXML collects children in order at construction time and omits None. Text stays separate from markup.
XML
def XML(
tag, children, attrs, ns
):A detached XML expression with fixed namespace bindings.
A child keeps its namespace bindings when nested under a parent with different bindings. Serialization declares the child’s changed bindings on that child.
str(expr) and expr.bytes() serialize a standalone document. To insert the markup into an existing document, pass its namespace bindings to expr.render(inherited).
render({}) declares the expression’s bindings. For an unqualified element, this includes xmlns="". That declaration prevents the destination’s default namespace from applying to the element.
XML.render
def render(
inherited:NoneType=None
):Serialize, declaring the bindings not already in scope: inherited maps the prefixes bound where the markup will be inserted, {} declares every binding, and None renders a standalone document
An E factory stores separate defaults for element and attribute prefixes. WordprocessingML uses w for both. The DrawingML example uses prefixed elements with unqualified attributes. Configure these defaults on the factory without supplying a schema.
E
def E(
prefix:str='', # Default element prefix; empty uses the default namespace
*, attr_ns:NoneType=None, # Default attribute prefix; None leaves attributes unqualified
ns:NoneType=None, # Prefix-to-URI bindings, including prefixes used only in values
):Create a namespace-bound XML factory: e.tag(...) and e('tag', ...) build detached expressions
E() creates unqualified XML. Attribute access supplies a tag name, and keyword arguments supply attributes. An expression’s repr shows its markup. .bytes() returns UTF-8. An unqualified root has no namespace declaration:
plain = E()
note = plain.Note('Please bring tea & biscuits.', priority=2, signed=False, draft=None)
parsed_note = fromstring(note.bytes())
test_eq(parsed_note.tag, 'Note')
test_eq(parsed_note.attrib, {'priority': '2', 'signed': 'false'})
test_eq(parsed_note.text, 'Please bring tea & biscuits.')
test_eq(repr(note), str(note))
assert 'xmlns' not in str(note)
note<Note xmlns="" priority="2" signed="false">Please bring tea & biscuits.</Note>Pass children individually or in lists, tuples and generators. None omits a child. Calling an expression appends children and returns the same expression.
Adding children consumes generators and preserves their order. Later serializations reuse the collected children:
notes = plain.Notes(note)
assert notes((plain.Note(t) for t in ['Bring cups.', 'And a teapot.']), None) is notes
test_eq([n.text for n in fromstring(notes.bytes())],
['Please bring tea & biscuits.', 'Bring cups.', 'And a teapot.'])
test_eq(notes.bytes(), notes.bytes())
notes<Notes xmlns=""><Note priority="2" signed="false">Please bring tea & biscuits.</Note><Note>Bring cups.</Note><Note>And a teapot.</Note></Notes>Element and attribute namespaces
An XML prefix names a namespace URI. WordprocessingML qualifies both element and attribute names with w. Configure those two defaults once; subsequent expressions use ordinary Python names. Case is preserved.
word_ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
rel_ns = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
e = E('w', attr_ns='w', ns={'w': word_ns, 'r': rel_ns})
width = e.tcW(type='dxa', w=2400)
parsed_width = fromstring(width.bytes())
test_eq(parsed_width.tag, f'{{{word_ns}}}tcW')
test_eq(parsed_width.attrib, {f'{{{word_ns}}}type': 'dxa', f'{{{word_ns}}}w': '2400'})
test_eq(width.attrs, {'w:type': 'dxa', 'w:w': '2400'})
width<w:tcW xmlns="" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" w:type="dxa" w:w="2400"/>Other vocabularies use unqualified attributes on qualified elements. Leaving attr_ns unset gives that behavior. An explicit prefix uses __ in a keyword: r__embed becomes r:embed. The xml prefix is predefined.
drawing_ns = 'http://schemas.openxmlformats.org/drawingml/2006/main'
a = E('a', ns={'a': drawing_ns, 'r': rel_ns})
picture = a.blip(r__embed='rId7', cstate='print')
test_eq(fromstring(picture.bytes()).attrib, {f'{{{rel_ns}}}embed': 'rId7', 'cstate': 'print'})
paragraph = e.p(e.r(e.t(' Open ', xml__space='preserve')),
e.hyperlink(e.r(e.t('the picture')), r__id='rId7'))
test_eq(''.join(fromstring(paragraph.bytes()).itertext()), ' Open the picture')
paragraph<w:p xmlns="" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:r><w:t xml:space="preserve"> Open </w:t></w:r><w:hyperlink r:id="rId7"><w:r><w:t>the picture</w:t></w:r></w:hyperlink></w:p>Literal names and namespace scope
For Python keywords, add a trailing underscore: class_ becomes class, and e.del_() creates w:del. Other underscores remain literal.
Use attrs_ to supply exact attribute names. These names bypass attr_ns, including when they have no prefix.
Pass a string to the factory for tags such as e('custom-name', ...) or an explicitly prefixed tag. An explicit tag prefix does not change the attribute default.
marker = e('custom-name', name='intro', class_='chapter', source_id='s1',
attrs_={'plain': 'unqualified', 'w:custom-name': 'literal'})
marker_attrs = fromstring(marker.bytes()).attrib
test_eq(marker_attrs['plain'], 'unqualified')
test_eq([marker_attrs[f'{{{word_ns}}}{k}'] for k in ['name', 'class', 'source_id', 'custom-name']],
['intro', 'chapter', 's1', 'literal'])
test_eq(fromstring(e.del_(e.r(e.delText('old')), id=7).bytes()).tag, f'{{{word_ns}}}del')
marker<w:custom-name xmlns="" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" w:name="intro" w:class="chapter" w:source_id="s1" plain="unqualified" w:custom-name="literal"/>ns={'': 'urn:catalog'} sets the default namespace. Use '', not None, as the mapping key. The default namespace applies to elements, not unqualified attributes.
Here Entry uses the catalog namespace. The nested plain.Note still has no namespace. Bindings supplied in ns are declared even when no element or attribute name uses them. The example declares kind because it appears in ns, although it is used only in the attribute value kind:Personal.
catalog = E(ns={'': 'urn:catalog', 'kind': 'urn:note-types'})
entry = catalog.Entry(plain.Note('An unqualified child'), kind='kind:Personal')
parsed_entry = fromstring(entry.bytes())
test_eq(parsed_entry.tag, '{urn:catalog}Entry')
test_eq(parsed_entry[0].tag, 'Note')
test_eq(parsed_entry.attrib, {'kind': 'kind:Personal'})
assert 'xmlns' not in plain.Note('x').render()
assert 'xmlns=""' in plain.Note('x').render({}) and 'xmlns=""' in plain.Note('x').render({'': 'urn:catalog'})
entry<Entry xmlns="urn:catalog" xmlns:kind="urn:note-types" kind="kind:Personal"><Note xmlns="">An unqualified child</Note></Entry>A reused prefix can mean different things in different expressions. The child declares its own binding rather than taking its parent’s meaning.
other = E('w', ns={'w': 'urn:another-vocabulary'})
mixed = e.p(other.item(), e.r(e.t('Still WordprocessingML')))
parsed_mixed = fromstring(mixed.bytes())
test_eq([c.tag for c in parsed_mixed], ['{urn:another-vocabulary}item', f'{{{word_ns}}}r'])
mixed<w:p xmlns="" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:item xmlns:w="urn:another-vocabulary"/><w:r><w:t>Still WordprocessingML</w:t></w:r></w:p>Text is text
Serialization escapes markup in string children, including Safe strings. XML rejects byte-string children.
Serialization adds no indentation or line breaks. Carriage returns in text and whitespace in attributes survive parsing.
spacing = plain.Note(Safe('<em>not markup</em>'), '\rkeep this return', label='one\t"two"\nthree')
parsed_spacing = fromstring(spacing.bytes())
test_eq(parsed_spacing.text, '<em>not markup</em>\rkeep this return')
test_eq(parsed_spacing.get('label'), 'one\t"two"\nthree')
with expect_fail(TypeError): plain.Note(b'<raw/>')
spacing<Note xmlns="" label='one	"two" three'><em>not markup</em> keep this return</Note>E checks namespace bindings when you create a factory. When you create an element, it checks element and attribute names and rejects unknown prefixes with ValueError. Put namespace declarations in ns, not in the attributes. These checks do not validate the document against a schema.
with expect_fail(ValueError, contains='Unknown XML prefix'): plain('missing:Note')
with expect_fail(ValueError, contains='Invalid XML name'): plain('bad name')
with expect_fail(ValueError, contains='Illegal XML character'): plain.Note('\x00')
with expect_fail(ValueError, contains='namespace binding'): E(ns={'xml': 'urn:wrong'})
with expect_fail(TypeError): E(ns={None: 'urn:catalog'})Two prefixes for the same URI still name the same attributes. Supplying an attribute twice is an error, including a duplicate supplied through attrs_.
aliases = E('w', attr_ns='w', ns={'w': word_ns, 'word': word_ns})
with expect_fail(ValueError, contains='Duplicate XML attribute'):
aliases.tcW(w=2400, attrs_={'word:w': 1200})Attribute values are serialized by attr_value. A vocabulary with its own conventions overrides it. WordprocessingML’s on/off elements are one example: their val takes on or off, not true or false.
class OnOffE(E):
def attr_value(self, tag, name, value):
if isinstance(value, bool): return 'on' if value else 'off'
return super().attr_value(tag, name, value)
onoff = OnOffE('w', attr_ns='w', ns={'w': word_ns})
test_eq(fromstring(onoff.cantSplit(val=False).bytes()).attrib, {f'{{{word_ns}}}val': 'off'})
onoff.tblHeader(val=True)