2023-05-16 23:34:27 +03:00
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
from pelican import signals
|
|
|
|
|
from pelican.writers import Writer
|
|
|
|
|
from pelican.paginator import Paginator
|
|
|
|
|
from pelican.readers import MarkdownReader, DUPLICATES_DEFINITIONS_ALLOWED
|
|
|
|
|
from pelican.utils import get_relative_path, is_selected_for_writing, path_to_url, sanitised_join, pelican_open
|
|
|
|
|
|
|
|
|
|
from markdown.extensions.meta import BEGIN_RE, META_RE, META_MORE_RE, END_RE
|
|
|
|
|
|
|
|
|
|
from mistune import Markdown
|
|
|
|
|
from mistune.core import BlockState
|
|
|
|
|
from mistune.util import strip_end
|
|
|
|
|
from mistune.renderers._list import render_list
|
|
|
|
|
from mistune.renderers.markdown import MarkdownRenderer
|
|
|
|
|
|
|
|
|
|
from typing import Dict, Any
|
|
|
|
|
from textwrap import indent
|
|
|
|
|
|
|
|
|
|
# === U N D E R L I N E D M I S T U N E P L U G I N ===
|
|
|
|
|
# from https://github.com/randogoth/md2mu
|
|
|
|
|
|
|
|
|
|
UNDERLINED = r'\b_{1,3}(?=[^\s_])'
|
|
|
|
|
UNDERLINED_END_RE = {
|
|
|
|
|
'_': re.compile(r'(?:(?<!\\)(?:\\\\)*\\_|[^\s_])_(?!_)\b'),
|
|
|
|
|
'__': re.compile(r'(?:(?<!\\)(?:\\\\)*\\_|[^\s_])__(?!_)\b'),
|
|
|
|
|
'___': re.compile(r'(?:(?<!\\)(?:\\\\)*\\_|[^\s_])___(?!_)\b'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def parse_underlined(self, m, state) -> int:
|
|
|
|
|
pos = m.end()
|
|
|
|
|
marker = m.group(0)
|
|
|
|
|
mlen = len(marker)
|
|
|
|
|
_end_re = UNDERLINED_END_RE[marker]
|
|
|
|
|
m1 = _end_re.search(state.src, pos)
|
|
|
|
|
if not m1:
|
|
|
|
|
state.append_token({'type': 'text', 'raw': marker})
|
|
|
|
|
return pos
|
|
|
|
|
end_pos = m1.end()
|
|
|
|
|
text = state.src[pos:end_pos-mlen]
|
|
|
|
|
prec_pos = self.precedence_scan(m, state, end_pos)
|
|
|
|
|
if prec_pos:
|
|
|
|
|
return prec_pos
|
|
|
|
|
new_state = state.copy()
|
|
|
|
|
new_state.src = text
|
|
|
|
|
new_state.in_underlined = True
|
|
|
|
|
state.append_token({
|
|
|
|
|
'type': 'underlined',
|
|
|
|
|
'children': self.render(new_state),
|
|
|
|
|
})
|
|
|
|
|
return end_pos
|
|
|
|
|
|
|
|
|
|
def render_underlined_mu(self, token, state) -> str:
|
|
|
|
|
return '`_' + self.render_children(token, state) + '`_'
|
|
|
|
|
|
|
|
|
|
def render_underlined_html(self, token, state) -> str:
|
|
|
|
|
return '<u>' + self.render_children(token, state) + '</u>'
|
|
|
|
|
|
|
|
|
|
# === M I C R O N R E N D E R E R F O R M I S T U N E ===
|
|
|
|
|
# from https://github.com/randogoth/md2mu
|
|
|
|
|
|
|
|
|
|
class MicronRenderer(MarkdownRenderer):
|
|
|
|
|
"""A renderer to format Micron text."""
|
|
|
|
|
NAME = 'micron'
|
2023-05-19 11:46:15 +03:00
|
|
|
settings = {}
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
def __call__(self, tokens, state: BlockState):
|
|
|
|
|
out = self.render_tokens(tokens, state)
|
|
|
|
|
# special handle for line breaks
|
|
|
|
|
out += '\n\n'.join(self.render_referrences(state)) + '\n'
|
|
|
|
|
return strip_end(out)
|
|
|
|
|
|
|
|
|
|
def render_children(self, token, state: BlockState):
|
|
|
|
|
children = token['children']
|
|
|
|
|
return self.render_tokens(children, state)
|
|
|
|
|
|
|
|
|
|
def text(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return token['raw']
|
|
|
|
|
|
|
|
|
|
def emphasis(self, token: Dict[str, Any], state: BlockState) -> str:
|
2023-05-19 11:46:15 +03:00
|
|
|
fpre, fpost = self.settings.get('MICRON_EMPHASIS_FORMAT', ['', ''])
|
|
|
|
|
return fpre + '`*' + self.render_children(token, state) + '`*' + fpost
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
def strong(self, token: Dict[str, Any], state: BlockState) -> str:
|
2023-05-19 11:46:15 +03:00
|
|
|
fpre, fpost = self.settings.get('MICRON_STRONG_FORMAT', ['', ''])
|
|
|
|
|
return fpre + '`!' + self.render_children(token, state) + '`!' + fpost
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
def link(self, token: Dict[str, Any], state: BlockState) -> str:
|
2023-05-19 11:46:15 +03:00
|
|
|
fpre, fpost = self.settings.get('MICRON_LINK_FORMAT', ['', ''])
|
2023-05-16 23:34:27 +03:00
|
|
|
label = token.get('label')
|
|
|
|
|
text = self.render_children(token, state)
|
2023-05-19 11:46:15 +03:00
|
|
|
out = fpre
|
|
|
|
|
out += '`[' + text + '`'
|
2023-05-16 23:34:27 +03:00
|
|
|
attrs = token['attrs']
|
|
|
|
|
url = attrs['url']
|
|
|
|
|
out += url
|
2023-05-19 11:46:15 +03:00
|
|
|
return out + ']' + fpost
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
def image(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return self.link(token, state)
|
|
|
|
|
|
|
|
|
|
def codespan(self, token: Dict[str, Any], state: BlockState) -> str:
|
2023-05-19 11:46:15 +03:00
|
|
|
fpre, fpost = self.settings.get('MICRON_CODE_FORMAT', ['', ''])
|
|
|
|
|
return fpre + '`=' + token['raw'] + '`=' + fpost
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
def linebreak(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return ' \n'
|
|
|
|
|
|
|
|
|
|
def softbreak(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return '\n'
|
|
|
|
|
|
|
|
|
|
def blank_line(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return ''
|
|
|
|
|
|
|
|
|
|
def inline_html(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return ''
|
|
|
|
|
|
|
|
|
|
def paragraph(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
text = self.render_children(token, state)
|
|
|
|
|
return text + '\n\n'
|
|
|
|
|
|
|
|
|
|
def heading(self, token: Dict[str, Any], state: BlockState) -> str:
|
2023-05-19 11:46:15 +03:00
|
|
|
fpre, fpost = self.settings.get('MICRON_HEADER_FORMAT', ['', ''])
|
2023-05-16 23:34:27 +03:00
|
|
|
level = token['attrs']['level']
|
|
|
|
|
if level > 3:
|
|
|
|
|
level = 3
|
|
|
|
|
marker = '>' * level
|
|
|
|
|
text = self.render_children(token, state)
|
2023-05-19 11:46:15 +03:00
|
|
|
return fpre + marker + ' ' + text + fpost + '\n\n'
|
|
|
|
|
|
2023-05-16 23:34:27 +03:00
|
|
|
def thematic_break(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return '-\n\n'
|
|
|
|
|
|
|
|
|
|
def block_text(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return self.render_children(token, state) + '\n'
|
|
|
|
|
|
|
|
|
|
def block_code(self, token: Dict[str, Any], state: BlockState) -> str:
|
2023-05-19 11:46:15 +03:00
|
|
|
fpre, fpost = self.settings.get('MICRON_CODE_FORMAT', ['', ''])
|
2023-05-16 23:34:27 +03:00
|
|
|
code = token['raw']
|
|
|
|
|
if code and code[-1] != '\n':
|
|
|
|
|
code += '\n'
|
|
|
|
|
marker = '`='
|
2023-05-19 11:46:15 +03:00
|
|
|
return fpre + marker + '\n' + code + marker + fpost + '\n\n'
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
def block_quote(self, token: Dict[str, Any], state: BlockState) -> str:
|
2023-05-19 11:46:15 +03:00
|
|
|
fpre, fpost = self.settings.get('MICRON_QUOTE_FORMAT', ['', ''])
|
2023-05-16 23:34:27 +03:00
|
|
|
text = indent(self.render_children(token, state), '>>>>')
|
2023-05-19 11:46:15 +03:00
|
|
|
return fpre + text + fpost + '\n\n'
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
def block_html(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return ''
|
|
|
|
|
|
|
|
|
|
def block_error(self, token: Dict[str, Any], state: BlockState) -> str:
|
|
|
|
|
return ''
|
|
|
|
|
|
|
|
|
|
def list(self, token: Dict[str, Any], state: BlockState) -> str:
|
2023-05-19 11:46:15 +03:00
|
|
|
fpre, fpost = self.settings.get('MICRON_LIST_FORMAT', ['', ''])
|
|
|
|
|
return fpre + render_list(self, token, state) + fpost
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
# === M A R K D O W N T O M I C R O N R E A D E R ===
|
|
|
|
|
|
|
|
|
|
class MarkdownToMicronReader(MarkdownReader):
|
|
|
|
|
enabled = True
|
|
|
|
|
file_extensions = ['md', 'markdown', 'mkd', 'mdown']
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
def _parse_metadata(self, meta):
|
|
|
|
|
"""Return the dict containing document metadata"""
|
|
|
|
|
output = {}
|
|
|
|
|
for name, value in meta.items():
|
|
|
|
|
name = name.lower()
|
|
|
|
|
if not DUPLICATES_DEFINITIONS_ALLOWED.get(name, True):
|
|
|
|
|
output[name] = self.process_metadata(name, value[0])
|
|
|
|
|
elif len(value) > 1:
|
|
|
|
|
output[name] = self.process_metadata(name, value)
|
|
|
|
|
else:
|
|
|
|
|
output[name] = self.process_metadata(name, value[0])
|
|
|
|
|
return output
|
|
|
|
|
|
|
|
|
|
def read(self, source_path):
|
|
|
|
|
"""Parse content and metadata of markdown files"""
|
|
|
|
|
|
|
|
|
|
with pelican_open(source_path) as text:
|
|
|
|
|
lines = text.split('\n')
|
|
|
|
|
|
|
|
|
|
meta = {}
|
|
|
|
|
key = None
|
|
|
|
|
if lines and BEGIN_RE.match(lines[0]):
|
|
|
|
|
lines.pop(0)
|
|
|
|
|
while lines:
|
|
|
|
|
line = lines.pop(0)
|
|
|
|
|
m1 = META_RE.match(line)
|
|
|
|
|
if line.strip() == '' or END_RE.match(line):
|
|
|
|
|
break # blank line or end of YAML header - done
|
|
|
|
|
if m1:
|
|
|
|
|
key = m1.group('key').lower().strip()
|
|
|
|
|
value = m1.group('value').strip()
|
|
|
|
|
try:
|
|
|
|
|
meta[key].append(value)
|
|
|
|
|
except KeyError:
|
|
|
|
|
meta[key] = [value]
|
|
|
|
|
else:
|
|
|
|
|
m2 = META_MORE_RE.match(line)
|
|
|
|
|
if m2 and key:
|
|
|
|
|
# Add another line to existing key
|
|
|
|
|
meta[key].append(m2.group('value').strip())
|
|
|
|
|
else:
|
|
|
|
|
lines.insert(0, line)
|
|
|
|
|
break # no meta data - done
|
|
|
|
|
|
|
|
|
|
metadata = self._parse_metadata(meta)
|
|
|
|
|
content = '\n'.join(lines)
|
|
|
|
|
m2μr = MicronRenderer()
|
|
|
|
|
m2μ = Markdown(renderer=m2μr)
|
|
|
|
|
m2μ.inline.register('underlined', UNDERLINED, parse_underlined, before='emphasis')
|
|
|
|
|
m2μ.renderer.register('underlined', render_underlined_mu)
|
|
|
|
|
content = m2μ(content)
|
|
|
|
|
return content, metadata
|
|
|
|
|
|
|
|
|
|
# === M I C R O N W R I T E R ===
|
|
|
|
|
|
|
|
|
|
class MicronWriter(Writer):
|
|
|
|
|
|
|
|
|
|
def __init__(self, output_path, settings):
|
|
|
|
|
super().__init__(output_path, settings)
|
2023-05-17 00:24:52 +03:00
|
|
|
out = settings.get('OUTPUT_PATH', 'output/')
|
|
|
|
|
self.output_path = settings.get('MICRON_PATH', out)
|
2023-05-16 23:34:27 +03:00
|
|
|
|
|
|
|
|
def write_file(self, name, template, context, relative_urls=False,
|
|
|
|
|
paginated=None, template_name=None, override_output=False,
|
|
|
|
|
url=None, **kwargs):
|
|
|
|
|
template = template.environment.get_template( template.name.replace('.html', '.mu'))
|
|
|
|
|
name = name.replace('.html', '.mu')
|
|
|
|
|
if name is False or \
|
|
|
|
|
name == "" or \
|
|
|
|
|
not is_selected_for_writing(self.settings,
|
|
|
|
|
os.path.join(self.output_path, name)):
|
|
|
|
|
return
|
|
|
|
|
elif not name:
|
|
|
|
|
# other stuff, just return for now
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def _write_file(template, localcontext, output_path, name, override):
|
|
|
|
|
"""Render the template write the file."""
|
|
|
|
|
# set localsiteurl for context so that Contents can adjust links
|
|
|
|
|
if localcontext['localsiteurl']:
|
|
|
|
|
context['localsiteurl'] = localcontext['localsiteurl']
|
2023-05-17 12:29:44 +03:00
|
|
|
output = template.render(localcontext).replace('.html', '.mu')
|
2023-05-16 23:34:27 +03:00
|
|
|
path = sanitised_join(output_path, name)
|
|
|
|
|
try:
|
|
|
|
|
os.makedirs(os.path.dirname(path))
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
with self._open_w(path, 'utf-8', override=override) as f:
|
|
|
|
|
f.write(output)
|
|
|
|
|
|
|
|
|
|
# Send a signal to say we're writing a file with some specific
|
|
|
|
|
# local context.
|
|
|
|
|
signals.content_written.send(path, context=localcontext)
|
|
|
|
|
|
|
|
|
|
def _get_localcontext(context, name, kwargs, relative_urls):
|
|
|
|
|
localcontext = context.copy()
|
|
|
|
|
localcontext['localsiteurl'] = localcontext.get(
|
|
|
|
|
'localsiteurl', None)
|
|
|
|
|
if relative_urls:
|
|
|
|
|
relative_url = path_to_url(get_relative_path(name))
|
|
|
|
|
localcontext['SITEURL'] = relative_url
|
|
|
|
|
localcontext['localsiteurl'] = relative_url
|
|
|
|
|
localcontext['output_file'] = name
|
|
|
|
|
localcontext.update(kwargs)
|
|
|
|
|
return localcontext
|
|
|
|
|
|
|
|
|
|
if paginated is None:
|
|
|
|
|
paginated = {key: val for key, val in kwargs.items()
|
|
|
|
|
if key in {'articles', 'dates'}}
|
|
|
|
|
|
|
|
|
|
# pagination
|
|
|
|
|
if paginated and template_name in self.settings['PAGINATED_TEMPLATES']:
|
|
|
|
|
# pagination needed
|
|
|
|
|
per_page = self.settings['PAGINATED_TEMPLATES'][template_name] \
|
|
|
|
|
or self.settings['DEFAULT_PAGINATION']
|
|
|
|
|
|
|
|
|
|
# init paginators
|
|
|
|
|
paginators = {key: Paginator(name, url, val, self.settings,
|
|
|
|
|
per_page)
|
|
|
|
|
for key, val in paginated.items()}
|
|
|
|
|
|
|
|
|
|
# generated pages, and write
|
|
|
|
|
for page_num in range(list(paginators.values())[0].num_pages):
|
|
|
|
|
paginated_kwargs = kwargs.copy()
|
|
|
|
|
for key in paginators.keys():
|
|
|
|
|
paginator = paginators[key]
|
|
|
|
|
previous_page = paginator.page(page_num) \
|
|
|
|
|
if page_num > 0 else None
|
|
|
|
|
page = paginator.page(page_num + 1)
|
|
|
|
|
next_page = paginator.page(page_num + 2) \
|
|
|
|
|
if page_num + 1 < paginator.num_pages else None
|
|
|
|
|
paginated_kwargs.update(
|
|
|
|
|
{'%s_paginator' % key: paginator,
|
|
|
|
|
'%s_page' % key: page,
|
|
|
|
|
'%s_previous_page' % key: previous_page,
|
|
|
|
|
'%s_next_page' % key: next_page})
|
|
|
|
|
|
|
|
|
|
localcontext = _get_localcontext(
|
|
|
|
|
context, page.save_as, paginated_kwargs, relative_urls)
|
|
|
|
|
_write_file(template, localcontext, self.output_path,
|
|
|
|
|
page.save_as, override_output)
|
|
|
|
|
else:
|
|
|
|
|
# no pagination
|
|
|
|
|
localcontext = _get_localcontext(
|
|
|
|
|
context, name, kwargs, relative_urls)
|
|
|
|
|
_write_file(template, localcontext, self.output_path, name,
|
|
|
|
|
override_output)
|
|
|
|
|
|
|
|
|
|
def add_reader(readers):
|
|
|
|
|
readers.reader_classes['md'] = MarkdownToMicronReader
|
|
|
|
|
|
|
|
|
|
def add_writer(pelican_object):
|
|
|
|
|
return MicronWriter
|
|
|
|
|
|
|
|
|
|
def register():
|
|
|
|
|
signals.readers_init.connect(add_reader)
|
|
|
|
|
signals.get_writer.connect(add_writer)
|