280 lines
11 KiB
Python
280 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright © 2012-2015 Roberto Alsina and others.
|
|
|
|
# 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.
|
|
|
|
from __future__ import unicode_literals
|
|
import io
|
|
import os
|
|
import re
|
|
|
|
try:
|
|
import docutils.core
|
|
import docutils.nodes
|
|
import docutils.utils
|
|
import docutils.io
|
|
import docutils.readers.standalone
|
|
has_docutils = True
|
|
except ImportError:
|
|
has_docutils = False
|
|
|
|
try:
|
|
import rst2html5
|
|
has_rst2html5 = True
|
|
except ImportError:
|
|
has_rst2html5 = False
|
|
|
|
from nikola.plugin_categories import PageCompiler
|
|
from nikola.utils import get_logger, makedirs, req_missing, write_metadata
|
|
|
|
|
|
class CompileRestHTML5(PageCompiler):
|
|
"""Compile reSt into HTML."""
|
|
|
|
name = "rest_html5"
|
|
demote_headers = True
|
|
logger = None
|
|
|
|
def _read_extra_deps(self, post):
|
|
"""Reads contents of .dep file and returns them as a list"""
|
|
dep_path = post.base_path + '.dep'
|
|
if os.path.isfile(dep_path):
|
|
with io.open(dep_path, 'r+', encoding='utf8') as depf:
|
|
deps = [l.strip() for l in depf.readlines()]
|
|
return deps
|
|
return []
|
|
|
|
def register_extra_dependencies(self, post):
|
|
"""Adds dependency to post object to check .dep file."""
|
|
post.add_dependency(lambda: self._read_extra_deps(post), 'fragment')
|
|
|
|
def compile_html(self, source, dest, is_two_file=True):
|
|
"""Compile reSt into HTML."""
|
|
|
|
if not has_docutils:
|
|
req_missing(['docutils'], 'build this site (compile reStructuredText)')
|
|
if not has_rst2html5:
|
|
req_missing(['rst2html5'], 'build this site (compile reStructuredText into HTML5)')
|
|
makedirs(os.path.dirname(dest))
|
|
error_level = 100
|
|
with io.open(dest, "w+", encoding="utf8") as out_file:
|
|
with io.open(source, "r", encoding="utf8") as in_file:
|
|
data = in_file.read()
|
|
add_ln = 0
|
|
if not is_two_file:
|
|
spl = re.split('(\n\n|\r\n\r\n)', data, maxsplit=1)
|
|
data = spl[-1]
|
|
if len(spl) != 1:
|
|
# If errors occur, this will be added to the line
|
|
# number reported by docutils so the line number
|
|
# matches the actual line number (off by 7 with default
|
|
# metadata, could be more or less depending on the post
|
|
# author).
|
|
add_ln = len(spl[0].splitlines()) + 1
|
|
|
|
default_template_path = os.path.join(os.path.dirname(__file__), 'template.txt')
|
|
output, error_level, deps = rst2html(
|
|
data, settings_overrides={
|
|
'initial_header_level': 0,
|
|
'record_dependencies': True,
|
|
'stylesheet_path': None,
|
|
'link_stylesheet': True,
|
|
'syntax_highlight': 'short',
|
|
'math_output': 'mathjax',
|
|
'template': default_template_path,
|
|
}, logger=self.logger, source_path=source, l_add_ln=add_ln)
|
|
out_file.write(output)
|
|
deps_path = dest + '.dep'
|
|
if deps.list:
|
|
with io.open(deps_path, "w+", encoding="utf8") as deps_file:
|
|
deps_file.write('\n'.join(deps.list))
|
|
else:
|
|
if os.path.isfile(deps_path):
|
|
os.unlink(deps_path)
|
|
if error_level < 3:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def create_post(self, path, **kw):
|
|
content = kw.pop('content', None)
|
|
onefile = kw.pop('onefile', False)
|
|
# is_page is not used by create_post as of now.
|
|
kw.pop('is_page', False)
|
|
metadata = {}
|
|
metadata.update(self.default_metadata)
|
|
metadata.update(kw)
|
|
makedirs(os.path.dirname(path))
|
|
if not content.endswith('\n'):
|
|
content += '\n'
|
|
with io.open(path, "w+", encoding="utf8") as fd:
|
|
if onefile:
|
|
fd.write(write_metadata(metadata))
|
|
fd.write('\n')
|
|
fd.write(content)
|
|
|
|
def set_site(self, site):
|
|
self.config_dependencies = []
|
|
for plugin_info in site.plugin_manager.getPluginsOfCategory("RestExtension"):
|
|
if plugin_info.name in site.config['DISABLED_PLUGINS']:
|
|
site.plugin_manager.removePluginFromCategory(plugin_info, "RestExtension")
|
|
continue
|
|
|
|
site.plugin_manager.activatePluginByName(plugin_info.name)
|
|
self.config_dependencies.append(plugin_info.name)
|
|
plugin_info.plugin_object.set_site(site)
|
|
plugin_info.plugin_object.short_help = plugin_info.description
|
|
|
|
self.logger = get_logger('compile_rest', site.loghandlers)
|
|
if not site.debug:
|
|
self.logger.level = 4
|
|
|
|
return super(CompileRestHTML5, self).set_site(site)
|
|
|
|
|
|
def get_observer(settings):
|
|
"""Return an observer for the docutils Reporter."""
|
|
def observer(msg):
|
|
"""Report docutils/rest messages to a Nikola user.
|
|
|
|
Error code mapping:
|
|
|
|
+------+---------+------+----------+
|
|
| dNUM | dNAME | lNUM | lNAME | d = docutils, l = logbook
|
|
+------+---------+------+----------+
|
|
| 0 | DEBUG | 1 | DEBUG |
|
|
| 1 | INFO | 2 | INFO |
|
|
| 2 | WARNING | 4 | WARNING |
|
|
| 3 | ERROR | 5 | ERROR |
|
|
| 4 | SEVERE | 6 | CRITICAL |
|
|
+------+---------+------+----------+
|
|
"""
|
|
errormap = {0: 1, 1: 2, 2: 4, 3: 5, 4: 6}
|
|
text = docutils.nodes.Element.astext(msg)
|
|
line = msg['line'] + settings['add_ln'] if 'line' in msg else 0
|
|
out = '[{source}{colon}{line}] {text}'.format(
|
|
source=settings['source'], colon=(':' if line else ''),
|
|
line=line, text=text)
|
|
settings['logger'].log(errormap[msg['level']], out)
|
|
|
|
return observer
|
|
|
|
|
|
class NikolaReader(docutils.readers.standalone.Reader):
|
|
|
|
def new_document(self):
|
|
"""Create and return a new empty document tree (root node)."""
|
|
document = docutils.utils.new_document(self.source.source_path, self.settings)
|
|
document.reporter.stream = False
|
|
document.reporter.attach_observer(get_observer(self.l_settings))
|
|
return document
|
|
|
|
|
|
def add_node(node, visit_function=None, depart_function=None):
|
|
"""
|
|
Register a Docutils node class.
|
|
This function is completely optional. It is a same concept as
|
|
`Sphinx add_node function <http://sphinx-doc.org/ext/appapi.html#sphinx.application.Sphinx.add_node>`_.
|
|
|
|
For example::
|
|
|
|
class Plugin(RestExtension):
|
|
|
|
name = "rest_math"
|
|
|
|
def set_site(self, site):
|
|
self.site = site
|
|
directives.register_directive('math', MathDirective)
|
|
add_node(MathBlock, visit_Math, depart_Math)
|
|
return super(Plugin, self).set_site(site)
|
|
|
|
class MathDirective(Directive):
|
|
def run(self):
|
|
node = MathBlock()
|
|
return [node]
|
|
|
|
class Math(docutils.nodes.Element): pass
|
|
|
|
def visit_Math(self, node):
|
|
self.body.append(self.starttag(node, 'math'))
|
|
|
|
def depart_Math(self, node):
|
|
self.body.append('</math>')
|
|
|
|
For full example, you can refer to `Microdata plugin <http://plugins.getnikola.com/#microdata>`_
|
|
"""
|
|
docutils.nodes._add_node_class_names([node.__name__])
|
|
if visit_function:
|
|
setattr(rst2html5.HTML5Translator, 'visit_' + node.__name__, visit_function)
|
|
if depart_function:
|
|
setattr(rst2html5.HTML5Translator, 'depart_' + node.__name__, depart_function)
|
|
|
|
|
|
def rst2html(source, source_path=None, source_class=docutils.io.StringInput,
|
|
destination_path=None, reader=None,
|
|
parser=None, parser_name='restructuredtext', writer=None,
|
|
writer_name='html5', settings=None, settings_spec=None,
|
|
settings_overrides=None, config_section=None,
|
|
enable_exit_status=None, logger=None, l_add_ln=0):
|
|
"""
|
|
Set up & run a `Publisher`, and return a dictionary of document parts.
|
|
Dictionary keys are the names of parts, and values are Unicode strings;
|
|
encoding is up to the client. For programmatic use with string I/O.
|
|
|
|
For encoded string input, be sure to set the 'input_encoding' setting to
|
|
the desired encoding. Set it to 'unicode' for unencoded Unicode string
|
|
input. Here's how::
|
|
|
|
publish_parts(..., settings_overrides={'input_encoding': 'unicode'})
|
|
|
|
Parameters: see `publish_programmatically`.
|
|
|
|
WARNING: `reader` should be None (or NikolaReader()) if you want Nikola to report
|
|
reStructuredText syntax errors.
|
|
"""
|
|
if reader is None:
|
|
reader = NikolaReader()
|
|
# For our custom logging, we have special needs and special settings we
|
|
# specify here.
|
|
# logger a logger from Nikola
|
|
# source source filename (docutils gets a string)
|
|
# add_ln amount of metadata lines (see comment in compile_html above)
|
|
reader.l_settings = {'logger': logger, 'source': source_path,
|
|
'add_ln': l_add_ln}
|
|
|
|
if writer is None:
|
|
writer = rst2html5.HTML5Writer()
|
|
|
|
pub = docutils.core.Publisher(reader, parser, writer, settings=settings,
|
|
source_class=source_class,
|
|
destination_class=docutils.io.StringOutput)
|
|
pub.set_components(None, parser_name, writer_name)
|
|
pub.process_programmatic_settings(
|
|
settings_spec, settings_overrides, config_section)
|
|
pub.set_source(source, None)
|
|
pub.settings._nikola_source_path = source_path
|
|
pub.set_destination(None, destination_path)
|
|
pub.publish(enable_exit_status=enable_exit_status)
|
|
|
|
return pub.writer.parts['body'], pub.document.reporter.max_level, pub.settings.record_dependencies
|