another working version
This commit is contained in:
@@ -1,86 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from lektor.markdown import Markdown, Markup
|
||||
from lektor.context import get_ctx
|
||||
from lektor.markdown import Markup
|
||||
from lektor.pluginsystem import Plugin
|
||||
from lektor.reporter import reporter, style
|
||||
from lektor.types.formats import MarkdownDescriptor
|
||||
from lektor.utils import slugify
|
||||
from lektor.sourceobj import VirtualSourceObject as VObj
|
||||
import re
|
||||
from typing import Set, Dict, Iterator, Tuple
|
||||
from collections import namedtuple
|
||||
|
||||
InlineTag = namedtuple('InlineTag', ['slug', 'title', 'link'])
|
||||
from typing import Set, Dict, Iterator, Generator
|
||||
from lektor_groupby import report_config_error
|
||||
|
||||
|
||||
class InlineTagsPlugin(Plugin):
|
||||
name = 'InlineTags'
|
||||
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 _get_tags(record, *, recursive=False) -> Iterator[VObj]:
|
||||
fn = self.env.jinja_env.filters['groupby']
|
||||
yield from fn(record, *self.config_keys, recursive=recursive)
|
||||
|
||||
self.env.jinja_env.filters.update(inlinetags=_iter_inlinetags)
|
||||
self.env.jinja_env.filters.update(inlinetags=_get_tags)
|
||||
|
||||
def on_process_template_context(self, context, **extra) -> None:
|
||||
# track dependency for observer replacement below
|
||||
if hasattr(context.get('this'), '_inlinetag_modified'):
|
||||
ctx = get_ctx()
|
||||
if ctx:
|
||||
ctx.record_dependency(self.config_filename)
|
||||
|
||||
def on_groupby_before_build_all(self, groupby, **extra) -> None:
|
||||
self.config_keys = set() # type: Set[str]
|
||||
# load config
|
||||
config = self.get_config()
|
||||
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
|
||||
for sect in config.sections():
|
||||
if '.' in sect: # e.g., sect.fields and sect.key_map
|
||||
continue
|
||||
if self.add_observer(groupby, config, sect):
|
||||
self.config_keys.add(sect)
|
||||
|
||||
# normalize and validate input
|
||||
def add_observer(self, groupby, config, sect_key) -> bool:
|
||||
# get regex
|
||||
pattern = config.section_as_dict(sect_key + '.pattern')
|
||||
regex_str = pattern.get('match', r'{{([^}]{1,32})}}') # type: str
|
||||
tag_replace = pattern.get('replace', '{name}') # type: str
|
||||
|
||||
# validate input
|
||||
try:
|
||||
regex = re.compile(regex)
|
||||
regex = re.compile(regex_str)
|
||||
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}')
|
||||
report_config_error(sect_key, 'pattern.match', regex_str, e)
|
||||
return False
|
||||
|
||||
# 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]]:
|
||||
# add observer
|
||||
watcher = groupby.add_watcher(sect_key, config)
|
||||
|
||||
@watcher.grouping()
|
||||
def convert_inlinetags(args) -> Generator[str, str, None]:
|
||||
arr = args.field if isinstance(args.field, list) else [args.field]
|
||||
tmptags = {} # type: Dict[str, InlineTag]
|
||||
_tags = {} # type: Dict[str, str]
|
||||
for obj in arr:
|
||||
if isinstance(obj, (Markdown, MarkdownDescriptor)):
|
||||
if hasattr(obj, 'source'):
|
||||
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]
|
||||
name = match.group(1)
|
||||
_tags[name] = yield name
|
||||
# ignore other types (int, float, date, url, undefined)
|
||||
|
||||
# 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:
|
||||
if _tags:
|
||||
def _repl_tags(match: re.Match) -> str:
|
||||
inl_tag = tmptags[match.group(1)]
|
||||
return link_format.format(**inl_tag._asdict())
|
||||
name = match.group(1)
|
||||
return tag_replace.format(key=_tags[name], name=name)
|
||||
|
||||
args.record._inlinetag_modified = True
|
||||
# get field value
|
||||
key, b_idx, b_key = args.key
|
||||
obj = args.record[key]
|
||||
@@ -88,7 +79,7 @@ class InlineTagsPlugin(Plugin):
|
||||
obj = obj.blocks[b_idx][b_key]
|
||||
|
||||
# type = markdown
|
||||
if isinstance(obj, (Markdown, MarkdownDescriptor)):
|
||||
if hasattr(obj, 'source'):
|
||||
obj.source = regex.sub(_repl_tags, obj.source)
|
||||
# type = checkboxes, strings
|
||||
elif isinstance(obj, list):
|
||||
@@ -102,8 +93,9 @@ class InlineTagsPlugin(Plugin):
|
||||
newval = Markup(newval)
|
||||
if b_idx is None:
|
||||
# _data is only writable in source info update
|
||||
# during build, write to _bound_data is necessary
|
||||
# thus, during build, _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
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user