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", @@ -27,6 +28,7 @@ "iframe-resizer": "^4.0.4", "jquery": "^3.3.1", "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": "^9.10.1", 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,8 @@ "__STATIC__": false, "Image": false, "Cookies": false, - "grecaptcha": false + "grecaptcha": false, + "nb": false }, "rules": { 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,48 @@ +/** + * 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}); + return converter.makeHtml(text); + } + + 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; + + fetch(nbJsonUrl) + .then(response => response.json()) + .then(nbJson => { + let notebook = nb.parse(nbJson); + let rendered = notebook.render(); + $(domElt).append(rendered); + }); +} 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/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" %} +
{{ content }}
@@ -47,6 +50,8 @@