Compare commits

...

9 commits

Author SHA1 Message Date
Jakub Jirutka
2b662a10cb TEMP: Deploy to gh-pages from branch viewer 2021-06-12 15:20:22 +02:00
Jakub Jirutka
813a55718e CI: Add job for deploying viewer to gh-pages 2021-06-12 15:18:25 +02:00
Jakub Jirutka
ae9c35a367 Viewer: Add support for adding content hash into asset file names 2021-06-12 15:18:25 +02:00
Jakub Jirutka
3afc120a3b Viewer: Generate index.html from template with injected asset paths 2021-06-12 15:18:25 +02:00
Jakub Jirutka
ed9d86b894 Viewer: Run PostCSS via Rollup 2021-06-12 15:18:25 +02:00
Jakub Jirutka
9c3002271f Viewer: Build dev and minified bundle separately
This is a preparation for the upcoming commits.
2021-06-12 15:18:25 +02:00
Jakub Jirutka
b27a8d79f7 Restructuralize package build scripts 2021-06-12 15:18:25 +02:00
Jakub Jirutka
eead007e6f Rename package script "build" to "build-ts" 2021-06-12 15:18:25 +02:00
Jakub Jirutka
dd0f34e7a9 Create package ipynb2html-viewer 2021-06-12 15:18:24 +02:00
35 changed files with 2869 additions and 28 deletions

View file

@ -26,7 +26,7 @@ jobs:
- run: yarn test
- run: yarn lint
publish:
publish-release:
name: Publish to npmjs and GitHub Releases
needs: [test]
if: startsWith(github.ref, 'refs/tags/v')
@ -66,3 +66,26 @@ jobs:
dist/*.tar.gz
packages/ipynb2html-cli/dist/*.tar.gz
packages/ipynb2html-cli/dist/*.zip
deploy-viewer:
name: Build and deploy viewer to ipynb.js.org
needs: [test]
if: github.ref == 'refs/heads/viewer' && github.event_name == 'push' # TODO: replace 'viewer' with 'master'
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0 # fetch all history to make `git describe` work
- run: yarn install
- name: Build ipynb-viewer
run: BUILD_DESTDIR=$(pwd)/site yarn workspace ipynb2html-viewer build-site
- name: Deploy to gh-pages
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: site
CLEAN: true

View file

@ -4,6 +4,7 @@
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"cpylua.language-postcss",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"gamunu.vscode-yarn",

View file

@ -8,6 +8,11 @@
"typescript"
],
"files.associations": {
// XXX: PostCSS extension doesn't provide as nice autocompletion as
// VSCode's native CSS support. But this doesn't understand nested rules...
"*.pcss": "css",
},
"files.encoding": "utf8",
"files.eol": "\n",
"files.exclude": {

View file

@ -215,6 +215,7 @@ ifndef::npm-readme[]
* https://yarnpkg.com[yarn] for dependencies management and building
* https://eslint.org[ESLint] for linting JS/TypeScript code
* https://jestjs.io[Jest] for testing
* https://postcss.org[PostCSS] for transforming stylesheets to plain CSS
* https://rollupjs.org[Rollup] for building single-file bundles
@ -254,6 +255,7 @@ If you use Visual Studio Code, you should install the following extensions:
* link:{vs-marketplace-uri}ryanluker.vscode-coverage-gutters[Coverage Gutters]
* link:{vs-marketplace-uri}EditorConfig.EditorConfig[EditorConfig for VS Code]
* link:{vs-marketplace-uri}dbaeumer.vscode-eslint[ESLint]
* link:{vs-marketplace-uri}cpylua.language-postcss[language-postcss]
* link:{vs-marketplace-uri}Orta.vscode-jest[Jest] (and link:{vs-marketplace-uri}shtian.jest-snippets-standard[Jest Snippets Standard Style])
* link:{vs-marketplace-uri}gamunu.vscode-yarn[yarn]

View file

@ -3,11 +3,11 @@
"version": "0.3.0",
"private": true,
"scripts": {
"build": "ttsc --build",
"bundle": "wsrun --exclude-missing bundle",
"build": "ttsc --build && wsrun --exclude-missing build-except-ts",
"build-ts": "ttsc --build",
"clean": "rimraf coverage/ dist/ lib/ .eslintcache *.log && wsrun clean",
"lint": "eslint --cache --ext .ts,.tsx,.js .",
"postinstall": "patch-package && run-s build",
"postinstall": "patch-package && run-s build-ts",
"publish-all": "wsrun --serial publish",
"test": "jest --detectOpenHandles --coverage --verbose",
"version": "./scripts/bump-version && git add README.adoc **/package.json",
@ -21,6 +21,7 @@
"@babel/preset-env": "^7.10.3",
"@rollup/plugin-babel": "^5.0.3",
"@rollup/plugin-commonjs": "^13.0.0",
"@rollup/plugin-html": "^0.2.0",
"@rollup/plugin-node-resolve": "^8.0.1",
"@types/dedent": "^0.7.0",
"@types/jest": "^24.9.1",
@ -30,6 +31,7 @@
"arrify": "^2.0.1",
"common-path-prefix": "^3.0.0",
"core-js": "^3.6.5",
"cssnano": "^4.1.10",
"csso-cli": "^3.0.0",
"dedent": "^0.7.0",
"eslint": "^7.3.0",
@ -42,17 +44,25 @@
"fs-extra": "^8.1.0",
"jest": "^25.5.4",
"jest-chain": "^1.1.5",
"mkdirp": "^1.0.4",
"node-html-parser": "^1.2.19",
"nodom": "^2.3.0",
"npm-run-all": "^4.1.5",
"patch-package": "^6.2.2",
"postcss-inline-svg": "^4.1.0",
"postcss-nesting": "^7.0.1",
"postcss-sort-media-queries": "^1.6.24",
"postinstall-postinstall": "^2.1.0",
"rimraf": "^3.0.2",
"rollup": "^2.17.1",
"rollup-plugin-add-git-msg": "^1.1.0",
"rollup-plugin-conditional": "^3.1.2",
"rollup-plugin-executable": "^1.6.0",
"rollup-plugin-livereload": "^1.3.0",
"rollup-plugin-node-externals": "^2.2.0",
"rollup-plugin-node-license": "^0.2.0",
"rollup-plugin-postcss": "2.0.6",
"rollup-plugin-serve": "^1.0.1",
"rollup-plugin-terser": "^6.1.0",
"rollup-plugin-typescript2": "^0.27.1",
"tar": "^5.0.5",

View file

@ -29,11 +29,14 @@
"src"
],
"scripts": {
"build": "ttsc --build",
"bundle": "rollup -c && ./scripts/pack-bundle",
"build": "run-p build-except-ts build-ts",
"build-bundle": "rollup -c",
"build-except-ts": "run-s build-bundle",
"build-ts": "ttsc --build",
"clean": "rimraf coverage/ dist/ lib/ .eslintcache .tsbuildinfo",
"lint": "PKGDIR=$PWD; cd ../../ && eslint --cache --ext .ts,.tsx,.js $PKGDIR",
"prepublishOnly": "run-s readme2md",
"pack-bundle": "./scripts/pack-bundle",
"prepublishOnly": "run-s build readme2md pack-bundle",
"readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md",
"watch-ts": "ttsc --build --watch"
},

View file

@ -24,10 +24,11 @@
"src"
],
"scripts": {
"build": "ttsc --build",
"build": "run-s build-ts",
"build-ts": "ttsc --build",
"clean": "rimraf coverage/ lib/ .eslintcache .tsbuildinfo",
"lint": "PKGDIR=$PWD; cd ../../ && eslint --cache --ext .ts,.tsx,.js $PKGDIR",
"prepublishOnly": "run-s readme2md",
"prepublishOnly": "run-p build readme2md",
"test": "jest --detectOpenHandles --coverage --verbose",
"readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md",
"watch-ts": "ttsc --build --watch"

View file

@ -0,0 +1,33 @@
export default ({ files: { css: [css], js: [js] }, publicPath }) => `\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" lang="en">
<meta name="viewport" content="initial-scale=1">
<title>.ipynb viewer</title>
<link
rel="stylesheet"
href="${publicPath}${css.fileName}"
crossorigin="anonymous">
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ipynb2html@0.2.0/dist/notebook.min.css"
crossorigin="anonymous">
<link
rel="stylesheet"
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@9.15.10/build/styles/default.min.css"
crossorigin="anonymous">
<script defer src="https://cdn.jsdelivr.net/npm/ipynb2html@0.2.0/dist/ipynb2html-full.min.js" crossorigin="anonymous"></script>
<script defer src="${publicPath}${js.fileName}" crossorigin="anonymous" onload="init()"></script>
</head>
<body>
<noscript>
This page needs JavaScript. Enable JavaScript in your browser or use a different browser.
</noscript>
</body>
</html>
`

View file

@ -0,0 +1,54 @@
{
"name": "ipynb2html-viewer",
"version": "0.0.0",
"description": "Web viewer of Jupyter Notebooks",
"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": [
"browser",
"converter",
"ipython",
"jupyter",
"notebook",
"webapp"
],
"main": "lib/index.js",
"browser": "dist/ipynb-viewer.min.js",
"types": "lib/index.d.ts",
"files": [
"dist",
"lib",
"src",
"index.html"
],
"scripts": {
"build": "run-p build:*",
"build:dev": "rollup -c",
"build:prod": "BUILD_FLAGS=minify rollup -c",
"build-except-ts": "run-p build:*",
"build-site": "BUILD_FLAGS=minify,hash rollup -c",
"build-ts": "ttsc --build",
"clean": "rimraf coverage/ dist/ lib/ .eslintcache .tsbuildinfo",
"lint": "PKGDIR=$PWD; cd ../../ && eslint --cache --ext .ts,.tsx,.js $PKGDIR",
"prepublishOnly": "run-p build readme2md",
"readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md",
"watch": "rollup -c --watch",
"watch-ts": "tssc --build --watch"
},
"dependencies": {
"@hyperapp/events": "^0.0.4",
"hyperapp": "^2.0.4",
"ipynb2html": "0.3.0"
},
"browserslist": [
">0.5%",
"Firefox ESR",
"not dead"
]
}

View file

@ -0,0 +1,13 @@
module.exports = ({ env }) => ({
map: {
inline: false,
},
plugins: {
'postcss-inline-svg': {},
'postcss-nesting': {},
'postcss-sort-media-queries': {
sort: 'desktop-first',
},
'cssnano': env === 'production',
},
})

View file

@ -0,0 +1,130 @@
import addGitMsg from 'rollup-plugin-add-git-msg'
import babel from '@rollup/plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import conditional from 'rollup-plugin-conditional'
import html from '@rollup/plugin-html'
import license from 'rollup-plugin-node-license'
import livereload from 'rollup-plugin-livereload'
import postcss from 'rollup-plugin-postcss'
import resolve from '@rollup/plugin-node-resolve'
import serve from 'rollup-plugin-serve'
import { terser } from 'rollup-plugin-terser'
import ttypescript from 'ttypescript'
import typescript from 'rollup-plugin-typescript2'
import indexTemplate from './index-html'
import pkg from './package.json'
const flags = (process.env.BUILD_FLAGS || '').split(',').reduce((obj, flag) => {
obj[flag] = true
return obj
}, {})
flags.watch = !!process.env.ROLLUP_WATCH
const assetInfix =
flags.hash ? '.[hash]' :
flags.minify ? '.min' :
''
const destDir = process.env.BUILD_DESTDIR || './dist'
const extensions = ['.mjs', '.js', '.ts']
export default {
input: 'src/index.ts',
plugins: [
// Transpile TypeScript sources to JS.
typescript({
typescript: ttypescript,
tsconfigOverride: {
compilerOptions: {
target: 'ES5',
module: 'ESNext',
declaration: false,
declarationMap: false,
composite: false,
incremental: true,
},
},
clean: true,
}),
// Resolve node modules.
resolve({
extensions,
mainFields: ['browser', 'module', 'main'],
}),
// Convert CommonJS modules to ES6 modules.
commonjs(),
// Transpile all sources for older browsers and inject needed polyfills.
babel({
babelrc: false,
// To avoid Babel injecting core-js polyfills into core-js.
exclude: [/node_modules\/core-js\//],
extensions,
presets: [
[
'@babel/env', {
corejs: 3,
debug: false,
modules: false,
useBuiltIns: 'usage', // inject polyfills
// targets: reads from "browserslist" in package.json
},
],
],
}),
// Convert PostCSS styles to CSS.
postcss({
autoModules: false,
extract: true,
sourceMap: true,
minimize: !!flags.minify,
}),
// Generate index.html from the template.
html({
template: indexTemplate,
}),
conditional(!flags.watch, [
// 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' }),
]),
conditional(flags.minify, [
// Minify JS.
terser({
ecma: 5,
output: {
// Preserve comment injected by addGitMsg and license.
comments: RegExp(`(?:\\$\\{${pkg.name}\\}|Bundled npm packages)`),
},
}),
]),
// Use only when running in watch mode...
conditional(flags.watch, () => [
serve({
contentBase: destDir,
}),
livereload(),
]),
],
external: [
'ipynb2html',
],
output: {
dir: destDir,
entryFileNames: `ipynb-viewer${assetInfix}.js`,
assetFileNames: `ipynb-viewer${assetInfix}.[ext]`,
format: 'umd',
name: 'init',
sourcemap: true,
globals: {
ipynb2html: 'ipynb2html',
},
},
}

View file

@ -0,0 +1,220 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-use-before-define */
import * as hyperapp from 'hyperapp'
import * as ipynb2html from 'ipynb2html'
import {
historyPush,
readFile,
readFromStorage,
request,
ResponseError,
setPageTitle,
writeToStorage,
} from './effects'
import { State, ErrorMessage } from './types'
type Action<Payload = void> = hyperapp.Action<State, Payload>
type Dispatchable<DPayload = void, CPayload = any> = hyperapp.Dispatchable<State, DPayload, CPayload>
const initState: State = {
mainView: 'BLANK',
fileUrl: null,
title: '.ipynb viewer',
notebook: null,
error: null,
dragover: false,
historyIdx: 0,
fromHistory: false,
}
// Helpers
// FIXME: This is just a quick & dirty solution.
const isNotebook = (notebook: any): notebook is ipynb2html.Notebook => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return typeof notebook === 'object' && !!notebook?.metadata && !!notebook?.cells
}
const isFileDataTransfer = (transfer: DataTransfer | null): transfer is DataTransfer => {
return !!transfer && !!Array.from(transfer.items).find(item => item.kind === 'file')
}
const rewriteFileUrl = (url: string): string => {
const { host, pathname } = new URL(url)
switch (host) {
case 'gist.github.com':
return `https://gist.githubusercontent.com${pathname}/raw/`
default:
return url
}
}
const stateToUrlQuery = (state: State): string => {
const params: Record<string, string> = {}
if (state.fileUrl) {
params.url = state.fileUrl
}
return new URLSearchParams(params).toString()
}
// Actions
export const Initialize: hyperapp.ActionOnInit<State> = () => {
const url = new URLSearchParams(window.location.search).get('url')
return url
? (FetchNotebook({ ...initState, fileUrl: url }) as hyperapp.DispatchableOnInit<State>)
: initState
}
export const ShowNotebook: Action<unknown> = (state, notebook): Dispatchable => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
notebook = typeof notebook === 'string' ? JSON.parse(notebook) : notebook
if (!isNotebook(notebook)) {
return [ShowError, {
title: 'Invalid Format',
message: 'The file is not in Jupyter Notebook format.',
}]
}
const title = ipynb2html.readNotebookTitle(notebook) ?? initState.title
const effects = [
setPageTitle(title),
]
if (!state.fromHistory) {
state.historyIdx++
effects.push(
writeToStorage({
key: `notebook/${state.historyIdx}`,
value: notebook,
session: true,
}),
historyPush({
state: { ...state, notebook: null },
urlQuery: stateToUrlQuery(state),
}),
)
}
return [
{
...state,
title,
notebook,
mainView: 'NOTEBOOK',
fromHistory: false,
error: null,
},
...effects,
]
}
export const ShowBlank: Action = (state): Dispatchable => [
(state = { ...state, mainView: 'BLANK', fileUrl: null, notebook: null }),
setPageTitle(initState.title!),
historyPush({
state,
urlQuery: '',
}),
]
export const ShowError: Action<Error | ErrorMessage> = (state, error): Dispatchable => {
let title = 'title' in error ? error.title : 'Error'
let message = error.message
let detail = error.message
if (error instanceof SyntaxError && error.message.startsWith('JSON.parse:')) {
title = 'Malformed Notebook'
message = 'The file is not a valid JSON.'
} else if (error instanceof ResponseError) {
title = 'HTTP Error'
message = 'Failed to fetch notebook from the given address.'
} else if (error instanceof TypeError
&& (error.message === 'Failed to fetch' || error.message.startsWith('NetworkError '))
) {
title = 'Network Error'
message = 'Failed to fetch notebook from the given address.'
} else {
detail = ''
}
console.error(error)
return {
...state,
mainView: 'ERROR',
error: { title, message, detail },
notebook: null,
}
}
export const SubmitFileUrl: Action = (state): Dispatchable => (
state.fileUrl ? FetchNotebook : ShowBlank
)
export const SetFileUrl: Action<string> = (state, fileUrl): Dispatchable => (
{ ...state, fileUrl }
)
export const FetchNotebook: Action = (state): Dispatchable => [
{ ...state, mainView: 'FETCHING', notebook: null },
request({
url: rewriteFileUrl(state.fileUrl!),
options: {
mode: 'cors',
redirect: 'follow',
referrerPolicy: 'no-referrer',
},
onSuccess: ShowNotebook,
onError: ShowError,
}),
]
export const LoadLocalFile: Action<File> = (state, file): Dispatchable => [
{ ...state, fileUrl: null },
readFile({ file, onSuccess: ShowNotebook }),
]
export const HistoryPop: Action<PopStateEvent> = (state, event): Dispatchable => [
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
{ ...(state = event.state ?? state), fromHistory: true },
readFromStorage({
key: `notebook/${state.historyIdx}`,
session: true,
onSuccess: ShowNotebook,
}),
]
export const DragOver: Action<DragEvent> = (state, event): Dispatchable => {
if (!isFileDataTransfer(event.dataTransfer)) {
return state
}
event.preventDefault()
event.dataTransfer.dropEffect = 'copy'
return { ...state, dragover: true }
}
export const DragLeave: Action = (state): Dispatchable => ({
...state,
dragover: false,
})
export const DropFile: Action<DragEvent> = (state, event): Dispatchable => {
if (!isFileDataTransfer(event.dataTransfer)) {
return state
}
event.preventDefault()
const file = event.dataTransfer.files?.[0]
return [
{ ...state, dragover: false, fileUrl: null },
readFile({ file, onSuccess: ShowNotebook }),
]
}

View file

@ -0,0 +1,113 @@
import { AnyState, Dispatch, Dispatchable, Effect } from 'hyperapp'
function fx <State extends AnyState, Props> (
fn: (dispatch: Dispatch<State>, props: Props) => void,
) {
return (props: Props): Effect<State, Props> => [fn, props]
}
type HistoryPush = (props: { state: any, urlQuery?: string }) => Effect<any>
export const historyPush: HistoryPush = fx((_, { state, urlQuery }) => {
const url = new URL(window.location.href)
if (urlQuery != null) {
url.search = urlQuery
}
history.pushState(state, document.title, url.href)
})
type ReadFile = <S extends AnyState>(props: {
file: File,
onSuccess: Dispatchable<S, string>,
onError?: Dispatchable<S, DOMException>,
}) => Effect<S>
export const readFile: ReadFile = fx((dispatch, { file, onSuccess, onError }) => {
const reader = new FileReader()
reader.onload = (event) => {
dispatch(onSuccess, event.target?.result?.toString() ?? '')
}
if (onError) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
reader.onerror = () => dispatch(onError, reader.error!)
}
reader.readAsText(file, 'utf8')
})
type ReadFromStorage = <S extends AnyState, T = unknown>(props: {
key: string,
session: boolean,
onSuccess: Dispatchable<S, T>,
}) => Effect<S>
export const readFromStorage: ReadFromStorage = fx((dispatch, { key, session, onSuccess }) => {
const storage = session ? window.sessionStorage : window.localStorage
const value = storage.getItem(key)
if (value == null) {
// FIXME
} else {
dispatch(onSuccess, JSON.parse(value))
}
})
type WriteToStorage = (props: {
key: string,
value: unknown,
session: boolean,
}) => Effect<any>
export const writeToStorage: WriteToStorage = fx((_, { key, value, session }) => {
const storage = session ? window.sessionStorage : window.localStorage
if (value == null) {
storage.removeItem(key)
} else {
storage.setItem(key, JSON.stringify(value))
}
})
export class ResponseError extends Error {
constructor (readonly response: Response) {
super(`${response.status} ${response.statusText}`)
// See https://github.com/Microsoft/TypeScript/issues/13965 *facepalm*
Object.setPrototypeOf(this, new.target.prototype)
}
}
type Request = <S extends AnyState>(props: {
url: Parameters<typeof fetch>[0],
options?: Parameters<typeof fetch>[1],
onSuccess: Dispatchable<S, unknown>,
onError: Dispatchable<S, ResponseError | Error>,
}) => Effect<S>
export const request: Request = fx((dispatch, { url, options = {}, onSuccess, onError }) => {
options.headers = {
...options.headers,
accept: 'application/json',
}
fetch(url, options)
.then(async response => {
if (!response.ok) {
throw new ResponseError(response)
}
return await response.json() as unknown
})
.then(result => dispatch(onSuccess, result))
.catch(error => dispatch(onError, error))
})
type SetPageTitle = (title: string) => Effect<any>
export const setPageTitle: SetPageTitle = fx((_, title: string) => {
document.title = title
})

View file

@ -0,0 +1,7 @@
import { createOnCustomEvent } from '@hyperapp/events'
export const onDragOver = createOnCustomEvent<DragEvent>('dragover')
export const onDragLeave = createOnCustomEvent<DragEvent>('dragleave')
export const onDrop = createOnCustomEvent<DragEvent>('drop')
export const onPopState = createOnCustomEvent<PopStateEvent>('popstate')

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M507 426L283 54a31 31 0 00-54 0L5 426a31 31 0 0026 48h450a31 31 0 0026-48zM256 167c13 0 24 8 24 20 0 40-5 96-5 136 0 10-11 14-19 14-10 0-19-4-19-14 0-40-5-96-5-136 0-12 11-20 24-20zm0 244a25 25 0 010-51c14 0 26 12 26 26 0 13-12 25-26 25z" />
</svg>

After

Width:  |  Height:  |  Size: 323 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 444.819 444.819">
<path d="M434 114l-21-21c-8-7-16-11-26-11s-19 4-26 11L222 232 84 93a37 37 0 00-52 0l-21 21c-7 7-11 16-11 26s4 19 11 26l186 186a35 35 0 0052 0l185-186a37 37 0 000-52z" />
</svg>

After

Width:  |  Height:  |  Size: 250 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 445 445">
<path d="M434 279L248 93c-7-7-16-11-26-11s-18 4-25 11L11 279c-7 7-11 16-11 26s4 18 11 25l21 22c7 7 16 11 26 11s19-4 26-11l138-139 139 139c7 7 16 11 26 11s19-4 26-11l21-22a35 35 0 000-52z" />
</svg>

After

Width:  |  Height:  |  Size: 263 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<path d="M3 4a3 3 0 00-3 3v26l3.2-16.9c.5-2.6 2.4-4.1 5.2-4.1H47v-1a3 3 0 00-3-3H18c-.2-.1-.8-1-1.1-1.5C16 5.3 15.3 4 14 4zm5.4 10c-1.2 0-2.8.4-3.2 2.5L0 44v.1a3 3 0 003 3h39a3 3 0 003-3l5-26.8V17a3 3 0 00-3-3zm0 0" />
</svg>

After

Width:  |  Height:  |  Size: 289 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>

After

Width:  |  Height:  |  Size: 671 B

View file

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="120"
height="30"
viewBox="0 0 31.750001 7.9375002"
version="1.1"
id="svg8"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="drawing.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="40.353189"
inkscape:cy="24.629519"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1389"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-289.06248)">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:5.64444447px;line-height:1.25;font-family:Lemonada;-inkscape-font-specification:'Lemonada, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15247695"
x="0.69665426"
y="271.22836"
id="text12-5"
transform="scale(0.91908892,1.088034)"><tspan
sodipodi:role="line"
id="tspan10"
x="0.69665426"
y="271.22836"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:5.64444447px;font-family:Lemonada;-inkscape-font-specification:'Lemonada, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.15247695"><tspan
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:5.64444447px;font-family:Lemonada;-inkscape-font-specification:'Lemonada, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.15247695"
id="tspan14">ipynb</tspan></tspan></text>
<text
transform="scale(0.92984193,1.0754516)"
id="text173-0"
y="272.58124"
x="19.826342"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.5690515px;line-height:1.25;font-family:Lemonada;-inkscape-font-specification:'Lemonada, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.102001"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.5690515px;font-family:Lemonada;-inkscape-font-specification:'Lemonada, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.102001"
y="272.58124"
x="19.826342"
id="tspan171"
sodipodi:role="line">viewer</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.75 7.94">
<g aria-label="ipynbviewer">
<path d="M1.52 6.2q-.22 0-.33-.14-.1-.13-.1-.4l.02-.35q.02-.23.08-.55l.13-.75.18-.97-.25-.37q.16-.14.36-.23.2-.09.36-.09.17 0 .24.09.08.09.08.29l-.04.22-.08.41-.1.54-.12.6-.08.59q-.03.27-.03.5 0 .14.03.23.03.1.08.16-.2.21-.43.21zm.41-4.31q-.18 0-.3-.11t-.12-.3q0-.14.08-.26.07-.12.2-.19.12-.06.26-.06.19 0 .3.1.12.11.12.29 0 .15-.07.27-.08.12-.2.19-.12.07-.27.07zM4.26 6.04q-.24 0-.47-.05-.03-.07-.04-.16l-.02-.2q.54 0 .94-.22.4-.22.63-.61.22-.4.22-.92 0-.38-.13-.59-.14-.21-.38-.21-.25 0-.46.28-.22.28-.4.8-.16.53-.3 1.27l.09-2.31.32.22q.17-.47.45-.73t.61-.26q.29 0 .5.18.22.18.33.5.12.33.12.77 0 .48-.15.89-.15.41-.42.71t-.64.47q-.36.17-.8.17zm-.82 1.23q-.2 0-.3-.15-.1-.14-.1-.44l.03-.46.1-.83.15-1.1.18-1.25-.25-.37q.17-.14.36-.23.2-.09.36-.09.18 0 .24.09.08.08.08.28 0 .16-.05.45l-.13.68-.15.84q-.08.45-.13.93-.05.49-.05.98 0 .15.03.26.04.12.1.2-.1.1-.22.15-.12.06-.25.06zM7.02 7.27q-.36 0-.54-.16-.18-.15-.18-.41 0-.12.05-.24t.1-.19q.06.09.14.17.08.08.2.13.13.05.32.05.18 0 .4-.09.22-.09.46-.28.25-.18.48-.48.22-.3.4-.71.19-.42.3-.96.1-.54.1-1.22v-.17l-.01-.17q.11-.1.22-.14.1-.05.2-.05.15 0 .25.1t.1.33q0 .43-.1.93-.09.5-.27 1-.18.5-.44.96-.25.46-.58.82-.34.36-.73.57-.4.2-.87.2zm1.22-1.08q-.34 0-.54-.15-.19-.16-.28-.43-.09-.27-.12-.6l-.03-.7q0-.36-.02-.7-.01-.34-.08-.62-.06-.28-.22-.45.2-.19.44-.19.23 0 .36.15.13.15.18.4.06.25.07.57L8 4.1v.65q.02.3.08.56.05.25.18.4.13.15.37.15zM11.04 6.2q-.23 0-.33-.14-.1-.13-.1-.4l.03-.42q.03-.29.1-.66l.13-.78.15-.76-.24-.37q.15-.13.34-.22.2-.08.36-.08.18 0 .25.09.08.08.08.28 0 .16-.05.36-.04.19-.1.35l.05.03q.28-.57.62-.85.35-.28.76-.28.33 0 .49.2.16.2.16.6 0 .27-.05.57-.05.3-.13.6l-.12.64q-.05.32-.05.63 0 .27.09.4-.2.2-.4.2-.23 0-.34-.13-.1-.13-.1-.4 0-.25.05-.55l.11-.62.12-.58q.04-.27.04-.47 0-.22-.07-.32-.06-.1-.2-.1-.2 0-.39.13-.18.14-.35.39-.17.24-.3.57-.14.33-.21.7-.08.38-.08.78 0 .27.08.4-.2.2-.4.2zM15.3 6.2q-.24 0-.4-.09-.18-.08-.26-.27-.1-.2-.1-.54 0-.2.04-.5.03-.3.1-.73l.14-.95.2-1.15-.25-.36q.15-.15.35-.23.2-.1.36-.1.17 0 .25.1.07.07.07.27 0 .21-.1.65l-.2.99.05.03q.19-.46.47-.71.3-.26.63-.26.3 0 .5.18.22.17.33.48.12.31.12.74 0 .52-.17.97-.17.44-.48.78-.32.33-.74.51t-.91.18zm-.12-.48q.35-.02.66-.17.3-.15.53-.4.23-.25.35-.57.13-.33.13-.7 0-.38-.13-.59-.14-.21-.38-.21-.22 0-.43.19-.21.18-.38.53-.16.35-.26.84-.1.48-.1 1.08z"/>
<path d="M19.32 4.18q-.16 0-.25-.08-.08-.06-.12-.24-.03-.17-.03-.47v-.45l-.01-.42q-.01-.2-.06-.37-.04-.16-.14-.28.1-.1.24-.1.14 0 .21.12.08.11.1.37.04.26.04.7V3.85h.03q.27-.44.44-.76.16-.32.24-.54.07-.23.07-.36 0-.18-.07-.32l.15-.07.12-.02q.09 0 .14.08.05.07.05.2 0 .12-.05.29-.05.17-.15.4l-.25.51-.35.64.05.11q-.04.05-.1.09-.07.04-.15.05-.08.03-.15.03zM21.2 4.18q-.1 0-.16-.07-.06-.06-.06-.2l.01-.22.05-.34.09-.5.14-.72-.15-.19q.08-.08.19-.12.1-.04.19-.04.1 0 .13.04.05.05.05.17l-.03.13-.05.27-.08.36-.07.4-.05.39-.02.33v.12l.06.08q-.11.1-.23.1zm.3-2.73q-.1 0-.17-.06-.06-.06-.06-.16 0-.07.04-.14.03-.06.1-.1.07-.04.15-.04.11 0 .17.06.07.05.07.15 0 .08-.04.15-.04.07-.1.1-.07.04-.16.04zM22.94 4.18q-.35 0-.55-.25-.2-.25-.2-.69 0-.32.09-.58.09-.27.25-.47.16-.2.38-.3.22-.11.48-.11t.4.14q.16.15.16.4 0 .28-.17.48-.17.19-.5.29-.32.1-.78.1l.04-.21q.5 0 .77-.17.27-.18.27-.5 0-.13-.07-.2-.06-.09-.18-.09-.15 0-.28.1-.13.1-.24.26-.1.17-.15.4-.06.23-.06.48 0 .3.1.48t.27.18q.22 0 .35-.16.14-.15.15-.42.13 0 .2.06.07.05.07.17 0 .16-.1.3-.11.13-.3.22-.17.09-.4.09zM24.8 4.18q-.15 0-.2-.1-.05-.1-.05-.39v-.54-.5q-.02-.25-.06-.44-.05-.2-.15-.34.1-.1.23-.1.14 0 .22.12.07.11.1.37t.03.7V3.85h.03q.22-.32.36-.6.14-.29.21-.5.08-.22.1-.34l.04-.14q0-.04-.03-.1l-.06-.1-.04-.08.12-.07q.05-.03.1-.03.1 0 .15.08.05.07.05.2t-.04.29l-.13.34-.22.46q-.13.28-.24.46-.1.18-.19.28-.08.1-.17.14-.08.04-.17.04zm1.14 0q-.12 0-.19-.07t-.1-.19l-.03-.28-.1-1.65q.04-.05.1-.07.06-.03.13-.03l.08.23.06 1.73h.03q.23-.44.37-.76.14-.32.2-.54.07-.23.07-.36 0-.18-.06-.32l.14-.07.12-.02q.09 0 .13.08.05.07.05.2 0 .12-.04.29-.04.17-.12.4l-.22.51-.3.64.05.11q-.04.05-.1.09-.06.04-.14.05-.06.03-.13.03zM28.13 4.18q-.35 0-.55-.25-.2-.25-.2-.69 0-.32.09-.58.09-.27.25-.47.16-.2.38-.3.22-.11.48-.11t.4.14q.16.15.16.4 0 .28-.17.48-.17.19-.5.29-.31.1-.78.1l.04-.21q.5 0 .77-.17.28-.18.28-.5 0-.13-.07-.2-.07-.09-.19-.09-.15 0-.28.1-.13.1-.23.26-.1.17-.16.4t-.06.48q0 .3.1.48t.27.18q.22 0 .35-.16.14-.15.15-.42.13 0 .2.06.07.05.07.17 0 .16-.1.3-.11.13-.3.22-.17.09-.4.09zM29.85 4.18q-.12 0-.17-.08-.06-.08-.06-.24l.02-.27.07-.43.1-.5.1-.5-.15-.19.17-.13q.09-.05.16-.05.1 0 .14.07.04.06.04.14 0 .13-.04.28l-.1.3.04.03q.1-.29.21-.47.11-.19.22-.28.11-.09.22-.09.1 0 .16.06t.06.17q0 .13-.09.2-.08.08-.22.08-.13 0-.26.14-.13.13-.23.36-.1.22-.16.5-.06.3-.06.59 0 .14.04.2-.1.1-.21.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -0,0 +1,20 @@
import { app } from 'hyperapp'
import { preventDefault } from '@hyperapp/events'
import * as actions from './actions'
import { onDragLeave, onDragOver, onDrop, onPopState } from './events'
import { State } from './types'
import { App } from './view'
export default (): void => app<State>({
node: document.body,
init: actions.Initialize,
view: App,
subscriptions: () => [
onDragOver(actions.DragOver),
onDragLeave(preventDefault(actions.DragLeave)),
onDrop(actions.DropFile),
onPopState(actions.HistoryPop),
],
})

View file

@ -0,0 +1,18 @@
import { Notebook } from 'ipynb2html'
export type State = {
fileUrl: string | null,
title: string | null,
notebook: Notebook | null,
error: ErrorMessage | null,
dragover: boolean,
historyIdx: number,
fromHistory: boolean,
mainView: 'BLANK' | 'ERROR' | 'FETCHING' | 'NOTEBOOK',
}
export type ErrorMessage = {
title: string,
message: string,
detail?: string,
}

View file

@ -0,0 +1,25 @@
import { AnyState, Dispatch, Effect } from 'hyperapp'
export function fx <State extends AnyState, Props> (
fn: (dispatch: Dispatch<State>, props: Props) => void,
) {
return (props: Props): Effect<State, Props> => [fn, props]
}
export function match <T extends string, U extends {[K in T]: (k: K) => any}> (
value: T,
matcher: U,
): U extends {[K in T]: (k: K) => infer R} ? R : never {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return matcher[value](value)
}
export function targetFile (event: Event): File {
const file = (event.target as HTMLInputElement | null)?.files?.[0]
if (!file) {
throw TypeError("Event doesn't contain any file")
}
return file
}

View file

@ -0,0 +1,281 @@
html,
body {
margin: 0;
padding: 0;
color: #2a2a2a;
font-family: sans-serif;
background-color: #dedede;
@media print {
color: black;
background-color: white;
}
}
code,
pre {
font-size: 0.85rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 1.6em;
position: relative;
}
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: 1px solid #cfcfcf;
border-collapse: collapse;
}
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;
}
.dragover {
opacity: 0.5;
}
#header {
display: flex;
flex-wrap: nowrap;
align-items: center;
color: #fff;
background-color: #24292e;
@media print {
display: none;
}
& .logo {
flex-shrink: 0;
width: 7.5rem;
height: 2rem;
margin: 0 0.6rem;
font-size: 0;
background: svg-load('icons/logo.svg', fill: #fff) no-repeat left center / contain;
@media (max-width: 40em) {
width: 3.8rem;
margin: 0 0.2rem 0 0.5rem;
background-size: 6.8rem 1.75rem;
}
}
& .file-input {
position: absolute !important;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
white-space: nowrap;
border: 0;
clip: rect(0, 0, 0, 0);
}
& .file-label {
display: inline-block;
flex-shrink: 0;
width: 1.5rem;
height: 1.5rem;
margin: 0 0.8rem;
color: transparent;
background: svg-load('icons/folder.svg', fill: #ddd) no-repeat center / contain;
cursor: pointer;
@media (max-width: 40em) {
margin: 0.6rem;
}
}
& .url-form {
width: 100%;
}
& .url-input {
box-sizing: border-box;
width: 100%;
margin: 0.6rem 0;
padding: 0 0.5em;
color: #fff;
font-size: 0.9rem;
line-height: 2;
background-color: hsla(0, 0%, 100%, 0.15);
border: 0;
border-radius: 0.2em;
}
& .github-link {
display: block;
flex-shrink: 0;
width: 1.6rem;
height: 1.6rem;
margin: 0 1rem;
font-size: 0;
background: svg-load('icons/github.svg', fill: #eee) no-repeat center / contain;
@media (max-width: 40em) {
margin: 0 0.5rem;
}
}
}
.error {
display: table;
max-width: 28rem;
height: 6rem;
margin: 32vh auto 0 auto;
padding-left: 8.5rem;
background: svg-load('icons/alert.svg') no-repeat;
background-position: left top;
background-size: 6rem 6rem;
@media (max-width: 40em) {
height: unset;
margin: 10vh auto 0 auto;
padding: 8rem 1rem 0 1rem;
text-align: center;
background-position: top center;
}
& summary {
display: block;
font-size: 1.13em;
cursor: pointer;
&::-webkit-details-marker {
display: none;
}
&::after {
/* Expand arrow after the summary text. */
display: inline-block;
width: 0.7em;
height: 0.7em;
margin-left: 0.5em;
background: svg-load('icons/chevron-down.svg') no-repeat center / contain;
content: '';
}
}
& details[open] summary::after {
/* Collapse arrow after the summary text. */
background: svg-load('icons/chevron-up.svg') no-repeat center / contain;
}
& details :not(summary) {
color: #666;
}
& :first-child {
margin-top: 0;
}
}
.fetching {
width: 12rem;
margin: 32vh auto 0 auto;
font-size: 1.13rem;
text-align: center;
&::before {
/* Spinner */
display: block;
width: 4rem;
height: 4rem;
margin: 0 auto 2rem auto;
border-top: 0.3rem solid #f37726;
border-right: 0.3rem solid transparent;
border-radius: 50%;
animation: rotation 1s linear infinite;
content: "";
}
}
.nb-notebook {
max-width: 45rem;
margin: 1rem auto 2rem auto;
padding: 3rem 6rem;
background-color: white;
box-shadow: 4px 4px 8px #cfcfcf, -4px -4px 8px #cfcfcf;
@media screen and (max-width: 940px) {
max-width: none;
margin: 0;
padding-right: 3rem;
padding-left: 5rem;
box-shadow: none;
}
@media screen and (max-width: 768px) {
padding: 0.1rem 5% 2rem 5%;
}
@media print {
max-width: none;
margin: 0;
padding: 0 0 0 5rem;
box-shadow: none;
}
}
.nb-output table {
font-size: 0.9em;
}
@keyframes rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@page {
size: A4;
margin: 30mm 15mm 15mm 7mm;
}

View file

@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-use-before-define */
import { h, Lazy, View } from 'hyperapp'
import { preventDefault, targetValue } from '@hyperapp/events'
import * as ipynb2html from 'ipynb2html'
import { $INLINE_JSON } from 'ts-transformer-inline-file'
import * as actions from './actions'
import { State, ErrorMessage } from './types'
import { match, targetFile } from './utils'
import './view.pcss'
const { homepage } = $INLINE_JSON<{ homepage: string }>('../package.json')
const renderNotebook = ipynb2html.createRenderer(document)
export const App: View<State> = (state) => (
<body class={{ dragover: state.dragover }}>
<Header fileUrl={ state.fileUrl } />
<main>{
match(state.mainView, {
ERROR: () => <ErrorBox error={ state.error! } />,
FETCHING: () => <FetchingSpinner />,
NOTEBOOK: () => <Lazy view={ Notebook } notebook={ state.notebook } />,
BLANK: () => null,
})
}</main>
</body>
)
const Header: View<Pick<State, 'fileUrl'>> = ({ fileUrl }) => (
<header id='header'>
<a class='logo' href='/'>
ipynb<sup>viewer</sup>
</a>
<label class='file-label' title='Open a notebook'>
<input
class='file-input'
type='file'
accept='.ipynb, .json, application/x-ipynb+json'
onchange={ [actions.LoadLocalFile, targetFile] }
/>
</label>
<form class='url-form' onSubmit={ preventDefault(actions.SubmitFileUrl) }>
<input
class='url-input'
name='url'
type='url'
placeholder='Notebook URL'
title='URL of ipynb file or Gist with ipynb file to render'
pattern='https?://.*'
value={ fileUrl }
onchange={ [actions.SetFileUrl, targetValue] }
/>
</form>
<a class='github-link' href={ homepage } title='Projects homepage'>
GitHub
</a>
</header>
)
const Notebook: View<Pick<State, 'notebook'>> = ({ notebook }) => (
<div class='nb-notebook' innerHTML={ renderNotebook(notebook as any).innerHTML } />
)
const ErrorBox: View<{ error: ErrorMessage }> = ({ error }) => (
<aside class='error' role='alert'>
<h2>{ error.title }</h2>
<details>
<summary>{ error.message }</summary>
<p>{ error.detail }</p>
</details>
</aside>
)
const FetchingSpinner: View<{}> = () => (
<aside class='fetching' role='alert'>
<p>Fetching notebook</p>
</aside>
)

View file

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

View file

@ -29,12 +29,14 @@
"styles"
],
"scripts": {
"build": "ttsc --build",
"bundle": "rollup -c",
"build": "run-p build-except-ts build-ts",
"build-bundle": "rollup -c",
"build-css": "mkdirp dist/ && csso styles/notebook.css -o dist/notebook.min.css -s dist/notebook.min.css.map",
"build-except-ts": "run-p build-bundle build-css",
"build-ts": "ttsc --build",
"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",
"prepublishOnly": "run-p build readme2md",
"test": "jest --detectOpenHandles --coverage --verbose",
"readme2md": "../../scripts/adoc2md -a npm-readme ../../README.adoc > README.md",
"watch-ts": "ttsc --build --watch"

View file

@ -0,0 +1,85 @@
Ported from https://github.com/egoist/rollup-plugin-postcss/pull/226
diff --git a/node_modules/rollup-plugin-postcss/dist/index.js b/node_modules/rollup-plugin-postcss/dist/index.js
index 3d04b2e..01dda37 100644
--- a/node_modules/rollup-plugin-postcss/dist/index.js
+++ b/node_modules/rollup-plugin-postcss/dist/index.js
@@ -789,15 +789,14 @@ var index = ((options = {}) => {
},
generateBundle(opts, bundle) {
- return _asyncToGenerator(function* () {
- if (extracted.size === 0) return; // TODO: support `[hash]`
+ var _this2 = this;
+ return _asyncToGenerator(function* () {
+ if (extracted.size === 0) return;
const dir = opts.dir || path.dirname(opts.file);
const file = opts.file || path.join(opts.dir, Object.keys(bundle).find(fileName => bundle[fileName].isEntry));
const getExtracted = () => {
- const fileName = typeof postcssLoaderOptions.extract === 'string' ? normalizePath(path.relative(dir, postcssLoaderOptions.extract)) : `${path.basename(file, path.extname(file))}.css`;
- const concat = new Concat(true, fileName, '\n');
const entries = Array.from(extracted.values());
const modules = bundle[normalizePath(path.relative(dir, file))].modules;
@@ -806,6 +805,32 @@ var index = ((options = {}) => {
entries.sort((a, b) => fileList.indexOf(a.id) - fileList.indexOf(b.id));
}
+ let referenceId;
+
+ if (typeof postcssLoaderOptions.extract === 'string') {
+ referenceId = _this2.emitFile({
+ type: 'asset',
+ source: '',
+ // init
+ fileName: normalizePath(path.relative(dir, postcssLoaderOptions.extract))
+ });
+ } else {
+ const name = `${path.basename(file, path.extname(file))}.css`; // Base hash digest on concatenation of extracted code...
+
+ const source = entries.reduce((acc, {
+ code
+ }) => acc + code, '');
+ referenceId = _this2.emitFile({
+ type: 'asset',
+ source,
+ name
+ });
+ }
+
+ const fileName = _this2.getFileName(referenceId);
+
+ const concat = new Concat(true, fileName, '\n');
+
for (var _i = 0; _i < entries.length; _i++) {
const res = entries[_i];
const relative = normalizePath(path.relative(dir, res.id));
@@ -872,20 +897,14 @@ var index = ((options = {}) => {
}
}
- const codeFile = {
- fileName: codeFileName,
- isAsset: true,
- source: code
- };
- bundle[codeFile.fileName] = codeFile;
+ bundle[codeFileName].source = code; // update
if (map) {
- const mapFile = {
- fileName: mapFileName,
- isAsset: true,
- source: map
- };
- bundle[mapFile.fileName] = mapFile;
+ _this2.emitFile({
+ type: 'asset',
+ source: map,
+ fileName: mapFileName
+ });
}
})();
}

View file

@ -4,5 +4,6 @@
{ "path": "./packages/ipynb2html-core" },
{ "path": "./packages/ipynb2html" },
{ "path": "./packages/ipynb2html-cli" },
{ "path": "./packages/ipynb2html-viewer" },
]
}

62
types/@hyperapp/events.d.ts vendored Normal file
View file

@ -0,0 +1,62 @@
type EventArgument<E extends keyof GlobalEventHandlers> = Parameters<NonNullable<GlobalEventHandlers[E]>>[0];
export declare function onMouseEnter<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onmouseenter'>>): hyperappSubset.Subscription<S>;
export declare function onMouseLeave<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onmouseleave'>>): hyperappSubset.Subscription<S>;
export declare function onMouseMove<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onmousemove'>>): hyperappSubset.Subscription<S>;
export declare function onMouseOut<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onmouseout'>>): hyperappSubset.Subscription<S>;
export declare function onMouseOver<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onmouseover'>>): hyperappSubset.Subscription<S>;
export declare function onMouseUp<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onmouseup'>>): hyperappSubset.Subscription<S>;
export declare function onTouchStart<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'ontouchstart'>>): hyperappSubset.Subscription<S>;
export declare function onTouchMove<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'ontouchmove'>>): hyperappSubset.Subscription<S>;
export declare function onTouchEnd<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'ontouchend'>>): hyperappSubset.Subscription<S>;
export declare function onKeyDown<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onkeydown'>>): hyperappSubset.Subscription<S>;
export declare function onKeyUp<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onkeyup'>>): hyperappSubset.Subscription<S>;
export declare function onFocus<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onfocus'>>): hyperappSubset.Subscription<S>;
export declare function onBlur<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, EventArgument<'onblur'>>): hyperappSubset.Subscription<S>;
export declare function onAnimationFrame<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, DOMHighResTimeStamp>): hyperappSubset.Subscription<S>;
export declare function eventKey(e: Event): any;
export declare function eventDetail(e: Event): any;
export declare function targetChecked(e: Event): any;
export declare function targetValue(e: Event): any;
export declare function eventOptions<S extends hyperappSubset.AnyState>(props: { preventDefault?: boolean, stopPropagation?: boolean, action?: hyperappSubset.Dispatchable<S, Event> }): hyperappSubset.Effect<S>;
export declare function preventDefault<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, Event>): hyperappSubset.Action<S, Event>;
export declare function stopPropagation<S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, Event>): hyperappSubset.Action<S, Event>;
export declare function dispatchCustomEvent(props: {name: string}): hyperappSubset.Action<any>;
export declare function createOnCustomEvent<E extends Event = Event>(eventName: string): <S extends hyperappSubset.AnyState>(action: hyperappSubset.Dispatchable<S, E>) => hyperappSubset.Subscription<any>;
declare namespace hyperappSubset {
type AnyState = boolean | string | number | object | symbol | null | undefined;
type PayloadCreator<DPayload, CPayload> = ((data: DPayload) => CPayload);
type Dispatchable<State extends AnyState, DPayload = void, CPayload = any> = (
State
| [State, ...Effect<State>[]]
| ([Action<State, CPayload>, PayloadCreator<DPayload, CPayload>])
| ([Action<State, CPayload>, CPayload])
| Action<State, void> // (state) => ({ ... }) | (state) => ([{ ... }, effect1, ...])
| Action<State, DPayload> // (state, data) => ({ ... }) | (state, data) => ([{ ... }, effect1, ...])
);
type Dispatch<State extends AnyState, NextPayload> = (obj: Dispatchable<State, NextPayload>, data: NextPayload) => State;
interface EffectRunner<State extends AnyState = AnyState, NextPayload = void, Props = void> {
(dispatch: Dispatch<State, NextPayload>, props: Props): void;
}
type Effect<State extends AnyState = AnyState> = [EffectRunner<State, any, any>, any] | [EffectRunner<State, any, void>];
interface SubscriptionRunner<State extends AnyState = AnyState, NextPayload = void, Props = void> {
(dispatch: Dispatch<State, NextPayload>, props: Props): (() => void);
}
type Subscription<State extends AnyState = AnyState> = [SubscriptionRunner<State, any, any>, any] | [SubscriptionRunner<State, any, void>];
interface Action<State extends AnyState, Payload = void> {
(state: State, data: Payload): Dispatchable<State>;
}
}

64
types/@hyperapp/http.d.ts vendored Normal file
View file

@ -0,0 +1,64 @@
export declare function request<S extends hyperappSubset.AnyState>(props: RequestProps<S>): hyperappSubset.Effect<S>;
type RequestProps<S extends hyperappSubset.AnyState> =
JsonRequestProps<S> | TextRequestProps<S> | FormDataRequestProps<S> | BlobRequestProps<S> | ArrayBufferRequestProps<S>;
interface RequestPropsBase<S extends hyperappSubset.AnyState> {
url: Parameters<typeof fetch>[0];
options?: Parameters<typeof fetch>[1];
}
interface JsonRequestProps<S extends hyperappSubset.AnyState> extends RequestPropsBase<S> {
expect?: 'json';
action: hyperappSubset.Dispatchable<S, any>;
}
interface TextRequestProps<S extends hyperappSubset.AnyState> extends RequestPropsBase<S> {
expect: 'text';
action: hyperappSubset.Dispatchable<S, string>;
}
interface FormDataRequestProps<S extends hyperappSubset.AnyState> extends RequestPropsBase<S> {
expect: 'formData';
action: hyperappSubset.Dispatchable<S, FormData>;
}
interface BlobRequestProps<S extends hyperappSubset.AnyState> extends RequestPropsBase<S> {
expect: 'blob';
action: hyperappSubset.Dispatchable<S, Blob>;
}
interface ArrayBufferRequestProps<S extends hyperappSubset.AnyState> extends RequestPropsBase<S> {
expect: 'arrayBuffer';
action: hyperappSubset.Dispatchable<S, ArrayBuffer>;
}
declare namespace hyperappSubset {
type AnyState = boolean | string | number | object | symbol | null | undefined;
type PayloadCreator<DPayload, CPayload> = ((data: DPayload) => CPayload);
type Dispatchable<State extends AnyState, DPayload = void, CPayload = any> = (
State
| [State, ...Effect<State>[]]
| ([Action<State, CPayload>, PayloadCreator<DPayload, CPayload>])
| ([Action<State, CPayload>, CPayload])
| Action<State, void> // (state) => ({ ... }) | (state) => ([{ ... }, effect1, ...])
| Action<State, DPayload> // (state, data) => ({ ... }) | (state, data) => ([{ ... }, effect1, ...])
);
type Dispatch<State extends AnyState, NextPayload> = (obj: Dispatchable<State, NextPayload>, data: NextPayload) => State;
interface EffectRunner<State extends AnyState = AnyState, NextPayload = void, Props = void> {
(dispatch: Dispatch<State, NextPayload>, props: Props): void;
}
type Effect<State extends AnyState = AnyState> = [EffectRunner<State, any, any>, any] | [EffectRunner<State, any, void>];
interface SubscriptionRunner<State extends AnyState = AnyState, NextPayload = void, Props = void> {
(dispatch: Dispatch<State, NextPayload>, props: Props): (() => void);
}
type Subscription<State extends AnyState = AnyState> = [SubscriptionRunner<State, any, any>, any] | [SubscriptionRunner<State, any, void>];
interface Action<State extends AnyState, Payload = void> {
(state: State, data: Payload): Dispatchable<State>;
}
}

1
types/@hyperapp/index.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module '@hyperapp' {}

232
types/hyperapp.d.ts vendored Normal file
View file

@ -0,0 +1,232 @@
// TypeScript Version: 3.0
export as namespace hyperapp;
/** @namespace [VDOM] */
/** The VDOM representation of an Element.
*
* @memberOf [VDOM]
*/
export interface VNode {
name: unknown; // protected (internal implementation)
props: unknown; // protected (internal implementation)
children: unknown; // protected (internal implementation)
node: unknown; // protected (internal implementation)
type: unknown; // protected (internal implementation)
key: unknown; // protected (internal implementation)
lazy: unknown; // protected (internal implementation)
}
/**
* Possibles state types (all except Array and Function)
*/
export type AnyState = boolean | string | number | object | symbol | null | undefined;
/**
* Possibles children types
*/
export type Children = VNode | string | number | null
/** A Component is a function that returns a custom VNode or View.
*
* @memberOf [VDOM]
*/
export interface Component<Attributes = {}> {
(attributes: Attributes, children: VNode[]): VNode | null;
}
/** The soft way to create a VNode.
* @param name An element name or a Component function
* @param attributes Any valid HTML atributes, events, styles, and meta data
* @param children The children of the VNode
* @returns A VNode tree.
*
* @memberOf [VDOM]
*/
export function h<Attributes>(
nodeName: Component<Attributes> | string,
attributes?: Attributes,
...children: (Children | Children[])[]
): VNode
/** @namespace [App] */
type PayloadCreator<DPayload, CPayload> = ((data: DPayload) => CPayload);
/** Usable to 1st argument of `dispatch`. Usually, This is a reference to an action to be invoked by Hyperapp, with custom payload
*
* @memberOf [App]
*/
export type Dispatchable<State extends AnyState, DPayload = void, CPayload = any> =
| State
| [State, ...Effect<State>[]]
| [Action<State, CPayload>, PayloadCreator<DPayload, CPayload>]
| [Action<State, CPayload>, CPayload]
| Action<State, void> // (state) => ({ ... }) | (state) => ([{ ... }, effect1, ...])
| Action<State, DPayload>; // (state, data) => ({ ... }) | (state, data) => ([{ ... }, effect1, ...])
/** Usable to 1st argument of `dispatch`. make strict for `init` (initial state and default payload are always undefined)
*
* @memberOf [App]
*/
export type DispatchableOnInit<State extends AnyState, CPayload = any> =
| State
| [State, ...Effect<State>[]]
| [ActionOnInit<State, CPayload>, CPayload]
| ActionOnInit<State, void>;
/** A definition of `dispatch`. This is passed as an argument of effect or subscription runner.
*
* @memberOf [App]
*/
export type Dispatch<State extends AnyState> = <Payload>(obj: Dispatchable<State, Payload>, data: Payload) => State;
/** An effect runner. It is actually invoked when effect is reflected.
*
* @memberOf [App]
*/
export interface EffectRunner<State extends AnyState = AnyState, Props = void> {
(dispatch: Dispatch<State>, props: Props): void;
}
/** An effect as the result of an action.
*
* @memberOf [App]
*/
export type Effect<State extends AnyState = AnyState, Props = any> = [EffectRunner<State, Props>, Props] | [EffectRunner<State, void>];
/** An subscription runner. It is actually invoked when effect is reflected.
*
* @memberOf [App]
*/
export interface SubscriptionRunner<State extends AnyState = AnyState, Props = void> {
(dispatch: Dispatch<State>, props: Props): (() => void);
}
/** A reference to an subscription to be managed by Hyperapp, with optional additional parameters
*
* @memberOf [App]
*/
export type Subscription<State extends AnyState = AnyState, Props = any> = [SubscriptionRunner<State, Props>, Props] | [SubscriptionRunner<State, void>];
/** The interface for a single action implementation.
*
* @memberOf [App]
*/
export interface Action<State extends AnyState, Payload = void> {
(state: State, data: Payload): Dispatchable<State>;
}
/** The interface for a single action implementation, make strict for `init` (given state are always undefined)
*
* @memberOf [App]
*/
export interface ActionOnInit<State extends AnyState, Payload = void> {
(state: undefined, data: Payload): DispatchableOnInit<State>;
}
/** The view function describes the application UI as a tree of VNodes.
* @returns A VNode tree.
* @memberOf [App]
*/
export interface View<State extends AnyState> {
(state: State): VNode | null;
}
/** The possible response types for the subscription callback for an application
*
* @memberOf [App]
*/
export type SubscriptionsResult<State extends AnyState> = (Subscription<State> | Falsy)[] | Subscription<State> | Falsy;
type Falsy = false | '' | 0 | null | undefined;
/** The lazy view. {@link https://github.com/jorgebucaran/hyperapp/issues/721#issuecomment-402150041}
*
* @memberOf [App]
*/
export function Lazy<Props extends object>(props: { view: (props: Props) => VNode | null, key?: string | number | null } & Props): VNode;
/** The set of properties that define a Hyperapp application.
*
* @memberOf [App]
*/
export interface AppProps<State extends AnyState> {
init?: DispatchableOnInit<State>;
view?: View<State>;
node: Element;
subscriptions?: (state: State) => SubscriptionsResult<State>;
middleware?: Middleware<State>;
}
/** The middleware function.
*
* @memberOf [App]
*/
export type MiddlewareFunc<State extends AnyState = AnyState> = (action: Dispatchable<State>, props: unknown) => void;
/** The middleware.
*
* @memberOf [App]
*/
export type Middleware<State extends AnyState = AnyState> = (func: MiddlewareFunc<State>) => MiddlewareFunc<State>;
/** The app() call creates and renders a new application.
*
* @param state The state object.
* @param actions The actions object implementation.
* @param view The view function.
* @param container The DOM element where the app will be rendered to.
* @returns The actions wired to the application.
* @memberOf [App]
*/
export function app<State extends AnyState>(app: AppProps<State>): void
/**
* The class attribute value of VNode.
*
* @memberOf [VDOM]
*/
export type ClassAttribute = ClassAttributeItem | null | undefined;
type ClassAttributeItem = (string | { [key: string]: any } | ClassAttributeArray);
interface ClassAttributeArray extends Array<ClassAttributeItem> { }
/**
* The style attribute value of VNode.
*
* @memberOf [VDOM]
*/
export type StyleAttribute = { [key: string]: any } | null | string | undefined;
// e.g.) onchange, onupdate, oninput, ...
//
type EventKeys = keyof GlobalEventHandlers;
type EventParameterType<Key extends EventKeys> = Parameters<Exclude<GlobalEventHandlers[Key], null | undefined>>[0];
// <div onclick={A} />
// -> A: Dispatchable<any, MouseEvent>
//
type EventAttributes = Partial<{ [key in EventKeys]: Dispatchable<AnyState, EventParameterType<key>> }>;
export interface JSXAttribute extends EventAttributes {
key?: PropertyKey;
class?: ClassAttribute;
style?: StyleAttribute;
[attrName: string]: any;
}
// /** @namespace [JSX] */
declare global {
namespace JSX {
interface Element extends VNode { }
interface IntrinsicElements {
[elemName: string]: JSXAttribute;
}
}
}

1264
yarn.lock

File diff suppressed because it is too large Load diff