diff --git a/markata/plugins/auto_description.py b/markata/plugins/auto_description.py index 45b1e94cd..5b22c452a 100644 --- a/markata/plugins/auto_description.py +++ b/markata/plugins/auto_description.py @@ -119,20 +119,24 @@ def get_description(article: "Post") -> str: content = article.content # Remove admonitions (e.g., !!!, !!!+, ???, ???+) - content = re.sub(r'^[!?]{3}\+? .*?$', '', content, flags=re.MULTILINE) + content = re.sub(r"^[!?]{3}\+? .*?$", "", content, flags=re.MULTILINE) # Remove CSS class attributes {.class-name} - content = re.sub(r'\{\.[\w\-]+\}', '', content) + content = re.sub(r"\{\.[\w\-]+\}", "", content) # Remove Jinja template tags {% %} and {{ }} - content = re.sub(r'\{%.*?%\}', '', content, flags=re.DOTALL) - content = re.sub(r'\{\{.*?\}\}', '', content, flags=re.DOTALL) + content = re.sub(r"\{%.*?%\}", "", content, flags=re.DOTALL) + content = re.sub(r"\{\{.*?\}\}", "", content, flags=re.DOTALL) # Remove wikilinks [[link]] or [[link|text]] - content = re.sub(r'\[\[([^\]|]+)(?:\|([^\]]+))?\]\]', lambda m: m.group(2) if m.group(2) else m.group(1), content) + content = re.sub( + r"\[\[([^\]|]+)(?:\|([^\]]+))?\]\]", + lambda m: m.group(2) if m.group(2) else m.group(1), + content, + ) # Remove HTML comments - content = re.sub(r'', '', content, flags=re.DOTALL) + content = re.sub(r"", "", content, flags=re.DOTALL) # Remove HTML tags before markdown parsing soup = BeautifulSoup(content, "html.parser") @@ -155,7 +159,7 @@ def extract_text(tokens): description = extract_text(tokens) # Clean up excessive whitespace - description = re.sub(r'\s+', ' ', description).strip() + description = re.sub(r"\s+", " ", description).strip() return description diff --git a/markata/plugins/feeds.py b/markata/plugins/feeds.py index 46f9b2af0..c82400ba9 100644 --- a/markata/plugins/feeds.py +++ b/markata/plugins/feeds.py @@ -478,6 +478,7 @@ def save(markata: Markata) -> None: if should_write: xsl_file.write_text(xsl) + def create_page( markata: Markata, feed: Feed, @@ -503,7 +504,9 @@ def create_page( if cache_key_posts not in markata._feed_hash_cache: # Use post slugs and published dates instead of full to_dict() # This provides a stable, lightweight cache key - posts_data = feed.map("(post.slug, str(getattr(post, 'date', '')), getattr(post, 'title', ''))") + posts_data = feed.map( + "(post.slug, str(getattr(post, 'date', '')), getattr(post, 'title', ''))" + ) markata._feed_hash_cache[cache_key_posts] = str(sorted(posts_data)) posts_hash_data = markata._feed_hash_cache[cache_key_posts] @@ -542,9 +545,7 @@ def create_page( sitemap_output_file = ( Path(markata.config.output_dir) / feed.config.slug / "sitemap.xml" ) - atom_output_file = ( - Path(markata.config.output_dir) / feed.config.slug / "atom.xml" - ) + atom_output_file = Path(markata.config.output_dir) / feed.config.slug / "atom.xml" # Create all directories in one batch partial_output_file.parent.mkdir(exist_ok=True, parents=True) @@ -597,7 +598,9 @@ def create_page( if feed.config.sitemap: if feed_sitemap_from_cache is None: from_cache = False - sitemap_template = get_template(markata.jinja_env, feed.config.sitemap_template) + sitemap_template = get_template( + markata.jinja_env, feed.config.sitemap_template + ) feed_sitemap = sitemap_template.render(markata=markata, feed=feed) cache.set(feed_sitemap_key, feed_sitemap) else: @@ -662,7 +665,6 @@ def create_page( atom_output_file.write_text(feed_atom) - @background.task def create_card( markata: "Markata", diff --git a/markata/plugins/jinja_env.py b/markata/plugins/jinja_env.py index 4e4182d9e..ad13d56df 100644 --- a/markata/plugins/jinja_env.py +++ b/markata/plugins/jinja_env.py @@ -252,7 +252,7 @@ def get_templates_mtime(env: Environment) -> float: for template_dir in get_template_paths(env): template_path = Path(template_dir) if template_path.exists(): - for path in template_path.rglob('*'): + for path in template_path.rglob("*"): if path.is_file(): try: max_mtime = max(max_mtime, path.stat().st_mtime) diff --git a/markata/plugins/mermaid.py b/markata/plugins/mermaid.py new file mode 100644 index 000000000..2492f7b3f --- /dev/null +++ b/markata/plugins/mermaid.py @@ -0,0 +1,73 @@ +""" +Markata Plugin: Mermaid Diagram Renderer + +This plugin converts Mermaid code blocks in Markdown files into rendered Mermaid diagrams. + +# Installation + +Ensure Mermaid.js is available in your site. If serving locally, add the script to your template: + +```html + +``` + +# Configuration + +Enable the plugin in `markata.toml`: + +```toml +[markata] +hooks = ["markata.plugins.mermaid"] +``` + +# Usage + +Use Mermaid code blocks in your Markdown content: + +```markdown +```mermaid +graph TD; + A-->B; + A-->C; + B-->D; + C-->D; +``` +``` + +# Notes + +- Requires the Markata markdown-it-py backend with the `html` option enabled. +""" + +import re +from typing import TYPE_CHECKING + +from markata.hookspec import hook_impl + +if TYPE_CHECKING: + from markata import Markata + +MERMAID_BLOCK_RE = re.compile(r"```[\s]*mermaid\n(.*?)\n```", re.DOTALL) + + +@hook_impl +def pre_render(markata: "Markata") -> None: + for article in markata.iter_articles("processing mermaid blocks"): + markata.make_hash("mermaid", article.content) + if "mermaid" in article.content: + article.content = MERMAID_BLOCK_RE.sub( + replace_mermaid_block, article.content + ) + + +def replace_mermaid_block(match: re.Match) -> str: + mermaid_code = match.group(1).strip() + mermaid_block = f'
{mermaid_code}'
+ return mermaid_block
+
+
+MERMAID_SCRIPT = """
+"""
diff --git a/markata/plugins/post_template.py b/markata/plugins/post_template.py
index 0c3144421..eb6834158 100644
--- a/markata/plugins/post_template.py
+++ b/markata/plugins/post_template.py
@@ -1,6 +1,4 @@
"""
-
-
The `markata.plugins.post_template` plugin handles the rendering of posts using Jinja2
templates. It provides extensive configuration options for HTML head elements, styling,
and template customization.
@@ -226,6 +224,7 @@
from typing import Any
from typing import Dict
from typing import List
+from typing import Literal
from typing import Optional
from typing import Union
@@ -238,11 +237,14 @@
from pydantic import Field
from pydantic import field_validator
from pydantic import model_validator
+from pydantic import root_validator
+from rich.console import Console
from markata import __version__
from markata.hookspec import hook_impl
from markata.plugins.jinja_env import get_template
from markata.plugins.jinja_env import get_templates_mtime
+from markata.plugins.theme import THEME_DEFAULTS
if TYPE_CHECKING:
from markata import Markata
@@ -270,20 +272,175 @@ def dec(_cls):
return dec
+from markata.plugins.theme import Color
+
+# class ThemeStyle(pydantic.BaseModel):
+# text: Optional[Color] = None
+# muted: Optional[Color] = None
+# heading: Optional[Color] = None
+# accent: Optional[Color] = None
+# accent_alt: Optional[Color] = None
+# background: Optional[Color] = None
+# surface: Optional[Color] = None
+# code_bg: Optional[Color] = None
+# blockquote_bg: Optional[Color] = None
+# blockquote_border: Optional[Color] = None
+# link_hover: Optional[Color] = None
+# selection_bg: Optional[Color] = None
+# selection_text: Optional[Color] = None
+# border: Optional[Color] = None
+# background_image: Optional[str] = None
+
+
+class ThemeStyle(pydantic.BaseModel):
+ text: Optional[Color] = None
+ muted: Optional[Color] = None
+ heading: Optional[Color] = None
+ accent: Optional[Color] = None
+ accent_alt: Optional[Color] = None
+ background: Optional[Color] = None
+ surface: Optional[Color] = None
+ code_bg: Optional[Color] = None
+ blockquote_bg: Optional[Color] = None
+ blockquote_border: Optional[Color] = None
+ link_hover: Optional[Color] = None
+ selection_bg: Optional[Color] = None
+ selection_text: Optional[Color] = None
+ border: Optional[Color] = None
+ background_image: Optional[str] = None
+ code_theme: Literal[
+ "abap",
+ "algol",
+ "algol_nu",
+ "arduino",
+ "autumn",
+ "bw",
+ "borland",
+ "coffee",
+ "colorful",
+ "default",
+ "dracula",
+ "emacs",
+ "friendly_grayscale",
+ "friendly",
+ "fruity",
+ "github-dark",
+ "gruvbox-dark",
+ "gruvbox-light",
+ "igor",
+ "inkpot",
+ "lightbulb",
+ "lilypond",
+ "lovelace",
+ "manni",
+ "material",
+ "monokai",
+ "murphy",
+ "native",
+ "nord-darker",
+ "nord",
+ "one-dark",
+ "paraiso-dark",
+ "paraiso-light",
+ "pastie",
+ "perldoc",
+ "rainbow_dash",
+ "rrt",
+ "sas",
+ "solarized-dark",
+ "solarized-light",
+ "staroffice",
+ "stata-dark",
+ "stata-light",
+ "tango",
+ "trac",
+ "vim",
+ "vs",
+ "xcode",
+ "zenburn",
+ ] = "nord"
+ highlight_styles: Optional[str] = None
+
+ def __rich__(self):
+ from rich.text import Text
+
+ for k, v in self.dict().items():
+ if v:
+ yield Text(f"--{k} {v}", style="bold")
+ return self
+
+
+import re
+
+
+def wrap_raw_color(key: str, value: Optional[str]) -> Optional[str]:
+ if key.endswith("_theme"):
+ return value
+ if not value:
+ return value
+ if re.match(r"^[a-z]+-\d{3}$", value): # Tailwind class like "blue-500"
+ return value
+ if value.startswith("[") and value.endswith("]"): # already wrapped
+ return value
+ return f"[{value}]"
+
+
+def merge_styles(defaults: dict, overrides: Optional[dict]) -> ThemeStyle:
+ final = (defaults or {}).copy()
+ if overrides:
+ final.update({k: v for k, v in overrides.items() if v is not None})
+
+ code_theme = final.pop("code_theme", None)
+ background_image = final.pop("background_image", None)
+ wrapped_final = {k: Color(v) for k, v in final.items()}
+
+ if code_theme:
+ wrapped_final["code_theme"] = code_theme
+ if background_image:
+ wrapped_final["background_image"] = background_image
+
+ return ThemeStyle(**wrapped_final)
+
+
class Style(pydantic.BaseModel):
- color_bg: str = "#1f2022"
- color_bg_code: str = "#1f2022"
- color_text: str = "#eefbfe"
- color_link: str = "#fb30c4"
- color_accent: str = "#e1bd00c9"
- overlay_brightness: str = ".85"
- body_width: str = "800px"
- color_bg_light: str = "#eefbfe"
- color_bg_code_light: str = "#eefbfe"
- color_text_light: str = "#1f2022"
- color_link_light: str = "#fb30c4"
- color_accent_light: str = "#ffeb00"
- overlay_brightness_light: str = ".95"
+ theme: Literal[
+ "tokyo-night",
+ "catppuccin",
+ "everforest",
+ "gruvbox",
+ "kanagwa",
+ "nord",
+ "synthwave-84",
+ ] = "tokyo-night"
+
+ light: Optional[ThemeStyle] = None
+ dark: Optional[ThemeStyle] = None
+ # overlay_brightness: Optional[str] = ".85"
+ # body_width: Optional[str] = "800px"
+
+ @root_validator(pre=True)
+ def apply_theme_defaults(cls, values):
+ theme_name = values.get("theme")
+ theme_defaults = THEME_DEFAULTS.get(theme_name, {})
+ values["light"] = merge_styles(
+ theme_defaults.get("light", {}), values.get("light")
+ )
+ values["dark"] = merge_styles(
+ theme_defaults.get("dark", {}), values.get("dark")
+ )
+
+ from pygments.formatters import HtmlFormatter
+ from pygments.styles import get_style_by_name
+
+ light_style = get_style_by_name(values["light"].code_theme)
+ light_formatter = HtmlFormatter(style=light_style)
+ values["light"].highlight_styles = light_formatter.get_style_defs(".highlight")
+
+ dark_style = get_style_by_name(values["dark"].code_theme)
+ dark_formatter = HtmlFormatter(style=dark_style)
+ values["dark"].highlight_styles = dark_formatter.get_style_defs(".highlight")
+
+ return values
@optional
@@ -320,6 +477,7 @@ class Link(pydantic.BaseModel):
class Script(pydantic.BaseModel):
src: str
+ defer: Optional[bool] = False
class HeadConfig(pydantic.BaseModel):
@@ -348,7 +506,11 @@ def html(self):
class Config(pydantic.BaseModel):
head: HeadConfig = HeadConfig()
style: Style = Style()
- post_template: Optional[Union[str | Dict[str, str]]] = "post.html"
+ post_template: Optional[Union[str | Dict[str, str]]] = {
+ "index": "post.html",
+ "partial": "post_partial.html",
+ "og": "og.html",
+ }
dynamic_templates_dir: Path = Path(".markata.cache/templates")
templates_dir: Union[Path, List[Path]] = pydantic.Field(Path("templates"))
template_cache_dir: Path = Path(".markata.cache/template_bytecode")
@@ -398,6 +560,7 @@ def render_article(markata, cache, article):
article.key,
str(templates_mtime), # Track template file changes
)
+
html = markata.precache.get(key)
if html is not None:
@@ -409,7 +572,9 @@ def render_article(markata, cache, article):
if isinstance(article.template, dict):
html = {
- slug: render_template(markata, article, get_template(markata.jinja_env, template))
+ slug: render_template(
+ markata, article, get_template(markata.jinja_env, template)
+ )
for slug, template in article.template.items()
}
cache.set(key, html, expire=markata.config.default_cache_expire)
@@ -635,3 +800,90 @@ def render(markata: "Markata") -> None:
for article in markata.filter("not skip"):
html = render_article(markata=markata, cache=cache, article=article)
article.html = html
+
+
+console = Console(record=True)
+
+
+def print_theme(theme: str):
+ console.print()
+ console.print()
+ console.print(
+ f"[bold {Color(THEME_DEFAULTS[theme]['dark']['text'])} on {Color(THEME_DEFAULTS[theme]['dark']['background'])}]{theme.title()} Theme[/]".center(
+ 80
+ )
+ )
+ console.print()
+ console.print("[bold]Light Theme[/]")
+ for key, color in THEME_DEFAULTS[theme]["light"].items():
+ if key not in ["code_theme", "highlight_styles"]:
+ console.print(key, Color(color))
+ # print_color_swatch(
+ # f"dark.{key}: {color}",
+ # color.replace("[", "").replace("]", ""),
+ # )
+
+ console.print("\n[bold]Dark Theme[/]")
+ for key, color in THEME_DEFAULTS[theme]["dark"].items():
+ if key not in ["code_theme", "highlight_styles"]:
+ console.print(key, Color(color))
+ # print_color_swatch(
+ # f"light.{key}: {color}",
+ # color.replace("[", "").replace("]", ""),
+ # )
+
+
+@hook_impl()
+def cli(app: typer.Typer, markata: "Markata") -> None:
+ """
+ Markata hook to implement base cli commands.
+ """
+ theme_app = typer.Typer()
+ app.add_typer(theme_app, name="theme")
+
+ @theme_app.callback()
+ def theme():
+ "configuration management"
+
+ @theme_app.command()
+ def show():
+ "show the application summary"
+
+ markata.console.quiet = True
+ console.print(f"[bold]{markata.config.style.theme.title()} Theme[/]")
+ console.print()
+ console.print("[bold]Light Theme[/]")
+
+ for key, color in markata.config.style.dark.model_dump().items():
+ if "#" in color and key not in ["code_theme", "highlight_styles"]:
+ print_color_swatch(
+ f"dark.{key}: {color}",
+ color.replace("[", "").replace("]", ""),
+ )
+
+ console.print("\n[bold]Dark Theme[/]")
+ for key, color in markata.config.style.light.model_dump().items():
+ if "#" in color and key not in ["code_theme", "highlight_styles"]:
+ print_color_swatch(
+ f"light.{key}: {color}",
+ color.replace("[", "").replace("]", ""),
+ )
+
+ @theme_app.command()
+ def list():
+ "show the application summary"
+
+ markata.console.quiet = True
+ # console.print(markata.config.style)
+ for theme in THEME_DEFAULTS:
+ console.print(theme)
+
+ @theme_app.command()
+ def show_all():
+ "show the application summary"
+
+ markata.console.quiet = True
+ for theme in THEME_DEFAULTS:
+ print_theme(theme)
+ html = console.export_html(inline_styles=True)
+ Path("themes.html").write_text(html)
diff --git a/markata/plugins/redirects.py b/markata/plugins/redirects.py
index 93cbdc6fa..0bd11fd02 100644
--- a/markata/plugins/redirects.py
+++ b/markata/plugins/redirects.py
@@ -174,7 +174,9 @@ def save(markata: "Markata") -> None:
# Get template mtime to bust cache when template changes
template_mtime = template_file.stat().st_mtime if template_file.exists() else 0
- key = markata.make_hash("redirects", "raw_redirects", raw_redirects, str(template_mtime))
+ key = markata.make_hash(
+ "redirects", "raw_redirects", raw_redirects, str(template_mtime)
+ )
with markata.cache as cache:
cache.get(key)
if cache.get(key) == "done":
diff --git a/markata/plugins/theme.py b/markata/plugins/theme.py
new file mode 100644
index 000000000..df9a303b3
--- /dev/null
+++ b/markata/plugins/theme.py
@@ -0,0 +1,796 @@
+# "background_image": "https://transparenttextures.com/patterns/asfalt-dark.png",
+
+from typing import Dict
+
+from colour import Color as BaseColor
+from pydantic import GetCoreSchemaHandler
+from pydantic_core import core_schema
+from rich.console import Console
+from rich.console import ConsoleOptions
+from rich.console import RenderResult
+from rich.repr import RichReprResult
+from rich.text import Text
+
+
+def print_color_swatch(name: str, hex_color: str):
+ text_color = "#ffffff"
+ hex_color = str(hex_color)
+ if Color(hex_color).get_luminance() > 0.5:
+ text_color = "#000000"
+
+ text = Text(f" {name.ljust(46)} ", style=f"on {hex_color} {text_color}")
+ console = Console()
+ console.print(text, justify="left", end=" ")
+
+
+class Color(BaseColor):
+ def __init__(self, value=None, alpha=1.0, **kwargs):
+ # store tailwind token if we got one
+ self.__dict__["alpha"] = alpha
+ self.__dict__["_tw_token"] = None
+
+ if value is not None:
+ if isinstance(value, str):
+ # name-index form, e.g. "rose-500"
+ if "-" in value:
+ name, index = value.split("-", 1)
+ if name in tailwind_v4_colors:
+ palette = tailwind_v4_colors[name]
+ try:
+ color_val = palette[int(index)]
+ except (ValueError, KeyError):
+ # fall back to original string – let BaseColor deal with it
+ color_val = value
+ else:
+ self.__dict__["_tw_token"] = f"{name}-{index}"
+ value = color_val
+
+ # just a tailwind name like "rose" or "slate"
+ elif value in tailwind_v4_colors:
+ self.__dict__["_tw_token"] = value
+ val = tailwind_v4_colors[value]
+ if isinstance(val, dict):
+ # fallback to 500 or first value
+ value = val.get(500, list(val.values())[0])
+ else:
+ value = val
+ # else: hex or other format; no special token stored
+
+ super().__init__(value)
+ else:
+ super().__init__(**kwargs)
+
+ @property
+ def tw(self) -> str:
+ """
+ Tailwind-friendly suffix for utilities:
+
+ - If constructed from a tailwind token like "rose-500", return "rose-500".
+ - If constructed from hex like "#2a2a38", return "[#2a2a38]" for arbitrary values.
+ - If alpha < 1, use Tailwind's opacity modifier syntax (e.g., "rose-500/50" or "[#2a2a38]/50").
+ """
+ token = getattr(self, "_tw_token", None)
+ alpha = getattr(self, "alpha", 1.0)
+
+ if alpha < 1.0:
+ # Convert alpha to percentage for Tailwind opacity modifier
+ opacity = int(alpha * 100)
+ if token:
+ return f"{token}/{opacity}"
+ else:
+ return f"[{self.hex_l}]/{opacity}"
+
+ if token:
+ return token
+ # arbitrary value syntax for pure hex
+ return f"[{self.hex_l}]"
+
+ @classmethod
+ def __get_pydantic_core_schema__(
+ cls, source_type, handler: GetCoreSchemaHandler
+ ) -> core_schema.CoreSchema:
+ # Parse from a string and return a Color
+ return core_schema.no_info_plain_validator_function(
+ cls._validate_color,
+ serialization=core_schema.plain_serializer_function_ser_schema(str),
+ )
+
+ @classmethod
+ def _validate_color(cls, value):
+ if isinstance(value, cls):
+ return value
+ if isinstance(value, BaseColor):
+ return cls(value.hex_l)
+ if isinstance(value, str):
+ return cls(value)
+ raise TypeError(f"Cannot convert {value!r} to {cls.__name__}")
+
+ def __str__(self):
+ return self.hex_l
+
+ def __str__(self):
+ # Convert to 8-digit hex if alpha < 1
+ hex_rgb = self.hex_l
+ if self.alpha < 1.0:
+ alpha_hex = f"{round(self.alpha * 255):02x}"
+ return f"{hex_rgb}{alpha_hex}"
+ return f"{hex_rgb}"
+
+ def __repr__(self):
+ return str(self)
+
+ def __rich_repr__(self) -> RichReprResult:
+ text_color = "#000000" if self.get_luminance() > 0.5 else "#ffffff"
+ swatch = Text(f" {str(self).ljust(46)} ", style=f"on {str(self)} {text_color}")
+ yield "swatch", swatch
+
+ def __rich_console__(
+ self, console: Console, options: ConsoleOptions
+ ) -> RenderResult:
+ text_color = "#000000" if self.get_luminance() > 0.5 else "#ffffff"
+ swatch = Text(f" {self.hex.ljust(46)} ", style=f"on {self.hex_l} {text_color}")
+ yield swatch
+
+ def __rich__(self):
+ text_color = "#000000" if self.get_luminance() > 0.5 else "#ffffff"
+ return Text(f" {str(self).ljust(46)} ", style=f"on {str(self)} {text_color}")
+
+ def __add__(self, other):
+ return self.mix_hsl(other)
+
+ def __radd__(self, other):
+ return self.mix_hsl(other)
+
+ def __sub__(self, other):
+ return self.mix_hsl(other, ratio=0.5)
+
+ def __rsub__(self, other):
+ return self.mix_hsl(other, ratio=0.5)
+
+ def __mul__(self, other):
+ return self.mix_hsl(other)
+
+ def mix(self, other, ratio=0.5):
+ base = self.get_hsl()
+ overlay = other.get_hsl()
+
+ h = (1 - ratio) * base[0] + ratio * overlay[0]
+ s = (1 - ratio) * base[1] + ratio * overlay[1]
+ l = (1 - ratio) * base[2] + ratio * overlay[2]
+ return Color(hsl=(h, s, l)) # .hex_l
+
+ def blend(self, other, ratio=0.5):
+ return self.mix(other, ratio)
+
+ def _rgb_math(self, other, op):
+ a = self.get_rgb()
+ b = other.get_rgb()
+ result = [op(x, y) for x, y in zip(a, b)]
+ # Clamp between 0 and 1
+ clamped = [min(max(c, 0), 1) for c in result]
+ return Color(rgb=tuple(clamped))
+
+ def _rgb_scalar_math(self, scalar, op):
+ a = self.get_rgb()
+ result = [op(x, scalar) for x in a]
+ clamped = [min(max(c, 0), 1) for c in result]
+ return Color(rgb=tuple(clamped))
+
+ def __add__(self, other):
+ return self._rgb_math(other, lambda a, b: a + b)
+
+ def __sub__(self, other):
+ return self._rgb_math(other, lambda a, b: a - b)
+
+ def __mul__(self, other):
+ if isinstance(other, Color):
+ return self._rgb_math(other, lambda a, b: a * b)
+ else:
+ return self._rgb_scalar_math(other, lambda a, b: a * b)
+
+ def __truediv__(self, other):
+ if isinstance(other, Color):
+ return self._rgb_math(other, lambda a, b: a / b if b != 0 else 0)
+ else:
+ return self._rgb_scalar_math(other, lambda a, b: a / b if b != 0 else 0)
+
+ def __pow__(self, exponent):
+ return self._rgb_scalar_math(exponent, lambda a, b: a**b)
+
+ def print_color_swatch(self):
+ text_color = "#ffffff"
+ if self.get_luminance() > 0.5:
+ text_color = "#000000"
+
+ text = Text(f" {self.hex.ljust(46)} ", style=f"on {self.hex_l} {text_color}")
+ console = Console()
+ console.print(text, justify="left", end=" ")
+
+ def compliment(self):
+ h, s, l = self.get_hsl()
+ return Color(hsl=((h + 0.5) % 1, s, l))
+
+ def lighten(self, amount=0.1):
+ h, s, l = self.get_hsl()
+ return Color(hsl=(h, s, clamp(l + amount)))
+
+ def darken(self, amount=0.1):
+ h, s, l = self.get_hsl()
+
+ return Color(hsl=(h, s, clamp(l - amount)))
+
+ def saturate(self, amount=0.1):
+ h, s, l = self.get_hsl()
+ return Color(hsl=(h, clamp(s + amount), l))
+
+ def desaturate(self, amount=0.1):
+ h, s, l = self.get_hsl()
+ return Color(hsl=(h, clamp(s - amount), l))
+
+ def invert(self):
+ r, g, b = self.get_rgb()
+ return Color(rgb=(1 - r, 1 - g, 1 - b))
+
+ def with_alpha(self, alpha):
+ return Color(rgb=self.get_rgb(), alpha=clamp(alpha, 0, 1))
+
+
+def clamp(value, min_value=0, max_value=1):
+ return max(min(value, max_value), min_value)
+
+
+tailwind_v4_colors = {
+ "black": "#000000",
+ "white": "#ffffff",
+ "transparent": "transparent",
+ "slate": {
+ 50: "#f8fafc",
+ 100: "#f1f5f9",
+ 200: "#e2e8f0",
+ 300: "#cbd5e1",
+ 400: "#94a3b8",
+ 500: "#64748b",
+ 600: "#475569",
+ 700: "#334155",
+ 800: "#1e293b",
+ 900: "#0f172a",
+ 950: "#020617",
+ },
+ "gray": {
+ 50: "#f9fafb",
+ 100: "#f3f4f6",
+ 200: "#e5e7eb",
+ 300: "#d1d5db",
+ 400: "#9ca3af",
+ 500: "#6b7280",
+ 600: "#4b5563",
+ 700: "#374151",
+ 800: "#1f2937",
+ 900: "#111827",
+ 950: "#030712",
+ },
+ "zinc": {
+ 50: "#fafafa",
+ 100: "#f4f4f5",
+ 200: "#e4e4e7",
+ 300: "#d4d4d8",
+ 400: "#a1a1aa",
+ 500: "#71717a",
+ 600: "#52525b",
+ 700: "#3f3f46",
+ 800: "#27272a",
+ 900: "#18181b",
+ 950: "#09090b",
+ },
+ "neutral": {
+ 50: "#fafafa",
+ 100: "#f5f5f5",
+ 200: "#e5e5e5",
+ 300: "#d4d4d4",
+ 400: "#a3a3a3",
+ 500: "#737373",
+ 600: "#525252",
+ 700: "#404040",
+ 800: "#262626",
+ 900: "#171717",
+ 950: "#0a0a0a",
+ },
+ "stone": {
+ 50: "#fafaf9",
+ 100: "#f5f5f4",
+ 200: "#e7e5e4",
+ 300: "#d6d3d1",
+ 400: "#a8a29e",
+ 500: "#78716c",
+ 600: "#57534e",
+ 700: "#44403c",
+ 800: "#292524",
+ 900: "#1c1917",
+ 950: "#0c0a09",
+ },
+ "red": {
+ 50: "#fef2f2",
+ 100: "#fee2e2",
+ 200: "#fecaca",
+ 300: "#fca5a5",
+ 400: "#f87171",
+ 500: "#ef4444",
+ 600: "#dc2626",
+ 700: "#b91c1c",
+ 800: "#991b1b",
+ 900: "#7f1d1d",
+ 950: "#450a0a",
+ },
+ "orange": {
+ 50: "#fff7ed",
+ 100: "#ffedd5",
+ 200: "#fed7aa",
+ 300: "#fdba74",
+ 400: "#fb923c",
+ 500: "#f97316",
+ 600: "#ea580c",
+ 700: "#c2410c",
+ 800: "#9a3412",
+ 900: "#7c2d12",
+ 950: "#431407",
+ },
+ "amber": {
+ 50: "#fffbeb",
+ 100: "#fef3c7",
+ 200: "#fde68a",
+ 300: "#fcd34d",
+ 400: "#fbbf24",
+ 500: "#f59e0b",
+ 600: "#d97706",
+ 700: "#b45309",
+ 800: "#92400e",
+ 900: "#78350f",
+ 950: "#451a03",
+ },
+ "yellow": {
+ 50: "#fefce8",
+ 100: "#fef9c3",
+ 200: "#fef08a",
+ 300: "#fde047",
+ 400: "#facc15",
+ 500: "#eab308",
+ 600: "#ca8a04",
+ 700: "#a16207",
+ 800: "#854d0e",
+ 900: "#713f12",
+ 950: "#422006",
+ },
+ "lime": {
+ 50: "#f7fee7",
+ 100: "#ecfccb",
+ 200: "#d9f99d",
+ 300: "#bef264",
+ 400: "#a3e635",
+ 500: "#84cc16",
+ 600: "#65a30d",
+ 700: "#4d7c0f",
+ 800: "#3f6212",
+ 900: "#365314",
+ 950: "#1a2e05",
+ },
+ "green": {
+ 50: "#f0fdf4",
+ 100: "#dcfce7",
+ 200: "#bbf7d0",
+ 300: "#86efac",
+ 400: "#4ade80",
+ 500: "#22c55e",
+ 600: "#16a34a",
+ 700: "#15803d",
+ 800: "#166534",
+ 900: "#14532d",
+ 950: "#052e16",
+ },
+ "emerald": {
+ 50: "#ecfdf5",
+ 100: "#d1fae5",
+ 200: "#a7f3d0",
+ 300: "#6ee7b7",
+ 400: "#34d399",
+ 500: "#10b981",
+ 600: "#059669",
+ 700: "#047857",
+ 800: "#065f46",
+ 900: "#064e3b",
+ 950: "#022c22",
+ },
+ "teal": {
+ 50: "#f0fdfa",
+ 100: "#ccfbf1",
+ 200: "#99f6e4",
+ 300: "#5eead4",
+ 400: "#2dd4bf",
+ 500: "#14b8a6",
+ 600: "#0d9488",
+ 700: "#0f766e",
+ 800: "#115e59",
+ 900: "#134e4a",
+ 950: "#042f2e",
+ },
+ "cyan": {
+ 50: "#ecfeff",
+ 100: "#cffafe",
+ 200: "#a5f3fc",
+ 300: "#67e8f9",
+ 400: "#22d3ee",
+ 500: "#06b6d4",
+ 600: "#0891b2",
+ 700: "#0e7490",
+ 800: "#155e75",
+ 900: "#164e63",
+ 950: "#083344",
+ },
+ "sky": {
+ 50: "#f0f9ff",
+ 100: "#e0f2fe",
+ 200: "#bae6fd",
+ 300: "#7dd3fc",
+ 400: "#38bdf8",
+ 500: "#0ea5e9",
+ 600: "#0284c7",
+ 700: "#0369a1",
+ 800: "#075985",
+ 900: "#0c4a6e",
+ 950: "#082f49",
+ },
+ "blue": {
+ 50: "#eff6ff",
+ 100: "#dbeafe",
+ 200: "#bfdbfe",
+ 300: "#93c5fd",
+ 400: "#60a5fa",
+ 500: "#3b82f6",
+ 600: "#2563eb",
+ 700: "#1d4ed8",
+ 800: "#1e40af",
+ 900: "#1e3a8a",
+ 950: "#172554",
+ },
+ "indigo": {
+ 50: "#eef2ff",
+ 100: "#e0e7ff",
+ 200: "#c7d2fe",
+ 300: "#a5b4fc",
+ 400: "#818cf8",
+ 500: "#6366f1",
+ 600: "#4f46e5",
+ 700: "#4338ca",
+ 800: "#3730a3",
+ 900: "#312e81",
+ 950: "#1e1b4b",
+ },
+ "violet": {
+ 50: "#f5f3ff",
+ 100: "#ede9fe",
+ 200: "#ddd6fe",
+ 300: "#c4b5fd",
+ 400: "#a78bfa",
+ 500: "#8b5cf6",
+ 600: "#7c3aed",
+ 700: "#6d28d9",
+ 800: "#5b21b6",
+ 900: "#4c1d95",
+ 950: "#2e1065",
+ },
+ "purple": {
+ 50: "#faf5ff",
+ 100: "#f3e8ff",
+ 200: "#e9d5ff",
+ 300: "#d8b4fe",
+ 400: "#c084fc",
+ 500: "#a855f7",
+ 600: "#9333ea",
+ 700: "#7e22ce",
+ 800: "#6b21a8",
+ 900: "#581c87",
+ 950: "#3b0764",
+ },
+ "fuchsia": {
+ 50: "#fdf4ff",
+ 100: "#fae8ff",
+ 200: "#f5d0fe",
+ 300: "#f0abfc",
+ 400: "#e879f9",
+ 500: "#d946ef",
+ 600: "#c026d3",
+ 700: "#a21caf",
+ 800: "#86198f",
+ 900: "#701a75",
+ 950: "#4a044e",
+ },
+ "pink": {
+ 50: "#fdf2f8",
+ 100: "#fce7f3",
+ 200: "#fbcfe8",
+ 300: "#f9a8d4",
+ 400: "#f472b6",
+ 500: "#ec4899",
+ 600: "#db2777",
+ 700: "#be185d",
+ 800: "#9d174d",
+ 900: "#831843",
+ 950: "#500724",
+ },
+ "rose": {
+ 50: "#fff1f2",
+ 100: "#ffe4e6",
+ 200: "#fecdd3",
+ 300: "#fda4af",
+ 400: "#fb7185",
+ 500: "#f43f5e",
+ 600: "#e11d48",
+ 700: "#be123c",
+ 800: "#9f1239",
+ 900: "#881337",
+ 950: "#4c0519",
+ },
+}
+
+THEME_DEFAULTS: Dict[str, Dict[str, str]] = {
+ "tokyo-night": {
+ "light": {
+ "text": "gray-900",
+ "muted": "gray-500",
+ "heading": "black",
+ "accent": "indigo-600",
+ "accent_alt": "purple-600",
+ "background": "white",
+ "surface": "gray-50",
+ "code_bg": "gray-100",
+ "blockquote_bg": "gray-100",
+ "blockquote_border": "indigo-300",
+ "link_hover": "black",
+ "selection_bg": "indigo-100",
+ "selection_text": "gray-900",
+ "border": "gray-200",
+ "background_image": "https://transparenttextures.com/patterns/black-thread-light.png",
+ },
+ "dark": {
+ "text": "gray-100",
+ "muted": "gray-400",
+ "heading": "white",
+ "accent": "indigo-400",
+ "accent_alt": "purple-400",
+ "background": "#1a1b26",
+ "surface": "#222436",
+ "code_bg": "#2f3549",
+ "blockquote_bg": "#1f2335",
+ "blockquote_border": "indigo-500",
+ "link_hover": "white",
+ "selection_bg": "#2f3549",
+ "selection_text": "white",
+ "border": "#3b4261",
+ # "background_image": "https://transparenttextures.com/patterns/dark-leather.png",
+ "background_image": "https://transparenttextures.com/patterns/black-scales.png",
+ },
+ },
+ "catppuccin": {
+ "light": {
+ "text": "slate-800",
+ "muted": "slate-500",
+ "heading": "slate-900",
+ "accent": "pink-500",
+ "accent_alt": "purple-400",
+ "background": "pink-50",
+ "surface": "pink-100",
+ "code_bg": "slate-100",
+ "blockquote_bg": "pink-100",
+ "blockquote_border": "pink-400",
+ "link_hover": "pink-800",
+ "selection_bg": "pink-200",
+ "selection_text": "slate-900",
+ "border": "pink-200",
+ "code_theme": "stata-light",
+ "background_image": "https://transparenttextures.com/patterns/gray-floral.png",
+ },
+ "dark": {
+ "text": "rose-200",
+ "muted": "rose-400",
+ "heading": "rose-100",
+ "accent": "pink-400",
+ "accent_alt": "violet-300",
+ "background": "#1e1e28",
+ "surface": "#2a2a38",
+ "code_bg": "#2c2c3a",
+ "blockquote_bg": "#2b2b3a",
+ "blockquote_border": "pink-500",
+ "link_hover": "white",
+ "selection_bg": "#403d52",
+ "selection_text": "rose-50",
+ "border": "#4e4e5a",
+ "code_theme": "dracula",
+ "background_image": "https://transparenttextures.com/patterns/crissxcross.png",
+ },
+ },
+ "everforest": {
+ "light": {
+ "text": "green-900",
+ "muted": "green-500",
+ "heading": "green-800",
+ "accent": "green-600",
+ "accent_alt": "lime-500",
+ "background": "green-50",
+ "surface": "green-100",
+ "code_bg": "green-100",
+ "blockquote_bg": "green-200",
+ "blockquote_border": "green-400",
+ "link_hover": "green-800",
+ "selection_bg": "green-200",
+ "selection_text": "green-900",
+ "border": "green-300",
+ "code_theme": "stata-light",
+ "background_image": "https://transparenttextures.com/patterns/cartographer.png",
+ },
+ "dark": {
+ "text": "green-100",
+ "muted": "green-400",
+ "heading": "green-300",
+ "accent": "green-400",
+ "accent_alt": "lime-400",
+ "background": "#2b3339",
+ "surface": "#374045",
+ "code_bg": "#3b444a",
+ "blockquote_bg": "#3d484f",
+ "blockquote_border": "green-500",
+ "link_hover": "white",
+ "selection_bg": "#475258",
+ "selection_text": "white",
+ "border": "#517d90",
+ "code_theme": "stata-dark",
+ "background_image": "https://transparenttextures.com/patterns/cartographer.png",
+ },
+ },
+ "gruvbox": {
+ "light": {
+ "text": "orange-900",
+ "muted": "orange-400",
+ "heading": "yellow-900",
+ "accent": "orange-600",
+ "accent_alt": "yellow-500",
+ "background": "white",
+ "surface": "orange-50",
+ "code_bg": "orange-100",
+ "blockquote_bg": "orange-200",
+ "blockquote_border": "orange-300",
+ "link_hover": "orange-800",
+ "selection_bg": "orange-200",
+ "selection_text": "orange-900",
+ "border": "orange-300",
+ "background_image": "https://transparenttextures.com/patterns/cartographer.png",
+ },
+ "dark": {
+ "text": "orange-100",
+ "muted": "orange-400",
+ "heading": "yellow-100",
+ "accent": "orange-400",
+ "accent_alt": "yellow-400",
+ "background": "#282828",
+ "surface": "#3c3836",
+ "code_bg": "#504945",
+ "blockquote_bg": "#3a3634",
+ "blockquote_border": "orange-500",
+ "link_hover": "white",
+ "selection_bg": "#665c54",
+ "selection_text": "orange-50",
+ "border": "#7c6f64",
+ "background_image": "https://transparenttextures.com/patterns/green-dust-and-scratches.png",
+ },
+ },
+ "kanagwa": {
+ "light": {
+ "text": "slate-900",
+ "muted": "slate-400",
+ "heading": "slate-800",
+ "accent": "blue-600",
+ "accent_alt": "indigo-500",
+ "background": "slate-50",
+ "surface": "slate-100",
+ "code_bg": "slate-100",
+ "blockquote_bg": "slate-200",
+ "blockquote_border": "blue-300",
+ "link_hover": "blue-800",
+ "selection_bg": "blue-100",
+ "selection_text": "slate-900",
+ "border": "slate-300",
+ "background_image": "https://transparenttextures.com/patterns/white-wave.png",
+ },
+ "dark": {
+ "text": "slate-100",
+ "muted": "slate-400",
+ "heading": "slate-50",
+ "accent": "blue-400",
+ "accent_alt": "indigo-400",
+ "background": "#1f2335",
+ "surface": "#2a2e3e",
+ "code_bg": "#3a3f52",
+ "blockquote_bg": "#2e3440",
+ "blockquote_border": "blue-500",
+ "link_hover": "white",
+ "selection_bg": "#394260",
+ "selection_text": "white",
+ "border": "#4b5162",
+ "background_image": "https://transparenttextures.com/patterns/white-wave.png",
+ },
+ },
+ "nord": {
+ "light": {
+ "text": "cyan-900",
+ "muted": "cyan-400",
+ "heading": "cyan-800",
+ "accent": "cyan-600",
+ "accent_alt": "blue-500",
+ "background": "cyan-200",
+ "surface": "cyan-100",
+ "code_bg": "cyan-50",
+ "blockquote_bg": "cyan-200",
+ "blockquote_border": "cyan-300",
+ "link_hover": "cyan-800",
+ "selection_bg": "cyan-200",
+ "selection_text": "cyan-900",
+ "border": "cyan-300",
+ "code_theme": "solarized-light",
+ "background_image": "https://transparenttextures.com/patterns/green-gobbler.png",
+ },
+ "dark": {
+ "text": "cyan-100",
+ "muted": "cyan-400",
+ "heading": "cyan-50",
+ "accent": "cyan-400",
+ "accent_alt": "blue-300",
+ "background": "#2e3440",
+ "surface": "#3b4252",
+ "code_bg": "#434c5e",
+ "blockquote_bg": "#4c566a",
+ "blockquote_border": "cyan-500",
+ "link_hover": "white",
+ "selection_bg": "#5e81ac",
+ "selection_text": "cyan-50",
+ "border": "#6b7d97",
+ "code_theme": "nord-darker",
+ # "background_image": "https://transparenttextures.com/patterns/green-gobbler.png",
+ # "background_image": "https://transparenttextures.com/patterns/stardust.png",
+ # "background_image": "https://transparenttextures.com/patterns/asfalt-dark.png",
+ },
+ },
+ "synthwave-84": {
+ "light": {
+ "text": "purple-900",
+ "muted": "pink-500",
+ "heading": "fuchsia-800",
+ "accent": "pink-500",
+ "accent_alt": "fuchsia-500",
+ "background": "pink-50",
+ "surface": "pink-100",
+ "code_bg": "pink-100",
+ "blockquote_bg": "pink-200",
+ "blockquote_border": "pink-400",
+ "link_hover": "purple-800",
+ "selection_bg": "fuchsia-200",
+ "selection_text": "purple-900",
+ "border": "pink-300",
+ "code_theme": "monokai",
+ },
+ "dark": {
+ "text": "#ff00ff",
+ "muted": "#c060c0",
+ "heading": "#ff66ff",
+ "accent": "pink-400",
+ "accent_alt": "fuchsia-400",
+ "background": "#2d0036",
+ "surface": "#440055",
+ "code_bg": "#3d0047",
+ "blockquote_bg": "#520066",
+ "blockquote_border": "pink-500",
+ "link_hover": "white",
+ "selection_bg": "#8800aa",
+ "selection_text": "#ffffff",
+ "border": "#ff00ff",
+ "code_theme": "monokai",
+ },
+ },
+}
diff --git a/markata/standard_config.py b/markata/standard_config.py
index f53d52be8..cbbee9e37 100644
--- a/markata/standard_config.py
+++ b/markata/standard_config.py
@@ -440,15 +440,32 @@ def _load_files(config_path_specs: path_spec_type) -> Dict[str, Any]:
def _load_env(tool: str) -> Dict[str, Any]:
"""Load config from environment variables.
+ Supports nested configuration with double underscore:
+ MARKATA_STYLE__THEME=nord -> {"style": {"theme": "nord"}}
+
Args:
tool (str): name of the tool to configure
"""
env_prefix = tool.upper()
- env_config = {
- key.replace(f"{env_prefix}_", "").lower(): value
- for key, value in os.environ.items()
- if key.startswith(f"{env_prefix}_")
- }
+ env_config = {}
+
+ for key, value in os.environ.items():
+ if key.startswith(f"{env_prefix}_"):
+ # Remove prefix
+ config_key = key.replace(f"{env_prefix}_", "").lower()
+
+ # Handle nested keys with double underscore
+ if "__" in config_key:
+ keys = config_key.split("__")
+ current = env_config
+ for k in keys[:-1]:
+ if k not in current:
+ current[k] = {}
+ current = current[k]
+ current[keys[-1]] = value
+ else:
+ env_config[config_key] = value
+
return env_config
@@ -456,13 +473,14 @@ def load(
tool: str,
project_home: Union[Path, str] = ".",
overrides: Optional[Dict[str, Any]] = None,
+ config_file: Optional[Union[Path, str]] = None,
) -> Dict[str, Any]:
"""Load tool config from standard config files.
Resolution Order
* First global file with a tool key
- * First local file with a tool key
+ * First local file with a tool key (or specific config_file if provided)
* Environment variables prefixed with `TOOL`
* Overrides
@@ -470,6 +488,7 @@ def load(
tool (str): name of the tool to configure
project_home (Union[Path, str], optional): Project directory to search for config files. Defaults to ".".
overrides (Dict, optional): Override values to apply last. Defaults to None.
+ config_file (Union[Path, str], optional): Specific config file to load instead of searching. Defaults to None.
Returns:
Dict[str, Any]: Configuration object
@@ -479,7 +498,39 @@ def load(
# Load from files in order of precedence
config.update(_load_files(_get_global_path_specs(tool)) or {})
- config.update(_load_files(_get_local_path_specs(tool, project_home)) or {})
+
+ # If a specific config file is provided, use it instead of searching
+ if config_file:
+ config_path = Path(config_file)
+ if not config_path.exists():
+ raise FileNotFoundError(f"Config file not found: {config_file}")
+
+ # Determine parser from file extension
+ suffix = config_path.suffix.lower()
+ if suffix == ".toml":
+ parser = "toml"
+ elif suffix in (".yml", ".yaml"):
+ parser = "yaml"
+ elif suffix in (".ini", ".cfg"):
+ parser = "ini"
+ else:
+ # Try toml as default
+ parser = "toml"
+
+ file_spec = {
+ "path_specs": config_path,
+ "parser": parser,
+ "keys": [tool]
+ if parser == "ini"
+ else (["tool", tool] if parser == "toml" else [tool]),
+ }
+
+ file_config = _load_config_file(file_spec)
+ if file_config:
+ config.update(file_config)
+ else:
+ config.update(_load_files(_get_local_path_specs(tool, project_home)) or {})
+
config.update(_load_env(tool))
config.update(overrides)
diff --git a/markata/templates/base.html b/markata/templates/base.html
index ad274ad9e..acef06d8b 100644
--- a/markata/templates/base.html
+++ b/markata/templates/base.html
@@ -1,50 +1,13 @@
- {% include "base_head.html" %}
-
{% block head %}
{% include 'base_head.html' %}
{% endblock %}
-
-
-
+
+
{% block body %}
{% block content %} {% endblock %}
{% block footer %} {% endblock %}
diff --git a/pyproject.toml b/pyproject.toml
index 404fb173a..5aa2688d9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,7 +65,7 @@ description = "Static site generator plugins all the way down."
keywords = ["static-site"]
name = "markata"
readme = "README.md"
-requires-python = ">=3.6"
+requires-python = ">=3.9"
[[project.authors]]
name = "Waylon Walker"
diff --git a/static/htmx.js b/static/htmx.js
new file mode 100644
index 000000000..f889d0ea5
--- /dev/null
+++ b/static/htmx.js
@@ -0,0 +1 @@
+(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var z={onLoad:t,process:Tt,on:le,off:ue,trigger:ie,ajax:dr,find:b,findAll:f,closest:d,values:function(e,t){var r=Jt(e,t||"post");return r.values},remove:B,addClass:j,removeClass:n,toggleClass:U,takeClass:V,defineExtension:yr,removeExtension:br,logAll:F,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=z.config.wsBinaryType;return t},version:"1.9.2"};var C={addTriggerHandler:xt,bodyContains:ee,canAccessLocalStorage:D,filterValues:er,hasAttribute:q,getAttributeValue:G,getClosestMatch:c,getExpressionVars:fr,getHeaders:Qt,getInputValues:Jt,getInternalData:Y,getSwapSpecification:rr,getTriggerSpecs:ze,getTarget:de,makeFragment:l,mergeObjects:te,makeSettleInfo:S,oobSwap:me,selectAndSwap:Me,settleImmediately:Bt,shouldCancel:Ke,triggerEvent:ie,triggerErrorEvent:ne,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function $(e,t){return e.getAttribute&&e.getAttribute(t)}function q(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function G(e,t){return $(e,t)||$(e,"data-"+t)}function u(e){return e.parentElement}function J(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=G(t,r);var i=G(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Z(t,r){var n=null;c(t,function(e){return n=T(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function H(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=J().createDocumentFragment()}return i}function L(e){return e.match(/"+e+"",0);return r.querySelector("template").content}else{var n=H(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("