├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitattributes
├── .gitignore
├── .npmignore
├── .travis.yml
├── license.md
├── package.json
├── readme.md
├── src
└── index.js
└── tests
└── test.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "dustinspecker/esnext",
3 | plugins: [
4 | "no-use-extend-native"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | lib/
3 | node_modules/
4 | .nyc_output
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: node_js
3 | node_js:
4 | - '5'
5 | - '4'
6 | notifications:
7 | email: false
8 | before_script:
9 | - npm prune
10 | script:
11 | - npm run test
12 | after_success:
13 | - 'curl -Lo travis_after_all.py https://git.io/travis_after_all'
14 | - python travis_after_all.py
15 | - 'export $(cat .to_export_back) &> /dev/null'
16 | - npm run-script coveralls
17 | - npm run semantic-release
18 | branches:
19 | except:
20 | - "/^v\\d+\\.\\d+\\.\\d+$/"
21 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Dustin Specker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dscript",
3 | "version": "0.0.0-semantic-release",
4 | "description": "Framework agnostic hyperscript",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "compile": "babel src --out-dir lib",
8 | "coveralls": "nyc report --reporter=text-lcov | coveralls",
9 | "lint": "eslint ./ --ignore-pattern node_modules/ --ignore-pattern lib",
10 | "semantic-release": "semantic-release pre && npm publish && semantic-release post",
11 | "test": "npm run lint && npm run compile && nyc ava"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/dustinspecker/dscript.git"
16 | },
17 | "keywords": [
18 | "dscript",
19 | "hyperscript",
20 | "virtual",
21 | "deku",
22 | "react"
23 | ],
24 | "author": {
25 | "name": "Dustin Specker",
26 | "email": "DustinSpecker@DustinSpecker.com",
27 | "url": "https://github.com/dustinspecker"
28 | },
29 | "license": "MIT",
30 | "files": [
31 | "lib"
32 | ],
33 | "dependencies": {
34 | "html-tags": "^1.1.1",
35 | "object-assign": "^4.0.1",
36 | "parse-css-class-id-selector": "^1.0.0"
37 | },
38 | "devDependencies": {
39 | "ava": "^0.16.0",
40 | "babel-cli": "^6.4.0",
41 | "babel-preset-es2015": "^6.3.13",
42 | "babel-register": "^6.4.3",
43 | "coveralls": "^2.11.6",
44 | "cz-conventional-changelog": "^1.1.5",
45 | "deku": "^2.0.0-rc16",
46 | "eslint": "^3.10.0",
47 | "eslint-config-dustinspecker": "^1.0.0",
48 | "eslint-path-formatter": "^0.1.1",
49 | "eslint-plugin-no-use-extend-native": "^0.3.1",
50 | "eslint-plugin-xo": "^1.0.0",
51 | "nyc": "^8.4.0",
52 | "react": "^15.0.0",
53 | "semantic-release": "^4.3.5"
54 | },
55 | "ava": {
56 | "files": [
57 | "tests/test.js"
58 | ],
59 | "require": [
60 | "babel-register"
61 | ]
62 | },
63 | "nyc": {
64 | "exclude": [
65 | "node_modules",
66 | "tests"
67 | ]
68 | },
69 | "config": {
70 | "commitizen": {
71 | "path": "./node_modules/cz-conventional-changelog"
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # dscript
2 | [](https://badge.fury.io/js/dscript)
3 | [](https://travis-ci.org/dustinspecker/dscript)
4 | [](https://coveralls.io/r/dustinspecker/dscript?branch=master)
5 |
6 | [](https://codeclimate.com/github/dustinspecker/dscript)
7 | [](https://david-dm.org/dustinspecker/dscript/#info=dependencies&view=table)
8 | [](https://david-dm.org/dustinspecker/dscript/#info=devDependencies&view=table)
9 |
10 | [](http://makeapullrequest.com)
11 | [](http://commitizen.github.io/cz-cli/)
12 | [](https://github.com/semantic-release/semantic-release)
13 |
14 | > Framework agnostic hyperscript
15 |
16 | > Should work with any JSX pragma that works out of the box with Babel's JSX implementation or a function that accepts an HTML tag or component, attributes object, and children list.
17 |
18 | ## Install
19 | ```
20 | npm install --save dscript
21 | ```
22 |
23 | **Note: Webpack users will need to setup a [json-loader](https://github.com/webpack/json-loader) as dscript relies on [html-tags](https://github.com/sindresorhus/html-tags), which uses a [JSON file](https://github.com/sindresorhus/html-tags/blob/master/html-tags.json)**
24 |
25 | ## General Usage
26 | ```javascript
27 | import dscript from 'dscript'
28 | import {element} from 'deku'
29 |
30 | const {div, li, ul} = dscript(element)
31 |
32 | const handleClick = () => alert('hi!')
33 |
34 | export default ({props}) =>
35 | div('.list-container', {onClick: handleClick}, [
36 | ul(
37 | props.items.map(item =>
38 | li(item.name)
39 | )
40 | )
41 | ])
42 | ```
43 |
44 | ## Usage with React
45 | **It is recommended to use [dscript-react](https://github.com/dustinspecker/dscript-react) to remove dscript boilerplate.**
46 |
47 | Take the following:
48 | ```javascript
49 | import React from 'react'
50 |
51 | export default props =>
52 |
53 | {props.items.map(item =>
54 | - {item.name}
55 | )}
56 |
57 | ```
58 | or:
59 | ```javascript
60 | import {createElement} from 'react'
61 |
62 | export default props =>
63 | createElement('ul', {},
64 | props.items.map(item =>
65 | createElement('li', {}, [
66 | item.name
67 | ])
68 | )
69 | )
70 | ```
71 |
72 | and instead write:
73 |
74 | ```javascript
75 | import {createElement} from 'react'
76 | import dscript from 'dscript'
77 |
78 | const {li, ul} = dscript(createElement)
79 |
80 | export default props =>
81 | ul(
82 | props.items.map(item =>
83 | li(item.name)
84 | )
85 | )
86 |
87 | ```
88 |
89 | ## Usage with Deku
90 | **It is recommended to use [dscript-deku](https://github.com/dustinspecker/dscript-deku) to remove dscript boilerplate.**
91 |
92 | Take the following:
93 | ```javascript
94 | import {element} from 'deku'
95 |
96 | export default ({props}) =>
97 |
98 | {props.items.map(item =>
99 | - {item.name}
100 | }
101 |
102 | ```
103 |
104 | or:
105 |
106 | ```javascript
107 | import {element} from 'deku'
108 |
109 | export default ({props}) =>
110 | element('ul', {},
111 | props.items.map(item =>
112 | element('li', {}, [
113 | item.name
114 | ])
115 | )
116 | )
117 | ```
118 |
119 | and instead write:
120 | ```javascript
121 | import dscript from 'dscript'
122 | import {element} from 'deku'
123 |
124 | const {li, ul} = dscript(element)
125 |
126 | export default ({props}) =>
127 | ul(
128 | props.items.map(item =>
129 | li(item.name)
130 | )
131 | )
132 | ```
133 |
134 | ## Usage with Custom Components
135 | Custom components example is shown using React, but works with any framework that works with dscript's basic functionality.
136 |
137 | ```javascript
138 | import dscript from 'dscript'
139 | import {createElement} from 'react'
140 |
141 | import customComponent from './custom-component'
142 |
143 | const creator = dscript(createElement)
144 |
145 | const {div, li, ul} = creator
146 | const customComponentCreator = creator(customComponent)
147 |
148 | const handleClick = () => alert('hi!')
149 |
150 | export default props =>
151 | div('.list-container', {onClick: handleClick}, [
152 | customComponentCreator({total: props.total}),
153 | ul(
154 | props.items.map(item =>
155 | li(item.name)
156 | )
157 | )
158 | ])
159 | ```
160 |
161 | ## API
162 | ### dscript(createElement)
163 | Returns an object with properties consisting of HTML tags with values being [creator functions](#creator-functions).
164 |
165 | #### createElement
166 | type: `function`
167 |
168 | A function to use to create the Virtual DOM. For example, React's `createElement` or Deku's `element`.
169 |
170 | ### dscript(createElement)(customComponent)
171 |
172 | Returns a [creator function](#creator-functions) to be used in dscript.
173 |
174 | For example:
175 |
176 | ```javascript
177 | import {createElement} from 'react'
178 | import customComponent from './lib/custom-react-component/'
179 | import dscript from 'dscript'
180 |
181 | const creator = dscript(createElement)
182 |
183 | const {div} = creator
184 | const custom = creator(customComponent)
185 |
186 | export default div([custom()])
187 | ```
188 |
189 | #### createElement
190 |
191 | Same as above
192 |
193 | #### customComponent
194 |
195 | type: `any`
196 |
197 | Should be a valid component for the `createElement` function.
198 |
199 |
200 | ### Creator Functions
201 | `creatorFunction([cssClassesAndOrIdSelector,] [attributes,] [children])`
202 |
203 | A function that returns a virtual DOM node created with `createElement`.
204 |
205 | #### cssClassesAndOrIdSelector
206 | type: `string`
207 |
208 | default: `null`
209 |
210 | A convenience to add class names and an id to a virtual DOM node. **Note: The provided selector will override `attribute`'s class and id.**
211 |
212 | ### attributes
213 | type: `object`
214 |
215 | default: `{}`
216 |
217 | An object that will be passed as the attributes to the virutal DOM node.
218 |
219 | ### children
220 | type: `...Elements`
221 |
222 | default: `[]`
223 |
224 | The list of children passed to the created virtual DOM node.
225 |
226 | ## LICENSE
227 | MIT © [Dustin Specker](https://github.com/dustinspecker)
228 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | import htmlTags from 'html-tags'
3 | import objectAssign from 'object-assign'
4 | import parseCssClassIdSelector from 'parse-css-class-id-selector'
5 |
6 | /**
7 | * Retrieve class names and id from a selector
8 | * @param {String} selector - CSS class and/or id selector to parse
9 | * @return {Object} - object with class and id properties, if found in selector
10 | * {String} class
11 | * {String} id
12 | */
13 | const getClassesAndId = selector => {
14 | const {classNames, id} = parseCssClassIdSelector(selector)
15 |
16 | const classesAndId = {}
17 |
18 | if (classNames.length) {
19 | classesAndId.class = classNames.join(' ')
20 | }
21 |
22 | if (id) {
23 | classesAndId.id = id
24 | }
25 |
26 | return classesAndId
27 | }
28 |
29 | /**
30 | * Return a creator function with properties having HTML tag names with values
31 | * being creator functions for the respective HTML element.
32 | *
33 | * The stand-alone returned creator function is meant to be used with custom
34 | * components
35 | *
36 | * @param {Function} createElement - function producing virtual nodes - typically the function used for JSX
37 | * @return {Function} - explained above
38 | */
39 | module.exports = createElement => {
40 | /* eslint-disable complexity */
41 | if (typeof createElement !== 'function') {
42 | throw new TypeError('Expected createElement to be a function')
43 | }
44 |
45 | const creator = tagOrComponent => (classesAndId, attrs, ...children) => {
46 | let attrsToPass = attrs || {}
47 | , childrenToPass = children || []
48 |
49 | if (Array.isArray(attrsToPass)) {
50 | // case: div('.hello', ['hi'])
51 | childrenToPass = attrsToPass
52 | attrsToPass = {}
53 | } else if (typeof attrsToPass !== 'object') {
54 | // case: div('.hello', 7)
55 | childrenToPass = [attrsToPass]
56 | attrsToPass = {}
57 | }
58 |
59 | if (Array.isArray(classesAndId)) {
60 | // case: div(['hi'])
61 | childrenToPass = classesAndId
62 | } else if (typeof classesAndId === 'object') {
63 | // case: div({tabindex: 4})
64 | attrsToPass = classesAndId
65 | }
66 |
67 | if (typeof classesAndId === 'string' && (classesAndId.indexOf('.') === 0 || classesAndId.indexOf('#') === 0)) {
68 | // case: div('.hello')
69 | objectAssign(attrsToPass, getClassesAndId(classesAndId))
70 | } else if (classesAndId !== undefined && typeof classesAndId !== 'object') {
71 | // case: div(2342374)
72 | childrenToPass = [classesAndId]
73 | }
74 |
75 | return createElement(tagOrComponent, attrsToPass, ...childrenToPass)
76 | }
77 |
78 | // attach each HTML creator function to a creator function for custom components
79 | return htmlTags.reduce((acc, tag) => {
80 | acc[tag] = creator(tag)
81 |
82 | return acc
83 | }, creator)
84 | }
85 |
--------------------------------------------------------------------------------
/tests/test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | import {createElement} from 'react'
3 | import {element} from 'deku'
4 | import test from 'ava'
5 |
6 | import dscript from '../lib/'
7 |
8 | test('throws when createElement is not a function', t => {
9 | t.throws(dscript, TypeError)
10 | t.throws(dscript, /Expected createElement to be a function/)
11 | })
12 |
13 | test('returns an object with html tag names that execute createElement', t => {
14 | let aCalled = false
15 | , buttonCalled = false
16 |
17 | const {a, button} = dscript(tagName => {
18 | if (tagName === 'a') {
19 | aCalled = true
20 | }
21 |
22 | if (tagName === 'button') {
23 | buttonCalled = true
24 | }
25 | })
26 |
27 | a()
28 | button()
29 |
30 | t.truthy(aCalled)
31 | t.truthy(buttonCalled)
32 | })
33 |
34 | test('dscript fns pass attributes object', t => {
35 | let divCalled = false
36 | , pCalled = false
37 |
38 | const divAttrs = {
39 | id: 'hello'
40 | }
41 |
42 | const pAttrs = {
43 | class: 'goodbye'
44 | }
45 |
46 | const {div, p} = dscript((tagName, attrs, ...children) => {
47 | if (tagName === 'div' && attrs === divAttrs) {
48 | t.deepEqual(children, [])
49 | divCalled = true
50 | }
51 |
52 | if (tagName === 'p' && attrs === pAttrs) {
53 | t.deepEqual(children, [])
54 | pCalled = true
55 | }
56 | })
57 |
58 | div(divAttrs)
59 | p(pAttrs)
60 |
61 | t.truthy(divCalled)
62 | t.truthy(pCalled)
63 | })
64 |
65 | test('dscript fns pass empty object by default for attributes', t => {
66 | let inputCalled = false
67 |
68 | const {input} = dscript((tagName, attrs) => {
69 | if (tagName === 'input') {
70 | t.deepEqual(attrs, {})
71 | inputCalled = true
72 | }
73 | })
74 |
75 | input()
76 |
77 | t.truthy(inputCalled)
78 | })
79 |
80 | test('dscript fns pass children array', t => {
81 | let bCalled = false
82 | , videoCalled = false
83 |
84 | const bChildren = ['yo']
85 | const videoChildren = ['hello']
86 |
87 | const {b, video} = dscript((tagName, attrs, ...children) => {
88 | if (tagName === 'b') {
89 | t.deepEqual(children, bChildren)
90 | bCalled = true
91 | }
92 |
93 | if (tagName === 'video') {
94 | t.deepEqual(children, videoChildren)
95 | videoCalled = true
96 | }
97 | })
98 |
99 | b({}, bChildren)
100 | video({}, videoChildren)
101 |
102 | t.truthy(bCalled)
103 | t.truthy(videoCalled)
104 | })
105 |
106 | test('dscript fns pass empty children array by default', t => {
107 | let spanCalled = false
108 |
109 | const {span} = dscript((tagName, attrs, ...children) => {
110 | if (tagName === 'span') {
111 | t.deepEqual(children, [])
112 | spanCalled = true
113 | }
114 | })
115 |
116 | span()
117 |
118 | t.truthy(spanCalled)
119 | })
120 |
121 | test('it passes single child when class and attrs are provided', t => {
122 | let spanCalled = false
123 |
124 | const {span} = dscript((tagName, attrs, ...children) => {
125 | if (tagName === 'span') {
126 | t.deepEqual(children, [789])
127 | spanCalled = true
128 | }
129 | })
130 |
131 | span('.hello', {}, 789)
132 |
133 | t.truthy(spanCalled)
134 | })
135 |
136 | test('it passes single child when class is provied', t => {
137 | let spanCalled = false
138 |
139 | const {span} = dscript((tagName, attrs, ...children) => {
140 | if (tagName === 'span') {
141 | t.deepEqual(children, [789])
142 | spanCalled = true
143 | }
144 | })
145 |
146 | span('.hello', 789)
147 |
148 | t.truthy(spanCalled)
149 | })
150 |
151 | test('it passes single child when only non-string child is passed', t => {
152 | let spanCalled = false
153 |
154 | const {span} = dscript((tagName, attrs, ...children) => {
155 | if (tagName === 'span') {
156 | t.deepEqual(children, [789])
157 | spanCalled = true
158 | }
159 | })
160 |
161 | span(789)
162 |
163 | t.truthy(spanCalled)
164 | })
165 |
166 | test('it passes single child when only string child is passed', t => {
167 | let spanCalled = false
168 |
169 | const {span} = dscript((tagName, attrs, ...children) => {
170 | if (tagName === 'span') {
171 | t.deepEqual(children, ['hello'])
172 | spanCalled = true
173 | }
174 | })
175 |
176 | span('hello')
177 |
178 | t.truthy(spanCalled)
179 | })
180 |
181 | test('dscript fn can handle no attrs, but selector and chilren', t => {
182 | let spanCalled = false
183 |
184 | const {span} = dscript((tagName, attrs, ...children) => {
185 | if (tagName === 'span') {
186 | t.is(attrs.class, 'hi')
187 | t.deepEqual(children, ['yo'])
188 | spanCalled = true
189 | }
190 | })
191 |
192 | span('.hi', ['yo'])
193 |
194 | t.truthy(spanCalled)
195 | })
196 |
197 | test('dscript fns pass empty object if attrs is not passed but children is', t => {
198 | let divCalled = false
199 |
200 | const divChildren = ['hi']
201 |
202 | const {div} = dscript((tagName, attrs, ...children) => {
203 | if (tagName === 'div') {
204 | t.deepEqual(attrs, {})
205 | t.deepEqual(children, divChildren)
206 | divCalled = true
207 | }
208 | })
209 |
210 | div(divChildren)
211 |
212 | t.truthy(divCalled)
213 | })
214 |
215 | test('dscript is a fn that accepts a list of non html tags to pass to createElement', t => {
216 | let fancyCalled = false
217 |
218 | const fancy = dscript(tagName => {
219 | if (tagName === 'fancy') {
220 | fancyCalled = true
221 | }
222 | })('fancy')
223 |
224 | fancy()
225 |
226 | t.truthy(fancyCalled)
227 | })
228 |
229 | test('dscript attaches optional classes and id to attributes and overrides provided attrs', t => {
230 | let divCalled = false
231 |
232 | const {div} = dscript((tagName, attrs, ...children) => {
233 | if (tagName === 'div') {
234 | t.is(attrs.id, 'world')
235 | t.deepEqual(children, [])
236 | divCalled = true
237 | }
238 | })
239 |
240 | div('.hello#world.good-bye', {class: 'yo', id: '3'})
241 |
242 | t.truthy(divCalled)
243 | })
244 |
245 | test('id selector overrides attrs.id if provided', t => {
246 | let divCalled = false
247 |
248 | const {div} = dscript((tagName, attrs) => {
249 | if (tagName === 'div') {
250 | t.is(attrs.id, 'yo')
251 | t.is(attrs.class, 'hi')
252 | divCalled = true
253 | }
254 | })
255 |
256 | div('#yo', {class: 'hi', id: '3'})
257 |
258 | t.truthy(divCalled)
259 | })
260 |
261 | test('class selector overrides attrs.clas if provided', t => {
262 | let divCalled = false
263 |
264 | const {div} = dscript((tagName, attrs) => {
265 | if (tagName === 'div') {
266 | t.is(attrs.id, '3')
267 | t.is(attrs.class, 'yo')
268 | divCalled = true
269 | }
270 | })
271 |
272 | div('.yo', {class: 'hi', id: '3'})
273 |
274 | t.truthy(divCalled)
275 | })
276 |
277 | test('verify it works with Deku\'s element', t => {
278 | const {div} = dscript(element)
279 |
280 | const dekuDiv = div('.yo#hi', {title: 'hello'}, ['world'])
281 |
282 | t.is(dekuDiv.type, 'div')
283 | t.deepEqual(dekuDiv.attributes, {title: 'hello', class: 'yo', id: 'hi'})
284 | t.deepEqual(dekuDiv.children[0], {type: '#text', nodeValue: 'world'})
285 | })
286 |
287 | test('verify it works with React\'s createElement', t => {
288 | const {div} = dscript(createElement)
289 |
290 | const reactDiv = div('.yo#hi', {title: 'hello'}, ['world'])
291 |
292 | t.is(reactDiv.type, 'div')
293 | t.is(reactDiv.props.title, 'hello')
294 | t.is(reactDiv.props.id, 'hi')
295 | t.is(reactDiv.props.class, 'yo')
296 | t.deepEqual(reactDiv.props.children[0], 'world')
297 | })
298 |
--------------------------------------------------------------------------------