Compare commits

..

No commits in common. "master" and "v0.1.0-beta.6" have entirely different histories.

65 changed files with 2328 additions and 6215 deletions

View file

@ -1,11 +1,8 @@
node_modules/
/.*cache/ /.*cache/
/.tmp/ /.tmp/
/coverage/ /coverage/
/dist/ /dist/
/packages/*/bin/
/packages/*/coverage/ /packages/*/coverage/
/packages/*/dist/
/packages/*/lib/ /packages/*/lib/
/packages/*/*.js /packages/*/*.js
/scripts/* /scripts/*

View file

@ -1,6 +1,4 @@
const tsconfigs = [ const tsconfigs = [
'tsconfig.json',
'*/tsconfig.json',
'packages/*/tsconfig.json', 'packages/*/tsconfig.json',
'packages/*/test/tsconfig.json', 'packages/*/test/tsconfig.json',
] ]
@ -20,14 +18,15 @@ module.exports = {
}, },
settings: { settings: {
'import/resolver': { 'import/resolver': {
// Use eslint-import-resolver-typescript to obey "paths" in tsconfig.json. // Use eslint-import-resolver-ts to obey "paths" in tsconfig.json.
typescript: { ts: {
directory: tsconfigs, directory: tsconfigs,
}, },
}, },
}, },
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking', 'plugin:@typescript-eslint/recommended-requiring-type-checking',
'standard-with-typescript', 'standard-with-typescript',
@ -36,63 +35,48 @@ module.exports = {
], ],
rules: { rules: {
'comma-dangle': ['error', 'always-multiline'], 'comma-dangle': ['error', 'always-multiline'],
// Disable in favour of TypeScript rule.
'func-call-spacing': 'off',
'linebreak-style': ['error', 'unix'], 'linebreak-style': ['error', 'unix'],
// Changed from error to warn and enabled ignoreEOLComments. 'lines-between-class-members': 'off',
// Disable in favour of TypeScript rule.
'no-extra-semi': 'off',
'no-multi-spaces': ['warn', { 'no-multi-spaces': ['warn', {
ignoreEOLComments: true, ignoreEOLComments: true,
}], }],
// Changed from error to warn and adjusted options.
'no-multiple-empty-lines': ['warn', { 'no-multiple-empty-lines': ['warn', {
max: 2, max: 2,
maxEOF: 1, maxEOF: 1,
maxBOF: 1, maxBOF: 1,
}], }],
'no-template-curly-in-string': 'off', 'no-template-curly-in-string': 'off',
// Changed from 'after' to 'before'.
'operator-linebreak': ['error', 'before'], 'operator-linebreak': ['error', 'before'],
// Changed from error and all 'never' to warn and switches 'never'.
'padded-blocks': ['warn', { 'padded-blocks': ['warn', {
switches: 'never', switches: 'never',
}], }],
// Changed from 'as-needed' to 'consistent-as-needed'.
'quote-props': ['error', 'consistent-as-needed'], 'quote-props': ['error', 'consistent-as-needed'],
// Disable in favour of TypeScript rule.
'semi': 'off',
// Import // Import
// Some packages have wrong type declarations.
'import/default': 'off', 'import/default': 'off',
'import/newline-after-import': 'warn', 'import/newline-after-import': 'warn',
'import/no-absolute-path': 'error',
// This rule disallows using both wildcard and selective imports from the same module. // This rule disallows using both wildcard and selective imports from the same module.
'import/no-duplicates': 'off', 'import/no-duplicates': 'off',
// Some packages have it wrong in type declarations (e.g. katex, marked). // Some packages has it wrong in type declarations (e.g. katex, marked).
'import/no-named-as-default-member': 'off', 'import/no-named-as-default-member': 'off',
// TypeScript // TypeScript
// Changed options.
'@typescript-eslint/ban-types': ['error', {
// Allow to use {} and object - they are actually useful.
types: {
'{}': false,
'object': false,
},
extendDefaults: true,
}],
'@typescript-eslint/class-literal-property-style': ['error', 'fields'],
// Changed from error to off.
'@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/consistent-type-definitions': 'off',
// Changed from error to off.
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-member-accessibility': ['error', { '@typescript-eslint/explicit-member-accessibility': ['warn', {
accessibility: 'no-public', accessibility: 'no-public',
overrides: { overrides: {
parameterProperties: 'off', parameterProperties: 'off',
}, },
}], }],
// Changed from warn to error and adjusted options. '@typescript-eslint/func-call-spacing': ['error', 'never'],
'@typescript-eslint/explicit-module-boundary-types': ['error', {
allowArgumentsExplicitlyTypedAsAny: true,
}],
'@typescript-eslint/indent': ['error', 2, { '@typescript-eslint/indent': ['error', 2, {
SwitchCase: 1, SwitchCase: 1,
VariableDeclarator: 1, VariableDeclarator: 1,
@ -111,58 +95,31 @@ module.exports = {
flatTernaryExpressions: true, flatTernaryExpressions: true,
ignoreComments: false, ignoreComments: false,
}], }],
// Changed from error to warn.
'@typescript-eslint/lines-between-class-members': 'warn',
// Changed delimiter for type literals from none to comma.
// The reason is just aesthetic symmetry with object literals.
'@typescript-eslint/member-delimiter-style': ['error', { '@typescript-eslint/member-delimiter-style': ['error', {
multiline: { delimiter: 'comma', requireLast: true }, multiline: { delimiter: 'comma', requireLast: true },
singleline: { delimiter: 'comma', requireLast: false }, singleline: { delimiter: 'comma', requireLast: false },
overrides: {
interface: {
multiline: { delimiter: 'none' },
},
},
}], }],
'@typescript-eslint/member-ordering': 'warn',
// Changed from warn to off.
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
// Changed from error to warn.
'@typescript-eslint/no-extra-semi': 'warn',
// It disallows using void even in valid cases.
'@typescript-eslint/no-invalid-void-type': 'off',
// Changed from error to warn.
'@typescript-eslint/no-namespace': 'warn', '@typescript-eslint/no-namespace': 'warn',
// Changed from error to warn.
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-require-imports': 'error', '@typescript-eslint/no-require-imports': 'error',
// Changed from error to warn. '@typescript-eslint/no-unused-vars': ['error', {
'@typescript-eslint/no-unsafe-assignment': 'warn', argsIgnorePattern: '^_',
// Changed from error to warn. }],
'@typescript-eslint/no-unsafe-member-access': 'warn',
// Disabled in favour of the next rule.
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars-experimental': 'error',
// Changed options.
'@typescript-eslint/no-use-before-define': ['error', { '@typescript-eslint/no-use-before-define': ['error', {
classes: true,
functions: false, functions: false,
typedefs: false, typedefs: false,
variables: true,
}], }],
'@typescript-eslint/prefer-for-of': 'warn', '@typescript-eslint/prefer-for-of': 'warn',
// Changed from error to warn.
'@typescript-eslint/prefer-includes': 'warn', '@typescript-eslint/prefer-includes': 'warn',
// Changed from error to warn.
'@typescript-eslint/prefer-regexp-exec': 'warn', '@typescript-eslint/prefer-regexp-exec': 'warn',
'@typescript-eslint/prefer-string-starts-ends-with': 'warn', '@typescript-eslint/prefer-string-starts-ends-with': 'warn',
// It has too many false positives. '@typescript-eslint/promise-function-async': ['error', {
'@typescript-eslint/restrict-template-expressions': 'off', allowAny: true,
// Changed from error to off.
'@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
// Changed from error to warn and adjusted options.
'@typescript-eslint/unbound-method': ['warn', {
ignoreStatic: true,
}], }],
'@typescript-eslint/semi': ['error', 'never'],
'@typescript-eslint/strict-boolean-expressions': 'off',
}, },
overrides: [ overrides: [
{ {
@ -170,14 +127,7 @@ module.exports = {
rules: { rules: {
// Allow to format arrays for parametrized tests as tables. // Allow to format arrays for parametrized tests as tables.
'array-bracket-spacing': 'off', 'array-bracket-spacing': 'off',
'comma-dangle': ['error', { 'comma-spacing': 'off',
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
// Changed to not require comma in a multiline expect().
functions: 'only-multiline',
}],
'object-curly-spacing': 'off', 'object-curly-spacing': 'off',
'no-multi-spaces': 'off', 'no-multi-spaces': 'off',
'standard/array-bracket-even-spacing': 'off', 'standard/array-bracket-even-spacing': 'off',
@ -185,10 +135,7 @@ module.exports = {
'space-in-parens': 'off', 'space-in-parens': 'off',
// jest.mock() must be above imports. // jest.mock() must be above imports.
'import/first': 'off', 'import/first': 'off',
'@typescript-eslint/comma-spacing': 'off', '@typescript-eslint/no-non-null-assertion': 'off',
// False positive on expect() functions.
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'warn',
}, },
}, },
], ],

3
.github/FUNDING.yml vendored
View file

@ -1,3 +0,0 @@
# These are supported funding model platforms
github: jirutka

View file

@ -1,68 +0,0 @@
name: CI
on:
- push
- pull_request
jobs:
test:
name: Test on Node.js ${{ matrix.node-version }}
runs-on: ubuntu-20.04
strategy:
matrix:
node-version: [10, 12, 13, 14]
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # fetch all history to make `git describe` work
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: yarn install
- run: yarn build
- run: yarn bundle
- run: yarn test
- run: yarn lint
publish:
name: Publish to npmjs and GitHub Releases
needs: [test]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # fetch all history to make `git describe` work
- run: sudo apt-get install asciidoctor pandoc
- uses: actions/setup-node@v2
with:
node-version: 14
registry-url: https://registry.npmjs.org
- run: yarn install
- name: Generate source tarball
run: ./scripts/create-src-tarball dist/ipynb2html-${GITHUB_REF/refs\/tags\//}-src.tar.gz
- run: yarn build
- run: yarn bundle
- name: Publish packages to npmjs
run: yarn publish-all --non-interactive
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Upload tarballs to Releases
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
fail_on_unmatched_files: true
files: |
dist/*.tar.gz
packages/ipynb2html-cli/dist/*.tar.gz
packages/ipynb2html-cli/dist/*.zip

2
.gitignore vendored
View file

@ -1,13 +1,11 @@
/coverage/ /coverage/
/dist/ /dist/
/packages/*/coverage/ /packages/*/coverage/
/packages/*/dist/
/packages/*/lib/ /packages/*/lib/
/packages/*/README.md /packages/*/README.md
/.*cache/ /.*cache/
/.tmp/ /.tmp/
node_modules/ node_modules/
.eslintcache
.tsbuildinfo .tsbuildinfo
tsconfig.tsbuildinfo tsconfig.tsbuildinfo
*.log *.log

38
.travis.yml Normal file
View file

@ -0,0 +1,38 @@
dist: bionic
language: node_js
node_js:
- '10'
- '12'
- node
addons:
apt:
packages:
- asciidoctor
- pandoc
env:
global:
- secure: "DrPA1eYBRjS+1/w/sRq/r5wQyOewuh1PGPzFtQR62TJeAyO6AvKQ6wfesxMA898b+0D3SCNxrCVK12XB3auySEOZocQuN7N51hsteA/QtPoBBbnoHy8Dap2YbiJ5fbCVnM/Wl/Z2rZmQWFBM3rmqXggCyEhKEw3kkz8WMm/7UCGVxmoHUelpMnDEII0RiJdPCGT19IA90KpJDsqbSzTVY+TsqjSNuN91LQ23ApwSHKklvbvKWxcgrtAzJDXLeS9CS3QqSHucurOM2Kpv0umOkBzds4da+NtWKYZC3XxThmMB5wT7b60EZPIc/iFftQFy2qiDAFxeGN+j9kwsNX68aXl4MuCGlzdvGj0KkeXYYhl1Jusc30uTzXYMlz2b3u+AcsMLLxFs2HvUU94SpAfe9VrarSnQK+6CZz0eCtF/NCCi6J9GqlBTsqzZDmdVaJpFDG1FidC4Ka9FcteKcWXqffowQ5KjIhqaearSmRESMqepV7T8tDUCb217PE0C+L0NGfg6RaY4DtGsJAawDeh/09aXrP6NakAKjWUfaJqjhkMexB8JTb+yanjVXsgj4VUfvTgvWjMi+yU3DIfopL+mawvgckRRL9DTEIf5ICjSruyEH8FWEz+kMzas41zboabR12YMLuorHfgZu31DiDsJkSD292t/lwWW6oybRl0iJBk=" # NPM_TOKEN=ceed......2bf2
cache: yarn
install:
- yarn install
script:
- yarn run build
- yarn run test
- yarn run lint
before_deploy:
- echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' >> "$HOME"/.npmrc
deploy:
provider: script
script: yarn publish-all --non-interactive
skip_cleanup: true
on:
tags: true
node_js: '10'

View file

@ -1,6 +1,6 @@
The MIT License The MIT License
Copyright 2019-2020 Jakub Jirutka <jakub@jirutka.cz>. Copyright 2019 Jakub Jirutka <jakub@jirutka.cz>.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -2,18 +2,13 @@
:npm-name: ipynb2html :npm-name: ipynb2html
:gh-name: jirutka/{npm-name} :gh-name: jirutka/{npm-name}
:gh-branch: master :gh-branch: master
:version: 0.3.0
:ansiup-version: 5.0.1
:hljs-version: 10.7.3
:katex-version: 0.13.11
:marked-version: 2.0.7
:vs-marketplace-uri: https://marketplace.visualstudio.com/items?itemName= :vs-marketplace-uri: https://marketplace.visualstudio.com/items?itemName=
ifdef::env-github[] ifdef::env-github[]
image:https://github.com/{gh-name}/workflows/CI/badge.svg[CI Status, link=https://github.com/{gh-name}/actions?query=workflow%3A%22CI%22] image:https://travis-ci.com/{gh-name}.svg?branch={gh-branch}[Build Status, link="https://travis-ci.com/{gh-name}"]
endif::env-github[] endif::env-github[]
{npm-name} is a converter (renderer) of the https://nbformat.readthedocs.io/en/stable/[Jupyter Notebook Format] 4.0+ to static HTML. ipynb2html is a converter (renderer) of the https://nbformat.readthedocs.io/en/stable/[Jupyter Notebook Format] 4.0+ to static HTML.
It works both in Node.js and browser environment. It works both in Node.js and browser environment.
@ -26,18 +21,15 @@ This repository contains the following packages, all published on https://www.np
ifdef::env-github[] ifdef::env-github[]
image:https://img.shields.io/npm/v/{npm-name}-core.svg[Version on npm, link="https://www.npmjs.org/package/{npm-name}-core"] image:https://img.shields.io/npm/v/{npm-name}-core.svg[Version on npm, link="https://www.npmjs.org/package/{npm-name}-core"]
image:https://img.shields.io/bundlephobia/min/{npm-name}-core.svg[Minified bundle size, link="https://bundlephobia.com/result?p={npm-name}-core"]
endif::env-github[] endif::env-github[]
This package provides the converter itself and some utilities with *no dependencies*. This package provides the converter itself and some utilities with *no dependencies*.
You have to provide your own syntax highlighter and Markdown, math and ANSI sequences renderer; or not, if you dont need them. You have to provide your own syntax highlighter and Markdown, math and ANSI sequences renderer; or not, if you dont need them.
=== {npm-name} === {npm-name}
ifdef::env-github[] ifdef::env-github[]
image:https://img.shields.io/npm/v/{npm-name}.svg[Version on npm, link="https://www.npmjs.org/package/{npm-name}"] image:https://img.shields.io/npm/v/{npm-name}.svg[Version on npm, link="https://www.npmjs.org/package/{npm-name}"]
image:https://img.shields.io/bundlephobia/min/{npm-name}.svg[Minified bundle size, link="https://bundlephobia.com/result?p={npm-name}"]
endif::env-github[] endif::env-github[]
This package builds on the {npm-name}-core and provides a complete, ready-to-go renderer configured with: This package builds on the {npm-name}-core and provides a complete, ready-to-go renderer configured with:
@ -47,162 +39,12 @@ This package builds on the {npm-name}-core and provides a complete, ready-to-go
* https://github.com/IonicaBizau/anser[anser] as ANSI sequences renderer, * https://github.com/IonicaBizau/anser[anser] as ANSI sequences renderer,
* https://github.com/highlightjs/highlight.js[highlight.js] as syntax highlighter. * https://github.com/highlightjs/highlight.js[highlight.js] as syntax highlighter.
It also provides a reference stylesheet which you can find in `dist/notebook.min.css` (or non-minified link:packages/{npm-name}/styles/notebook.css[`styles/notebook.css`]).
=== {npm-name}-cli
ifdef::env-github[]
image:https://img.shields.io/npm/v/{npm-name}-cli.svg[Version on npm, link="https://www.npmjs.org/package/{npm-name}-cli"]
image:https://img.shields.io/bundlephobia/min/{npm-name}-cli.svg[Minified bundle size, link="https://bundlephobia.com/result?p={npm-name}-cli"]
endif::env-github[]
This package provides a CLI interface for {npm-name}.
ifndef::npm-readme[] ifndef::npm-readme[]
== Installation
All the <<Packages, packages>> can be installed using `npm` or `yarn` from https://www.npmjs.com/[npmjs.com].
=== Standalone CLI Tool
{npm-name}-cli is also provided as a single minified JavaScript with all the external dependencies bundled in.
It requires only Node.js (version 10 or newer) to be installed on the system.
* https://github.com/{gh-name}/releases/download/v{version}/{npm-name}-cli-v{version}.tar.gz[{npm-name}-cli-v{version}.tar.gz]
* https://github.com/{gh-name}/releases/download/v{version}/{npm-name}-cli-v{version}.zip[{npm-name}-cli-v{version}.zip]
The archive also contains source maps (useful for debugging).
endif::[]
== Usage
=== CLI
[source, subs="+attributes"]
{npm-name} notebook.ipynb notebook.html
Run `{npm-name} --help` for more information.
=== Node.js (server-side)
To render HTML in Node.js (server-side rendering), you need some (fake) DOM implementation.
The recommended one is https://github.com/redom/nodom/[nodom] -- its lightweight, https://bundlephobia.com/result?p=nodom[small], doesnt have any external dependencies and {npm-name} is tested against it.
However, you can choose any other if you like.
[source, subs="+attributes"]
npm install {npm-name} nodom
[source, js, subs="+attributes"]
----
import * as fs from 'fs'
import * as ipynb from '{npm-name}'
import { Document } from 'nodom'
const renderNotebook = ipynb.createRenderer(new Document())
const notebook = JSON.parse(fs.readFileSync('./example.ipynb', 'utf8'))
console.log(renderNotebook(notebook).outerHTML)
----
=== Browser (client-side)
You have basically two options how to use {npm-name} in the browser: use the browser bundles provided in the {npm-name} package, or build your own bundle (using e.g. https://rollupjs.org[Rollup] or https://webpack.js.org/[webpack]).
The provided bundles are in UMD format (AMD, CommonJS and IIFE in one file), so they should work in all environments (old and modern browsers, Node.js).
They are transpiled and have injected https://github.com/zloirock/core-js/[core-js] polyfills to be compatible with browsers that have https://browserl.ist/?q=%3E0.5%25%2C+Firefox+ESR%2C+not+dead[>0.5% global coverage, Firefox ESR, and not dead browsers].
==== Full Bundle
`{npm-name}-full.min.js` is a self-contained bundle with all the external dependencies included (marked, KaTeX, Anser and Highlight.js).
You can link it from https://www.jsdelivr.com/[jsDelivr CDN], for example:
[source, html, subs="+attributes"]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/{npm-name}@{version}/dist/notebook.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@{katex-version}/dist/katex.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@{hljs-version}/build/styles/default.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/{npm-name}@{version}/dist/{npm-name}-full.min.js" crossorigin="anonymous"></script>
</head>
...
</html>
The bundle exposes global variable `{npm-name}`:
[source, js, subs="+attributes"]
const element = {npm-name}.render(notebook)
document.body.appendChild(element)
{npm-name} also provides function `autoRender` that renders each notebook on the page embedded (as JSON) inside `<script type="application/x-ipynb+json">\...</script>`.footnote:[Dont forget to escape HTML special characters: `<`, `>`, and `&`.]
[source, html, subs="+attributes"]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/{npm-name}@{version}/dist/notebook.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@{katex-version}/dist/katex.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@{hljs-version}/build/styles/default.min.css" crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/{npm-name}@{version}/dist/{npm-name}-full.min.js" crossorigin="anonymous"
onload="{npm-name}.autoRender();"></script>
</head>
<body>
<main>
<script type="application/x-ipynb+json">
{
"cells": [ ... ],
"metadata": { ... },
"nbformat": 4,
"nbformat_minor": 3
}
</script>
</main>
</body>
<html>
==== Slim Bundle
`{npm-name}.min.js` contains only {npm-name} and {npm-name}-core code (plus polyfills).
If you load marked, KaTeX, AnsiUp, and Highlight.js in the page, you will get the same functionality as with `{npm-name}-full.min.js`:
[source, html, subs="+attributes"]
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@{katex-version}/dist/katex.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@{hljs-version}/build/styles/default.min.css" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/marked@{marked-version}/marked.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/ansi_up@{ansiup-version}/ansi_up.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@{hljs-version}/build/highlight.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@{katex-version}/dist/katex.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/{npm-name}@{version}/dist/{npm-name}.min.js" crossorigin="anonymous"></script>
</head>
...
</html>
Or you may use any other implementations and provide them to the `{npm-name}.createRenderer` function.
All of them are optional, but you usually need at least a Markdown renderer.
ifndef::npm-readme[]
== Development == Development
=== System Requirements == System Requirements
* https://nodejs.org[NodeJS] 10.13+ * https://nodejs.org[NodeJS] 10.13+
* https://pandoc.org[Pandoc] and https://asciidoctor.org[Asciidoctor] (used only for converting README.adoc to Markdown for npmjs) * https://pandoc.org[Pandoc] and https://asciidoctor.org[Asciidoctor] (used only for converting README.adoc to Markdown for npmjs)
@ -215,7 +57,6 @@ ifndef::npm-readme[]
* https://yarnpkg.com[yarn] for dependencies management and building * https://yarnpkg.com[yarn] for dependencies management and building
* https://eslint.org[ESLint] for linting JS/TypeScript code * https://eslint.org[ESLint] for linting JS/TypeScript code
* https://jestjs.io[Jest] for testing * https://jestjs.io[Jest] for testing
* https://rollupjs.org[Rollup] for building single-file bundles
=== How to Start === How to Start

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -63,7 +63,7 @@ module.exports = {
globals: { globals: {
'ts-jest': { 'ts-jest': {
compiler: 'ttypescript', compiler: 'ttypescript',
tsConfig: '<rootDir>/test/tsconfig.json', tsConfig: '<rootDir>/tsconfig.json',
}, },
}, },

View file

@ -1,70 +1,51 @@
{ {
"name": "ipynb2html-parent", "name": "ipynb2html-parent",
"version": "0.3.0", "version": "0.1.0-beta.6",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "ttsc --build", "build": "ttsc --build",
"bundle": "wsrun --exclude-missing bundle", "clean": "rimraf coverage/ lib/ *.log && wsrun clean",
"clean": "rimraf coverage/ dist/ lib/ .eslintcache *.log && wsrun clean", "lint": "eslint --ext .ts,.tsx,.js .",
"lint": "eslint --cache --ext .ts,.tsx,.js .", "postinstall": "run-s build",
"postinstall": "patch-package && run-s build",
"publish-all": "wsrun --serial publish", "publish-all": "wsrun --serial publish",
"test": "jest --detectOpenHandles --coverage --verbose", "test": "jest --detectOpenHandles --coverage --verbose",
"version": "./scripts/bump-version && git add README.adoc **/package.json", "version": "./scripts/bump-version && git add **/package.json",
"watch-ts": "ttsc --build --watch" "watch-ts": "ttsc --build --watch"
}, },
"engines": { "engines": {
"node": ">=10.13.0" "node": ">=10.13.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.10.3",
"@babel/preset-env": "^7.10.3",
"@rollup/plugin-babel": "^5.0.3",
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-node-resolve": "^8.0.1",
"@types/dedent": "^0.7.0", "@types/dedent": "^0.7.0",
"@types/jest": "^24.9.1", "@types/highlightjs": "^9.12.0",
"@types/node": "^10.17.25", "@types/jest": "^24.0.18",
"@typescript-eslint/eslint-plugin": "^3.3.0", "@types/katex": "^0.10.2",
"@typescript-eslint/parser": "^3.3.0", "@types/marked": "^0.6.5",
"@types/node": "^10.13.0",
"@typescript-eslint/eslint-plugin": "^2.4.0",
"@typescript-eslint/parser": "^2.4.0",
"arrify": "^2.0.1", "arrify": "^2.0.1",
"common-path-prefix": "^3.0.0",
"core-js": "^3.6.5",
"csso-cli": "^3.0.0",
"dedent": "^0.7.0", "dedent": "^0.7.0",
"eslint": "^7.3.0", "eslint": "^6.5.1",
"eslint-config-standard-with-typescript": "^18.0.2", "eslint-config-standard-with-typescript": "^10.0.0",
"eslint-import-resolver-typescript": "^2.0.0", "eslint-import-resolver-ts": "^0.4.0",
"eslint-plugin-import": "^2.21.2", "eslint-plugin-import": "^2.18.0",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.0",
"fs-extra": "^8.1.0", "jest": "^24.9.0",
"jest": "^25.5.4", "jest-chain": "^1.1.2",
"jest-chain": "^1.1.5",
"node-html-parser": "^1.2.19",
"nodom": "^2.3.0", "nodom": "^2.3.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"patch-package": "^6.2.2", "rimraf": "^3.0.0",
"postinstall-postinstall": "^2.1.0", "ts-jest": "^24.1.0",
"rimraf": "^3.0.2", "ts-node": "^8.4.1",
"rollup": "^2.17.1",
"rollup-plugin-add-git-msg": "^1.1.0",
"rollup-plugin-executable": "^1.6.0",
"rollup-plugin-node-externals": "^2.2.0",
"rollup-plugin-node-license": "^0.2.0",
"rollup-plugin-terser": "^6.1.0",
"rollup-plugin-typescript2": "^0.27.1",
"tar": "^5.0.10",
"ts-jest": "^25.5.1",
"ts-node": "^8.10.2",
"ts-transformer-export-default-name": "^0.1.0", "ts-transformer-export-default-name": "^0.1.0",
"ts-transformer-inline-file": "^0.1.1", "ts-transformer-inline-file": "^0.1.1",
"ttypescript": "^1.5.10", "ttypescript": "^1.5.7",
"typescript": "~3.9.5", "typescript": "~3.6.4",
"wsrun": "^5.2.1", "wsrun": "^5.0.2",
"yarn-version-bump": "^0.0.3", "yarn-version-bump": "^0.0.3"
"yazl": "^2.5.1"
}, },
"workspaces": [ "workspaces": [
"packages/*" "packages/*"

View file

@ -1,2 +0,0 @@
#!/usr/bin/env node
require('../lib/index').default(process.argv.slice(2))

View file

@ -1,54 +0,0 @@
{
"name": "ipynb2html-cli",
"version": "0.3.0",
"description": "CLI tool for converting Jupyter Notebooks to static HTML",
"author": "Jakub Jirutka <jakub@jirutka.cz>",
"license": "MIT",
"homepage": "https://github.com/jirutka/ipynb2html",
"bugs": "https://github.com/jirutka/ipynb2html/issues",
"repository": {
"type": "git",
"url": "https://github.com/jirutka/ipynb2html.git"
},
"keywords": [
"cli",
"converter",
"html",
"ipython",
"jupyter",
"notebook"
],
"bin": {
"ipynb2html": "bin/ipynb2html"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"bin",
"lib",
"src"
],
"scripts": {
"build": "ttsc --build",
"bundle": "rollup -c && ./scripts/pack-bundle",
"clean": "rimraf coverage/ dist/ lib/ .eslintcache .tsbuildinfo",
"lint": "PKGDIR=$PWD; cd ../../ && eslint --cache --ext .ts,.tsx,.js $PKGDIR",
"prepublishOnly": "run-s readme2md",
"readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md",
"watch-ts": "ttsc --build --watch"
},
"engines": {
"node": ">=10.13.0"
},
"dependencies": {
"ipynb2html": "0.3.0",
"minimist": "^1.2.6",
"minimist-options": "^4.0.2",
"nodom": "^2.3.0",
"source-map-support": "^0.5.16"
},
"devDependencies": {
"@types/minimist": "^1.2.0",
"@types/source-map-support": "^0.5.1"
}
}

View file

@ -1,67 +0,0 @@
import addGitMsg from 'rollup-plugin-add-git-msg'
import commonjs from '@rollup/plugin-commonjs'
import executable from 'rollup-plugin-executable'
import externals from 'rollup-plugin-node-externals'
import license from 'rollup-plugin-node-license'
import resolve from '@rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
import typescript from 'rollup-plugin-typescript2'
import ttypescript from 'ttypescript'
import pkg from './package.json'
export default {
input: 'src/index.ts',
plugins: [
// Transpile TypeScript sources to JS.
typescript({
typescript: ttypescript,
tsconfigOverride: {
compilerOptions: {
module: 'ESNext',
declaration: false,
declarationMap: false,
composite: false,
incremental: true,
},
},
clean: true,
}),
// Make node builtins external.
externals(),
// Resolve node modules.
resolve({
extensions: ['.mjs', '.js', '.ts'],
mainFields: ['jsnext:main', 'module', 'main'],
}),
// Convert CommonJS modules to ES6 modules.
commonjs(),
// Add git tag, commit SHA and build date at top of the file.
addGitMsg({
copyright: [
pkg.author,
'* This project is licensed under the terms of the MIT license.'
].join('\n'),
}),
// Generate table of the bundled packages at top of the file.
license({ format: 'table' }),
// Minify JS.
terser({
ecma: 2018,
output: {
// Preserve comment injected by addGitMsg and license.
comments: RegExp(`(?:\\$\\{${pkg.name}\\}|Bundled npm packages)`),
},
}),
// Make the output file executable.
executable(),
],
output: {
file: 'dist/ipynb2html',
format: 'cjs',
banner: '#!/usr/bin/env node',
exports: 'named',
sourcemap: true,
},
}

View file

@ -1,8 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "$0")/../dist"
export PATH="$(pwd)/../../../scripts:$PATH"
assemble-license ipynb2html > LICENSE
create-archives ipynb2html-cli ipynb2html ipynb2html.map LICENSE

View file

@ -1,134 +0,0 @@
import fs from 'fs'
import minimist from 'minimist'
import minimistOptions from 'minimist-options'
import { Document } from 'nodom'
import { exit } from 'process'
import { $INLINE_FILE, $INLINE_JSON } from 'ts-transformer-inline-file'
import * as ipynb2html from 'ipynb2html'
import renderPage from './page'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { version, bugs: bugsUrl } = $INLINE_JSON('../package.json')
const notebookCss = $INLINE_FILE('../../ipynb2html/styles/notebook.css')
const pageCss = $INLINE_FILE('./page.css')
const progName = 'ipynb2html'
const helpMsg = `\
Usage: ${progName} [options] <input> [<output>]
Convert Jupyter Notebook 4.0+ to a static HTML page.
Arguments:
<input> Path of the Jupyter notebook to read, or "-" for STDIN.
<output> Path of the file to write the output HTML into. If not
provided, the output will be written to STDOUT.
Options:
-d --debug Print debug messages.
-s --style <file,...> Comma separated stylesheet(s) to embed into the output
HTML. The stylesheet may be a path to a CSS file,
"@base" for the base ipynb2html style, or "@default"
for the default full page style. Default is @default.
-h --help Show this message and exit.
-V --version Print version and exit.
Exit Codes:
1 Generic error code.
2 Missing required arguments or invalid option.
Please report bugs at <${bugsUrl}>.
`
function logErr (msg: string): void {
console.error(`${progName}: ${msg}`)
}
function arrify <T> (obj: T | T[]): T[] {
return Array.isArray(obj) ? obj : [obj]
}
function parseCliArgs (argv: string[]) {
const opts = minimist(argv, minimistOptions({
debug: { alias: 'd', type: 'boolean' },
style: { alias: 's', type: 'string', default: '@default' },
version: { alias: 'V', type: 'boolean' },
help: { alias: 'h', type: 'boolean' },
arguments: 'string',
stopEarly: true,
unknown: (arg: string) => {
if (arg.startsWith('-')) {
logErr(`Unknown option: ${arg}`)
return exit(2)
} else {
return true
}
},
}))
if (opts.help) {
console.log(helpMsg)
return exit(0)
}
if (opts.version) {
console.log(`${progName} ${version}`)
return exit(0)
}
if (opts._.length < 1 || opts._.length > 2) {
logErr('Invalid number of arguments\n')
console.log(helpMsg)
return exit(2)
}
const [input, output] = opts._
return {
styles: arrify(opts.style).join(',').split(/,\s*/),
debug: opts.debug as boolean,
input: input === '-' ? 0 : input, // 0 = stdin
output,
}
}
function loadStyle (name: string): string {
switch (name) {
case '@base': return notebookCss
case '@default': return pageCss + notebookCss
default: return fs.readFileSync(name, 'utf8')
}
}
export default (argv: string[]): void => {
const opts = parseCliArgs(argv)
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const notebook = JSON.parse(fs.readFileSync(opts.input, 'utf-8'))
const style = opts.styles.map(loadStyle).join('\n')
const title = ipynb2html.readNotebookTitle(notebook) || 'Notebook'
const renderNotebook = ipynb2html.createRenderer(new Document())
const contents = renderNotebook(notebook).outerHTML
const html = renderPage({ contents, title, style })
if (opts.output) {
fs.writeFileSync(opts.output, html)
} else {
console.log(html)
}
} catch (err) {
if (opts.debug) {
console.debug(err)
} else {
logErr((err as Error).message)
}
return exit(1)
}
}

View file

@ -1,15 +0,0 @@
import sourceMapSupport from 'source-map-support'
import cli from './cli'
// Allow to disable sourcemap when running from pkg bundle.
if (!/^(0|disable|false|no|off)$/i.test(process.env.NODE_SOURCEMAP ?? '')) {
sourceMapSupport.install({ environment: 'node' })
}
// If the file is run directly (not required as a module), call CLI.
if (require.main === module) {
cli(process.argv.slice(2))
}
export default cli

View file

@ -1,95 +0,0 @@
html, body {
margin: 0;
padding: 0;
background-color: #dedede;
color: #2a2a2a;
font-family: sans-serif;
}
code,
pre {
font-size: 0.85rem;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.6em;
position: relative;
}
h1 {
font-size: 2em;
}
h1 .anchor,
h2 .anchor,
h3 .anchor,
h4 .anchor,
h5 .anchor,
h6 .anchor {
display: block;
width: 1em;
left: -1em;
height: 100%;
position: absolute;
}
h1:hover .anchor,
h2:hover .anchor,
h3:hover .anchor,
h4:hover .anchor,
h5:hover .anchor,
h6:hover .anchor {
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 50 50'%3E%3Cg fill='none' stroke='%23000' stroke-width='3'%3E%3Cpath d='M26.1 22a5.8 5.8 0 00-8.1 0l-9.6 9.5a5.8 5.8 0 008.2 8.2l5.8-5.9'/%3E%3Cpath d='M23.6 28a5.8 5.8 0 008.2 0l9.5-9.5a5.8 5.8 0 00-8.1-8.2l-5.8 5.9'/%3E%3C/g%3E%3C/svg%3E") no-repeat 100% center;
}
table {
border-collapse: collapse;
border: 1px solid #cfcfcf;
}
th {
font-weight: 600;
}
td,
th {
padding: 0.2em 0.4em;
border: 1px solid #cfcfcf;
}
thead tr,
tbody tr:nth-child(even) {
background-color: #f7f7f7;
}
thead tr:hover,
tbody tr:hover {
background-color: #cfcfcf;
}
.nb-notebook {
max-width: 45rem;
margin: 1rem auto;
padding: 3rem 6rem;
background-color: white;
box-shadow: 4px 4px 8px #cfcfcf, -4px -4px 8px #cfcfcf;
}
.nb-output table {
font-size: 0.9em;
}
@media (max-width: 900px) {
.nb-notebook {
margin: 0;
padding-left: 5rem;
padding-right: 3rem;
max-width: none;
box-shadow: none;
}
}
@media (max-width: 768px) {
.nb-notebook {
padding: 1rem 5% 2rem 5%;
}
}

View file

@ -1,34 +0,0 @@
import { version } from 'ipynb2html'
export type Options = {
contents: string,
title: string,
style: string,
}
export default ({ contents, title, style }: Options): string => `\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1">
<meta name="generator" content="ipynb2html ${version}">
<title>${title}</title>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.7.3/build/styles/default.min.css"
integrity="sha384-s4RLYRjGGbVqKOyMGGwfxUTMOO6D7r2eom7hWZQ6BjK2Df4ZyfzLXEkonSm0KLIQ"
crossorigin="anonymous">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css"
integrity="sha384-Um5gpz1odJg5Z4HAmzPtgZKdTBHZdw8S29IecapCSB31ligYPhHQZMIlWLYQGVoc"
crossorigin="anonymous">
<style>
${style.replace(/\n\n/g, '\n').replace(/\n$/, '').replace(/^/gm, ' ')}
</style>
</head>
<body>
${contents}
</body>
</html>
`

View file

@ -1,16 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"tsBuildInfoFile": "./.tsbuildinfo",
"baseUrl": ".",
},
"include": [
"./src",
],
"references": [
{ "path": "../ipynb2html-core" },
{ "path": "../ipynb2html" },
],
}

View file

@ -1,6 +1,6 @@
{ {
"name": "ipynb2html-core", "name": "ipynb2html-core",
"version": "0.3.0", "version": "0.1.0-beta.6",
"description": "Convert Jupyter Notebook to static HTML", "description": "Convert Jupyter Notebook to static HTML",
"author": "Jakub Jirutka <jakub@jirutka.cz>", "author": "Jakub Jirutka <jakub@jirutka.cz>",
"license": "MIT", "license": "MIT",
@ -25,8 +25,8 @@
], ],
"scripts": { "scripts": {
"build": "ttsc --build", "build": "ttsc --build",
"clean": "rimraf coverage/ lib/ .eslintcache .tsbuildinfo", "clean": "rimraf coverage/ lib/ .tsbuildinfo",
"lint": "PKGDIR=$PWD; cd ../../ && eslint --cache --ext .ts,.tsx,.js $PKGDIR", "lint": "eslint --ext .ts,.tsx,.js .",
"prepublishOnly": "run-s readme2md", "prepublishOnly": "run-s readme2md",
"test": "jest --detectOpenHandles --coverage --verbose", "test": "jest --detectOpenHandles --coverage --verbose",
"readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md", "readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md",

View file

@ -4,8 +4,8 @@ type Attributes = { [k: string]: string }
// for this module's function. // for this module's function.
export type MinimalElement = { export type MinimalElement = {
innerHTML: string, innerHTML: string,
setAttribute: (name: string, value: string) => void, setAttribute (name: string, value: string): void,
appendChild: (child: any) => unknown, appendChild (child: any): any,
} }
export type ElementCreator<TElement = HTMLElement> = export type ElementCreator<TElement = HTMLElement> =

View file

@ -16,7 +16,7 @@ export default <TElement> (opts: Options<TElement>): DataRenderer<TElement> => {
const { elementCreator: el, mathRenderer: renderMath } = opts const { elementCreator: el, mathRenderer: renderMath } = opts
return (data: string): TElement => { return (data: string): TElement => {
const math = (extractMathRx.exec(data) ?? [])[1] const math = (extractMathRx.exec(data) || [])[1]
return math return math
? el('div', ['latex-output'], renderMath(math)) ? el('div', ['latex-output'], renderMath(math))
: el('div', ['html-output'], data) : el('div', ['html-output'], data)

View file

@ -1,8 +1,8 @@
import * as mathExtractor from './mathExtractor' import * as mathExtractor from './mathExtractor'
export { default as createElementCreator, ElementCreator, MinimalElement } from './elementCreator' export { default as createElementCreator, ElementCreator } from './elementCreator'
export { default as createHtmlRenderer } from './htmlRenderer' export { default as createHtmlRenderer } from './htmlRenderer'
export * from './nbformat' export * from './nbformat'
export { default as NbRenderer, DataRenderer, NbRendererOpts } from './renderer' export { default as createNbRenderer, DataRenderer, NbRenderer, Options as NbRendererOpts } from './renderer'
export { mathExtractor } export { mathExtractor }
export { default as version } from './version' export { default as version } from './version'

View file

@ -1,53 +1,36 @@
const htmlEntities: Record<string, string> = { const htmlEntities = {
'&': '&amp;', '&': '&amp;',
'<': '&lt;', '<': '&lt;',
'>': '&gt;', '>': '&gt;',
} }
type Callable = (...args: any[]) => any /**
* Creates a "callable object" with the given properties. In fact, it creates
* a function that calls `obj[funcName]` and copies all the enumerable
* properties of the *template* to the created function.
*
* @param {string} funcName Name of the function property of the *template*.
* @param {Object} template The source object from which to copy enumerable properties.
* @return A function with all enumerable properties of the *template*.
*/
export function callableObject <T, K extends keyof T> (
funcName: K,
template: T,
): T[K] extends Function ? T & T[K] : never {
type CallableConstructor = new <T> () => T extends { __call__: Callable } const fn = function (...args: any[]) {
? T['__call__'] return (template[funcName] as any)(...args)
: 'subclass does not implement method __call__' }
return Object.assign(fn, template) as any
/* eslint-disable @typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access,
@typescript-eslint/ban-types */
export const CallableInstance: CallableConstructor = function Callable (
this: object,
): Callable {
const func = this.constructor.prototype.__call__ as Callable
const cls = function (...args: any[]) {
return func.apply(cls, args) as unknown
} }
Object.setPrototypeOf(cls, this.constructor.prototype)
Object.defineProperties(cls, {
name: {
value: this.constructor.name,
configurable: true,
},
length: {
value: func.length,
configurable: true,
},
})
return cls
} as any
CallableInstance.prototype = Object.create(Function.prototype)
/* eslint-enable @typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access,
@typescript-eslint/ban-types */
/** /**
* Escapes characters with special meaning in HTML with the corresponding * Escapes characters with special meaning in HTML with the corresponding
* HTML entities. * HTML entities.
*/ */
export function escapeHTML (str: string): string { export function escapeHTML (str: string): string {
return str.replace(/[&<>]/g, c => htmlEntities[c]) return str.replace(/[&<>]/g, c => (htmlEntities as any)[c])
} }
/** /**

View file

@ -191,7 +191,7 @@ export function extractMath (text: string): [string, MathExpression[]] {
} }
} }
if (lastIdx) { if (lastIdx) {
processMath(unescape, math, blocks, startIdx ?? 0, lastIdx) processMath(unescape, math, blocks, startIdx || 0, lastIdx)
startIdx = endDelim = lastIdx = null startIdx = endDelim = lastIdx = null
} }
return [unescape(blocks.join('')), math.map(parseDelimitedMath)] return [unescape(blocks.join('')), math.map(parseDelimitedMath)]
@ -204,6 +204,6 @@ export function extractMath (text: string): [string, MathExpression[]] {
*/ */
export function restoreMath (text: string, math: string[]): string { export function restoreMath (text: string, math: string[]): string {
return text return text
.replace(/@@([1-9][0-9]*)@@/g, (_, n) => math[Number(n) - 1] ?? '') .replace(/@@([1-9][0-9]*)@@/g, (_, n) => math[Number(n) - 1])
.replace(/@@0(\d+)@@/g, (_, n) => `@@${n}@@`) .replace(/@@0(\d+)@@/g, (_, n) => `@@${n}@@`)
} }

View file

@ -1,79 +1,78 @@
// These types are based on https://github.com/jupyter/nbformat/blob/b6b5a18e5a40d37f1cc0f71f65108288bdec9bb7/nbformat/v4/nbformat.v4.schema.json. // These types are based on https://github.com/jupyter/nbformat/blob/b6b5a18e5a40d37f1cc0f71f65108288bdec9bb7/nbformat/v4/nbformat.v4.schema.json.
// This file was originally generated using json-schema-to-typescript 7.0.0 and // This file was originally generated using json-schema-to-typescript 7.0.0 and
// then manually polished. // then manually polished.
/* eslint-disable @typescript-eslint/member-ordering */
/** Jupyter Notebook v4.3. */ /** Jupyter Notebook v4.3. */
export interface Notebook { export interface Notebook {
/** Notebook root-level metadata. */ /** Notebook root-level metadata. */
metadata: NotebookMetadata metadata: NotebookMetadata,
/** Notebook format (minor number). Incremented for backward compatible changes to the notebook format. */ /** Notebook format (minor number). Incremented for backward compatible changes to the notebook format. */
nbformat_minor: number nbformat_minor: number,
/** Notebook format (major number). Incremented between backwards incompatible changes to the notebook format. */ /** Notebook format (major number). Incremented between backwards incompatible changes to the notebook format. */
nbformat: 4 nbformat: 4,
/** Array of cells of the current notebook. */ /** Array of cells of the current notebook. */
cells: Cell[] cells: Cell[],
} }
/** Notebook root-level metadata. */ /** Notebook root-level metadata. */
export interface NotebookMetadata { export interface NotebookMetadata {
/** Kernel information. */ /** Kernel information. */
kernelspec?: KernelSpec kernelspec?: KernelSpec,
/** Kernel information. */ /** Kernel information. */
language_info?: LanguageInfo language_info?: LanguageInfo,
/** Original notebook format (major number) before converting the notebook between versions. This should never be written to a file. */ /** Original notebook format (major number) before converting the notebook between versions. This should never be written to a file. */
orig_nbformat?: number orig_nbformat?: number,
/** The title of the notebook document */ /** The title of the notebook document */
title?: string title?: string,
/** The author(s) of the notebook document */ /** The author(s) of the notebook document */
authors?: any[] authors?: any[],
/** Extra properties. */ /** Extra properties. */
[key: string]: any [key: string]: any,
} }
/** Kernel information. */ /** Kernel information. */
export interface KernelSpec { export interface KernelSpec {
/** Name of the kernel specification. */ /** Name of the kernel specification. */
name: string name: string,
/** Name to display in UI. */ /** Name to display in UI. */
display_name: string display_name: string,
/** Extra properties. */ /** Extra properties. */
[key: string]: any [key: string]: any,
} }
/** Kernel information. */ /** Kernel information. */
export interface LanguageInfo { export interface LanguageInfo {
/** The programming language which this kernel runs. */ /** The programming language which this kernel runs. */
name: string name: string,
/** The codemirror mode to use for code in this language. */ /** The codemirror mode to use for code in this language. */
codemirror_mode?: string | { [k: string]: any } codemirror_mode?: string | { [k: string]: any },
/** The file extension for files in this language. */ /** The file extension for files in this language. */
file_extension?: string file_extension?: string,
/** The mimetype corresponding to files in this language. */ /** The mimetype corresponding to files in this language. */
mimetype?: string mimetype?: string,
/** The pygments lexer to use for code in this language. */ /** The pygments lexer to use for code in this language. */
pygments_lexer?: string pygments_lexer?: string,
/** Extra properties. */ /** Extra properties. */
[key: string]: any [key: string]: any,
} }
@ -90,46 +89,46 @@ export enum CellType {
interface BaseCell { interface BaseCell {
/** String identifying the type of cell. */ /** String identifying the type of cell. */
cell_type: CellType cell_type: CellType,
/** Cell-level metadata. */ /** Cell-level metadata. */
metadata: CellMetadata metadata: CellMetadata,
/** Contents of the cell, represented as an array of lines. */ /** Contents of the cell, represented as an array of lines. */
source: MultilineString source: MultilineString,
} }
/** Notebook raw nbconvert cell. */ /** Notebook raw nbconvert cell. */
export interface RawCell extends BaseCell { export interface RawCell extends BaseCell {
/** String identifying the type of cell. */ /** String identifying the type of cell. */
cell_type: CellType.Raw cell_type: CellType.Raw,
/** Cell-level metadata. */ /** Cell-level metadata. */
metadata: CellMetadata & { metadata: CellMetadata & {
/** Raw cell metadata format for nbconvert. */ /** Raw cell metadata format for nbconvert. */
format?: string, format?: string,
} },
/** Media attachments (e.g. inline images), stored as mimebundle keyed by filename. */ /** Media attachments (e.g. inline images), stored as mimebundle keyed by filename. */
attachments?: MediaAttachments attachments?: MediaAttachments,
} }
/** Notebook markdown cell. */ /** Notebook markdown cell. */
export interface MarkdownCell extends BaseCell { export interface MarkdownCell extends BaseCell {
/** String identifying the type of cell. */ /** String identifying the type of cell. */
cell_type: CellType.Markdown cell_type: CellType.Markdown,
/** Media attachments (e.g. inline images), stored as mimebundle keyed by filename. */ /** Media attachments (e.g. inline images), stored as mimebundle keyed by filename. */
attachments?: MediaAttachments attachments?: MediaAttachments,
} }
/** Notebook code cell. */ /** Notebook code cell. */
export interface CodeCell extends BaseCell { export interface CodeCell extends BaseCell {
/** String identifying the type of cell. */ /** String identifying the type of cell. */
cell_type: CellType.Code cell_type: CellType.Code,
/** Cell-level metadata. */ /** Cell-level metadata. */
metadata: CellMetadata & { metadata: CellMetadata & {
@ -139,38 +138,38 @@ export interface CodeCell extends BaseCell {
/** Whether the cell's output is scrolled, unscrolled, or autoscrolled. */ /** Whether the cell's output is scrolled, unscrolled, or autoscrolled. */
scrolled?: true | false | 'auto', scrolled?: true | false | 'auto',
} },
/** Execution, display, or stream outputs. */ /** Execution, display, or stream outputs. */
outputs: Output[] outputs: Output[],
/** The code cell's prompt number. Will be null if the cell has not been run. */ /** The code cell's prompt number. Will be null if the cell has not been run. */
execution_count: number | null execution_count: number | null,
} }
export interface CellMetadata { export interface CellMetadata {
/** Official Jupyter Metadata for Raw Cells */ /** Official Jupyter Metadata for Raw Cells */
jupyter?: { [k: string]: any } jupyter?: { [k: string]: any },
/** /**
* The cell's name. If present, must be a non-empty string. Cell names are expected to be unique * The cell's name. If present, must be a non-empty string. Cell names are expected to be unique
* across all the cells in a given notebook. This criterion cannot be checked by the json schema * across all the cells in a given notebook. This criterion cannot be checked by the json schema
* and must be established by an additional check. * and must be established by an additional check.
*/ */
name?: string name?: string,
/** The cell's tags. Tags must be unique, and must not contain commas. */ /** The cell's tags. Tags must be unique, and must not contain commas. */
tags?: string[] tags?: string[],
/** Extra properties. */ /** Extra properties. */
[key: string]: any [key: string]: any,
} }
export interface MediaAttachments { export interface MediaAttachments {
/** The attachment's data stored as a mimebundle. */ /** The attachment's data stored as a mimebundle. */
[filename: string]: MimeBundle [filename: string]: MimeBundle,
} }
@ -189,62 +188,62 @@ export enum OutputType {
export interface ExecuteResult { export interface ExecuteResult {
/** Type of cell output. */ /** Type of cell output. */
output_type: OutputType.ExecuteResult output_type: OutputType.ExecuteResult,
/** A result's prompt number. */ /** A result's prompt number. */
execution_count: number | null execution_count: number | null,
/** A mime-type keyed dictionary of data */ /** A mime-type keyed dictionary of data */
data: MimeBundle data: MimeBundle,
/** Cell output metadata. */ /** Cell output metadata. */
metadata: { metadata: {
[k: string]: any, [k: string]: any,
} },
} }
/** Data displayed as a result of code cell execution. */ /** Data displayed as a result of code cell execution. */
export interface DisplayData { export interface DisplayData {
/** Type of cell output. */ /** Type of cell output. */
output_type: OutputType.DisplayData output_type: OutputType.DisplayData,
/** A mime-type keyed dictionary of data */ /** A mime-type keyed dictionary of data */
data: MimeBundle data: MimeBundle,
/** Cell output metadata. */ /** Cell output metadata. */
metadata: { metadata: {
[k: string]: any, [k: string]: any,
} },
} }
/** Stream output from a code cell. */ /** Stream output from a code cell. */
export interface StreamOutput { export interface StreamOutput {
/** Type of cell output. */ /** Type of cell output. */
output_type: OutputType.Stream output_type: OutputType.Stream,
/** The name of the stream (stdout, stderr). */ /** The name of the stream (stdout, stderr). */
name: string name: string,
/** The stream's text output, represented as an array of strings. */ /** The stream's text output, represented as an array of strings. */
text: MultilineString text: MultilineString,
} }
/** Output of an error that occurred during code cell execution. */ /** Output of an error that occurred during code cell execution. */
export interface ErrorOutput { export interface ErrorOutput {
/** Type of cell output. */ /** Type of cell output. */
output_type: OutputType.Error output_type: OutputType.Error,
/** The name of the error. */ /** The name of the error. */
ename: string ename: string,
/** The value, or message, of the error. */ /** The value, or message, of the error. */
evalue: string evalue: string,
/** The error's traceback, represented as an array of strings. */ /** The error's traceback, represented as an array of strings. */
traceback: string[] traceback: string[],
} }
@ -253,7 +252,7 @@ export interface ErrorOutput {
export interface MimeBundle { export interface MimeBundle {
/** mimetype output (e.g. text/plain), represented as either an array of strings or a string. */ /** mimetype output (e.g. text/plain), represented as either an array of strings or a string. */
[mediaType: string]: MultilineString [mediaType: string]: MultilineString,
} }
export type MultilineString = string | string[] export type MultilineString = string | string[]

View file

@ -1,6 +1,6 @@
// This code is originally based on notebookjs 0.4.2 distributed under the MIT license. // This code is originally based on notebookjs 0.4.2 distributed under the MIT license.
import { ElementCreator } from './elementCreator' import { ElementCreator } from './elementCreator'
import { CallableInstance, escapeHTML, identity } from './internal/utils' import { callableObject, escapeHTML, identity } from './internal/utils'
import { import {
Cell, Cell,
CellType, CellType,
@ -17,18 +17,18 @@ import {
} from './nbformat' } from './nbformat'
export type NbRendererOpts<TElement = HTMLElement> = { export type Options<TElement = HTMLElement> = {
/** /**
* An object with additional data renderers indexed by a media type. * An object with additional data renderers indexed by a media type.
*/ */
dataRenderers?: DataRenderers<TElement>, dataRenderers?: DataRenderers<TElement>,
/** /**
* An array of the supported MIME types in the priority order. When a cell * An array of the supported media types in the priority order. When a cell
* contains multiple representations of the data, the one with the media type * contains multiple representations of the data, the one with the media type
* that has the lowest index in this array will be rendered. The default is * that has the lowest index in this array will be rendered. The default is
* `Object.keys({ ...dataRenderers, ...builtinRenderers })`. * `Object.keys({ ...dataRenderers, ...builtinRenderers })`.
*/ */
dataTypesPriority?: string[], dataRenderersOrder?: string[],
/** /**
* A function for converting ANSI escape sequences in the given *text* to HTML. * A function for converting ANSI escape sequences in the given *text* to HTML.
* It gets the text from the cell as-is, without prior escaping, so it must * It gets the text from the cell as-is, without prior escaping, so it must
@ -48,7 +48,7 @@ export type NbRendererOpts<TElement = HTMLElement> = {
markdownRenderer?: (markup: string) => string, markdownRenderer?: (markup: string) => string,
} }
export type DataRenderer<TElement = HTMLElement> = (this: NbRenderer<TElement> | void, data: string) => TElement export type DataRenderer<TElement = HTMLElement> = (data: string) => TElement
type DataRenderers<TElement> = { [mediaType: string]: DataRenderer<TElement> } type DataRenderers<TElement> = { [mediaType: string]: DataRenderer<TElement> }
@ -83,173 +83,155 @@ function executionCountAttrs ({ execution_count: count }: CodeCell): { [k: strin
} }
function notebookLanguage ({ metadata: meta }: Notebook): string { function notebookLanguage ({ metadata: meta }: Notebook): string {
return meta.language_info?.name ?? 'python' return (meta.language_info && meta.language_info.name) || 'python'
} }
class NbRenderer <TElement> extends CallableInstance<NbRenderer<TElement>> {
readonly el: ElementCreator<TElement>
readonly renderMarkdown: NonNullable<NbRendererOpts['markdownRenderer']>
readonly renderAnsiCodes: NonNullable<NbRendererOpts['ansiCodesRenderer']>
readonly highlightCode: NonNullable<NbRendererOpts['codeHighlighter']>
readonly dataRenderers: DataRenderers<TElement>
readonly dataTypesPriority: string[]
/** /**
* Creates a Notebook renderer with the given options. The constructed object * Builds a Notebook renderer function with the given options. It returns a
* is "callable", i.e. you can treat it as a function. * "callable object" that exposes a renderer function for each of the
* * Notebook's AST node. You can easily replace any of the functions to modify
* @example * behaviour of the renderer.
* const renderer = new NbRenderer(document.createElement.bind(document))
* console.log(renderer(notebook).outerHTML)
* *
* @param {ElementCreator} elementCreator The function that will be used for * @param {ElementCreator} elementCreator The function that will be used for
* building all HTML elements. * building all HTML elements.
* @param {NbRendererOpts} opts The renderer's options. * @param {Options} opts
* @return {NbRenderer}
* @template TElement Type of the element object that *elementCreator* produces.
*/ */
constructor (elementCreator: ElementCreator<TElement>, opts: NbRendererOpts<TElement> = {}) { function buildRenderer <TElement> (elementCreator: ElementCreator<TElement>, opts: Options<TElement> = {}) {
super() const renderMarkdown = opts.markdownRenderer || identity
const renderAnsiCodes = opts.ansiCodesRenderer || escapeHTML
const highlightCode = opts.codeHighlighter || escapeHTML
this.el = elementCreator const el = elementCreator
this.renderMarkdown = opts.markdownRenderer ?? identity const el2 = (tag: string, classes: string[]) => (data: string) => el(tag, classes, data)
this.renderAnsiCodes = opts.ansiCodesRenderer ?? escapeHTML
this.highlightCode = opts.codeHighlighter ?? escapeHTML
const el2 = (tag: string, classes: string[]) => (data: string) => this.el(tag, classes, data) const embeddedImageEl = (format: string) => (data: string) => el('img', {
const embeddedImageEl = (format: string) => (data: string) => this.el('img', {
class: 'image-output', class: 'image-output',
src: `data:image/${format};base64,${data.replace(/\n/g, '')}`, src: `data:image/${format};base64,${data.replace(/\n/g, '')}`,
}) })
// opts.dataRenderers is intentionally included twice; to get the user's // opts.dataRenderers is intentionally included twice; to get the user's
// provided renderers in the default dataTypesPriority before the built-in // provided renderers in the default dataRenderersOrder before the built-in
// renderers and at the same time allow to override any built-in renderer. // renderers and at the same time allow to override any built-in renderer.
this.dataRenderers = { const dataRenderers: DataRenderers<TElement> = {
...opts.dataRenderers, ...opts.dataRenderers,
'image/png': embeddedImageEl('png'), 'image/png': embeddedImageEl('png'),
'image/jpeg': embeddedImageEl('jpeg'), 'image/jpeg': embeddedImageEl('jpeg'),
'image/svg+xml': el2('div', ['svg-output']), 'image/svg+xml': el2('div', ['svg-output']),
'text/svg+xml': (data) => this.dataRenderers['image/svg+xml'].call(this, data), 'text/svg+xml': (data) => dataRenderers['image/svg+xml'](data),
'text/html': el2('div', ['html-output']), 'text/html': el2('div', ['html-output']),
'text/markdown': (data) => this.el('div', ['html-output'], this.renderMarkdown(data)), 'text/markdown': (data) => dataRenderers['text/html'](renderMarkdown(data)),
'text/latex': el2('div', ['latex-output']), 'text/latex': el2('div', ['latex-output']),
'application/javascript': el2('script', []), 'application/javascript': el2('script', []),
'text/plain': (data) => this.el('pre', ['text-output'], escapeHTML(data)), 'text/plain': (data) => el('pre', ['text-output'], escapeHTML(data)),
...opts.dataRenderers, ...opts.dataRenderers,
} }
this.dataTypesPriority = opts.dataTypesPriority ?? Object.keys(this.dataRenderers) const dataRenderersOrder = opts.dataRenderersOrder || Object.keys(dataRenderers)
const resolveDataType = (output: DisplayData | ExecuteResult) => {
return dataRenderersOrder.find(type => output.data[type] && dataRenderers[type])
} }
/** const r = callableObject('Notebook', {
* Renders the given Jupyter *notebook*. Notebook: (notebook: Notebook): TElement => {
*/ const children = notebook.cells.map(cell => r.Cell(cell, notebook))
__call__ (notebook: Notebook): TElement { // Class "worksheet" is for backward compatibility with notebook.js.
return this.render(notebook) return el('div', ['notebook', 'worksheet'], children)
} },
/** Cell: (cell: Cell, notebook: Notebook): TElement => {
* Renders the given Jupyter *notebook*.
*/
render (notebook: Notebook): TElement {
const children = notebook.cells.map(cell => this.renderCell(cell, notebook))
return this.el('div', ['notebook'], children)
}
renderCell (cell: Cell, notebook: Notebook): TElement {
switch (cell.cell_type) { switch (cell.cell_type) {
case CellType.Code: return this.renderCodeCell(cell, notebook) case CellType.Code: return r.CodeCell(cell, notebook)
case CellType.Markdown: return this.renderMarkdownCell(cell, notebook) case CellType.Markdown: return r.MarkdownCell(cell, notebook)
case CellType.Raw: return this.renderRawCell(cell, notebook) case CellType.Raw: return r.RawCell(cell, notebook)
default: return this.el('div', [], '<!-- Unsupported cell type -->') default: return el('div', [], '<!-- Unsupported cell type -->')
}
} }
},
renderMarkdownCell (cell: MarkdownCell, _notebook: Notebook): TElement { MarkdownCell: (cell: MarkdownCell, _notebook: Notebook): TElement => {
return this.el('section', ['cell', 'markdown-cell'], this.renderMarkdown(joinText(cell.source))) return el('div', ['cell', 'markdown-cell'], renderMarkdown(joinText(cell.source)))
} },
renderRawCell (cell: RawCell, _notebook: Notebook): TElement { RawCell: (cell: RawCell, _notebook: Notebook): TElement => {
return this.el('section', ['cell', 'raw-cell'], joinText(cell.source)) return el('div', ['cell', 'raw-cell'], joinText(cell.source))
} },
renderCodeCell (cell: CodeCell, notebook: Notebook): TElement { CodeCell: (cell: CodeCell, notebook: Notebook): TElement => {
const source = cell.source.length > 0 const source = cell.source.length > 0
? this.renderSource(cell, notebook) ? r.Source(cell, notebook)
: this.el('div') : el('div')
const outputs = coalesceStreams(cell.outputs ?? []) const outputs = coalesceStreams(cell.outputs || [])
.map(output => this.renderOutput(output, cell)) .map(output => r.Output(output, cell))
return this.el('section', ['cell', 'code-cell'], [source, ...outputs]) return el('div', ['cell', 'code-cell'], [source, ...outputs])
} },
renderSource (cell: CodeCell, notebook: Notebook): TElement { Source: (cell: CodeCell, notebook: Notebook): TElement => {
const lang = notebookLanguage(notebook) const lang = notebookLanguage(notebook)
const html = this.highlightCode(joinText(cell.source), lang) const html = highlightCode(joinText(cell.source), lang)
const codeEl = this.el('code', { 'class': `lang-${lang}`, 'data-language': lang }, html) const codeEl = el('code', { 'class': `lang-${lang}`, 'data-language': lang }, html)
const preEl = this.el('pre', [], [codeEl]) const preEl = el('pre', [], [codeEl])
// Class "input" is for backward compatibility with notebook.js. // Class "input" is for backward compatibility with notebook.js.
const attrs = { ...executionCountAttrs(cell), class: 'source input' } const attrs = { ...executionCountAttrs(cell), class: 'source input' }
return this.el('div', attrs, [preEl]) return el('div', attrs, [preEl])
} },
renderOutput (output: Output, cell: CodeCell): TElement { Output: (output: Output, cell: CodeCell): TElement => {
const innerEl = (() => { const innerEl = (() => {
switch (output.output_type) { switch (output.output_type) {
case OutputType.DisplayData: return this.renderDisplayData(output) case OutputType.DisplayData: return r.DisplayData(output)
case OutputType.ExecuteResult: return this.renderExecuteResult(output) case OutputType.ExecuteResult: return r.ExecuteResult(output)
case OutputType.Stream: return this.renderStream(output) case OutputType.Stream: return r.Stream(output)
case OutputType.Error: return this.renderError(output) case OutputType.Error: return r.Error(output)
default: return this.el('div', [], '<!-- Unsupported output type -->') default: return el('div', [], '<!-- Unsupported output type -->')
} }
})() })()
const attrs = { ...executionCountAttrs(cell), class: 'output' } const attrs = { ...executionCountAttrs(cell), class: 'output' }
return this.el('div', attrs, [innerEl]) return el('div', attrs, [innerEl])
} },
renderDisplayData (output: DisplayData): TElement { DisplayData: (output: DisplayData): TElement => {
const type = this.resolveDataType(output) const type = resolveDataType(output)
if (type) { if (type) {
return this.renderData(type, joinText(output.data[type])) return dataRenderers[type](joinText(output.data[type]))
}
return this.el('div', ['empty-output'])
} }
return el('div', ['empty-output'])
},
renderExecuteResult (output: ExecuteResult): TElement { ExecuteResult: (output: ExecuteResult): TElement => {
const type = this.resolveDataType(output) const type = resolveDataType(output)
if (type) { if (type) {
return this.renderData(type, joinText(output.data[type])) return dataRenderers[type](joinText(output.data[type]))
}
return this.el('div', ['empty-output'])
} }
return el('div', ['empty-output'])
},
renderError (error: ErrorOutput): TElement { Error: (error: ErrorOutput): TElement => {
const html = this.renderAnsiCodes(error.traceback.join('\n')) const html = renderAnsiCodes(error.traceback.join('\n'))
// Class "pyerr" is for backward compatibility with notebook.js. // Class "pyerr" is for backward compatibility with notebook.js.
return this.el('pre', ['error', 'pyerr'], html) return el('pre', ['error', 'pyerr'], html)
},
Stream: (stream: StreamOutput): TElement => {
const html = renderAnsiCodes(joinText(stream.text))
return el('pre', [stream.name], html)
},
})
return r
} }
renderStream (stream: StreamOutput): TElement { export default buildRenderer
const html = this.renderAnsiCodes(joinText(stream.text))
return this.el('pre', [stream.name], html)
}
renderData (mimeType: string, data: string): TElement { // XXX: An ugly hack to infer return type of generic function that returns
const render = this.dataRenderers[mimeType] // generalized object.
if (!render) { abstract class DummyClass<T> {
throw RangeError(`missing renderer for MIME type: ${mimeType}`) renderer = buildRenderer<T>(this.elementCreator())
abstract elementCreator (): ElementCreator<T>
} }
return render.call(this, data) export type NbRenderer<TElement> = DummyClass<TElement>['renderer']
}
resolveDataType (output: DisplayData | ExecuteResult): string | undefined {
return this.dataTypesPriority.find(type => output.data[type])
}
}
export default NbRenderer

View file

@ -1,6 +1,5 @@
import { $INLINE_JSON } from 'ts-transformer-inline-file' import { $INLINE_JSON } from 'ts-transformer-inline-file'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { version } = $INLINE_JSON('../package.json') const { version } = $INLINE_JSON('../package.json')
export default version as string export default version as string

View file

@ -1,64 +1,31 @@
import { CallableInstance, escapeHTML, identity } from '@/internal/utils' import { callableObject, escapeHTML, identity } from '@/internal/utils'
describe('CallableInstance', () => { describe('.callableObject', () => {
class FixtureCallable extends CallableInstance<FixtureCallable> { describe('returned value', () => {
readonly salutation: string const template = {
str: 'allons-y!',
constructor (salutation: string) { func1: jest.fn().mockReturnValue(1),
super() func2: jest.fn().mockReturnValue(2),
this.salutation = salutation
} }
const subject = callableObject('func1', template)
__call__ (name: string) { it('is a function that calls the specified template function', () => {
return this.salute(name) expect( subject ).toBeInstanceOf(Function)
} expect( subject('a', 'b') ).toBe(1)
expect( template.func1 ).toBeCalledWith('a', 'b')
salute (name: string) {
return `${this.salutation}, ${name}!`
}
}
describe('subclass', () => {
it('can be instantiated using new', () => {
expect(() => new FixtureCallable('Hello') ).not.toThrow()
})
}) })
describe('subclass instance', () => { it('is not the same function as the specified template function', () => {
let instance: FixtureCallable expect( subject ).not.toBe(template.func1)
beforeEach(() => {
instance = new FixtureCallable('Hello')
}) })
it('is an instance of its class', () => { it('has all enumerable properties of the given template', () => {
expect( instance ).toBeInstanceOf(FixtureCallable) expect( subject )
expect( instance.salutation ).toBe('Hello') .toHaveProperty('str', template.str)
expect( instance.salute('world') ).toBe('Hello, world!') .toHaveProperty('func1', template.func1)
}) .toHaveProperty('func2', template.func2)
it('is an instance of Function', () => {
expect( instance ).toBeInstanceOf(Function)
})
it('is a typeof function', () => {
expect( typeof instance ).toBe('function')
})
it('has function property "name" that equals the class name', () => {
expect( instance.name ).toBe('FixtureCallable')
})
it('has function property "length" that equals number of arguments of the __call__ method', () => {
expect( instance.length ).toBe(1)
})
it('can be called, redirects to the method __call__', () => {
expect( instance('world') ).toBe('Hello, world!')
expect( instance.apply(null, ['world']) ).toBe('Hello, world!') // eslint-disable-line no-useless-call
expect( instance.call(null, 'world') ).toBe('Hello, world!') // eslint-disable-line no-useless-call
}) })
}) })
}) })

View file

@ -20,7 +20,7 @@ describe('.extractMath', () => {
it('extracts and substitutes math expression in the given text', () => { it('extracts and substitutes math expression in the given text', () => {
expect( expect(
extractMath(`Let's define ${raw}.`), extractMath(`Let's define ${raw}.`)
).toEqual(["Let's define @@1@@.", [{ displayMode, raw, value }]]) ).toEqual(["Let's define @@1@@.", [{ displayMode, raw, value }]])
}) })
}) })
@ -31,7 +31,7 @@ describe('.extractMath', () => {
it('extracts and substitutes math expression in the given text', () => { it('extracts and substitutes math expression in the given text', () => {
expect( expect(
extractMath(`Let's define ${raw}.`), extractMath(`Let's define ${raw}.`)
).toEqual(["Let's define @@1@@.", [{ displayMode, raw, value }]]) ).toEqual(["Let's define @@1@@.", [{ displayMode, raw, value }]])
}) })
}) })
@ -43,7 +43,7 @@ describe('.extractMath', () => {
it('extracts and substitutes math expression in the given text', () => { it('extracts and substitutes math expression in the given text', () => {
expect( expect(
extractMath(`Let's define ${raw}.`), extractMath(`Let's define ${raw}.`)
).toEqual(["Let's define @@1@@.", [{ displayMode: true, raw, value: raw }]]) ).toEqual(["Let's define @@1@@.", [{ displayMode: true, raw, value: raw }]])
}) })
}) })
@ -52,14 +52,14 @@ describe('.extractMath', () => {
it('escapes @@[0-9]+@@ as @@0[0-9]+@@', () => { it('escapes @@[0-9]+@@ as @@0[0-9]+@@', () => {
expect( expect(
extractMath('This @@02@@ is not our marker'), extractMath('This @@02@@ is not our marker')
).toEqual(['This @@002@@ is not our marker', []]) ).toEqual(['This @@002@@ is not our marker', []])
}) })
}) })
it('ignores math delimiters inside `inline code`', () => { it('ignores math delimiters inside `inline code`', () => {
expect( expect(
extractMath('`$x$` and ``$`x`$`` is a code, $x$ is not'), extractMath('`$x$` and ``$`x`$`` is a code, $x$ is not')
).toEqual([ ).toEqual([
'`$x$` and ``$`x`$`` is a code, @@1@@ is not', '`$x$` and ``$`x`$`` is a code, @@1@@ is not',
[{ displayMode: false, raw: '$x$', value: 'x' }], [{ displayMode: false, raw: '$x$', value: 'x' }],
@ -68,7 +68,7 @@ describe('.extractMath', () => {
it('ignores math delimiters inside `inline code` with line breaks', () => { it('ignores math delimiters inside `inline code` with line breaks', () => {
expect( expect(
extractMath('`$x\n$` and ``\n$`x`$\n`` is a code, `$x$\n\nis` not'), extractMath('`$x\n$` and ``\n$`x`$\n`` is a code, `$x$\n\nis` not')
).toEqual([ ).toEqual([
'`$x\n$` and ``\n$`x`$\n`` is a code, `@@1@@\n\nis` not', '`$x\n$` and ``\n$`x`$\n`` is a code, `@@1@@\n\nis` not',
[{ displayMode: false, raw: '$x$', value: 'x' }], [{ displayMode: false, raw: '$x$', value: 'x' }],
@ -135,13 +135,13 @@ describe('.restoreMath', () => {
repl[21] = 'second' repl[21] = 'second'
expect( expect(
restoreMath("Let's define @@1@@ and @@22@@.", repl), restoreMath("Let's define @@1@@ and @@22@@.", repl)
).toEqual("Let's define first and second.") ).toEqual("Let's define first and second.")
}) })
it('unescapes marker-like sequences', () => { it('unescapes marker-like sequences', () => {
expect( expect(
restoreMath('This @@001@@ is not our marker, nor @@01@@, but @@1@@ is.', ['this one']), restoreMath('This @@001@@ is not our marker, nor @@01@@, but @@1@@ is.', ['this one'])
).toEqual('This @@01@@ is not our marker, nor @@1@@, but this one is.') ).toEqual('This @@01@@ is not our marker, nor @@1@@, but this one is.')
}) })
}) })

View file

@ -1,20 +1,19 @@
/* eslint-disable @typescript-eslint/unbound-method */
import '~/test/setup' // setupFilesAfterEnv doesn't work here import '~/test/setup' // setupFilesAfterEnv doesn't work here
import arrify from 'arrify' import arrify from 'arrify'
import { Document, HTMLElement } from 'nodom' import { Document, HTMLElement } from 'nodom'
import buildElementCreator from '@/elementCreator' import buildElementCreator from '@/elementCreator'
import NbRenderer, { NbRendererOpts } from '@/renderer' import buildRenderer, { NbRenderer, Options as RendererOpts } from '@/renderer'
import { DisplayData, MimeBundle, MultilineString, Notebook } from '@/nbformat' import { DisplayData, MimeBundle, MultilineString, Notebook } from '@/nbformat'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Anything } from '~/test/support/matchers/toMatchElement' import { Anything } from '~/test/support/matchers/toMatchElement'
import { mockLastResult, mockResults } from '~/test/support/helpers' import { mockLastResult, mockResults } from '~/test/support/helpers'
import * as fixtures from './support/fixtures/notebook' import * as _fixtures from './support/fixtures/notebook'
const document = new Document() const document = new Document()
const fixtures = _fixtures // workaround to allow indexed access
describe('built renderer', () => { describe('built renderer', () => {
@ -28,7 +27,7 @@ describe('built renderer', () => {
const dataRenderers = { const dataRenderers = {
'text/custom': rendererMock('DisplayData'), 'text/custom': rendererMock('DisplayData'),
} }
const rendererOpts: NbRendererOpts<HTMLElement> = { const rendererOpts: RendererOpts<HTMLElement> = {
ansiCodesRenderer, ansiCodesRenderer,
codeHighlighter, codeHighlighter,
markdownRenderer, markdownRenderer,
@ -38,46 +37,45 @@ describe('built renderer', () => {
beforeEach(() => { beforeEach(() => {
renderer = new NbRenderer(elementCreator, rendererOpts) renderer = buildRenderer(elementCreator, rendererOpts)
}) })
describe('.render', () => { describe('.Notebook', () => {
beforeEach(() => { beforeEach(() => {
renderer.renderCell = rendererMock('Cell') renderer.Cell = rendererMock('Cell')
}) })
it('returns div.notebook', () => { it('returns div.notebook.worksheet', () => {
expect( renderer.render({ ...notebook, cells: [] }) ).toHtmlEqual( expect( renderer.Notebook({ ...notebook, cells: [] }) ).toHtmlEqual(
<div class="notebook"></div> <div class="notebook worksheet"></div>
) )
}) })
it('returns element with $cells converted using .renderCell() as the children', () => { it('returns element with $cells converted using .Cell() as the children', () => {
const result = renderer.render(notebook) const result = renderer.Notebook(notebook)
notebook.cells.forEach((cell, idx) => { notebook.cells.forEach((cell, idx) => {
expect( renderer.renderCell ).toHaveBeenNthCalledWith(idx + 1, cell, notebook) expect( renderer.Cell ).toHaveBeenNthCalledWith(idx + 1, cell, notebook)
}) })
expect( result.children ).toHtmlEqual(mockResults(renderer.renderCell)) expect( result.children ).toHtmlEqual(mockResults(renderer.Cell))
}) })
}) })
describe('.renderCell', () => { describe('.Cell', () => {
describe.each([ describe.each([
'renderCodeCell', 'renderMarkdownCell', 'renderRawCell', 'CodeCell', 'MarkdownCell', 'RawCell',
] as const)('with %s', (funcName) => { ] as const)('with %s', (type) => {
const type = funcName.replace('render', '') const cell = fixtures[type]
const cell = (fixtures as any)[type]
it(`returns result of calling .${funcName}() with the given cell`, () => { it(`returns result of calling .${type}() with the given cell`, () => {
const expected = stubElement(type) const expected = stubElement(type)
const rendererFunc = (renderer as any)[funcName] = jest.fn(() => expected) renderer[type] = jest.fn(() => expected)
expect( renderer.renderCell(cell, notebook) ).toBe(expected) expect( renderer.Cell(cell, notebook) ).toBe(expected)
expect( rendererFunc ).toBeCalledWith(cell, notebook) expect( renderer[type] ).toBeCalledWith(cell, notebook)
}) })
}) })
@ -88,7 +86,7 @@ describe('built renderer', () => {
} as any } as any
it('returns div with comment "Unsupported cell type"', () => { it('returns div with comment "Unsupported cell type"', () => {
expect( renderer.renderCell(cell, notebook) ).toHtmlEqual( expect( renderer.Cell(cell, notebook) ).toHtmlEqual(
<div> <div>
{{__html: '<!-- Unsupported cell type -->' }} {{__html: '<!-- Unsupported cell type -->' }}
</div> </div>
@ -98,15 +96,15 @@ describe('built renderer', () => {
}) })
describe('.renderMarkdownCell', () => { describe('.MarkdownCell', () => {
eachMultilineVariant(fixtures.MarkdownCell, 'source', (cell) => { eachMultilineVariant(fixtures.MarkdownCell, 'source', (cell) => {
const source = join(cell.source) const source = join(cell.source)
it('returns section.cell.markdown-cell with the $source converted using markdownRenderer() as content', () => { it('returns div.cell.markdown-cell with the $source converted using markdownRenderer() as content', () => {
expect( renderer.renderMarkdownCell(cell, notebook) ).toHtmlEqual( expect( renderer.MarkdownCell(cell, notebook) ).toHtmlEqual(
<section class="cell markdown-cell"> <div class="cell markdown-cell">
{{__html: mockLastResult(markdownRenderer) }} {{__html: mockLastResult(markdownRenderer) }}
</section> </div>
) )
expect( markdownRenderer ).toBeCalledWith(source) expect( markdownRenderer ).toBeCalledWith(source)
}) })
@ -114,48 +112,48 @@ describe('built renderer', () => {
}) })
describe('.renderRawCell', () => { describe('.RawCell', () => {
eachMultilineVariant(fixtures.RawCell, 'source', (cell) => { eachMultilineVariant(fixtures.RawCell, 'source', (cell) => {
it('returns section.cell.raw-cell with the $source as content', () => { it('returns div.cell.raw-cell with the $source as content', () => {
expect( renderer.renderRawCell(cell, notebook) ).toHtmlEqual( expect( renderer.RawCell(cell, notebook) ).toHtmlEqual(
<section class="cell raw-cell"> <div class="cell raw-cell">
{{__html: join(cell.source) }} {{__html: join(cell.source) }}
</section> </div>
) )
}) })
}) })
}) })
describe('.renderCodeCell', () => { describe('.CodeCell', () => {
const cell = fixtures.CodeCell const cell = fixtures.CodeCell
let result: HTMLElement let result: HTMLElement
beforeEach(() => { beforeEach(() => {
renderer.renderSource = rendererMock('Source') renderer.Source = rendererMock('Source')
renderer.renderOutput = rendererMock('Output') renderer.Output = rendererMock('Output')
result = renderer.renderCodeCell(cell, notebook) result = renderer.CodeCell(cell, notebook)
}) })
it('returns section.cell.code-cell', () => { it('returns div.cell.code-cell', () => {
expect( result ).toMatchElement( expect( result ).toMatchElement(
<section class="cell code-cell"><Anything /></section> <div class="cell code-cell"><Anything /></div>
) )
}) })
describe('with non-empty $source', () => { describe('with non-empty $source', () => {
it('returns element with $source rendered using .renderSource() as children[0]', () => { it('returns element with $source rendered using .Source() as children[0]', () => {
expect( renderer.renderSource ).toBeCalledWith(cell, notebook) expect( renderer.Source ).toBeCalledWith(cell, notebook)
expect( result.children[0] ).toHtmlEqual(mockLastResult(renderer.renderSource)!) expect( result.children[0] ).toHtmlEqual(mockLastResult(renderer.Source))
}) })
}) })
describe('with empty $source', () => { describe('with empty $source', () => {
beforeEach(() => { beforeEach(() => {
result = renderer.renderCodeCell({ ...cell, source: [] }, notebook) result = renderer.CodeCell({ ...cell, source: [] }, notebook)
}) })
it('returns element with empty div as children[0]', () => { it('returns element with empty div as children[0]', () => {
@ -165,22 +163,22 @@ describe('built renderer', () => {
}) })
}) })
it('returns element with $outputs rendered using .renderOutput() as children[1+]', () => { it('returns element with $outputs rendered using .Output() as children[1+]', () => {
cell.outputs.forEach((output, idx) => { cell.outputs.forEach((output, idx) => {
expect( renderer.renderOutput ).toHaveBeenNthCalledWith(idx + 1, output, cell) expect( renderer.Output ).toHaveBeenNthCalledWith(idx + 1, output, cell)
}) })
expect( result.children.slice(1) ).toHtmlEqual(mockResults(renderer.renderOutput)) expect( result.children.slice(1) ).toHtmlEqual(mockResults(renderer.Output))
}) })
}) })
describe('.renderSource', () => { describe('.Source', () => {
const cell = fixtures.CodeCell const cell = fixtures.CodeCell
const notebookLang = notebook.metadata.language_info!.name const notebookLang = notebook.metadata.language_info!.name
let result: HTMLElement let result: HTMLElement
beforeEach(() => { beforeEach(() => {
result = renderer.renderSource(cell, notebook) result = renderer.Source(cell, notebook)
}) })
it('returns div > pre > code', () => { it('returns div > pre > code', () => {
@ -208,7 +206,7 @@ describe('built renderer', () => {
const myCell = { ...cell, execution_count: 2 } const myCell = { ...cell, execution_count: 2 }
it('has data-execution-count and data-prompt-number attributes', () => { it('has data-execution-count and data-prompt-number attributes', () => {
const result = renderer.renderSource(myCell, notebook) const result = renderer.Source(myCell, notebook)
expect( result.attributes ).toMatchObject({ expect( result.attributes ).toMatchObject({
'data-execution-count': String(myCell.execution_count), 'data-execution-count': String(myCell.execution_count),
@ -221,7 +219,7 @@ describe('built renderer', () => {
const myCell = { ...cell, execution_count: null } const myCell = { ...cell, execution_count: null }
it('has data-execution-count and data-prompt-number attributes', () => { it('has data-execution-count and data-prompt-number attributes', () => {
const result = renderer.renderSource(myCell, notebook) const result = renderer.Source(myCell, notebook)
expect( result.attributes ) expect( result.attributes )
.not.toHaveProperty('data-execution-count') .not.toHaveProperty('data-execution-count')
@ -234,7 +232,7 @@ describe('built renderer', () => {
let codeEl: HTMLElement let codeEl: HTMLElement
beforeEach(() => { beforeEach(() => {
codeEl = renderer.renderSource(cell, notebook).firstChild!.firstChild as HTMLElement codeEl = renderer.Source(cell, notebook).firstChild!.firstChild as HTMLElement
}) })
it("has class lang-<lang> where lang is the notebook's language", () => { it("has class lang-<lang> where lang is the notebook's language", () => {
@ -256,7 +254,7 @@ describe('built renderer', () => {
const notebookLang = 'python' const notebookLang = 'python'
it('uses the default language: python', () => { it('uses the default language: python', () => {
const result = renderer.renderSource(cell, myNotebook) const result = renderer.Source(cell, myNotebook)
const codeEl = result.firstChild!.firstChild as HTMLElement const codeEl = result.firstChild!.firstChild as HTMLElement
expect( codeEl.getAttribute('data-language') ).toBe(notebookLang) expect( codeEl.getAttribute('data-language') ).toBe(notebookLang)
@ -267,20 +265,19 @@ describe('built renderer', () => {
}) })
describe('.renderOutput', () => { describe('.Output', () => {
const cell = { ...fixtures.CodeCell, execution_count: null } const cell = { ...fixtures.CodeCell, execution_count: null }
describe.each([ describe.each([
'renderDisplayData', 'renderExecuteResult', 'renderStream', 'renderError', 'DisplayData', 'ExecuteResult', 'Stream', 'Error',
] as const)('with %s output', (funcName) => { ] as const)('with %s output', (type) => {
const type = funcName.replace('render', '') const output = fixtures[type]
const output = (fixtures as any)[type]
let result: HTMLElement let result: HTMLElement
beforeEach(() => { beforeEach(() => {
renderer[funcName] = rendererMock(type) renderer[type] = rendererMock(type)
result = renderer.renderOutput(output, cell) result = renderer.Output(output, cell)
}) })
it('returns div.output', () => { it('returns div.output', () => {
@ -289,16 +286,16 @@ describe('built renderer', () => {
) )
}) })
it(`returns element with the output rendered using .${funcName}() as the only child`, () => { it(`returns element with the output rendered using .${type}() as the only child`, () => {
expect( renderer[funcName] ).toBeCalledWith(output) expect( renderer[type] ).toBeCalledWith(output)
expect( result.children ).toHtmlEqual([mockLastResult(renderer[funcName])!]) expect( result.children ).toHtmlEqual([mockLastResult(renderer[type])])
}) })
describe('when the cell has non-null execution_count', () => { describe('when the cell has non-null execution_count', () => {
const cell = { ...fixtures.CodeCell, execution_count: 2 } const cell = { ...fixtures.CodeCell, execution_count: 2 }
it('returns element with attributes data-execution-count and data-prompt-number', () => { it('returns element with attributes data-execution-count and data-prompt-number', () => {
const result = renderer.renderOutput(output, cell) const result = renderer.Output(output, cell)
expect( result.attributes ).toMatchObject({ expect( result.attributes ).toMatchObject({
'data-execution-count': String(cell.execution_count), 'data-execution-count': String(cell.execution_count),
@ -320,7 +317,7 @@ describe('built renderer', () => {
} }
it('returns div with comment "Unsupported output type"', () => { it('returns div with comment "Unsupported output type"', () => {
expect( renderer.renderOutput(output, cell) ).toHtmlEqual( expect( renderer.Output(output, cell) ).toHtmlEqual(
<div class="output"> <div class="output">
<div> <div>
{{__html: '<!-- Unsupported output type -->' }} {{__html: '<!-- Unsupported output type -->' }}
@ -332,7 +329,7 @@ describe('built renderer', () => {
}) })
describe('.renderDisplayData', () => { describe('.DisplayData', () => {
function displayDataWith (data: MimeBundle): DisplayData { function displayDataWith (data: MimeBundle): DisplayData {
return { ...fixtures.DisplayData, data } return { ...fixtures.DisplayData, data }
@ -361,7 +358,7 @@ describe('built renderer', () => {
const displayData = displayDataWith({ 'text/non-sense': 'whaat' }) const displayData = displayDataWith({ 'text/non-sense': 'whaat' })
it('returns div.empty-output', () => { it('returns div.empty-output', () => {
expect( renderer.renderDisplayData(displayData) ).toHtmlEqual( expect( renderer.DisplayData(displayData) ).toHtmlEqual(
<div class="empty-output"></div> <div class="empty-output"></div>
) )
}) })
@ -373,7 +370,7 @@ describe('built renderer', () => {
withMimeData(mimeType, ['aW1hZ2Ug\n', 'ZGF0YQ=='], (output) => { withMimeData(mimeType, ['aW1hZ2Ug\n', 'ZGF0YQ=='], (output) => {
it('returns img.image-output with the data in the src attribute', () => { it('returns img.image-output with the data in the src attribute', () => {
expect( renderer.renderDisplayData(output) ).toHtmlEqual( expect( renderer.DisplayData(output) ).toHtmlEqual(
<img class="image-output" src={`data:${mimeType};base64,aW1hZ2UgZGF0YQ==`}></img> <img class="image-output" src={`data:${mimeType};base64,aW1hZ2UgZGF0YQ==`}></img>
) )
}) })
@ -391,7 +388,7 @@ describe('built renderer', () => {
withMimeData(mimeType, '<stub>data</stub>', (output, data) => { withMimeData(mimeType, '<stub>data</stub>', (output, data) => {
it(`returns div${classes.map(x => `.${x}`)} with the data as content`, () => { it(`returns div${classes.map(x => `.${x}`)} with the data as content`, () => {
expect( renderer.renderDisplayData(output) ).toHtmlEqual( expect( renderer.DisplayData(output) ).toHtmlEqual(
<div class={ classes.join(' ') }> <div class={ classes.join(' ') }>
{{__html: join(data) }} {{__html: join(data) }}
</div> </div>
@ -403,7 +400,7 @@ describe('built renderer', () => {
withMimeData('text/markdown', ['Lorem\n', 'ipsum'], (output, data) => { withMimeData('text/markdown', ['Lorem\n', 'ipsum'], (output, data) => {
it('returns div.html-output with the data converted using markdownRenderer() as content', () => { it('returns div.html-output with the data converted using markdownRenderer() as content', () => {
expect( renderer.renderDisplayData(output) ).toHtmlEqual( expect( renderer.DisplayData(output) ).toHtmlEqual(
<div class="html-output"> <div class="html-output">
{{__html: mockLastResult(markdownRenderer) }} {{__html: mockLastResult(markdownRenderer) }}
</div> </div>
@ -415,7 +412,7 @@ describe('built renderer', () => {
withMimeData('text/plain', '>_<', (output) => { withMimeData('text/plain', '>_<', (output) => {
it('returns pre.text-output with html-escaped data', () => { it('returns pre.text-output with html-escaped data', () => {
expect( renderer.renderDisplayData(output) ).toHtmlEqual( expect( renderer.DisplayData(output) ).toHtmlEqual(
<pre class="text-output">{ '>_<' }</pre> <pre class="text-output">{ '>_<' }</pre>
) )
}) })
@ -424,7 +421,7 @@ describe('built renderer', () => {
withMimeData('application/javascript', 'alert("Hello &!")', (output, data) => { withMimeData('application/javascript', 'alert("Hello &!")', (output, data) => {
it('returns script with the data', () => { it('returns script with the data', () => {
expect( renderer.renderDisplayData(output) ).toHtmlEqual( expect( renderer.DisplayData(output) ).toHtmlEqual(
<script>{{__html: join(data) }}</script> <script>{{__html: join(data) }}</script>
) )
}) })
@ -436,8 +433,8 @@ describe('built renderer', () => {
withMimeData('text/custom', 'Lorem ipsum', (output, data) => { withMimeData('text/custom', 'Lorem ipsum', (output, data) => {
it('renders the data using the associated external renderer', () => { it('renders the data using the associated external renderer', () => {
expect( renderer.renderDisplayData(output) ).toHtmlEqual( expect( renderer.DisplayData(output) ).toHtmlEqual(
mockLastResult(dataRenderers['text/custom'])! mockLastResult(dataRenderers['text/custom'])
) )
expect( dataRenderers['text/custom'] ).toBeCalledWith(join(data)) expect( dataRenderers['text/custom'] ).toBeCalledWith(join(data))
}) })
@ -453,7 +450,7 @@ describe('built renderer', () => {
const output = displayDataWith(mimeBundle) const output = displayDataWith(mimeBundle)
it('renders the data of the MIME type with a higher priority', () => { it('renders the data of the MIME type with a higher priority', () => {
expect( renderer.renderDisplayData(output) ).toHtmlEqual( expect( renderer.DisplayData(output) ).toHtmlEqual(
<div class="html-output"> <div class="html-output">
{{__html: mimeBundle['text/html'] }} {{__html: mimeBundle['text/html'] }}
</div> </div>
@ -467,8 +464,8 @@ describe('built renderer', () => {
} }
const output = displayDataWith(mimeBundle) const output = displayDataWith(mimeBundle)
expect( renderer.renderDisplayData(output) ).toHtmlEqual( expect( renderer.DisplayData(output) ).toHtmlEqual(
mockLastResult(dataRenderers['text/custom'])! mockLastResult(dataRenderers['text/custom'])
) )
}) })
}) })
@ -477,7 +474,7 @@ describe('built renderer', () => {
const dataRenderer = rendererMock('DisplayData') const dataRenderer = rendererMock('DisplayData')
beforeEach(() => { beforeEach(() => {
renderer = new NbRenderer(elementCreator, { renderer = buildRenderer(elementCreator, {
...rendererOpts, ...rendererOpts,
dataRenderers: { 'text/plain': dataRenderer }, dataRenderers: { 'text/plain': dataRenderer },
}) })
@ -487,19 +484,19 @@ describe('built renderer', () => {
const data = 'allons-y!' const data = 'allons-y!'
const output = displayDataWith({ 'text/plain': [data] }) const output = displayDataWith({ 'text/plain': [data] })
expect( renderer.renderDisplayData(output) ).toBe(mockLastResult(dataRenderer)) expect( renderer.DisplayData(output) ).toBe(mockLastResult(dataRenderer))
expect( dataRenderer ).toBeCalledWith(data) expect( dataRenderer ).toBeCalledWith(data)
}) })
}) })
}) })
describe('.renderError', () => { describe('.Error', () => {
const error = fixtures.Error const error = fixtures.Error
const traceback = error.traceback.join('\n') const traceback = error.traceback.join('\n')
it('returns pre.error.pyerr with inner $traceback converted using ansiCodesRenderer', () => { it('returns pre.error.pyerr with inner $traceback converted using ansiCodesRenderer', () => {
expect( renderer.renderError(error) ).toHtmlEqual( expect( renderer.Error(error) ).toHtmlEqual(
<pre class="error pyerr"> <pre class="error pyerr">
{{__html: mockLastResult(ansiCodesRenderer) }} {{__html: mockLastResult(ansiCodesRenderer) }}
</pre> </pre>
@ -509,12 +506,12 @@ describe('built renderer', () => {
}) })
describe('.renderStream', () => { describe('.Stream', () => {
eachMultilineVariant(fixtures.Stream, 'text', (stream) => { eachMultilineVariant(fixtures.Stream, 'text', (stream) => {
const text = join(stream.text) const text = join(stream.text)
it('returns pre.$name with inner $text converted using ansiCodesRenderer', () => { it('returns pre.$name with inner $text converted using ansiCodesRenderer', () => {
expect( renderer.renderStream(stream) ).toHtmlEqual( expect( renderer.Stream(stream) ).toHtmlEqual(
<pre class={ stream.name }> <pre class={ stream.name }>
{{__html: mockLastResult(ansiCodesRenderer) }} {{__html: mockLastResult(ansiCodesRenderer) }}
</pre> </pre>

View file

@ -1,9 +1,5 @@
// This file is needed only for VSCode, see https://github.com/palmerhq/tsdx/issues/84.
{ {
"extends": "../../../tsconfig.test.json", "extends": "../tsconfig.json",
"compilerOptions": { "include": ["."],
"baseUrl": ".",
},
"references": [
{ "path": "../" },
],
} }

View file

@ -5,6 +5,11 @@
"outDir": "./lib", "outDir": "./lib",
"tsBuildInfoFile": "./.tsbuildinfo", "tsBuildInfoFile": "./.tsbuildinfo",
"baseUrl": ".", "baseUrl": ".",
"paths": {
"*": ["../../types/*"],
"@/*": ["./src/*"],
"~/*": ["../../*"],
},
}, },
"include": [ "include": [
"./src", "./src",

View file

@ -1,6 +1,6 @@
{ {
"name": "ipynb2html", "name": "ipynb2html",
"version": "0.3.0", "version": "0.1.0-beta.6",
"description": "Convert Jupyter Notebook to static HTML", "description": "Convert Jupyter Notebook to static HTML",
"author": "Jakub Jirutka <jakub@jirutka.cz>", "author": "Jakub Jirutka <jakub@jirutka.cz>",
"license": "MIT", "license": "MIT",
@ -18,23 +18,16 @@
"notebook" "notebook"
], ],
"main": "lib/index.js", "main": "lib/index.js",
"browser": "dist/ipynb2html.min.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",
"files": [ "files": [
"dist/ipynb2html.min.js*",
"dist/ipynb2html-full.min.js*",
"dist/notebook.min.css*",
"lib", "lib",
"src", "src"
"styles"
], ],
"scripts": { "scripts": {
"build": "ttsc --build", "build": "ttsc --build",
"bundle": "rollup -c", "clean": "rimraf coverage/ lib/ .tsbuildinfo",
"clean": "rimraf coverage/ dist/ lib/ .eslintcache .tsbuildinfo", "lint": "eslint --ext .ts,.tsx,.js .",
"lint": "PKGDIR=$PWD; cd ../../ && eslint --cache --ext .ts,.tsx,.js $PKGDIR", "prepublishOnly": "run-s readme2md",
"minify-css": "csso styles/notebook.css -o dist/notebook.min.css -s dist/notebook.min.css.map",
"prepublishOnly": "run-p bundle minify-css readme2md",
"test": "jest --detectOpenHandles --coverage --verbose", "test": "jest --detectOpenHandles --coverage --verbose",
"readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md", "readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md",
"watch-ts": "ttsc --build --watch" "watch-ts": "ttsc --build --watch"
@ -44,22 +37,10 @@
}, },
"dependencies": { "dependencies": {
"anser": "^1.4.9", "anser": "^1.4.9",
"highlight.js": "^10.7.3", "highlightjs": "^9.12.0",
"ipynb2html-core": "0.3.0", "ipynb2html-core": "0.1.0-beta.6",
"katex": "^0.13.11", "katex": "^0.11.0",
"marked": "^2.0.7" "marked": "^0.7.0",
},
"devDependencies": {
"@types/katex": "^0.11.0",
"@types/marked": "^2.0.3",
"ansi_up": "^5.0.1"
},
"peerDependencies": {
"nodom": "^2.3.0" "nodom": "^2.3.0"
}, }
"browserslist": [
">0.5%",
"Firefox ESR",
"not dead"
]
} }

View file

@ -1,109 +0,0 @@
import addGitMsg from 'rollup-plugin-add-git-msg'
import babel from '@rollup/plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import license from 'rollup-plugin-node-license'
import resolve from '@rollup/plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
import ttypescript from 'ttypescript'
import typescript from 'rollup-plugin-typescript2'
import pkg from './package.json'
const extensions = ['.mjs', '.js', '.ts']
const globals = {
'anser': 'anser',
'highlight.js': 'hljs',
'katex': 'katex',
'marked': 'marked',
}
const plugins = [
// Transpile TypeScript sources to JS.
typescript({
typescript: ttypescript,
tsconfigOverride: {
compilerOptions: {
target: 'ES5',
module: 'ESNext',
declaration: false,
declarationMap: false,
composite: false,
incremental: true,
},
},
clean: true,
}),
// Resolve node modules.
resolve({
extensions,
mainFields: ['browser', 'module', 'main'],
}),
// Convert CommonJS modules to ES6 modules.
commonjs(),
// Transpile all sources for older browsers and inject needed polyfills.
babel({
babelrc: false,
babelHelpers: 'bundled',
// To avoid Babel injecting core-js polyfills into core-js.
exclude: [/node_modules\/core-js\//],
extensions,
presets: [
[
'@babel/env', {
corejs: 3,
debug: false,
modules: false,
useBuiltIns: 'usage', // inject polyfills
// targets: reads from "browserslist" in package.json
},
],
],
}),
// Add git tag, commit SHA and build date at top of the file.
addGitMsg({
copyright: [
pkg.author,
'* This project is licensed under the terms of the MIT license.'
].join('\n'),
}),
// Generate table of the bundled packages at top of the file.
license({ format: 'table' }),
]
const output = (filename, extra = {}) => [false, true].map(minify => ({
name: pkg.name,
file: `${filename}${minify ? '.min.js' : '.js'}`,
format: 'umd',
sourcemap: true,
plugins: [
// Minify JS when building .min.js file.
minify && terser({
ecma: 5,
output: {
// Preserve comment injected by addGitMsg and license.
comments: RegExp(`(?:\\$\\{${pkg.name}\\}|Bundled npm packages)`),
},
}),
].filter(Boolean),
...extra,
}))
export default [
// Bundle third-party dependencies except core-js polyfills.
{
input: 'src/browser.ts',
external: Object.keys(globals),
plugins,
output: output(`dist/${pkg.name}`, { globals }),
},
// Bundle with all dependencies.
{
input: 'src/browser.ts',
plugins,
// Tree-shaking breaks KaTeX and has almost zero effect on the bundle size.
treeshake: false,
output: output(`dist/${pkg.name}-full`),
}
]

View file

@ -1,48 +0,0 @@
import { createRenderer, NbRendererOpts, Notebook } from '.'
export * from '.'
function unescapeHTML (input: string): string {
return new DOMParser()
.parseFromString(input, 'text/html')
.documentElement
.textContent ?? ''
}
/**
* Renders the given Jupyter *notebook* to HTML. It's a shorthand for
* `createRenderer(document, opts)(notebook)`.
*
* @example
* document.body.appendChild(ipynb2html.render(notebook))
*
* @param notebook Object in Jupyter Notebook Format 4.0+.
* @param opts The renderer options.
* @return An HTMLElement with the rendered *notebook*.
* @see createRenderer
*/
export function render (notebook: Notebook, opts: NbRendererOpts = {}): HTMLElement {
return createRenderer(document, opts)(notebook)
}
/**
* Renders Jupyter Notebook inside each
* `<script type="application/x-ipynb+json">...</script>` found in
* the page's body.
*
* @param opts The renderer options.
*/
export function autoRender (opts: NbRendererOpts = {}): void {
const selector = 'script[type="application/x-ipynb+json"]'
const render = createRenderer(document, opts)
document.querySelectorAll(selector).forEach(script => {
if (script.textContent && script.parentElement) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const notebook = JSON.parse(unescapeHTML(script.textContent))
const nbElement = render(notebook)
script.parentElement.replaceChild(nbElement, script)
}
})
}

View file

@ -1,6 +0,0 @@
// AnsiUp may be provided in the browser environment as an external dependency
// via global variable. It's an alternative to Anser that doesn't provide
// bundle for browsers.
declare class AnsiUp {
ansi_to_html (input: string): string
}

View file

@ -1,25 +1,25 @@
import anser from 'anser' import anser from 'anser'
import hljs from 'highlight.js' import hljs from 'highlightjs'
import katex, { KatexOptions } from 'katex' import katex, { KatexOptions } from 'katex'
import marked from 'marked' import { MarkedOptions } from 'marked'
import { Document, HTMLElement } from 'nodom'
import { import {
createElementCreator, createElementCreator,
createHtmlRenderer, createHtmlRenderer,
MinimalElement, createNbRenderer,
NbRenderer, NbRenderer,
NbRendererOpts as BaseOptions, NbRendererOpts as BaseOptions,
Notebook,
} from 'ipynb2html-core' } from 'ipynb2html-core'
import buildMarkdownRenderer, { MarkedOptions } from './markdownRenderer' import buildMarkdownRenderer from './markdownRenderer'
export { default as version } from './version' export { default as version } from './version'
export { default as readNotebookTitle } from './readNotebookTitle'
export { NbRenderer, Notebook }
export type NbRendererOpts<TElement = HTMLElement> = BaseOptions<TElement> & { export { NbRenderer }
export type NbRendererOpts = BaseOptions<HTMLElement> & {
/** /**
* The prefix to be used for all CSS class names except `lang-*`. * The prefix to be used for all CSS class names except `lang-*`.
* Default is `nb-`. * Default is `nb-`.
@ -37,32 +37,24 @@ export type NbRendererOpts<TElement = HTMLElement> = BaseOptions<TElement> & {
markedOpts?: MarkedOptions, markedOpts?: MarkedOptions,
} }
/**
* Definition of the smallest possible subset of the Document type required
* for this module's function.
*/
type MinimalDocument<TElement extends MinimalElement> = {
createElement: (tag: string) => TElement,
}
const defaultKatexOpts: KatexOptions = { const defaultKatexOpts: KatexOptions = {
displayMode: true, displayMode: true,
throwOnError: false, throwOnError: false,
} }
const defaultMarkedOpts: MarkedOptions = { function ansiCodesRenderer (input: string): string {
headerAnchors: true, return anser.ansiToHtml(anser.escapeForHtml(input))
headerIdsStripAccents: true,
} }
function hljsCodeHighlighter (code: string, lang: string): string { function codeHighlighter (code: string, lang: string): string {
return hljs.getLanguage(lang) return hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang }).value ? hljs.highlight(lang, code).value
: code : code
} }
/** /**
* Builds a full-fledged Jupyter Notebook renderer. * Builds a full-fledged Notebook renderer for server-side rendering with a
* fake DOM implementation "nodom".
* *
* It supports rendering of Markdown cells with math (using marked and KaTeX), * It supports rendering of Markdown cells with math (using marked and KaTeX),
* code highlighting (using highlight.js), rendering of ANSI escape sequences * code highlighting (using highlight.js), rendering of ANSI escape sequences
@ -72,63 +64,21 @@ function hljsCodeHighlighter (code: string, lang: string): string {
* It returns a "callable object" that exposes one renderer function for each * It returns a "callable object" that exposes one renderer function for each
* of the Notebook's AST nodes. You can easily replace any of the functions to * of the Notebook's AST nodes. You can easily replace any of the functions to
* modify behaviour of the renderer. * modify behaviour of the renderer.
*
* @example // Node.js
* import * as fs from 'fs'
* import * as ipynb from 'ipynb2html'
* import { Document } from 'nodom'
*
* const renderNotebook = ipynb.createRenderer(new Document())
* const notebook = JSON.parse(fs.readFileSync('./example.ipynb', 'utf8'))
*
* console.log(renderNotebook(notebook).outerHTML)
*
* @example // Browser
* const render = ipynb2html.createRenderer(document)
* document.body.appendChild(render(notebook))
*
* @param document The `Document` object from the browser's native DOM or any
* fake/virtual DOM library (e.g. nodom). The only required method is
* `createElement`.
* @param opts The renderer options.
* @return A configured instance of the Notebook renderer.
*/ */
export function createRenderer <TElement extends MinimalElement> ( export default (opts: NbRendererOpts = {}): NbRenderer<HTMLElement> => {
document: MinimalDocument<TElement>,
opts: NbRendererOpts<TElement> = {},
): NbRenderer<TElement> {
let { ansiCodesRenderer, codeHighlighter, dataRenderers = {}, markdownRenderer } = opts
const katexOpts = { ...defaultKatexOpts, ...opts.katexOpts } const katexOpts = { ...defaultKatexOpts, ...opts.katexOpts }
const elementCreator = createElementCreator( const doc = new Document()
document.createElement.bind(document), const elementCreator = createElementCreator(doc.createElement.bind(doc), opts.classPrefix)
opts.classPrefix, const markdownRenderer = buildMarkdownRenderer(opts.markedOpts, katexOpts)
)
// The following ifs for the imported modules are for the browser bundle
// without dependencies.
if (!ansiCodesRenderer) {
if (anser) {
ansiCodesRenderer = (input) => anser.ansiToHtml(anser.escapeForHtml(input))
} else if (AnsiUp) {
const ansiUp = new AnsiUp()
ansiCodesRenderer = ansiUp.ansi_to_html.bind(ansiUp)
}
}
if (!codeHighlighter && hljs) {
codeHighlighter = hljsCodeHighlighter
}
if (!markdownRenderer && marked) {
const markedOpts = { ...defaultMarkedOpts, ...opts.markedOpts }
markdownRenderer = buildMarkdownRenderer(markedOpts, katexOpts)
}
if (!dataRenderers['text/html'] && katex) {
const mathRenderer = (tex: string) => katex.renderToString(tex, katexOpts) const mathRenderer = (tex: string) => katex.renderToString(tex, katexOpts)
dataRenderers['text/html'] = createHtmlRenderer({ elementCreator, mathRenderer })
const dataRenderers = {
'text/html': createHtmlRenderer({ elementCreator, mathRenderer }),
...opts.dataRenderers,
} }
return new NbRenderer(elementCreator, { return createNbRenderer(elementCreator, {
ansiCodesRenderer, ansiCodesRenderer,
codeHighlighter, codeHighlighter,
dataRenderers, dataRenderers,

View file

@ -1,62 +1,30 @@
import hljs from 'highlight.js' import hljs from 'highlightjs'
import katex, { KatexOptions } from 'katex' import katex, { KatexOptions } from 'katex'
import marked, { Slugger } from 'marked' import marked, { MarkedOptions } from 'marked'
import { mathExtractor } from 'ipynb2html-core' import { mathExtractor } from 'ipynb2html-core'
export type MarkdownRenderer = (markdown: string) => string
export interface MarkedOptions extends marked.MarkedOptions {
/** Generate heading anchors (this implies headingIds). */
headerAnchors?: boolean
headerIdsStripAccents?: boolean
}
// Removes accents from the given string.
const stripAccents = (text: string) => text.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
// Removes math markers from the given string.
const stripMath = (text: string) => mathExtractor.restoreMath(text, []).trim()
class Renderer extends marked.Renderer {
heading (text: string, level: 1 | 2 | 3 | 4 | 5 | 6, raw: string, slugger: Slugger): string {
const opts = this.options as MarkedOptions
if (!opts.headerIds && !opts.headerAnchors) {
return super.heading(text, level, raw, slugger)
}
let id = (opts.headerPrefix ?? '') + slugger.slug(stripMath(raw))
if (opts.headerIdsStripAccents) {
id = stripAccents(id)
}
if (opts.headerAnchors) {
text = `<a class="anchor" href="#${id}" aria-hidden="true"></a>${text}`
}
return `<h${level} id="${id}">${text}</h${level}>`
}
link (href: string | null, title: string | null, text: string): string {
return super.link(href && stripMath(href), title && stripMath(title), text)
}
image (href: string | null, title: string | null, text: string): string {
return super.image(href && stripMath(href), title && stripMath(title), stripMath(text))
}
}
function highlight (code: string, lang: string): string { function highlight (code: string, lang: string): string {
return hljs.getLanguage(lang) return hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang, ignoreIllegals: true }).value ? hljs.highlight(lang, code, true).value
: code : code
} }
const renderer = (markedOpts: MarkedOptions) => (markdown: string) => marked.parse(markdown, markedOpts) /**
* Returns a pre-configured marked parser with math support (using KaTeX)
* and code highlighter (highlight.js).
*
* @param {MarkedOptions} markedOpts Options for the marked Markdown renderer.
* @param {KatexOptions} katexOpts Options for the KaTeX math renderer.
*/
export default (markedOpts: MarkedOptions = {}, katexOpts: KatexOptions = {}) => {
markedOpts = { highlight, ...markedOpts }
const rendererWithMath = (markedOpts: MarkedOptions, katexOpts: KatexOptions) => (markdown: string) => { /**
* Converts the given *markdown* into HTML.
*/
return (markdown: string): string => {
const [text, math] = mathExtractor.extractMath(markdown) const [text, math] = mathExtractor.extractMath(markdown)
const html = marked.parse(text, markedOpts) const html = marked.parse(text, markedOpts)
@ -65,22 +33,4 @@ const rendererWithMath = (markedOpts: MarkedOptions, katexOpts: KatexOptions) =>
}) })
return mathExtractor.restoreMath(html, mathHtml) return mathExtractor.restoreMath(html, mathHtml)
} }
/**
* Returns a pre-configured marked parser with (optional) math support (using
* KaTeX) and code highlighter (highlight.js).
*
* @param {MarkedOptions} markedOpts Options for the marked Markdown renderer.
* @param {KatexOptions} katexOpts Options for the KaTeX math renderer.
*/
export default (markedOpts: MarkedOptions = {}, katexOpts: KatexOptions = {}): MarkdownRenderer => {
markedOpts = { renderer: new Renderer(markedOpts), ...markedOpts }
if (hljs) { // highlight.js may be an optional dependency (in browser bundle)
markedOpts = { highlight, ...markedOpts }
}
return katex // katex may be an optional dependency (in browser bundle)
? rendererWithMath(markedOpts, katexOpts)
: renderer(markedOpts)
} }

View file

@ -1,20 +1,20 @@
import marked from 'marked' import marked from 'marked'
import { Notebook } from 'ipynb2html-core' import { Notebook, CellType } from 'ipynb2html-core'
class EmptyRenderer extends marked.Renderer {} class EmptyRenderer extends marked.Renderer {
// Override all the EmptyRenderer's methods inherited from marked.Renderer to constructor (options?: marked.MarkedOptions) {
// always return an empty string. super(options)
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
const RendererProto = marked.Renderer.prototype for (const prop in this) {
for (const prop of Object.getOwnPropertyNames(RendererProto)) { if (this[prop] instanceof Function) {
if (prop !== 'constructor' && typeof (RendererProto as any)[prop] === 'function') { (this as any)[prop] = () => ''
(EmptyRenderer.prototype as any)[prop] = () => '' }
}
} }
} }
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
class MainTitleRenderer extends EmptyRenderer { class MainTitleRenderer extends EmptyRenderer {
_titleFound = false _titleFound = false
@ -38,7 +38,7 @@ export default (notebook: Notebook): string => {
if (notebook.metadata.title) { if (notebook.metadata.title) {
return notebook.metadata.title return notebook.metadata.title
} }
if (notebook.cells.length > 0 && notebook.cells[0].cell_type === 'markdown') { if (notebook.cells.length > 0 && notebook.cells[0].cell_type === CellType.Markdown) {
const source = notebook.cells[0].source const source = notebook.cells[0].source
const markup = Array.isArray(source) ? source.join('') : source const markup = Array.isArray(source) ? source.join('') : source

View file

@ -1,6 +1,5 @@
import { $INLINE_JSON } from 'ts-transformer-inline-file' import { $INLINE_JSON } from 'ts-transformer-inline-file'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { version } = $INLINE_JSON('../package.json') const { version } = $INLINE_JSON('../package.json')
export default version as string export default version as string

View file

@ -1,86 +0,0 @@
/* Reference styles for ipynb2html */
.nb-notebook {
line-height: 1.5;
}
.nb-cell + .nb-cell {
margin-top: 1.4rem;
}
.nb-raw-cell {
font-family: monospace;
white-space: pre-wrap;
}
.nb-code-cell {
position: relative;
}
.nb-source::before,
.nb-output::before {
position: absolute;
font-family: monospace;
color: #999;
left: -7.5em;
width: 7em;
text-align: right;
}
.nb-source::before {
content: "In [" attr(data-execution-count) "]:";
}
.nb-output::before {
content: "Out[" attr(data-execution-count) "]:";
}
.nb-source + .nb-output,
.nb-output + .nb-output {
margin-top: 1.4rem;
}
.nb-source > pre {
background-color: #f7f7f7;
border: 1px solid #cfcfcf;
border-radius: 2px;
padding: 0.5em;
margin: 0;
overflow-x: auto;
}
.nb-output {
min-height: 1em;
width: 100%;
}
.nb-output > pre {
padding: 0.5em;
margin: 0;
overflow-x: auto;
}
.nb-output > img {
max-width: 100%;
}
.nb-stdout,
.nb-stderr {
white-space: pre-wrap;
}
.nb-error,
.nb-stderr {
background-color: #fdd;
border-radius: 2px;
}
@media (max-width: 768px) {
.nb-source::before,
.nb-output::before {
display: block;
position: relative;
left: 0;
padding-bottom: 0.5em;
text-align: left;
}
}

View file

@ -1 +0,0 @@
../src/global.d.ts

View file

@ -1,152 +0,0 @@
import { KatexOptions } from 'katex'
import parseHtml from 'node-html-parser'
import markdownRenderer, { MarkedOptions } from '@/markdownRenderer'
import '~/test/setup' // setupFilesAfterEnv doesn't work here
import { Anything } from '~/test/support/matchers/toMatchElement'
let katexOpts: KatexOptions
let markedOpts: MarkedOptions
beforeEach(() => {
katexOpts = {
displayMode: true,
throwOnError: false,
}
markedOpts = {}
})
describe('headings', () => {
it('renders h tag without anchor', () => {
expect( render('## Some Title') ).toHtmlEqual(
<h2 id="some-title">Some Title</h2>
)
})
it('renders math in text, but strips it in id', () => {
expect( render('## Integers $\\mathbb{Z}$') ).toMatchElement(
<h2 id="integers">
Integers <span class="katex"><Anything /></span>
</h2>
)
})
describe('when headerAnchors is true', () => {
beforeEach(() => {
markedOpts = { headerAnchors: true }
})
it('renders h tag with anchor', () => {
expect( render('## Some Title') ).toHtmlEqual(
<h2 id="some-title">
<a class="anchor" href="#some-title" aria-hidden="true"></a>Some Title
</h2>
)
})
it('renders math in text, but strips it in href and id', () => {
expect( render('## Integers $\\mathbb{Z}$') ).toMatchElement(
<h2 id="integers">
<a class="anchor" href="#integers" aria-hidden="true"></a>
Integers <span class="katex"><Anything /></span>
</h2>
)
})
})
describe('when headerIds is false', () => {
beforeEach(() => {
markedOpts = { headerIds: false }
})
it('renders h tag without id and anchor', () => {
expect( render('## Some Title') ).toHtmlEqual(
<h2>Some Title</h2>
)
})
})
describe('when headerIdsStripAccents is true', () => {
beforeEach(() => {
markedOpts = { headerIdsStripAccents: true }
})
it('strips accents in generated id', () => {
expect( render('## Příliš žluťoučký kůň') ).toHtmlEqual(
<h2 id="prilis-zlutoucky-kun">
Příliš žluťoučký kůň
</h2>
)
})
})
})
describe('link', () => {
it('strips math in title', () => {
expect( render('[link](https://example.org "This is $\\TeX$!")') ).toHtmlEqual(
<p><a href="https://example.org" title="This is !">link</a></p>
)
})
it('strips math in href', () => {
expect( render('[link](https://example.org/$\\TeX$)') ).toHtmlEqual(
<p><a href="https://example.org/">link</a></p>
)
})
it('renders math in text', () => {
expect( render('[This is $\\TeX$!](https://example.org/)') ).toMatchElement(
<p><a href="https://example.org/">This is <span class="katex"><Anything /></span>!</a></p>
)
})
})
describe('image', () => {
it('strips math in title', () => {
expect( render('![x](https://example.org/img.png "This is $\\TeX$!")') ).toHtmlEqual(
<p><img src="https://example.org/img.png" alt="x" title="This is !" /></p>
)
})
it('strips math in href', () => {
expect( render('![image](https://example.org/$\\TeX$.png)') ).toHtmlEqual(
<p><img src="https://example.org/.png" alt="image" /></p>
)
})
it('strips math in alt text', () => {
expect( render('![This is $\\TeX$!](https://example.org/img.png)') ).toHtmlEqual(
<p><img src="https://example.org/img.png" alt="This is !" /></p>
)
})
})
describe('code', () => {
it('highlights fenced code block', () => {
// Note: We use `backticks` in this example to avoid problem that JSX
// implicitly decodes HTML entities, but node-html-parser does not.
expect( render('```js\nconsole.log(`Hello, world!`)\n```') ).toHtmlEqual(
<pre>
<code class="language-js">
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Hello, world!`</span>)
</code>
</pre>
)
})
})
function render (text: string) {
const html = markdownRenderer(markedOpts, katexOpts)(text).replace(/\n/, '')
return parseHtml(html, { pre: true }).childNodes[0]
}

View file

@ -1,7 +1,7 @@
import { Notebook, CellType, MarkdownCell } from 'ipynb2html-core'
import readNotebookTitle from '@/readNotebookTitle' import readNotebookTitle from '@/readNotebookTitle'
import { Notebook, CellType, MarkdownCell } from 'ipynb2html-core'
const markdownCell = (source: string | string[]): MarkdownCell => ({ const markdownCell = (source: string | string[]): MarkdownCell => ({
cell_type: CellType.Markdown, cell_type: CellType.Markdown,

View file

@ -1,9 +1,5 @@
// This file is needed only for VSCode, see https://github.com/palmerhq/tsdx/issues/84.
{ {
"extends": "../../../tsconfig.test.json", "extends": "../tsconfig.json",
"compilerOptions": { "include": ["."],
"baseUrl": ".",
},
"references": [
{ "path": "../" },
],
} }

View file

@ -5,6 +5,11 @@
"outDir": "./lib", "outDir": "./lib",
"tsBuildInfoFile": "./.tsbuildinfo", "tsBuildInfoFile": "./.tsbuildinfo",
"baseUrl": ".", "baseUrl": ".",
"paths": {
"*": ["../../types/*"],
"@/*": ["./src/*"],
"~/*": ["../../*"],
},
}, },
"include": [ "include": [
"./src", "./src",

View file

View file

@ -1,15 +0,0 @@
TODO: Remove after https://github.com/chrisdarroch/yarn-bump/pull/1 is merged and released.
diff --git a/node_modules/yarn-version-bump/src/workspace.js b/node_modules/yarn-version-bump/src/workspace.js
index 28b1dd1..0cf2ecb 100644
--- a/node_modules/yarn-version-bump/src/workspace.js
+++ b/node_modules/yarn-version-bump/src/workspace.js
@@ -9,7 +9,7 @@ class Workspace {
get workspaceSnapshot() {
return runCommand('yarn',
- ['workspaces', 'info', '--silent'],
+ ['--silent', 'workspaces', 'info'],
{ cwd: this.root }
)
.then(data => JSON.parse(data))

View file

@ -1,4 +1,4 @@
#!/bin/sh -e #!/bin/sh -e
( set -o pipefail 2>/dev/null ) && set -o pipefail ( set -o pipefail 2>/dev/null ) && set -o pipefail
asciidoctor -o - -b docbook "$@" | pandoc -f docbook -t markdown_github --base-header-level 2 -o - asciidoctor -o - -b docbook "$@" | pandoc -f docbook -t markdown_github -o -

View file

@ -1,42 +0,0 @@
#!/usr/bin/env node
'use strict'
const fs = require('fs')
const path = require('path')
function extractLicenseTable (filename) {
const source = fs.readFileSync(filename, 'utf8')
const start = source.indexOf('/*!\n * Bundled npm packages')
const end = start + source.slice(start).indexOf('*/')
return source
.slice(start, end)
.split('\n')
.filter(s => s.startsWith(' * |'))
.map(s => s.slice(3))
.join('\n')
}
function readLicense () {
return fs.readFileSync(path.join(__dirname, '../LICENSE'), 'utf8')
}
const argv = process.argv.slice(2)
if (argv.length < 1 || ['-h', '--help'].includes(argv[0])) {
console.warn('Usage: assemble-license <bundle-file>')
process.exit(2)
}
const bundlePath = path.resolve(argv[0])
console.log(`\
${readLicense()}
-------------------------
This product bundles source code of the following projects:
${extractLicenseTable(bundlePath)}\
`)

View file

@ -1,7 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict' 'use strict'
const fs = require('fs-extra')
const path = require('path') const path = require('path')
const { bumpVersion } = require('yarn-version-bump/src/bump-version') const { bumpVersion } = require('yarn-version-bump/src/bump-version')
const { processJsonFile } = require('yarn-version-bump/src/util/json') const { processJsonFile } = require('yarn-version-bump/src/util/json')
@ -24,20 +23,11 @@ async function bumpAllPackages (newVersion) {
} }
} }
async function updateReadme (newVersion) {
return fs.readFile('README.adoc', 'utf8')
.then(str => str.replace(/^:version: \d.*$/m, `:version: ${newVersion}`))
.then(str => fs.writeFile('README.adoc', str))
}
const newVersion = process.argv[2] || rootPkg.version const newVersion = process.argv[2] || rootPkg.version
process.chdir(path.resolve(__dirname, '..')) process.chdir(path.resolve(__dirname, '..'))
Promise.all([ bumpAllPackages(newVersion).catch(err => {
bumpAllPackages(newVersion),
updateReadme(newVersion),
]).catch(err => {
console.error(err) console.error(err)
process.exit(1) process.exit(1)
}) })

View file

@ -1,79 +0,0 @@
#!/usr/bin/env node
'use strict'
const commonPathPrefix = require('common-path-prefix')
const execa = require('execa')
const fs = require('fs-extra')
const path = require('path')
const tar = require('tar')
const { ZipFile } = require('yazl')
async function writeTarGz (basename, fileList, cwd) {
console.log(`Creating ${basename}.tar.gz...`)
return tar.create({
file: `${basename}.tar.gz`,
prefix: path.basename(basename),
gzip: true,
portable: true,
cwd: cwd || '.',
}, fileList)
}
async function writeZip (basename, fileList, cwd) {
const prefix = path.basename(basename)
const zipfile = new ZipFile()
return new Promise((resolve, reject) => {
console.log(`Creating ${basename}.zip...`)
for (let fname of fileList) {
zipfile.addFile(path.join(cwd || '.', fname), path.join(prefix, fname))
}
zipfile.outputStream
.pipe(fs.createWriteStream(`${basename}.zip`))
.on('close', resolve)
.on('error', reject)
zipfile.end()
})
}
async function gitDescribe (prefix) {
const output = await execa.stdout('git', [
'describe',
'--always',
`--match=${prefix}*`
])
return output.startsWith(prefix) ? output : `v0.0.0-${output}`
}
async function createArchives (destName, srcFiles) {
const version = await gitDescribe('v')
destName += `-${version}`
const srcDir = commonPathPrefix(srcFiles)
const fileList = srcFiles.map(p => p.slice(srcDir.length))
fs.mkdirpSync(path.dirname(destName))
return Promise.all([
writeTarGz(destName, fileList, srcDir),
writeZip(destName, fileList, srcDir),
])
}
const argv = process.argv.slice(2)
if (argv.length < 3 || ['-h', '--help'].includes(argv[0])) {
console.warn('Usage: create-archives <dest-basename> <src-file>...')
process.exit(2)
}
const destName = argv[0]
const srcFiles = argv.slice(1)
createArchives(destName, srcFiles).catch(err => {
console.error(err)
process.exit(1)
})

View file

@ -1,19 +0,0 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")/.."
case "${1:-}" in
'' | -h | --help) echo "Usage: $0 <out-file>" >&2; exit 2;;
esac
out_file="$1"
base_dir=$(basename "$(pwd)")
base_name=$(basename "${out_file%.tar.gz}")
mkdir -p dist
tar -czf "$out_file" \
-C .. --exclude-vcs \
--exclude="**/$out_file" \
--xform="s|^$base_dir|$base_name|" \
"$base_dir"

View file

@ -1,5 +1,5 @@
type Callable = (...args: any[]) => unknown type Callable = (...args: any[]) => any
export type Mock<F extends Callable> = jest.Mock<ReturnType<F>, Parameters<F>> export type Mock<F extends Callable> = jest.Mock<ReturnType<F>, Parameters<F>>
@ -13,9 +13,9 @@ export function asMock <F extends Callable> (fn: F): Mock<F> {
export function mockResults <F extends Callable> (fn: F): Array<ReturnType<F>> { export function mockResults <F extends Callable> (fn: F): Array<ReturnType<F>> {
return asMock(fn).mock.results return asMock(fn).mock.results
.filter(x => x.type === 'return') .filter(x => x.type === 'return')
.map(x => x.value as ReturnType<F>) .map(x => x.value)
} }
export function mockLastResult <F extends Callable> (fn: F): ReturnType<F> | undefined { export function mockLastResult <F extends Callable> (fn: F): ReturnType<F> {
return mockResults(fn).pop() return mockResults(fn).pop() as ReturnType<F>
} }

View file

@ -60,8 +60,7 @@ export function createElement (
} else if (child instanceof NNode) { } else if (child instanceof NNode) {
el.appendChild(child) el.appendChild(child)
} else if (typeof child === 'object' && '__html' in child) { } else if (typeof child === 'object' && '__html' in child) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access el.innerHTML = child.__html
el.innerHTML = child.__html as string
} else { } else {
el.appendChild(document.createTextNode(String(child))) el.appendChild(document.createTextNode(String(child)))
} }
@ -69,7 +68,6 @@ export function createElement (
return el return el
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(global as any).JSX = { (global as any).JSX = {
createElement, createElement,
} }

View file

@ -6,8 +6,8 @@ type MatcherResult = jest.CustomMatcherResult
declare global { declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest { namespace jest {
interface Matchers<R, T> { interface Matchers<R> {
toHtmlEqual: (expected: HTMLElement | string | Array<HTMLElement | string>) => R toHtmlEqual (expected: HTMLElement | string | Array<HTMLElement | string>): R,
} }
} }
} }

View file

@ -10,41 +10,33 @@ type Options = {
declare global { declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest { namespace jest {
interface Matchers<R, T> { interface Matchers<R> {
toMatchElement: (expected: HTMLElement, opts?: Options) => R toMatchElement (expected: HTMLElement, opts?: Options): R,
} }
} }
} }
export const AnythingNode = new class extends Node { export const AnythingNode = new class extends Node {
render () { return '<!--Anything-->' } render () { return '<!--Anything-->' }
toString () { return this.render() }
}() }()
export const Anything = (): Node => AnythingNode export const Anything = () => AnythingNode
function isWritable (obj: any, prop: string): boolean {
const desc = Object.getOwnPropertyDescriptor(obj, prop)
// eslint-disable-next-line @typescript-eslint/unbound-method
return !!desc?.writable || !!desc?.set
}
function filterWildcardChildren (rec: Node, exp: Node): void { function filterWildcardChildren (rec: Node, exp: Node): void {
if (exp.firstChild === AnythingNode if (exp.firstChild === AnythingNode
&& exp.childNodes.length === 1 && exp.children.length === 1
&& (rec as HTMLElement).innerHTML && rec instanceof HTMLElement
&& rec.innerHTML
) { ) {
if (isWritable(rec, 'innerHTML')) { rec.innerHTML = ''
(rec as HTMLElement).innerHTML = '' rec.children.splice(0, rec.children.length, AnythingNode)
}
rec.childNodes.splice(0, rec.childNodes.length, AnythingNode)
return return
} }
for (let i = 0; i < exp.childNodes.length && i < rec.childNodes.length; i++) { for (let i = 0; i < exp.children.length && i < rec.children.length; i++) {
if (exp.childNodes[i] === AnythingNode) { if (exp.children[i] === AnythingNode) {
rec.childNodes[i] = AnythingNode rec.children[i] = AnythingNode
} else { } else {
filterWildcardChildren(rec.childNodes[i], exp.childNodes[i]) filterWildcardChildren(exp.children[i], rec.children[i])
} }
} }
} }
@ -54,15 +46,13 @@ function clearAttributes (node: Node): void {
node.attributes = {} node.attributes = {}
node.className = '' node.className = ''
} }
node.childNodes.forEach(clearAttributes) node.children.forEach(clearAttributes)
} }
export function toMatchElement (received: HTMLElement, expected: HTMLElement, opts?: Options): MatcherResult { export function toMatchElement (received: HTMLElement, expected: HTMLElement, opts?: Options): MatcherResult {
if (received.cloneNode) {
received = received.cloneNode(true) as HTMLElement received = received.cloneNode(true) as HTMLElement
}
if (opts?.ignoreAttrs) { if (opts && opts.ignoreAttrs) {
clearAttributes(received) clearAttributes(received)
} }
filterWildcardChildren(received, expected) filterWildcardChildren(received, expected)

View file

@ -1,6 +0,0 @@
{
"extends": "../tsconfig.test.json",
"compilerOptions": {
"baseUrl": ".",
},
}

View file

@ -42,9 +42,7 @@
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
"*": ["../../types/*"], /* This path is relative from baseUrl which is defined in tsconfig.json that includes this file inside each project. */
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [ /* List of folders to include type definitions from. */ "typeRoots": [ /* List of folders to include type definitions from. */
"./types", "./types",

View file

@ -3,6 +3,5 @@
"references": [ "references": [
{ "path": "./packages/ipynb2html-core" }, { "path": "./packages/ipynb2html-core" },
{ "path": "./packages/ipynb2html" }, { "path": "./packages/ipynb2html" },
{ "path": "./packages/ipynb2html-cli" },
] ]
} }

View file

@ -1,16 +0,0 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"declaration": false,
"declarationMap": false,
"sourceMap": false,
"composite": false,
"noEmit": true,
"paths": {
// These paths are relative from baseUrl which is defined in
// tsconfig.json that includes this file inside each project.
"@/*": ["../src/*"],
"~/*": ["../../../*"],
},
},
}

5354
yarn.lock

File diff suppressed because it is too large Load diff