first working version

This commit is contained in:
relikd
2022-03-31 14:25:50 +02:00
parent b552ba6603
commit 68b5fb406a
6 changed files with 193 additions and 86 deletions

25
.gitignore vendored Normal file
View File

@@ -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

7
LICENSE Normal file
View File

@@ -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.

21
Makefile Normal file
View File

@@ -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/*

View File

@@ -1,8 +1,14 @@
Example config file:
# lektor plugin: inlinetags
### Default config file
```ini
template = <a class="tag" href="/tag/{tag}">{title}</a>
root = /
template = inlinetag.html
regex = {{([^}]{1,32})}}
link = tags/{tag}/
replace = <a href="{link}">{title}</a>
[replace]
C# = C-Sharp
[slugs]
C# = c-sharp
```

View File

@@ -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', '<a href="{link}">{title}</a>') # 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

View File

@@ -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',
],
)