html_compose.resource
Resource import helper classes
For javascript:
- Manage preload
- Manage import maps
- Manage cache busting for local resources
- Manage SRI hashes
For css:
- Manage preload
- Manage cache busting for local resources
- Manage SRI hashes
For fonts:
- Manage preconnect
- Manage preload
- Manage css @font-face generation or importing from providers i.e. Google Fonts
1""" 2Resource import helper classes 3 4For javascript: 5* Manage preload 6* Manage import maps 7* Manage cache busting for local resources 8* Manage SRI hashes 9 10For css: 11* Manage preload 12* Manage cache busting for local resources 13* Manage SRI hashes 14 15For fonts: 16* Manage preconnect 17* Manage preload 18* Manage css @font-face generation or importing from providers i.e. Google Fonts 19 20""" 21 22import json 23from typing import Any, Iterable 24 25from .. import base_types, unsafe_text 26from .. import elements as el 27 28 29class settings: 30 """ 31 Global settings for js_import/css_import behavior. 32 33 `base_dir` is base directory from which local static files are served 34 and is used to construct cache busting URLs. 35 """ 36 37 # Base directory when resolving relative paths for local resources 38 base_dir = "." 39 # html-compose cache-buster timestamp 40 query_string = "hccbts" 41 # Maximum number of cached URIs for cache busting 42 cache_cap = 1000 43 stat_poll_interval: int | float = 1 # seconds 44 45 46class _State: 47 """ 48 Internal state for local static resource imports 49 """ 50 51 stat_cache: dict[str, int | float] = {} 52 53 misc_stat_cache: dict[str, int | float] = {} 54 55 56from .css_import import css_import # noqa: E402 57from .font_import import font_import_manual, font_import_provider # noqa: E402 58from .js_import import js_import # noqa: E402 59 60 61def to_elements( 62 js: Iterable[str | js_import] | None = None, 63 css: Iterable[str | css_import] | None = None, 64 fonts: Iterable[font_import_manual | font_import_provider] | None = None, 65): 66 """ 67 Generate elements for `head` element from resource imports 68 69 Depending on your use case consider caching the resolution of this function 70 71 72 :param js: Javascript imports. A string is treated as a simple script src 73 :param css: CSS imports. A string is treated as a simple link rel=stylesheet 74 :param fonts: Font imports 75 """ 76 head_elements: list[base_types.Node] = [] 77 78 preconnect_links = [] 79 preload_links = [] 80 links = [] 81 if css: 82 for css_resource in css: 83 if isinstance(css_resource, css_import): 84 for link in css_resource.links(): 85 links.append(link) 86 for preload in css_resource.preloads(): 87 preload_links.append(preload) 88 else: 89 if not isinstance(css_resource, str): 90 raise TypeError("css must be str or css_import") 91 92 links.append(el.link(rel="stylesheet", href=css_resource)) 93 94 script_tags = [] 95 if js: 96 # <link rel="modulepreload" href="main.js" /> 97 js_imports: dict[str, str] = {} 98 scopes: dict[str, dict[str, str]] = {} 99 for js_src in js: 100 if isinstance(js_src, js_import): 101 jsi: js_import = js_src 102 # Generate script tag 103 script_tags.append(jsi.script()) 104 link = jsi.preload_link() 105 if link: 106 preload_links.append(link) 107 108 entry = jsi.import_map_entry() 109 if entry: 110 # Add to import map 111 name, src = entry.name, entry.src 112 js_imports[name] = src 113 if entry.scope_urls: 114 for scope_key in entry.scope_urls: 115 sdict = scopes.setdefault(scope_key, {}) 116 sdict[name] = src 117 118 else: 119 if not isinstance(js_src, str): 120 raise TypeError("js must be str or js_import") 121 script_tags.append(el.script(src=js_src)) 122 123 if js_imports: 124 import_map: dict[str, Any] = {"imports": js_imports} 125 if scopes: 126 import_map["scopes"] = scopes 127 # dump to json; prevent escapes 128 js_dump = json.dumps(import_map).replace("<", "\\u003c") 129 preload_links.insert( 130 0, el.script(type="importmap")[unsafe_text(js_dump)] 131 ) 132 style_text: list[str] = [] 133 if fonts: 134 for font in fonts: 135 # Each font may be a different kind but the interface means 136 # we capture anything they generate 137 links.extend(font.links()) 138 preconnect_links.extend(font.preconnect_links()) 139 preload_links.extend(font.preload_links()) 140 style_data = font.style_data() 141 if style_data: 142 style_text.append(style_data) 143 144 if preconnect_links: 145 head_elements.extend(preconnect_links) 146 147 if preload_links: 148 head_elements.extend(preload_links) 149 150 if links: 151 head_elements.extend(links) 152 153 if style_text: 154 head_elements.append(el.style()[unsafe_text("\n".join(style_text))]) 155 156 if script_tags: 157 head_elements.extend(script_tags) 158 159 return head_elements 160 161 162__all__ = [ 163 "js_import", 164 "css_import", 165 "font_import_manual", 166 "font_import_provider", 167 "to_elements", 168 "settings", 169]
19class js_import: 20 """ 21 A javascript import helper class which employs one or more of: 22 23 - `script(` to define the import 24 - `importmap` which maps javascript module names to URLs 25 Setting the name parameter like so 26 ```python 27 js_import( 28 "https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm", 29 name="alpinejs") 30 ``` 31 creates an import map so javascript modules can be imported by name 32 - `link(rel="modulepreload")` to preload the import 33 - if cache_bust is true, appends a timestamp to the URL to 34 35 An production usage might look like: 36 37 ```python 38 [ 39 js_import('./static/admin.js', 40 name='admin', 41 cache_bust=True), 42 43 js_import( 44 "https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm", 45 name="alpinejs", 46 preload=True, 47 hash=( 48 "sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/" 49 "9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p" 50 ), 51 crossorigin="anonymous" 52 ), 53 54 ] 55 ``` 56 57 ... 58 59 ## Usage in window: 60 61 ```python 62 script(type="module")[ 63 ''' 64 import Alpine from 'alpinejs' 65 window.Alpine = Alpine 66 Alpine.start() 67 ''' 68 ] 69 ``` 70 71 ## Generated "HTML": 72 73 ```python 74 import json 75 from html_compose import el, unsafe_text 76 77 [ 78 el.script(type="importmap")[ 79 unsafe_text('{"imports": {"admin": "./static/admin.js?ts=1760157623", "alpinejs": "https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm"}}') 80 ], 81 82 el.link(href="./static/admin.js?ts=1760157623", rel=["modulepreload"]), 83 84 85 el.link( 86 href="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm", 87 integrity="sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p", 88 crossorigin="anonymous", 89 rel=["modulepreload"], 90 ), 91 92 el.script(src="./static/admin.js?ts=1760157623", type="module"), 93 94 el.script( 95 src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm", 96 type="module", 97 integrity="sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p", 98 crossorigin="anonymous", 99 ), 100 ] 101 ``` 102 103 See: 104 - https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap 105 - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules 106 - https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/modulepreload 107 - https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload 108 - https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity 109 - https://www.srihash.org/ 110 111 """ 112 113 def __init__( 114 self, 115 source: str, 116 name: str | None = None, 117 preload: bool = False, 118 hash: str | None = None, 119 async_: bool = False, 120 defer: bool = False, 121 nomodule: bool = False, 122 scope_url: str | Iterable[str] | None = None, 123 crossorigin: Literal["", "anonymous", "user-credentials"] 124 | str 125 | None = None, 126 cache_bust: bool = False, 127 ): 128 """ 129 A javascript import wrapper to manage optimal window import strategies. 130 131 If name is set, the import is added to an import map and imported 132 as a module. 133 134 Parameters 135 ---------- 136 `source`: 137 The literal src passed to tags i.e. script and link preload. 138 139 If cache_bust is set, a timestamp is appended to the URL. 140 `name`: 141 The name of the import, e.g. "lodash" which can be used in 142 javascript type="module" imports. 143 144 Doing this automatically changes the script type to module 145 146 `preload`: 147 The modulepreload keyword, for the rel attribute of the `<link/>` 148 element, provides a declarative way to preemptively fetch a module 149 script, parse and compile it, and store it in the document's module 150 map for later execution. 151 152 If this is not a javascript module (i.e. name is not set), 153 the rel attribute is set to "preload" and the as_ attribute 154 is set to "script" instead. 155 156 `async_`: 157 The async attribute causes the script to be executed asynchronously as soon as it is available, 158 without blocking HTML parsing. 159 160 `defer`: 161 Defers script execution until document parsing is complete. 162 Has no effect on module scripts (they are deferred by default). 163 164 `nomodule`: 165 Added to script tag. 166 167 Indicates that the script should not be executed in browsers that 168 support ES modules — in effect, this can be used to serve fallback 169 scripts to older browsers that do not support javascript modules. 170 171 `hash`: 172 An optional SRI integrity hash for the import 173 174 `crossorigin`: 175 Optionally sets the crossorigin attribute on the script tag. 176 Valid values are "", "anonymous", "use-credentials" 177 178 `cache_bust`: 179 If true, appends a timestamp to the URL to work with browser 180 caching. Webservers configured for static resources manage this 181 feature automatically, but for development this can be useful. 182 183 This feature only works for local resources, i.e. those that 184 exist relative to `resource.settings.base_dir` 185 186 `scope_url`: 187 Optional route or list of paths where this import should be 188 included. 189 190 Scopes let you have different mappings for different parts of your 191 app. 192 193 Because they affect the import map but not what is loaded - 194 the script tag controls what loads - they are rarely used. 195 196 """ 197 198 self.name = name 199 self.source = source 200 self.preload = preload 201 self.hash = hash 202 self.scope_urls = scope_url 203 if scope_url: 204 if isinstance(scope_url, str): 205 self.scope_urls = [scope_url] 206 elif is_iterable_but_not_str(scope_url): 207 # Materialize scope URLs so they can be iterated multiple times 208 # (flatten_iterable returns a generator by default). 209 self.scope_urls = list(flatten_iterable(scope_url)) 210 else: 211 self.scope_urls = [str(scope_url)] 212 self.crossorigin = crossorigin 213 self.cache_bust = cache_bust 214 self.has_link = preload 215 self.async_ = async_ 216 self.defer = defer 217 self.nomodule = nomodule 218 self._src = self.uri() 219 if hash and self.crossorigin is None: 220 raise ValueError( 221 "If hash is set, crossorigin must be set to ''/'anonymous'" 222 ) 223 224 def uri(self) -> str: 225 """ 226 Returns the source URI - with cache busting if enabled 227 which is implemented by getting the mtime of the local file 228 229 This operation performs up to two fs stats per call 230 231 1. The base static directory is checked once and cached per interpreter 232 2. The specific resource is checked if it is time to poll again 233 based on settings.stat_poll_interval 234 """ 235 if not self.cache_bust: 236 return self.source 237 238 return _cachebust_resource_uri(self.source) 239 240 def import_map_entry(self) -> _ImportMapEntry | None: 241 """ 242 Returns a tuple of (name, source, scope_url) for use in an import map 243 """ 244 245 if self.name: 246 if self.scope_urls is None: 247 return _ImportMapEntry(name=self.name, src=self._src) 248 else: 249 return _ImportMapEntry( 250 name=self.name, src=self._src, scope_urls=self.scope_urls 251 ) 252 253 return None 254 255 def preload_link(self) -> el.link | None: 256 """ 257 Returns one or more link element for this import 258 """ 259 if self.preload: 260 attrs = {"href": self._src} 261 if self.hash: 262 attrs["integrity"] = self.hash 263 if self.crossorigin: 264 attrs["crossorigin"] = self.crossorigin 265 if self.name: 266 return el.link(attrs=attrs, rel="modulepreload") 267 else: 268 return el.link(attrs=attrs, rel="preload", as_="script") 269 270 return None 271 272 def script(self) -> el.script: 273 """ 274 Returns a script tag for this import 275 """ 276 attrs: dict[str, str | bool] = {"src": self._src} 277 278 if self.name: 279 attrs["type"] = "module" 280 if self.hash: 281 attrs["integrity"] = self.hash 282 if self.crossorigin: 283 attrs["crossorigin"] = self.crossorigin 284 if self.async_: 285 attrs["async"] = True 286 if self.defer and not self.name: 287 # Only set defer for non-modules 288 attrs["defer"] = True 289 if self.nomodule: 290 attrs["nomodule"] = True 291 292 return el.script(attrs=attrs)
A javascript import helper class which employs one or more of:
script(to define the importimportmapwhich maps javascript module names to URLs Setting the name parameter like so
js_import(
"https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm",
name="alpinejs")
creates an import map so javascript modules can be imported by name
link(rel="modulepreload")to preload the import- if cache_bust is true, appends a timestamp to the URL to
An production usage might look like:
[
js_import('./static/admin.js',
name='admin',
cache_bust=True),
js_import(
"https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm",
name="alpinejs",
preload=True,
hash=(
"sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/"
"9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p"
),
crossorigin="anonymous"
),
]
...
Usage in window:
script(type="module")[
'''
import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start()
'''
]
Generated "HTML":
import json
from html_compose import el, unsafe_text
[
el.script(type="importmap")[
unsafe_text('{"imports": {"admin": "./static/admin.js?ts=1760157623", "alpinejs": "https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm"}}')
],
el.link(href="./static/admin.js?ts=1760157623", rel=["modulepreload"]),
el.link(
href="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm",
integrity="sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p",
crossorigin="anonymous",
rel=["modulepreload"],
),
el.script(src="./static/admin.js?ts=1760157623", type="module"),
el.script(
src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/+esm",
type="module",
integrity="sha384-Yf57wlxlrA1+0X6Ye9NOBxQ1tpmiwI/9mFpv9tT/Rh2UAajwwAlTWHnvTGYhgv7p",
crossorigin="anonymous",
),
]
See:
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/modulepreload
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload
- https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
- https://www.srihash.org/
113 def __init__( 114 self, 115 source: str, 116 name: str | None = None, 117 preload: bool = False, 118 hash: str | None = None, 119 async_: bool = False, 120 defer: bool = False, 121 nomodule: bool = False, 122 scope_url: str | Iterable[str] | None = None, 123 crossorigin: Literal["", "anonymous", "user-credentials"] 124 | str 125 | None = None, 126 cache_bust: bool = False, 127 ): 128 """ 129 A javascript import wrapper to manage optimal window import strategies. 130 131 If name is set, the import is added to an import map and imported 132 as a module. 133 134 Parameters 135 ---------- 136 `source`: 137 The literal src passed to tags i.e. script and link preload. 138 139 If cache_bust is set, a timestamp is appended to the URL. 140 `name`: 141 The name of the import, e.g. "lodash" which can be used in 142 javascript type="module" imports. 143 144 Doing this automatically changes the script type to module 145 146 `preload`: 147 The modulepreload keyword, for the rel attribute of the `<link/>` 148 element, provides a declarative way to preemptively fetch a module 149 script, parse and compile it, and store it in the document's module 150 map for later execution. 151 152 If this is not a javascript module (i.e. name is not set), 153 the rel attribute is set to "preload" and the as_ attribute 154 is set to "script" instead. 155 156 `async_`: 157 The async attribute causes the script to be executed asynchronously as soon as it is available, 158 without blocking HTML parsing. 159 160 `defer`: 161 Defers script execution until document parsing is complete. 162 Has no effect on module scripts (they are deferred by default). 163 164 `nomodule`: 165 Added to script tag. 166 167 Indicates that the script should not be executed in browsers that 168 support ES modules — in effect, this can be used to serve fallback 169 scripts to older browsers that do not support javascript modules. 170 171 `hash`: 172 An optional SRI integrity hash for the import 173 174 `crossorigin`: 175 Optionally sets the crossorigin attribute on the script tag. 176 Valid values are "", "anonymous", "use-credentials" 177 178 `cache_bust`: 179 If true, appends a timestamp to the URL to work with browser 180 caching. Webservers configured for static resources manage this 181 feature automatically, but for development this can be useful. 182 183 This feature only works for local resources, i.e. those that 184 exist relative to `resource.settings.base_dir` 185 186 `scope_url`: 187 Optional route or list of paths where this import should be 188 included. 189 190 Scopes let you have different mappings for different parts of your 191 app. 192 193 Because they affect the import map but not what is loaded - 194 the script tag controls what loads - they are rarely used. 195 196 """ 197 198 self.name = name 199 self.source = source 200 self.preload = preload 201 self.hash = hash 202 self.scope_urls = scope_url 203 if scope_url: 204 if isinstance(scope_url, str): 205 self.scope_urls = [scope_url] 206 elif is_iterable_but_not_str(scope_url): 207 # Materialize scope URLs so they can be iterated multiple times 208 # (flatten_iterable returns a generator by default). 209 self.scope_urls = list(flatten_iterable(scope_url)) 210 else: 211 self.scope_urls = [str(scope_url)] 212 self.crossorigin = crossorigin 213 self.cache_bust = cache_bust 214 self.has_link = preload 215 self.async_ = async_ 216 self.defer = defer 217 self.nomodule = nomodule 218 self._src = self.uri() 219 if hash and self.crossorigin is None: 220 raise ValueError( 221 "If hash is set, crossorigin must be set to ''/'anonymous'" 222 )
A javascript import wrapper to manage optimal window import strategies.
If name is set, the import is added to an import map and imported as a module.
Parameters
source:
The literal src passed to tags i.e. script and link preload.
If cache_bust is set, a timestamp is appended to the URL.
name:
The name of the import, e.g. "lodash" which can be used in
javascript type="module" imports.
Doing this automatically changes the script type to module
preload:
The modulepreload keyword, for the rel attribute of the <link/>
element, provides a declarative way to preemptively fetch a module
script, parse and compile it, and store it in the document's module
map for later execution.
If this is not a javascript module (i.e. name is not set),
the rel attribute is set to "preload" and the as_ attribute
is set to "script" instead.
async_:
The async attribute causes the script to be executed asynchronously as soon as it is available,
without blocking HTML parsing.
defer:
Defers script execution until document parsing is complete.
Has no effect on module scripts (they are deferred by default).
nomodule:
Added to script tag.
Indicates that the script should not be executed in browsers that
support ES modules — in effect, this can be used to serve fallback
scripts to older browsers that do not support javascript modules.
hash:
An optional SRI integrity hash for the import
crossorigin:
Optionally sets the crossorigin attribute on the script tag.
Valid values are "", "anonymous", "use-credentials"
cache_bust:
If true, appends a timestamp to the URL to work with browser
caching. Webservers configured for static resources manage this
feature automatically, but for development this can be useful.
This feature only works for local resources, i.e. those that
exist relative to `resource.settings.base_dir`
scope_url:
Optional route or list of paths where this import should be
included.
Scopes let you have different mappings for different parts of your
app.
Because they affect the import map but not what is loaded -
the script tag controls what loads - they are rarely used.
224 def uri(self) -> str: 225 """ 226 Returns the source URI - with cache busting if enabled 227 which is implemented by getting the mtime of the local file 228 229 This operation performs up to two fs stats per call 230 231 1. The base static directory is checked once and cached per interpreter 232 2. The specific resource is checked if it is time to poll again 233 based on settings.stat_poll_interval 234 """ 235 if not self.cache_bust: 236 return self.source 237 238 return _cachebust_resource_uri(self.source)
Returns the source URI - with cache busting if enabled which is implemented by getting the mtime of the local file
This operation performs up to two fs stats per call
- The base static directory is checked once and cached per interpreter
- The specific resource is checked if it is time to poll again based on settings.stat_poll_interval
240 def import_map_entry(self) -> _ImportMapEntry | None: 241 """ 242 Returns a tuple of (name, source, scope_url) for use in an import map 243 """ 244 245 if self.name: 246 if self.scope_urls is None: 247 return _ImportMapEntry(name=self.name, src=self._src) 248 else: 249 return _ImportMapEntry( 250 name=self.name, src=self._src, scope_urls=self.scope_urls 251 ) 252 253 return None
Returns a tuple of (name, source, scope_url) for use in an import map
255 def preload_link(self) -> el.link | None: 256 """ 257 Returns one or more link element for this import 258 """ 259 if self.preload: 260 attrs = {"href": self._src} 261 if self.hash: 262 attrs["integrity"] = self.hash 263 if self.crossorigin: 264 attrs["crossorigin"] = self.crossorigin 265 if self.name: 266 return el.link(attrs=attrs, rel="modulepreload") 267 else: 268 return el.link(attrs=attrs, rel="preload", as_="script") 269 270 return None
Returns one or more link element for this import
272 def script(self) -> el.script: 273 """ 274 Returns a script tag for this import 275 """ 276 attrs: dict[str, str | bool] = {"src": self._src} 277 278 if self.name: 279 attrs["type"] = "module" 280 if self.hash: 281 attrs["integrity"] = self.hash 282 if self.crossorigin: 283 attrs["crossorigin"] = self.crossorigin 284 if self.async_: 285 attrs["async"] = True 286 if self.defer and not self.name: 287 # Only set defer for non-modules 288 attrs["defer"] = True 289 if self.nomodule: 290 attrs["nomodule"] = True 291 292 return el.script(attrs=attrs)
Returns a script tag for this import
8class css_import: 9 """ 10 A css import helper class which employs: 11 - `link(rel="stylesheet")` to define the import 12 - `link(rel="preload")` to preload the import 13 - `hash` and `crossorigin` for SRI 14 - Local resource cache busting 15 16 An production usage might look like: 17 ```python 18 [ 19 css_import('./static/admin.css', 20 cache_bust=True), 21 22 css_import( 23 "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css", 24 hash=( 25 "sha384-9ndCyUaIbzAi2FUVXJi0CjmCapS" 26 "mO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" 27 ), 28 crossorigin="anonymous", 29 preload=True 30 ) 31 32 ] 33 ``` 34 35 ## Generated "HTML": 36 37 ```python 38 import json 39 from html_compose import el 40 41 [ 42 link(rel='preload', 43 href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css', 44 as_='style', 45 integrity='sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7' 46 'SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM', 47 crossorigin='anonymous'), 48 49 link(rel='stylesheet', href='./static/admin.css?ts=1760153426'), 50 51 link(rel='stylesheet', 52 href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css', 53 integrity='sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7' 54 'SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM', 55 crossorigin='anonymous') 56 ] 57 ``` 58 59 See: 60 https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload 61 https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity 62 https://www.srihash.org/ 63 64 """ 65 66 def __init__( 67 self, 68 href: str, 69 preload: bool = False, 70 hash: str | None = None, 71 crossorigin: Literal["", "anonymous", "user-credentials"] 72 | str 73 | None = None, 74 cache_bust: bool = False, 75 ): 76 """ 77 A css import wrapper to wrap some of the complexity of optimal 78 resource loading. 79 80 81 Parameters 82 ---------- 83 `href`: 84 The literal href passed to the link tag 85 86 `preload`: 87 If true, adds a preload link for this resource 88 89 `hash`: 90 An optional SRI integrity hash for the import 91 92 `crossorigin`: 93 Optionally sets the crossorigin attribute on the link tag. 94 Valid values are "", "anonymous", "use-credentials" 95 96 `cache_bust`: 97 If true, appends a timestamp to the URL to prevent browser 98 caching. Webservers configured for static resources manage this 99 feature automatically, but for development this can be useful. 100 101 This feature only works for local resources, i.e. those that 102 exist in `resource.settings.base_dir` 103 104 """ 105 self.href = href 106 self.preload = preload 107 self.hash = hash 108 self.crossorigin = crossorigin 109 self.cache_bust = cache_bust 110 self.has_link = preload 111 self._href = self.uri() 112 113 if hash and self.crossorigin is None: 114 raise ValueError( 115 "If hash is set, crossorigin must be set to ''/'anonymous'" 116 ) 117 118 def uri(self): 119 """ 120 Returns the source URI - with cache busting if enabled 121 which is implemented by getting the mtime of the local file 122 123 This operation performs up to two fs stats per call 124 125 1. The base static directory is checked once and cached per interpreter 126 2. The specific resource is checked if it is time to poll again 127 based on settings.stat_poll_interval 128 """ 129 if not self.cache_bust: 130 return self.href 131 132 return _cachebust_resource_uri(self.href) 133 134 def preloads(self) -> list[el.link]: 135 """ 136 Returns a link element for preloading this import if preload is set 137 """ 138 attrs = {"href": self._href} 139 if self.hash: 140 attrs["integrity"] = self.hash 141 if self.crossorigin: 142 attrs["crossorigin"] = self.crossorigin 143 if self.preload: 144 return [el.link(attrs=attrs, rel="preload", as_="style")] 145 146 return [] 147 148 def links(self): 149 """ 150 Returns one or more link element for this import 151 """ 152 links = [] 153 attrs = {"href": self._href} 154 if self.hash: 155 attrs["integrity"] = self.hash 156 if self.crossorigin: 157 attrs["crossorigin"] = self.crossorigin 158 159 links.append(el.link(attrs=attrs, rel="stylesheet")) 160 161 return links
A css import helper class which employs:
link(rel="stylesheet")to define the importlink(rel="preload")to preload the importhashandcrossoriginfor SRI- Local resource cache busting
An production usage might look like:
[
css_import('./static/admin.css',
cache_bust=True),
css_import(
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css",
hash=(
"sha384-9ndCyUaIbzAi2FUVXJi0CjmCapS"
"mO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
),
crossorigin="anonymous",
preload=True
)
]
Generated "HTML":
import json
from html_compose import el
[
link(rel='preload',
href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css',
as_='style',
integrity='sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7'
'SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM',
crossorigin='anonymous'),
link(rel='stylesheet', href='./static/admin.css?ts=1760153426'),
link(rel='stylesheet',
href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css',
integrity='sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7'
'SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM',
crossorigin='anonymous')
]
See: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity https://www.srihash.org/
66 def __init__( 67 self, 68 href: str, 69 preload: bool = False, 70 hash: str | None = None, 71 crossorigin: Literal["", "anonymous", "user-credentials"] 72 | str 73 | None = None, 74 cache_bust: bool = False, 75 ): 76 """ 77 A css import wrapper to wrap some of the complexity of optimal 78 resource loading. 79 80 81 Parameters 82 ---------- 83 `href`: 84 The literal href passed to the link tag 85 86 `preload`: 87 If true, adds a preload link for this resource 88 89 `hash`: 90 An optional SRI integrity hash for the import 91 92 `crossorigin`: 93 Optionally sets the crossorigin attribute on the link tag. 94 Valid values are "", "anonymous", "use-credentials" 95 96 `cache_bust`: 97 If true, appends a timestamp to the URL to prevent browser 98 caching. Webservers configured for static resources manage this 99 feature automatically, but for development this can be useful. 100 101 This feature only works for local resources, i.e. those that 102 exist in `resource.settings.base_dir` 103 104 """ 105 self.href = href 106 self.preload = preload 107 self.hash = hash 108 self.crossorigin = crossorigin 109 self.cache_bust = cache_bust 110 self.has_link = preload 111 self._href = self.uri() 112 113 if hash and self.crossorigin is None: 114 raise ValueError( 115 "If hash is set, crossorigin must be set to ''/'anonymous'" 116 )
A css import wrapper to wrap some of the complexity of optimal resource loading.
Parameters
href:
The literal href passed to the link tag
preload:
If true, adds a preload link for this resource
hash:
An optional SRI integrity hash for the import
crossorigin:
Optionally sets the crossorigin attribute on the link tag.
Valid values are "", "anonymous", "use-credentials"
cache_bust:
If true, appends a timestamp to the URL to prevent browser
caching. Webservers configured for static resources manage this
feature automatically, but for development this can be useful.
This feature only works for local resources, i.e. those that
exist in `resource.settings.base_dir`
118 def uri(self): 119 """ 120 Returns the source URI - with cache busting if enabled 121 which is implemented by getting the mtime of the local file 122 123 This operation performs up to two fs stats per call 124 125 1. The base static directory is checked once and cached per interpreter 126 2. The specific resource is checked if it is time to poll again 127 based on settings.stat_poll_interval 128 """ 129 if not self.cache_bust: 130 return self.href 131 132 return _cachebust_resource_uri(self.href)
Returns the source URI - with cache busting if enabled which is implemented by getting the mtime of the local file
This operation performs up to two fs stats per call
- The base static directory is checked once and cached per interpreter
- The specific resource is checked if it is time to poll again based on settings.stat_poll_interval
134 def preloads(self) -> list[el.link]: 135 """ 136 Returns a link element for preloading this import if preload is set 137 """ 138 attrs = {"href": self._href} 139 if self.hash: 140 attrs["integrity"] = self.hash 141 if self.crossorigin: 142 attrs["crossorigin"] = self.crossorigin 143 if self.preload: 144 return [el.link(attrs=attrs, rel="preload", as_="style")] 145 146 return []
Returns a link element for preloading this import if preload is set
148 def links(self): 149 """ 150 Returns one or more link element for this import 151 """ 152 links = [] 153 attrs = {"href": self._href} 154 if self.hash: 155 attrs["integrity"] = self.hash 156 if self.crossorigin: 157 attrs["crossorigin"] = self.crossorigin 158 159 links.append(el.link(attrs=attrs, rel="stylesheet")) 160 161 return links
Returns one or more link element for this import
45class font_import_manual(_font_import_base): 46 def __init__( 47 self, 48 hrefs: str | Iterable[str], 49 family: str, 50 weight: int 51 | tuple 52 | str 53 | Literal["bold", "light", "lighter", "bolder", "normal"] 54 | None = "normal", 55 style: Literal["normal", "italic", "oblique"] | str = "normal", 56 display: Literal["swap", "optional", "fallback", "auto", "block"] 57 | str = "swap", 58 preload: bool = True, 59 crossorigin: Literal["", "anonymous", "use-credentials"] | None = None, 60 unicode_range: str | None = None, 61 cache_bust: bool = False, 62 ) -> None: 63 """ 64 Declare a web font via @font-face and (optionally) emit 65 a preload `<link>`. 66 This initializer targets the direct-file flow (you supply the exact .woff2 URL). 67 68 If multiple hrefs are provided, preload must choose only one and so 69 the first href is used for preload and the rest are only 70 in the @font-face. 71 72 Values are passed verbatim. NEVER use this with untrusted user input. 73 74 Parameters 75 ---------- 76 77 `hrefs`: 78 1 or more URL to the font file (typically .woff2). 79 Pass a str for a single URL, or an iterable of str for multiple URLs. 80 81 The @font-face `src` will reference this URL verbatim. 82 83 `family`: 84 CSS `font-family` name to expose (e.g., "Noto Sans"). 85 Do not include quotes; they are added automatically. 86 87 `weight`: 88 Single numeric weight (e.g., 400) for a static face, or a `(low, high)` 89 tuple (e.g., `(100, 900)`) for a variable font range. 90 91 `style`: 92 Font style for this face: `"normal"` or `"italic"`. Declare another 93 instance for the other style if needed. 94 95 `display`: 96 `font-display` strategy. `"swap"` is a safe default to avoid FOIT. 97 98 `preload`: 99 If `True`, also create a `<link rel="preload" as="font" ...>` for `href`. 100 Preload only warms the fetch; the @font-face rule still does the actual use. 101 102 `crossorigin`: 103 CORS mode for cross-origin fonts. When unset, it is 104 fetched as same-origin Use `""` or `anonymous` for most CDN/remote 105 cases, or `"use-credentials"` to pass cookies if strictly necessary. 106 107 `unicode_range`: 108 Optional CSS `unicode-range` (e.g., `"U+0000-00FF"`) to subset coverage. 109 """ 110 if isinstance(hrefs, str): 111 hrefs = [hrefs] 112 self.hrefs = hrefs 113 self.weight = weight 114 self.style = style 115 self.display = display 116 self.preload = preload 117 self.crossorigin = crossorigin 118 self.cache_bust = cache_bust 119 self.has_link = preload 120 self.unicode_range = unicode_range 121 self._hrefs = self.uris() 122 123 # Escape most stuff that could break out of quotes 124 safe_family = ( 125 family.replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"') 126 ) 127 self.family = safe_family 128 129 def uris(self) -> list[str]: # -> list[str] | list[Any]: 130 """ 131 Returns the source URI - with cache busting if enabled 132 which is implemented by getting the mtime of the local file 133 134 This operation performs up to two fs stats per call 135 136 1. The base static directory is checked once and cached per interpreter 137 2. The specific resource is checked if it is time to poll again 138 based on settings.stat_poll_interval 139 """ 140 hrefs = [] 141 for h in self.hrefs: 142 if not self.cache_bust: 143 hrefs.append(h) 144 continue 145 146 hrefs.append(_cachebust_resource_uri(h)) 147 return hrefs 148 149 @staticmethod 150 def get_font_format(href: str) -> str | None: 151 ext = href.split(".")[-1].lower() 152 return _AUTODETECT_CSS_FONT_TYPES.get(ext, None) 153 154 @staticmethod 155 def get_font_mime(href: str) -> str | None: 156 ext = href.split(".")[-1].lower() 157 return _AUTODETECT_FONT_MIMETYPES.get(ext, None) 158 159 def preload_links(self) -> list[el.link]: 160 """ 161 Returns preload links if preload is set 162 """ 163 if not self.preload: 164 return [] 165 166 first_href = self._hrefs[0] 167 attrs = {"href": first_href} 168 169 if self.crossorigin: 170 attrs["crossorigin"] = self.crossorigin 171 172 mimetype = font_import_manual.get_font_mime(first_href) 173 174 if mimetype: 175 attrs["type"] = mimetype 176 177 return [el.link(attrs=attrs, rel="preload", as_="font")] 178 179 def links(self) -> list[el.link]: 180 return [] 181 182 def style_data(self) -> str: 183 """ 184 Returns one or more nodes for this import 185 """ 186 187 font_refs = [] 188 189 if len(self._hrefs) == 1: 190 # we don't have to hint format if there's only one 191 font_refs.append(f"url('{self._hrefs[0]}')") 192 else: 193 for h in self._hrefs: 194 entry = f"url('{h}')" 195 format = font_import_manual.get_font_format(h) 196 if format: 197 entry += f" format('{format}')" 198 199 font_refs.append(entry) 200 201 if isinstance(self.weight, tuple): 202 font_weight = f"{self.weight[0]} {self.weight[1]}" 203 elif isinstance(self.weight, str): 204 font_weight = self.weight 205 elif isinstance(self.weight, int): 206 font_weight = str(self.weight) 207 else: 208 raise TypeError("font weight must be int, str, or tuple") 209 210 style_attrs = { 211 "font-family": f"'{self.family}'", 212 "src": ",\n\t\t".join(font_refs), 213 "font-style": self.style, 214 "font-display": self.display, 215 "font-weight": font_weight, 216 "unicode-range": self.unicode_range, 217 } 218 for k in list(style_attrs.keys()): 219 v = style_attrs[k] 220 if v is None: 221 del style_attrs[k] 222 223 return "\n".join( 224 [ 225 "@font-face {", 226 "".join( 227 [ 228 "\t", 229 ";\n\t".join( 230 [f"{k}: {v}" for k, v in style_attrs.items()] 231 ), 232 ";", # Ensure last property ends with semicolon 233 ] 234 ), 235 "}", 236 ] 237 ) 238 239 def preconnect_links(self) -> list[el.link]: 240 return []
46 def __init__( 47 self, 48 hrefs: str | Iterable[str], 49 family: str, 50 weight: int 51 | tuple 52 | str 53 | Literal["bold", "light", "lighter", "bolder", "normal"] 54 | None = "normal", 55 style: Literal["normal", "italic", "oblique"] | str = "normal", 56 display: Literal["swap", "optional", "fallback", "auto", "block"] 57 | str = "swap", 58 preload: bool = True, 59 crossorigin: Literal["", "anonymous", "use-credentials"] | None = None, 60 unicode_range: str | None = None, 61 cache_bust: bool = False, 62 ) -> None: 63 """ 64 Declare a web font via @font-face and (optionally) emit 65 a preload `<link>`. 66 This initializer targets the direct-file flow (you supply the exact .woff2 URL). 67 68 If multiple hrefs are provided, preload must choose only one and so 69 the first href is used for preload and the rest are only 70 in the @font-face. 71 72 Values are passed verbatim. NEVER use this with untrusted user input. 73 74 Parameters 75 ---------- 76 77 `hrefs`: 78 1 or more URL to the font file (typically .woff2). 79 Pass a str for a single URL, or an iterable of str for multiple URLs. 80 81 The @font-face `src` will reference this URL verbatim. 82 83 `family`: 84 CSS `font-family` name to expose (e.g., "Noto Sans"). 85 Do not include quotes; they are added automatically. 86 87 `weight`: 88 Single numeric weight (e.g., 400) for a static face, or a `(low, high)` 89 tuple (e.g., `(100, 900)`) for a variable font range. 90 91 `style`: 92 Font style for this face: `"normal"` or `"italic"`. Declare another 93 instance for the other style if needed. 94 95 `display`: 96 `font-display` strategy. `"swap"` is a safe default to avoid FOIT. 97 98 `preload`: 99 If `True`, also create a `<link rel="preload" as="font" ...>` for `href`. 100 Preload only warms the fetch; the @font-face rule still does the actual use. 101 102 `crossorigin`: 103 CORS mode for cross-origin fonts. When unset, it is 104 fetched as same-origin Use `""` or `anonymous` for most CDN/remote 105 cases, or `"use-credentials"` to pass cookies if strictly necessary. 106 107 `unicode_range`: 108 Optional CSS `unicode-range` (e.g., `"U+0000-00FF"`) to subset coverage. 109 """ 110 if isinstance(hrefs, str): 111 hrefs = [hrefs] 112 self.hrefs = hrefs 113 self.weight = weight 114 self.style = style 115 self.display = display 116 self.preload = preload 117 self.crossorigin = crossorigin 118 self.cache_bust = cache_bust 119 self.has_link = preload 120 self.unicode_range = unicode_range 121 self._hrefs = self.uris() 122 123 # Escape most stuff that could break out of quotes 124 safe_family = ( 125 family.replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"') 126 ) 127 self.family = safe_family
Declare a web font via @font-face and (optionally) emit
a preload <link>.
This initializer targets the direct-file flow (you supply the exact .woff2 URL).
If multiple hrefs are provided, preload must choose only one and so the first href is used for preload and the rest are only in the @font-face.
Values are passed verbatim. NEVER use this with untrusted user input.
Parameters
hrefs:
1 or more URL to the font file (typically .woff2).
Pass a str for a single URL, or an iterable of str for multiple URLs.
The @font-face `src` will reference this URL verbatim.
family:
CSS font-family name to expose (e.g., "Noto Sans").
Do not include quotes; they are added automatically.
weight:
Single numeric weight (e.g., 400) for a static face, or a (low, high)
tuple (e.g., (100, 900)) for a variable font range.
style:
Font style for this face: "normal" or "italic". Declare another
instance for the other style if needed.
display:
font-display strategy. "swap" is a safe default to avoid FOIT.
preload:
If True, also create a <link rel="preload" as="font" ...> for href.
Preload only warms the fetch; the @font-face rule still does the actual use.
crossorigin:
CORS mode for cross-origin fonts. When unset, it is
fetched as same-origin Use "" or anonymous for most CDN/remote
cases, or "use-credentials" to pass cookies if strictly necessary.
unicode_range:
Optional CSS unicode-range (e.g., "U+0000-00FF") to subset coverage.
129 def uris(self) -> list[str]: # -> list[str] | list[Any]: 130 """ 131 Returns the source URI - with cache busting if enabled 132 which is implemented by getting the mtime of the local file 133 134 This operation performs up to two fs stats per call 135 136 1. The base static directory is checked once and cached per interpreter 137 2. The specific resource is checked if it is time to poll again 138 based on settings.stat_poll_interval 139 """ 140 hrefs = [] 141 for h in self.hrefs: 142 if not self.cache_bust: 143 hrefs.append(h) 144 continue 145 146 hrefs.append(_cachebust_resource_uri(h)) 147 return hrefs
Returns the source URI - with cache busting if enabled which is implemented by getting the mtime of the local file
This operation performs up to two fs stats per call
- The base static directory is checked once and cached per interpreter
- The specific resource is checked if it is time to poll again based on settings.stat_poll_interval
159 def preload_links(self) -> list[el.link]: 160 """ 161 Returns preload links if preload is set 162 """ 163 if not self.preload: 164 return [] 165 166 first_href = self._hrefs[0] 167 attrs = {"href": first_href} 168 169 if self.crossorigin: 170 attrs["crossorigin"] = self.crossorigin 171 172 mimetype = font_import_manual.get_font_mime(first_href) 173 174 if mimetype: 175 attrs["type"] = mimetype 176 177 return [el.link(attrs=attrs, rel="preload", as_="font")]
Returns preload links if preload is set
182 def style_data(self) -> str: 183 """ 184 Returns one or more nodes for this import 185 """ 186 187 font_refs = [] 188 189 if len(self._hrefs) == 1: 190 # we don't have to hint format if there's only one 191 font_refs.append(f"url('{self._hrefs[0]}')") 192 else: 193 for h in self._hrefs: 194 entry = f"url('{h}')" 195 format = font_import_manual.get_font_format(h) 196 if format: 197 entry += f" format('{format}')" 198 199 font_refs.append(entry) 200 201 if isinstance(self.weight, tuple): 202 font_weight = f"{self.weight[0]} {self.weight[1]}" 203 elif isinstance(self.weight, str): 204 font_weight = self.weight 205 elif isinstance(self.weight, int): 206 font_weight = str(self.weight) 207 else: 208 raise TypeError("font weight must be int, str, or tuple") 209 210 style_attrs = { 211 "font-family": f"'{self.family}'", 212 "src": ",\n\t\t".join(font_refs), 213 "font-style": self.style, 214 "font-display": self.display, 215 "font-weight": font_weight, 216 "unicode-range": self.unicode_range, 217 } 218 for k in list(style_attrs.keys()): 219 v = style_attrs[k] 220 if v is None: 221 del style_attrs[k] 222 223 return "\n".join( 224 [ 225 "@font-face {", 226 "".join( 227 [ 228 "\t", 229 ";\n\t".join( 230 [f"{k}: {v}" for k, v in style_attrs.items()] 231 ), 232 ";", # Ensure last property ends with semicolon 233 ] 234 ), 235 "}", 236 ] 237 )
Returns one or more nodes for this import
243class font_import_provider(_font_import_base): 244 def __init__( 245 self, 246 href: str, 247 preload: bool = False, 248 hash: str | None = None, 249 crossorigin: Literal["", "anonymous", "user-credentials"] 250 | str 251 | None = None, 252 cache_bust: bool = False, 253 preconnect: list[str] | None = None, 254 preconnect_crossorigin: Literal["", "anonymous", "user-credentials"] 255 | None = None, 256 ): 257 """ 258 A font import helper class for css based imports which employs: 259 - `link(rel="stylesheet")` to define the import 260 - `link(rel="preload")` to preload the import 261 - `link(rel="preconnect")` to preconnect to font providers 262 - `hash` and `crossorigin` for SRI 263 - Local resource cache busting 264 265 NEVER use this with untrusted user input. 266 267 Parameters 268 ---------- 269 `href`: 270 The literal href of the CSS resource which sets up the font 271 272 `preload`: 273 If true, adds a preload link for this resource 274 275 `hash`: 276 An optional SRI integrity hash for the import 277 278 `crossorigin`: 279 Optionally sets the crossorigin attribute on the link tag. 280 Valid values are "", "anonymous", "use-credentials" 281 282 `preconnect`: 283 A list of URLs to preconnect to. This is useful for font providers 284 such as Google Fonts. 285 286 `preconnect_crossorigin`: 287 Optionally sets the crossorigin attribute on the preconnect link tag. 288 If not set, it defaults to the value of `crossorigin`. 289 290 `cache_bust`: 291 If true, appends a timestamp to the URL to prevent browser 292 caching. Webservers configured for static resources manage this 293 feature automatically, but for development this can be useful. 294 295 This feature only works for local resources, i.e. those that 296 exist in `resource.settings.base_dir` 297 298 """ 299 self.href = href 300 self.preload = preload 301 self.hash = hash 302 self.crossorigin = crossorigin 303 self.cache_bust = cache_bust 304 self.has_link = preload 305 self._href = self.uri() 306 self.preconnect = preconnect 307 308 if hash and self.crossorigin is None: 309 raise ValueError( 310 "If hash is set, crossorigin must be set to ''/'anonymous'" 311 ) 312 self.preconnect_crossorigin = preconnect_crossorigin 313 if preconnect_crossorigin is None and self.crossorigin: 314 if self.crossorigin != "user-credentials": 315 self.preconnect_crossorigin = self.crossorigin # type: ignore[assignment] 316 else: 317 raise ValueError( 318 "Please set preconnect_crossorigin explicitly if " 319 "crossorigin is 'user-credentials'" 320 ) 321 322 def uri(self): 323 """ 324 Returns the source URI - with cache busting if enabled 325 which is implemented by getting the mtime of the local file 326 327 This operation performs up to two fs stats per call 328 329 1. The base static directory is checked once and cached per interpreter 330 2. The specific resource is checked if it is time to poll again 331 based on settings.stat_poll_interval 332 """ 333 if not self.cache_bust: 334 return self.href 335 336 return _cachebust_resource_uri(self.href) 337 338 def preload_links( 339 self, 340 ) -> list[el.link]: # -> list[Any]:# -> list[Any]:# -> list[Any]: 341 """ 342 Returns preload links if preload is set 343 """ 344 345 links = [] 346 347 if self.preload: 348 attrs = {"href": self._href} 349 if self.hash: 350 attrs["integrity"] = self.hash 351 if self.crossorigin: 352 attrs["crossorigin"] = self.crossorigin 353 links.append(el.link(attrs=attrs, rel="preload", as_="style")) 354 355 return links 356 357 def links(self) -> list[el.link]: 358 """ 359 Returns link elements for this import 360 """ 361 links = [] 362 363 attrs = {"href": self._href} 364 if self.hash: 365 attrs["integrity"] = self.hash 366 if self.crossorigin: 367 attrs["crossorigin"] = self.crossorigin 368 369 links.append(el.link(attrs=attrs, rel="stylesheet")) 370 371 return links 372 373 def style_data(self) -> str: 374 # Fonts imported via CSS do not have style nodes 375 return "" 376 377 def preconnect_links(self) -> list[el.link]: 378 """ 379 Returns generated preconnect link elements for this import 380 """ 381 links = [] 382 if self.preconnect: 383 for pc in self.preconnect: 384 pc_attrs = {"href": pc} 385 if self.preconnect_crossorigin: 386 pc_attrs["crossorigin"] = self.preconnect_crossorigin 387 links.append(el.link(attrs=pc_attrs, rel="preconnect")) 388 return links
244 def __init__( 245 self, 246 href: str, 247 preload: bool = False, 248 hash: str | None = None, 249 crossorigin: Literal["", "anonymous", "user-credentials"] 250 | str 251 | None = None, 252 cache_bust: bool = False, 253 preconnect: list[str] | None = None, 254 preconnect_crossorigin: Literal["", "anonymous", "user-credentials"] 255 | None = None, 256 ): 257 """ 258 A font import helper class for css based imports which employs: 259 - `link(rel="stylesheet")` to define the import 260 - `link(rel="preload")` to preload the import 261 - `link(rel="preconnect")` to preconnect to font providers 262 - `hash` and `crossorigin` for SRI 263 - Local resource cache busting 264 265 NEVER use this with untrusted user input. 266 267 Parameters 268 ---------- 269 `href`: 270 The literal href of the CSS resource which sets up the font 271 272 `preload`: 273 If true, adds a preload link for this resource 274 275 `hash`: 276 An optional SRI integrity hash for the import 277 278 `crossorigin`: 279 Optionally sets the crossorigin attribute on the link tag. 280 Valid values are "", "anonymous", "use-credentials" 281 282 `preconnect`: 283 A list of URLs to preconnect to. This is useful for font providers 284 such as Google Fonts. 285 286 `preconnect_crossorigin`: 287 Optionally sets the crossorigin attribute on the preconnect link tag. 288 If not set, it defaults to the value of `crossorigin`. 289 290 `cache_bust`: 291 If true, appends a timestamp to the URL to prevent browser 292 caching. Webservers configured for static resources manage this 293 feature automatically, but for development this can be useful. 294 295 This feature only works for local resources, i.e. those that 296 exist in `resource.settings.base_dir` 297 298 """ 299 self.href = href 300 self.preload = preload 301 self.hash = hash 302 self.crossorigin = crossorigin 303 self.cache_bust = cache_bust 304 self.has_link = preload 305 self._href = self.uri() 306 self.preconnect = preconnect 307 308 if hash and self.crossorigin is None: 309 raise ValueError( 310 "If hash is set, crossorigin must be set to ''/'anonymous'" 311 ) 312 self.preconnect_crossorigin = preconnect_crossorigin 313 if preconnect_crossorigin is None and self.crossorigin: 314 if self.crossorigin != "user-credentials": 315 self.preconnect_crossorigin = self.crossorigin # type: ignore[assignment] 316 else: 317 raise ValueError( 318 "Please set preconnect_crossorigin explicitly if " 319 "crossorigin is 'user-credentials'" 320 )
A font import helper class for css based imports which employs:
link(rel="stylesheet")to define the importlink(rel="preload")to preload the importlink(rel="preconnect")to preconnect to font providershashandcrossoriginfor SRI- Local resource cache busting
NEVER use this with untrusted user input.
Parameters
href:
The literal href of the CSS resource which sets up the font
preload:
If true, adds a preload link for this resource
hash:
An optional SRI integrity hash for the import
crossorigin:
Optionally sets the crossorigin attribute on the link tag.
Valid values are "", "anonymous", "use-credentials"
preconnect:
A list of URLs to preconnect to. This is useful for font providers
such as Google Fonts.
preconnect_crossorigin:
Optionally sets the crossorigin attribute on the preconnect link tag.
If not set, it defaults to the value of crossorigin.
cache_bust:
If true, appends a timestamp to the URL to prevent browser
caching. Webservers configured for static resources manage this
feature automatically, but for development this can be useful.
This feature only works for local resources, i.e. those that
exist in `resource.settings.base_dir`
322 def uri(self): 323 """ 324 Returns the source URI - with cache busting if enabled 325 which is implemented by getting the mtime of the local file 326 327 This operation performs up to two fs stats per call 328 329 1. The base static directory is checked once and cached per interpreter 330 2. The specific resource is checked if it is time to poll again 331 based on settings.stat_poll_interval 332 """ 333 if not self.cache_bust: 334 return self.href 335 336 return _cachebust_resource_uri(self.href)
Returns the source URI - with cache busting if enabled which is implemented by getting the mtime of the local file
This operation performs up to two fs stats per call
- The base static directory is checked once and cached per interpreter
- The specific resource is checked if it is time to poll again based on settings.stat_poll_interval
338 def preload_links( 339 self, 340 ) -> list[el.link]: # -> list[Any]:# -> list[Any]:# -> list[Any]: 341 """ 342 Returns preload links if preload is set 343 """ 344 345 links = [] 346 347 if self.preload: 348 attrs = {"href": self._href} 349 if self.hash: 350 attrs["integrity"] = self.hash 351 if self.crossorigin: 352 attrs["crossorigin"] = self.crossorigin 353 links.append(el.link(attrs=attrs, rel="preload", as_="style")) 354 355 return links
Returns preload links if preload is set
357 def links(self) -> list[el.link]: 358 """ 359 Returns link elements for this import 360 """ 361 links = [] 362 363 attrs = {"href": self._href} 364 if self.hash: 365 attrs["integrity"] = self.hash 366 if self.crossorigin: 367 attrs["crossorigin"] = self.crossorigin 368 369 links.append(el.link(attrs=attrs, rel="stylesheet")) 370 371 return links
Returns link elements for this import
377 def preconnect_links(self) -> list[el.link]: 378 """ 379 Returns generated preconnect link elements for this import 380 """ 381 links = [] 382 if self.preconnect: 383 for pc in self.preconnect: 384 pc_attrs = {"href": pc} 385 if self.preconnect_crossorigin: 386 pc_attrs["crossorigin"] = self.preconnect_crossorigin 387 links.append(el.link(attrs=pc_attrs, rel="preconnect")) 388 return links
Returns generated preconnect link elements for this import
62def to_elements( 63 js: Iterable[str | js_import] | None = None, 64 css: Iterable[str | css_import] | None = None, 65 fonts: Iterable[font_import_manual | font_import_provider] | None = None, 66): 67 """ 68 Generate elements for `head` element from resource imports 69 70 Depending on your use case consider caching the resolution of this function 71 72 73 :param js: Javascript imports. A string is treated as a simple script src 74 :param css: CSS imports. A string is treated as a simple link rel=stylesheet 75 :param fonts: Font imports 76 """ 77 head_elements: list[base_types.Node] = [] 78 79 preconnect_links = [] 80 preload_links = [] 81 links = [] 82 if css: 83 for css_resource in css: 84 if isinstance(css_resource, css_import): 85 for link in css_resource.links(): 86 links.append(link) 87 for preload in css_resource.preloads(): 88 preload_links.append(preload) 89 else: 90 if not isinstance(css_resource, str): 91 raise TypeError("css must be str or css_import") 92 93 links.append(el.link(rel="stylesheet", href=css_resource)) 94 95 script_tags = [] 96 if js: 97 # <link rel="modulepreload" href="main.js" /> 98 js_imports: dict[str, str] = {} 99 scopes: dict[str, dict[str, str]] = {} 100 for js_src in js: 101 if isinstance(js_src, js_import): 102 jsi: js_import = js_src 103 # Generate script tag 104 script_tags.append(jsi.script()) 105 link = jsi.preload_link() 106 if link: 107 preload_links.append(link) 108 109 entry = jsi.import_map_entry() 110 if entry: 111 # Add to import map 112 name, src = entry.name, entry.src 113 js_imports[name] = src 114 if entry.scope_urls: 115 for scope_key in entry.scope_urls: 116 sdict = scopes.setdefault(scope_key, {}) 117 sdict[name] = src 118 119 else: 120 if not isinstance(js_src, str): 121 raise TypeError("js must be str or js_import") 122 script_tags.append(el.script(src=js_src)) 123 124 if js_imports: 125 import_map: dict[str, Any] = {"imports": js_imports} 126 if scopes: 127 import_map["scopes"] = scopes 128 # dump to json; prevent escapes 129 js_dump = json.dumps(import_map).replace("<", "\\u003c") 130 preload_links.insert( 131 0, el.script(type="importmap")[unsafe_text(js_dump)] 132 ) 133 style_text: list[str] = [] 134 if fonts: 135 for font in fonts: 136 # Each font may be a different kind but the interface means 137 # we capture anything they generate 138 links.extend(font.links()) 139 preconnect_links.extend(font.preconnect_links()) 140 preload_links.extend(font.preload_links()) 141 style_data = font.style_data() 142 if style_data: 143 style_text.append(style_data) 144 145 if preconnect_links: 146 head_elements.extend(preconnect_links) 147 148 if preload_links: 149 head_elements.extend(preload_links) 150 151 if links: 152 head_elements.extend(links) 153 154 if style_text: 155 head_elements.append(el.style()[unsafe_text("\n".join(style_text))]) 156 157 if script_tags: 158 head_elements.extend(script_tags) 159 160 return head_elements
Generate elements for head element from resource imports
Depending on your use case consider caching the resolution of this function
Parameters
- js: Javascript imports. A string is treated as a simple script src
- css: CSS imports. A string is treated as a simple link rel=stylesheet
- fonts: Font imports
30class settings: 31 """ 32 Global settings for js_import/css_import behavior. 33 34 `base_dir` is base directory from which local static files are served 35 and is used to construct cache busting URLs. 36 """ 37 38 # Base directory when resolving relative paths for local resources 39 base_dir = "." 40 # html-compose cache-buster timestamp 41 query_string = "hccbts" 42 # Maximum number of cached URIs for cache busting 43 cache_cap = 1000 44 stat_poll_interval: int | float = 1 # seconds
Global settings for js_import/css_import behavior.
base_dir is base directory from which local static files are served
and is used to construct cache busting URLs.