diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "admin-lte": "^3.0.0-alpha", + "ansi_up": "^4.0.3", "bootstrap": "^4.3.1", "bootstrap-year-calendar-bs4": "^1.0.0", "clipboard": "^2.0.4", @@ -28,6 +29,7 @@ "iframe-resizer": "^4.1.1", "jquery": "^3.4.0", "js-cookie": "^2.2.0", + "notebookjs": "^0.4.2", "object-fit-images": "^3.2.4", "octicons": "^8.5.0", "open-iconic": "^1.1.1", @@ -80,6 +82,7 @@ "robotstxt-webpack-plugin": "^5.0.0", "sass-loader": "^7.1.0", "schema-utils": "^1.0.0", + "script-loader": "^0.7.2", "spdx-expression-parse": "^3.0.0", "style-loader": "^0.23.1", "stylelint": "^10.0.0", diff --git a/swh/web/assets/config/.eslintrc b/swh/web/assets/config/.eslintrc --- a/swh/web/assets/config/.eslintrc +++ b/swh/web/assets/config/.eslintrc @@ -40,7 +40,9 @@ "__STATIC__": false, "Image": false, "Cookies": false, - "grecaptcha": false + "grecaptcha": false, + "nb": false, + "MathJax": false }, "rules": { diff --git a/swh/web/assets/config/bootstrap-pre-customize.scss b/swh/web/assets/config/bootstrap-pre-customize.scss --- a/swh/web/assets/config/bootstrap-pre-customize.scss +++ b/swh/web/assets/config/bootstrap-pre-customize.scss @@ -9,14 +9,14 @@ // global text colors and fonts $body-color: rgba(0, 0, 0, 0.55); -$font-family-sans-serif: "Alegreya Sans", sans-serif; +$font-family-sans-serif: "Alegreya Sans", sans-serif !important; $link-color: rgba(0, 0, 0, 0.75); $code-color: #c7254e; // headings $headings-line-height: 1.1; $headings-color: #e20026; -$headings-font-family: "Alegreya Sans", sans-serif; +$headings-font-family: "Alegreya Sans", sans-serif !important; // remove the ugly box shadow from bootstrap 4.x $input-btn-focus-width: 0; diff --git a/swh/web/assets/config/webpack-plugins/generate-weblabels-webpack-plugin/index.js b/swh/web/assets/config/webpack-plugins/generate-weblabels-webpack-plugin/index.js --- a/swh/web/assets/config/webpack-plugins/generate-weblabels-webpack-plugin/index.js +++ b/swh/web/assets/config/webpack-plugins/generate-weblabels-webpack-plugin/index.js @@ -99,6 +99,12 @@ } } + // remove webpack loader call if any + let loaderEndPos = srcFilePath.indexOf('!'); + if (loaderEndPos !== -1) { + srcFilePath = srcFilePath.slice(loaderEndPos + 1); + } + // iterate on all chunks containing the module mod.chunks.forEach(chunk => { diff --git a/swh/web/assets/src/bundles/webapp/index.js b/swh/web/assets/src/bundles/webapp/index.js --- a/swh/web/assets/src/bundles/webapp/index.js +++ b/swh/web/assets/src/bundles/webapp/index.js @@ -21,3 +21,4 @@ export * from './code-highlighting'; export * from './readme-rendering'; export * from './pdf-rendering'; +export * from './notebook-rendering'; diff --git a/swh/web/assets/src/bundles/webapp/notebook-rendering.js b/swh/web/assets/src/bundles/webapp/notebook-rendering.js new file mode 100644 --- /dev/null +++ b/swh/web/assets/src/bundles/webapp/notebook-rendering.js @@ -0,0 +1,106 @@ +/** + * Copyright (C) 2019 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +import 'script-loader!notebookjs'; + +import AnsiUp from 'ansi_up'; +import './notebook.css'; + +const ansiup = new AnsiUp(); + +export async function renderNotebook(nbJsonUrl, domElt) { + + let showdown = await import(/* webpackChunkName: "showdown" */ 'utils/showdown'); + + await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); + + function renderMarkdown(text) { + let converter = new showdown.Converter({ + tables: true, + literalMidWordUnderscores: true + }); + // showdown will remove some LaTex escaping sequences when + // converting md to html. So we use the following hack to keep + // them in the html output and avoid MathJax typesetting errors + for (let specialLaTexChar of ['{', '}', '#', '%', '&', '_']) { + text = text.replace('\\' + specialLaTexChar, + '\\\\' + specialLaTexChar); + } + // line breaks in LaTex array need also special escaping + text = text.replace('\\\\', '\\\\\\\\'); + let rendered = converter.makeHtml(text); + return rendered; + } + + function highlightCode(text, preElt, codeElt, lang) { + if (lang) { + return hljs.highlight(lang, text).value; + } else { + return text; + } + } + + function renderAnsi(text) { + return ansiup.ansi_to_html(text); + } + + nb.markdown = renderMarkdown; + nb.highlighter = highlightCode; + nb.ansi = renderAnsi; + + function initMathJax() { + + // same config as in nbviewer + window.MathJax = { + TeX: { + equationNumbers: { + autoNumber: 'AMS', + useLabelIds: true + } + }, + tex2jax: { + inlineMath: [ ['$', '$'], ['\\(', '\\)'] ], + displayMath: [ ['$$', '$$'], ['\\[', '\\]'] ], + processEscapes: true, + processEnvironments: true + }, + displayAlign: 'center', + 'HTML-CSS': { + styles: {'.MathJax_Display': {'margin': 0}}, + linebreaks: { automatic: true } + } + }; + + // MathJax is not easily webpackable in its current version + // (https://github.com/mathjax/MathJax/issues/1629) + // and is quite a monster regarding the number of files to distribute. + // So we will load it through a CDN for commodity of use here. + let head = document.getElementsByTagName('head')[0]; + let script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-AMS_HTML'; + head.appendChild(script); + } + + fetch(nbJsonUrl) + .then(response => response.json()) + .then(nbJson => { + // load MathJax library for math typesetting + initMathJax(); + + // give some time to MathJax to load + setTimeout(() => { + // parse and render the notebook + let notebook = nb.parse(nbJson); + let rendered = notebook.render(); + // insert rendered notebook in the DOM + $(domElt).append(rendered); + // execute math typesetting + MathJax.Hub.Queue(['Typeset', MathJax.Hub]); + }, 1000); + }); +} diff --git a/swh/web/assets/src/bundles/webapp/notebook.css b/swh/web/assets/src/bundles/webapp/notebook.css new file mode 100644 --- /dev/null +++ b/swh/web/assets/src/bundles/webapp/notebook.css @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2019 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +.card { + overflow-x: visible; +} + +.nb-notebook { + line-height: 1.5; +} + +.nb-stdout, +.nb-stderr { + white-space: pre-wrap; + margin: 1em 0; + padding: 0.1em 0.5em; +} + +.nb-stderr { + background-color: #faa; +} + +.nb-cell + .nb-cell { + margin-top: 0.5em; +} + +.nb-output table { + border: 1px solid #000; + border-collapse: collapse; +} + +.nb-output th { + font-weight: bold; +} + +.nb-output th, +.nb-output td { + border: 1px solid #000; + padding: 0.25em; + text-align: left; + vertical-align: middle; + border-collapse: collapse; +} + +.nb-cell { + position: relative; +} + +.nb-raw-cell { + white-space: pre-wrap; + background-color: #f5f2f0; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + padding: 1em; + margin: 0.5em 0; +} + +.nb-input { + border: 1px solid #cfcfcf; + border-radius: 2px; + background: #f7f7f7; + margin: 0.4em; + padding: 0; +} + +pre { + margin: 0.4em !important; + border: none; + padding: 0; + background-color: transparent; +} + +.nb-output { + min-height: 1em; + width: 100%; + overflow-x: auto; + border-right: 1px dotted #ccc; +} + +.nb-output img { + max-width: 100%; +} + +.nb-output::before, +.nb-input::before { + position: absolute; + font-family: monospace; + color: #999; + left: -7.5em; + width: 7em; + text-align: right; +} + +.nb-input::before { + content: "In [" attr(data-prompt-number) "]:"; +} + +.nb-output::before { + content: "Out [" attr(data-prompt-number) "]:"; +} + +div[style="max-height:1000px;max-width:1500px;overflow:auto;"] { + max-height: none !important; +} + +#main { + width: 99%; + max-width: 750px; + margin: 0 auto; +} + +#header { + line-height: 2; + margin-bottom: 0.25em; + font-weight: bold; +} + +#controls { + border: 1px dotted #ccc; + padding: 0.75em; + margin-bottom: 0.5em; + background-color: #eef; +} + +#footer { + border: 1px dotted #ccc; + background-color: #eef; + font-size: 0.8em; + padding: 0.5em; + text-align: center; +} + +#footer a, +#footer a:visited { + color: #07e; +} diff --git a/swh/web/config.py b/swh/web/config.py --- a/swh/web/config.py +++ b/swh/web/config.py @@ -37,7 +37,7 @@ 'port': ('int', 5004), 'secret_key': ('string', 'development key'), # do not display code highlighting for content > 1MB - 'content_display_max_size': ('int', 1024 * 1024), + 'content_display_max_size': ('int', 5 * 1024 * 1024), 'snapshot_content_max_size': ('int', 1000), 'throttling': ('dict', { 'cache_uri': None, # production: memcached as cache (127.0.0.1:11211) diff --git a/swh/web/templates/includes/content-display.html b/swh/web/templates/includes/content-display.html --- a/swh/web/templates/includes/content-display.html +++ b/swh/web/templates/includes/content-display.html @@ -21,6 +21,9 @@ Content is too large to be displayed (size is greater than {{ max_content_size|filesizeformat }}). {% elif "inode/x-empty" == mimetype %} File is empty + {% elif swh_object_metadata.filename and swh_object_metadata.filename|default:""|slice:"-5:" == "ipynb" %} +
+
{% elif "text/" in mimetype %}
{{ content }}
@@ -47,6 +50,8 @@