Repository: ipython/ipython

Issue:
Include profile dir custom.css in nbconvert HTML output
Suggested by @ksmith13 in #4902 , we should include `custom.css` from the user's profile directory in nbconvert HTML output, _at least_ for the print preview / download as in the live notebook.  It's not practical to have to copy `custom.css` into every folder in which you want print preview and download as to work.

This could be a quick fix, if we decide the behavior of nbconvert --to html should match that of download as and print preview; else, we may need to add a parameter to the nbconvert web API used by the notebook.



Discussion:

Resolving patch:
diff --git a/IPython/nbconvert/nbconvertapp.py b/IPython/nbconvert/nbconvertapp.py
--- a/IPython/nbconvert/nbconvertapp.py
+++ b/IPython/nbconvert/nbconvertapp.py
@@ -280,6 +280,7 @@ def convert_notebooks(self):
             if self.output_base:
                 notebook_name = self.output_base
             resources = {}
+            resources['profile_dir'] = self.profile_dir.location
             resources['unique_key'] = notebook_name
             resources['output_files_dir'] = '%s_files' % notebook_name
             self.log.info("Support files will be in %s", os.path.join(resources['output_files_dir'], ''))
diff --git a/IPython/nbconvert/preprocessors/csshtmlheader.py b/IPython/nbconvert/preprocessors/csshtmlheader.py
--- a/IPython/nbconvert/preprocessors/csshtmlheader.py
+++ b/IPython/nbconvert/preprocessors/csshtmlheader.py
@@ -1,58 +1,27 @@
 """Module that pre-processes the notebook for export to HTML.
 """
-#-----------------------------------------------------------------------------
-# Copyright (c) 2013, the IPython Development Team.
-#
+# Copyright (c) IPython Development Team.
 # Distributed under the terms of the Modified BSD License.
-#
-# The full license is in the file COPYING.txt, distributed with this software.
-#-----------------------------------------------------------------------------
-
-#-----------------------------------------------------------------------------
-# Imports
-#-----------------------------------------------------------------------------
-
 import os
 import io
+import hashlib
 
 from IPython.utils import path
-
-from .base import Preprocessor
-
 from IPython.utils.traitlets import Unicode
-
-#-----------------------------------------------------------------------------
-# Classes and functions
-#-----------------------------------------------------------------------------
+from IPython.utils.py3compat import str_to_bytes
+from .base import Preprocessor
 
 class CSSHTMLHeaderPreprocessor(Preprocessor):
     """
     Preprocessor used to pre-process notebook for HTML output.  Adds IPython notebook
     front-end CSS and Pygments CSS to HTML output.
     """
-
-    header = []
-
     highlight_class = Unicode('.highlight', config=True,
                               help="CSS highlight class identifier")
 
-    def __init__(self, config=None, **kw):
-        """
-        Public constructor
-        
-        Parameters
-        ----------
-        config : Config
-            Configuration file structure
-        **kw : misc
-            Additional arguments
-        """
-        
-        super(CSSHTMLHeaderPreprocessor, self).__init__(config=config, **kw)
-
-        if self.enabled :
-            self._regen_header()
-
+    def __init__(self, *pargs, **kwargs):
+        Preprocessor.__init__(self, *pargs, **kwargs)
+        self._default_css_hash = None
 
     def preprocess(self, nb, resources):
         """Fetch and add CSS to the resource dictionary
@@ -69,38 +38,48 @@ def preprocess(self, nb, resources):
             Additional resources used in the conversion process.  Allows
             preprocessors to pass variables into the Jinja engine.
         """
-        
         resources['inlining'] = {}
-        resources['inlining']['css'] = self.header
-        
+        resources['inlining']['css'] = self._generate_header(resources)
         return nb, resources
 
-
-    def _regen_header(self):
+    def _generate_header(self, resources):
         """ 
         Fills self.header with lines of CSS extracted from IPython 
         and Pygments.
         """
         from pygments.formatters import HtmlFormatter
-        
-        #Clear existing header.
         header = []
         
-        #Construct path to IPy CSS
+        # Construct path to IPy CSS
         from IPython.html import DEFAULT_STATIC_FILES_PATH
         sheet_filename = os.path.join(DEFAULT_STATIC_FILES_PATH,
             'style', 'style.min.css')
         
-        #Load style CSS file.
-        with io.open(sheet_filename, encoding='utf-8') as file:
-            file_text = file.read()
-            header.append(file_text)
+        # Load style CSS file.
+        with io.open(sheet_filename, encoding='utf-8') as f:
+            header.append(f.read())
 
-        #Add pygments CSS
+        # Add pygments CSS
         formatter = HtmlFormatter()
         pygments_css = formatter.get_style_defs(self.highlight_class)
         header.append(pygments_css)
 
-        #Set header        
-        self.header = header
-
+        # Load the user's custom CSS and IPython's default custom CSS.  If they
+        # differ, assume the user has made modifications to his/her custom CSS
+        # and that we should inline it in the nbconvert output.
+        profile_dir = resources['profile_dir']
+        custom_css_filename = os.path.join(profile_dir, 'static', 'custom', 'custom.css')
+        if os.path.isfile(custom_css_filename):
+            if self._default_css_hash is None:
+                self._default_css_hash = self._hash(os.path.join(DEFAULT_STATIC_FILES_PATH, 'custom', 'custom.css'))
+            if self._hash(custom_css_filename) != self._default_css_hash:
+                with io.open(custom_css_filename, encoding='utf-8') as f:
+                    header.append(f.read())
+        return header
+
+    def _hash(self, filename):
+        """Compute the hash of a file."""
+        md5 = hashlib.md5()
+        with open(filename) as f:
+            md5.update(str_to_bytes(f.read()))
+        return md5.digest()


Test patch: