diff options
| author | Dennis Fink | 2026-06-18 17:57:51 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-06-18 17:57:51 +0200 |
| commit | a4e35942fee745e64db5d0099ba2582e0fb4417e (patch) | |
| tree | f109c3e77805cdacd6f443c8a2d47514be5254a2 /packages | |
| parent | e0904e48a5c1c5b327c29218fb39bf2fa05f1871 (diff) | |
| download | dennisfink.me-a4e35942fee745e64db5d0099ba2582e0fb4417e.tar.gz dennisfink.me-a4e35942fee745e64db5d0099ba2582e0fb4417e.zip | |
Build the first usable version of the blog
This adds the Lektor project structure, models, templates, styling,
feeds, categories, tags, archives, custom Markdown heading handling, and
the initial content needed to make the blog usable.
Diffstat (limited to '')
| -rw-r--r-- | packages/blog-markdown-headings/lektor_blog_markdown_headings.py | 152 | ||||
| -rw-r--r-- | packages/blog-markdown-headings/setup.py | 13 |
2 files changed, 165 insertions, 0 deletions
diff --git a/packages/blog-markdown-headings/lektor_blog_markdown_headings.py b/packages/blog-markdown-headings/lektor_blog_markdown_headings.py new file mode 100644 index 0000000..5293676 --- /dev/null +++ b/packages/blog-markdown-headings/lektor_blog_markdown_headings.py @@ -0,0 +1,152 @@ +import uuid +from collections import namedtuple +from html.parser import HTMLParser + +import mistune +from lektor.pluginsystem import Plugin +from lektor.utils import slugify +from markupsafe import Markup, escape + +TocEntry = namedtuple("TocEntry", ["anchor", "title", "children"]) + + +def get_int_config(config, key, default, minimum=None, maximum=None): + value = config.get(key, str(default)) + + try: + value = int(value) + except (TypeError, ValueError): + value = default + + if minimum is not None: + value = max(value, minimum) + + if maximum is not None: + value = min(value, maximum) + + return value + + +class BlogMarkdownHeadingsPlugin(Plugin): + name = "Blog Markdown Headings" + description = ( + "Adds Markdown heading anchors, TOC metadata, and configurable heading offsets." + ) + + def on_markdown_config(self, config, **extra): + plugin_config = self.get_config() + + anchor_type = plugin_config.get("anchors.type", "slug") + + heading_offset = get_int_config( + plugin_config, + "headings.offset", + default=2, + minimum=-5, + maximum=5, + ) + + max_heading_level = get_int_config( + plugin_config, + "headings.max_level", + default=6, + minimum=1, + maximum=6, + ) + + min_heading_level = get_int_config( + plugin_config, + "headings.min_level", + default=1, + minimum=1, + maximum=6, + ) + + def adjusted_level(level): + return min( + max(level + heading_offset, min_heading_level), + max_heading_level, + ) + + config.renderer_mixins.append( + renderer_mixin_factory( + anchor_type=anchor_type, + adjusted_level=adjusted_level, + ) + ) + + def on_markdown_meta_init(self, meta, **extra): + meta["toc"] = [] + + def on_markdown_meta_postprocess(self, meta, **extra): + prev_level = None + toc = [] + stack = [toc] + + for level, anchor, title in meta["toc"]: + if prev_level is None: + prev_level = level + elif prev_level == level - 1: + stack.append(stack[-1][-1].children) + prev_level = level + elif prev_level > level: + while prev_level > level: + if len(stack) > 1: + stack.pop() + prev_level -= 1 + + stack[-1].append(TocEntry(anchor, title, [])) + + meta["toc"] = toc + + +def renderer_mixin_factory(anchor_type, adjusted_level): + def render_heading(text, original_level, meta, raw=None): + level = adjusted_level(original_level) + + if anchor_type == "random": + anchor = uuid.uuid4().hex[:6] + else: + if raw is None: + raw = escape(_strip_tags(text)) + anchor = slugify(raw) + + meta["toc"].append((level, anchor, Markup(text))) + + return '<h%d id="%s">%s</h%d>' % (level, anchor, text, level) + + class RendererMixinMistune0(object): + def header(self, text, level, raw): + return render_heading(text, level, self.meta, raw) + + class RendererMixinMistune2(object): + def heading(self, text, level): + meta = self.lektor.meta + return render_heading(text, level, meta) + + if mistune.__version__.startswith("0."): + return RendererMixinMistune0 + + if mistune.__version__.startswith("2."): + return RendererMixinMistune2 + + raise RuntimeError("Unsupported mistune version: %s" % mistune.__version__) + + +def _strip_tags(inline_html): + parser = _StripTagsParser() + parser.feed(inline_html) + return parser.close() + + +class _StripTagsParser(HTMLParser): + def reset(self): + super().reset() + self._data = [] + + def handle_data(self, data): + self._data.append(data) + + def close(self): + super().close() + return "".join(self._data) diff --git a/packages/blog-markdown-headings/setup.py b/packages/blog-markdown-headings/setup.py new file mode 100644 index 0000000..59f9895 --- /dev/null +++ b/packages/blog-markdown-headings/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +setup( + name="lektor-blog-markdown-headings", + version="0.1", + py_modules=["lektor_blog_markdown_headings"], + entry_points={ + "lektor.plugins": [ + "blog-markdown-headings = lektor_blog_markdown_headings:BlogMarkdownHeadingsPlugin", + ] + }, + install_requires=[], +) |
