html_compose.document

HTML5 document generation functions and classes.

The most featureful way to generate a full HTML5 document is to use the HTML5Document class with html_compose.resource types managing imports. The benefit of this is automatic and correct ordering of resources, preloads, and import mapping.

The most control over the document generation process is to use document_generator which yields parts of the document as strings.

Streaming alternatives are available via document_streamer, or HTML5Document.stream().

These will yield chunks as they are generated.

  1"""
  2HTML5 document generation functions and classes.
  3
  4The most featureful way to generate a full HTML5 document is to use
  5the `HTML5Document` class with `html_compose.resource` types managing
  6imports. The benefit of this is automatic and correct ordering of resources,
  7preloads, and import mapping.
  8
  9The most control over the document generation process is to use
 10`document_generator` which yields parts of the document as strings.
 11
 12Streaming alternatives are available via `document_streamer`, or
 13`HTML5Document.stream()`.
 14
 15These will yield chunks as they are generated.
 16"""
 17
 18import os
 19from typing import Any, Generator, Iterable, Literal, TypeAlias
 20from urllib.parse import urlencode
 21
 22from . import base_types, doctype, pretty_print, resource, unsafe_text
 23from . import elements as el
 24from .util_funcs import get_livereload_env
 25
 26Node: TypeAlias = base_types.Node
 27
 28
 29def generate_head(
 30    title: str | None = None,
 31    js: Iterable[str | resource.js_import] | None = None,
 32    css: Iterable[str | resource.css_import] | None = None,
 33    fonts: Iterable[resource.font_import_manual | resource.font_import_provider]
 34    | None = None,
 35    extra: Iterable[Node] | None = None,
 36    skip_meta: bool = False,
 37) -> el.head:
 38    """
 39    Generate a head element with common imports and arguments.
 40
 41    By default, this includes a viewport meta tag.
 42
 43    :param title: HTML document title
 44    :param js: A list of javascript imports to include in the head
 45    :param css: A list of CSS imports to include in the head
 46    :param fonts: A list of font imports to include in the head
 47    :param extra: Any extra elements to include at the end of the head
 48    :param skip_meta: Skip the meta viewport tag
 49
 50    :return: A head element with the specified imports and title
 51    """
 52    head_elements: list[Node] = resource.to_elements(
 53        js=js, css=css, fonts=fonts
 54    )
 55    if extra:
 56        head_elements.extend(extra)
 57
 58    return el.head()[
 59        el.meta(
 60            name="viewport", content="width=device-width, initial-scale=1.0"
 61        )
 62        if not skip_meta
 63        else None,
 64        el.title()[title] if title else None,
 65        head_elements,
 66    ]
 67
 68
 69def document_streamer(
 70    lang: str | None = None,
 71    head: Iterable[Node] | el.head | None = None,
 72    body: Iterable[Node] | el.body | None = None,
 73    stream_mode: Literal["head_only", "full"] = "head_only",
 74) -> Generator[str, Any, None]:
 75    """
 76    Return a full HTML5 document as a generator, yielding parts as strings.
 77
 78    stream_mode controls whether to yield just the head first, then body,
 79    or the full document in parts.
 80
 81    tldr:
 82    ```
 83      doctype("html")
 84      html(lang=lang)[
 85        head[
 86          meta(name="viewport", content="width=device-width, initial-scale=1.0")
 87          ...,
 88        ]
 89        body[body]]
 90    ```
 91
 92    When using livereload, an environment variable is set which adds
 93    livereload-js to the head of the document.
 94
 95    :param lang: The language of the document.
 96                 English is "en", or consult HTML documentation
 97    :param head: Children to add to the <head> element,
 98                 which already defines viewport.
 99                 A head element passed directly will be used unmodified.
100    :param body: A 'body' element or a list of children to add to the 'body' element
101    :param stream_mode: If set, return a generator that yields parts of the document.
102                        "head_only" yields the head, then full body,
103                        "full" yields the entire document in parts.
104
105    :return: A generator that yields parts of the HTML5 document as strings
106    """
107    # Enable HTML5 and prevent quirks mode
108    header = doctype("html")
109    if isinstance(head, el.head):
110        head_el = head
111    else:
112        head_el = generate_head(extra=head)
113    # None if disabled
114    live_reload_flags = get_livereload_env()
115    # Feature: Live reloading for development
116    # Fires when HTMLCOMPOSE_LIVERELOAD=1
117    if live_reload_flags:
118        head_el.append(_livereload_script_tag(live_reload_flags))
119    # Produce our HTML element and save its parts
120    html_el = el.html(lang=lang).resolve()
121    html_el_start = next(html_el)
122    html_el_end = next(html_el)
123    # Yield up until end of the head element
124    yield f"{header}\n{html_el_start}\n{head_el.render()}\n\n"
125
126    # Setup the body element
127    if isinstance(body, el.body):
128        body_el = body
129    else:
130        body_el = el.body()[body]
131    if stream_mode == "full":
132        # Resolve in pieces
133        for body_part in body_el.resolve():
134            yield body_part
135        yield "\n"
136        yield html_el_end
137    elif stream_mode == "head_only":
138        # Resolve all at once
139        yield f"{body_el.render()}\n{html_el_end}"
140    else:
141        raise ValueError("stream_mode must be 'head_only' or 'full'")
142
143
144def document_generator(
145    lang: str | None = None,
146    head: el.head | list | None = None,
147    body: Iterable[Node] | el.body | None = None,
148) -> str:
149    """
150    Return a full HTML5 document as a string.
151
152    tldr:
153    ```
154      doctype("html")
155      html(lang=lang)[
156        head[
157          meta(name="viewport", content="width=device-width, initial-scale=1.0")
158          title(title)
159        ]
160        body[body]]
161    ```
162
163    When using livereload, an environment variable is set which adds
164    livereload-js to the head of the document.
165
166
167    :param lang: The language of the document.
168                 English is "en", or consult HTML documentation
169    :param head: Children to add to the <head> element,
170                 which already defines viewport.
171                 A head element passed directly will be used unmodified.
172    :param body: A 'body' element or a list of children to add to the 'body' element
173
174    :return: A full HTML5 document as a string
175
176    """
177    return "".join(
178        document_streamer(lang=lang, head=head, body=body, stream_mode="full")
179    )
180
181
182def get_livereload_uri() -> str:
183    """
184    Return livereload-js compatible resource URI
185    But if the user wants they can override this function to return a local
186    resource i.e.
187
188    html_compose.document.get_live_reload_uri =
189      lambda: "mydomain.com/static/livereload.js";
190
191    Or just set env var HTMLCOMPOSE_LIVERELOAD_URL to the desired URL.
192
193    We default to livereload-morph, which fits our use case better.
194
195    """
196    env_url = os.getenv("HTMLCOMPOSE_LIVERELOAD_URL", None)
197    # allow user override.
198    if env_url:
199        return env_url
200    VERSION = "0.3.0"
201    # We ported from livereload-js to livereload-morph to better fit our use case
202    return f"cdn.jsdelivr.net/npm/livereload-morph@{VERSION}/dist/livereload-morph.js"
203
204
205def _livereload_script_tag(live_reload_settings):
206    """
207    Returns a script tag which injects livereload.js.
208    """
209    # Fires when HTMLCOMPOSE_LIVERELOAD=1
210    # Livereload: https://github.com/livereload/livereload-js
211    uri = get_livereload_uri()
212
213    proxy_uri = live_reload_settings["proxy_uri"]
214    proxy_host = live_reload_settings["proxy_host"]
215    if proxy_host:
216        # Websocket is behind a proxy, likely SSL
217        # Port isn't important for these but the URI is
218        if proxy_uri.startswith("/"):
219            proxy_uri = proxy_uri.lstrip("/")
220        uri_encoded_flags = urlencode({"host": proxy_host, "path": proxy_uri})
221    else:
222        # Regular development enviroment with no proxy. host:port will do.
223        host = live_reload_settings["host"]
224        port = live_reload_settings["port"]
225        uri_encoded_flags = urlencode(
226            {"host": host, "port": port, "verbose": True}
227        )
228
229    # This scriptlet auto-inserts the livereload script and detects protocol
230    return el.script()[
231        unsafe_text(
232            "\n".join(
233                [
234                    "(function(){",
235                    'var s = document.createElement("script");',
236                    f"s.src = location.protocol + '//{uri}?{uri_encoded_flags}';",
237                    "document.head.appendChild(s)",
238                    "})()",
239                ]
240            )
241        )
242    ]
243
244
245class HTML5Document:
246    """
247    A convenience class to generate a full HTML5 document.
248
249    Allows you to specify common elements like JavaScript and CSS imports,
250    as well as additional head content.
251
252    When using livereload, an environment variable is set which adds
253    livereload-js to the head of the document.
254    """
255
256    def __init__(
257        self,
258        title: str | None = None,
259        lang: str | None = None,
260        js: Iterable[str | resource.js_import] | None = None,
261        css: Iterable[str | resource.css_import] | None = None,
262        fonts: Iterable[
263            resource.font_import_manual | resource.font_import_provider
264        ]
265        | None = None,
266        head_extra: Iterable[Node] | None = None,
267        body: Iterable[Node] | el.body | None = None,
268    ) -> None:
269        """
270
271        :param title: The title of the document
272        :param lang: The language of the document.
273                     English is "en", or consult HTML documentation
274        :param js: A list of javascript imports to include in the head
275        :param css: A list of CSS imports to include in the head
276        :param fonts: A list of font imports to include in the head
277        :param head_extra: Additional elements to include in the head
278        :param body: A 'body' element or a list of elements to include in the body
279        :param stream_mode: If set, return a generator that yields parts of the document.
280                            "head_only" yields the head, then full body,
281                            "full" yields the entire document in parts.
282        """
283        self.title = title
284        self.lang = lang
285        self.js = js
286        self.css = css
287        self.fonts = fonts
288        self.head_extra = head_extra
289        if isinstance(body, el.body):
290            self.body = body
291        else:
292            self.body = el.body()[body]
293
294    def render(self) -> str:
295        """
296        Return the full HTML5 document as a string.
297        """
298        return "".join(self.stream(stream_mode="full"))
299
300    def stream(
301        self, stream_mode: Literal["head_only", "full"] = "head_only"
302    ) -> Generator[str, Any, None]:
303        """
304        Return a generator that yields parts of the HTML5 document as strings.
305
306        :param stream_mode: Parts of the document to stream. If "head_only",
307                            we yield the head, then the full body.
308                            If "full", we yield the entire document in parts.
309
310        :return: A generator that yields parts of the HTML5 document as strings.
311        """
312        return document_streamer(
313            lang=self.lang,
314            head=generate_head(
315                title=self.title,
316                js=self.js,
317                css=self.css,
318                fonts=self.fonts,
319                extra=self.head_extra,
320            ),
321            body=self.body,
322            stream_mode=stream_mode,
323        )
324
325    def __html__(self) -> str:
326        return self.render()
327
328    def __str__(self) -> str:
329        return self.render()
330
331    def __repr__(self) -> str:
332        return pretty_print(str(self))
Node: TypeAlias = Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]
def generate_head( title: str | None = None, js: Optional[Iterable[str | html_compose.resource.js_import]] = None, css: Optional[Iterable[str | html_compose.resource.css_import]] = None, fonts: Optional[Iterable[html_compose.resource.font_import_manual | html_compose.resource.font_import_provider]] = None, extra: Optional[Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]]] = None, skip_meta: bool = False) -> html_compose.elements.head_element.head:
30def generate_head(
31    title: str | None = None,
32    js: Iterable[str | resource.js_import] | None = None,
33    css: Iterable[str | resource.css_import] | None = None,
34    fonts: Iterable[resource.font_import_manual | resource.font_import_provider]
35    | None = None,
36    extra: Iterable[Node] | None = None,
37    skip_meta: bool = False,
38) -> el.head:
39    """
40    Generate a head element with common imports and arguments.
41
42    By default, this includes a viewport meta tag.
43
44    :param title: HTML document title
45    :param js: A list of javascript imports to include in the head
46    :param css: A list of CSS imports to include in the head
47    :param fonts: A list of font imports to include in the head
48    :param extra: Any extra elements to include at the end of the head
49    :param skip_meta: Skip the meta viewport tag
50
51    :return: A head element with the specified imports and title
52    """
53    head_elements: list[Node] = resource.to_elements(
54        js=js, css=css, fonts=fonts
55    )
56    if extra:
57        head_elements.extend(extra)
58
59    return el.head()[
60        el.meta(
61            name="viewport", content="width=device-width, initial-scale=1.0"
62        )
63        if not skip_meta
64        else None,
65        el.title()[title] if title else None,
66        head_elements,
67    ]

Generate a head element with common imports and arguments.

By default, this includes a viewport meta tag.

Parameters
  • title: HTML document title
  • js: A list of javascript imports to include in the head
  • css: A list of CSS imports to include in the head
  • fonts: A list of font imports to include in the head
  • extra: Any extra elements to include at the end of the head
  • skip_meta: Skip the meta viewport tag
Returns

A head element with the specified imports and title

def document_streamer( lang: str | None = None, head: Union[Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]], html_compose.elements.head_element.head, NoneType] = None, body: Union[Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]], html_compose.elements.body_element.body, NoneType] = None, stream_mode: Literal['head_only', 'full'] = 'head_only') -> Generator[str, Any, NoneType]:
 70def document_streamer(
 71    lang: str | None = None,
 72    head: Iterable[Node] | el.head | None = None,
 73    body: Iterable[Node] | el.body | None = None,
 74    stream_mode: Literal["head_only", "full"] = "head_only",
 75) -> Generator[str, Any, None]:
 76    """
 77    Return a full HTML5 document as a generator, yielding parts as strings.
 78
 79    stream_mode controls whether to yield just the head first, then body,
 80    or the full document in parts.
 81
 82    tldr:
 83    ```
 84      doctype("html")
 85      html(lang=lang)[
 86        head[
 87          meta(name="viewport", content="width=device-width, initial-scale=1.0")
 88          ...,
 89        ]
 90        body[body]]
 91    ```
 92
 93    When using livereload, an environment variable is set which adds
 94    livereload-js to the head of the document.
 95
 96    :param lang: The language of the document.
 97                 English is "en", or consult HTML documentation
 98    :param head: Children to add to the <head> element,
 99                 which already defines viewport.
100                 A head element passed directly will be used unmodified.
101    :param body: A 'body' element or a list of children to add to the 'body' element
102    :param stream_mode: If set, return a generator that yields parts of the document.
103                        "head_only" yields the head, then full body,
104                        "full" yields the entire document in parts.
105
106    :return: A generator that yields parts of the HTML5 document as strings
107    """
108    # Enable HTML5 and prevent quirks mode
109    header = doctype("html")
110    if isinstance(head, el.head):
111        head_el = head
112    else:
113        head_el = generate_head(extra=head)
114    # None if disabled
115    live_reload_flags = get_livereload_env()
116    # Feature: Live reloading for development
117    # Fires when HTMLCOMPOSE_LIVERELOAD=1
118    if live_reload_flags:
119        head_el.append(_livereload_script_tag(live_reload_flags))
120    # Produce our HTML element and save its parts
121    html_el = el.html(lang=lang).resolve()
122    html_el_start = next(html_el)
123    html_el_end = next(html_el)
124    # Yield up until end of the head element
125    yield f"{header}\n{html_el_start}\n{head_el.render()}\n\n"
126
127    # Setup the body element
128    if isinstance(body, el.body):
129        body_el = body
130    else:
131        body_el = el.body()[body]
132    if stream_mode == "full":
133        # Resolve in pieces
134        for body_part in body_el.resolve():
135            yield body_part
136        yield "\n"
137        yield html_el_end
138    elif stream_mode == "head_only":
139        # Resolve all at once
140        yield f"{body_el.render()}\n{html_el_end}"
141    else:
142        raise ValueError("stream_mode must be 'head_only' or 'full'")

Return a full HTML5 document as a generator, yielding parts as strings.

stream_mode controls whether to yield just the head first, then body, or the full document in parts.

tldr:

  doctype("html")
  html(lang=lang)[
    head[
      meta(name="viewport", content="width=device-width, initial-scale=1.0")
      ...,
    ]
    body[body]]

When using livereload, an environment variable is set which adds livereload-js to the head of the document.

Parameters
  • lang: The language of the document. English is "en", or consult HTML documentation
  • head: Children to add to the element, which already defines viewport. A head element passed directly will be used unmodified.
  • body: A 'body' element or a list of children to add to the 'body' element
  • stream_mode: If set, return a generator that yields parts of the document. "head_only" yields the head, then full body, "full" yields the entire document in parts.
Returns

A generator that yields parts of the HTML5 document as strings

def document_generator( lang: str | None = None, head: html_compose.elements.head_element.head | list | None = None, body: Union[Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]], html_compose.elements.body_element.body, NoneType] = None) -> str:
145def document_generator(
146    lang: str | None = None,
147    head: el.head | list | None = None,
148    body: Iterable[Node] | el.body | None = None,
149) -> str:
150    """
151    Return a full HTML5 document as a string.
152
153    tldr:
154    ```
155      doctype("html")
156      html(lang=lang)[
157        head[
158          meta(name="viewport", content="width=device-width, initial-scale=1.0")
159          title(title)
160        ]
161        body[body]]
162    ```
163
164    When using livereload, an environment variable is set which adds
165    livereload-js to the head of the document.
166
167
168    :param lang: The language of the document.
169                 English is "en", or consult HTML documentation
170    :param head: Children to add to the <head> element,
171                 which already defines viewport.
172                 A head element passed directly will be used unmodified.
173    :param body: A 'body' element or a list of children to add to the 'body' element
174
175    :return: A full HTML5 document as a string
176
177    """
178    return "".join(
179        document_streamer(lang=lang, head=head, body=body, stream_mode="full")
180    )

Return a full HTML5 document as a string.

tldr:

  doctype("html")
  html(lang=lang)[
    head[
      meta(name="viewport", content="width=device-width, initial-scale=1.0")
      title(title)
    ]
    body[body]]

When using livereload, an environment variable is set which adds livereload-js to the head of the document.

Parameters
  • lang: The language of the document. English is "en", or consult HTML documentation
  • head: Children to add to the element, which already defines viewport. A head element passed directly will be used unmodified.
  • body: A 'body' element or a list of children to add to the 'body' element
Returns

A full HTML5 document as a string

def get_livereload_uri() -> str:
183def get_livereload_uri() -> str:
184    """
185    Return livereload-js compatible resource URI
186    But if the user wants they can override this function to return a local
187    resource i.e.
188
189    html_compose.document.get_live_reload_uri =
190      lambda: "mydomain.com/static/livereload.js";
191
192    Or just set env var HTMLCOMPOSE_LIVERELOAD_URL to the desired URL.
193
194    We default to livereload-morph, which fits our use case better.
195
196    """
197    env_url = os.getenv("HTMLCOMPOSE_LIVERELOAD_URL", None)
198    # allow user override.
199    if env_url:
200        return env_url
201    VERSION = "0.3.0"
202    # We ported from livereload-js to livereload-morph to better fit our use case
203    return f"cdn.jsdelivr.net/npm/livereload-morph@{VERSION}/dist/livereload-morph.js"

Return livereload-js compatible resource URI But if the user wants they can override this function to return a local resource i.e.

html_compose.document.get_live_reload_uri = lambda: "mydomain.com/static/livereload.js";

Or just set env var HTMLCOMPOSE_LIVERELOAD_URL to the desired URL.

We default to livereload-morph, which fits our use case better.

class HTML5Document:
246class HTML5Document:
247    """
248    A convenience class to generate a full HTML5 document.
249
250    Allows you to specify common elements like JavaScript and CSS imports,
251    as well as additional head content.
252
253    When using livereload, an environment variable is set which adds
254    livereload-js to the head of the document.
255    """
256
257    def __init__(
258        self,
259        title: str | None = None,
260        lang: str | None = None,
261        js: Iterable[str | resource.js_import] | None = None,
262        css: Iterable[str | resource.css_import] | None = None,
263        fonts: Iterable[
264            resource.font_import_manual | resource.font_import_provider
265        ]
266        | None = None,
267        head_extra: Iterable[Node] | None = None,
268        body: Iterable[Node] | el.body | None = None,
269    ) -> None:
270        """
271
272        :param title: The title of the document
273        :param lang: The language of the document.
274                     English is "en", or consult HTML documentation
275        :param js: A list of javascript imports to include in the head
276        :param css: A list of CSS imports to include in the head
277        :param fonts: A list of font imports to include in the head
278        :param head_extra: Additional elements to include in the head
279        :param body: A 'body' element or a list of elements to include in the body
280        :param stream_mode: If set, return a generator that yields parts of the document.
281                            "head_only" yields the head, then full body,
282                            "full" yields the entire document in parts.
283        """
284        self.title = title
285        self.lang = lang
286        self.js = js
287        self.css = css
288        self.fonts = fonts
289        self.head_extra = head_extra
290        if isinstance(body, el.body):
291            self.body = body
292        else:
293            self.body = el.body()[body]
294
295    def render(self) -> str:
296        """
297        Return the full HTML5 document as a string.
298        """
299        return "".join(self.stream(stream_mode="full"))
300
301    def stream(
302        self, stream_mode: Literal["head_only", "full"] = "head_only"
303    ) -> Generator[str, Any, None]:
304        """
305        Return a generator that yields parts of the HTML5 document as strings.
306
307        :param stream_mode: Parts of the document to stream. If "head_only",
308                            we yield the head, then the full body.
309                            If "full", we yield the entire document in parts.
310
311        :return: A generator that yields parts of the HTML5 document as strings.
312        """
313        return document_streamer(
314            lang=self.lang,
315            head=generate_head(
316                title=self.title,
317                js=self.js,
318                css=self.css,
319                fonts=self.fonts,
320                extra=self.head_extra,
321            ),
322            body=self.body,
323            stream_mode=stream_mode,
324        )
325
326    def __html__(self) -> str:
327        return self.render()
328
329    def __str__(self) -> str:
330        return self.render()
331
332    def __repr__(self) -> str:
333        return pretty_print(str(self))

A convenience class to generate a full HTML5 document.

Allows you to specify common elements like JavaScript and CSS imports, as well as additional head content.

When using livereload, an environment variable is set which adds livereload-js to the head of the document.

HTML5Document( title: str | None = None, lang: str | None = None, js: Optional[Iterable[str | html_compose.resource.js_import]] = None, css: Optional[Iterable[str | html_compose.resource.css_import]] = None, fonts: Optional[Iterable[html_compose.resource.font_import_manual | html_compose.resource.font_import_provider]] = None, head_extra: Optional[Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]]] = None, body: Union[Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], Union[NoneType, str, int, float, bool, html_compose.base_types.ElementBase, html_compose.base_types._HasHtml, Iterable[ForwardRef('Node')], Callable[[], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase], ForwardRef('Node')], Callable[[html_compose.base_types.ElementBase, html_compose.base_types.ElementBase], ForwardRef('Node')]]]]], html_compose.elements.body_element.body, NoneType] = None)
257    def __init__(
258        self,
259        title: str | None = None,
260        lang: str | None = None,
261        js: Iterable[str | resource.js_import] | None = None,
262        css: Iterable[str | resource.css_import] | None = None,
263        fonts: Iterable[
264            resource.font_import_manual | resource.font_import_provider
265        ]
266        | None = None,
267        head_extra: Iterable[Node] | None = None,
268        body: Iterable[Node] | el.body | None = None,
269    ) -> None:
270        """
271
272        :param title: The title of the document
273        :param lang: The language of the document.
274                     English is "en", or consult HTML documentation
275        :param js: A list of javascript imports to include in the head
276        :param css: A list of CSS imports to include in the head
277        :param fonts: A list of font imports to include in the head
278        :param head_extra: Additional elements to include in the head
279        :param body: A 'body' element or a list of elements to include in the body
280        :param stream_mode: If set, return a generator that yields parts of the document.
281                            "head_only" yields the head, then full body,
282                            "full" yields the entire document in parts.
283        """
284        self.title = title
285        self.lang = lang
286        self.js = js
287        self.css = css
288        self.fonts = fonts
289        self.head_extra = head_extra
290        if isinstance(body, el.body):
291            self.body = body
292        else:
293            self.body = el.body()[body]
Parameters
  • title: The title of the document
  • lang: The language of the document. English is "en", or consult HTML documentation
  • js: A list of javascript imports to include in the head
  • css: A list of CSS imports to include in the head
  • fonts: A list of font imports to include in the head
  • head_extra: Additional elements to include in the head
  • body: A 'body' element or a list of elements to include in the body
  • stream_mode: If set, return a generator that yields parts of the document. "head_only" yields the head, then full body, "full" yields the entire document in parts.
title
lang
js
css
fonts
head_extra
def render(self) -> str:
295    def render(self) -> str:
296        """
297        Return the full HTML5 document as a string.
298        """
299        return "".join(self.stream(stream_mode="full"))

Return the full HTML5 document as a string.

def stream( self, stream_mode: Literal['head_only', 'full'] = 'head_only') -> Generator[str, Any, NoneType]:
301    def stream(
302        self, stream_mode: Literal["head_only", "full"] = "head_only"
303    ) -> Generator[str, Any, None]:
304        """
305        Return a generator that yields parts of the HTML5 document as strings.
306
307        :param stream_mode: Parts of the document to stream. If "head_only",
308                            we yield the head, then the full body.
309                            If "full", we yield the entire document in parts.
310
311        :return: A generator that yields parts of the HTML5 document as strings.
312        """
313        return document_streamer(
314            lang=self.lang,
315            head=generate_head(
316                title=self.title,
317                js=self.js,
318                css=self.css,
319                fonts=self.fonts,
320                extra=self.head_extra,
321            ),
322            body=self.body,
323            stream_mode=stream_mode,
324        )

Return a generator that yields parts of the HTML5 document as strings.

Parameters
  • stream_mode: Parts of the document to stream. If "head_only", we yield the head, then the full body. If "full", we yield the entire document in parts.
Returns

A generator that yields parts of the HTML5 document as strings.