diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b0817f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+.DS_Store
+/dist-env/
+
+__pycache__/
+*.py[cod]
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6db61c3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,7 @@
+Copyright 2022 relikd
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3dea4e5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+.PHONY: help
+help:
+ @echo 'commands:'
+ @echo ' dist'
+
+dist-env:
+ @echo Creating virtual environment...
+ @python3 -m venv 'dist-env'
+ @source dist-env/bin/activate && pip install twine
+
+.PHONY: dist
+dist: dist-env
+ [ -z "$${VIRTUAL_ENV}" ] # you can not do this inside a virtual environment.
+ rm -rf dist
+ @echo Building...
+ python3 setup.py sdist bdist_wheel
+ @echo
+ rm -rf ./*.egg-info/ ./build/ MANIFEST
+ @echo Publishing...
+ @echo "\033[0;31mEnter your PyPI token:\033[0m"
+ @source dist-env/bin/activate && export TWINE_USERNAME='__token__' && twine upload dist/*
diff --git a/README.md b/README.md
index 828ace8..a100e5f 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,14 @@
-Example config file:
+# lektor plugin: inlinetags
+
+### Default config file
```ini
-template = {title}
+root = /
+template = inlinetag.html
+regex = {{([^}]{1,32})}}
+link = tags/{tag}/
+replace = {title}
-[replace]
-C# = C-Sharp
+[slugs]
+C# = c-sharp
```
\ No newline at end of file
diff --git a/lektor_inlinetags.py b/lektor_inlinetags.py
index 504536b..2f5cc83 100644
--- a/lektor_inlinetags.py
+++ b/lektor_inlinetags.py
@@ -1,93 +1,109 @@
# -*- coding: utf-8 -*-
-import re
-from typing import List, Tuple
-from lektor.context import get_ctx
-from lektor.db import Page
-from lektor.markdown import Markdown
+from lektor.markdown import Markdown, Markup
from lektor.pluginsystem import Plugin
-from lektor.types.flow import Flow, FlowBlock, FlowDescriptor
+from lektor.reporter import reporter, style
from lektor.types.formats import MarkdownDescriptor
from lektor.utils import slugify
+import re
+from typing import Set, Dict, Iterator, Tuple
+from collections import namedtuple
-_regex = re.compile(r'{{[^}]{1,32}}}')
-_tag_repl: List[Tuple[str, str]] = []
+InlineTag = namedtuple('InlineTag', ['slug', 'title', 'link'])
-def tagify(text):
- for old, new in _tag_repl:
- text = text.replace(old, new)
- return slugify(text)
+class InlineTagsPlugin(Plugin):
+ name = 'InlineTags'
+ description = 'Auto-detect and reference tags inside written text.'
+ def on_setup_env(self, **extra) -> None:
+ def _iter_inlinetags(record, *, children=False) -> Set[InlineTag]:
+ res = set()
+ if hasattr(record, 'inlinetags'):
+ res.update(record.inlinetags.values())
+ if children and hasattr(record, 'children'):
+ for child in record.children:
+ res.update(_iter_inlinetags(child, children=True))
+ return res
-def _recursive_tags(obj):
- res = {}
- source = [obj]
- while source:
- x = source.pop()
- if isinstance(x, Markdown):
- res.update(x.meta['tags'])
- elif isinstance(x, FlowBlock) or isinstance(x, Page):
- for key, field in x._data.items():
- if isinstance(field, Markdown):
- source.append(field)
- elif isinstance(field, (MarkdownDescriptor, FlowDescriptor)):
- source.append(x[key])
- elif isinstance(x, Flow):
- source.extend(x.blocks)
- else:
- print(type(x))
- return res.items()
+ self.env.jinja_env.filters.update(inlinetags=_iter_inlinetags)
-
-class AutoTagMixin(object):
- def _replace_tag(self, match):
- title = match.group()[2:-2]
- slug = tagify(title)
- self.meta['tags'][slug] = title
- return self.meta['tag_template'].format(tag=slug, title=title)
-
- def text(self, text):
- return _regex.sub(self._replace_tag, super().text(text))
-
-
-# Wrapper with an __html__ method prevents
-# Lektor from escaping HTML tags.
-class HTML(object):
- def __init__(self, html):
- self.html = html
-
- def __html__(self):
- return self.html
-
-
-class Main(Plugin):
- def _to_tag(self, title, slug=None):
- if not slug:
- slug = tagify(title)
- get_ctx().record_dependency(self.config_filename)
- return HTML(self.tag_template.format(tag=slug, title=title))
-
- def on_setup_env(self, **extra):
- self.env.jinja_env.globals.update(
- make_tag=self._to_tag
- )
- self.env.jinja_env.filters.update(
- tags=_recursive_tags
- )
-
- def on_before_build_all(self, builder, **extra):
- global _tag_repl
+ def on_groupby_before_build_all(self, groupby, **extra) -> None:
+ # load config
config = self.get_config()
- self.tag_template = config.get('template', '{title}')
- _tag_repl = config.section_as_dict('replace').items()
+ root = config.get('root', '/') # type: str
+ temp = config.get('template', 'inlinetag.html') # type: str
+ regex = config.get('regex', r'{{([^}]{1,32})}}') # type: re.Pattern
+ rel_url = config.get('link', 'tags/{tag}/') # type: str
+ link_format = config.get('replace', '{title}') # type: str
+ slug_map = config.section_as_dict('slugs') # type: Dict
- def on_markdown_config(self, config, **extra):
- config.renderer_mixins.append(AutoTagMixin)
+ # normalize and validate input
+ try:
+ regex = re.compile(regex)
+ except Exception as e:
+ err = 'inlinetags.regex not valid: ' + str(e)
+ reporter._write_line(style(err, fg='red'))
+ return
+ if rel_url.endswith('/index.html'):
+ rel_url = rel_url[:-10]
+ root = root.rstrip('/') + '/' # ensure end slash
+ abs_url = rel_url if rel_url.startswith('/') else root + rel_url
+ rel_url = rel_url.replace('{tag}', '{group}')
- def on_markdown_meta_init(self, meta, record, **extra):
- meta['tags'] = {}
- meta['tag_template'] = self.tag_template
+ # detect and replace tags
+ @groupby.depends_on(self.config_filename)
+ @groupby.watch(root, 'inlinetags', slug=rel_url, template=temp)
+ def convert_inlinetags(args) -> Iterator[Tuple[str, InlineTag]]:
+ arr = args.field if isinstance(args.field, list) else [args.field]
+ tmptags = {} # type: Dict[str, InlineTag]
+ for obj in arr:
+ if isinstance(obj, (Markdown, MarkdownDescriptor)):
+ obj = obj.source
+ if isinstance(obj, str) and str:
+ for match in regex.finditer(obj):
+ title = match.group(1)
+ slug = slug_map.get(title, slugify(title))
+ link = abs_url.replace('{tag}', slug)
+ tmptags[title] = InlineTag(slug, title, link)
+ yield slug, tmptags[title]
+ # ignore other types (int, float, date, url, undefined)
- def on_markdown_meta_postprocess(self, meta, record, **extra):
- if meta['tags']:
- get_ctx().record_dependency(self.config_filename)
+ # Create new attribute on page record.
+ # All tagged records are guaranteed to have this attribute.
+ if not hasattr(args.record, 'inlinetags'):
+ args.record.inlinetags = tmptags
+ elif tmptags:
+ args.record.inlinetags.update(tmptags)
+
+ # replace inline-tags with hyperlink
+ if tmptags:
+ def _repl_tags(match: re.Match) -> str:
+ inl_tag = tmptags[match.group(1)]
+ return link_format.format(**inl_tag._asdict())
+
+ # get field value
+ key, b_idx, b_key = args.key
+ obj = args.record[key]
+ if b_idx is not None:
+ obj = obj.blocks[b_idx][b_key]
+
+ # type = markdown
+ if isinstance(obj, (Markdown, MarkdownDescriptor)):
+ obj.source = regex.sub(_repl_tags, obj.source)
+ # type = checkboxes, strings
+ elif isinstance(obj, list):
+ for i in range(len(obj)):
+ obj[i] = regex.sub(_repl_tags, obj[i])
+ # type = html, select, string, text
+ elif isinstance(obj, str):
+ newval = regex.sub(_repl_tags, obj)
+ # type = html
+ if isinstance(obj, Markup):
+ newval = Markup(newval)
+ if b_idx is None:
+ # _data is only writable in source info update
+ # during build, write to _bound_data is necessary
+ args.record._bound_data[key] = newval
+ else:
+ # here, using _data seems fine...
+ args.record[key].blocks[b_idx]._data[b_key] = newval
diff --git a/setup.py b/setup.py
index 1ecd75b..eb0ce30 100644
--- a/setup.py
+++ b/setup.py
@@ -1,12 +1,44 @@
from setuptools import setup
+with open('README.md') as fp:
+ longdesc = fp.read()
+
setup(
name='lektor-inlinetags',
py_modules=['lektor_inlinetags'],
- version='1.0',
+ install_requires=['lektor-groupby>=0.9.1'],
entry_points={
'lektor.plugins': [
- 'inlinetags = lektor_inlinetags:Main',
+ 'inlinetags = lektor_inlinetags:InlineTagsPlugin',
]
- }
+ },
+ author='relikd',
+ url='https://github.com/relikd/lektor-inlinetags-plugin',
+ version='0.9',
+ description='Auto-detect and reference tags inside written text.',
+ long_description=longdesc,
+ long_description_content_type="text/markdown",
+ license='MIT',
+ python_requires='>=3.6',
+ keywords=[
+ 'lektor',
+ 'plugin',
+ 'blog',
+ 'tags',
+ 'tagging',
+ ],
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Environment :: Web Environment',
+ 'Environment :: Plugins',
+ 'Framework :: Lektor',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3 :: Only',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ ],
)