├── src ├── browser.d.ts ├── browser.js ├── node.d.ts ├── index.d.ts ├── node.js └── index.js ├── .prettierrc ├── benchmark ├── README.md ├── .eslintrc └── benchmark.js ├── tsconfig.json ├── tools ├── pre-commit.js ├── .eslintrc ├── README.md ├── test.js ├── lint.js └── build.js ├── test ├── browser.test.tsx ├── browser.test.js ├── node.test.tsx ├── .eslintrc ├── README.md ├── index.test.tsx ├── node.test.js └── index.test.js ├── .github ├── SUPPORT.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── ci.yml ├── ISSUE_TEMPLATE.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── .editorconfig ├── .eslintrc ├── .gitignore ├── logo.svg ├── .babelrc ├── .gitattributes ├── LICENSE.md ├── package.json ├── dist ├── hyperapp-render.min.js ├── hyperapp-render.js ├── hyperapp-render.min.js.map └── hyperapp-render.js.map ├── CHANGELOG.md └── README.md /src/browser.d.ts: -------------------------------------------------------------------------------- 1 | import { renderToString } from './index' 2 | 3 | export { renderToString } 4 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | import { renderToString } from './index' 2 | 3 | export { renderToString } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": false, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | Measure rendering performance using [benchr](https://github.com/robertklep/node-benchr): 4 | 5 | ```bash 6 | npm run benchmark 7 | ``` 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "jsxFactory": "h", 5 | "module": "commonjs", 6 | "noEmit": true, 7 | "strict": true 8 | }, 9 | "include": ["src", "test"] 10 | } 11 | -------------------------------------------------------------------------------- /tools/pre-commit.js: -------------------------------------------------------------------------------- 1 | const lint = require('./lint') 2 | const test = require('./test') 3 | 4 | async function preCommit() { 5 | await lint() 6 | await test() 7 | } 8 | 9 | module.exports = preCommit().catch(process.exit) 10 | -------------------------------------------------------------------------------- /tools/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-extraneous-dependencies": [ 4 | "error", 5 | { 6 | "optionalDependencies": false, 7 | "peerDependencies": false 8 | } 9 | ], 10 | "no-console": "off" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/browser.test.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'hyperapp' 2 | import { renderToString } from '../src/browser' 3 | import { Counter, print } from './index.test' 4 | 5 | print(renderToString(Counter.view, Counter.state, Counter.actions)) 6 | print(renderToString(

hello world

)) 7 | -------------------------------------------------------------------------------- /src/node.d.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream' 2 | import { renderToString } from './index' 3 | 4 | export { renderToString } 5 | 6 | export function renderToStream( 7 | view: View, 8 | state?: State, 9 | actions?: Actions, 10 | ): Readable 11 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | For personal support requests with Hyperapp Render please use 4 | [Slack Chat](https://hyperappjs.herokuapp.com/) (`#help` room). 5 | 6 | Please check the respective repository/website for support regarding the code in 7 | [`Hyperapp`](https://github.com/hyperapp/hyperapp) or 8 | [`Babel`](https://github.com/babel/babel). 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | -------------------------------------------------------------------------------- /test/browser.test.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import { h } from 'hyperapp' 3 | import { renderToString } from '../src/browser' 4 | 5 | describe('renderToString(view, state, actions)', () => { 6 | it('should render simple markup', () => { 7 | const html = renderToString(
hello world
) 8 | expect(html).toBe('
hello world
') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier"], 3 | "rules": { 4 | "import/prefer-default-export": "off", 5 | "no-continue": "off", 6 | "no-plusplus": "off", 7 | "no-restricted-syntax": "off", 8 | "prefer-template": "off" 9 | }, 10 | "settings": { 11 | "react": { 12 | "pragma": "h", 13 | "version": "v18.2.0" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files 2 | 3 | # Dependencies 4 | node_modules/ 5 | yarn.lock 6 | 7 | # Compiled output 8 | dist/* 9 | !dist/hyperapp-render* 10 | 11 | # Test coverage 12 | coverage/ 13 | 14 | # Logs 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Editors and IDEs 20 | .idea/ 21 | .vscode/ 22 | 23 | # Misc 24 | .DS_Store 25 | -------------------------------------------------------------------------------- /benchmark/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "suite": true, 4 | "benchmark": true 5 | }, 6 | "rules": { 7 | "import/no-extraneous-dependencies": [ 8 | "error", 9 | { 10 | "optionalDependencies": false, 11 | "peerDependencies": false 12 | } 13 | ], 14 | "react/jsx-filename-extension": "off", 15 | "react/jsx-props-no-spreading": "off", 16 | "react/prop-types": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | [ 14 | "@babel/plugin-transform-react-jsx", 15 | { 16 | "pragma": "h", 17 | "pragmaFrag": "Fragment", 18 | "useBuiltIns": true, 19 | "throwIfNamespace": false 20 | } 21 | ] 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export function escapeHtml(value: any): string 2 | 3 | export function concatClassNames(value: any): string 4 | 5 | export function stringifyStyles(style: any): string 6 | 7 | export function renderer( 8 | view: View, 9 | state?: State, 10 | actions?: Actions, 11 | ): (bytes: number) => string 12 | 13 | export function renderToString( 14 | view: View, 15 | state?: State, 16 | actions?: Actions, 17 | ): string 18 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream' 2 | import { renderer, renderToString } from './index' 3 | 4 | export { renderToString } 5 | 6 | export function renderToStream(view, state, actions) { 7 | const read = renderer(view, state, actions) 8 | 9 | // https://nodejs.org/api/stream.html 10 | return new Readable({ 11 | read(size) { 12 | try { 13 | this.push(read(size)) 14 | } catch (err) { 15 | this.emit('error', err) 16 | } 17 | }, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /test/node.test.tsx: -------------------------------------------------------------------------------- 1 | import { Readable } from 'stream' 2 | import { h } from 'hyperapp' 3 | import { renderToStream, renderToString } from '../src/node' 4 | import { Counter, print } from './index.test' 5 | 6 | function printStream(stream: Readable) { 7 | stream.pipe(process.stdout) 8 | } 9 | 10 | print(renderToString(Counter.view, Counter.state, Counter.actions)) 11 | print(renderToString(

hello world

)) 12 | 13 | printStream(renderToStream(Counter.view, Counter.state, Counter.actions)) 14 | printStream(renderToStream(

hello world

)) 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # https://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | 4 | * text=auto 5 | 6 | # For the following file types, normalize line endings to LF on 7 | # checkin and prevent conversion to CRLF when they are checked out 8 | # (this is required in order to prevent newline related issues like, 9 | # for example, after the build script is run) 10 | 11 | .* text eol=lf 12 | *.js text eol=lf 13 | *.json text eol=lf 14 | *.map text eol=lf 15 | *.md text eol=lf 16 | *.svg text eol=lf 17 | *.ts text eol=lf 18 | *.tsx text eol=lf 19 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "rules": { 6 | "import/no-extraneous-dependencies": [ 7 | "error", 8 | { 9 | "optionalDependencies": false, 10 | "peerDependencies": false 11 | } 12 | ], 13 | "jsx-a11y/accessible-emoji": "off", 14 | "jsx-a11y/alt-text": "off", 15 | "jsx-a11y/control-has-associated-label": "off", 16 | "react/jsx-curly-brace-presence": "off", 17 | "react/jsx-filename-extension": "off", 18 | "react/jsx-props-no-spreading": "off", 19 | "react/no-unknown-property": "off", 20 | "react/prop-types": "off", 21 | "react/style-prop-object": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Build Automation Tools 2 | 3 | Compile the lib into the **./dist** folder: 4 | 5 | ```bash 6 | npm run build 7 | ``` 8 | 9 | Find problematic patterns in code 10 | using [ESLint](https://eslint.org/) 11 | and [Prettier](https://prettier.io/) 12 | by following [Airbnb Style Guide](https://github.com/airbnb/javascript): 13 | 14 | ```bash 15 | npm run lint 16 | ``` 17 | 18 | Run unit tests using [Jest](https://jestjs.io/) and [TypeScript](http://www.typescriptlang.org/): 19 | 20 | ```bash 21 | npm run test 22 | ``` 23 | 24 | Run [pre-commit git hook](https://git-scm.com/docs/githooks#_pre_commit) manually: 25 | 26 | ```bash 27 | npm run pre-commit 28 | ``` 29 | -------------------------------------------------------------------------------- /tools/test.js: -------------------------------------------------------------------------------- 1 | const cp = require('child_process') 2 | const jest = require('jest') 3 | 4 | const jestConfig = { 5 | modulePathIgnorePatterns: ['/dist/'], 6 | testMatch: ['**/*.test.js'], 7 | } 8 | 9 | function spawn(command, args) { 10 | return new Promise((resolve, reject) => { 11 | cp.spawn(command, args, { stdio: 'inherit' }).on('close', (code) => { 12 | if (code === 0) { 13 | resolve() 14 | } else { 15 | reject(code) 16 | } 17 | }) 18 | }) 19 | } 20 | 21 | async function test() { 22 | await spawn('tsc', ['--project', '.']) 23 | await jest.run(['--config', JSON.stringify(jestConfig), ...process.argv.slice(2)]) 24 | } 25 | 26 | module.exports = module.parent ? test : test().catch(process.exit) 27 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | [![Build Status](https://img.shields.io/travis/kriasoft/hyperapp-render/master.svg)](https://travis-ci.org/kriasoft/hyperapp-render) 4 | [![Coverage Status](https://img.shields.io/codecov/c/github/kriasoft/hyperapp-render.svg)](https://codecov.io/gh/kriasoft/hyperapp-render) 5 | [![Dependency Status](https://img.shields.io/david/kriasoft/hyperapp-render.svg)](https://david-dm.org/kriasoft/hyperapp-render) 6 | 7 | Find problematic patterns in code using [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) 8 | by following [Airbnb Style Guide](https://github.com/airbnb/javascript): 9 | 10 | ```bash 11 | npm run lint 12 | ``` 13 | 14 | Run unit tests using [Jest](http://facebook.github.io/jest/): 15 | 16 | ```bash 17 | npm run test 18 | ``` 19 | -------------------------------------------------------------------------------- /tools/lint.js: -------------------------------------------------------------------------------- 1 | const cp = require('child_process') 2 | 3 | function spawn(command, args) { 4 | return new Promise((resolve, reject) => { 5 | cp.spawn(command, args, { stdio: 'inherit' }).on('close', (code) => { 6 | if (code === 0) { 7 | resolve() 8 | } else { 9 | reject(code) 10 | } 11 | }) 12 | }) 13 | } 14 | 15 | async function lint() { 16 | const fix = process.argv.includes('--fix') 17 | const eslintOptions = fix ? ['--fix'] : [] 18 | const prettierOptions = fix ? '--write' : '--list-different' 19 | await spawn('eslint', [...eslintOptions, '{benchmark,src,test,tools}/**/*.js']) 20 | await spawn('prettier', [prettierOptions, '{benchmark,src,test,tools}/**/*.{js,ts,tsx,md}']) 21 | } 22 | 23 | module.exports = module.parent ? lint : lint().catch(process.exit) 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Types of changes 2 | 3 | 4 | 5 | - [ ] Bug fix (non-breaking change which fixes an issue) 6 | - [ ] New feature (non-breaking change which adds functionality) 7 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 8 | 9 | ## Checklist: 10 | 11 | 12 | 13 | 14 | - [ ] My code follows the code style of this project. 15 | - [ ] My change requires a change to the documentation. 16 | - [ ] I have updated the documentation accordingly. 17 | - [ ] I have read the **CONTRIBUTING** document. 18 | - [ ] I have added tests to cover my changes. 19 | - [ ] All new and existing tests passed. 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - '*' 7 | push: 8 | branches: 9 | - '*' 10 | tags: 11 | - '*' 12 | 13 | jobs: 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | node: ['*', 'lts/*'] 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | - name: Set up Node ${{ matrix.node }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node }} 27 | check-latest: true 28 | cache: 'npm' 29 | - name: Install dependencies 30 | run: npm install 31 | - name: Run lint 32 | run: npm run lint 33 | - name: Run tests and collect coverage 34 | run: npm run test -- --coverage 35 | - name: Run build 36 | run: npm run build 37 | - name: Upload coverage to Codecov 38 | uses: codecov/codecov-action@v3 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2018-present [Vladimir Kutepov](https://github.com/frenzzy) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { h, ActionsType, View } from 'hyperapp' 2 | import { 3 | escapeHtml, 4 | concatClassNames, 5 | stringifyStyles, 6 | renderer, 7 | renderToString, 8 | } from '../src/index' 9 | 10 | export namespace Counter { 11 | export interface State { 12 | count: number 13 | } 14 | 15 | export interface Actions { 16 | up(): State 17 | } 18 | 19 | export const state: State = { 20 | count: 0, 21 | } 22 | 23 | export const actions: ActionsType = { 24 | up: () => (state) => ({ count: state.count + 1 }), 25 | } 26 | 27 | export const view: View = (state, actions) => ( 28 | 31 | ) 32 | } 33 | 34 | export function print(message: string) { 35 | console.log(message) 36 | } 37 | 38 | print(escapeHtml(100500)) 39 | print(escapeHtml('hello world')) 40 | 41 | print(concatClassNames('foo bar baz')) 42 | print(concatClassNames({ foo: true, bar: 'ok', baz: null })) 43 | print(concatClassNames(['foo', ['bar', 0], { baz: 'ok' }, false, null])) 44 | 45 | print(stringifyStyles({ color: 'red', backgroundColor: null })) 46 | 47 | print(renderer(Counter.view, Counter.state, Counter.actions)(Infinity)) 48 | print(renderToString(Counter.view, Counter.state, Counter.actions)) 49 | print(renderToString(

hello world

)) 50 | -------------------------------------------------------------------------------- /test/node.test.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import { h } from 'hyperapp' 3 | import { Readable, Writable } from 'stream' 4 | import { renderToString, renderToStream } from '../src/node' 5 | 6 | function readFromStream(stream) { 7 | return new Promise((resolve, reject) => { 8 | let buffer = '' 9 | const writable = new Writable({ 10 | write(chunk, encoding, callback) { 11 | buffer += chunk 12 | callback() 13 | }, 14 | }) 15 | writable.on('finish', () => resolve(buffer)) 16 | stream.on('error', reject) 17 | stream.pipe(writable) 18 | }) 19 | } 20 | 21 | describe('renderToString(view, state, actions)', () => { 22 | it('should render simple markup', () => { 23 | const html = renderToString(
hello world
) 24 | expect(html).toBe('
hello world
') 25 | }) 26 | }) 27 | 28 | describe('renderToStream(view, state, actions)', () => { 29 | it('should return a readable stream', () => { 30 | const stream = renderToStream(
hello world
) 31 | expect(stream).toBeInstanceOf(Readable) 32 | }) 33 | 34 | it('should emit an error for invalid input', async () => { 35 | const node = { nodeName: 'InvalidVNode', attributes: {}, children: null } 36 | const stream = renderToStream(node) 37 | let err 38 | try { 39 | await readFromStream(stream) 40 | } catch (e) { 41 | err = e 42 | } 43 | expect(err).toBeInstanceOf(Error) 44 | expect(err.message).toMatch( 45 | /^Cannot read property 'length' of null|Cannot read properties of null \(reading 'length'\)$/, 46 | ) 47 | }) 48 | 49 | it('should render markup', async () => { 50 | const stream = renderToStream(
hello world
) 51 | const html = await readFromStream(stream) 52 | expect(html).toBe('
hello world
') 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "hyperapp-render", 4 | "version": "4.0.1", 5 | "description": "Render Hyperapp to an HTML string with SSR and Node.js streaming support", 6 | "repository": "kriasoft/hyperapp-render", 7 | "author": "Vladimir Kutepov", 8 | "license": "MIT", 9 | "keywords": [ 10 | "hyperapp", 11 | "render", 12 | "string", 13 | "stream", 14 | "server", 15 | "html", 16 | "ssr" 17 | ], 18 | "type": "commonjs", 19 | "main": "commonjs/node.js", 20 | "module": "esm/node.js", 21 | "types": "src/node.d.ts", 22 | "esnext": "src/node.js", 23 | "unpkg": "hyperapp-render.min.js", 24 | "jsdelivr": "hyperapp-render.min.js", 25 | "browser": { 26 | "commonjs/node.js": "./commonjs/browser.js", 27 | "esm/node.js": "./esm/browser.js", 28 | "src/node.d.ts": "./src/browser.d.ts", 29 | "src/node.js": "./src/browser.js" 30 | }, 31 | "exports": { 32 | "node": { 33 | "require": "./commonjs/node.js", 34 | "default": "./esm/node.js" 35 | }, 36 | "require": "./commonjs/browser.js", 37 | "default": "./esm/browser.js" 38 | }, 39 | "dependencies": { 40 | "@types/node": "*" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.18.5", 44 | "@babel/plugin-transform-react-jsx": "^7.17.12", 45 | "@babel/preset-env": "^7.18.2", 46 | "@babel/register": "^7.17.7", 47 | "@rollup/plugin-babel": "^5.3.1", 48 | "@rollup/plugin-commonjs": "^22.0.0", 49 | "@rollup/plugin-node-resolve": "^13.3.0", 50 | "babel-jest": "^28.1.1", 51 | "benchr": "^4.3.0", 52 | "eslint": "^8.18.0", 53 | "eslint-config-airbnb": "^19.0.4", 54 | "eslint-config-prettier": "^8.5.0", 55 | "eslint-plugin-import": "^2.26.0", 56 | "eslint-plugin-jsx-a11y": "^6.5.1", 57 | "eslint-plugin-react": "^7.30.1", 58 | "eslint-plugin-react-hooks": "^4.6.0", 59 | "fs-extra": "^10.1.0", 60 | "hyperapp": "^1.2.10", 61 | "jest": "^28.1.1", 62 | "prettier": "^2.7.1", 63 | "rollup": "^2.75.7", 64 | "rollup-plugin-terser": "^7.0.2", 65 | "typescript": "^4.7.4" 66 | }, 67 | "scripts": { 68 | "lint": "node tools/lint.js", 69 | "test": "node tools/test.js", 70 | "build": "node tools/build.js", 71 | "benchmark": "benchr benchmark/benchmark.js --require @babel/register", 72 | "pre-commit": "node tools/pre-commit.js" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /dist/hyperapp-render.min.js: -------------------------------------------------------------------------------- 1 | /*! Hyperapp Render v4.0.1 | MIT Licence | https://github.com/kriasoft/hyperapp-render */ 2 | !function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports):"function"==typeof define&&define.amd?define(["exports"],r):r((e="undefined"!=typeof globalThis?globalThis:e||self).hyperappRender={})}(this,(function(e){"use strict";var r=Array.isArray,n=Object.prototype.hasOwnProperty,t=new Map,a=/[A-Z]/g,o=/^ms-/,i=/["&'<>]/,l=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);function f(e){if(null==e)return"";var r=""+e;if("number"==typeof e)return r;var n=i.exec(r);if(!n)return r;for(var t=n.index,a=0,o="",l="";t":(o+=">",i="")}if(t.length>0)a.push({childIndex:0,children:t,footer:i});else{var d=r.innerHTML;null!=d&&(o+=d),o+=i}return o}function d(e,r,n){return"function"==typeof e?d(e(r,n),r,n):e&&2===e.type?d(e.lazy.view(e.lazy),r,n):e}e.renderToString=function(e,n,t){return function(e,n,t){var a=[{childIndex:0,children:[e],footer:""}],o=!1;return function(e){if(o)return null;for(var i="";i.length=l.children.length)i+=l.footer,a.pop();else{var c=d(l.children[l.childIndex++],n,t);null!=c&&"boolean"!=typeof c&&(r(c)?a.push({childIndex:0,children:c,footer:""}):3===c.type?i+=f(c.tag||c.name):i+="object"==typeof c?p(c.tag||c.nodeName,c.props||c.attributes,c.children,a):f(c))}}return i}}(e,n,t)(1/0)},Object.defineProperty(e,"__esModule",{value:!0})})); 3 | //# sourceMappingURL=hyperapp-render.min.js.map 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **I'm submitting a ...** 2 | 3 | 4 | 5 | - [ ] bug report 6 | - [ ] feature request 7 | - [ ] other (Please do not submit support requests here (below)) 8 | 9 | ## Notes: 10 | 11 | - Please **do not** use the issue tracker for personal support requests (use 12 | [Slack Chat](https://hyperappjs.herokuapp.com/)). 13 | 14 | - Please **do not** derail or troll issues. Keep the discussion on topic and 15 | respect the opinions of others. 16 | 17 | - Please **do not** open issues or pull requests regarding the code in 18 | [`Hyperapp`](https://github.com/hyperapp/hyperapp) or 19 | [`Babel`](https://github.com/babel/babel) 20 | (open them in their respective repositories). 21 | 22 | ## Bug reports 23 | 24 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 25 | Good bug reports are extremely helpful - thank you! 26 | 27 | Guidelines for bug reports: 28 | 29 | 1. **Use the GitHub issue search** — check if the issue has already been 30 | reported. 31 | 32 | 2. **Check if the issue has been fixed** — try to reproduce it using the 33 | latest `master` or development branch in the repository. 34 | 35 | 3. **Isolate the problem** — ideally create a [reduced test 36 | case](https://css-tricks.com/reduced-test-cases/) and a live example. 37 | 38 | A good bug report shouldn't leave others needing to chase you up for more 39 | information. Please try to be as detailed as possible in your report. What is 40 | your environment? What steps will reproduce the issue? What browser(s) and OS 41 | experience the problem? What would you expect to be the outcome? All these 42 | details will help people to fix any potential bugs. 43 | 44 | Example: 45 | 46 | > Short and descriptive example bug report title 47 | > 48 | > A summary of the issue and the browser/OS environment in which it occurs. If 49 | > suitable, include the steps required to reproduce the bug. 50 | > 51 | > 1. This is the first step 52 | > 2. This is the second step 53 | > 3. Further steps, etc. 54 | > 55 | > `` - a link to the reduced test case 56 | > 57 | > Any other information you want to share that is relevant to the issue being 58 | > reported. This might include the lines of code that you have identified as 59 | > causing the bug, and potential solutions (and your opinions on their 60 | > merits). 61 | 62 | ## Feature requests 63 | 64 | Feature requests are welcome. But take a moment to find out whether your idea 65 | fits with the scope and aims of the project. It's up to _you_ to make a strong 66 | case to convince the project's developers of the merits of this feature. Please 67 | provide as much detail and context as possible. 68 | -------------------------------------------------------------------------------- /tools/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra') 2 | const rollup = require('rollup') 3 | const { babel } = require('@rollup/plugin-babel') 4 | const { terser } = require('rollup-plugin-terser') 5 | const pkg = require('../package.json') 6 | 7 | // The source files to be compiled by Rollup 8 | const files = [ 9 | { 10 | input: 'dist/src/browser.js', 11 | output: 'dist/hyperapp-render.js', 12 | format: 'umd', 13 | name: 'hyperappRender', 14 | }, 15 | { 16 | input: 'dist/src/browser.js', 17 | output: 'dist/hyperapp-render.min.js', 18 | format: 'umd', 19 | name: 'hyperappRender', 20 | }, 21 | { 22 | input: 'dist/src/browser.js', 23 | output: 'dist/commonjs/browser.js', 24 | format: 'cjs', 25 | }, 26 | { 27 | input: 'dist/src/node.js', 28 | output: 'dist/commonjs/node.js', 29 | format: 'cjs', 30 | }, 31 | { 32 | input: 'dist/src/browser.js', 33 | output: 'dist/esm/browser.js', 34 | format: 'es', 35 | }, 36 | { 37 | input: 'dist/src/node.js', 38 | output: 'dist/esm/node.js', 39 | format: 'es', 40 | }, 41 | ] 42 | 43 | async function build() { 44 | // Clean up the output directory 45 | await fs.emptyDir('dist') 46 | 47 | // Copy source code, readme and license 48 | await fs.copy('src', 'dist/src') 49 | await fs.copy('README.md', 'dist/README.md') 50 | await fs.copy('LICENSE.md', 'dist/LICENSE.md') 51 | 52 | // Compile source code into a distributable format with Babel 53 | await Promise.all( 54 | files.map(async (file) => { 55 | const bundle = await rollup.rollup({ 56 | input: file.input, 57 | external: ['stream'], 58 | plugins: [ 59 | babel({ 60 | babelrc: false, 61 | babelHelpers: 'inline', 62 | presets: [ 63 | [ 64 | '@babel/preset-env', 65 | { 66 | modules: false, 67 | loose: true, 68 | useBuiltIns: 'entry', 69 | exclude: ['transform-typeof-symbol'], 70 | corejs: 3, 71 | }, 72 | ], 73 | ], 74 | comments: false, 75 | }), 76 | ...(file.output.endsWith('.min.js') ? [terser({ output: { comments: '/^!/' } })] : []), 77 | ], 78 | }) 79 | 80 | bundle.write({ 81 | file: file.output, 82 | format: file.format, 83 | sourcemap: true, 84 | exports: 'named', 85 | name: file.name, 86 | banner: `/*! Hyperapp Render v${pkg.version} | MIT Licence | https://github.com/kriasoft/hyperapp-render */\n`, 87 | }) 88 | }), 89 | ) 90 | 91 | // Create package.json for npm publishing 92 | await fs.outputJson( 93 | 'dist/package.json', 94 | { 95 | ...pkg, 96 | private: undefined, 97 | devDependencies: undefined, 98 | scripts: undefined, 99 | }, 100 | { spaces: 2 }, 101 | ) 102 | await fs.outputJson('dist/commonjs/package.json', { type: 'commonjs' }, { spaces: 2 }) 103 | await fs.outputJson('dist/esm/package.json', { type: 'module' }, { spaces: 2 }) 104 | } 105 | 106 | module.exports = build() 107 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at vladimir.kutepov@kriasoft.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /benchmark/benchmark.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import { h } from 'hyperapp' 3 | import { escapeHtml, concatClassNames, stringifyStyles, renderToString } from '../src/index' 4 | 5 | suite('escapeHtml(value)', () => { 6 | benchmark('numeric value', () => { 7 | escapeHtml(123456789.012) 8 | }) 9 | 10 | benchmark('no special characters', () => { 11 | escapeHtml('hello world') 12 | }) 13 | 14 | benchmark('single special character', () => { 15 | escapeHtml('hello wor&d') 16 | }) 17 | 18 | benchmark('many special characters', () => { 19 | escapeHtml('"&&"') 20 | }) 21 | }) 22 | 23 | suite('concatClassNames(value)', () => { 24 | benchmark('string value', () => { 25 | concatClassNames('foo bar baz') 26 | }) 27 | 28 | benchmark('values array', () => { 29 | concatClassNames(['foo', 'bar', 'baz', null]) 30 | }) 31 | 32 | benchmark('values map', () => { 33 | concatClassNames({ foo: true, bar: 'ok', baz: 1, qux: null }) 34 | }) 35 | 36 | benchmark('mixed values', () => { 37 | concatClassNames(['foo', false, 0, null, { bar: 'ok', baz: 1, qux: null }]) 38 | }) 39 | 40 | benchmark('nested values', () => { 41 | concatClassNames(['foo', ['bar', { baz: 1 }, [false, { qux: null }]]]) 42 | }) 43 | }) 44 | 45 | suite('stringifyStyles(style)', () => { 46 | benchmark('no values', () => { 47 | stringifyStyles({ 48 | color: null, 49 | border: null, 50 | opacity: null, 51 | }) 52 | }) 53 | 54 | benchmark('basic styles', () => { 55 | stringifyStyles({ 56 | color: '#000', 57 | border: '1px solid', 58 | opacity: 0.5, 59 | }) 60 | }) 61 | 62 | benchmark('camel-case styles', () => { 63 | stringifyStyles({ 64 | backgroundColor: '#000', 65 | borderTop: '1px solid', 66 | lineHeight: 1.23, 67 | }) 68 | }) 69 | 70 | benchmark('vendor specific', () => { 71 | stringifyStyles({ 72 | webkitTransform: 'rotate(5deg)', 73 | MozTransform: 'rotate(5deg)', 74 | msTransform: 'rotate(5deg)', 75 | }) 76 | }) 77 | }) 78 | 79 | suite('renderAttributes(props)', () => { 80 | benchmark('no value', () => { 81 | renderToString(
) 82 | }) 83 | 84 | benchmark('boolean value', () => { 85 | renderToString(
) 86 | }) 87 | 88 | benchmark('string value', () => { 89 | renderToString(
) 90 | }) 91 | 92 | benchmark('special attributes', () => { 93 | renderToString(
) 94 | }) 95 | }) 96 | 97 | suite('renderToString(node)', () => { 98 | const Fragment = '' 99 | function Component(attributes, children) { 100 | return

{children}

101 | } 102 | 103 | benchmark('basic', () => { 104 | renderToString(

Hello World

) 105 | }) 106 | 107 | benchmark('fragment', () => { 108 | renderToString(Hello World) 109 | }) 110 | 111 | benchmark('component', () => { 112 | renderToString(Hello World) 113 | }) 114 | 115 | benchmark('array', () => { 116 | renderToString( 117 | 118 | A 119 | B 120 | C 121 | , 122 | ) 123 | }) 124 | 125 | benchmark('nested', () => { 126 | renderToString( 127 | 128 | A 129 | 130 | BC 131 | 132 | , 133 | ) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [4.0.1] - 2022-06-29 9 | 10 | - Allow empty string text nodes ([#23](https://github.com/kriasoft/hyperapp-render/pull/23)). 11 | 12 | ## [4.0.0] - 2021-01-25 13 | 14 | - [BREAKING] Drop support for legacy Hyperapp v2.0.0-v2.0.8. 15 | - Add compatibility with Hyperapp [v2.0.9](https://github.com/hyperapp/hyperapp/releases/tag/2.0.9) 16 | after internal VNode schema change. 17 | 18 | ## [3.5.0] - 2020-11-06 19 | 20 | - Compatibility with Hyperapp [v2.0.6](https://github.com/hyperapp/hyperapp/releases/tag/2.0.6) 21 | after internal VNode schema change. 22 | 23 | ## [3.4.0] - 2020-05-25 24 | 25 | - Hybrid npm package with both CommonJS and ESM versions 26 | ([#20](https://github.com/kriasoft/hyperapp-render/pull/20)). 27 | 28 | ## [3.3.0] - 2020-05-20 29 | 30 | - Add `unpkg`, `jsdelivr` and `exports` fields to package.json 31 | ([#18](https://github.com/kriasoft/hyperapp-render/pull/18)). 32 | 33 | ## [3.2.0] - 2020-05-14 34 | 35 | - Add support for `Lazy` component from Hyperapp v2 36 | ([#16](https://github.com/kriasoft/hyperapp-render/pull/16)). 37 | 38 | ## [3.1.0] - 2019-03-18 39 | 40 | - Ignore `innerHTML` attribute when child nodes exist. 41 | - Fix styles rendering in IE11 ([#14](https://github.com/kriasoft/hyperapp-render/pull/14)). 42 | 43 | ## [3.0.0] - 2018-11-15 44 | 45 | - [BREAKING] Remove higher-order app `withRender` from the library due to redundancy. 46 | - Support for `className` attribute and allow to use array and object as a value. 47 | - Compatibility with upcoming [Hyperapp V2](https://github.com/hyperapp/hyperapp/pull/726). 48 | - Various performance optimizations. 49 | 50 | ## [2.1.0] - 2018-07-11 51 | 52 | - Add [TypeScript](https://www.typescriptlang.org/) typings. 53 | 54 | ## [2.0.0] - 2018-03-19 55 | 56 | - [BREAKING] Rename higher-order app from `render` to `withRender`. 57 | - [BREAKING] Rename global exports from `window.*` to `window.hyperappRender.*`. 58 | - [BREAKING] Rename server package from `hyperapp-render/server` to `hyperapp-render`. 59 | - Add support for [lazy components](https://github.com/hyperapp/hyperapp/tree/1.2.0#lazy-components). 60 | 61 | ## [1.3.0] - 2018-02-24 62 | 63 | - Render `style` attribute with `cssText` correctly. 64 | - Better performance for numeric attributes. 65 | 66 | ## [1.2.0] - 2018-02-14 67 | 68 | - Ignore [jsx `__source`](https://babeljs.io/docs/plugins/transform-react-jsx-source/) attribute. 69 | 70 | ## [1.1.0] - 2018-02-07 71 | 72 | - Compatibility with Hyperapp [v1.1.0](https://github.com/hyperapp/hyperapp/releases/tag/1.1.0) 73 | after internal VNode schema change. 74 | 75 | ## 1.0.0 - 2018-01-24 76 | 77 | - Initial public release. 78 | 79 | [unreleased]: https://github.com/kriasoft/hyperapp-render/compare/v4.0.1...HEAD 80 | [4.0.1]: https://github.com/kriasoft/hyperapp-render/compare/v4.0.0...v4.0.1 81 | [4.0.0]: https://github.com/kriasoft/hyperapp-render/compare/v3.5.0...v4.0.0 82 | [3.5.0]: https://github.com/kriasoft/hyperapp-render/compare/v3.4.0...v3.5.0 83 | [3.4.0]: https://github.com/kriasoft/hyperapp-render/compare/v3.3.0...v3.4.0 84 | [3.3.0]: https://github.com/kriasoft/hyperapp-render/compare/v3.2.0...v3.3.0 85 | [3.2.0]: https://github.com/kriasoft/hyperapp-render/compare/v3.1.0...v3.2.0 86 | [3.1.0]: https://github.com/kriasoft/hyperapp-render/compare/v3.0.0...v3.1.0 87 | [3.0.0]: https://github.com/kriasoft/hyperapp-render/compare/v2.1.0...v3.0.0 88 | [2.1.0]: https://github.com/kriasoft/hyperapp-render/compare/v2.0.0...v2.1.0 89 | [2.0.0]: https://github.com/kriasoft/hyperapp-render/compare/v1.3.0...v2.0.0 90 | [1.3.0]: https://github.com/kriasoft/hyperapp-render/compare/v1.2.0...v1.3.0 91 | [1.2.0]: https://github.com/kriasoft/hyperapp-render/compare/v1.1.0...v1.2.0 92 | [1.1.0]: https://github.com/kriasoft/hyperapp-render/compare/v1.0.0...v1.1.0 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperapp Render 2 | 3 | [![npm version](https://img.shields.io/npm/v/hyperapp-render.svg)](https://www.npmjs.com/package/hyperapp-render) 4 | [![npm downloads](https://img.shields.io/npm/dw/hyperapp-render.svg)](https://www.npmjs.com/package/hyperapp-render) 5 | [![library size](https://img.shields.io/bundlephobia/minzip/hyperapp-render.svg)](https://bundlephobia.com/result?p=hyperapp-render) 6 | [![discord chat](https://img.shields.io/discord/804672552348680192)](https://discord.gg/eFvZXzXF9U 'Join us') 7 | 8 | This library is allowing you to render 9 | [Hyperapp](https://github.com/hyperapp/hyperapp) views to an HTML string. 10 | 11 | - **User experience** — Generate HTML on the server and send the markup 12 | down on the initial request for faster page loads. Built-in 13 | [mounting](https://github.com/hyperapp/hyperapp/tree/1.2.9#mounting) 14 | feature in Hyperapp is allowing you to have a very performant first-load experience. 15 | - **Accessibility** — Allow search engines to crawl your pages for 16 | [SEO](https://en.wikipedia.org/wiki/Search_engine_optimization) purposes. 17 | - **Testability** — [Check HTML validity](https://en.wikipedia.org/wiki/Validator) and use 18 | [snapshot testing](https://jestjs.io/docs/en/snapshot-testing.html) 19 | to improve quality of your software. 20 | 21 | ## Getting Started 22 | 23 | Our first example is an interactive app from which you can generate an HTML markup. 24 | Go ahead and [try it online](https://codepen.io/frenzzy/pen/zpmRQY/left/?editors=0010). 25 | 26 | ```jsx 27 | import { h } from 'hyperapp' 28 | import { renderToString } from 'hyperapp-render' 29 | 30 | const state = { 31 | text: 'Hello' 32 | } 33 | 34 | const actions = { 35 | setText: text => ({ text }) 36 | } 37 | 38 | const view = (state, actions) => ( 39 |
40 |

{state.text.trim() === '' ? '👋' : state.text}

41 | actions.setText(e.target.value)} /> 42 |
43 | ) 44 | 45 | const html = renderToString(view(state, actions)) 46 | 47 | console.log(html) // =>

Hello

48 | ``` 49 | 50 | Looking for a boilerplate? 51 | Try [Hyperapp Starter](https://github.com/kriasoft/hyperapp-starter) 52 | with pre-configured server-side rendering and many more. 53 | 54 | ## Installation 55 | 56 | Using [npm](https://www.npmjs.com/package/hyperapp-render): 57 | 58 | ```bash 59 | npm install hyperapp-render --save 60 | ``` 61 | 62 | Or using a [CDN](https://en.wikipedia.org/wiki/Content_delivery_network) like 63 | [unpkg.com](https://unpkg.com/hyperapp-render) or 64 | [jsDelivr](https://cdn.jsdelivr.net/npm/hyperapp-render) 65 | with the following script tag: 66 | 67 | ```html 68 | 69 | ``` 70 | 71 | You can find the library in `window.hyperappRender`. 72 | 73 | We support all ES5-compliant browsers, including Internet Explorer 9 and above, 74 | but depending on your target browsers you may need to include 75 | [polyfills]() for 76 | [`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) and 77 | [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) 78 | before any other code. 79 | 80 | ## Usage 81 | 82 | The library provides two functions 83 | which you can use depending on your needs or personal preferences: 84 | 85 | ```jsx 86 | import { renderToString, renderToStream } from 'hyperapp-render' 87 | 88 | renderToString() // => 89 | renderToString(view(state, actions)) // => 90 | renderToString(view, state, actions) // => 91 | 92 | renderToStream() // => => 93 | renderToStream(view(state, actions)) // => => 94 | renderToStream(view, state, actions) // => => 95 | ``` 96 | 97 | **Note:** `renderToStream` is available from 98 | [Node.js](https://nodejs.org/en/) environment only (v6 or newer). 99 | 100 | ## Overview 101 | 102 | You can use `renderToString` function to generate HTML on the server 103 | and send the markup down on the initial request for faster page loads 104 | and to allow search engines to crawl your pages for 105 | [SEO](https://en.wikipedia.org/wiki/Search_engine_optimization) purposes. 106 | 107 | If you call [`hyperapp.app()`](https://github.com/hyperapp/hyperapp/tree/1.2.9#mounting) 108 | on a node that already has this server-rendered markup, 109 | Hyperapp will preserve it and only attach event handlers, allowing you 110 | to have a very performant first-load experience. 111 | 112 | The `renderToStream` function returns a 113 | [Readable stream](https://nodejs.org/api/stream.html#stream_readable_streams) 114 | that outputs an HTML string. 115 | The HTML output by this stream is exactly equal to what `renderToString` would return. 116 | By using this function you can reduce [TTFB](https://en.wikipedia.org/wiki/Time_to_first_byte) 117 | and improve user experience even more. 118 | 119 | ## Caveats 120 | 121 | The library automatically escapes text content and attribute values 122 | of [virtual DOM nodes](https://github.com/hyperapp/hyperapp/tree/1.2.9#view) 123 | to protect your application against 124 | [XSS](https://en.wikipedia.org/wiki/Cross-site_scripting) attacks. 125 | However, it is not safe to allow "user input" for node names or attribute keys: 126 | 127 | ```jsx 128 | const Node = 'div onclick="alert()"' 129 | renderToString(Hi) 130 | // =>
Hi
131 | 132 | const attributes = { 'onclick="alert()" title': 'XSS' } 133 | renderToString(
Hi
) 134 | // =>
Hi
135 | 136 | const userInput = '' 137 | renderToString(
Hi
) 138 | // =>
139 | ``` 140 | 141 | ## License 142 | 143 | Hyperapp Render is MIT licensed. 144 | See [LICENSE](https://github.com/kriasoft/hyperapp-render/blob/master/LICENSE.md). 145 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Hyperapp Render 2 | 3 | ♥ [Hyperapp Render](https://github.com/kriasoft/hyperapp-render) and want to 4 | get involved? Thanks! We're actively looking for folks interested in helping 5 | out and there are plenty of ways you can help! 6 | 7 | Please take a moment to review this document in order to make the contribution 8 | process easy and effective for everyone involved. 9 | 10 | Following these guidelines helps to communicate that you respect the time of 11 | the developers managing and developing this open source project. In return, 12 | they should reciprocate that respect in addressing your issue or assessing 13 | patches and features. 14 | 15 | ## Using the issue tracker 16 | 17 | The [issue tracker](https://github.com/kriasoft/hyperapp-render/issues) is 18 | the preferred channel for [bug reports](#bugs), [features requests](#features) 19 | and [submitting pull requests](#pull-requests), but please respect the following 20 | restrictions: 21 | 22 | - Please **do not** use the issue tracker for personal support requests (use 23 | [Slack Chat](https://hyperappjs.herokuapp.com/)). 24 | 25 | - Please **do not** derail or troll issues. Keep the discussion on topic and 26 | respect the opinions of others. 27 | 28 | - Please **do not** open issues or pull requests regarding the code in 29 | [`Hyperapp`](https://github.com/hyperapp/hyperapp) or 30 | [`Babel`](https://github.com/babel/babel) 31 | (open them in their respective repositories). 32 | 33 | 34 | 35 | ## Bug reports 36 | 37 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 38 | Good bug reports are extremely helpful - thank you! 39 | 40 | Guidelines for bug reports: 41 | 42 | 1. **Use the GitHub issue search** — check if the issue has already been 43 | reported. 44 | 45 | 2. **Check if the issue has been fixed** — try to reproduce it using the 46 | latest `master` or development branch in the repository. 47 | 48 | 3. **Isolate the problem** — ideally create a [reduced test 49 | case](https://css-tricks.com/reduced-test-cases/) and a live example. 50 | 51 | A good bug report shouldn't leave others needing to chase you up for more 52 | information. Please try to be as detailed as possible in your report. What is 53 | your environment? What steps will reproduce the issue? What browser(s) and OS 54 | experience the problem? What would you expect to be the outcome? All these 55 | details will help people to fix any potential bugs. 56 | 57 | Example: 58 | 59 | > Short and descriptive example bug report title 60 | > 61 | > A summary of the issue and the browser/OS environment in which it occurs. If 62 | > suitable, include the steps required to reproduce the bug. 63 | > 64 | > 1. This is the first step 65 | > 2. This is the second step 66 | > 3. Further steps, etc. 67 | > 68 | > `` - a link to the reduced test case 69 | > 70 | > Any other information you want to share that is relevant to the issue being 71 | > reported. This might include the lines of code that you have identified as 72 | > causing the bug, and potential solutions (and your opinions on their 73 | > merits). 74 | 75 | 76 | 77 | ## Feature requests 78 | 79 | Feature requests are welcome. But take a moment to find out whether your idea 80 | fits with the scope and aims of the project. It's up to _you_ to make a strong 81 | case to convince the project's developers of the merits of this feature. Please 82 | provide as much detail and context as possible. 83 | 84 | 85 | 86 | ## Pull requests 87 | 88 | Good pull requests - patches, improvements, new features - are a fantastic 89 | help. They should remain focused in scope and avoid containing unrelated 90 | commits. 91 | 92 | **Please ask first** before embarking on any significant pull request (e.g. 93 | implementing features, refactoring code, porting to a different language), 94 | otherwise you risk spending a lot of time working on something that the 95 | project's developers might not want to merge into the project. 96 | 97 | Please adhere to the coding conventions used throughout a project (indentation, 98 | accurate comments, etc.) and any other requirements (such as test coverage). 99 | 100 | Adhering to the following process is the best way to get your work 101 | included in the project: 102 | 103 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your 104 | fork, and configure the remotes: 105 | 106 | ```bash 107 | # Clone your fork of the repo into the current directory 108 | git clone https://github.com//hyperapp-render.git 109 | # Navigate to the newly cloned directory 110 | cd hyperapp-render 111 | # Assign the original repo to a remote called "upstream" 112 | git remote add upstream https://github.com/kriasoft/hyperapp-render.git 113 | ``` 114 | 115 | 2. If you cloned a while ago, get the latest changes from upstream: 116 | 117 | ```bash 118 | git checkout master 119 | git pull upstream master 120 | ``` 121 | 122 | 3. Create a new topic branch (off the main project development branch) to 123 | contain your feature, change, or fix: 124 | 125 | ```bash 126 | git checkout -b 127 | ``` 128 | 129 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 130 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 131 | or your code is unlikely be merged into the main project. Use Git's 132 | [interactive rebase](https://help.github.com/articles/about-git-rebase/) 133 | feature to tidy up your commits before making them public. 134 | 135 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 136 | 137 | ```bash 138 | git pull [--rebase] upstream master 139 | ``` 140 | 141 | 6. Push your topic branch up to your fork: 142 | 143 | ```bash 144 | git push origin 145 | ``` 146 | 147 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 148 | with a clear title and description. 149 | 150 | **IMPORTANT**: By submitting a patch, you agree to allow the project 151 | owners to license your work under the terms of the 152 | [MIT License](https://github.com/kriasoft/hyperapp-render/blob/master/LICENSE.md). 153 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { isArray } = Array 2 | const { hasOwnProperty } = Object.prototype 3 | const styleNameCache = new Map() 4 | const uppercasePattern = /[A-Z]/g 5 | const msPattern = /^ms-/ 6 | 7 | // https://www.w3.org/International/questions/qa-escapes#use 8 | const escapeRegExp = /["&'<>]/ 9 | 10 | // https://www.w3.org/TR/html/syntax.html#void-elements 11 | const voidElements = new Set([ 12 | 'area', 13 | 'base', 14 | 'br', 15 | 'col', 16 | 'embed', 17 | 'hr', 18 | 'img', 19 | 'input', 20 | 'link', 21 | 'meta', 22 | 'param', 23 | 'source', 24 | 'track', 25 | 'wbr', 26 | ]) 27 | 28 | // credits to https://github.com/component/escape-html 29 | export function escapeHtml(value) { 30 | if (value == null) return '' 31 | const str = '' + value 32 | if (typeof value === 'number') { 33 | // better performance for safe values 34 | return str 35 | } 36 | 37 | const match = escapeRegExp.exec(str) 38 | if (!match) { 39 | return str 40 | } 41 | 42 | let { index } = match 43 | let lastIndex = 0 44 | let out = '' 45 | 46 | for (let escape = ''; index < str.length; index++) { 47 | switch (str.charCodeAt(index)) { 48 | case 34: // " 49 | escape = '"' 50 | break 51 | case 38: // & 52 | escape = '&' 53 | break 54 | case 39: // ' 55 | escape = ''' // shorter than "'" and "'" plus supports HTML4 56 | break 57 | case 60: // < 58 | escape = '<' 59 | break 60 | case 62: // > 61 | escape = '>' 62 | break 63 | default: 64 | continue 65 | } 66 | 67 | if (lastIndex !== index) { 68 | out += str.substring(lastIndex, index) 69 | } 70 | 71 | lastIndex = index + 1 72 | out += escape 73 | } 74 | 75 | return lastIndex !== index ? out + str.substring(lastIndex, index) : out 76 | } 77 | 78 | // credits to https://github.com/jorgebucaran/classcat 79 | export function concatClassNames(value) { 80 | if (typeof value === 'string' || typeof value === 'number') { 81 | return value || '' 82 | } 83 | 84 | let out = '' 85 | let delimiter = '' 86 | 87 | if (isArray(value)) { 88 | for (let i = 0; i < value.length; i++) { 89 | const name = concatClassNames(value[i]) 90 | if (name !== '') { 91 | out += delimiter + name 92 | delimiter = ' ' 93 | } 94 | } 95 | } else { 96 | for (const name in value) { 97 | if (hasOwnProperty.call(value, name) && value[name]) { 98 | out += delimiter + name 99 | delimiter = ' ' 100 | } 101 | } 102 | } 103 | 104 | return out 105 | } 106 | 107 | // "backgroundColor" => "background-color" 108 | // "MozTransition" => "-moz-transition" 109 | // "msTransition" => "-ms-transition" 110 | function hyphenateStyleName(styleName) { 111 | if (!styleNameCache.has(styleName)) { 112 | const name = styleName.replace(uppercasePattern, '-$&').toLowerCase().replace(msPattern, '-ms-') 113 | 114 | // returns 'undefined' instead of the 'Map' object in IE11 115 | styleNameCache.set(styleName, name) 116 | } 117 | return styleNameCache.get(styleName) 118 | } 119 | 120 | export function stringifyStyles(style) { 121 | let out = '' 122 | let delimiter = '' 123 | 124 | for (const name in style) { 125 | if (hasOwnProperty.call(style, name)) { 126 | const value = style[name] 127 | 128 | if (value != null) { 129 | if (name === 'cssText') { 130 | out += delimiter + value 131 | } else { 132 | out += delimiter + hyphenateStyleName(name) + ':' + value 133 | } 134 | delimiter = ';' 135 | } 136 | } 137 | } 138 | 139 | return out 140 | } 141 | 142 | // https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments 143 | function renderFragment(name, props, children, stack) { 144 | let out = '' 145 | let footer = '' 146 | 147 | if (name) { 148 | out += '<' + name 149 | 150 | for (let prop in props) { 151 | if (hasOwnProperty.call(props, prop)) { 152 | let value = props[prop] 153 | 154 | if ( 155 | value != null && 156 | prop !== 'key' && 157 | prop !== 'innerHTML' && 158 | prop !== '__source' && // babel-plugin-transform-react-jsx-source 159 | !(prop[0] === 'o' && prop[1] === 'n') 160 | ) { 161 | if (prop === 'class' || prop === 'className') { 162 | prop = 'class' 163 | value = concatClassNames(value) || false 164 | } else if (prop === 'style' && typeof value === 'object') { 165 | value = stringifyStyles(value) || false 166 | } 167 | 168 | if (value !== false) { 169 | out += ' ' + prop 170 | 171 | if (value !== true) { 172 | out += '="' + escapeHtml(value) + '"' 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | if (voidElements.has(name)) { 180 | out += '/>' 181 | } else { 182 | out += '>' 183 | footer = '' 184 | } 185 | } 186 | 187 | if (children.length > 0) { 188 | stack.push({ 189 | childIndex: 0, 190 | children, 191 | footer, 192 | }) 193 | } else { 194 | const { innerHTML } = props 195 | 196 | if (innerHTML != null) { 197 | out += innerHTML 198 | } 199 | 200 | out += footer 201 | } 202 | 203 | return out 204 | } 205 | 206 | function resolveNode(node, state, actions) { 207 | if (typeof node === 'function') { 208 | return resolveNode(node(state, actions), state, actions) 209 | } 210 | if (node && node.type === 2) { 211 | return resolveNode(node.lazy.view(node.lazy), state, actions) 212 | } 213 | return node 214 | } 215 | 216 | export function renderer(view, state, actions) { 217 | const stack = [ 218 | { 219 | childIndex: 0, 220 | children: [view], 221 | footer: '', 222 | }, 223 | ] 224 | let end = false 225 | 226 | return (bytes) => { 227 | if (end) { 228 | return null 229 | } 230 | 231 | let out = '' 232 | 233 | while (out.length < bytes) { 234 | if (stack.length === 0) { 235 | end = true 236 | break 237 | } 238 | 239 | const frame = stack[stack.length - 1] 240 | 241 | if (frame.childIndex >= frame.children.length) { 242 | out += frame.footer 243 | stack.pop() 244 | } else { 245 | const node = resolveNode(frame.children[frame.childIndex++], state, actions) 246 | 247 | if (node != null && typeof node !== 'boolean') { 248 | if (isArray(node)) { 249 | stack.push({ 250 | childIndex: 0, 251 | children: node, 252 | footer: '', 253 | }) 254 | } else if (node.type === 3) { 255 | out += escapeHtml(node.tag || node.name) 256 | } else if (typeof node === 'object') { 257 | out += renderFragment( 258 | node.tag || node.nodeName, 259 | node.props || node.attributes, 260 | node.children, 261 | stack, 262 | ) 263 | } else { 264 | out += escapeHtml(node) 265 | } 266 | } 267 | } 268 | } 269 | 270 | return out 271 | } 272 | } 273 | 274 | export function renderToString(view, state, actions) { 275 | return renderer(view, state, actions)(Infinity) 276 | } 277 | -------------------------------------------------------------------------------- /dist/hyperapp-render.js: -------------------------------------------------------------------------------- 1 | /*! Hyperapp Render v4.0.1 | MIT Licence | https://github.com/kriasoft/hyperapp-render */ 2 | 3 | (function (global, factory) { 4 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 5 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 6 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.hyperappRender = {})); 7 | })(this, (function (exports) { 'use strict'; 8 | 9 | var isArray = Array.isArray; 10 | var hasOwnProperty = Object.prototype.hasOwnProperty; 11 | var styleNameCache = new Map(); 12 | var uppercasePattern = /[A-Z]/g; 13 | var msPattern = /^ms-/; 14 | var escapeRegExp = /["&'<>]/; 15 | var voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']); 16 | function escapeHtml(value) { 17 | if (value == null) return ''; 18 | var str = '' + value; 19 | 20 | if (typeof value === 'number') { 21 | return str; 22 | } 23 | 24 | var match = escapeRegExp.exec(str); 25 | 26 | if (!match) { 27 | return str; 28 | } 29 | 30 | var index = match.index; 31 | var lastIndex = 0; 32 | var out = ''; 33 | 34 | for (var _escape = ''; index < str.length; index++) { 35 | switch (str.charCodeAt(index)) { 36 | case 34: 37 | _escape = '"'; 38 | break; 39 | 40 | case 38: 41 | _escape = '&'; 42 | break; 43 | 44 | case 39: 45 | _escape = '''; 46 | break; 47 | 48 | case 60: 49 | _escape = '<'; 50 | break; 51 | 52 | case 62: 53 | _escape = '>'; 54 | break; 55 | 56 | default: 57 | continue; 58 | } 59 | 60 | if (lastIndex !== index) { 61 | out += str.substring(lastIndex, index); 62 | } 63 | 64 | lastIndex = index + 1; 65 | out += _escape; 66 | } 67 | 68 | return lastIndex !== index ? out + str.substring(lastIndex, index) : out; 69 | } 70 | function concatClassNames(value) { 71 | if (typeof value === 'string' || typeof value === 'number') { 72 | return value || ''; 73 | } 74 | 75 | var out = ''; 76 | var delimiter = ''; 77 | 78 | if (isArray(value)) { 79 | for (var i = 0; i < value.length; i++) { 80 | var name = concatClassNames(value[i]); 81 | 82 | if (name !== '') { 83 | out += delimiter + name; 84 | delimiter = ' '; 85 | } 86 | } 87 | } else { 88 | for (var _name in value) { 89 | if (hasOwnProperty.call(value, _name) && value[_name]) { 90 | out += delimiter + _name; 91 | delimiter = ' '; 92 | } 93 | } 94 | } 95 | 96 | return out; 97 | } 98 | 99 | function hyphenateStyleName(styleName) { 100 | if (!styleNameCache.has(styleName)) { 101 | var name = styleName.replace(uppercasePattern, '-$&').toLowerCase().replace(msPattern, '-ms-'); 102 | styleNameCache.set(styleName, name); 103 | } 104 | 105 | return styleNameCache.get(styleName); 106 | } 107 | 108 | function stringifyStyles(style) { 109 | var out = ''; 110 | var delimiter = ''; 111 | 112 | for (var name in style) { 113 | if (hasOwnProperty.call(style, name)) { 114 | var value = style[name]; 115 | 116 | if (value != null) { 117 | if (name === 'cssText') { 118 | out += delimiter + value; 119 | } else { 120 | out += delimiter + hyphenateStyleName(name) + ':' + value; 121 | } 122 | 123 | delimiter = ';'; 124 | } 125 | } 126 | } 127 | 128 | return out; 129 | } 130 | 131 | function renderFragment(name, props, children, stack) { 132 | var out = ''; 133 | var footer = ''; 134 | 135 | if (name) { 136 | out += '<' + name; 137 | 138 | for (var prop in props) { 139 | if (hasOwnProperty.call(props, prop)) { 140 | var value = props[prop]; 141 | 142 | if (value != null && prop !== 'key' && prop !== 'innerHTML' && prop !== '__source' && !(prop[0] === 'o' && prop[1] === 'n')) { 143 | if (prop === 'class' || prop === 'className') { 144 | prop = 'class'; 145 | value = concatClassNames(value) || false; 146 | } else if (prop === 'style' && typeof value === 'object') { 147 | value = stringifyStyles(value) || false; 148 | } 149 | 150 | if (value !== false) { 151 | out += ' ' + prop; 152 | 153 | if (value !== true) { 154 | out += '="' + escapeHtml(value) + '"'; 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | if (voidElements.has(name)) { 162 | out += '/>'; 163 | } else { 164 | out += '>'; 165 | footer = ''; 166 | } 167 | } 168 | 169 | if (children.length > 0) { 170 | stack.push({ 171 | childIndex: 0, 172 | children: children, 173 | footer: footer 174 | }); 175 | } else { 176 | var innerHTML = props.innerHTML; 177 | 178 | if (innerHTML != null) { 179 | out += innerHTML; 180 | } 181 | 182 | out += footer; 183 | } 184 | 185 | return out; 186 | } 187 | 188 | function resolveNode(node, state, actions) { 189 | if (typeof node === 'function') { 190 | return resolveNode(node(state, actions), state, actions); 191 | } 192 | 193 | if (node && node.type === 2) { 194 | return resolveNode(node.lazy.view(node.lazy), state, actions); 195 | } 196 | 197 | return node; 198 | } 199 | 200 | function renderer(view, state, actions) { 201 | var stack = [{ 202 | childIndex: 0, 203 | children: [view], 204 | footer: '' 205 | }]; 206 | var end = false; 207 | return function (bytes) { 208 | if (end) { 209 | return null; 210 | } 211 | 212 | var out = ''; 213 | 214 | while (out.length < bytes) { 215 | if (stack.length === 0) { 216 | end = true; 217 | break; 218 | } 219 | 220 | var frame = stack[stack.length - 1]; 221 | 222 | if (frame.childIndex >= frame.children.length) { 223 | out += frame.footer; 224 | stack.pop(); 225 | } else { 226 | var node = resolveNode(frame.children[frame.childIndex++], state, actions); 227 | 228 | if (node != null && typeof node !== 'boolean') { 229 | if (isArray(node)) { 230 | stack.push({ 231 | childIndex: 0, 232 | children: node, 233 | footer: '' 234 | }); 235 | } else if (node.type === 3) { 236 | out += escapeHtml(node.tag || node.name); 237 | } else if (typeof node === 'object') { 238 | out += renderFragment(node.tag || node.nodeName, node.props || node.attributes, node.children, stack); 239 | } else { 240 | out += escapeHtml(node); 241 | } 242 | } 243 | } 244 | } 245 | 246 | return out; 247 | }; 248 | } 249 | function renderToString(view, state, actions) { 250 | return renderer(view, state, actions)(Infinity); 251 | } 252 | 253 | exports.renderToString = renderToString; 254 | 255 | Object.defineProperty(exports, '__esModule', { value: true }); 256 | 257 | })); 258 | //# sourceMappingURL=hyperapp-render.js.map 259 | -------------------------------------------------------------------------------- /dist/hyperapp-render.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"hyperapp-render.min.js","sources":["src/index.js"],"sourcesContent":["const { isArray } = Array\nconst { hasOwnProperty } = Object.prototype\nconst styleNameCache = new Map()\nconst uppercasePattern = /[A-Z]/g\nconst msPattern = /^ms-/\n\n// https://www.w3.org/International/questions/qa-escapes#use\nconst escapeRegExp = /[\"&'<>]/\n\n// https://www.w3.org/TR/html/syntax.html#void-elements\nconst voidElements = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\n// credits to https://github.com/component/escape-html\nexport function escapeHtml(value) {\n if (value == null) return ''\n const str = '' + value\n if (typeof value === 'number') {\n // better performance for safe values\n return str\n }\n\n const match = escapeRegExp.exec(str)\n if (!match) {\n return str\n }\n\n let { index } = match\n let lastIndex = 0\n let out = ''\n\n for (let escape = ''; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"'\n break\n case 38: // &\n escape = '&'\n break\n case 39: // '\n escape = ''' // shorter than \"'\" and \"'\" plus supports HTML4\n break\n case 60: // <\n escape = '<'\n break\n case 62: // >\n escape = '>'\n break\n default:\n continue\n }\n\n if (lastIndex !== index) {\n out += str.substring(lastIndex, index)\n }\n\n lastIndex = index + 1\n out += escape\n }\n\n return lastIndex !== index ? out + str.substring(lastIndex, index) : out\n}\n\n// credits to https://github.com/jorgebucaran/classcat\nexport function concatClassNames(value) {\n if (typeof value === 'string' || typeof value === 'number') {\n return value || ''\n }\n\n let out = ''\n let delimiter = ''\n\n if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const name = concatClassNames(value[i])\n if (name !== '') {\n out += delimiter + name\n delimiter = ' '\n }\n }\n } else {\n for (const name in value) {\n if (hasOwnProperty.call(value, name) && value[name]) {\n out += delimiter + name\n delimiter = ' '\n }\n }\n }\n\n return out\n}\n\n// \"backgroundColor\" => \"background-color\"\n// \"MozTransition\" => \"-moz-transition\"\n// \"msTransition\" => \"-ms-transition\"\nfunction hyphenateStyleName(styleName) {\n if (!styleNameCache.has(styleName)) {\n const name = styleName.replace(uppercasePattern, '-$&').toLowerCase().replace(msPattern, '-ms-')\n\n // returns 'undefined' instead of the 'Map' object in IE11\n styleNameCache.set(styleName, name)\n }\n return styleNameCache.get(styleName)\n}\n\nexport function stringifyStyles(style) {\n let out = ''\n let delimiter = ''\n\n for (const name in style) {\n if (hasOwnProperty.call(style, name)) {\n const value = style[name]\n\n if (value != null) {\n if (name === 'cssText') {\n out += delimiter + value\n } else {\n out += delimiter + hyphenateStyleName(name) + ':' + value\n }\n delimiter = ';'\n }\n }\n }\n\n return out\n}\n\n// https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\nfunction renderFragment(name, props, children, stack) {\n let out = ''\n let footer = ''\n\n if (name) {\n out += '<' + name\n\n for (let prop in props) {\n if (hasOwnProperty.call(props, prop)) {\n let value = props[prop]\n\n if (\n value != null &&\n prop !== 'key' &&\n prop !== 'innerHTML' &&\n prop !== '__source' && // babel-plugin-transform-react-jsx-source\n !(prop[0] === 'o' && prop[1] === 'n')\n ) {\n if (prop === 'class' || prop === 'className') {\n prop = 'class'\n value = concatClassNames(value) || false\n } else if (prop === 'style' && typeof value === 'object') {\n value = stringifyStyles(value) || false\n }\n\n if (value !== false) {\n out += ' ' + prop\n\n if (value !== true) {\n out += '=\"' + escapeHtml(value) + '\"'\n }\n }\n }\n }\n }\n\n if (voidElements.has(name)) {\n out += '/>'\n } else {\n out += '>'\n footer = ''\n }\n }\n\n if (children.length > 0) {\n stack.push({\n childIndex: 0,\n children,\n footer,\n })\n } else {\n const { innerHTML } = props\n\n if (innerHTML != null) {\n out += innerHTML\n }\n\n out += footer\n }\n\n return out\n}\n\nfunction resolveNode(node, state, actions) {\n if (typeof node === 'function') {\n return resolveNode(node(state, actions), state, actions)\n }\n if (node && node.type === 2) {\n return resolveNode(node.lazy.view(node.lazy), state, actions)\n }\n return node\n}\n\nexport function renderer(view, state, actions) {\n const stack = [\n {\n childIndex: 0,\n children: [view],\n footer: '',\n },\n ]\n let end = false\n\n return (bytes) => {\n if (end) {\n return null\n }\n\n let out = ''\n\n while (out.length < bytes) {\n if (stack.length === 0) {\n end = true\n break\n }\n\n const frame = stack[stack.length - 1]\n\n if (frame.childIndex >= frame.children.length) {\n out += frame.footer\n stack.pop()\n } else {\n const node = resolveNode(frame.children[frame.childIndex++], state, actions)\n\n if (node != null && typeof node !== 'boolean') {\n if (isArray(node)) {\n stack.push({\n childIndex: 0,\n children: node,\n footer: '',\n })\n } else if (node.type === 3) {\n out += escapeHtml(node.tag || node.name)\n } else if (typeof node === 'object') {\n out += renderFragment(\n node.tag || node.nodeName,\n node.props || node.attributes,\n node.children,\n stack,\n )\n } else {\n out += escapeHtml(node)\n }\n }\n }\n }\n\n return out\n }\n}\n\nexport function renderToString(view, state, actions) {\n return renderer(view, state, actions)(Infinity)\n}\n"],"names":["isArray","Array","hasOwnProperty","Object","prototype","styleNameCache","Map","uppercasePattern","msPattern","escapeRegExp","voidElements","Set","escapeHtml","value","str","match","exec","index","lastIndex","out","escape","length","charCodeAt","substring","concatClassNames","delimiter","i","name","call","hyphenateStyleName","styleName","has","replace","toLowerCase","set","get","stringifyStyles","style","renderFragment","props","children","stack","footer","prop","push","childIndex","innerHTML","resolveNode","node","state","actions","type","lazy","view","end","bytes","frame","pop","tag","nodeName","attributes","renderer","Infinity"],"mappings":";sPAAA,IAAQA,EAAYC,MAAZD,QACAE,EAAmBC,OAAOC,UAA1BF,eACFG,EAAiB,IAAIC,IACrBC,EAAmB,SACnBC,EAAY,OAGZC,EAAe,UAGfC,EAAe,IAAIC,IAAI,CAC3B,OACA,OACA,KACA,MACA,QACA,KACA,MACA,QACA,OACA,OACA,QACA,SACA,QACA,QAIK,SAASC,EAAWC,GACzB,GAAa,MAATA,EAAe,MAAO,GAC1B,IAAMC,EAAM,GAAKD,EACjB,GAAqB,iBAAVA,EAET,OAAOC,EAGT,IAAMC,EAAQN,EAAaO,KAAKF,GAChC,IAAKC,EACH,OAAOD,EAOT,IAJA,IAAMG,EAAUF,EAAVE,MACFC,EAAY,EACZC,EAAM,GAEDC,EAAS,GAAIH,EAAQH,EAAIO,OAAQJ,IAAS,CACjD,OAAQH,EAAIQ,WAAWL,IACrB,KAAK,GACHG,EAAS,SACT,MACF,KAAK,GACHA,EAAS,QACT,MACF,KAAK,GACHA,EAAS,QACT,MACF,KAAK,GACHA,EAAS,OACT,MACF,KAAK,GACHA,EAAS,OACT,MACF,QACE,SAGAF,IAAcD,IAChBE,GAAOL,EAAIS,UAAUL,EAAWD,IAGlCC,EAAYD,EAAQ,EACpBE,GAAOC,EAGT,OAAOF,IAAcD,EAAQE,EAAML,EAAIS,UAAUL,EAAWD,GAASE,EAIhE,SAASK,EAAiBX,GAC/B,GAAqB,iBAAVA,GAAuC,iBAAVA,EACtC,OAAOA,GAAS,GAGlB,IAAIM,EAAM,GACNM,EAAY,GAEhB,GAAIzB,EAAQa,GACV,IAAK,IAAIa,EAAI,EAAGA,EAAIb,EAAMQ,OAAQK,IAAK,CACrC,IAAMC,EAAOH,EAAiBX,EAAMa,IACvB,KAATC,IACFR,GAAOM,EAAYE,EACnBF,EAAY,UAIhB,IAAK,IAAME,KAAQd,EACbX,EAAe0B,KAAKf,EAAOc,IAASd,EAAMc,KAC5CR,GAAOM,EAAYE,EACnBF,EAAY,KAKlB,OAAON,EAMT,SAASU,EAAmBC,GAC1B,IAAKzB,EAAe0B,IAAID,GAAY,CAClC,IAAMH,EAAOG,EAAUE,QAAQzB,EAAkB,OAAO0B,cAAcD,QAAQxB,EAAW,QAGzFH,EAAe6B,IAAIJ,EAAWH,GAEhC,OAAOtB,EAAe8B,IAAIL,GAGrB,SAASM,EAAgBC,GAC9B,IAAIlB,EAAM,GACNM,EAAY,GAEhB,IAAK,IAAME,KAAQU,EACjB,GAAInC,EAAe0B,KAAKS,EAAOV,GAAO,CACpC,IAAMd,EAAQwB,EAAMV,GAEP,MAATd,IAEAM,GADW,YAATQ,EACKF,EAAYZ,EAEZY,EAAYI,EAAmBF,GAAQ,IAAMd,EAEtDY,EAAY,KAKlB,OAAON,EAIT,SAASmB,EAAeX,EAAMY,EAAOC,EAAUC,GAC7C,IAAItB,EAAM,GACNuB,EAAS,GAEb,GAAIf,EAAM,CAGR,IAAK,IAAIgB,KAFTxB,GAAO,IAAMQ,EAEIY,EACf,GAAIrC,EAAe0B,KAAKW,EAAOI,GAAO,CACpC,IAAI9B,EAAQ0B,EAAMI,GAGP,MAAT9B,GACS,QAAT8B,GACS,cAATA,GACS,aAATA,GACc,MAAZA,EAAK,IAA0B,MAAZA,EAAK,KAEb,UAATA,GAA6B,cAATA,GACtBA,EAAO,QACP9B,EAAQW,EAAiBX,KAAU,GACjB,UAAT8B,GAAqC,iBAAV9B,IACpCA,EAAQuB,EAAgBvB,KAAU,IAGtB,IAAVA,IACFM,GAAO,IAAMwB,GAEC,IAAV9B,IACFM,GAAO,KAAOP,EAAWC,GAAS,OAOxCH,EAAaqB,IAAIJ,GACnBR,GAAO,MAEPA,GAAO,IACPuB,EAAS,KAAOf,EAAO,KAI3B,GAAIa,EAASnB,OAAS,EACpBoB,EAAMG,KAAK,CACTC,WAAY,EACZL,SAAAA,EACAE,OAAAA,QAEG,CACL,IAAQI,EAAcP,EAAdO,UAES,MAAbA,IACF3B,GAAO2B,GAGT3B,GAAOuB,EAGT,OAAOvB,EAGT,SAAS4B,EAAYC,EAAMC,EAAOC,GAChC,MAAoB,mBAATF,EACFD,EAAYC,EAAKC,EAAOC,GAAUD,EAAOC,GAE9CF,GAAsB,IAAdA,EAAKG,KACRJ,EAAYC,EAAKI,KAAKC,KAAKL,EAAKI,MAAOH,EAAOC,GAEhDF,mBA6DF,SAAwBK,EAAMJ,EAAOC,GAC1C,OA3DK,SAAkBG,EAAMJ,EAAOC,GACpC,IAAMT,EAAQ,CACZ,CACEI,WAAY,EACZL,SAAU,CAACa,GACXX,OAAQ,KAGRY,GAAM,EAEV,OAAO,SAACC,GACN,GAAID,EACF,OAAO,KAKT,IAFA,IAAInC,EAAM,GAEHA,EAAIE,OAASkC,GAAO,CACzB,GAAqB,IAAjBd,EAAMpB,OAAc,CACtBiC,GAAM,EACN,MAGF,IAAME,EAAQf,EAAMA,EAAMpB,OAAS,GAEnC,GAAImC,EAAMX,YAAcW,EAAMhB,SAASnB,OACrCF,GAAOqC,EAAMd,OACbD,EAAMgB,UACD,CACL,IAAMT,EAAOD,EAAYS,EAAMhB,SAASgB,EAAMX,cAAeI,EAAOC,GAExD,MAARF,GAAgC,kBAATA,IACrBhD,EAAQgD,GACVP,EAAMG,KAAK,CACTC,WAAY,EACZL,SAAUQ,EACVN,OAAQ,KAEa,IAAdM,EAAKG,KACdhC,GAAOP,EAAWoC,EAAKU,KAAOV,EAAKrB,MAEnCR,GADyB,iBAAT6B,EACTV,EACLU,EAAKU,KAAOV,EAAKW,SACjBX,EAAKT,OAASS,EAAKY,WACnBZ,EAAKR,SACLC,GAGK7B,EAAWoC,KAM1B,OAAO7B,GAKF0C,CAASR,EAAMJ,EAAOC,EAAtBW,CAA+BC"} -------------------------------------------------------------------------------- /dist/hyperapp-render.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"hyperapp-render.js","sources":["src/index.js"],"sourcesContent":["const { isArray } = Array\nconst { hasOwnProperty } = Object.prototype\nconst styleNameCache = new Map()\nconst uppercasePattern = /[A-Z]/g\nconst msPattern = /^ms-/\n\n// https://www.w3.org/International/questions/qa-escapes#use\nconst escapeRegExp = /[\"&'<>]/\n\n// https://www.w3.org/TR/html/syntax.html#void-elements\nconst voidElements = new Set([\n 'area',\n 'base',\n 'br',\n 'col',\n 'embed',\n 'hr',\n 'img',\n 'input',\n 'link',\n 'meta',\n 'param',\n 'source',\n 'track',\n 'wbr',\n])\n\n// credits to https://github.com/component/escape-html\nexport function escapeHtml(value) {\n if (value == null) return ''\n const str = '' + value\n if (typeof value === 'number') {\n // better performance for safe values\n return str\n }\n\n const match = escapeRegExp.exec(str)\n if (!match) {\n return str\n }\n\n let { index } = match\n let lastIndex = 0\n let out = ''\n\n for (let escape = ''; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"'\n break\n case 38: // &\n escape = '&'\n break\n case 39: // '\n escape = ''' // shorter than \"'\" and \"'\" plus supports HTML4\n break\n case 60: // <\n escape = '<'\n break\n case 62: // >\n escape = '>'\n break\n default:\n continue\n }\n\n if (lastIndex !== index) {\n out += str.substring(lastIndex, index)\n }\n\n lastIndex = index + 1\n out += escape\n }\n\n return lastIndex !== index ? out + str.substring(lastIndex, index) : out\n}\n\n// credits to https://github.com/jorgebucaran/classcat\nexport function concatClassNames(value) {\n if (typeof value === 'string' || typeof value === 'number') {\n return value || ''\n }\n\n let out = ''\n let delimiter = ''\n\n if (isArray(value)) {\n for (let i = 0; i < value.length; i++) {\n const name = concatClassNames(value[i])\n if (name !== '') {\n out += delimiter + name\n delimiter = ' '\n }\n }\n } else {\n for (const name in value) {\n if (hasOwnProperty.call(value, name) && value[name]) {\n out += delimiter + name\n delimiter = ' '\n }\n }\n }\n\n return out\n}\n\n// \"backgroundColor\" => \"background-color\"\n// \"MozTransition\" => \"-moz-transition\"\n// \"msTransition\" => \"-ms-transition\"\nfunction hyphenateStyleName(styleName) {\n if (!styleNameCache.has(styleName)) {\n const name = styleName.replace(uppercasePattern, '-$&').toLowerCase().replace(msPattern, '-ms-')\n\n // returns 'undefined' instead of the 'Map' object in IE11\n styleNameCache.set(styleName, name)\n }\n return styleNameCache.get(styleName)\n}\n\nexport function stringifyStyles(style) {\n let out = ''\n let delimiter = ''\n\n for (const name in style) {\n if (hasOwnProperty.call(style, name)) {\n const value = style[name]\n\n if (value != null) {\n if (name === 'cssText') {\n out += delimiter + value\n } else {\n out += delimiter + hyphenateStyleName(name) + ':' + value\n }\n delimiter = ';'\n }\n }\n }\n\n return out\n}\n\n// https://www.w3.org/TR/html51/syntax.html#serializing-html-fragments\nfunction renderFragment(name, props, children, stack) {\n let out = ''\n let footer = ''\n\n if (name) {\n out += '<' + name\n\n for (let prop in props) {\n if (hasOwnProperty.call(props, prop)) {\n let value = props[prop]\n\n if (\n value != null &&\n prop !== 'key' &&\n prop !== 'innerHTML' &&\n prop !== '__source' && // babel-plugin-transform-react-jsx-source\n !(prop[0] === 'o' && prop[1] === 'n')\n ) {\n if (prop === 'class' || prop === 'className') {\n prop = 'class'\n value = concatClassNames(value) || false\n } else if (prop === 'style' && typeof value === 'object') {\n value = stringifyStyles(value) || false\n }\n\n if (value !== false) {\n out += ' ' + prop\n\n if (value !== true) {\n out += '=\"' + escapeHtml(value) + '\"'\n }\n }\n }\n }\n }\n\n if (voidElements.has(name)) {\n out += '/>'\n } else {\n out += '>'\n footer = ''\n }\n }\n\n if (children.length > 0) {\n stack.push({\n childIndex: 0,\n children,\n footer,\n })\n } else {\n const { innerHTML } = props\n\n if (innerHTML != null) {\n out += innerHTML\n }\n\n out += footer\n }\n\n return out\n}\n\nfunction resolveNode(node, state, actions) {\n if (typeof node === 'function') {\n return resolveNode(node(state, actions), state, actions)\n }\n if (node && node.type === 2) {\n return resolveNode(node.lazy.view(node.lazy), state, actions)\n }\n return node\n}\n\nexport function renderer(view, state, actions) {\n const stack = [\n {\n childIndex: 0,\n children: [view],\n footer: '',\n },\n ]\n let end = false\n\n return (bytes) => {\n if (end) {\n return null\n }\n\n let out = ''\n\n while (out.length < bytes) {\n if (stack.length === 0) {\n end = true\n break\n }\n\n const frame = stack[stack.length - 1]\n\n if (frame.childIndex >= frame.children.length) {\n out += frame.footer\n stack.pop()\n } else {\n const node = resolveNode(frame.children[frame.childIndex++], state, actions)\n\n if (node != null && typeof node !== 'boolean') {\n if (isArray(node)) {\n stack.push({\n childIndex: 0,\n children: node,\n footer: '',\n })\n } else if (node.type === 3) {\n out += escapeHtml(node.tag || node.name)\n } else if (typeof node === 'object') {\n out += renderFragment(\n node.tag || node.nodeName,\n node.props || node.attributes,\n node.children,\n stack,\n )\n } else {\n out += escapeHtml(node)\n }\n }\n }\n }\n\n return out\n }\n}\n\nexport function renderToString(view, state, actions) {\n return renderer(view, state, actions)(Infinity)\n}\n"],"names":["isArray","Array","hasOwnProperty","Object","prototype","styleNameCache","Map","uppercasePattern","msPattern","escapeRegExp","voidElements","Set","escapeHtml","value","str","match","exec","index","lastIndex","out","escape","length","charCodeAt","substring","concatClassNames","delimiter","i","name","call","hyphenateStyleName","styleName","has","replace","toLowerCase","set","get","stringifyStyles","style","renderFragment","props","children","stack","footer","prop","push","childIndex","innerHTML","resolveNode","node","state","actions","type","lazy","view","renderer","end","bytes","frame","pop","tag","nodeName","attributes","renderToString","Infinity"],"mappings":"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"} -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | /** @jsx h */ 2 | import { h } from 'hyperapp' 3 | import { renderer, renderToString } from '../src/index' 4 | 5 | describe('escape', () => { 6 | it('should escape ampersand when passed as text content', () => { 7 | const html = renderToString(
{'&'}
) 8 | expect(html).toBe('
&
') 9 | }) 10 | 11 | it('should escape double quote when passed as text content', () => { 12 | const html = renderToString(
{'"'}
) 13 | expect(html).toBe('
"
') 14 | }) 15 | 16 | it('should escape single quote when passed as text content', () => { 17 | const html = renderToString(
{"'"}
) 18 | expect(html).toBe('
'
') 19 | }) 20 | 21 | it('should escape greater than entity when passed as text content', () => { 22 | const html = renderToString(
{'>'}
) 23 | expect(html).toBe('
>
') 24 | }) 25 | 26 | it('should escape lower than entity when passed as text content', () => { 27 | const html = renderToString(
{'<'}
) 28 | expect(html).toBe('
<
') 29 | }) 30 | 31 | it('should escape script tag when passed as text content', () => { 32 | const html = renderToString(
{''}
) 33 | expect(html).toBe('
<script type='' src=""></script>
') 34 | }) 35 | 36 | it('should escape ampersand inside attributes', () => { 37 | const html = renderToString(
) 38 | expect(html).toBe('
') 39 | }) 40 | 41 | it('should escape double quote inside attributes', () => { 42 | const html = renderToString(
) 43 | expect(html).toBe('
') 44 | }) 45 | 46 | it('should escape single quote inside attributes', () => { 47 | const html = renderToString(
) 48 | expect(html).toBe('
') 49 | }) 50 | 51 | it('should escape greater than entity inside attributes', () => { 52 | const html = renderToString(
) 53 | expect(html).toBe('
') 54 | }) 55 | 56 | it('should escape lower than entity inside attributes', () => { 57 | const html = renderToString(
) 58 | expect(html).toBe('
') 59 | }) 60 | 61 | it('should escape script tag inside attributes', () => { 62 | const html = renderToString(
'} />) 63 | expect(html).toBe( 64 | '
', 65 | ) 66 | }) 67 | 68 | it('should escape url', () => { 69 | const html = renderToString(ref) 70 | expect(html).toBe('ref') 71 | }) 72 | 73 | it('should not escape innerHTML', () => { 74 | const html = renderToString(
'} />) 75 | expect(html).toBe('
') 76 | }) 77 | }) 78 | 79 | describe('class', () => { 80 | it('should render a string', () => { 81 | const html = renderToString(
) 82 | expect(html).toBe('
') 83 | }) 84 | 85 | it('should not render an empty string', () => { 86 | const html = renderToString(
) 87 | expect(html).toBe('
') 88 | }) 89 | 90 | it('should not render an empty object', () => { 91 | const html = renderToString(
) 92 | expect(html).toBe('
') 93 | }) 94 | 95 | it('should not render an empty array', () => { 96 | const html = renderToString(
) 97 | expect(html).toBe('
') 98 | }) 99 | 100 | it('should not render falsy values', () => { 101 | const html = renderToString(
) 102 | expect(html).toBe('
') 103 | }) 104 | 105 | it('should render an array of values', () => { 106 | const html = renderToString(
) 107 | expect(html).toBe('
') 108 | }) 109 | 110 | it('should support nested arrays', () => { 111 | const html = renderToString(
) 112 | expect(html).toBe('
') 113 | }) 114 | 115 | it('should render an object of class names', () => { 116 | const className = { 117 | foo: true, 118 | bar: true, 119 | quux: false, 120 | baz: true, 121 | } 122 | const html = renderToString(
) 123 | expect(html).toBe('
') 124 | }) 125 | 126 | it('should render a mix of array of object values', () => { 127 | const className = [ 128 | 'foo', 129 | 'foo-bar', 130 | { 131 | 'foo-baz': true, 132 | }, 133 | ['fum', 'bam', 'pow'], 134 | ] 135 | const html = renderToString(
) 136 | expect(html).toBe('
') 137 | }) 138 | 139 | it('should render className as class', () => { 140 | const html = renderToString(
) 141 | expect(html).toBe('
') 142 | }) 143 | 144 | it('should not throw an exception', () => { 145 | const className = Object.create({ hasOwnProperty: null }) 146 | const html = renderToString(
) 147 | expect(html).toBe('
') 148 | }) 149 | }) 150 | 151 | describe('styles', () => { 152 | it('should generate markup for style attribute', () => { 153 | const styles = { 154 | left: 0, 155 | margin: 16, 156 | opacity: 0.5, 157 | padding: '4px', 158 | } 159 | const html = renderToString(
) 160 | expect(html).toBe('
') 161 | }) 162 | 163 | it('should not trim values', () => { 164 | const styles = { 165 | left: '16 ', 166 | opacity: 0.5, 167 | right: ' 4 ', 168 | } 169 | const html = renderToString(
) 170 | expect(html).toBe('
') 171 | }) 172 | 173 | it('should create vendor-prefixed markup correctly', () => { 174 | const styles = { 175 | WebkitTransition: 'none', 176 | MozTransition: 'none', 177 | msTransition: 'none', 178 | } 179 | const html = renderToString(
) 180 | expect(html).toBe( 181 | '
', 182 | ) 183 | }) 184 | 185 | it('should render style attribute when styles exist', () => { 186 | const styles = { 187 | backgroundColor: '#000', 188 | display: 'none', 189 | } 190 | const html = renderToString(
) 191 | expect(html).toBe('
') 192 | }) 193 | 194 | it('should not render style attribute when no styles exist', () => { 195 | const styles = { 196 | backgroundColor: null, 197 | display: undefined, 198 | } 199 | const html = renderToString(
) 200 | expect(html).toBe('
') 201 | }) 202 | 203 | it('should render hyphenated style names', () => { 204 | const styles = { 205 | 'background-color': 'Orange', 206 | '-webkit-transform': 'translateX(0)', 207 | } 208 | const html = renderToString(
) 209 | expect(html).toBe('
') 210 | }) 211 | 212 | it('should render custom properties', () => { 213 | const styles = { 214 | '--foo': 'red', 215 | color: 'var(--foo)', 216 | } 217 | const html = renderToString(
) 218 | expect(html).toBe('
') 219 | }) 220 | 221 | it('should render invalid values', () => { 222 | const styles = { 223 | height: NaN, 224 | fontSize: 1 / 0, 225 | backgroundImage: 'url(foo;bar)', 226 | } 227 | const html = renderToString(
) 228 | expect(html).toBe( 229 | '
', 230 | ) 231 | }) 232 | 233 | it('should not add units', () => { 234 | const styles = { 235 | '--foo': 5, 236 | flex: 0, 237 | opacity: 0.5, 238 | } 239 | const html = renderToString(
) 240 | expect(html).toBe('
') 241 | }) 242 | 243 | it('should render cssText', () => { 244 | const styles = { 245 | top: 0, 246 | cssText: 'color:blue;font-size:10px', 247 | bottom: 0, 248 | } 249 | const html = renderToString(
) 250 | expect(html).toBe('
') 251 | }) 252 | 253 | it('should render non-object style', () => { 254 | const html = renderToString(
) 255 | expect(html).toBe('
') 256 | }) 257 | 258 | it('should not throw an exception', () => { 259 | const style = Object.create({ hasOwnProperty: null }) 260 | const html = renderToString(
) 261 | expect(html).toBe('
') 262 | }) 263 | }) 264 | 265 | describe('attributes', () => { 266 | it('should render attribute', () => { 267 | const html = renderToString(
) 268 | expect(html).toBe('
') 269 | }) 270 | it('should render boolean attribute', () => { 271 | const html = renderToString() 272 | expect(html).toBe('') 273 | }) 274 | 275 | it('should render attribute with empty string value', () => { 276 | const html = renderToString(
) 277 | expect(html).toBe('
') 278 | }) 279 | 280 | it('should render attribute with number value', () => { 281 | const html = renderToString(
) 282 | expect(html).toBe('
') 283 | }) 284 | 285 | it('should render attribute with NaN value', () => { 286 | const html = renderToString(
) 287 | expect(html).toBe('
') 288 | }) 289 | 290 | it('should render attribute with Infinity value', () => { 291 | const html = renderToString(
) 292 | expect(html).toBe('
') 293 | }) 294 | 295 | it('should render attribute with array value', () => { 296 | const html = renderToString(
) 297 | expect(html).toBe('
') 298 | }) 299 | 300 | it('should render attribute with object value', () => { 301 | const sampleObject = { 302 | toString() { 303 | return 'sample' 304 | }, 305 | } 306 | const html = renderToString(
) 307 | expect(html).toBe('
') 308 | }) 309 | 310 | it('should not render attribute with falsy value', () => { 311 | const html = renderToString(
) 312 | expect(html).toBe('
') 313 | }) 314 | 315 | it('should not render attribute with null value', () => { 316 | const html = renderToString(
) 317 | expect(html).toBe('
') 318 | }) 319 | 320 | it('should not render attribute with undefined value', () => { 321 | const html = renderToString(
) 322 | expect(html).toBe('
') 323 | }) 324 | 325 | it('should not render key attribute', () => { 326 | const html = renderToString(
) 327 | expect(html).toBe('
') 328 | }) 329 | 330 | it('should not render innerHTML attribute', () => { 331 | const html = renderToString(
) 332 | expect(html).toBe('
') 333 | }) 334 | 335 | it('should prefer child nodes over innerHTML attribute', () => { 336 | const html = renderToString(
bar
) 337 | expect(html).toBe('
bar
') 338 | }) 339 | 340 | it('should not render __source attribute', () => { 341 | const source = { fileName: 'this/file.js', lineNumber: 10 } 342 | const html = renderToString(
) 343 | expect(html).toBe('
') 344 | }) 345 | 346 | it('should not render event attribute', () => { 347 | const html = renderToString(') 349 | }) 350 | 351 | it('should not throw an exception', () => { 352 | const attributes = Object.create({ hasOwnProperty: null }) 353 | const html = renderToString(
) 354 | expect(html).toBe('
') 355 | }) 356 | }) 357 | 358 | describe('renderer(view, state, actions)(bytes)', () => { 359 | it('should create a reader function', () => { 360 | const read = renderer(
) 361 | expect(read).toBeInstanceOf(Function) 362 | expect(read(0)).toBe('') 363 | }) 364 | 365 | it('should render chunks', () => { 366 | const read = renderer( 367 |
368 | 369 |
, 370 | ) 371 | expect(read(1)).toBe('
') 372 | expect(read(1)).toBe('') 373 | expect(read(1)).toBe('
') 374 | }) 375 | 376 | it('should return null at the end', () => { 377 | const read = renderer(
) 378 | expect(read(Infinity)).toBe('
') 379 | expect(read(Infinity)).toBe(null) 380 | }) 381 | }) 382 | 383 | describe('renderToString(view, state, actions)', () => { 384 | it('should render simple markup', () => { 385 | const html = renderToString(
hello world
) 386 | expect(html).toBe('
hello world
') 387 | }) 388 | 389 | it('should render closing tags for empty elements', () => { 390 | const html = renderToString(
) 391 | expect(html).toBe('
') 392 | }) 393 | 394 | it('should render markup for self-closing tags', () => { 395 | const html = renderToString() 396 | expect(html).toBe('') 397 | }) 398 | 399 | it('should render empty markup for components which return null', () => { 400 | function NullComponent() { 401 | return null 402 | } 403 | const html = renderToString() 404 | expect(html).toBe('') 405 | }) 406 | 407 | it('should render composite components', () => { 408 | function Child({ name }) { 409 | return

Hello {name}

410 | } 411 | function Parent() { 412 | return ( 413 |
414 | 415 |
416 | ) 417 | } 418 | const html = renderToString() 419 | expect(html).toBe('

Hello World

') 420 | }) 421 | 422 | it('should render web components', () => { 423 | const html = renderToString() 424 | expect(html).toBe('') 425 | }) 426 | 427 | it('should render undefined, null and booleans as an empty string', () => { 428 | const html = renderToString({ 429 | nodeName: 'div', 430 | attributes: {}, 431 | children: [undefined, null, false, true, 0], 432 | }) 433 | expect(html).toBe('
0
') 434 | }) 435 | 436 | it('should render content of JSX fragment', () => { 437 | const Fragment = '' 438 | const html = renderToString( 439 | 440 | 441 | 442 | , 443 | ) 444 | expect(html).toBe('') 445 | }) 446 | 447 | it('should render raw html without extra markup', () => { 448 | const Fragment = '' 449 | // eslint-disable-next-line react/jsx-no-useless-fragment 450 | const html = renderToString() 451 | expect(html).toBe(`alert('hello world')`) 452 | }) 453 | 454 | it('should render an array of elements', () => { 455 | const html = renderToString([, ]) 456 | expect(html).toBe('') 457 | }) 458 | 459 | it('should support Hyperapp v2.0.9', () => { 460 | const VNode = { 461 | tag: 'div', 462 | props: {}, 463 | key: null, 464 | children: [ 465 | { 466 | tag: 'foo bar baz', 467 | props: {}, 468 | key: null, 469 | children: [], 470 | type: 3, 471 | node: null, 472 | }, 473 | ], 474 | type: 1, 475 | node: null, 476 | } 477 | const html = renderToString(VNode) 478 | expect(html).toBe('
foo bar baz
') 479 | }) 480 | 481 | it('should correctly render empty text nodes', () => { 482 | const VNode = { 483 | tag: 'div', 484 | props: {}, 485 | key: null, 486 | children: [ 487 | { 488 | tag: '', 489 | props: {}, 490 | key: null, 491 | children: [], 492 | type: 3, 493 | node: null, 494 | }, 495 | ], 496 | type: 1, 497 | node: null, 498 | } 499 | const html = renderToString(VNode) 500 | expect(html).toBe('
') 501 | }) 502 | 503 | it('should support Hyperapp V2 lazy nodes', () => { 504 | const VNode = { 505 | lazy: { 506 | view: ({ name }) =>
{name}
, 507 | name: 'foo', 508 | }, 509 | type: 2, 510 | } 511 | const html = renderToString(VNode) 512 | expect(html).toBe('
foo
') 513 | }) 514 | 515 | it('should support Hyperapp V2 nested lazy nodes', () => { 516 | const VNode = { 517 | lazy: { 518 | view: () => ({ 519 | lazy: { 520 | view: ({ name }) =>
{name}
, 521 | name: 'foo', 522 | }, 523 | type: 2, 524 | }), 525 | }, 526 | type: 2, 527 | } 528 | const html = renderToString(VNode) 529 | expect(html).toBe('
foo
') 530 | }) 531 | 532 | it('should render counter', () => { 533 | const testState = { count: 100 } 534 | const testActions = { 535 | up: () => (state) => ({ count: state.count + 1 }), 536 | getState: () => (state) => state, 537 | } 538 | const testView = (state, actions) => { 539 | expect(state).toBe(testState) 540 | expect(actions).toBe(testActions) 541 | return ( 542 | 545 | ) 546 | } 547 | const html = renderToString(testView, testState, testActions) 548 | expect(html).toBe('') 549 | }) 550 | }) 551 | --------------------------------------------------------------------------------