diff options
Diffstat (limited to 'nikola/plugins/compile')
41 files changed, 879 insertions, 805 deletions
diff --git a/nikola/plugins/compile/__init__.py b/nikola/plugins/compile/__init__.py index 60f1919..db78fce 100644 --- a/nikola/plugins/compile/__init__.py +++ b/nikola/plugins/compile/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated diff --git a/nikola/plugins/compile/html.plugin b/nikola/plugins/compile/html.plugin index 53ade61..be1f876 100644 --- a/nikola/plugins/compile/html.plugin +++ b/nikola/plugins/compile/html.plugin @@ -5,9 +5,9 @@ module = html [Documentation] author = Roberto Alsina version = 1.0 -website = http://getnikola.com +website = https://getnikola.com/ description = Compile HTML into HTML (just copy) [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = HTML diff --git a/nikola/plugins/compile/html.py b/nikola/plugins/compile/html.py index 5f8b244..80b6713 100644 --- a/nikola/plugins/compile/html.py +++ b/nikola/plugins/compile/html.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,33 +24,48 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html for HTML source files.""" +"""Page compiler plugin for HTML source files.""" -from __future__ import unicode_literals -import os import io +import os + +import lxml.html +from nikola import shortcodes as sc from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, write_metadata +from nikola.utils import LocaleBorg, makedirs, map_metadata, write_metadata class CompileHtml(PageCompiler): - """Compile HTML into HTML.""" name = "html" friendly_name = "HTML" + supports_metadata = True + + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile HTML into HTML strings, with shortcode support.""" + if not is_two_file: + _, data = self.split_metadata(data, post, lang) + new_data, shortcodes = sc.extract_shortcodes(data) + return self.site.apply_shortcodes_uuid(new_data, shortcodes, filename=source_path, extra_context={'post': post}) - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) - with io.open(dest, "w+", encoding="utf8") as out_file: - with io.open(source, "r", encoding="utf8") as in_file: + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: data = in_file.read() - if not is_two_file: - _, data = self.split_metadata(data) + data, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang) out_file.write(data) + if post is None: + if shortcode_deps: + self.logger.error( + "Cannot save dependencies for post {0} (post unknown)", + source) + else: + post._depfile[dest] += shortcode_deps return True def create_post(self, path, **kw): @@ -65,9 +80,41 @@ class CompileHtml(PageCompiler): makedirs(os.path.dirname(path)) if not content.endswith('\n'): content += '\n' - with io.open(path, "w+", encoding="utf8") as fd: + with io.open(path, "w+", encoding="utf-8") as fd: if onefile: - fd.write('<!--\n') - fd.write(write_metadata(metadata)) - fd.write('-->\n\n') + fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self)) fd.write(content) + + def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None): + """Read the metadata from a post's meta tags, and return a metadata dict.""" + if lang is None: + lang = LocaleBorg().current_lang + source_path = post.translated_source_path(lang) + + with io.open(source_path, 'r', encoding='utf-8-sig') as inf: + data = inf.read() + + metadata = {} + try: + doc = lxml.html.document_fromstring(data) + except lxml.etree.ParserError as e: + # Issue #374 -> #2851 + if str(e) == "Document is empty": + return {} + # let other errors raise + raise + title_tag = doc.find('*//title') + if title_tag is not None and title_tag.text: + metadata['title'] = title_tag.text + meta_tags = doc.findall('*//meta') + for tag in meta_tags: + k = tag.get('name', '').lower() + if not k: + continue + elif k == 'keywords': + k = 'tags' + content = tag.get('content') + if content: + metadata[k] = content + map_metadata(metadata, 'html_metadata', self.site.config) + return metadata diff --git a/nikola/plugins/compile/ipynb.plugin b/nikola/plugins/compile/ipynb.plugin index c369ab2..c146172 100644 --- a/nikola/plugins/compile/ipynb.plugin +++ b/nikola/plugins/compile/ipynb.plugin @@ -6,8 +6,8 @@ module = ipynb author = Damian Avila, Chris Warrick and others version = 2.0.0 website = http://www.damian.oquanta.info/ -description = Compile IPython notebooks into Nikola posts +description = Compile Jupyter notebooks into Nikola posts [Nikola] -plugincategory = Compiler -friendlyname = Jupyter/IPython Notebook +PluginCategory = Compiler +friendlyname = Jupyter Notebook diff --git a/nikola/plugins/compile/ipynb.py b/nikola/plugins/compile/ipynb.py index a9dedde..039604b 100644 --- a/nikola/plugins/compile/ipynb.py +++ b/nikola/plugins/compile/ipynb.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2013-2015 Damián Avila, Chris Warrick and others. +# Copyright © 2013-2020 Damián Avila, Chris Warrick and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,76 +24,95 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html based on nbconvert.""" +"""Page compiler plugin for nbconvert.""" -from __future__ import unicode_literals, print_function import io +import json import os -import sys try: - import IPython - from IPython.nbconvert.exporters import HTMLExporter - if IPython.version_info[0] >= 3: # API changed with 3.0.0 - from IPython import nbformat - current_nbformat = nbformat.current_nbformat - from IPython.kernel import kernelspec - else: - import IPython.nbformat.current as nbformat - current_nbformat = 'json' - kernelspec = None - - from IPython.config import Config + import nbconvert + from nbconvert.exporters import HTMLExporter + import nbformat + current_nbformat = nbformat.current_nbformat + from jupyter_client import kernelspec + from traitlets.config import Config + NBCONVERT_VERSION_MAJOR = int(nbconvert.__version__.partition(".")[0]) flag = True except ImportError: flag = None +from nikola import shortcodes as sc from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing, get_logger, STDERR_HANDLER +from nikola.utils import makedirs, req_missing, LocaleBorg class CompileIPynb(PageCompiler): - """Compile IPynb into HTML.""" name = "ipynb" - friendly_name = "Jupyter/IPython Notebook" + friendly_name = "Jupyter Notebook" demote_headers = True - default_kernel = 'python2' if sys.version_info[0] == 2 else 'python3' - - def set_site(self, site): - """Set Nikola site.""" - self.logger = get_logger('compile_ipynb', STDERR_HANDLER) - super(CompileIPynb, self).set_site(site) + default_kernel = 'python3' + supports_metadata = True - def compile_html_string(self, source, is_two_file=True): + def _compile_string(self, nb_json): """Export notebooks as HTML strings.""" - if flag is None: - req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)') - HTMLExporter.default_template = 'basic' - c = Config(self.site.config['IPYNB_CONFIG']) + self._req_missing_ipynb() + c = Config(get_default_jupyter_config()) + c.merge(Config(self.site.config['IPYNB_CONFIG'])) + if 'template_file' not in self.site.config['IPYNB_CONFIG'].get('Exporter', {}): + if NBCONVERT_VERSION_MAJOR >= 6: + c['Exporter']['template_file'] = 'classic/base.html.j2' + else: + c['Exporter']['template_file'] = 'basic.tpl' # not a typo exportHtml = HTMLExporter(config=c) - with io.open(source, "r", encoding="utf8") as in_file: - nb_json = nbformat.read(in_file, current_nbformat) - (body, resources) = exportHtml.from_notebook_node(nb_json) + body, _ = exportHtml.from_notebook_node(nb_json) return body - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + @staticmethod + def _nbformat_read(in_file): + return nbformat.read(in_file, current_nbformat) + + def _req_missing_ipynb(self): + if flag is None: + req_missing(['notebook>=4.0.0'], 'build this site (compile ipynb)') + + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile notebooks into HTML strings.""" + new_data, shortcodes = sc.extract_shortcodes(data) + output = self._compile_string(nbformat.reads(new_data, current_nbformat)) + return self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post}) + + def compile(self, source, dest, is_two_file=False, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) - with io.open(dest, "w+", encoding="utf8") as out_file: - out_file.write(self.compile_html_string(source, is_two_file)) + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: + nb_str = in_file.read() + output, shortcode_deps = self.compile_string(nb_str, source, + is_two_file, post, + lang) + out_file.write(output) + if post is None: + if shortcode_deps: + self.logger.error( + "Cannot save dependencies for post {0} (post unknown)", + source) + else: + post._depfile[dest] += shortcode_deps - def read_metadata(self, post, file_metadata_regexp=None, unslugify_titles=False, lang=None): + def read_metadata(self, post, lang=None): """Read metadata directly from ipynb file. - As ipynb file support arbitrary metadata as json, the metadata used by Nikola + As ipynb files support arbitrary metadata as json, the metadata used by Nikola will be assume to be in the 'nikola' subfield. """ - if flag is None: - req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)') - source = post.source_path - with io.open(source, "r", encoding="utf8") as in_file: + self._req_missing_ipynb() + if lang is None: + lang = LocaleBorg().current_lang + source = post.translated_source_path(lang) + with io.open(source, "r", encoding="utf-8-sig") as in_file: nb_json = nbformat.read(in_file, current_nbformat) # Metadata might not exist in two-file posts or in hand-crafted # .ipynb files. @@ -101,11 +120,10 @@ class CompileIPynb(PageCompiler): def create_post(self, path, **kw): """Create a new post.""" - if flag is None: - req_missing(['ipython[notebook]>=2.0.0'], 'build this site (compile ipynb)') + self._req_missing_ipynb() content = kw.pop('content', None) onefile = kw.pop('onefile', False) - kernel = kw.pop('ipython_kernel', None) + kernel = kw.pop('jupyter_kernel', None) # is_page is not needed to create the file kw.pop('is_page', False) @@ -119,40 +137,52 @@ class CompileIPynb(PageCompiler): # imported .ipynb file, guaranteed to start with "{" because it’s JSON. nb = nbformat.reads(content, current_nbformat) else: - if IPython.version_info[0] >= 3: - nb = nbformat.v4.new_notebook() - nb["cells"] = [nbformat.v4.new_markdown_cell(content)] - else: - nb = nbformat.new_notebook() - nb["worksheets"] = [nbformat.new_worksheet(cells=[nbformat.new_text_cell('markdown', [content])])] - - if kernelspec is not None: - if kernel is None: - kernel = self.default_kernel - self.logger.notice('No kernel specified, assuming "{0}".'.format(kernel)) - - IPYNB_KERNELS = {} - ksm = kernelspec.KernelSpecManager() - for k in ksm.find_kernel_specs(): - IPYNB_KERNELS[k] = ksm.get_kernel_spec(k).to_dict() - IPYNB_KERNELS[k]['name'] = k - del IPYNB_KERNELS[k]['argv'] - - if kernel not in IPYNB_KERNELS: - self.logger.error('Unknown kernel "{0}". Maybe you mispelled it?'.format(kernel)) - self.logger.info("Available kernels: {0}".format(", ".join(sorted(IPYNB_KERNELS)))) - raise Exception('Unknown kernel "{0}"'.format(kernel)) - - nb["metadata"]["kernelspec"] = IPYNB_KERNELS[kernel] - else: - # Older IPython versions don’t need kernelspecs. - pass + nb = nbformat.v4.new_notebook() + nb["cells"] = [nbformat.v4.new_markdown_cell(content)] + + if kernel is None: + kernel = self.default_kernel + self.logger.warning('No kernel specified, assuming "{0}".'.format(kernel)) + + IPYNB_KERNELS = {} + ksm = kernelspec.KernelSpecManager() + for k in ksm.find_kernel_specs(): + IPYNB_KERNELS[k] = ksm.get_kernel_spec(k).to_dict() + IPYNB_KERNELS[k]['name'] = k + del IPYNB_KERNELS[k]['argv'] + + if kernel not in IPYNB_KERNELS: + self.logger.error('Unknown kernel "{0}". Maybe you mispelled it?'.format(kernel)) + self.logger.info("Available kernels: {0}".format(", ".join(sorted(IPYNB_KERNELS)))) + raise Exception('Unknown kernel "{0}"'.format(kernel)) + + nb["metadata"]["kernelspec"] = IPYNB_KERNELS[kernel] if onefile: nb["metadata"]["nikola"] = metadata - with io.open(path, "w+", encoding="utf8") as fd: - if IPython.version_info[0] >= 3: - nbformat.write(nb, fd, 4) - else: - nbformat.write(nb, fd, 'ipynb') + with io.open(path, "w+", encoding="utf-8") as fd: + nbformat.write(nb, fd, 4) + + +def get_default_jupyter_config(): + """Search default jupyter configuration location paths. + + Return dictionary from configuration json files. + """ + config = {} + from jupyter_core.paths import jupyter_config_path + + for parent in jupyter_config_path(): + try: + for file in os.listdir(parent): + if 'nbconvert' in file and file.endswith('.json'): + abs_path = os.path.join(parent, file) + with open(abs_path) as config_file: + config.update(json.load(config_file)) + except OSError: + # some paths jupyter uses to find configurations + # may not exist + pass + + return config diff --git a/nikola/plugins/compile/markdown.plugin b/nikola/plugins/compile/markdown.plugin index f7d11b1..85c67c3 100644 --- a/nikola/plugins/compile/markdown.plugin +++ b/nikola/plugins/compile/markdown.plugin @@ -5,9 +5,9 @@ module = markdown [Documentation] author = Roberto Alsina version = 1.0 -website = http://getnikola.com +website = https://getnikola.com/ description = Compile Markdown into HTML [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = Markdown diff --git a/nikola/plugins/compile/markdown/__init__.py b/nikola/plugins/compile/markdown/__init__.py index c1425a1..74e8c75 100644 --- a/nikola/plugins/compile/markdown/__init__.py +++ b/nikola/plugins/compile/markdown/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,59 +24,110 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html based on markdown.""" - -from __future__ import unicode_literals +"""Page compiler plugin for Markdown.""" import io +import json import os +import threading + +from nikola import shortcodes as sc +from nikola.plugin_categories import PageCompiler +from nikola.utils import makedirs, req_missing, write_metadata, LocaleBorg, map_metadata try: - from markdown import markdown + from markdown import Markdown except ImportError: - markdown = None # NOQA - nikola_extension = None - gist_extension = None - podcast_extension = None + Markdown = None -from nikola.plugin_categories import PageCompiler -from nikola.utils import makedirs, req_missing, write_metadata +class ThreadLocalMarkdown(threading.local): + """Convert Markdown to HTML using per-thread Markdown objects. -class CompileMarkdown(PageCompiler): + See discussion in #2661. + """ + + def __init__(self, extensions, extension_configs): + """Create a Markdown instance.""" + self.markdown = Markdown(extensions=extensions, extension_configs=extension_configs, output_format="html5") + + def convert(self, data): + """Convert data to HTML and reset internal state.""" + result = self.markdown.convert(data) + try: + meta = {} + for k in self.markdown.Meta: # This reads everything as lists + meta[k.lower()] = ','.join(self.markdown.Meta[k]) + except Exception: + meta = {} + self.markdown.reset() + return result, meta + +class CompileMarkdown(PageCompiler): """Compile Markdown into HTML.""" name = "markdown" friendly_name = "Markdown" demote_headers = True - extensions = [] site = None + supports_metadata = False def set_site(self, site): """Set Nikola site.""" - super(CompileMarkdown, self).set_site(site) + super().set_site(site) self.config_dependencies = [] + extensions = [] for plugin_info in self.get_compiler_extensions(): self.config_dependencies.append(plugin_info.name) - self.extensions.append(plugin_info.plugin_object) + extensions.append(plugin_info.plugin_object) plugin_info.plugin_object.short_help = plugin_info.description - self.config_dependencies.append(str(sorted(site.config.get("MARKDOWN_EXTENSIONS")))) - - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" - if markdown is None: + site_extensions = self.site.config.get("MARKDOWN_EXTENSIONS") + self.config_dependencies.append(str(sorted(site_extensions))) + extensions.extend(site_extensions) + + site_extension_configs = self.site.config.get("MARKDOWN_EXTENSION_CONFIGS") + if site_extension_configs: + self.config_dependencies.append(json.dumps(site_extension_configs.values, sort_keys=True)) + + if Markdown is not None: + self.converters = {} + for lang in self.site.config['TRANSLATIONS']: + lang_extension_configs = site_extension_configs(lang) if site_extension_configs else {} + self.converters[lang] = ThreadLocalMarkdown(extensions, lang_extension_configs) + self.supports_metadata = 'markdown.extensions.meta' in extensions + + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile Markdown into HTML strings.""" + if lang is None: + lang = LocaleBorg().current_lang + if Markdown is None: + req_missing(['markdown'], 'build this site (compile Markdown)') + if not is_two_file: + _, data = self.split_metadata(data, post, lang) + new_data, shortcodes = sc.extract_shortcodes(data) + output, _ = self.converters[lang].convert(new_data) + output, shortcode_deps = self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post}) + return output, shortcode_deps + + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" + if Markdown is None: req_missing(['markdown'], 'build this site (compile Markdown)') makedirs(os.path.dirname(dest)) - self.extensions += self.site.config.get("MARKDOWN_EXTENSIONS") - with io.open(dest, "w+", encoding="utf8") as out_file: - with io.open(source, "r", encoding="utf8") as in_file: + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: data = in_file.read() - if not is_two_file: - _, data = self.split_metadata(data) - output = markdown(data, self.extensions) + output, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang) out_file.write(output) + if post is None: + if shortcode_deps: + self.logger.error( + "Cannot save dependencies for post {0} (post unknown)", + source) + else: + post._depfile[dest] += shortcode_deps def create_post(self, path, **kw): """Create a new post.""" @@ -91,9 +142,30 @@ class CompileMarkdown(PageCompiler): makedirs(os.path.dirname(path)) if not content.endswith('\n'): content += '\n' - with io.open(path, "w+", encoding="utf8") as fd: + with io.open(path, "w+", encoding="utf-8") as fd: if onefile: - fd.write('<!-- \n') - fd.write(write_metadata(metadata)) - fd.write('-->\n\n') + fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self)) fd.write(content) + + def read_metadata(self, post, lang=None): + """Read the metadata from a post, and return a metadata dict.""" + lang = lang or self.site.config['DEFAULT_LANG'] + if not self.supports_metadata: + return {} + if Markdown is None: + req_missing(['markdown'], 'build this site (compile Markdown)') + if lang is None: + lang = LocaleBorg().current_lang + source = post.translated_source_path(lang) + with io.open(source, 'r', encoding='utf-8-sig') as inf: + # Note: markdown meta returns lowercase keys + data = inf.read() + # If the metadata starts with "---" it's actually YAML and + # we should not let markdown parse it, because it will do + # bad things like setting empty tags to "''" + if data.startswith('---\n'): + return {} + _, meta = self.converters[lang].convert(data) + # Map metadata from other platforms to names Nikola expects (Issue #2817) + map_metadata(meta, 'markdown_metadata', self.site.config) + return meta diff --git a/nikola/plugins/compile/markdown/mdx_gist.plugin b/nikola/plugins/compile/markdown/mdx_gist.plugin index 7fe676c..f962cb7 100644 --- a/nikola/plugins/compile/markdown/mdx_gist.plugin +++ b/nikola/plugins/compile/markdown/mdx_gist.plugin @@ -4,11 +4,11 @@ module = mdx_gist [Nikola] compiler = markdown -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Extension for embedding gists diff --git a/nikola/plugins/compile/markdown/mdx_gist.py b/nikola/plugins/compile/markdown/mdx_gist.py index f439fa2..f6ce20a 100644 --- a/nikola/plugins/compile/markdown/mdx_gist.py +++ b/nikola/plugins/compile/markdown/mdx_gist.py @@ -22,7 +22,7 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # Warning: URL formats of "raw" gists are undocummented and subject to change. -# See also: http://developer.github.com/v3/gists/ +# See also: https://developer.github.com/v3/gists/ # # Inspired by "[Python] reStructuredText GitHub Gist directive" # (https://gist.github.com/brianhsu/1407759), public domain by Brian Hsu @@ -31,164 +31,54 @@ Extension to Python Markdown for Embedded Gists (gist.github.com). Basic Example: - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... [:gist: 4747847] - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/4747847.js"></script> - <noscript> - <pre>import this</pre> - </noscript> - </div> - </p> + Text of the gist: + [:gist: 4747847] Example with filename: - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... [:gist: 4747847 zen.py] - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/4747847.js?file=zen.py"></script> - <noscript> - <pre>import this</pre> - </noscript> - </div> - </p> + Text of the gist: + [:gist: 4747847 zen.py] Basic Example with hexidecimal id: - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... [:gist: c4a43d6fdce612284ac0] - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/c4a43d6fdce612284ac0.js"></script> - <noscript> - <pre>Moo</pre> - </noscript> - </div> - </p> + Text of the gist: + [:gist: c4a43d6fdce612284ac0] Example with hexidecimal id filename: - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... [:gist: c4a43d6fdce612284ac0 cow.txt] - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/c4a43d6fdce612284ac0.js?file=cow.txt"></script> - <noscript> - <pre>Moo</pre> - </noscript> - </div> - </p> + Text of the gist: + [:gist: c4a43d6fdce612284ac0 cow.txt] Example using reStructuredText syntax: - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... .. gist:: 4747847 zen.py - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/4747847.js?file=zen.py"></script> - <noscript> - <pre>import this</pre> - </noscript> - </div> - </p> + Text of the gist: + .. gist:: 4747847 zen.py Example using hexidecimal ID with reStructuredText syntax: - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... .. gist:: c4a43d6fdce612284ac0 - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/c4a43d6fdce612284ac0.js"></script> - <noscript> - <pre>Moo</pre> - </noscript> - </div> - </p> + Text of the gist: + .. gist:: c4a43d6fdce612284ac0 Example using hexidecimal ID and filename with reStructuredText syntax: - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... .. gist:: c4a43d6fdce612284ac0 cow.txt - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/c4a43d6fdce612284ac0.js?file=cow.txt"></script> - <noscript> - <pre>Moo</pre> - </noscript> - </div> - </p> + Text of the gist: + .. gist:: c4a43d6fdce612284ac0 cow.txt Error Case: non-existent Gist ID: - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... [:gist: 0] - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/0.js"></script> - <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.githubusercontent.com/raw/0 --></noscript> - </div> - </p> - -Error Case: non-existent file: - - >>> import markdown - >>> text = ''' - ... Text of the gist: - ... [:gist: 4747847 doesntexist.py] - ... ''' - >>> html = markdown.markdown(text, [GistExtension()]) - >>> print(html) - <p>Text of the gist: - <div class="gist"> - <script src="https://gist.github.com/4747847.js?file=doesntexist.py"></script> - <noscript><!-- WARNING: Received a 404 response from Gist URL: https://gist.githubusercontent.com/raw/4747847/doesntexist.py --></noscript> - </div> - </p> + Text of the gist: + [:gist: 0] + +Error Case: non-existent file: + + Text of the gist: + [:gist: 4747847 doesntexist.py] """ -from __future__ import unicode_literals, print_function +import requests + +from nikola.plugin_categories import MarkdownExtension +from nikola.utils import get_logger try: from markdown.extensions import Extension @@ -200,12 +90,8 @@ except ImportError: # the markdown compiler will fail first Extension = Pattern = object -from nikola.plugin_categories import MarkdownExtension -from nikola.utils import get_logger, STDERR_HANDLER - -import requests -LOGGER = get_logger('compile_markdown.mdx_gist', STDERR_HANDLER) +LOGGER = get_logger('compile_markdown.mdx_gist') GIST_JS_URL = "https://gist.github.com/{0}.js" GIST_FILE_JS_URL = "https://gist.github.com/{0}.js?file={1}" @@ -217,7 +103,6 @@ GIST_RST_RE = r'(?m)^\.\.\s*gist::\s*(?P<gist_id>[^\]\s]+)(?:\s*(?P<filename>.+? class GistFetchException(Exception): - """Raised when attempt to fetch content of a Gist from github.com fails.""" def __init__(self, url, status_code): @@ -228,7 +113,6 @@ class GistFetchException(Exception): class GistPattern(Pattern): - """InlinePattern for footnote markers in a document's body text.""" def __init__(self, pattern, configs): @@ -282,7 +166,7 @@ class GistPattern(Pattern): pre_elem.text = AtomicString(raw_gist) except GistFetchException as e: - LOGGER.warn(e.message) + LOGGER.warning(e.message) warning_comment = etree.Comment(' WARNING: {0} '.format(e.message)) noscript_elem.append(warning_comment) @@ -290,7 +174,6 @@ class GistPattern(Pattern): class GistExtension(MarkdownExtension, Extension): - """Gist extension for Markdown.""" def __init__(self, configs={}): @@ -302,15 +185,15 @@ class GistExtension(MarkdownExtension, Extension): for key, value in configs: self.setConfig(key, value) - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md, md_globals=None): """Extend Markdown.""" gist_md_pattern = GistPattern(GIST_MD_RE, self.getConfigs()) gist_md_pattern.md = md - md.inlinePatterns.add('gist', gist_md_pattern, "<not_strong") + md.inlinePatterns.register(gist_md_pattern, 'gist', 175) gist_rst_pattern = GistPattern(GIST_RST_RE, self.getConfigs()) gist_rst_pattern.md = md - md.inlinePatterns.add('gist-rst', gist_rst_pattern, ">gist") + md.inlinePatterns.register(gist_rst_pattern, 'gist-rst', 176) md.registerExtension(self) @@ -319,6 +202,7 @@ def makeExtension(configs=None): # pragma: no cover """Make Markdown extension.""" return GistExtension(configs) + if __name__ == '__main__': import doctest diff --git a/nikola/plugins/compile/markdown/mdx_nikola.plugin b/nikola/plugins/compile/markdown/mdx_nikola.plugin index 12e4fb6..9751598 100644 --- a/nikola/plugins/compile/markdown/mdx_nikola.plugin +++ b/nikola/plugins/compile/markdown/mdx_nikola.plugin @@ -4,11 +4,11 @@ module = mdx_nikola [Nikola] compiler = markdown -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Nikola-specific Markdown extensions diff --git a/nikola/plugins/compile/markdown/mdx_nikola.py b/nikola/plugins/compile/markdown/mdx_nikola.py index 54cc18c..06a6d9a 100644 --- a/nikola/plugins/compile/markdown/mdx_nikola.py +++ b/nikola/plugins/compile/markdown/mdx_nikola.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,25 +24,31 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Markdown Extension for Nikola-specific post-processing.""" +"""Markdown Extension for Nikola. + +- Specific post-processing. +- Strikethrough inline patterns. +""" -from __future__ import unicode_literals import re + +from nikola.plugin_categories import MarkdownExtension + try: from markdown.postprocessors import Postprocessor + from markdown.inlinepatterns import SimpleTagPattern from markdown.extensions import Extension except ImportError: # No need to catch this, if you try to use this without Markdown, # the markdown compiler will fail first - Postprocessor = Extension = object + Postprocessor = SimpleTagPattern = Extension = object -from nikola.plugin_categories import MarkdownExtension CODERE = re.compile('<div class="codehilite"><pre>(.*?)</pre></div>', flags=re.MULTILINE | re.DOTALL) +STRIKE_RE = r"(~{2})(.+?)(~{2})" # ~~strike~~ class NikolaPostProcessor(Postprocessor): - """Nikola-specific post-processing for Markdown.""" def run(self, text): @@ -57,13 +63,22 @@ class NikolaPostProcessor(Postprocessor): class NikolaExtension(MarkdownExtension, Extension): + """Nikola Markdown extensions.""" - """Extension for injecting the postprocessor.""" - - def extendMarkdown(self, md, md_globals): + def _add_nikola_post_processor(self, md): """Extend Markdown with the postprocessor.""" pp = NikolaPostProcessor() - md.postprocessors.add('nikola_post_processor', pp, '_end') + md.postprocessors.register(pp, 'nikola_post_processor', 1) + + def _add_strikethrough_inline_pattern(self, md): + """Support PHP-Markdown style strikethrough, for example: ``~~strike~~``.""" + pattern = SimpleTagPattern(STRIKE_RE, 'del') + md.inlinePatterns.register(pattern, 'strikethrough', 175) + + def extendMarkdown(self, md, md_globals=None): + """Extend markdown to Nikola flavours.""" + self._add_nikola_post_processor(md) + self._add_strikethrough_inline_pattern(md) md.registerExtension(self) diff --git a/nikola/plugins/compile/markdown/mdx_podcast.plugin b/nikola/plugins/compile/markdown/mdx_podcast.plugin index c92a8a0..df5260d 100644 --- a/nikola/plugins/compile/markdown/mdx_podcast.plugin +++ b/nikola/plugins/compile/markdown/mdx_podcast.plugin @@ -4,11 +4,11 @@ module = mdx_podcast [Nikola] compiler = markdown -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Markdown extensions for embedding podcasts and other audio files diff --git a/nikola/plugins/compile/markdown/mdx_podcast.py b/nikola/plugins/compile/markdown/mdx_podcast.py index 61afdbf..5090407 100644 --- a/nikola/plugins/compile/markdown/mdx_podcast.py +++ b/nikola/plugins/compile/markdown/mdx_podcast.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright © 2013-2015 Michael Rabbitt, Roberto Alsina and others. +# Copyright © 2013-2020 Michael Rabbitt, 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 @@ -30,13 +30,12 @@ Extension to Python Markdown for Embedded Audio. Basic Example: >>> import markdown ->>> text = "[podcast]http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3[/podcast]" +>>> text = "[podcast]https://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3[/podcast]" >>> html = markdown.markdown(text, [PodcastExtension()]) >>> print(html) -<p><audio controls=""><source src="http://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3" type="audio/mpeg"></source></audio></p> +<p><audio controls=""><source src="https://archive.org/download/Rebeldes_Stereotipos/rs20120609_1.mp3" type="audio/mpeg"></source></audio></p> """ -from __future__ import print_function, unicode_literals from nikola.plugin_categories import MarkdownExtension try: from markdown.extensions import Extension @@ -51,7 +50,6 @@ PODCAST_RE = r'\[podcast\](?P<url>.+)\[/podcast\]' class PodcastPattern(Pattern): - """InlinePattern for footnote markers in a document's body text.""" def __init__(self, pattern, configs): @@ -70,8 +68,7 @@ class PodcastPattern(Pattern): class PodcastExtension(MarkdownExtension, Extension): - - """"Podcast extension for Markdown.""" + """Podcast extension for Markdown.""" def __init__(self, configs={}): """Initialize extension.""" @@ -82,11 +79,11 @@ class PodcastExtension(MarkdownExtension, Extension): for key, value in configs: self.setConfig(key, value) - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md, md_globals=None): """Extend Markdown.""" podcast_md_pattern = PodcastPattern(PODCAST_RE, self.getConfigs()) podcast_md_pattern.md = md - md.inlinePatterns.add('podcast', podcast_md_pattern, "<not_strong") + md.inlinePatterns.register(podcast_md_pattern, 'podcast', 175) md.registerExtension(self) @@ -94,6 +91,7 @@ def makeExtension(configs=None): # pragma: no cover """Make Markdown extension.""" return PodcastExtension(configs) + if __name__ == '__main__': import doctest doctest.testmod(optionflags=(doctest.NORMALIZE_WHITESPACE + diff --git a/nikola/plugins/compile/pandoc.plugin b/nikola/plugins/compile/pandoc.plugin index 3ff3668..8f339e4 100644 --- a/nikola/plugins/compile/pandoc.plugin +++ b/nikola/plugins/compile/pandoc.plugin @@ -5,9 +5,9 @@ module = pandoc [Documentation] author = Roberto Alsina version = 1.0 -website = http://getnikola.com +website = https://getnikola.com/ description = Compile markups into HTML using pandoc [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = Pandoc diff --git a/nikola/plugins/compile/pandoc.py b/nikola/plugins/compile/pandoc.py index 3030626..af14344 100644 --- a/nikola/plugins/compile/pandoc.py +++ b/nikola/plugins/compile/pandoc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,12 +24,11 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html based on pandoc. +"""Page compiler plugin for pandoc. You will need, of course, to install pandoc """ -from __future__ import unicode_literals import io import os @@ -40,7 +39,6 @@ from nikola.utils import req_missing, makedirs, write_metadata class CompilePandoc(PageCompiler): - """Compile markups into HTML using pandoc.""" name = "pandoc" @@ -49,17 +47,32 @@ class CompilePandoc(PageCompiler): def set_site(self, site): """Set Nikola site.""" self.config_dependencies = [str(site.config['PANDOC_OPTIONS'])] - super(CompilePandoc, self).set_site(site) + super().set_site(site) - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) try: subprocess.check_call(['pandoc', '-o', dest, source] + self.site.config['PANDOC_OPTIONS']) + with open(dest, 'r', encoding='utf-8-sig') as inf: + output, shortcode_deps = self.site.apply_shortcodes(inf.read()) + with open(dest, 'w', encoding='utf-8') as outf: + outf.write(output) + if post is None: + if shortcode_deps: + self.logger.error( + "Cannot save dependencies for post {0} (post unknown)", + source) + else: + post._depfile[dest] += shortcode_deps except OSError as e: if e.strreror == 'No such file or directory': req_missing(['pandoc'], 'build this site (compile with pandoc)', python=False) + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile into HTML strings.""" + raise ValueError("Pandoc compiler does not support compile_string due to multiple output formats") + def create_post(self, path, **kw): """Create a new post.""" content = kw.pop('content', None) @@ -74,7 +87,5 @@ class CompilePandoc(PageCompiler): content += '\n' with io.open(path, "w+", encoding="utf8") as fd: if onefile: - fd.write('<!--\n') - fd.write(write_metadata(metadata)) - fd.write('-->\n\n') + fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self)) fd.write(content) diff --git a/nikola/plugins/compile/php.plugin b/nikola/plugins/compile/php.plugin index 151c022..13384bd 100644 --- a/nikola/plugins/compile/php.plugin +++ b/nikola/plugins/compile/php.plugin @@ -5,9 +5,9 @@ module = php [Documentation] author = Roberto Alsina version = 1.0 -website = http://getnikola.com +website = https://getnikola.com/ description = Compile PHP into HTML (just copy and name the file .php) [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = PHP diff --git a/nikola/plugins/compile/php.py b/nikola/plugins/compile/php.py index 28f4923..818e10d 100644 --- a/nikola/plugins/compile/php.py +++ b/nikola/plugins/compile/php.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -24,27 +24,24 @@ # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Implementation of compile_html for HTML+php.""" +"""Page compiler plugin for PHP.""" -from __future__ import unicode_literals - -import os import io +import os +from hashlib import md5 from nikola.plugin_categories import PageCompiler from nikola.utils import makedirs, write_metadata -from hashlib import md5 class CompilePhp(PageCompiler): - """Compile PHP into PHP.""" name = "php" friendly_name = "PHP" - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" makedirs(os.path.dirname(dest)) with io.open(dest, "w+", encoding="utf8") as out_file: with open(source, "rb") as in_file: @@ -52,6 +49,10 @@ class CompilePhp(PageCompiler): out_file.write('<!-- __NIKOLA_PHP_TEMPLATE_INJECTION source:{0} checksum:{1}__ -->'.format(source, hash)) return True + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): + """Compile PHP into HTML strings.""" + return data, [] + def create_post(self, path, **kw): """Create a new post.""" content = kw.pop('content', None) @@ -77,9 +78,7 @@ class CompilePhp(PageCompiler): content += '\n' with io.open(path, "w+", encoding="utf8") as fd: if onefile: - fd.write('<!--\n') - fd.write(write_metadata(metadata)) - fd.write('-->\n\n') + fd.write(write_metadata(metadata, comment_wrap=True, site=self.site, compiler=self)) fd.write(content) def extension(self): diff --git a/nikola/plugins/compile/rest.plugin b/nikola/plugins/compile/rest.plugin index cf842c7..43bdf2d 100644 --- a/nikola/plugins/compile/rest.plugin +++ b/nikola/plugins/compile/rest.plugin @@ -5,9 +5,9 @@ module = rest [Documentation] author = Roberto Alsina version = 1.0 -website = http://getnikola.com -description = Compile reSt into HTML +website = https://getnikola.com/ +description = Compile reST into HTML [Nikola] -plugincategory = Compiler +PluginCategory = Compiler friendlyname = reStructuredText diff --git a/nikola/plugins/compile/rest/__init__.py b/nikola/plugins/compile/rest/__init__.py index b99e872..44da076 100644 --- a/nikola/plugins/compile/rest/__init__.py +++ b/nikola/plugins/compile/rest/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,87 +26,138 @@ """reStructuredText compiler for Nikola.""" -from __future__ import unicode_literals import io +import logging import os import docutils.core import docutils.nodes +import docutils.transforms import docutils.utils import docutils.io import docutils.readers.standalone -import docutils.writers.html4css1 +import docutils.writers.html5_polyglot +import docutils.parsers.rst.directives +from docutils.parsers.rst import roles +from nikola.nikola import LEGAL_VALUES +from nikola.metadata_extractors import MetaCondition from nikola.plugin_categories import PageCompiler -from nikola.utils import unicode_str, get_logger, makedirs, write_metadata, STDERR_HANDLER +from nikola.utils import ( + makedirs, + write_metadata, + LocaleBorg, + map_metadata +) class CompileRest(PageCompiler): - """Compile reStructuredText into HTML.""" name = "rest" friendly_name = "reStructuredText" demote_headers = True logger = None - - def _read_extra_deps(self, post): - """Read 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): - """Add dependency to post object to check .dep file.""" - post.add_dependency(lambda: self._read_extra_deps(post), 'fragment') - - def compile_html_string(self, data, source_path=None, is_two_file=True): + supports_metadata = True + metadata_conditions = [(MetaCondition.config_bool, "USE_REST_DOCINFO_METADATA")] + + def read_metadata(self, post, lang=None): + """Read the metadata from a post, and return a metadata dict.""" + if lang is None: + lang = LocaleBorg().current_lang + source_path = post.translated_source_path(lang) + + # Silence reST errors, some of which are due to a different + # environment. Real issues will be reported while compiling. + null_logger = logging.getLogger('NULL') + null_logger.setLevel(1000) + with io.open(source_path, 'r', encoding='utf-8-sig') as inf: + data = inf.read() + _, _, _, document = rst2html(data, logger=null_logger, source_path=source_path, transforms=self.site.rst_transforms) + meta = {} + if 'title' in document: + meta['title'] = document['title'] + for docinfo in document.traverse(docutils.nodes.docinfo): + for element in docinfo.children: + if element.tagname == 'field': # custom fields (e.g. summary) + name_elem, body_elem = element.children + name = name_elem.astext() + value = body_elem.astext() + elif element.tagname == 'authors': # author list + name = element.tagname + value = [element.astext() for element in element.children] + else: # standard fields (e.g. address) + name = element.tagname + value = element.astext() + name = name.lower() + + meta[name] = value + + # Put 'authors' meta field contents in 'author', too + if 'authors' in meta and 'author' not in meta: + meta['author'] = '; '.join(meta['authors']) + + # Map metadata from other platforms to names Nikola expects (Issue #2817) + map_metadata(meta, 'rest_docinfo', self.site.config) + return meta + + def compile_string(self, data, source_path=None, is_two_file=True, post=None, lang=None): """Compile reST into HTML strings.""" # 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). add_ln = 0 if not is_two_file: - m_data, data = self.split_metadata(data) + m_data, data = self.split_metadata(data, post, lang) add_ln = len(m_data.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': 1, - '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_path, l_add_ln=add_ln, transforms=self.site.rst_transforms) - if not isinstance(output, unicode_str): + settings_overrides = { + 'initial_header_level': 1, + 'record_dependencies': True, + 'stylesheet_path': None, + 'link_stylesheet': True, + 'syntax_highlight': 'short', + # This path is not used by Nikola, but we need something to silence + # warnings about it from reST. + 'math_output': 'mathjax /assets/js/mathjax.js', + 'template': default_template_path, + 'language_code': LEGAL_VALUES['DOCUTILS_LOCALES'].get(LocaleBorg().current_lang, 'en'), + 'doctitle_xform': self.site.config.get('USE_REST_DOCINFO_METADATA'), + 'file_insertion_enabled': self.site.config.get('REST_FILE_INSERTION_ENABLED'), + } + + from nikola import shortcodes as sc + new_data, shortcodes = sc.extract_shortcodes(data) + if self.site.config.get('HIDE_REST_DOCINFO', False): + self.site.rst_transforms.append(RemoveDocinfo) + output, error_level, deps, _ = rst2html( + new_data, settings_overrides=settings_overrides, logger=self.logger, source_path=source_path, l_add_ln=add_ln, transforms=self.site.rst_transforms) + if not isinstance(output, str): # To prevent some weird bugs here or there. # Original issue: empty files. `output` became a bytestring. output = output.decode('utf-8') - return output, error_level, deps - def compile_html(self, source, dest, is_two_file=True): - """Compile source file into HTML and save as dest.""" + output, shortcode_deps = self.site.apply_shortcodes_uuid(output, shortcodes, filename=source_path, extra_context={'post': post}) + return output, error_level, deps, shortcode_deps + + def compile(self, source, dest, is_two_file=True, post=None, lang=None): + """Compile the source file into HTML and save as dest.""" 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: + with io.open(dest, "w+", encoding="utf-8") as out_file: + with io.open(source, "r", encoding="utf-8-sig") as in_file: data = in_file.read() - output, error_level, deps = self.compile_html_string(data, source, is_two_file) + output, error_level, deps, shortcode_deps = self.compile_string(data, source, is_two_file, post, lang) out_file.write(output) - deps_path = dest + '.dep' - if deps.list: - deps.list = [p for p in deps.list if p != dest] # Don't depend on yourself (#1671) - with io.open(deps_path, "w+", encoding="utf8") as deps_file: - deps_file.write('\n'.join(deps.list)) + if post is None: + if deps.list: + self.logger.error( + "Cannot save dependencies for post {0} (post unknown)", + source) else: - if os.path.isfile(deps_path): - os.unlink(deps_path) + post._depfile[dest] += deps.list + post._depfile[dest] += shortcode_deps if error_level < 3: return True else: @@ -124,23 +175,21 @@ class CompileRest(PageCompiler): makedirs(os.path.dirname(path)) if not content.endswith('\n'): content += '\n' - with io.open(path, "w+", encoding="utf8") as fd: + with io.open(path, "w+", encoding="utf-8") as fd: if onefile: - fd.write(write_metadata(metadata)) - fd.write('\n') + fd.write(write_metadata(metadata, comment_wrap=False, site=self.site, compiler=self)) fd.write(content) def set_site(self, site): """Set Nikola site.""" - super(CompileRest, self).set_site(site) + super().set_site(site) self.config_dependencies = [] for plugin_info in self.get_compiler_extensions(): self.config_dependencies.append(plugin_info.name) plugin_info.plugin_object.short_help = plugin_info.description - self.logger = get_logger('compile_rest', STDERR_HANDLER) if not site.debug: - self.logger.level = 4 + self.logger.level = logging.WARNING def get_observer(settings): @@ -150,19 +199,25 @@ def get_observer(settings): 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 | - +------+---------+------+----------+ + +----------+----------+ + | docutils | logging | + +----------+----------+ + | DEBUG | DEBUG | + | INFO | INFO | + | WARNING | WARNING | + | ERROR | ERROR | + | SEVERE | CRITICAL | + +----------+----------+ """ - errormap = {0: 1, 1: 2, 2: 4, 3: 5, 4: 6} + errormap = { + docutils.utils.Reporter.DEBUG_LEVEL: logging.DEBUG, + docutils.utils.Reporter.INFO_LEVEL: logging.INFO, + docutils.utils.Reporter.WARNING_LEVEL: logging.WARNING, + docutils.utils.Reporter.ERROR_LEVEL: logging.ERROR, + docutils.utils.Reporter.SEVERE_LEVEL: logging.CRITICAL + } text = docutils.nodes.Element.astext(msg) - line = msg['line'] + settings['add_ln'] if 'line' in msg else 0 + line = msg['line'] + settings['add_ln'] if 'line' in msg else '' out = '[{source}{colon}{line}] {text}'.format( source=settings['source'], colon=(':' if line else ''), line=line, text=text) @@ -172,12 +227,14 @@ def get_observer(settings): class NikolaReader(docutils.readers.standalone.Reader): - """Nikola-specific docutils reader.""" + config_section = 'nikola' + def __init__(self, *args, **kwargs): """Initialize the reader.""" self.transforms = kwargs.pop('transforms', []) + self.logging_settings = kwargs.pop('nikola_logging_settings', {}) docutils.readers.standalone.Reader.__init__(self, *args, **kwargs) def get_transforms(self): @@ -188,15 +245,26 @@ class NikolaReader(docutils.readers.standalone.Reader): """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)) + document.reporter.attach_observer(get_observer(self.logging_settings)) return document +def shortcode_role(name, rawtext, text, lineno, inliner, + options={}, content=[]): + """Return a shortcode role that passes through raw inline HTML.""" + return [docutils.nodes.raw('', text, format='html')], [] + + +roles.register_canonical_role('raw-html', shortcode_role) +roles.register_canonical_role('html', shortcode_role) +roles.register_canonical_role('sc', shortcode_role) + + 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>`_. + `Sphinx add_node function <http://sphinx-doc.org/extdev/appapi.html#sphinx.application.Sphinx.add_node>`_. For example:: @@ -208,7 +276,7 @@ def add_node(node, visit_function=None, depart_function=None): self.site = site directives.register_directive('math', MathDirective) add_node(MathBlock, visit_Math, depart_Math) - return super(Plugin, self).set_site(site) + return super().set_site(site) class MathDirective(Directive): def run(self): @@ -227,16 +295,52 @@ def add_node(node, visit_function=None, depart_function=None): """ docutils.nodes._add_node_class_names([node.__name__]) if visit_function: - setattr(docutils.writers.html4css1.HTMLTranslator, 'visit_' + node.__name__, visit_function) + setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'visit_' + node.__name__, visit_function) if depart_function: - setattr(docutils.writers.html4css1.HTMLTranslator, 'depart_' + node.__name__, depart_function) + setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'depart_' + node.__name__, depart_function) + + +# Output <code> for ``double backticks``. (Code and extra logic based on html4css1 translator) +def visit_literal(self, node): + """Output <code> for double backticks.""" + # special case: "code" role + classes = node.get('classes', []) + if 'code' in classes: + # filter 'code' from class arguments + node['classes'] = [cls for cls in classes if cls != 'code'] + self.body.append(self.starttag(node, 'code', '')) + return + self.body.append( + self.starttag(node, 'code', '', CLASS='docutils literal')) + text = node.astext() + for token in self.words_and_spaces.findall(text): + if token.strip(): + # Protect text like "--an-option" and the regular expression + # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping + if self.in_word_wrap_point.search(token): + self.body.append('<span class="pre">%s</span>' + % self.encode(token)) + else: + self.body.append(self.encode(token)) + elif token in ('\n', ' '): + # Allow breaks at whitespace: + self.body.append(token) + else: + # Protect runs of multiple spaces; the last space can wrap: + self.body.append(' ' * (len(token) - 1) + ' ') + self.body.append('</code>') + # Content already processed: + raise docutils.nodes.SkipNode + + +setattr(docutils.writers.html5_polyglot.HTMLTranslator, 'visit_literal', visit_literal) 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='html', settings=None, settings_spec=None, - settings_overrides=None, config_section=None, + writer_name='html5_polyglot', settings=None, settings_spec=None, + settings_overrides=None, config_section='nikola', enable_exit_status=None, logger=None, l_add_ln=0, transforms=None): """Set up & run a ``Publisher``, and return a dictionary of document parts. @@ -249,20 +353,22 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput, publish_parts(..., settings_overrides={'input_encoding': 'unicode'}) - Parameters: see `publish_programmatically`. + For a description of the 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(transforms=transforms) # 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} + # add_ln amount of metadata lines (see comment in CompileRest.compile above) + reader = NikolaReader(transforms=transforms, + nikola_logging_settings={ + 'logger': logger, 'source': source_path, + 'add_ln': l_add_ln + }) pub = docutils.core.Publisher(reader, parser, writer, settings=settings, source_class=source_class, @@ -275,4 +381,23 @@ def rst2html(source, source_path=None, source_class=docutils.io.StringInput, pub.set_destination(None, destination_path) pub.publish(enable_exit_status=enable_exit_status) - return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies + return pub.writer.parts['docinfo'] + pub.writer.parts['fragment'], pub.document.reporter.max_level, pub.settings.record_dependencies, pub.document + + +# Alignment helpers for extensions +_align_options_base = ('left', 'center', 'right') + + +def _align_choice(argument): + return docutils.parsers.rst.directives.choice(argument, _align_options_base + ("none", "")) + + +class RemoveDocinfo(docutils.transforms.Transform): + """Remove docinfo nodes.""" + + default_priority = 870 + + def apply(self): + """Remove docinfo nodes.""" + for node in self.document.traverse(docutils.nodes.docinfo): + node.parent.remove(node) diff --git a/nikola/plugins/compile/rest/chart.plugin b/nikola/plugins/compile/rest/chart.plugin index 438abe4..4434477 100644 --- a/nikola/plugins/compile/rest/chart.plugin +++ b/nikola/plugins/compile/rest/chart.plugin @@ -4,11 +4,11 @@ module = chart [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Chart directive based in PyGal diff --git a/nikola/plugins/compile/rest/chart.py b/nikola/plugins/compile/rest/chart.py index 88fdff3..17363cb 100644 --- a/nikola/plugins/compile/rest/chart.py +++ b/nikola/plugins/compile/rest/chart.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -23,27 +23,22 @@ # 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. - """Chart directive for reSTructuredText.""" -from ast import literal_eval - from docutils import nodes from docutils.parsers.rst import Directive, directives +from nikola.plugin_categories import RestExtension + try: import pygal except ImportError: - pygal = None # NOQA - -from nikola.plugin_categories import RestExtension -from nikola.utils import req_missing + pygal = None _site = None class Plugin(RestExtension): - """Plugin for chart role.""" name = "rest_chart" @@ -53,11 +48,10 @@ class Plugin(RestExtension): global _site _site = self.site = site directives.register_directive('chart', Chart) - return super(Plugin, self).set_site(site) + return super().set_site(site) class Chart(Directive): - """reStructuredText extension for inserting charts as SVG. Usage: @@ -74,52 +68,69 @@ class Chart(Directive): has_content = True required_arguments = 1 option_spec = { - "copy": directives.unchanged, + "box_mode": directives.unchanged, + "classes": directives.unchanged, "css": directives.unchanged, + "defs": directives.unchanged, + "data_file": directives.unchanged, "disable_xml_declaration": directives.unchanged, "dots_size": directives.unchanged, + "dynamic_print_values": directives.unchanged, "explicit_size": directives.unchanged, "fill": directives.unchanged, - "font_sizes": directives.unchanged, + "force_uri_protocol": directives.unchanged, + "half_pie": directives.unchanged, "height": directives.unchanged, "human_readable": directives.unchanged, "include_x_axis": directives.unchanged, + "inner_radius": directives.unchanged, "interpolate": directives.unchanged, "interpolation_parameters": directives.unchanged, "interpolation_precision": directives.unchanged, + "inverse_y_axis": directives.unchanged, "js": directives.unchanged, - "label_font_size": directives.unchanged, "legend_at_bottom": directives.unchanged, + "legend_at_bottom_columns": directives.unchanged, "legend_box_size": directives.unchanged, - "legend_font_size": directives.unchanged, "logarithmic": directives.unchanged, - "major_label_font_size": directives.unchanged, "margin": directives.unchanged, - "no_data_font_size": directives.unchanged, + "margin_bottom": directives.unchanged, + "margin_left": directives.unchanged, + "margin_right": directives.unchanged, + "margin_top": directives.unchanged, + "max_scale": directives.unchanged, + "min_scale": directives.unchanged, + "missing_value_fill_truncation": directives.unchanged, "no_data_text": directives.unchanged, "no_prefix": directives.unchanged, "order_min": directives.unchanged, "pretty_print": directives.unchanged, + "print_labels": directives.unchanged, "print_values": directives.unchanged, + "print_values_position": directives.unchanged, "print_zeroes": directives.unchanged, "range": directives.unchanged, "rounded_bars": directives.unchanged, + "secondary_range": directives.unchanged, "show_dots": directives.unchanged, "show_legend": directives.unchanged, "show_minor_x_labels": directives.unchanged, + "show_minor_y_labels": directives.unchanged, + "show_only_major_dots": directives.unchanged, + "show_x_guides": directives.unchanged, + "show_x_labels": directives.unchanged, + "show_y_guides": directives.unchanged, "show_y_labels": directives.unchanged, "spacing": directives.unchanged, + "stack_from_top": directives.unchanged, "strict": directives.unchanged, "stroke": directives.unchanged, + "stroke_style": directives.unchanged, "style": directives.unchanged, "title": directives.unchanged, - "title_font_size": directives.unchanged, - "to_dict": directives.unchanged, "tooltip_border_radius": directives.unchanged, - "tooltip_font_size": directives.unchanged, "truncate_label": directives.unchanged, "truncate_legend": directives.unchanged, - "value_font_size": directives.unchanged, "value_formatter": directives.unchanged, "width": directives.unchanged, "x_label_rotation": directives.unchanged, @@ -128,37 +139,23 @@ class Chart(Directive): "x_labels_major_count": directives.unchanged, "x_labels_major_every": directives.unchanged, "x_title": directives.unchanged, + "x_value_formatter": directives.unchanged, + "xrange": directives.unchanged, "y_label_rotation": directives.unchanged, "y_labels": directives.unchanged, + "y_labels_major": directives.unchanged, + "y_labels_major_count": directives.unchanged, + "y_labels_major_every": directives.unchanged, "y_title": directives.unchanged, "zero": directives.unchanged, } def run(self): """Run the directive.""" - if pygal is None: - msg = req_missing(['pygal'], 'use the Chart directive', optional=True) - return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')] - options = {} - if 'style' in self.options: - style_name = self.options.pop('style') - else: - style_name = 'BlueStyle' - if '(' in style_name: # Parametric style - style = eval('pygal.style.' + style_name) - else: - style = getattr(pygal.style, style_name) - for k, v in self.options.items(): - options[k] = literal_eval(v) - - chart = getattr(pygal, self.arguments[0])(style=style) - chart.config(**options) - for line in self.content: - label, series = literal_eval('({0})'.format(line)) - chart.add(label, series) - data = chart.render().decode('utf8') - if _site and _site.invariant: - import re - data = re.sub('id="chart-[a-f0-9\-]+"', 'id="chart-foobar"', data) - data = re.sub('#chart-[a-f0-9\-]+', '#chart-foobar', data) - return [nodes.raw('', data, format='html')] + self.options['site'] = None + html = _site.plugin_manager.getPluginByName( + 'chart', 'ShortcodePlugin').plugin_object.handler( + self.arguments[0], + data='\n'.join(self.content), + **self.options) + return [nodes.raw('', html, format='html')] diff --git a/nikola/plugins/compile/rest/doc.plugin b/nikola/plugins/compile/rest/doc.plugin index facdd03..3b5c9c7 100644 --- a/nikola/plugins/compile/rest/doc.plugin +++ b/nikola/plugins/compile/rest/doc.plugin @@ -4,11 +4,11 @@ module = doc [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Manuel Kaufmann version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Role to link another page / post from the blog diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py index 99cce81..705c0bc 100644 --- a/nikola/plugins/compile/rest/doc.py +++ b/nikola/plugins/compile/rest/doc.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -29,12 +29,11 @@ from docutils import nodes from docutils.parsers.rst import roles -from nikola.utils import split_explicit_title +from nikola.utils import split_explicit_title, LOGGER, slugify from nikola.plugin_categories import RestExtension class Plugin(RestExtension): - """Plugin for doc role.""" name = 'rest_doc' @@ -43,16 +42,13 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site roles.register_canonical_role('doc', doc_role) + self.site.register_shortcode('doc', doc_shortcode) doc_role.site = site - return super(Plugin, self).set_site(site) + return super().set_site(site) -def doc_role(name, rawtext, text, lineno, inliner, - options={}, content=[]): - """Handle the doc role.""" - # split link's text and post's slug in role content - has_explicit_title, title, slug = split_explicit_title(text) - # check if the slug given is part of our blog posts/pages +def _find_post(slug): + """Find a post with the given slug in posts or pages.""" twin_slugs = False post = None for p in doc_role.site.timeline: @@ -62,27 +58,72 @@ def doc_role(name, rawtext, text, lineno, inliner, else: twin_slugs = True break + return post, twin_slugs + + +def _doc_link(rawtext, text, options={}, content=[]): + """Handle the doc role.""" + # split link's text and post's slug in role content + has_explicit_title, title, slug = split_explicit_title(text) + if '#' in slug: + slug, fragment = slug.split('#', 1) + else: + fragment = None + + # Look for the unslugified input first, then try to slugify (Issue #3450) + post, twin_slugs = _find_post(slug) + if post is None: + slug = slugify(slug) + post, twin_slugs = _find_post(slug) try: if post is None: - raise ValueError + raise ValueError("No post with matching slug found.") except ValueError: + return False, False, None, None, slug + + if not has_explicit_title: + # use post's title as link's text + title = post.title() + permalink = post.permalink() + if fragment: + permalink += '#' + fragment + + return True, twin_slugs, title, permalink, slug + + +def doc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """Handle the doc role.""" + success, twin_slugs, title, permalink, slug = _doc_link(rawtext, text, options, content) + if success: + if twin_slugs: + inliner.reporter.warning( + 'More than one post with the same slug. Using "{0}"'.format(permalink)) + LOGGER.warning( + 'More than one post with the same slug. Using "{0}" for doc role'.format(permalink)) + node = make_link_node(rawtext, title, permalink, options) + return [node], [] + else: msg = inliner.reporter.error( '"{0}" slug doesn\'t exist.'.format(slug), line=lineno) prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] - if not has_explicit_title: - # use post's title as link's text - title = post.title() - permalink = post.permalink() - if twin_slugs: - msg = inliner.reporter.warning( - 'More than one post with the same slug. Using "{0}"'.format(permalink)) - node = make_link_node(rawtext, title, permalink, options) - return [node], [] +def doc_shortcode(*args, **kwargs): + """Implement the doc shortcode.""" + text = kwargs['data'] + success, twin_slugs, title, permalink, slug = _doc_link(text, text, LOGGER) + if success: + if twin_slugs: + LOGGER.warning( + 'More than one post with the same slug. Using "{0}" for doc shortcode'.format(permalink)) + return '<a href="{0}">{1}</a>'.format(permalink, title) + else: + LOGGER.error( + '"{0}" slug doesn\'t exist.'.format(slug)) + return '<span class="error text-error" style="color: red;">Invalid link: {0}</span>'.format(text) def make_link_node(rawtext, text, url, options): diff --git a/nikola/plugins/compile/rest/gist.plugin b/nikola/plugins/compile/rest/gist.plugin index 9fa2e82..4a8a3a7 100644 --- a/nikola/plugins/compile/rest/gist.plugin +++ b/nikola/plugins/compile/rest/gist.plugin @@ -4,11 +4,11 @@ module = gist [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Gist directive diff --git a/nikola/plugins/compile/rest/gist.py b/nikola/plugins/compile/rest/gist.py index 736ee37..08aa46d 100644 --- a/nikola/plugins/compile/rest/gist.py +++ b/nikola/plugins/compile/rest/gist.py @@ -11,7 +11,6 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): - """Plugin for gist directive.""" name = "rest_gist" @@ -20,11 +19,10 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('gist', GitHubGist) - return super(Plugin, self).set_site(site) + return super().set_site(site) class GitHubGist(Directive): - """Embed GitHub Gist. Usage: diff --git a/nikola/plugins/compile/rest/listing.plugin b/nikola/plugins/compile/rest/listing.plugin index 85c780f..5239f92 100644 --- a/nikola/plugins/compile/rest/listing.plugin +++ b/nikola/plugins/compile/rest/listing.plugin @@ -4,11 +4,11 @@ module = listing [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Extension for source listings diff --git a/nikola/plugins/compile/rest/listing.py b/nikola/plugins/compile/rest/listing.py index 4871bf3..e5a73fa 100644 --- a/nikola/plugins/compile/rest/listing.py +++ b/nikola/plugins/compile/rest/listing.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -28,26 +28,21 @@ """Define and register a listing directive using the existing CodeBlock.""" -from __future__ import unicode_literals import io import os import uuid -try: - from urlparse import urlunsplit -except ImportError: - from urllib.parse import urlunsplit # NOQA +from urllib.parse import urlunsplit import docutils.parsers.rst.directives.body import docutils.parsers.rst.directives.misc +import pygments +import pygments.util from docutils import core from docutils import nodes from docutils.parsers.rst import Directive, directives from docutils.parsers.rst.roles import set_classes from docutils.parsers.rst.directives.misc import Include - from pygments.lexers import get_lexer_by_name -import pygments -import pygments.util from nikola import utils from nikola.plugin_categories import RestExtension @@ -55,7 +50,6 @@ from nikola.plugin_categories import RestExtension # A sanitized version of docutils.parsers.rst.directives.body.CodeBlock. class CodeBlock(Directive): - """Parse and mark up content of a code block.""" optional_arguments = 1 @@ -120,13 +114,13 @@ class CodeBlock(Directive): return [node] + # Monkey-patch: replace insane docutils CodeBlock with our implementation. docutils.parsers.rst.directives.body.CodeBlock = CodeBlock docutils.parsers.rst.directives.misc.CodeBlock = CodeBlock class Plugin(RestExtension): - """Plugin for listing directive.""" name = "rest_listing" @@ -138,12 +132,13 @@ class Plugin(RestExtension): # leaving these to make the code directive work with # docutils < 0.9 CodeBlock.site = site + Listing.site = site directives.register_directive('code', CodeBlock) directives.register_directive('code-block', CodeBlock) directives.register_directive('sourcecode', CodeBlock) directives.register_directive('listing', Listing) Listing.folders = site.config['LISTINGS_FOLDERS'] - return super(Plugin, self).set_site(site) + return super().set_site(site) # Add sphinx compatibility option @@ -152,7 +147,6 @@ listing_spec['linenos'] = directives.unchanged class Listing(Include): - """Create a highlighted block of code from a file in listings/. Usage: @@ -171,7 +165,12 @@ class Listing(Include): """Run listing directive.""" _fname = self.arguments.pop(0) fname = _fname.replace('/', os.sep) - lang = self.arguments.pop(0) + try: + lang = self.arguments.pop(0) + self.options['code'] = lang + except IndexError: + self.options['literal'] = True + if len(self.folders) == 1: listings_folder = next(iter(self.folders.keys())) if fname.startswith(listings_folder): @@ -181,22 +180,27 @@ class Listing(Include): else: fpath = os.path.join(fname) # must be new syntax: specify folder name self.arguments.insert(0, fpath) - self.options['code'] = lang if 'linenos' in self.options: self.options['number-lines'] = self.options['linenos'] - with io.open(fpath, 'r+', encoding='utf8') as fileobject: + with io.open(fpath, 'r+', encoding='utf-8-sig') as fileobject: self.content = fileobject.read().splitlines() self.state.document.settings.record_dependencies.add(fpath) target = urlunsplit(("link", 'listing', fpath.replace('\\', '/'), '', '')) + src_target = urlunsplit(("link", 'listing_source', fpath.replace('\\', '/'), '', '')) + src_label = self.site.MESSAGES('Source') generated_nodes = ( - [core.publish_doctree('`{0} <{1}>`_'.format(_fname, target))[0]]) + [core.publish_doctree('`{0} <{1}>`_ `({2}) <{3}>`_' .format( + _fname, target, src_label, src_target))[0]]) generated_nodes += self.get_code_from_file(fileobject) return generated_nodes def get_code_from_file(self, data): """Create CodeBlock nodes from file object content.""" - return super(Listing, self).run() + return super().run() def assert_has_content(self): - """Listing has no content, override check from superclass.""" + """Override check from superclass with nothing. + + Listing has no content, override check from superclass. + """ pass diff --git a/nikola/plugins/compile/rest/media.plugin b/nikola/plugins/compile/rest/media.plugin index 9803c8f..396c2f9 100644 --- a/nikola/plugins/compile/rest/media.plugin +++ b/nikola/plugins/compile/rest/media.plugin @@ -4,11 +4,11 @@ module = media [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Directive to support oembed via micawber diff --git a/nikola/plugins/compile/rest/media.py b/nikola/plugins/compile/rest/media.py index 345e331..d29d0a2 100644 --- a/nikola/plugins/compile/rest/media.py +++ b/nikola/plugins/compile/rest/media.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -29,18 +29,16 @@ from docutils import nodes from docutils.parsers.rst import Directive, directives +from nikola.plugin_categories import RestExtension +from nikola.utils import req_missing + try: import micawber except ImportError: - micawber = None # NOQA - - -from nikola.plugin_categories import RestExtension -from nikola.utils import req_missing + micawber = None class Plugin(RestExtension): - """Plugin for reST media directive.""" name = "rest_media" @@ -49,11 +47,11 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('media', Media) - return super(Plugin, self).set_site(site) + self.site.register_shortcode('media', _gen_media_embed) + return super().set_site(site) class Media(Directive): - """reST extension for inserting any sort of media using micawber.""" has_content = False @@ -62,9 +60,13 @@ class Media(Directive): def run(self): """Run media directive.""" - if micawber is None: - msg = req_missing(['micawber'], 'use the media directive', optional=True) - return [nodes.raw('', '<div class="text-error">{0}</div>'.format(msg), format='html')] + html = _gen_media_embed(" ".join(self.arguments)) + return [nodes.raw('', html, format='html')] + - providers = micawber.bootstrap_basic() - return [nodes.raw('', micawber.parse_text(" ".join(self.arguments), providers), format='html')] +def _gen_media_embed(url, *q, **kw): + if micawber is None: + msg = req_missing(['micawber'], 'use the media directive', optional=True) + return '<div class="text-error">{0}</div>'.format(msg) + providers = micawber.bootstrap_basic() + return micawber.parse_text(url, providers) diff --git a/nikola/plugins/compile/rest/post_list.plugin b/nikola/plugins/compile/rest/post_list.plugin index 48969bf..68abaef 100644 --- a/nikola/plugins/compile/rest/post_list.plugin +++ b/nikola/plugins/compile/rest/post_list.plugin @@ -4,11 +4,11 @@ module = post_list [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Udo Spallek -version = 0.1 -website = http://getnikola.com -description = Includes a list of posts with tag and slide based filters. +version = 0.2 +website = https://getnikola.com/ +description = Includes a list of posts with tag and slice based filters. diff --git a/nikola/plugins/compile/rest/post_list.py b/nikola/plugins/compile/rest/post_list.py index a22ee85..f7e95ed 100644 --- a/nikola/plugins/compile/rest/post_list.py +++ b/nikola/plugins/compile/rest/post_list.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2013-2015 Udo Spallek, Roberto Alsina and others. +# Copyright © 2013-2020 Udo Spallek, Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -23,15 +23,8 @@ # 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. - """Post list directive for reStructuredText.""" -from __future__ import unicode_literals - -import os -import uuid -import natsort - from docutils import nodes from docutils.parsers.rst import Directive, directives @@ -43,7 +36,6 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): - """Plugin for reST post-list directive.""" name = "rest_post_list" @@ -51,74 +43,14 @@ class Plugin(RestExtension): def set_site(self, site): """Set Nikola site.""" self.site = site - directives.register_directive('post-list', PostList) - PostList.site = site - return super(Plugin, self).set_site(site) - - -class PostList(Directive): - - """Provide a reStructuredText directive to create a list of posts. - - Post List - ========= - :Directive Arguments: None. - :Directive Options: lang, start, stop, reverse, sort, tags, categories, slugs, all, template, id - :Directive Content: None. - - The posts appearing in the list can be filtered by options. - *List slicing* is provided with the *start*, *stop* and *reverse* options. - - The following not required options are recognized: - - ``start`` : integer - The index of the first post to show. - A negative value like ``-3`` will show the *last* three posts in the - post-list. - Defaults to None. - - ``stop`` : integer - The index of the last post to show. - A value negative value like ``-1`` will show every post, but not the - *last* in the post-list. - Defaults to None. - - ``reverse`` : flag - Reverse the order of the post-list. - Defaults is to not reverse the order of posts. - - ``sort``: string - Sort post list by one of each post's attributes, usually ``title`` or a - custom ``priority``. Defaults to None (chronological sorting). - - ``tags`` : string [, string...] - Filter posts to show only posts having at least one of the ``tags``. - Defaults to None. - - ``categories`` : string [, string...] - Filter posts to show only posts having one of the ``categories``. - Defaults to None. + directives.register_directive('post-list', PostListDirective) + directives.register_directive('post_list', PostListDirective) + PostListDirective.site = site + return super().set_site(site) - ``slugs`` : string [, string...] - Filter posts to show only posts having at least one of the ``slugs``. - Defaults to None. - ``all`` : flag - Shows all posts and pages in the post list. - Defaults to show only posts with set *use_in_feeds*. - - ``lang`` : string - The language of post *titles* and *links*. - Defaults to default language. - - ``template`` : string - The name of an alternative template to render the post-list. - Defaults to ``post_list_directive.tmpl`` - - ``id`` : string - A manual id for the post list. - Defaults to a random name composed by 'post_list_' + uuid.uuid4().hex. - """ +class PostListDirective(Directive): + """Provide a reStructuredText directive to create a list of posts.""" option_spec = { 'start': int, @@ -126,12 +58,16 @@ class PostList(Directive): 'reverse': directives.flag, 'sort': directives.unchanged, 'tags': directives.unchanged, + 'require_all_tags': directives.flag, 'categories': directives.unchanged, + 'sections': directives.unchanged, 'slugs': directives.unchanged, - 'all': directives.flag, + 'post_type': directives.unchanged, + 'type': directives.unchanged, 'lang': directives.unchanged, 'template': directives.path, 'id': directives.unchanged, + 'date': directives.unchanged, } def run(self): @@ -140,73 +76,42 @@ class PostList(Directive): stop = self.options.get('stop') reverse = self.options.get('reverse', False) tags = self.options.get('tags') - tags = [t.strip().lower() for t in tags.split(',')] if tags else [] + require_all_tags = 'require_all_tags' in self.options categories = self.options.get('categories') - categories = [c.strip().lower() for c in categories.split(',')] if categories else [] + sections = self.options.get('sections') slugs = self.options.get('slugs') - slugs = [s.strip() for s in slugs.split(',')] if slugs else [] - show_all = self.options.get('all', False) + post_type = self.options.get('post_type') + type = self.options.get('type', False) lang = self.options.get('lang', utils.LocaleBorg().current_lang) template = self.options.get('template', 'post_list_directive.tmpl') sort = self.options.get('sort') - if self.site.invariant: # for testing purposes - post_list_id = self.options.get('id', 'post_list_' + 'fixedvaluethatisnotauuid') - else: - post_list_id = self.options.get('id', 'post_list_' + uuid.uuid4().hex) - - filtered_timeline = [] - posts = [] - step = -1 if reverse is None else None - if show_all is None: - timeline = [p for p in self.site.timeline] + date = self.options.get('date') + filename = self.state.document.settings._nikola_source_path + + output, deps = self.site.plugin_manager.getPluginByName( + 'post_list', 'ShortcodePlugin').plugin_object.handler( + start, + stop, + reverse, + tags, + require_all_tags, + categories, + sections, + slugs, + post_type, + type, + lang, + template, + sort, + state=self.state, + site=self.site, + date=date, + filename=filename) + self.state.document.settings.record_dependencies.add( + "####MAGIC####TIMELINE") + for d in deps: + self.state.document.settings.record_dependencies.add(d) + if output: + return [nodes.raw('', output, format='html')] else: - timeline = [p for p in self.site.timeline if p.use_in_feeds] - - if categories: - timeline = [p for p in timeline if p.meta('category', lang=lang).lower() in categories] - - for post in timeline: - if tags: - cont = True - tags_lower = [t.lower() for t in post.tags] - for tag in tags: - if tag in tags_lower: - cont = False - - if cont: - continue - - filtered_timeline.append(post) - - if sort: - filtered_timeline = natsort.natsorted(filtered_timeline, key=lambda post: post.meta[lang][sort], alg=natsort.ns.F | natsort.ns.IC) - - for post in filtered_timeline[start:stop:step]: - if slugs: - cont = True - for slug in slugs: - if slug == post.meta('slug'): - cont = False - - if cont: - continue - - bp = post.translated_base_path(lang) - if os.path.exists(bp): - self.state.document.settings.record_dependencies.add(bp) - - posts += [post] - - if not posts: return [] - self.state.document.settings.record_dependencies.add("####MAGIC####TIMELINE") - - template_data = { - 'lang': lang, - 'posts': posts, - 'date_format': self.site.GLOBAL_CONTEXT.get('date_format'), - 'post_list_id': post_list_id, - } - output = self.site.template_system.render_template( - template, None, template_data) - return [nodes.raw('', output, format='html')] diff --git a/nikola/plugins/compile/rest/slides.plugin b/nikola/plugins/compile/rest/slides.plugin deleted file mode 100644 index 5c05b89..0000000 --- a/nikola/plugins/compile/rest/slides.plugin +++ /dev/null @@ -1,14 +0,0 @@ -[Core] -name = rest_slides -module = slides - -[Nikola] -compiler = rest -plugincategory = CompilerExtension - -[Documentation] -author = Roberto Alsina -version = 0.1 -website = http://getnikola.com -description = Slides directive - diff --git a/nikola/plugins/compile/rest/slides.py b/nikola/plugins/compile/rest/slides.py deleted file mode 100644 index 2522e55..0000000 --- a/nikola/plugins/compile/rest/slides.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- 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. - -"""Slides directive for reStructuredText.""" - -from __future__ import unicode_literals - -import uuid - -from docutils import nodes -from docutils.parsers.rst import Directive, directives - -from nikola.plugin_categories import RestExtension - - -class Plugin(RestExtension): - - """Plugin for reST slides directive.""" - - name = "rest_slides" - - def set_site(self, site): - """Set Nikola site.""" - self.site = site - directives.register_directive('slides', Slides) - Slides.site = site - return super(Plugin, self).set_site(site) - - -class Slides(Directive): - - """reST extension for inserting slideshows.""" - - has_content = True - - def run(self): - """Run the slides directive.""" - if len(self.content) == 0: # pragma: no cover - return - - if self.site.invariant: # for testing purposes - carousel_id = 'slides_' + 'fixedvaluethatisnotauuid' - else: - carousel_id = 'slides_' + uuid.uuid4().hex - - output = self.site.template_system.render_template( - 'slides.tmpl', - None, - { - 'slides_content': self.content, - 'carousel_id': carousel_id, - } - ) - return [nodes.raw('', output, format='html')] - - -directives.register_directive('slides', Slides) diff --git a/nikola/plugins/compile/rest/soundcloud.plugin b/nikola/plugins/compile/rest/soundcloud.plugin index 75469e4..f85a964 100644 --- a/nikola/plugins/compile/rest/soundcloud.plugin +++ b/nikola/plugins/compile/rest/soundcloud.plugin @@ -4,11 +4,11 @@ module = soundcloud [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Roberto Alsina version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = Soundcloud directive diff --git a/nikola/plugins/compile/rest/soundcloud.py b/nikola/plugins/compile/rest/soundcloud.py index 30134a9..5dbcfc3 100644 --- a/nikola/plugins/compile/rest/soundcloud.py +++ b/nikola/plugins/compile/rest/soundcloud.py @@ -1,16 +1,39 @@ # -*- coding: utf-8 -*- +# Copyright © 2012-2020 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. + """SoundCloud directive for reStructuredText.""" from docutils import nodes from docutils.parsers.rst import Directive, directives - +from nikola.plugins.compile.rest import _align_choice, _align_options_base from nikola.plugin_categories import RestExtension class Plugin(RestExtension): - """Plugin for soundclound directive.""" name = "rest_soundcloud" @@ -20,18 +43,19 @@ class Plugin(RestExtension): self.site = site directives.register_directive('soundcloud', SoundCloud) directives.register_directive('soundcloud_playlist', SoundCloudPlaylist) - return super(Plugin, self).set_site(site) + return super().set_site(site) -CODE = ("""<iframe width="{width}" height="{height}" +CODE = """\ +<div class="soundcloud-player{align}"> +<iframe width="{width}" height="{height}" scrolling="no" frameborder="no" -src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/{preslug}/""" - """{sid}"> -</iframe>""") +src="https://w.soundcloud.com/player/?url=http://api.soundcloud.com/{preslug}/{sid}"> +</iframe> +</div>""" class SoundCloud(Directive): - """reST extension for inserting SoundCloud embedded music. Usage: @@ -46,6 +70,7 @@ class SoundCloud(Directive): option_spec = { 'width': directives.positive_int, 'height': directives.positive_int, + "align": _align_choice } preslug = "tracks" @@ -59,6 +84,10 @@ class SoundCloud(Directive): 'preslug': self.preslug, } options.update(self.options) + if self.options.get('align') in _align_options_base: + options['align'] = ' align-' + self.options['align'] + else: + options['align'] = '' return [nodes.raw('', CODE.format(**options), format='html')] def check_content(self): @@ -70,7 +99,6 @@ class SoundCloud(Directive): class SoundCloudPlaylist(SoundCloud): - """reST directive for SoundCloud playlists.""" preslug = "playlists" diff --git a/nikola/plugins/compile/rest/thumbnail.plugin b/nikola/plugins/compile/rest/thumbnail.plugin index 0084310..e7b649d 100644 --- a/nikola/plugins/compile/rest/thumbnail.plugin +++ b/nikola/plugins/compile/rest/thumbnail.plugin @@ -4,11 +4,11 @@ module = thumbnail [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] author = Pelle Nilsson version = 0.1 -website = http://getnikola.com +website = https://getnikola.com/ description = reST directive to facilitate enlargeable images with thumbnails diff --git a/nikola/plugins/compile/rest/thumbnail.py b/nikola/plugins/compile/rest/thumbnail.py index 1fae06c..06ca9e4 100644 --- a/nikola/plugins/compile/rest/thumbnail.py +++ b/nikola/plugins/compile/rest/thumbnail.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2014-2015 Pelle Nilsson and others. +# Copyright © 2014-2020 Pelle Nilsson and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -35,7 +35,6 @@ from nikola.plugin_categories import RestExtension class Plugin(RestExtension): - """Plugin for thumbnail directive.""" name = "rest_thumbnail" @@ -44,11 +43,10 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('thumbnail', Thumbnail) - return super(Plugin, self).set_site(site) + return super().set_site(site) class Thumbnail(Figure): - """Thumbnail directive for reST.""" def align(argument): @@ -70,8 +68,12 @@ class Thumbnail(Figure): def run(self): """Run the thumbnail directive.""" uri = directives.uri(self.arguments[0]) + if uri.endswith('.svg'): + # the ? at the end makes docutil output an <img> instead of an object for the svg, which lightboxes may require + self.arguments[0] = '.thumbnail'.join(os.path.splitext(uri)) + '?' + else: + self.arguments[0] = '.thumbnail'.join(os.path.splitext(uri)) self.options['target'] = uri - self.arguments[0] = '.thumbnail'.join(os.path.splitext(uri)) if self.content: (node,) = Figure.run(self) else: diff --git a/nikola/plugins/compile/rest/vimeo.plugin b/nikola/plugins/compile/rest/vimeo.plugin index 688f981..89b171b 100644 --- a/nikola/plugins/compile/rest/vimeo.plugin +++ b/nikola/plugins/compile/rest/vimeo.plugin @@ -4,7 +4,7 @@ module = vimeo [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] description = Vimeo directive diff --git a/nikola/plugins/compile/rest/vimeo.py b/nikola/plugins/compile/rest/vimeo.py index c694a87..7047b03 100644 --- a/nikola/plugins/compile/rest/vimeo.py +++ b/nikola/plugins/compile/rest/vimeo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -26,18 +26,17 @@ """Vimeo directive for reStructuredText.""" -from docutils import nodes -from docutils.parsers.rst import Directive, directives - -import requests import json +import requests +from docutils import nodes +from docutils.parsers.rst import Directive, directives from nikola.plugin_categories import RestExtension +from nikola.plugins.compile.rest import _align_choice, _align_options_base class Plugin(RestExtension): - """Plugin for vimeo reST directive.""" name = "rest_vimeo" @@ -46,13 +45,15 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('vimeo', Vimeo) - return super(Plugin, self).set_site(site) + return super().set_site(site) -CODE = """<iframe src="//player.vimeo.com/video/{vimeo_id}" +CODE = """<div class="vimeo-video{align}"> +<iframe src="https://player.vimeo.com/video/{vimeo_id}" width="{width}" height="{height}" frameborder="0" webkitAllowFullScreen="webkitAllowFullScreen" mozallowfullscreen="mozallowfullscreen" allowFullScreen="allowFullScreen"> </iframe> +</div> """ VIDEO_DEFAULT_HEIGHT = 500 @@ -60,7 +61,6 @@ VIDEO_DEFAULT_WIDTH = 281 class Vimeo(Directive): - """reST extension for inserting vimeo embedded videos. Usage: @@ -75,6 +75,7 @@ class Vimeo(Directive): option_spec = { "width": directives.positive_int, "height": directives.positive_int, + "align": _align_choice } # set to False for not querying the vimeo api for size @@ -94,6 +95,10 @@ class Vimeo(Directive): return err self.set_video_size() options.update(self.options) + if self.options.get('align') in _align_options_base: + options['align'] = ' align-' + self.options['align'] + else: + options['align'] = '' return [nodes.raw('', CODE.format(**options), format='html')] def check_modules(self): @@ -109,7 +114,7 @@ class Vimeo(Directive): if json: # we can attempt to retrieve video attributes from vimeo try: - url = ('//vimeo.com/api/v2/video/{0}' + url = ('https://vimeo.com/api/v2/video/{0}' '.json'.format(self.arguments[0])) data = requests.get(url).text video_attributes = json.loads(data)[0] diff --git a/nikola/plugins/compile/rest/youtube.plugin b/nikola/plugins/compile/rest/youtube.plugin index 5fbd67b..d83d0f8 100644 --- a/nikola/plugins/compile/rest/youtube.plugin +++ b/nikola/plugins/compile/rest/youtube.plugin @@ -4,7 +4,7 @@ module = youtube [Nikola] compiler = rest -plugincategory = CompilerExtension +PluginCategory = CompilerExtension [Documentation] version = 0.1 diff --git a/nikola/plugins/compile/rest/youtube.py b/nikola/plugins/compile/rest/youtube.py index 6c5c211..d52ec64 100644 --- a/nikola/plugins/compile/rest/youtube.py +++ b/nikola/plugins/compile/rest/youtube.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright © 2012-2015 Roberto Alsina and others. +# Copyright © 2012-2020 Roberto Alsina and others. # Permission is hereby granted, free of charge, to any # person obtaining a copy of this software and associated @@ -29,12 +29,11 @@ from docutils import nodes from docutils.parsers.rst import Directive, directives - +from nikola.plugins.compile.rest import _align_choice, _align_options_base from nikola.plugin_categories import RestExtension class Plugin(RestExtension): - """Plugin for the youtube directive.""" name = "rest_youtube" @@ -43,18 +42,19 @@ class Plugin(RestExtension): """Set Nikola site.""" self.site = site directives.register_directive('youtube', Youtube) - return super(Plugin, self).set_site(site) + return super().set_site(site) CODE = """\ -<iframe width="{width}" -height="{height}" -src="//www.youtube.com/embed/{yid}?rel=0&hd=1&wmode=transparent" -></iframe>""" +<div class="youtube-video{align}"> +<iframe width="{width}" height="{height}" +src="https://www.youtube-nocookie.com/embed/{yid}?rel=0&wmode=transparent" +frameborder="0" allow="encrypted-media" allowfullscreen +></iframe> +</div>""" class Youtube(Directive): - """reST extension for inserting youtube embedded videos. Usage: @@ -67,8 +67,9 @@ class Youtube(Directive): has_content = True required_arguments = 1 option_spec = { - "width": directives.positive_int, - "height": directives.positive_int, + "width": directives.unchanged, + "height": directives.unchanged, + "align": _align_choice } def run(self): @@ -76,10 +77,14 @@ class Youtube(Directive): self.check_content() options = { 'yid': self.arguments[0], - 'width': 425, - 'height': 344, + 'width': 560, + 'height': 315, } - options.update(self.options) + options.update({k: v for k, v in self.options.items() if v}) + if self.options.get('align') in _align_options_base: + options['align'] = ' align-' + self.options['align'] + else: + options['align'] = '' return [nodes.raw('', CODE.format(**options), format='html')] def check_content(self): |
