diff --git a/CHANGELOG.org b/CHANGELOG.org index 9bdb782..1362e75 100644 --- a/CHANGELOG.org +++ b/CHANGELOG.org @@ -1,6 +1,18 @@ * Changelog ** Unreleased +** 0.8.3 + +- [[https://github.com/r0man/sablono/pull/181][#181]] Update React to =16.2.0=. + +** 0.8.2 + +- [[https://github.com/r0man/sablono/pull/183][#183]] Allow non strings as :value. +- [[https://github.com/r0man/sablono/pull/180][#180]] Update dependencies. + +** 0.8.1 + +- [[https://github.com/r0man/sablono/pull/174][#174]] Update React to =15.6.1=. - [[https://github.com/r0man/sablono/pull/167][#167]] Update React to =15.5.0-0= and get rid of =React.createClass=. ** 0.8.0 diff --git a/README.org b/README.org index 720b715..340a97c 100644 --- a/README.org +++ b/README.org @@ -16,8 +16,8 @@ provide the dependencies yourself like this: #+BEGIN_SRC clojure :exports code :results silent - [cljsjs/react "15.4.2-2"] - [cljsjs/react-dom "15.4.2-2"] + [cljsjs/react "16.2.0-3"] + [cljsjs/react-dom "16.2.0-3"] #+END_SRC If you want to do server rendering and use the =render= or @@ -25,7 +25,7 @@ need to add the following dependency as well: #+BEGIN_SRC clojure :exports code :results silent - [cljsjs/react-dom-server "15.4.2-2"] + [cljsjs/react-dom-server "16.2.0-3"] #+END_SRC ** Usage @@ -131,6 +131,20 @@ lein doo phantom none auto #+END_SRC +*** Why is there a compiler and an interpreter? + + The interpreter is executed at *run time*, and it's job is to + evaluate Hiccup forms and produce React elements. The compiler on + the other hand, is executed at *compile time* and can optimize + certain Hiccup forms. It's job is to evaluate Hiccup forms and + produce executable code. + + A good introduction to this topic can be found in Peter Seibel's + [[http://www.gigamonkeys.com/book/practical-an-html-generation-library-the-compiler.html][Practical Common Lisp]]: + + - [[http://www.gigamonkeys.com/book/practical-an-html-generation-library-the-interpreter.html][An HTML Generation Library, the Interpreter]] + - [[http://www.gigamonkeys.com/book/practical-an-html-generation-library-the-compiler.html][An HTML Generation Library, the Compiler]] + ** Thanks This library is based on James Reeves excellent [[https://github.com/weavejester/hiccup][Hiccup]] library. diff --git a/project.clj b/project.clj index 1c616ab..ab55c02 100644 --- a/project.clj +++ b/project.clj @@ -5,11 +5,11 @@ :min-lein-version "2.0.0" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.9.0-alpha16"] - [org.omcljs/om "1.0.0-beta1"]] + :dependencies [[org.clojure/clojure "1.9.0"] + [org.omcljs/om "1.0.0-beta2"]] :npm {:dependencies [[benchmark "1.0.0"] - [react "15.5.4"] - [react-dom "15.5.4"]]} + [react "16.2.0"] + [react-dom "16.2.0"]]} :profiles {:dev {:dependencies [[criterium "0.4.4"] [devcards "0.2.4" :exclusions [sablono]] [doo "0.1.8"] @@ -18,17 +18,18 @@ [org.clojure/test.check "0.9.0"] [perforate-x "0.1.0"] [reagent "0.7.0"] - [rum "0.10.8" :exclusions [sablono]]] + [rum "0.11.0" :exclusions [sablono]]] :plugins [[lein-cljsbuild "1.1.7"] [lein-doo "0.1.8"] [lein-figwheel "0.5.14"] [lein-npm "0.6.2"] [perforate "0.3.4"]] :resource-paths ["test-resources" "target"]} - :provided {:dependencies [[cljsjs/react "15.6.1-0"] - [cljsjs/react-dom "15.6.1-0"] - [cljsjs/react-dom-server "15.6.1-0"] - [org.clojure/clojurescript "1.9.562"]]} + :provided {:dependencies [[cljsjs/create-react-class "15.6.2-0"] + [cljsjs/react "16.2.0-3"] + [cljsjs/react-dom "16.2.0-3"] + [cljsjs/react-dom-server "16.2.0-3"] + [org.clojure/clojurescript "1.9.946"]]} :repl {:dependencies [[com.cemerick/piggieback "0.2.2"]] :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}}} :aliases {"ci" ["do" @@ -44,18 +45,7 @@ "deploy" ["do" "clean," "deploy" "clojars"]} :clean-targets ^{:protect false} [:target-path] :cljsbuild {:builds - [{:id "benchmark" - :compiler - {:asset-path "target/benchmark/out" - :main sablono.benchmark - :output-dir "target/benchmark/out" - :output-to "target/benchmark/sablono.js" - :optimizations :none - :target :nodejs - :pretty-print true - :verbose false} - :source-paths ["src" "benchmark"]} - {:id "devcards" + [{:id "devcards" :compiler {:asset-path "devcards" :main sablono.test.runner @@ -67,6 +57,17 @@ :verbose false} :figwheel {:devcards true} :source-paths ["src" "test"]} + {:id "benchmark" + :compiler + {:asset-path "target/benchmark/out" + :main sablono.benchmark + :output-dir "target/benchmark/out" + :output-to "target/benchmark/sablono.js" + :optimizations :none + :target :nodejs + :pretty-print true + :verbose false} + :source-paths ["src" "benchmark"]} {:id "nodejs" :compiler {:asset-path "target/nodejs/out" @@ -95,8 +96,14 @@ {:asset-path "target/advanced/out" :main sablono.test.runner :output-dir "target/advanced/out" - :output-to "target/advanced/sablono.js" :optimizations :advanced + :output-to "target/advanced/sablono.js" + ;; Polyfills needed for PhantomJS and Nashorn + :preamble ["polyfills/symbol.js" + "polyfills/symbol.iterator.js" + "polyfills/map.js" + "polyfills/set.js" + "polyfills/number.isnan.js"] :pretty-print true :verbose false} :source-paths ["src" "test"]} diff --git a/src/sablono/compiler.clj b/src/sablono/compiler.clj index f7c9e5d..4480851 100644 --- a/src/sablono/compiler.clj +++ b/src/sablono/compiler.clj @@ -14,6 +14,11 @@ (defprotocol IJSValue (to-js [x])) +(defn fragment? + "Returns true if `tag` is the fragment tag \"*\", otherwise false." + [tag] + (= (name tag) "*")) + (defmulti compile-attr (fn [name value] name)) (defmethod compile-attr :class [_ value] @@ -47,6 +52,13 @@ (html-to-dom-attrs) (to-js))) +(defn- compile-constructor + "Return the symbol of a fn that build a React element. " + [type] + (if (contains? #{:input :select :textarea} (keyword type)) + 'sablono.interpreter/create-element + 'js/React.createElement)) + (defn compile-merge-attrs [attrs-1 attrs-2] (let [empty-attrs? #(or (nil? %1) (and (map? %1) (empty? %1)))] (cond @@ -63,12 +75,20 @@ :else `(sablono.interpreter/attributes (sablono.normalize/merge-with-class ~attrs-1 ~attrs-2))))) +(defn- compile-tag + "Replace fragment syntax (`:*`) by 'js/React.Fragment, otherwise the + name of the tag" + [tag] + (if (fragment? tag) + 'js/React.Fragment + (name tag))) + (defn compile-react-element "Render an element vector as a HTML element." [element] (let [[tag attrs content] (normalize/element element)] - `(~(react-fn tag) - ~(name tag) + `(~(compile-constructor tag) + ~(compile-tag tag) ~(compile-attrs attrs) ~@(if content (compile-react content))))) @@ -204,8 +224,8 @@ (defmethod compile-element ::literal-tag-and-attributes [[tag attrs & content]] (let [[tag attrs _] (normalize/element [tag attrs])] - `(~(react-fn tag) - ~(name tag) + `(~(compile-constructor tag) + ~(compile-tag tag) ~(compile-attrs attrs) ~@(map compile-html content)))) @@ -222,8 +242,8 @@ (let [[tag tag-attrs _] (normalize/element [tag]) attrs-sym (gensym "attrs")] `(let [~attrs-sym ~attrs] - (apply ~(react-fn tag) - ~(name tag) + (apply ~(compile-constructor tag) + ~(compile-tag tag) ~(compile-merge-attrs tag-attrs attrs-sym) ~(when-not (empty? content) (mapv compile-html content)))))) @@ -233,8 +253,8 @@ (let [[tag tag-attrs _] (normalize/element [tag]) attrs-sym (gensym "attrs")] `(let [~attrs-sym ~attrs] - (apply ~(react-fn tag) - ~(name tag) + (apply ~(compile-constructor tag) + ~(compile-tag tag) (if (map? ~attrs-sym) ~(compile-merge-attrs tag-attrs attrs-sym) ~(compile-attrs tag-attrs)) diff --git a/src/sablono/util.cljc b/src/sablono/util.cljc index 68c9801..4b65239 100644 --- a/src/sablono/util.cljc +++ b/src/sablono/util.cljc @@ -68,13 +68,6 @@ (remove nil?))) (str/join " "))) -(defn react-fn - "Return the symbol of a fn that build a React element. " - [type] - (if (contains? #{:input :select :textarea} (keyword type)) - 'sablono.interpreter/create-element - 'js/React.createElement)) - #?(:cljs (extend-protocol ToString cljs.core.Keyword diff --git a/test-resources/polyfills/map.js b/test-resources/polyfills/map.js new file mode 100644 index 0000000..38e7b8f --- /dev/null +++ b/test-resources/polyfills/map.js @@ -0,0 +1,297 @@ +(function(global) { + + + // Deleted map items mess with iterator pointers, so rather than removing them mark them as deleted. Can't use undefined or null since those both valid keys so use a private symbol. + var undefMarker = Symbol('undef'); + + // NaN cannot be found in an array using indexOf, so we encode NaNs using a private symbol. + var NaNMarker = Symbol('NaN'); + + function encodeKey(key) { + return Number.isNaN(key) ? NaNMarker : key; + } + function decodeKey(encodedKey) { + return (encodedKey === NaNMarker) ? NaN : encodedKey; + } + + function makeIterator(mapInst, getter) { + var nextIdx = 0; + var done = false; + return { + next: function() { + if (!mapInst.size || nextIdx === mapInst._keys.length) { + done = true; + } + if (!done) { + while (nextIdx <= mapInst._keys.length) { + if (mapInst._keys[nextIdx] === undefMarker) { + nextIdx++; + } else { + break; + } + } + if (!mapInst.size || nextIdx === mapInst._keys.length) { + return {value: void 0, done:true}; + } + return {value: getter.call(mapInst, nextIdx++), done: false}; + } else { + return {value: void 0, done:true}; + } + } + }; + } + + function hasProtoMethod(instance, method){ + return typeof instance[method] === 'function'; + } + + var supportsGetters; + + var Map = function Map() { + if (!(this instanceof Map)) { + throw new TypeError('Constructor Map requires "new"'); + } + + var data = arguments[0]; + Object.defineProperty(this, '_keys', { + configurable: true, + enumerable: false, + writable: true, + value: [] + }); + Object.defineProperty(this, '_values', { + configurable: true, + enumerable: false, + writable: true, + value: [] + }); + Object.defineProperty(this, '_size', { + configurable: true, + enumerable: false, + writable: true, + value: 0 + }); + + // Some old engines do not support ES5 getters/setters. Since Map only requires these for the size property, we can fall back to setting the size property statically each time the size of the map changes. + try { + Object.defineProperty(Map.prototype, 'size', { + configurable: true, + enumerable: false, + get: function() { + return this._size; + }, + set: undefined + }); + Object.defineProperty(this, 'size', { + configurable: true, + enumerable: false, + get: function() { + return this._size; + }, + set: undefined + }); + supportsGetters = true; + } catch (e) { + supportsGetters = false; + Object.defineProperty(this, 'size', { + configurable: true, + enumerable: false, + writable: true, + value: 0 + }); + } + // If `data` is iterable (indicated by presence of a forEach method), pre-populate the map + if (data && hasProtoMethod(data, 'forEach')){ + // Fastpath: If `data` is a Map, shortcircuit all following the checks + if (data instanceof Map || + // If `data` is not an instance of Map, it could be because you have a Map from an iframe or a worker or something. + // Check if `data` has all the `Map` methods and if so, assume data is another Map + hasProtoMethod(data, 'clear') && + hasProtoMethod(data, 'delete') && + hasProtoMethod(data, 'entries') && + hasProtoMethod(data, 'forEach') && + hasProtoMethod(data, 'get') && + hasProtoMethod(data, 'has') && + hasProtoMethod(data, 'keys') && + hasProtoMethod(data, 'set') && + hasProtoMethod(data, 'values')){ + data.forEach(function (value, key) { + this.set.apply(this, [key, value]); + }, this); + } else { + data.forEach(function (item) { + this.set.apply(this, item); + }, this); + } + } + }; + + Object.defineProperty(Map, 'prototype', { + configurable: false, + enumerable: false, + writable: false, + value: {} + }); + + Object.defineProperty(Map.prototype, 'get', { + configurable: true, + enumerable: false, + writable: true, + value: function get (key) { + var idx = this._keys.indexOf(encodeKey(key)); + return (idx !== -1) ? this._values[idx] : undefined; + } + }); + Object.defineProperty(Map.prototype, 'set', { + configurable: true, + enumerable: false, + writable: true, + value: function set (key, value) { + var idx = this._keys.indexOf(encodeKey(key)); + if (idx !== -1) { + this._values[idx] = value; + } else { + this._keys.push(encodeKey(key)); + this._values.push(value); + ++this._size; + if (!supportsGetters) { + this.size = this._size; + } + } + return this; + } + }); + Object.defineProperty(Map.prototype, 'has', { + configurable: true, + enumerable: false, + writable: true, + value: function has (key) { + return (this._keys.indexOf(encodeKey(key)) !== -1); + } + }); + Object.defineProperty(Map.prototype, 'delete', { + configurable: true, + enumerable: false, + writable: true, + value: function (key) { + var idx = this._keys.indexOf(encodeKey(key)); + if (idx === -1) return false; + this._keys[idx] = undefMarker; + this._values[idx] = undefMarker; + --this._size; + if (!supportsGetters) { + this.size = this._size; + } + return true; + } + }); + Object.defineProperty(Map.prototype, 'clear', { + configurable: true, + enumerable: false, + writable: true, + value: function clear () { + this._keys = []; + this._values = []; + this._size = 0; + if (!supportsGetters) { + this.size = this._size; + } + } + }); + Object.defineProperty(Map.prototype, 'values', { + configurable: true, + enumerable: false, + writable: true, + value: function values () { + var iterator = makeIterator(this, function(i) { return this._values[i]; }); + iterator[Symbol.iterator] = this.values.bind(this); + return iterator; + } + }); + Object.defineProperty(Map.prototype, 'keys', { + configurable: true, + enumerable: false, + writable: true, + value: function keys () { + var iterator = makeIterator(this, function(i) { return decodeKey(this._keys[i]); }); + iterator[Symbol.iterator] = this.keys.bind(this); + return iterator; + } + }); + var entries = function entries () { + var iterator = makeIterator(this, function(i) { return [decodeKey(this._keys[i]), this._values[i]]; }); + iterator[Symbol.iterator] = this.entries.bind(this); + return iterator; + }; + Object.defineProperty(Map.prototype, Symbol.iterator, { + configurable: true, + enumerable: false, + writable: true, + value: entries + }); + Object.defineProperty(Map.prototype, 'entries', { + configurable: true, + enumerable: false, + writable: true, + value: entries + }); + Object.defineProperty(Map.prototype, 'forEach', { + configurable: true, + enumerable: false, + writable: true, + value: function(callbackFn, thisArg) { + thisArg = thisArg || global; + var iterator = this.entries(); + var result = iterator.next(); + while (result.done === false) { + callbackFn.call(thisArg, result.value[1], result.value[0], this); + result = iterator.next(); + } + } + }); + Object.defineProperty(Map.prototype, 'constructor', { + configurable: true, + enumerable: false, + writable: true, + value: Map + }); + try { + Object.defineProperty(Map, Symbol.species, { + configurable: true, + enumerable: false, + get: function get() { + return Map; + } + }); + } catch (e) {} + + // Safari 8 sets the name property with correct value but also to be non-configurable + if (!('name' in Map)) { + Object.defineProperty(Map, 'name', { + configurable: true, + enumerable: false, + writable: false, + value: 'Map' + }); + } + + // Export the object + try { + Object.defineProperty(global, 'Map', { + configurable: true, + enumerable: false, + writable: true, + value: Map + }); + } catch (e) { + // IE8 throws an error here if we set enumerable to false. + // More info on table 2: https://msdn.microsoft.com/en-us/library/dd229916(v=vs.85).aspx + Object.defineProperty(global, 'Map', { + configurable: true, + enumerable: true, + writable: true, + value: Map + }); + } + +}(this)); diff --git a/test-resources/polyfills/number.isnan.js b/test-resources/polyfills/number.isnan.js new file mode 100644 index 0000000..015e0e5 --- /dev/null +++ b/test-resources/polyfills/number.isnan.js @@ -0,0 +1,3 @@ +Number.isNaN = Number.isNaN || function(value) { + return typeof value === "number" && isNaN(value); +}; diff --git a/test-resources/polyfills/set.js b/test-resources/polyfills/set.js new file mode 100644 index 0000000..c7b59cf --- /dev/null +++ b/test-resources/polyfills/set.js @@ -0,0 +1,105 @@ +(function(global) { + + + // Deleted map items mess with iterator pointers, so rather than removing them mark them as deleted. Can't use undefined or null since those both valid keys so use a private symbol. + var undefMarker = Symbol('undef'); + + // NaN cannot be found in an array using indexOf, so we encode NaNs using a private symbol. + var NaNMarker = Symbol('NaN'); + + function encodeVal(data) { + return Number.isNaN(data) ? NaNMarker : data; + } + function decodeVal(encodedData) { + return (encodedData === NaNMarker) ? NaN : encodedData; + } + + function makeIterator(setInst, getter) { + var nextIdx = 0; + return { + next: function() { + while (setInst._values[nextIdx] === undefMarker) nextIdx++; + if (nextIdx === setInst._values.length) { + return {value: void 0, done: true}; + } + else { + return {value: getter.call(setInst, nextIdx++), done: false}; + } + } + }; + } + + var Set = function Set() { + var data = arguments[0]; + this._values = []; + this.size = this._size = 0; + + // If `data` is iterable (indicated by presence of a forEach method), pre-populate the set + data && (typeof data.forEach === 'function') && data.forEach(function (item) { + this.add.call(this, item); + }, this); + }; + + // Some old engines do not support ES5 getters/setters. Since Set only requires these for the size property, we can fall back to setting the size property statically each time the size of the set changes. + try { + Object.defineProperty(Set.prototype, 'size', { + get: function() { + return this._size; + } + }); + } catch(e) { + } + + Set.prototype['add'] = function(value) { + value = encodeVal(value); + if (this._values.indexOf(value) === -1) { + this._values.push(value); + this.size = ++this._size; + } + return this; + }; + Set.prototype['has'] = function(value) { + return (this._values.indexOf(encodeVal(value)) !== -1); + }; + Set.prototype['delete'] = function(value) { + var idx = this._values.indexOf(encodeVal(value)); + if (idx === -1) return false; + this._values[idx] = undefMarker; + this.size = --this._size; + return true; + }; + Set.prototype['clear'] = function() { + this._values = []; + this.size = this._size = 0; + }; + Set.prototype[Symbol.iterator] = + Set.prototype['values'] = + Set.prototype['keys'] = function() { + var iterator = makeIterator(this, function(i) { return decodeVal(this._values[i]); }); + iterator[Symbol.iterator] = this.keys.bind(this); + return iterator; + }; + Set.prototype['entries'] = function() { + var iterator = makeIterator(this, function(i) { return [decodeVal(this._values[i]), decodeVal(this._values[i])]; }); + iterator[Symbol.iterator] = this.entries.bind(this); + return iterator; + }; + Set.prototype['forEach'] = function(callbackFn, thisArg) { + thisArg = thisArg || global; + var iterator = this.entries(); + var result = iterator.next(); + while (result.done === false) { + callbackFn.call(thisArg, result.value[1], result.value[0], this); + result = iterator.next(); + } + }; + Set.prototype['constructor'] = + Set.prototype[Symbol.species] = Set; + + Set.prototype.constructor = Set; + Set.name = "Set"; + + // Export the object + global.Set = Set; + +}(this)); diff --git a/test-resources/polyfills/symbol.iterator.js b/test-resources/polyfills/symbol.iterator.js new file mode 100644 index 0000000..e2373d9 --- /dev/null +++ b/test-resources/polyfills/symbol.iterator.js @@ -0,0 +1 @@ +Object.defineProperty(Symbol, 'iterator', {value: Symbol('iterator')}); diff --git a/test-resources/polyfills/symbol.js b/test-resources/polyfills/symbol.js new file mode 100644 index 0000000..b644ee8 --- /dev/null +++ b/test-resources/polyfills/symbol.js @@ -0,0 +1,230 @@ +// A modification of https://github.com/WebReflection/get-own-property-symbols +// (C) Andrea Giammarchi - MIT Licensed + +(function (Object, GOPS, global) { + + var setDescriptor; + var id = 0; + var random = '' + Math.random(); + var prefix = '__\x01symbol:'; + var prefixLength = prefix.length; + var internalSymbol = '__\x01symbol@@' + random; + var DP = 'defineProperty'; + var DPies = 'defineProperties'; + var GOPN = 'getOwnPropertyNames'; + var GOPD = 'getOwnPropertyDescriptor'; + var PIE = 'propertyIsEnumerable'; + var ObjectProto = Object.prototype; + var hOP = ObjectProto.hasOwnProperty; + var pIE = ObjectProto[PIE]; + var toString = ObjectProto.toString; + var concat = Array.prototype.concat; + var cachedWindowNames = typeof window === 'object' ? Object.getOwnPropertyNames(window) : []; + var nGOPN = Object[GOPN]; + var gOPN = function getOwnPropertyNames (obj) { + if (toString.call(obj) === '[object Window]') { + try { + return nGOPN(obj); + } catch (e) { + // IE bug where layout engine calls userland gOPN for cross-domain `window` objects + return concat.call([], cachedWindowNames); + } + } + return nGOPN(obj); + }; + var gOPD = Object[GOPD]; + var create = Object.create; + var keys = Object.keys; + var freeze = Object.freeze || Object; + var defineProperty = Object[DP]; + var $defineProperties = Object[DPies]; + var descriptor = gOPD(Object, GOPN); + var addInternalIfNeeded = function (o, uid, enumerable) { + if (!hOP.call(o, internalSymbol)) { + try { + defineProperty(o, internalSymbol, { + enumerable: false, + configurable: false, + writable: false, + value: {} + }); + } catch (e) { + o[internalSymbol] = {}; + } + } + o[internalSymbol]['@@' + uid] = enumerable; + }; + var createWithSymbols = function (proto, descriptors) { + var self = create(proto); + gOPN(descriptors).forEach(function (key) { + if (propertyIsEnumerable.call(descriptors, key)) { + $defineProperty(self, key, descriptors[key]); + } + }); + return self; + }; + var copyAsNonEnumerable = function (descriptor) { + var newDescriptor = create(descriptor); + newDescriptor.enumerable = false; + return newDescriptor; + }; + var get = function get(){}; + var onlyNonSymbols = function (name) { + return name != internalSymbol && + !hOP.call(source, name); + }; + var onlySymbols = function (name) { + return name != internalSymbol && + hOP.call(source, name); + }; + var propertyIsEnumerable = function propertyIsEnumerable(key) { + var uid = '' + key; + return onlySymbols(uid) ? ( + hOP.call(this, uid) && + this[internalSymbol]['@@' + uid] + ) : pIE.call(this, key); + }; + var setAndGetSymbol = function (uid) { + var descriptor = { + enumerable: false, + configurable: true, + get: get, + set: function (value) { + setDescriptor(this, uid, { + enumerable: false, + configurable: true, + writable: true, + value: value + }); + addInternalIfNeeded(this, uid, true); + } + }; + try { + defineProperty(ObjectProto, uid, descriptor); + } catch (e) { + ObjectProto[uid] = descriptor.value; + } + return freeze(source[uid] = defineProperty( + Object(uid), + 'constructor', + sourceConstructor + )); + }; + var Symbol = function Symbol(description) { + if (this instanceof Symbol) { + throw new TypeError('Symbol is not a constructor'); + } + return setAndGetSymbol( + prefix.concat(description || '', random, ++id) + ); + }; + var source = create(null); + var sourceConstructor = {value: Symbol}; + var sourceMap = function (uid) { + return source[uid]; + }; + var $defineProperty = function defineProp(o, key, descriptor) { + var uid = '' + key; + if (onlySymbols(uid)) { + setDescriptor(o, uid, descriptor.enumerable ? + copyAsNonEnumerable(descriptor) : descriptor); + addInternalIfNeeded(o, uid, !!descriptor.enumerable); + } else { + defineProperty(o, key, descriptor); + } + return o; + }; + + var onlyInternalSymbols = function (obj) { + return function (name) { + return hOP.call(obj, internalSymbol) && hOP.call(obj[internalSymbol], '@@' + name); + }; + }; + var $getOwnPropertySymbols = function getOwnPropertySymbols(o) { + return gOPN(o).filter(o === ObjectProto ? onlyInternalSymbols(o) : onlySymbols).map(sourceMap); + } + ; + + descriptor.value = $defineProperty; + defineProperty(Object, DP, descriptor); + + descriptor.value = $getOwnPropertySymbols; + defineProperty(Object, GOPS, descriptor); + + descriptor.value = function getOwnPropertyNames(o) { + return gOPN(o).filter(onlyNonSymbols); + }; + defineProperty(Object, GOPN, descriptor); + + descriptor.value = function defineProperties(o, descriptors) { + var symbols = $getOwnPropertySymbols(descriptors); + if (symbols.length) { + keys(descriptors).concat(symbols).forEach(function (uid) { + if (propertyIsEnumerable.call(descriptors, uid)) { + $defineProperty(o, uid, descriptors[uid]); + } + }); + } else { + $defineProperties(o, descriptors); + } + return o; + }; + defineProperty(Object, DPies, descriptor); + + descriptor.value = propertyIsEnumerable; + defineProperty(ObjectProto, PIE, descriptor); + + descriptor.value = Symbol; + defineProperty(global, 'Symbol', descriptor); + + // defining `Symbol.for(key)` + descriptor.value = function (key) { + var uid = prefix.concat(prefix, key, random); + return uid in ObjectProto ? source[uid] : setAndGetSymbol(uid); + }; + defineProperty(Symbol, 'for', descriptor); + + // defining `Symbol.keyFor(symbol)` + descriptor.value = function (symbol) { + if (onlyNonSymbols(symbol)) + throw new TypeError(symbol + ' is not a symbol'); + return hOP.call(source, symbol) ? + symbol.slice(prefixLength * 2, -random.length) : + void 0 + ; + }; + defineProperty(Symbol, 'keyFor', descriptor); + + descriptor.value = function getOwnPropertyDescriptor(o, key) { + var descriptor = gOPD(o, key); + if (descriptor && onlySymbols(key)) { + descriptor.enumerable = propertyIsEnumerable.call(o, key); + } + return descriptor; + }; + defineProperty(Object, GOPD, descriptor); + + descriptor.value = function (proto, descriptors) { + return arguments.length === 1 || typeof descriptors === "undefined" ? + create(proto) : + createWithSymbols(proto, descriptors); + }; + defineProperty(Object, 'create', descriptor); + + descriptor.value = function () { + var str = toString.call(this); + return (str === '[object String]' && onlySymbols(this)) ? '[object Symbol]' : str; + }; + defineProperty(ObjectProto, 'toString', descriptor); + + + setDescriptor = function (o, key, descriptor) { + var protoDescriptor = gOPD(ObjectProto, key); + delete ObjectProto[key]; + defineProperty(o, key, descriptor); + if (o !== ObjectProto) { + defineProperty(ObjectProto, key, protoDescriptor); + } + }; + +}(Object, 'getOwnPropertySymbols', this)); diff --git a/test/sablono/compiler_test.clj b/test/sablono/compiler_test.clj index 96b00dd..e686bf8 100644 --- a/test/sablono/compiler_test.clj +++ b/test/sablono/compiler_test.clj @@ -1,6 +1,7 @@ (ns sablono.compiler-test (:refer-clojure :exclude [compile]) (:require [clojure.spec.alpha :as s] + [clojure.spec.gen.alpha :as gen] [clojure.test :refer :all] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] @@ -75,9 +76,12 @@ (is (= 2 (first (.val v)))) (is (= [3] (.val (second (.val v)))))))))) +(def gen-tag + (gen/such-that (complement fragment?) (s/gen keyword?))) + (defspec test-basic-tags (prop/for-all - [tag (s/gen keyword?)] + [tag gen-tag] (=== (eval `(compile [~tag])) `(js/React.createElement ~(name tag) nil)))) @@ -254,6 +258,38 @@ (html-expand [:div (foo)]) (is (= @times-called 1))))) +(deftest fragments + (testing "React 16 fragment syntactic support" + (are-html + '[:*] + '(js/React.createElement + js/React.Fragment nil) + + '[:* [:p]] + '(js/React.createElement + js/React.Fragment nil + (js/React.createElement "p" nil)) + + '[:* [:p] [:p]] + '(js/React.createElement + js/React.Fragment nil + (js/React.createElement "p" nil) + (js/React.createElement "p" nil)) + + '[:dl (for [n (range 2)] + [:* {:key n} + [:dt {} (str "term " n)] + [:dd {} (str "definition " n)]])] + '(js/React.createElement + "dl" nil + (into-array + (clojure.core/for + [n (range 2)] + (js/React.createElement + js/React.Fragment #j {:key n} + (js/React.createElement "dt" nil (sablono.interpreter/interpret (str "term " n))) + (js/React.createElement "dd" nil (sablono.interpreter/interpret (str "definition " n)))))))))) + (deftest test-benchmark-template (are-html '[:li diff --git a/test/sablono/core_test.cljs b/test/sablono/core_test.cljs index 449f5d5..69c0f13 100644 --- a/test/sablono/core_test.cljs +++ b/test/sablono/core_test.cljs @@ -1,7 +1,8 @@ (ns sablono.core-test (:require-macros [sablono.core :refer [html with-group]] - [sablono.test :refer [html-data]]) - (:require [clojure.pprint :refer [pprint]] + [sablono.test :refer [html-data html-str]]) + (:require [cljsjs.create-react-class] + [clojure.pprint :refer [pprint]] [clojure.test :refer [are is testing]] [devcards.core :refer-macros [defcard deftest]] [rum.core :as rum] @@ -1010,7 +1011,7 @@ (deftest test-issue-3-recursive-js-value (is (= (html-data [:div.interaction-row {:style {:position "relative"}}]) {:tag :div - :attributes {:style "position:relative;" :class "interaction-row"} + :attributes {:style "position:relative" :class "interaction-row"} :content []})) (let [username "foo", hidden #(if %1 {:display "none"} {:display "block"})] (is (= (html-data [:ul.nav.navbar-nav.navbar-right.pull-right @@ -1022,7 +1023,7 @@ :attributes {:class "nav navbar-nav navbar-right pull-right"} :content [{:tag :li - :attributes {:style "display:block;" :class "dropdown"} + :attributes {:style "display:block" :class "dropdown"} :content [{:tag :a :attributes {:role "button" :href "#" :class "dropdown-toggle"} @@ -1030,7 +1031,7 @@ ["Welcome, foo" {:tag :span :attributes {:class "caret"} :content []}]} {:tag :ul - :attributes {:role "menu" :style "left:0;" :class "dropdown-menu"} + :attributes {:role "menu" :style "left:0" :class "dropdown-menu"} :content []}]}]})))) (deftest test-issue-22-id-after-class @@ -1134,11 +1135,11 @@ (deftest test-issue-37-camel-case-style-attrs (is (= (html-data [:div {:style {:z-index 1000}}]) {:tag :div - :attributes {:style "z-index:1000;"} + :attributes {:style "z-index:1000"} :content []})) (is (= (html-data [:div (merge {:style {:z-index 1000}})]) {:tag :div - :attributes {:style "z-index:1000;"} + :attributes {:style "z-index:1000"} :content []}))) (deftest test-div-with-nested-lazy-seq @@ -1296,14 +1297,14 @@ (when focused? {:color "red"}))}]) {:tag :div :attributes - {:style "margin-left:2rem;color:red;"} + {:style "margin-left:2rem;color:red"} :content []}))) (let [focused? false] (is (= (html-data [:div {:style (merge {:margin-left "2rem"} (when focused? {:color "red"}))}]) {:tag :div :attributes - {:style "margin-left:2rem;"} + {:style "margin-left:2rem"} :content []})))) (deftest test-issue-160 @@ -1312,3 +1313,24 @@ {:tag :div :attributes {:data-foo "bar"} :content []})))) + +(deftest test-fragment-only + (is (= (html-str [:*]) + ""))) + +(deftest test-fragment-child-1 + (is (= (html-str [:* [:p]]) + "

"))) + +(deftest test-fragment-child-n + (is (= (html-str [:* [:p] [:p]]) + "

"))) + +(deftest test-fragment-for-loop + (is (= (html-str + [:dl (for [n (range 2)] + [:* {:key n} + [:dt {} (str "term " n)] + [:dd {} (str "definition " n)]])]) + (str "
term 0
definition 0
" + "
term 1
definition 1
")))) diff --git a/test/sablono/input_test.cljs b/test/sablono/input_test.cljs index 2e5c133..a311621 100644 --- a/test/sablono/input_test.cljs +++ b/test/sablono/input_test.cljs @@ -1,6 +1,6 @@ (ns sablono.input-test - (:require [devcards.core :refer-macros [defcard]] - [clojure.pprint :refer [pprint]] + (:require [clojure.pprint :refer [pprint]] + [devcards.core :refer-macros [defcard]] [rum.core :as rum])) (def fruits diff --git a/test/sablono/interpreter_test.cljc b/test/sablono/interpreter_test.cljc index e78b29e..b03cea0 100644 --- a/test/sablono/interpreter_test.cljc +++ b/test/sablono/interpreter_test.cljc @@ -120,8 +120,15 @@ :attributes {} :content [child]}))) -(defspec test-child-as-number - (for-all [child (s/gen number?)] +(defspec test-child-as-int + (for-all [child (s/gen int?)] + (= (interpret [:div child]) + {:tag :div + :attributes {} + :content [(str child)]}))) + +(defspec test-child-as-double + (for-all [child (s/gen (s/double-in :infinite? false :NaN? false))] (= (interpret [:div child]) {:tag :div :attributes {} diff --git a/test/sablono/server_test.cljs b/test/sablono/server_test.cljs index 10605df..a2e712e 100644 --- a/test/sablono/server_test.cljs +++ b/test/sablono/server_test.cljs @@ -4,12 +4,12 @@ [sablono.server :as server])) (deftest test-render - (are [markup match] - (re-matches (re-pattern match) (server/render markup)) + (are [markup expected] + (= (server/render markup) expected) (html [:div#a.b "c"]) - "
c
" + "
c
" (html [:div (when true [:p "data"]) (if true [:p "data"] nil)]) - "

data

data

")) + "

data

data

")) (deftest test-render-static (are [markup expected] diff --git a/test/sablono/specs.cljc b/test/sablono/specs.cljc index f93e60b..f9f6563 100644 --- a/test/sablono/specs.cljc +++ b/test/sablono/specs.cljc @@ -44,7 +44,7 @@ :caption :circle :cite - :clipPath + :clippath :code :col :colgroup @@ -88,7 +88,7 @@ :legend :li :line - :linearGradient + :lineargradient :link :main :map @@ -114,7 +114,7 @@ :pre :progress :q - :radialGradient + :radialgradient :rect :rp :rt