Add head configuration

This snippet allows users to configure their head in markata.toml.


{{ config.get('head', {}).pop('text') if 'text' in config.get('head',{}).keys() }}
{% for tag, meta in config.get('head', {}).items() %}
    {% for _meta in meta %}
        <{{ tag }}
            {% for attr, value in _meta.items() %}{{ attr }}="{{ value }}"{% endfor %}
        />
    {% endfor %}
{% endfor %}

Users can specify any sort of tag in their markata.toml


[[markata.head.meta]]
name = "og:type"
content = "article"

[[markata.head.meta]]
name = "og:author"
content = "Waylon Walker"

The above configuration becomes this once rendered.


<meta name='og:type' content='article' />
<meta name='og:Author' content='Waylon Walker' />

!! Note

Article variables can be used for dynamic entries like canonical_url
``` toml
[markata]
url = "markata.dev"

[[markata.head.meta]]
href="{{ config.url }}/{{ slug }}/"
rel="canonical"
```

Optionally users can also specify plain text to be appended to the head of their documents. This works well for things that involve full blocks.


[[markata.head.text]]
value = '''
<script>
    console.log('hello world')
</script>
'''

[[markata.head.text]]
value='''
html  {
    font-family: "Space Mono", monospace;
    background: var(--color-bg);
    color: var(--color-text);
}
'''

Add scripts to head

Markata config also supports adding scripts to the head via configuration.


[[ markata.head.script ]]
    src = "https://cdn.tailwindcss.com"

SilentUndefined class

SilentUndefined source


        class SilentUndefined(Undefined):
            def _fail_with_undefined_error(self, *args, **kwargs):
                return ""

optional function

optional source


        def optional(*fields):
            def dec(_cls):
                for field in fields:
                    _cls.__fields__[field].default = None
                return _cls

            if (
                fields
                and inspect.isclass(fields[0])
                and issubclass(fields[0], pydantic.BaseModel)
            ):
                cls = fields[0]
                fields = cls.__fields__
                return dec(cls)
            return dec

Style class

Style source


        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"

StyleOverrides class

StyleOverrides source


        class StyleOverrides(Style): ...

Meta class

Meta source


        class Meta(pydantic.BaseModel):
            name: str
            content: str

Text class

Text source


        class Text(pydantic.BaseModel):
            value: str

Link source


        class Link(pydantic.BaseModel):
            rel: str = "canonical"
            href: str

Script class

Script source


        class Script(pydantic.BaseModel):
            src: str

HeadConfig class

HeadConfig source


        class HeadConfig(pydantic.BaseModel):
            meta: List[Meta] = []
            link: List[Link] = []
            script: List[Script] = []
            text: Union[List[Text], str] = ""

            @pydantic.validator("text", pre=True)
            def text_to_list(cls, v):
                if isinstance(v, list):
                    return "\n".join([text["value"] for text in v])
                return v

            @property
            def html(self):
                html = self.text
                html += "\n"
                for meta in self.meta:
                    html += f'<meta name="{meta.name}" content="{meta.content}" />\n'
                for link in self.link:
                    html += f'<link rel="{link.rel}" href="{link.href}" />\n'
                return html

Config class

Config source


        class Config(pydantic.BaseModel):
            head: HeadConfig = HeadConfig()
            style: Style = Style()
            post_template: Optional[Union[str | Dict[str, str]]] = "post.html"
            dynamic_templates_dir: Path = Path(".markata.cache/templates")
            templates_dir: Union[Path, List[Path]] = pydantic.Field(Path("templates"))

            env_options: dict = {}

            @pydantic.model_validator(mode="after")
            def dynamic_templates_in_templates_dir(self):
                markata_templates = Path(__file__).parents[1] / "templates"

                if isinstance(self.templates_dir, Path):
                    self.templates_dir = [
                        self.templates_dir,
                        markata_templates,
                        self.dynamic_templates_dir,
                    ]

                if markata_templates not in self.templates_dir:
                    self.templates_dir.append(markata_templates)

                if self.dynamic_templates_dir not in self.templates_dir:
                    self.templates_dir.append(self.dynamic_templates_dir)

                return self

            @property
            def jinja_loader(self):
                return jinja2.FileSystemLoader(self.templates_dir)

            @property
            def jinja_env(
                self,
            ):
                if hasattr(self, "_jinja_env"):
                    return self._jinja_env
                self.env_options.setdefault("loader", self.jinja_loader)
                self.env_options.setdefault("undefined", SilentUndefined)
                self.env_options.setdefault("lstrip_blocks", True)
                self.env_options.setdefault("trim_blocks", True)

                env = jinja2.Environment(**self.env_options)

                self._jinja_env = env
                return env

PostOverrides class

PostOverrides source


        class PostOverrides(pydantic.BaseModel):
            head: HeadConfig = HeadConfig()
            style: Style = StyleOverrides()

Post class

Post source


        class Post(pydantic.BaseModel):
            config_overrides: PostOverrides = PostOverrides()
            template: Optional[str | Dict[str, str]] = None

            @pydantic.validator("template", pre=True, always=True)
            def default_template(cls, v, *, values):
                if v is None:
                    return values["markata"].config.post_template
                if isinstance(v, str):
                    v = {"index": v}
                if isinstance(values["markata"].config.post_template, str):
                    config_template = {
                        "index": values["markata"].config.post_template,
                    }
                else:
                    config_template = values["markata"].config.post_template
                return {**config_template, **v}

config_model function

config_model source


        def config_model(markata: "Markata") -> None:
            markata.config_models.append(Config)

post_model function

post_model source


        def post_model(markata: "Markata") -> None:
            markata.post_models.append(Post)

configure function

Massages the configuration limitations of toml to allow a little bit easier experience to the end user making configurations while allowing an simpler jinja template. This enablees the use of the markata.head.text list in configuration.

configure source


        def configure(markata: "Markata") -> None:
            """
            Massages the configuration limitations of toml to allow a little bit easier
            experience to the end user making configurations while allowing an simpler
            jinja template.  This enablees the use of the `markata.head.text` list in
            configuration.
            """

pre_render function

FOR EACH POST: Massages the configuration limitations of toml/yaml to allow a little bit easier experience to the end user making configurations while allowing an simpler jinja template. This enables the use of the markata.head.text list in configuration.

pre_render source


        def pre_render(markata: "Markata") -> None:
            """
            FOR EACH POST: Massages the configuration limitations of toml/yaml to allow
            a little bit easier experience to the end user making configurations while
            allowing an simpler jinja template.  This enables the use of the
            `markata.head.text` list in configuration.
            """

            markata.config.dynamic_templates_dir.mkdir(parents=True, exist_ok=True)
            head_template = markata.config.dynamic_templates_dir / "head.html"
            head_template.write_text(
                markata.config.jinja_env.get_template("dynamic_head.html").render(
                    {"markata": markata}
                ),
            )

            for article in [a for a in markata.articles if "config_overrides" in a]:
                raw_text = article.get("config_overrides", {}).get("head", {}).get("text", "")

                if isinstance(raw_text, list):
                    article["config_overrides"]["head"]["text"] = "\n".join(
                        flatten([t.values() for t in raw_text]),
                    )

render function

render source


        def render(markata: "Markata") -> None:
            with markata.cache as cache:
                for article in markata.articles:
                    html = render_article(markata=markata, cache=cache, article=article)
                    article.html = html

get_template function

get_template source


        def get_template(markata, template):
            try:
                return markata.config.jinja_env.get_template(template)
            except jinja2.TemplateNotFound:
                # try to load it as a file
                ...

            try:
                return Template(Path(template).read_text(), undefined=SilentUndefined)
            except FileNotFoundError:
                # default to load it as a string
                ...
            return Template(template, undefined=SilentUndefined)

render_article function

render_article source


        def render_article(markata, cache, article):
            key = markata.make_hash(
                "post_template",
                __version__,
                article.key,
            )
            html = markata.precache.get(key)

            if html is not None:
                return html

            if isinstance(article.template, str):
                template = get_template(markata, article.template)
                html = render_template(markata, article, template)

            if isinstance(article.template, dict):
                html = {
                    slug: render_template(markata, article, get_template(markata, template))
                    for slug, template in article.template.items()
                }
            cache.add(key, html, expire=markata.config.default_cache_expire)
            return html

render_template function

render_template source


        def render_template(markata, article, template):
            template = get_template(markata, template)
            merged_config = markata.config
            # TODO do we need to handle merge??
            # if head_template:
            #     head = eval(
            #         head_template.render(
            #             __version__=__version__,
            #             config=_full_config,
            #             **article,
            #         )
            #     )

            # merged_config = {
            #     **_full_config,
            #     **{"head": head},
            # }

            # merged_config = always_merger.merge(
            #     merged_config,
            #     copy.deepcopy(
            #         article.get(
            #             "config_overrides",
            #             {},
            #         )
            #     ),
            # )

            html = template.render(
                __version__=__version__,
                markata=markata,
                body=article.article_html,
                config=merged_config,
                post=article,
            )
            return html

save function

save source


        def save(markata: "Markata") -> None:
            linked_templates = [
                t
                for t in markata.config.jinja_env.list_templates()
                if t.endswith("css") or t.endswith("js") or t.endswith("xsl")
            ]
            for template in linked_templates:
                template = get_template(markata, template)
                css = template.render(markata=markata, __version__=__version__)
                Path(markata.config.output_dir / Path(template.filename).name).write_text(css)

cli function

Markata hook to implement base cli commands.

cli source


        def cli(app: typer.Typer, markata: "Markata") -> None:
            """
            Markata hook to implement base cli commands.
            """

            templates_app = typer.Typer()
            app.add_typer(templates_app)

            @templates_app.callback()
            def templates():
                "template management"

            @templates_app.command()
            def show(
                template: str = typer.Argument(None, help="template to show"),
                theme: str = typer.Option(None, help="pygments syntax theme"),
            ) -> None:
                markata.console.quiet = True
                if template:
                    template = get_template(markata, template)

                    markata.console.quiet = False
                    markata.console.print(template.filename)
                    if theme is None or theme.lower() == "none":
                        markata.console.print(Path(template.filename).read_text())
                    else:
                        syntax = Syntax.from_path(template.filename, theme=theme)
                        markata.console.print(syntax)

                    return
                templates = markata.config.jinja_env.list_templates()
                markata.console.quiet = False
                markata.console.print("Templates directories:", style="green underline")

                markata_templates = Path(__file__).parents[1] / "templates"
                for dir in markata.config.templates_dir:
                    if dir == markata.config.dynamic_templates_dir:
                        markata.console.print(
                            f"[gold3]{dir}[/][grey50] (dynamically created templates from configuration)[/] [gold3]\[markata.config.dynamic_templates_dir][/]",
                            style="red",
                        )
                    elif dir == markata_templates:
                        markata.console.print(
                            f"[cyan]{dir}[/][grey50] (built-in)[/]", style="red"
                        )
                    else:
                        markata.console.print(
                            f"[orchid]{dir}[/] [orchid]\[markata.config.templates_dir][/]",
                            style="red",
                        )

                markata.console.print()
                markata.console.print(
                    "Available Templates: [white]name -> path[/]", style="green underline"
                )
                for template in templates:
                    source, file, uptodate = markata.config.jinja_env.loader.get_source(
                        markata.config.jinja_env, template
                    )

                    if Path(file).is_relative_to(markata.config.dynamic_templates_dir):
                        markata.console.print(
                            f"[gold3]{template} -> [red]{file}[/] [grey50](dynamic)[/]"
                        )
                    elif Path(file).is_relative_to(markata_templates):
                        markata.console.print(
                            f"[cyan]{template} -> [red]{file}[/] [grey50](built-in)[/]"
                        )
                    else:
                        markata.console.print(f"[orchid]{template}[/] -> [red]{file}[/]")

_fail_with_undefined_error method

_fail_with_undefined_error source


        def _fail_with_undefined_error(self, *args, **kwargs):
                return ""

dec function

dec source


        def dec(_cls):
                for field in fields:
                    _cls.__fields__[field].default = None
                return _cls

text_to_list method

text_to_list source


        def text_to_list(cls, v):
                if isinstance(v, list):
                    return "\n".join([text["value"] for text in v])
                return v

html method

html source


        def html(self):
                html = self.text
                html += "\n"
                for meta in self.meta:
                    html += f'<meta name="{meta.name}" content="{meta.content}" />\n'
                for link in self.link:
                    html += f'<link rel="{link.rel}" href="{link.href}" />\n'
                return html

dynamic_templates_in_templates_dir method

dynamic_templates_in_templates_dir source


        def dynamic_templates_in_templates_dir(self):
                markata_templates = Path(__file__).parents[1] / "templates"

                if isinstance(self.templates_dir, Path):
                    self.templates_dir = [
                        self.templates_dir,
                        markata_templates,
                        self.dynamic_templates_dir,
                    ]

                if markata_templates not in self.templates_dir:
                    self.templates_dir.append(markata_templates)

                if self.dynamic_templates_dir not in self.templates_dir:
                    self.templates_dir.append(self.dynamic_templates_dir)

                return self

jinja_loader method

jinja_loader source


        def jinja_loader(self):
                return jinja2.FileSystemLoader(self.templates_dir)

jinja_env method

jinja_env source


        def jinja_env(
                self,
            ):
                if hasattr(self, "_jinja_env"):
                    return self._jinja_env
                self.env_options.setdefault("loader", self.jinja_loader)
                self.env_options.setdefault("undefined", SilentUndefined)
                self.env_options.setdefault("lstrip_blocks", True)
                self.env_options.setdefault("trim_blocks", True)

                env = jinja2.Environment(**self.env_options)

                self._jinja_env = env
                return env

default_template method

default_template source


        def default_template(cls, v, *, values):
                if v is None:
                    return values["markata"].config.post_template
                if isinstance(v, str):
                    v = {"index": v}
                if isinstance(values["markata"].config.post_template, str):
                    config_template = {
                        "index": values["markata"].config.post_template,
                    }
                else:
                    config_template = values["markata"].config.post_template
                return {**config_template, **v}

templates function

template management

templates source


        def templates():
                "template management"

show function

show source


        def show(
                template: str = typer.Argument(None, help="template to show"),
                theme: str = typer.Option(None, help="pygments syntax theme"),
            ) -> None:
                markata.console.quiet = True
                if template:
                    template = get_template(markata, template)

                    markata.console.quiet = False
                    markata.console.print(template.filename)
                    if theme is None or theme.lower() == "none":
                        markata.console.print(Path(template.filename).read_text())
                    else:
                        syntax = Syntax.from_path(template.filename, theme=theme)
                        markata.console.print(syntax)

                    return
                templates = markata.config.jinja_env.list_templates()
                markata.console.quiet = False
                markata.console.print("Templates directories:", style="green underline")

                markata_templates = Path(__file__).parents[1] / "templates"
                for dir in markata.config.templates_dir:
                    if dir == markata.config.dynamic_templates_dir:
                        markata.console.print(
                            f"[gold3]{dir}[/][grey50] (dynamically created templates from configuration)[/] [gold3]\[markata.config.dynamic_templates_dir][/]",
                            style="red",
                        )
                    elif dir == markata_templates:
                        markata.console.print(
                            f"[cyan]{dir}[/][grey50] (built-in)[/]", style="red"
                        )
                    else:
                        markata.console.print(
                            f"[orchid]{dir}[/] [orchid]\[markata.config.templates_dir][/]",
                            style="red",
                        )

                markata.console.print()
                markata.console.print(
                    "Available Templates: [white]name -> path[/]", style="green underline"
                )
                for template in templates:
                    source, file, uptodate = markata.config.jinja_env.loader.get_source(
                        markata.config.jinja_env, template
                    )

                    if Path(file).is_relative_to(markata.config.dynamic_templates_dir):
                        markata.console.print(
                            f"[gold3]{template} -> [red]{file}[/] [grey50](dynamic)[/]"
                        )
                    elif Path(file).is_relative_to(markata_templates):
                        markata.console.print(
                            f"[cyan]{template} -> [red]{file}[/] [grey50](built-in)[/]"
                        )
                    else:
                        markata.console.print(f"[orchid]{template}[/] -> [red]{file}[/]")