diff options
Diffstat (limited to 'nikola/plugins/task/listings.py')
| -rw-r--r-- | nikola/plugins/task/listings.py | 121 |
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) |
