Compare commits

..

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

64 changed files with 3046 additions and 4970 deletions

View file

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

View file

@ -1,6 +1,4 @@
const tsconfigs = [
'tsconfig.json',
'*/tsconfig.json',
'packages/*/tsconfig.json',
'packages/*/test/tsconfig.json',
]
@ -20,14 +18,15 @@ module.exports = {
},
settings: {
'import/resolver': {
// Use eslint-import-resolver-typescript to obey "paths" in tsconfig.json.
typescript: {
// Use eslint-import-resolver-ts to obey "paths" in tsconfig.json.
ts: {
directory: tsconfigs,
},
},
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'standard-with-typescript',
@ -36,63 +35,48 @@ module.exports = {
],
rules: {
'comma-dangle': ['error', 'always-multiline'],
// Disable in favour of TypeScript rule.
'func-call-spacing': 'off',
'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', {
ignoreEOLComments: true,
}],
// Changed from error to warn and adjusted options.
'no-multiple-empty-lines': ['warn', {
max: 2,
maxEOF: 1,
maxBOF: 1,
}],
'no-template-curly-in-string': 'off',
// Changed from 'after' to 'before'.
'operator-linebreak': ['error', 'before'],
// Changed from error and all 'never' to warn and switches 'never'.
'padded-blocks': ['warn', {
switches: 'never',
}],
// Changed from 'as-needed' to 'consistent-as-needed'.
'quote-props': ['error', 'consistent-as-needed'],
// Disable in favour of TypeScript rule.
'semi': 'off',
// Import
// Some packages have wrong type declarations.
'import/default': 'off',
'import/newline-after-import': 'warn',
'import/no-absolute-path': 'error',
// This rule disallows using both wildcard and selective imports from the same module.
'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',
// 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',
// Changed from error to off.
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-member-accessibility': ['error', {
'@typescript-eslint/explicit-member-accessibility': ['warn', {
accessibility: 'no-public',
overrides: {
parameterProperties: 'off',
},
}],
// Changed from warn to error and adjusted options.
'@typescript-eslint/explicit-module-boundary-types': ['error', {
allowArgumentsExplicitlyTypedAsAny: true,
}],
'@typescript-eslint/func-call-spacing': ['error', 'never'],
'@typescript-eslint/indent': ['error', 2, {
SwitchCase: 1,
VariableDeclarator: 1,
@ -111,58 +95,31 @@ module.exports = {
flatTernaryExpressions: true,
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', {
multiline: { delimiter: 'comma', requireLast: true },
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',
// 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',
// Changed from error to warn.
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-require-imports': 'error',
// Changed from error to warn.
'@typescript-eslint/no-unsafe-assignment': 'warn',
// 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-unused-vars': ['error', {
argsIgnorePattern: '^_',
}],
'@typescript-eslint/no-use-before-define': ['error', {
classes: true,
functions: false,
typedefs: false,
variables: true,
}],
'@typescript-eslint/prefer-for-of': 'warn',
// Changed from error to warn.
'@typescript-eslint/prefer-includes': 'warn',
// Changed from error to warn.
'@typescript-eslint/prefer-regexp-exec': 'warn',
'@typescript-eslint/prefer-string-starts-ends-with': 'warn',
// It has too many false positives.
'@typescript-eslint/restrict-template-expressions': 'off',
// 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/promise-function-async': ['error', {
allowAny: true,
}],
'@typescript-eslint/semi': ['error', 'never'],
'@typescript-eslint/strict-boolean-expressions': 'off',
},
overrides: [
{
@ -170,14 +127,7 @@ module.exports = {
rules: {
// Allow to format arrays for parametrized tests as tables.
'array-bracket-spacing': 'off',
'comma-dangle': ['error', {
arrays: 'always-multiline',
objects: 'always-multiline',
imports: 'always-multiline',
exports: 'always-multiline',
// Changed to not require comma in a multiline expect().
functions: 'only-multiline',
}],
'comma-spacing': 'off',
'object-curly-spacing': 'off',
'no-multi-spaces': 'off',
'standard/array-bracket-even-spacing': 'off',
@ -185,10 +135,7 @@ module.exports = {
'space-in-parens': 'off',
// jest.mock() must be above imports.
'import/first': 'off',
'@typescript-eslint/comma-spacing': 'off',
// False positive on expect() functions.
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-return': 'warn',
'@typescript-eslint/no-non-null-assertion': 'off',
},
},
],

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

1
.gitignore vendored
View file

@ -7,7 +7,6 @@
/.*cache/
/.tmp/
node_modules/
.eslintcache
.tsbuildinfo
tsconfig.tsbuildinfo
*.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
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
of this software and associated documentation files (the "Software"), to deal

View file

@ -2,15 +2,15 @@
:npm-name: ipynb2html
:gh-name: jirutka/{npm-name}
: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
:version: 0.1.0-beta.7
:ansiup-version: 4.0.4
:hljs-version: 9.15.10
:katex-version: 0.11.1
:marked-version: 0.7.0
:vs-marketplace-uri: https://marketplace.visualstudio.com/items?itemName=
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[]
{npm-name} is a converter (renderer) of the https://nbformat.readthedocs.io/en/stable/[Jupyter Notebook Format] 4.0+ to static HTML.
@ -32,7 +32,6 @@ endif::env-github[]
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.
=== {npm-name}
ifdef::env-github[]
@ -47,49 +46,9 @@ 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/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[]
== 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.
@ -132,7 +91,6 @@ You can link it from https://www.jsdelivr.com/[jsDelivr CDN], for example:
<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>
@ -153,7 +111,6 @@ document.body.appendChild(element)
<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"

View file

@ -4,14 +4,17 @@
<meta charset="utf-8" lang="cs">
<meta name="viewport" content="initial-scale=1">
<title>Example using ipynb2html-full.js</title>
<link rel="stylesheet" href="../packages/ipynb2html/dist/notebook.min.css">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css"
href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.7.3/build/styles/default.min.css"
href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.15.10/build/styles/default.min.css"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/jsvine/nbpreview@c54381b/css/vendor/notebook.css"
crossorigin="anonymous">
<script defer src="../packages/ipynb2html/dist/ipynb2html-full.js" onload="ipynb2html.autoRender();"></script>
<style>

View file

@ -4,19 +4,22 @@
<meta charset="utf-8" lang="cs">
<meta name="viewport" content="initial-scale=1">
<title>Example using ipynb2html.js</title>
<link rel="stylesheet" href="../packages/ipynb2html/dist/notebook.min.css">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css"
href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.7.3/build/styles/default.min.css"
href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.15.10/build/styles/default.min.css"
crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/marked@2.0.7/marked.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/ansi_up@5.0.1/ansi_up.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10.7.3/build/highlight.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.js" crossorigin="anonymous"></script>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/jsvine/nbpreview@c54381b/css/vendor/notebook.css"
crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/marked@0.7.0/marked.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/ansi_up@4.0.4/ansi_up.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.15.10/build/highlight.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js" crossorigin="anonymous"></script>
<script defer src="../packages/ipynb2html/dist/ipynb2html.js" onload="ipynb2html.autoRender();"></script>
<style>
html, body {

View file

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

View file

@ -1,13 +1,13 @@
{
"name": "ipynb2html-parent",
"version": "0.3.0",
"version": "0.1.0-beta.7",
"private": true,
"scripts": {
"build": "ttsc --build",
"bundle": "wsrun --exclude-missing bundle",
"clean": "rimraf coverage/ dist/ lib/ .eslintcache *.log && wsrun clean",
"lint": "eslint --cache --ext .ts,.tsx,.js .",
"postinstall": "patch-package && run-s build",
"clean": "rimraf coverage/ lib/ *.log && wsrun clean",
"lint": "eslint --ext .ts,.tsx,.js .",
"postinstall": "run-s build",
"publish-all": "wsrun --serial publish",
"test": "jest --detectOpenHandles --coverage --verbose",
"version": "./scripts/bump-version && git add README.adoc **/package.json",
@ -17,54 +17,48 @@
"node": ">=10.13.0"
},
"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",
"@babel/core": "^7.6.3",
"@babel/preset-env": "^7.6.3",
"@types/dedent": "^0.7.0",
"@types/jest": "^24.9.1",
"@types/node": "^10.17.25",
"@typescript-eslint/eslint-plugin": "^3.3.0",
"@typescript-eslint/parser": "^3.3.0",
"@types/highlightjs": "^9.12.0",
"@types/jest": "^24.0.18",
"@types/katex": "^0.10.2",
"@types/marked": "^0.6.5",
"@types/node": "^10.13.0",
"@typescript-eslint/eslint-plugin": "^2.4.0",
"@typescript-eslint/parser": "^2.4.0",
"ansi_up": "^4.0.4",
"arrify": "^2.0.1",
"common-path-prefix": "^3.0.0",
"core-js": "^3.6.5",
"csso-cli": "^3.0.0",
"core-js": "^3.2.1",
"dedent": "^0.7.0",
"eslint": "^7.3.0",
"eslint-config-standard-with-typescript": "^18.0.2",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-plugin-import": "^2.21.2",
"eslint-plugin-node": "^11.1.0",
"eslint": "^6.5.1",
"eslint-config-standard-with-typescript": "^10.0.0",
"eslint-import-resolver-ts": "^0.4.0",
"eslint-plugin-import": "^2.18.0",
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"fs-extra": "^8.1.0",
"jest": "^25.5.4",
"jest-chain": "^1.1.5",
"node-html-parser": "^1.2.19",
"jest": "^24.9.0",
"jest-chain": "^1.1.2",
"nodom": "^2.3.0",
"npm-run-all": "^4.1.5",
"patch-package": "^6.2.2",
"postinstall-postinstall": "^2.1.0",
"rimraf": "^3.0.2",
"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",
"rimraf": "^3.0.0",
"rollup": "^1.23.0",
"rollup-plugin-add-git-msg": "^1.0.3",
"rollup-plugin-babel": "^4.3.3",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-terser": "^5.1.2",
"rollup-plugin-typescript2": "^0.24.3",
"ts-jest": "^24.1.0",
"ts-node": "^8.4.1",
"ts-transformer-export-default-name": "^0.1.0",
"ts-transformer-inline-file": "^0.1.1",
"ttypescript": "^1.5.10",
"typescript": "~3.9.5",
"wsrun": "^5.2.1",
"yarn-version-bump": "^0.0.3",
"yazl": "^2.5.1"
"ttypescript": "^1.5.7",
"typescript": "~3.6.4",
"wsrun": "^5.0.2",
"yarn-version-bump": "^0.0.3"
},
"workspaces": [
"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",
"version": "0.3.0",
"version": "0.1.0-beta.7",
"description": "Convert Jupyter Notebook to static HTML",
"author": "Jakub Jirutka <jakub@jirutka.cz>",
"license": "MIT",
@ -25,8 +25,8 @@
],
"scripts": {
"build": "ttsc --build",
"clean": "rimraf coverage/ lib/ .eslintcache .tsbuildinfo",
"lint": "PKGDIR=$PWD; cd ../../ && eslint --cache --ext .ts,.tsx,.js $PKGDIR",
"clean": "rimraf coverage/ lib/ .tsbuildinfo",
"lint": "eslint --ext .ts,.tsx,.js .",
"prepublishOnly": "run-s readme2md",
"test": "jest --detectOpenHandles --coverage --verbose",
"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.
export type MinimalElement = {
innerHTML: string,
setAttribute: (name: string, value: string) => void,
appendChild: (child: any) => unknown,
setAttribute (name: string, value: string): void,
appendChild (child: any): any,
}
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
return (data: string): TElement => {
const math = (extractMathRx.exec(data) ?? [])[1]
const math = (extractMathRx.exec(data) || [])[1]
return math
? el('div', ['latex-output'], renderMath(math))
: el('div', ['html-output'], data)

View file

@ -3,6 +3,6 @@ import * as mathExtractor from './mathExtractor'
export { default as createElementCreator, ElementCreator, MinimalElement } from './elementCreator'
export { default as createHtmlRenderer } from './htmlRenderer'
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 { default as version } from './version'

View file

@ -1,53 +1,36 @@
const htmlEntities: Record<string, string> = {
const htmlEntities = {
'&': '&amp;',
'<': '&lt;',
'>': '&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 }
? T['__call__']
: 'subclass does not implement method __call__'
/* 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
const fn = function (...args: any[]) {
return (template[funcName] as any)(...args)
}
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 */
return Object.assign(fn, template) as any
}
/**
* Escapes characters with special meaning in HTML with the corresponding
* HTML entities.
*/
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) {
processMath(unescape, math, blocks, startIdx ?? 0, lastIdx)
processMath(unescape, math, blocks, startIdx || 0, lastIdx)
startIdx = endDelim = lastIdx = null
}
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 {
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}@@`)
}

View file

@ -1,79 +1,78 @@
// 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
// then manually polished.
/* eslint-disable @typescript-eslint/member-ordering */
/** Jupyter Notebook v4.3. */
export interface Notebook {
/** Notebook root-level metadata. */
metadata: NotebookMetadata
metadata: NotebookMetadata,
/** 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. */
nbformat: 4
nbformat: 4,
/** Array of cells of the current notebook. */
cells: Cell[]
cells: Cell[],
}
/** Notebook root-level metadata. */
export interface NotebookMetadata {
/** Kernel information. */
kernelspec?: KernelSpec
kernelspec?: KernelSpec,
/** 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. */
orig_nbformat?: number
orig_nbformat?: number,
/** The title of the notebook document */
title?: string
title?: string,
/** The author(s) of the notebook document */
authors?: any[]
authors?: any[],
/** Extra properties. */
[key: string]: any
[key: string]: any,
}
/** Kernel information. */
export interface KernelSpec {
/** Name of the kernel specification. */
name: string
name: string,
/** Name to display in UI. */
display_name: string
display_name: string,
/** Extra properties. */
[key: string]: any
[key: string]: any,
}
/** Kernel information. */
export interface LanguageInfo {
/** The programming language which this kernel runs. */
name: string
name: string,
/** 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. */
file_extension?: string
file_extension?: string,
/** The mimetype corresponding to files in this language. */
mimetype?: string
mimetype?: string,
/** The pygments lexer to use for code in this language. */
pygments_lexer?: string
pygments_lexer?: string,
/** Extra properties. */
[key: string]: any
[key: string]: any,
}
@ -90,46 +89,46 @@ export enum CellType {
interface BaseCell {
/** String identifying the type of cell. */
cell_type: CellType
cell_type: CellType,
/** Cell-level metadata. */
metadata: CellMetadata
metadata: CellMetadata,
/** Contents of the cell, represented as an array of lines. */
source: MultilineString
source: MultilineString,
}
/** Notebook raw nbconvert cell. */
export interface RawCell extends BaseCell {
/** String identifying the type of cell. */
cell_type: CellType.Raw
cell_type: CellType.Raw,
/** Cell-level metadata. */
metadata: CellMetadata & {
/** Raw cell metadata format for nbconvert. */
format?: string,
}
},
/** Media attachments (e.g. inline images), stored as mimebundle keyed by filename. */
attachments?: MediaAttachments
attachments?: MediaAttachments,
}
/** Notebook markdown cell. */
export interface MarkdownCell extends BaseCell {
/** 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. */
attachments?: MediaAttachments
attachments?: MediaAttachments,
}
/** Notebook code cell. */
export interface CodeCell extends BaseCell {
/** String identifying the type of cell. */
cell_type: CellType.Code
cell_type: CellType.Code,
/** Cell-level metadata. */
metadata: CellMetadata & {
@ -139,38 +138,38 @@ export interface CodeCell extends BaseCell {
/** Whether the cell's output is scrolled, unscrolled, or autoscrolled. */
scrolled?: true | false | 'auto',
}
},
/** 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. */
execution_count: number | null
execution_count: number | null,
}
export interface CellMetadata {
/** 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
* 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.
*/
name?: string
name?: string,
/** The cell's tags. Tags must be unique, and must not contain commas. */
tags?: string[]
tags?: string[],
/** Extra properties. */
[key: string]: any
[key: string]: any,
}
export interface MediaAttachments {
/** The attachment's data stored as a mimebundle. */
[filename: string]: MimeBundle
[filename: string]: MimeBundle,
}
@ -189,62 +188,62 @@ export enum OutputType {
export interface ExecuteResult {
/** Type of cell output. */
output_type: OutputType.ExecuteResult
output_type: OutputType.ExecuteResult,
/** A result's prompt number. */
execution_count: number | null
execution_count: number | null,
/** A mime-type keyed dictionary of data */
data: MimeBundle
data: MimeBundle,
/** Cell output metadata. */
metadata: {
[k: string]: any,
}
},
}
/** Data displayed as a result of code cell execution. */
export interface DisplayData {
/** Type of cell output. */
output_type: OutputType.DisplayData
output_type: OutputType.DisplayData,
/** A mime-type keyed dictionary of data */
data: MimeBundle
data: MimeBundle,
/** Cell output metadata. */
metadata: {
[k: string]: any,
}
},
}
/** Stream output from a code cell. */
export interface StreamOutput {
/** Type of cell output. */
output_type: OutputType.Stream
output_type: OutputType.Stream,
/** The name of the stream (stdout, stderr). */
name: string
name: string,
/** 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. */
export interface ErrorOutput {
/** Type of cell output. */
output_type: OutputType.Error
output_type: OutputType.Error,
/** The name of the error. */
ename: string
ename: string,
/** The value, or message, of the error. */
evalue: string
evalue: string,
/** The error's traceback, represented as an array of strings. */
traceback: string[]
traceback: string[],
}
@ -253,7 +252,7 @@ export interface ErrorOutput {
export interface MimeBundle {
/** 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[]

View file

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

View file

@ -1,6 +1,5 @@
import { $INLINE_JSON } from 'ts-transformer-inline-file'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { version } = $INLINE_JSON('../package.json')
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> {
readonly salutation: string
constructor (salutation: string) {
super()
this.salutation = salutation
describe('returned value', () => {
const template = {
str: 'allons-y!',
func1: jest.fn().mockReturnValue(1),
func2: jest.fn().mockReturnValue(2),
}
const subject = callableObject('func1', template)
__call__ (name: string) {
return this.salute(name)
}
salute (name: string) {
return `${this.salutation}, ${name}!`
}
}
describe('subclass', () => {
it('can be instantiated using new', () => {
expect(() => new FixtureCallable('Hello') ).not.toThrow()
})
})
describe('subclass instance', () => {
let instance: FixtureCallable
beforeEach(() => {
instance = new FixtureCallable('Hello')
it('is a function that calls the specified template function', () => {
expect( subject ).toBeInstanceOf(Function)
expect( subject('a', 'b') ).toBe(1)
expect( template.func1 ).toBeCalledWith('a', 'b')
})
it('is an instance of its class', () => {
expect( instance ).toBeInstanceOf(FixtureCallable)
expect( instance.salutation ).toBe('Hello')
expect( instance.salute('world') ).toBe('Hello, world!')
it('is not the same function as the specified template function', () => {
expect( subject ).not.toBe(template.func1)
})
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
it('has all enumerable properties of the given template', () => {
expect( subject )
.toHaveProperty('str', template.str)
.toHaveProperty('func1', template.func1)
.toHaveProperty('func2', template.func2)
})
})
})

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,7 @@
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 babel from 'rollup-plugin-babel'
import commonjs from 'rollup-plugin-commonjs'
import resolve from 'rollup-plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
import ttypescript from 'ttypescript'
import typescript from 'rollup-plugin-typescript2'
@ -13,10 +12,10 @@ import pkg from './package.json'
const extensions = ['.mjs', '.js', '.ts']
const globals = {
'anser': 'anser',
'highlight.js': 'hljs',
'katex': 'katex',
'marked': 'marked',
anser: 'anser',
highlightjs: 'hljs',
katex: 'katex',
marked: 'marked',
}
const plugins = [
@ -33,7 +32,6 @@ const plugins = [
incremental: true,
},
},
clean: true,
}),
// Resolve node modules.
resolve({
@ -45,7 +43,6 @@ const plugins = [
// 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,
@ -68,25 +65,22 @@ const plugins = [
'* 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: 5,
include: [/^.+\.min\.js$/],
output: {
// Preserve comment injected by addGitMsg.
comments: RegExp(`\\$\\{${pkg.name}\\}`),
},
}),
]
const output = (filename, extra = {}) => [false, true].map(minify => ({
const output = (filename, extra = {}) => ['.js', '.min.js'].map(ext => ({
name: pkg.name,
file: `${filename}${minify ? '.min.js' : '.js'}`,
file: `${filename}${ext}`,
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,
}))

View file

@ -7,7 +7,7 @@ function unescapeHTML (input: string): string {
return new DOMParser()
.parseFromString(input, 'text/html')
.documentElement
.textContent ?? ''
.textContent || ''
}
/**
@ -39,7 +39,6 @@ export function autoRender (opts: NbRendererOpts = {}): void {
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

@ -2,5 +2,6 @@
// via global variable. It's an alternative to Anser that doesn't provide
// bundle for browsers.
declare class AnsiUp {
// eslint-disable-next-line @typescript-eslint/camelcase
ansi_to_html (input: string): string
}

View file

@ -1,18 +1,19 @@
import anser from 'anser'
import hljs from 'highlight.js'
import hljs from 'highlightjs'
import katex, { KatexOptions } from 'katex'
import marked from 'marked'
import marked, { MarkedOptions } from 'marked'
import {
createElementCreator,
createHtmlRenderer,
createNbRenderer,
MinimalElement,
NbRenderer,
NbRendererOpts as BaseOptions,
Notebook,
} from 'ipynb2html-core'
import buildMarkdownRenderer, { MarkedOptions } from './markdownRenderer'
import buildMarkdownRenderer from './markdownRenderer'
export { default as version } from './version'
@ -42,7 +43,7 @@ export type NbRendererOpts<TElement = HTMLElement> = BaseOptions<TElement> & {
* for this module's function.
*/
type MinimalDocument<TElement extends MinimalElement> = {
createElement: (tag: string) => TElement,
createElement (tag: string): TElement,
}
const defaultKatexOpts: KatexOptions = {
@ -50,14 +51,9 @@ const defaultKatexOpts: KatexOptions = {
throwOnError: false,
}
const defaultMarkedOpts: MarkedOptions = {
headerAnchors: true,
headerIdsStripAccents: true,
}
function hljsCodeHighlighter (code: string, lang: string): string {
return hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang }).value
? hljs.highlight(lang, code).value
: code
}
@ -120,15 +116,18 @@ export function createRenderer <TElement extends MinimalElement> (
codeHighlighter = hljsCodeHighlighter
}
if (!markdownRenderer && marked) {
const markedOpts = { ...defaultMarkedOpts, ...opts.markedOpts }
markdownRenderer = buildMarkdownRenderer(markedOpts, katexOpts)
if (katex) {
markdownRenderer = buildMarkdownRenderer(opts.markedOpts, katexOpts)
} else {
markdownRenderer = (text) => marked.parse(text, opts.markedOpts)
}
}
if (!dataRenderers['text/html'] && katex) {
const mathRenderer = (tex: string) => katex.renderToString(tex, katexOpts)
dataRenderers['text/html'] = createHtmlRenderer({ elementCreator, mathRenderer })
}
return new NbRenderer(elementCreator, {
return createNbRenderer(elementCreator, {
ansiCodesRenderer,
codeHighlighter,
dataRenderers,

View file

@ -1,86 +1,38 @@
import hljs from 'highlight.js'
import hljs from 'highlightjs'
import katex, { KatexOptions } from 'katex'
import marked, { Slugger } from 'marked'
import marked, { MarkedOptions } from 'marked'
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 {
return hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang, ignoreIllegals: true }).value
? hljs.highlight(lang, code, true).value
: code
}
const renderer = (markedOpts: MarkedOptions) => (markdown: string) => marked.parse(markdown, markedOpts)
const rendererWithMath = (markedOpts: MarkedOptions, katexOpts: KatexOptions) => (markdown: string) => {
const [text, math] = mathExtractor.extractMath(markdown)
const html = marked.parse(text, markedOpts)
const mathHtml = math.map(({ value, displayMode }) => {
return katex.renderToString(value, { ...katexOpts, displayMode })
})
return mathExtractor.restoreMath(html, mathHtml)
}
/**
* Returns a pre-configured marked parser with (optional) math support (using
* KaTeX) and code highlighter (highlight.js).
* 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 = {}): MarkdownRenderer => {
markedOpts = { renderer: new Renderer(markedOpts), ...markedOpts }
if (hljs) { // highlight.js may be an optional dependency (in browser bundle)
export default (markedOpts: MarkedOptions = {}, katexOpts: KatexOptions = {}) => {
if (hljs) { // highlightjs 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)
/**
* Converts the given *markdown* into HTML.
*/
return (markdown: string): string => {
const [text, math] = mathExtractor.extractMath(markdown)
const html = marked.parse(text, markedOpts)
const mathHtml = math.map(({ value, displayMode }) => {
return katex.renderToString(value, { ...katexOpts, displayMode })
})
return mathExtractor.restoreMath(html, mathHtml)
}
}

View file

@ -3,18 +3,18 @@ import marked from 'marked'
import { Notebook } from 'ipynb2html-core'
class EmptyRenderer extends marked.Renderer {}
class EmptyRenderer extends marked.Renderer {
// Override all the EmptyRenderer's methods inherited from marked.Renderer to
// always return an empty string.
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
const RendererProto = marked.Renderer.prototype
for (const prop of Object.getOwnPropertyNames(RendererProto)) {
if (prop !== 'constructor' && typeof (RendererProto as any)[prop] === 'function') {
(EmptyRenderer.prototype as any)[prop] = () => ''
constructor (options?: marked.MarkedOptions) {
super(options)
for (const prop in this) {
if (this[prop] instanceof Function) {
(this as any)[prop] = () => ''
}
}
}
}
/* eslint-enable @typescript-eslint/no-unsafe-member-access */
class MainTitleRenderer extends EmptyRenderer {
_titleFound = false

View file

@ -1,6 +1,5 @@
import { $INLINE_JSON } from 'ts-transformer-inline-file'
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { version } = $INLINE_JSON('../package.json')
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 { Notebook, CellType, MarkdownCell } from 'ipynb2html-core'
const markdownCell = (source: string | string[]): MarkdownCell => ({
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",
"compilerOptions": {
"baseUrl": ".",
},
"references": [
{ "path": "../" },
],
"extends": "../tsconfig.json",
"include": ["."],
}

View file

@ -5,6 +5,11 @@
"outDir": "./lib",
"tsBuildInfoFile": "./.tsbuildinfo",
"baseUrl": ".",
"paths": {
"*": ["../../types/*"],
"@/*": ["./src/*"],
"~/*": ["../../*"],
},
},
"include": [
"./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
( 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,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>>
@ -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>> {
return asMock(fn).mock.results
.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 {
return mockResults(fn).pop()
export function mockLastResult <F extends Callable> (fn: F): ReturnType<F> {
return mockResults(fn).pop() as ReturnType<F>
}

View file

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

View file

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

View file

@ -10,41 +10,33 @@ type Options = {
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace jest {
interface Matchers<R, T> {
toMatchElement: (expected: HTMLElement, opts?: Options) => R
interface Matchers<R> {
toMatchElement (expected: HTMLElement, opts?: Options): R,
}
}
}
export const AnythingNode = new class extends Node {
render () { return '<!--Anything-->' }
toString () { return this.render() }
}()
export const Anything = (): Node => 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
}
export const Anything = () => AnythingNode
function filterWildcardChildren (rec: Node, exp: Node): void {
if (exp.firstChild === AnythingNode
&& exp.childNodes.length === 1
&& (rec as HTMLElement).innerHTML
&& exp.children.length === 1
&& rec instanceof HTMLElement
&& rec.innerHTML
) {
if (isWritable(rec, 'innerHTML')) {
(rec as HTMLElement).innerHTML = ''
}
rec.childNodes.splice(0, rec.childNodes.length, AnythingNode)
rec.innerHTML = ''
rec.children.splice(0, rec.children.length, AnythingNode)
return
}
for (let i = 0; i < exp.childNodes.length && i < rec.childNodes.length; i++) {
if (exp.childNodes[i] === AnythingNode) {
rec.childNodes[i] = AnythingNode
for (let i = 0; i < exp.children.length && i < rec.children.length; i++) {
if (exp.children[i] === AnythingNode) {
rec.children[i] = AnythingNode
} 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.className = ''
}
node.childNodes.forEach(clearAttributes)
node.children.forEach(clearAttributes)
}
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)
}
filterWildcardChildren(received, expected)

View file

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

View file

@ -42,9 +42,7 @@
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "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'. */
"*": ["../../types/*"], /* This path is relative from baseUrl which is defined in tsconfig.json that includes this file inside each project. */
},
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "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. */
"./types",

View file

@ -3,6 +3,5 @@
"references": [
{ "path": "./packages/ipynb2html-core" },
{ "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/*"],
"~/*": ["../../../*"],
},
},
}

5774
yarn.lock

File diff suppressed because it is too large Load diff