aboutsummaryrefslogtreecommitdiffstats
path: root/nikola/plugins/task/listings.py
diff options
context:
space:
mode:
Diffstat (limited to 'nikola/plugins/task/listings.py')
-rw-r--r--nikola/plugins/task/listings.py121
1 files changed, 77 insertions, 44 deletions
diff --git a/nikola/plugins/task/listings.py b/nikola/plugins/task/listings.py
index 5f79724..c946313 100644
--- a/nikola/plugins/task/listings.py
+++ b/nikola/plugins/task/listings.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,37 +26,32 @@
"""Render code listings."""
-from __future__ import unicode_literals, print_function
-
-import sys
import os
-import lxml.html
+from collections import defaultdict
-from pygments import highlight
-from pygments.lexers import get_lexer_for_filename, TextLexer
import natsort
+from pygments import highlight
+from pygments.lexers import get_lexer_for_filename, guess_lexer, TextLexer
from nikola.plugin_categories import Task
from nikola import utils
class Listings(Task):
-
"""Render code listings."""
name = "render_listings"
def register_output_name(self, input_folder, rel_name, rel_output_name):
"""Register proper and improper file mappings."""
- if rel_name not in self.improper_input_file_mapping:
- self.improper_input_file_mapping[rel_name] = []
- self.improper_input_file_mapping[rel_name].append(rel_output_name)
+ self.improper_input_file_mapping[rel_name].add(rel_output_name)
self.proper_input_file_mapping[os.path.join(input_folder, rel_name)] = rel_output_name
self.proper_input_file_mapping[rel_output_name] = rel_output_name
def set_site(self, site):
"""Set Nikola site."""
site.register_path_handler('listing', self.listing_path)
+ site.register_path_handler('listing_source', self.listing_source_path)
# We need to prepare some things for the listings path handler to work.
@@ -75,7 +70,7 @@ class Listings(Task):
if source in appearing_paths or dest in appearing_paths:
problem = source if source in appearing_paths else dest
utils.LOGGER.error("The listings input or output folder '{0}' appears in more than one entry in LISTINGS_FOLDERS, exiting.".format(problem))
- sys.exit(1)
+ continue
appearing_paths.add(source)
appearing_paths.add(dest)
@@ -85,7 +80,7 @@ class Listings(Task):
# a list is needed. This is needed for compatibility to previous Nikola
# versions, where there was no need to specify the input directory name
# when asking for a link via site.link('listing', ...).
- self.improper_input_file_mapping = {}
+ self.improper_input_file_mapping = defaultdict(set)
# proper_input_file_mapping maps relative input file (relative to CWD)
# to a generated output file. Since we don't allow an input directory
@@ -94,7 +89,7 @@ class Listings(Task):
self.proper_input_file_mapping = {}
for input_folder, output_folder in self.kw['listings_folders'].items():
- for root, dirs, files in os.walk(input_folder, followlinks=True):
+ for root, _, files in os.walk(input_folder, followlinks=True):
# Compute relative path; can't use os.path.relpath() here as it returns "." instead of ""
rel_path = root[len(input_folder):]
if rel_path[:1] == os.sep:
@@ -106,7 +101,7 @@ class Listings(Task):
# Register file names in the mapping.
self.register_output_name(input_folder, rel_name, rel_output_name)
- return super(Listings, self).set_site(site)
+ return super().set_site(site)
def gen_tasks(self):
"""Render pretty code listings."""
@@ -117,20 +112,31 @@ class Listings(Task):
needs_ipython_css = False
if in_name and in_name.endswith('.ipynb'):
# Special handling: render ipynbs in listings (Issue #1900)
- ipynb_compiler = self.site.plugin_manager.getPluginByName("ipynb", "PageCompiler").plugin_object
- ipynb_raw = ipynb_compiler.compile_html_string(in_name, True)
- ipynb_html = lxml.html.fromstring(ipynb_raw)
- # The raw HTML contains garbage (scripts and styles), we can’t leave it in
- code = lxml.html.tostring(ipynb_html.xpath('//*[@id="notebook"]')[0], encoding='unicode')
+ ipynb_plugin = self.site.plugin_manager.getPluginByName("ipynb", "PageCompiler")
+ if ipynb_plugin is None:
+ msg = "To use .ipynb files as listings, you must set up the Jupyter compiler in COMPILERS and POSTS/PAGES."
+ utils.LOGGER.error(msg)
+ raise ValueError(msg)
+
+ ipynb_compiler = ipynb_plugin.plugin_object
+ with open(in_name, "r", encoding="utf-8-sig") as in_file:
+ nb_json = ipynb_compiler._nbformat_read(in_file)
+ code = ipynb_compiler._compile_string(nb_json)
title = os.path.basename(in_name)
needs_ipython_css = True
elif in_name:
- with open(in_name, 'r') as fd:
+ with open(in_name, 'r', encoding='utf-8-sig') as fd:
try:
lexer = get_lexer_for_filename(in_name)
- except:
- lexer = TextLexer()
- code = highlight(fd.read(), lexer, utils.NikolaPygmentsHTML(in_name))
+ except Exception:
+ try:
+ lexer = guess_lexer(fd.read())
+ except Exception:
+ lexer = TextLexer()
+ fd.seek(0)
+ code = highlight(
+ fd.read(), lexer,
+ utils.NikolaPygmentsHTML(in_name, linenos='table'))
title = os.path.basename(in_name)
else:
code = ''
@@ -147,7 +153,7 @@ class Listings(Task):
os.path.join(
self.kw['output_folder'],
output_folder))))
- if self.site.config['COPY_SOURCES'] and in_name:
+ if in_name:
source_link = permalink[:-5] # remove '.html'
else:
source_link = None
@@ -182,7 +188,7 @@ class Listings(Task):
uptodate = {'c': self.site.GLOBAL_CONTEXT}
for k, v in self.site.GLOBAL_CONTEXT['template_hooks'].items():
- uptodate['||template_hooks|{0}||'.format(k)] = v._items
+ uptodate['||template_hooks|{0}||'.format(k)] = v.calculate_deps()
for k in self.site._GLOBAL_CONTEXT_TRANSLATABLE:
uptodate[k] = self.site.GLOBAL_CONTEXT[k](self.kw['default_lang'])
@@ -218,6 +224,8 @@ class Listings(Task):
'clean': True,
}, self.kw["filters"])
for f in files:
+ if f == '.DS_Store':
+ continue
ext = os.path.splitext(f)[-1]
if ext in ignored_extensions:
continue
@@ -240,22 +248,47 @@ class Listings(Task):
'uptodate': [utils.config_changed(uptodate, 'nikola.plugins.task.listings:source')],
'clean': True,
}, self.kw["filters"])
- if self.site.config['COPY_SOURCES']:
- rel_name = os.path.join(rel_path, f)
- rel_output_name = os.path.join(output_folder, rel_path, f)
- self.register_output_name(input_folder, rel_name, rel_output_name)
- out_name = os.path.join(self.kw['output_folder'], rel_output_name)
- yield utils.apply_filters({
- 'basename': self.name,
- 'name': out_name,
- 'file_dep': [in_name],
- 'targets': [out_name],
- 'actions': [(utils.copy_file, [in_name, out_name])],
- 'clean': True,
- }, self.kw["filters"])
+
+ rel_name = os.path.join(rel_path, f)
+ rel_output_name = os.path.join(output_folder, rel_path, f)
+ self.register_output_name(input_folder, rel_name, rel_output_name)
+ out_name = os.path.join(self.kw['output_folder'], rel_output_name)
+ yield utils.apply_filters({
+ 'basename': self.name,
+ 'name': out_name,
+ 'file_dep': [in_name],
+ 'targets': [out_name],
+ 'actions': [(utils.copy_file, [in_name, out_name])],
+ 'clean': True,
+ }, self.kw["filters"])
+
+ def listing_source_path(self, name, lang):
+ """Return a link to the source code for a listing.
+
+ It will try to use the file name if it's not ambiguous, or the file path.
+
+ Example:
+
+ link://listing_source/hello.py => /listings/tutorial/hello.py
+
+ link://listing_source/tutorial/hello.py => /listings/tutorial/hello.py
+ """
+ result = self.listing_path(name, lang)
+ if result[-1].endswith('.html'):
+ result[-1] = result[-1][:-5]
+ return result
def listing_path(self, namep, lang):
- """Return path to a listing."""
+ """Return a link to a listing.
+
+ It will try to use the file name if it's not ambiguous, or the file path.
+
+ Example:
+
+ link://listing/hello.py => /listings/tutorial/hello.py.html
+
+ link://listing/tutorial/hello.py => /listings/tutorial/hello.py.html
+ """
namep = namep.replace('/', os.sep)
nameh = namep + '.html'
for name in (namep, nameh):
@@ -268,14 +301,14 @@ class Listings(Task):
# ambiguities.
if len(self.improper_input_file_mapping[name]) > 1:
utils.LOGGER.error("Using non-unique listing name '{0}', which maps to more than one listing name ({1})!".format(name, str(self.improper_input_file_mapping[name])))
- sys.exit(1)
+ return ["ERROR"]
if len(self.site.config['LISTINGS_FOLDERS']) > 1:
- utils.LOGGER.notice("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.")
- name = self.improper_input_file_mapping[name][0]
+ utils.LOGGER.warning("Using listings names in site.link() without input directory prefix while configuration's LISTINGS_FOLDERS has more than one entry.")
+ name = list(self.improper_input_file_mapping[name])[0]
break
else:
utils.LOGGER.error("Unknown listing name {0}!".format(namep))
- sys.exit(1)
+ return ["ERROR"]
if not name.endswith(os.sep + self.site.config["INDEX_FILE"]):
name += '.html'
path_parts = name.split(os.sep)