Mercurial > wikked
changeset 476:71114096433c
core: Add support for Markdown extensions, add header anchor extension.
- New configuration option to specify Markdown extensions.
- Enable some extensions by default.
- Add CSS to make tables pretty.
- Add extension to generate anchors next to each HTML heading.
- Provide CSS to show those anchors on mouse-hover.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Thu, 11 Oct 2018 23:24:06 -0700 |
parents | 0ecf8303a135 |
children | 71c9259de019 |
files | wikked/assets/css/wikked/page.less wikked/formatter.py wikked/resolver.py wikked/resources/defaults.cfg wikked/utils.py wikked/wiki.py |
diffstat | 6 files changed, 158 insertions(+), 63 deletions(-) [+] |
line wrap: on
line diff
--- a/wikked/assets/css/wikked/page.less Thu Oct 11 23:18:45 2018 -0700 +++ b/wikked/assets/css/wikked/page.less Thu Oct 11 23:24:06 2018 -0700 @@ -1,5 +1,7 @@ +@wikked-icon-font: "Font Awesome 5 Free"; @wikked-external-link-icon: @fa-var-external-link-alt; //@wikked-external-link-icon: @fa-var-external-link-square-alt; +@wikked-anchor-icon: @fa-var-paragraph; // Article @@ -28,15 +30,52 @@ color: @color-orange; &:hover { color: @color-orange; text-decoration: underline; } } + + table { + display: block; + margin-top: 0; + margin-bottom: 1.5em; + + th, td { padding: 0.5em 1em; border: 1px solid @color-gray-medium; } + th { font-weight: bold; } + th, tr:nth-child(2n) { background: darken(@color-gray-light, 5%); } + } } article.wiki-page { - a { - &::after { font-family: 'Font Awesome 5 Free'; font-weight: 900; content: ' @{wikked-external-link-icon}'; } + a[href] { + &::after { + font-family: @wikked-icon-font; + font-size: 0.6em; + font-weight: 900; + vertical-align: middle; + content: ' @{wikked-external-link-icon}'; + } } a.wiki-link, a.wiki-meta-link { &::after { content: none; } } + + h1>a.wiki-header-link::after, + h2>a.wiki-header-link::after, + h3>a.wiki-header-link::after, + h4>a.wiki-header-link::after, + h5>a.wiki-header-link::after, + h6>a.wiki-header-link::after { + font-family: @wikked-icon-font; + font-size: 0.8em; + font-weight: 900; + content: ' @{wikked-anchor-icon}'; + visibility: hidden; + } + h1:hover>a.wiki-header-link::after, + h2:hover>a.wiki-header-link::after, + h3:hover>a.wiki-header-link::after, + h4:hover>a.wiki-header-link::after, + h5:hover>a.wiki-header-link::after, + h6:hover>a.wiki-header-link::after { + visibility: visible; + } } // Page title decorators
--- a/wikked/formatter.py Thu Oct 11 23:18:45 2018 -0700 +++ b/wikked/formatter.py Thu Oct 11 23:24:06 2018 -0700 @@ -4,7 +4,8 @@ import logging import jinja2 from io import StringIO -from .utils import get_meta_name_and_modifiers, html_escape, split_page_url +from .utils import ( + get_meta_name_and_modifiers, html_escape, split_page_url, get_url_tail) RE_FILE_FORMAT = re.compile(r'\r\n?', re.MULTILINE) @@ -18,13 +19,15 @@ re.MULTILINE | re.DOTALL) RE_LINK_ENDPOINT = re.compile( - r'\[\[(\w[\w\d]+)?\:([^\]]*/)?([^\]]+)\]\]') + r'(?<!\\)\[\[(\w[\w\d]+)?\:([^\#\]]+)(\#[\w\d\-]+)?\]\]') RE_LINK_ENDPOINT_DISPLAY = re.compile( - r'\[\[([^\|\]]+)\|\s*(\w[\w\d]+)?\:([^\]]+)\]\]') + r'(?<!\\)\[\[([^\|\]]+)\|\s*(\w[\w\d]+)?\:([^\#\]]+)(\#[\w\d\-]+)?\]\]') RE_LINK_DISPLAY = re.compile( - r'\[\[([^\|\]]+)\|([^\]]+)\]\]') + r'(?<!\\)\[\[([^\|\]]+)\|([^\#\]]+)(\#[\w\d\-]+)?\]\]') RE_LINK = re.compile( - r'\[\[([^\]]*/)?([^/\]]+)\]\]') + r'(?<!\\)\[\[([^\#\]]+)(\#[\w\d\-]+)?\]\]') +RE_ESCAPED_LINK = re.compile( + r'(?<!\\)\\\[\[') logger = logging.getLogger(__name__) @@ -128,13 +131,13 @@ # [[endpoint:Something/Blah.ext]] def repl1(m): endpoint = m.group(1) - a, b = m.group(2, 3) - value = b if a is None else (a + b) + value, frag = m.group(2, 3) + name = get_url_tail(value) if endpoint in self.endpoints: return self.endpoints[endpoint]( - ctx, endpoint, b.strip(), value.strip()) + ctx, endpoint, name.strip(), value.strip(), frag) return self._formatEndpointLink( - ctx, endpoint, b.strip(), value.strip()) + ctx, endpoint, name.strip(), value.strip(), frag) text = RE_LINK_ENDPOINT.sub(repl1, text) # [[display name|endpoint:Something/Whatever]] @@ -142,9 +145,10 @@ display = m.group(1).strip() endpoint = m.group(2) value = m.group(3).strip() + frag = m.group(4) if endpoint in self.endpoints: return self.endpoints[endpoint]( - ctx, endpoint, display, value) + ctx, endpoint, display, value, frag) return self._formatEndpointLink( ctx, endpoint, display, value) text = RE_LINK_ENDPOINT_DISPLAY.sub(repl2, text) @@ -152,16 +156,20 @@ # [[display name|Whatever/PageName]] def repl3(m): return s._formatWikiLink(ctx, m.group(1).strip(), - m.group(2).strip()) + m.group(2).strip(), + m.group(3)) text = RE_LINK_DISPLAY.sub(repl3, text) # [[Namespace/PageName]] def repl4(m): - a, b = m.group(1, 2) - url = b if a is None else (a + b) - return s._formatWikiLink(ctx, b.strip(), url.strip()) + value, frag = m.group(1, 2) + name = get_url_tail(value) + return s._formatWikiLink(ctx, name.strip(), value.strip(), frag) text = RE_LINK.sub(repl4, text) + # \[[Escaped Link]] + text = RE_ESCAPED_LINK.sub('[[', text) + return text def _coerceInclude(self, ctx, value): @@ -214,7 +222,7 @@ return '<div class="wiki-query"%s>%s</div>\n' % ( mod_attr, '|'.join(processed_args)) - def _formatFileLink(self, ctx, endpoint, display, value): + def _formatFileLink(self, ctx, endpoint, display, value, fragment): if value.startswith('./'): abs_url = os.path.join('/pagefiles', ctx.url.lstrip('/'), value[2:]) @@ -223,40 +231,27 @@ abs_url = os.path.normpath(abs_url).replace('\\', '/') return abs_url - def _formatImageLink(self, ctx, endpoint, display, value): - abs_url = self._formatFileLink(ctx, endpoint, display, value) + def _formatImageLink(self, ctx, endpoint, display, value, fragment): + abs_url = self._formatFileLink(ctx, endpoint, display, value, fragment) return ('<img class="wiki-image" src="%s" alt="%s"></img>' % (abs_url, display)) - def _formatEndpointLink(self, ctx, endpoint, display, local_url): - if True: # endpoint: - endpoint = endpoint or '' - url = '%s:%s' % (endpoint, local_url) - ctx.out_links.append(url) - return ('<a class="wiki-link" data-wiki-url="%s" ' - 'data-wiki-endpoint="%s">%s</a>' % (url, endpoint, - display)) - else: - # Endpoint link was actually: `[[:/Something/Blah]]`, which - # forces going back out of the endpoints. - # Render this like a normal wiki link. - ctx.out_links.append(local_url) - return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % ( - local_url, display) + def _formatEndpointLink(self, ctx, endpoint, display, local_url, fragment): + endpoint = endpoint or '' + url = '%s:%s' % (endpoint, local_url) + fragpart = (' data-wiki-fragment="%s"' % fragment) if fragment else '' + ctx.out_links.append(url) + return ('<a class="wiki-link" data-wiki-url="%s"' + '%s' + ' data-wiki-endpoint="%s">%s</a>' % + (url, fragpart, endpoint, display)) - def _formatWikiLink(self, ctx, display, url): - if True: # not ctx.endpoint: - ctx.out_links.append(url) - return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % ( - url, display) - else: - # The link is a relative link from a page that lives in an - # endpoint, so the generated link must be in the same endpoint. - url = '%s:%s' % (ctx.endpoint, url) - ctx.out_links.append(url) - return ('<a class="wiki-link" data-wiki-url="%s" ' - 'data-wiki-endpoint="%s">%s</a>' % (url, ctx.endpoint, - display)) + def _formatWikiLink(self, ctx, display, url, fragment): + fragpart = (' data-wiki-fragment="%s"' % fragment) if fragment else '' + ctx.out_links.append(url) + return ('<a class="wiki-link" data-wiki-url="%s"' + '%s>%s</a>' % + (url, fragpart, display)) @staticmethod def parseWikiLinks(text):
--- a/wikked/resolver.py Thu Oct 11 23:18:45 2018 -0700 +++ b/wikked/resolver.py Thu Oct 11 23:24:06 2018 -0700 @@ -23,6 +23,7 @@ re_wiki_link = re.compile( r'<a class="wiki-link(?P<isedit>-edit)?" ' r'data-wiki-url="(?P<url>[^"]+)"' + r'( data-wiki-fragment="(?P<frag>[^"]*)")?' r'( data-wiki-endpoint="(?P<endpoint>[^"]*)")?') re_wiki_include_param = re.compile( @@ -230,6 +231,7 @@ # Resolve link states. def repl1(m): raw_url = m.group('url') + fragment = m.group('frag') or '' endpoint = m.group('endpoint') is_edit = bool(m.group('isedit')) url = self.ctx.getAbsoluteUrl(raw_url, force_endpoint=endpoint) @@ -253,14 +255,18 @@ if validated_url: # The DB has confirmed that the target page exists, # so make a "real" link. - actual_url = '/%s/%s' % (action, quoted_url.lstrip('/')) + actual_url = '/%s/%s%s' % (action, + quoted_url.lstrip('/'), + fragment) return ('<a class="wiki-link" data-wiki-url="%s" ' 'href="%s"' % (quoted_url, actual_url) + endpoint_markup) # The DB doesn't know about the target page, so render # a link with the "missing" class so it shows up red and all. - actual_url = '/%s/%s' % (action, quoted_url.lstrip('/')) + actual_url = '/%s/%s%s' % (action, + quoted_url.lstrip('/'), + fragment) return ('<a class="wiki-link missing" data-wiki-url="%s" ' 'href="%s"' % (quoted_url, actual_url) + endpoint_markup)
--- a/wikked/resources/defaults.cfg Thu Oct 11 23:18:45 2018 -0700 +++ b/wikked/resources/defaults.cfg Thu Oct 11 23:24:06 2018 -0700 @@ -8,6 +8,9 @@ database=sql database_url=sqlite:///%(root)s/.wiki/wiki.db +[markdown] +extensions=abbr,def_list,fenced_code,footnotes,tables,toc + [endpoint:templates] query=False
--- a/wikked/utils.py Thu Oct 11 23:18:45 2018 -0700 +++ b/wikked/utils.py Thu Oct 11 23:24:06 2018 -0700 @@ -96,6 +96,10 @@ return '/' +def get_url_tail(url): + return os.path.basename(url) + + def is_endpoint_url(url): return endpoint_prefix_regex.match(url) is not None
--- a/wikked/wiki.py Thu Oct 11 23:18:45 2018 -0700 +++ b/wikked/wiki.py Thu Oct 11 23:24:06 2018 -0700 @@ -46,14 +46,12 @@ root = os.getcwd() self.root = root self.context = ctx - self.formatters = {} self.custom_heads = {} self.wiki_updater = synchronous_wiki_updater self._config = None self._index_factory = None self._scm_factory = None - - self._build() + self._formatters = None @property def config(self): @@ -79,15 +77,20 @@ def auth_factory(self): return UserManager(self.config) - def _build(self): - self.formatters[passthrough_formatter] = ['txt', 'html'] - self.tryAddFormatter('markdown', 'markdown', - ['md', 'mdown', 'markdown']) - self.tryAddFormatter('textile', 'textile', - ['tl', 'text', 'textile']) - self.tryAddFormatter('creole', 'creole2html', - ['cr', 'creole']) - self.tryAddFountainFormatter() + @property + def formatters(self): + if self._formatters is None: + self._formatters = {} + + self.formatters[passthrough_formatter] = ['txt', 'html'] + self.tryAddMarkdownFormatter() + self.tryAddFormatter('textile', 'textile', + ['tl', 'text', 'textile']) + self.tryAddFormatter('creole', 'creole2html', + ['cr', 'creole']) + self.tryAddFountainFormatter() + + return self._formatters def getSpecialFilenames(self): yield '.wikirc' @@ -101,10 +104,55 @@ try: module = importlib.import_module(module_name) func = getattr(module, module_func) - self.formatters[func] = extensions + self._formatters[func] = extensions except ImportError: pass + def tryAddMarkdownFormatter(self,): + try: + import markdown + except ImportError: + return + + from markdown.util import etree + + class HeaderAnchorsTreeprocessor( + markdown.treeprocessors.Treeprocessor): + HEADER_TAGS = {'h1', 'h2', 'h3', 'h4', 'h5', 'h6'} + + def run(self, root): + hd_tags = self.HEADER_TAGS + for elem in root.iter(): + if elem.tag in hd_tags: + hd_id = elem.text.lower().replace(' ', '-') + hd_id = elem.attrib.setdefault('id', hd_id) + elem.append(etree.Element( + 'a', + {'class': 'wiki-header-link', + 'href': '#%s' % hd_id})) + + class HeaderAnchorsExtension(markdown.extensions.Extension): + def extendMarkdown(self, md, *args, **kwargs): + md.treeprocessors.register( + HeaderAnchorsTreeprocessor(md), + 'header_anchors', + 100) + + class _MarkdownWrapper: + def __init__(self, md): + self._md = md + + def __call__(self, text): + self._md.reset() + return self._md.convert(text) + + exts = self.config.get('markdown', 'extensions').split(',') + exts.append(HeaderAnchorsExtension()) + md = markdown.Markdown(extensions=exts) + + md_wrapper = _MarkdownWrapper(md) + self._formatters[md_wrapper] = ['md', 'mdown', 'markdown'] + def tryAddFountainFormatter(self): try: from jouvence.parser import JouvenceParser @@ -122,7 +170,7 @@ rdr.render_doc(document, fp) return fp.getvalue() - self.formatters[_jouvence_to_html] = ['fountain'] + self._formatters[_jouvence_to_html] = ['fountain'] head_css = ('<link rel="stylesheet" type="text/css" ' 'href="/static/css/jouvence.css" />\n')