diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..104a3d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bower_components/ +node_modules/ +npm-debug.log +.module-cache +*.pyc diff --git a/README.md b/README.md index 0b422c6..a28c97b 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,71 @@ cuttlefish Human-friendly HTML rendering of machine-friendly JSON-LD --------------------------------------------------------- -Description to come. - +Cuttlefish allows you to get HTML out of almost any JSON-LD data structure by providing [ReactJS](https://facebook.github.io/react/) component based on the [Schema.org](http://schema.org/) definition of entities. What's in a name? ----------------- Story to come. +How do I use it? +---------- + +The basic compiled version allows you to visualise `Person`s, `Product`s and `Place`s. (see `index.html` example) + +To use it, all you need to do is : + +* Install cuttlefish with bower + +```sh +bower install cuttlefish +``` + +* Insert it in your webpage (before your scripts) and insert a node in which to insert the representation + +```html + + + +
+
+... + +... +``` + +* Call it with your JSON-ld and the ID of your node + +```javascript +cuttlefish.represent(JSONLDData, 'myJSON-LDRepresentationContainer'); +``` + +* Check out your HTML (right-click + inspect in Google Chrome) + +Develop +------- + +Ok, that's nice, but now, you want to put your hands in the dough. + +There are three ways to do that: + +* You want to change the style of what you see ... + + * ... by modifying only certain attributes? Just create a CSS file and override the classes you want by using the ID of the container as a higher CSS specificity selector! + + * ... by starting from scratch? Remove our stylesheet in the *head* tag and create your own, you will find that cuttlefish tries to follow the [SUIT CSS naming convention](https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md) to make it easy for you to style. + +* You want to customize the HTML itself, or extend the React components for them to really do something! + + * Fork this repo + * In the `src/es6/` folder, you will find the components: Modify them as you want! + * To Rebuild the all in one dist file: run `npm install && npm run build-dist` + +* You want to adapt cuttlefish to YOUR data structure: + * Put an extensive example of your JSON-LD data in the `source.json` file + * Run the `npm run generate-source` command + * Look in `src/es6` and you should have the corresponding react components in Ecma script 6 syntax. Customize them as you want and + * Run `npm run build` to get the new version of Cuttlefish with YOUR React components packaged. If you are happy with them, run `npm run build-dist` to generate the distribution-ready version (minified and stuff) License ------- @@ -31,4 +88,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/bower.json b/bower.json index db8a3b5..55fd1fd 100644 --- a/bower.json +++ b/bower.json @@ -1,16 +1,29 @@ { "name": "cuttlefish", + "version": "0.0.2", "description": "Human-friendly HTML rendering of machine-friendly JSON-LD. We believe in an open Internet of Things.", + "homepage": "https://github.com/reelyactive/cuttlefish", "keywords": [ "JSON-LD", "IoT" ], "authors": [ - { "name": "Adrien Thiery" } + "Adrien Thiery " ], "repository": { "type": "git", "url": "git://github.com/reelyactive/cuttlefish.git" }, - "license": "MIT" + "license": "MIT", + "main": "dist/cuttlefish.min.js", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + }, + "devDependencies": {} } diff --git a/dist/cuttlefish.css b/dist/cuttlefish.css new file mode 100644 index 0000000..9bc4580 --- /dev/null +++ b/dist/cuttlefish.css @@ -0,0 +1,111 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +@font-face { + font-family: 'EntypoRegular'; + src: url('../font/entypo.eot'); + src: url('../font/entypo.eot?#iefix') format('embedded-opentype'), + url('../font/entypo.woff') format('woff'), + url('../font/entypo.ttf') format('truetype'), + url('../font/entypo.svg#EntypoRegular') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'EntypoSocial'; + src: url('../font/entypo-social.eot'); + src: url('../font/entypo-social.eot?#iefix') format('embedded-opentype'), + url('../font/entypo-social.woff') format('woff'), + url('../font/entypo-social.ttf') format('truetype'), + url('../font/entypo-social.svg#entypo-social') format('svg'); + font-weight: normal; + font-style: normal; +} + +img { + display: block; + max-width: 100%; +} + +a { + color: #0770a2; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.entity { + border: solid 1px #ddd; + font-size: 0.8em; + padding: 5px; + background-color: white; + margin: 5px; + clear: both; +} + +.Device + .Device { + margin-top: 10px; +} + +/* Colored ! +.Device { + border-color: #FFAC71; + background-color: #FFDFA6; +} + +.Person { + border-color: #BFD4FF; + background-color: #BFD4FF; +} + +.Product { + border-color: #FFFF82; + background-color: #FFFF82; +} + +.tiraid { + border-color: #CCCCCC; + background-color: #EEEEEE; +} +*/ + +.clear { + clear: both; +} + +.Person img, .Device img { + max-width: 8em; + max-height: 8em; + width: auto; + height: auto; + float: left; + margin: 5px; +} + +.Person .clear, .Device .clear { + clear: both; +} + +img.icon { + clear: both; + max-height: 2em; +} + +/* Icons */ +.icon { + font-family: 'EntypoRegular'; + font-size: 2em; + display: inline-block; + margin-right: 5px; + vertical-align: bottom; +} +.social { + font-family: 'EntypoSocial'; +} + diff --git a/dist/cuttlefish.js b/dist/cuttlefish.js new file mode 100644 index 0000000..d431562 --- /dev/null +++ b/dist/cuttlefish.js @@ -0,0 +1,20203 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o camelize('background-color') + * < "backgroundColor" + * + * @param {string} string + * @return {string} + */ +function camelize(string) { + return string.replace(_hyphenPattern, function (_, character) { + return character.toUpperCase(); + }); +} + +module.exports = camelize; +},{}],4:[function(require,module,exports){ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @typechecks + */ + +'use strict'; + +var camelize = require('./camelize'); + +var msPattern = /^-ms-/; + +/** + * Camelcases a hyphenated CSS property name, for example: + * + * > camelizeStyleName('background-color') + * < "backgroundColor" + * > camelizeStyleName('-moz-transition') + * < "MozTransition" + * > camelizeStyleName('-ms-transition') + * < "msTransition" + * + * As Andi Smith suggests + * (http://www.andismith.com/blog/2012/02/modernizr-prefixed/), an `-ms` prefix + * is converted to lowercase `ms`. + * + * @param {string} string + * @return {string} + */ +function camelizeStyleName(string) { + return camelize(string.replace(msPattern, 'ms-')); +} + +module.exports = camelizeStyleName; +},{"./camelize":3}],5:[function(require,module,exports){ +'use strict'; + +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * + */ + +var isTextNode = require('./isTextNode'); + +/*eslint-disable no-bitwise */ + +/** + * Checks if a given DOM node contains or is another DOM node. + */ +function containsNode(outerNode, innerNode) { + if (!outerNode || !innerNode) { + return false; + } else if (outerNode === innerNode) { + return true; + } else if (isTextNode(outerNode)) { + return false; + } else if (isTextNode(innerNode)) { + return containsNode(outerNode, innerNode.parentNode); + } else if ('contains' in outerNode) { + return outerNode.contains(innerNode); + } else if (outerNode.compareDocumentPosition) { + return !!(outerNode.compareDocumentPosition(innerNode) & 16); + } else { + return false; + } +} + +module.exports = containsNode; +},{"./isTextNode":18}],6:[function(require,module,exports){ +(function (process){ +'use strict'; + +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @typechecks + */ + +var invariant = require('./invariant'); + +/** + * Convert array-like objects to arrays. + * + * This API assumes the caller knows the contents of the data type. For less + * well defined inputs use createArrayFromMixed. + * + * @param {object|function|filelist} obj + * @return {array} + */ +function toArray(obj) { + var length = obj.length; + + // Some browsers builtin objects can report typeof 'function' (e.g. NodeList + // in old versions of Safari). + !(!Array.isArray(obj) && (typeof obj === 'object' || typeof obj === 'function')) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'toArray: Array-like object expected') : invariant(false) : void 0; + + !(typeof length === 'number') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'toArray: Object needs a length property') : invariant(false) : void 0; + + !(length === 0 || length - 1 in obj) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'toArray: Object should have keys for indices') : invariant(false) : void 0; + + !(typeof obj.callee !== 'function') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'toArray: Object can\'t be `arguments`. Use rest params ' + '(function(...args) {}) or Array.from() instead.') : invariant(false) : void 0; + + // Old IE doesn't give collections access to hasOwnProperty. Assume inputs + // without method will throw during the slice call and skip straight to the + // fallback. + if (obj.hasOwnProperty) { + try { + return Array.prototype.slice.call(obj); + } catch (e) { + // IE < 9 does not support Array#slice on collections objects + } + } + + // Fall back to copying key by key. This assumes all keys have a value, + // so will not preserve sparsely populated inputs. + var ret = Array(length); + for (var ii = 0; ii < length; ii++) { + ret[ii] = obj[ii]; + } + return ret; +} + +/** + * Perform a heuristic test to determine if an object is "array-like". + * + * A monk asked Joshu, a Zen master, "Has a dog Buddha nature?" + * Joshu replied: "Mu." + * + * This function determines if its argument has "array nature": it returns + * true if the argument is an actual array, an `arguments' object, or an + * HTMLCollection (e.g. node.childNodes or node.getElementsByTagName()). + * + * It will return false for other array-like objects like Filelist. + * + * @param {*} obj + * @return {boolean} + */ +function hasArrayNature(obj) { + return( + // not null/false + !!obj && ( + // arrays are objects, NodeLists are functions in Safari + typeof obj == 'object' || typeof obj == 'function') && + // quacks like an array + 'length' in obj && + // not window + !('setInterval' in obj) && + // no DOM node should be considered an array-like + // a 'select' element has 'length' and 'item' properties on IE8 + typeof obj.nodeType != 'number' && ( + // a real array + Array.isArray(obj) || + // arguments + 'callee' in obj || + // HTMLCollection/NodeList + 'item' in obj) + ); +} + +/** + * Ensure that the argument is an array by wrapping it in an array if it is not. + * Creates a copy of the argument if it is already an array. + * + * This is mostly useful idiomatically: + * + * var createArrayFromMixed = require('createArrayFromMixed'); + * + * function takesOneOrMoreThings(things) { + * things = createArrayFromMixed(things); + * ... + * } + * + * This allows you to treat `things' as an array, but accept scalars in the API. + * + * If you need to convert an array-like object, like `arguments`, into an array + * use toArray instead. + * + * @param {*} obj + * @return {array} + */ +function createArrayFromMixed(obj) { + if (!hasArrayNature(obj)) { + return [obj]; + } else if (Array.isArray(obj)) { + return obj.slice(); + } else { + return toArray(obj); + } +} + +module.exports = createArrayFromMixed; +}).call(this,require('_process')) +},{"./invariant":16,"_process":27}],7:[function(require,module,exports){ +(function (process){ +'use strict'; + +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @typechecks + */ + +/*eslint-disable fb-www/unsafe-html*/ + +var ExecutionEnvironment = require('./ExecutionEnvironment'); + +var createArrayFromMixed = require('./createArrayFromMixed'); +var getMarkupWrap = require('./getMarkupWrap'); +var invariant = require('./invariant'); + +/** + * Dummy container used to render all markup. + */ +var dummyNode = ExecutionEnvironment.canUseDOM ? document.createElement('div') : null; + +/** + * Pattern used by `getNodeName`. + */ +var nodeNamePattern = /^\s*<(\w+)/; + +/** + * Extracts the `nodeName` of the first element in a string of markup. + * + * @param {string} markup String of markup. + * @return {?string} Node name of the supplied markup. + */ +function getNodeName(markup) { + var nodeNameMatch = markup.match(nodeNamePattern); + return nodeNameMatch && nodeNameMatch[1].toLowerCase(); +} + +/** + * Creates an array containing the nodes rendered from the supplied markup. The + * optionally supplied `handleScript` function will be invoked once for each + * + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..7898ac8 --- /dev/null +++ b/package.json @@ -0,0 +1,53 @@ +{ + "name": "cuttlefish", + "version": "0.0.2", + "description": "Human-friendly HTML rendering of machine-friendly JSON-LD. We believe in an open Internet of Things.", + "main": "dist/cuttlefish.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "generate-source": "cd react_generator_js/ && node writer.js", + "build": "gulp build", + "build-dist": "gulp build-dist", + "all-of-it": "npm run generate-source && npm run build && npm run build-dist" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/reelyactive/cuttlefish.git" + }, + "keywords": [ + "JSON-LD", + "cuttlefish", + "ReactJS", + "React", + "Schema.org" + ], + "author": "Adrien Thiery", + "license": "MIT", + "bugs": { + "url": "https://github.com/reelyactive/cuttlefish/issues" + }, + "homepage": "https://github.com/reelyactive/cuttlefish#readme", + "devDependencies": { + "babel-preset-es2015": "^6.9.0", + "babel-preset-react": "^6.5.0", + "browserify": "^13.0.1", + "coffee-script": "^1.10.0", + "del": "^2.2.0", + "gulp": "^3.9.1", + "gulp-babel": "^6.1.2", + "gulp-concat": "^2.6.0", + "gulp-debug": "^2.1.2", + "gulp-plumber": "^1.1.0", + "gulp-rename": "^1.2.2", + "gulp-uglify": "^1.5.2", + "path": "^0.12.7", + "require-dir": "^0.3.0", + "run-sequence": "^1.2.1", + "vinyl-paths": "^2.1.0", + "vinyl-source-stream": "^1.1.0" + }, + "dependencies": { + "react": "^15.1.0", + "react-dom": "^15.1.0" + } +} diff --git a/react_generator_js/ES6ClassSnippet.js b/react_generator_js/ES6ClassSnippet.js new file mode 100644 index 0000000..b374a28 --- /dev/null +++ b/react_generator_js/ES6ClassSnippet.js @@ -0,0 +1,19 @@ +%imports% + +import React, { + Component +} from 'react'; + +export default class %className% extends Component { + %constructor% + %componentWillMount% + %componentWillUnmount% + %componentWillReceiveProps% + %componentWillUpdate% + %render% +}; + +%className%.propTypes = { + %propTypes% +}; + diff --git a/react_generator_js/parser.js b/react_generator_js/parser.js new file mode 100644 index 0000000..5b3011f --- /dev/null +++ b/react_generator_js/parser.js @@ -0,0 +1,81 @@ +var fs = require('fs'); +var _ = require('lodash'); + +function stripKey(key) { + return key.substr(key.indexOf(':') + 1); +} + +function extractType(value) { + if ( + (/\.(gif|jpg|jpeg|tiff|png)$/i).test(value) + ) { + return 'image'; + } else if ( + (/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi).test(value) + ) { + return 'url'; + } else { + return typeof value; + } +} + +function extractComponent(data) { + var wrapper = { + id: '', + className: '', + properties: {} + }; + + _.forEach(data, function(value, key) { + if (key === '@id') { + wrapper.id = value; + } else if (key === '@type') { + wrapper.className = stripKey(value); + } else { + if (typeof value === 'object') { + if (!value.length) { + var subComponentWrapped = extractComponent(value); + components[subComponentWrapped.className] = subComponentWrapped; + wrapper.properties[stripKey(key)] = subComponentWrapped.className; + } else { + var type = extractType(value[0]); + wrapper.properties[stripKey(key)] = 'arrayOf(' + type + ')'; + } + } else { + wrapper.properties[stripKey(key)] = extractType(value); + } + } + }); + + return wrapper; +} + +function extractComponents(data) { + var prefixes = []; + + _.forEach(data, function(value, key) { + if (key === '@context') { + prefixes = Object.keys(value); + } else if (key === '@graph') { + var baseComponents = value; + + _.forEach(baseComponents, function(baseComponent) { + var component = extractComponent(baseComponent); + components[component.className] = component; + }); + } else { + console.log('Weird, first level should only have @context or @graph'); + } + }); + + return components; +} + +var parsedData = JSON.parse( + fs.readFileSync('../source.json', 'utf8') +); + +var components = {}; +extractComponents(parsedData) + +module.exports = components; diff --git a/react_generator_js/writer.js b/react_generator_js/writer.js new file mode 100644 index 0000000..4eee91d --- /dev/null +++ b/react_generator_js/writer.js @@ -0,0 +1,206 @@ +var fs = require('fs'); +var _ = require('lodash'); + +function isComponent(type) { + return type[0] === type[0].toUpperCase(); +} + +function capitalize(word, isSameAsDisplayName) { + return word[0].toUpperCase() + word.substr(1); +} + +function write(file, something) { + // console.log('+ ' + something); // For debugging + file.write(something + '\n'); +} + +function getTemplateWord(line) { + var matches = line.match(/.*%([^%]+)%.*/); + + if (matches) { + return matches[1]; + } else { + return null; + } +} + +function getSubTypeOfArray(arrayType) { + return arrayType.substr( + arrayType.indexOf('arrayOf(') + 8, + arrayType.length - 8 - 1 // 8: length of 'arrayOf(', 1: length of the ')' at the end + ); +} + +function getReactType(type) { + if (type.indexOf('arrayOf(') === 0) { + var subType = getReactType(getSubTypeOfArray(type)); + type = 'array'; + } + + switch(type) { + case 'url': + case 'image': + return 'React.PropTypes.string'; + case 'array': + return 'React.PropTypes.arrayOf(' + subType + ')'; + case 'boolean': + return 'React.PropTypes.bool'; + default: + return 'React.PropTypes.' + type; + } +} + +// *value* is a bit weird. It can be either *this.props.something* or *item* +function getRepresentationOf(component, value, property, type, subType) { + var representation = '('; + var extraAttribute = ''; + if (value === 'item') { + extraAttribute = 'key={index} '; + } + + switch(type) { + case 'component': + representation += "<" + subType + + " {..." + value + "} " + + extraAttribute + "/>"; + break; + case 'url': + representation += + "" + + capitalize(property) + + ""; + break; + case 'image': + representation += + ""; + break; + case 'string': + case 'number': + representation += + "

" + + capitalize(property) + ": {" + value + "}" + + "

"; + break; + case 'array': + representation += value + '\n ? ' + value + '.map(function(item, index) {\n'; + representation += ' return ' + getRepresentationOf(component, 'item', property, subType) + '\n'; + representation += ' })\n : null\n '; + break; + } + + representation += ');'; + + return representation; +} + +function writeRenderPreparation(file, component, property, type) { + if (type.indexOf('arrayOf(') === 0) { + var subType = getSubTypeOfArray(type); + type = 'array'; + } else if (isComponent(type)) { + subType = type; + type = 'component'; + } + + var value = 'this.props.' + property; + write(file, ' var ' + property + ';'); + write(file, ' if (this.props.' + property + ') {'); + write(file, ' ' + property + ' = ' + getRepresentationOf( + component, + value, + property, + type, + subType + )); + write(file, ' }'); + write(file, ''); +} + +function writeForTemplateLine(file, templateLine, component) { + switch (getTemplateWord(templateLine)) { + case null: // No template word in line + write(file, templateLine); + break; + case 'imports': + _.forEach(component.properties, function(type, property) { + if (isComponent(type)) { + /* write( + file, + "import " + type + " from './" + type + "';" + ); */ + // Do nothing, everything is compiled in one file. + } + }); + break; + case 'className': + write( + file, + templateLine.replace('%className%', component.className) + ); + break; + case 'propTypes': + _.forEach(component.properties, function(type, property) { + if (!isComponent(type)) { + write( + file, + " " + property + ": " + getReactType(type) + "," + ); + } else { + // Let's not put a propType on components + } + }); + break; + case 'render': + write(file, " render() {"); + _.forEach(component.properties, function(type, property) { + writeRenderPreparation(file, component, property, type); + }); + write(file, " return ("); + write(file, "
"); + _.forEach(component.properties, function(type, property) { + write(file, " {" + property + "}"); + }); + write(file, "
"); + write(file, " );"); + write(file, " }"); + break; + default: + break; + } +} + +var snippetAsArray = fs.readFileSync('./ES6ClassSnippet.js').toString().split('\n'); + +function writeComponent(component) { + var file = fs.createWriteStream( + '../src/es6/' + component.className + '.js' + ); + + console.log('--------------- file start [' + component.className + ']'); + + snippetAsArray.forEach(function (line) { + writeForTemplateLine(file, line, component); + }); + + console.log('--------------- file end [' + component.className + ']'); + + file.end(); +} + +var components = require('./parser'); + +_.forEach(components, function(component, className) { + writeComponent(component); +}); diff --git a/react_generator_python/ES6ClassSnippet.js b/react_generator_python/ES6ClassSnippet.js new file mode 100644 index 0000000..edff4e7 --- /dev/null +++ b/react_generator_python/ES6ClassSnippet.js @@ -0,0 +1,23 @@ +%imports% + +import React, { + Component +} from 'react'; + +export default class %className% extends Component { + %constructor% + %componentWillMount% + %componentWillUnmount% + %componentWillReceiveProps% + %componentWillUpdate% + %render% +}; + +%className%.defaultProps = { + %defaultProps% +}; + +%className%.propTypes = { + %propTypes% +}; + diff --git a/react_generator_python/component_extractor.py b/react_generator_python/component_extractor.py new file mode 100644 index 0000000..6484847 --- /dev/null +++ b/react_generator_python/component_extractor.py @@ -0,0 +1,48 @@ +from pattern.web import * +import os, re +import os.path as path +from config import * +from react_writer import ReactWriter +from helpers import Helpers + +class ComponentExtractor: + def __init__(self, componentName): + filename = path.join(outputPath, componentName.lower() + '.js') + if not path.isfile(filename) and Helpers.isEntity(componentName): + if not Helpers.isEntity(componentName): + print "XXX YOU SHOULDN'T BE HERE, %s is an 'End' component" % componentName + print "Fetching %s (%s%s)..." % (componentName, schemaOrgUrl, componentName) + urlObject = URL(schemaOrgUrl + componentName) + html = urlObject.download(cached=True) + dom = DOM(html) + + title = dom('h1.page-title')[0] + comment = dom('div[property=rdfs:comment]')[0] + outputTopComments = "/* " + plaintext("%s" % title) + " - " + plaintext("%s" % comment) + ". Generated automatically by the reactGenerator. */" + + table = dom('tbody.supertype') + properties = {} + dependencies = {} + for tbody in table: + for line in tbody('tr'): + if len(line('.prop-ect')) > 0: + typeElement = line('.prop-ect')[0] + typeText = plaintext("%s" % typeElement) + if Helpers.isEntity(typeText): + arrayOfDependencies = re.findall(r"(\w{2,}).{0,2}", typeText) + properties[ plaintext("%s" % line('.prop-nam')[0]) ] = arrayOfDependencies + for dependency in arrayOfDependencies: + if Helpers.isEntity(dependency): + dependencies[ dependency ] = './' + dependency.lower() + else: + properties[ plaintext("%s" % line('.prop-nam')[0]) ] = [ typeText ] + ReactWriter(componentName, outputTopComments, dependencies, properties) + for (dependencyName, fileName) in dependencies.iteritems(): + ComponentExtractor(dependencyName) + print "Done." + else: + print "Sorry, %s is already treated" % componentName + +ComponentExtractor('Person') +ComponentExtractor('Place') +ComponentExtractor('Product') diff --git a/react_generator_python/config.py b/react_generator_python/config.py new file mode 100644 index 0000000..1754883 --- /dev/null +++ b/react_generator_python/config.py @@ -0,0 +1,5 @@ +import os.path as path + +outputPath = path.join(path.dirname(path.realpath(__file__)), '../src/es6') +schemaOrgUrl = 'https://schema.org/' + diff --git a/react_generator_python/helpers.py b/react_generator_python/helpers.py new file mode 100644 index 0000000..3e48d4c --- /dev/null +++ b/react_generator_python/helpers.py @@ -0,0 +1,71 @@ +class Helpers: + @classmethod + def safe_unicode(klass, obj, * args): + """ return the unicode representation of obj """ + try: + return unicode(obj, * args) + except UnicodeDecodeError: + # obj is byte string + ascii_text = str(obj).encode('string_escape') + return unicode(ascii_text) + + @classmethod + def safe_str(klass, obj): + """ return the byte string representation of obj """ + try: + return str(obj) + except UnicodeEncodeError: + # obj is unicode + return unicode(obj).encode('unicode_escape') + + @classmethod + def isEntity(klass, typeText): + return ( + typeText != u'Date' and + typeText != u'DateTime' and + typeText != u'Text' and + typeText != u'URL' and + typeText != u'Number' and + typeText != u'Integer' and + typeText != u'Boolean' + ) + + @classmethod + def getNonEntityRepresentation(klass, typeText, value, componentName): + className = "%s--%s" % (componentName, value) + if typeText == u'Date' or typeText == u'DateTime': + return '' % (className, value, value) + elif typeText == u'Text': + return '

%s: {this.props.%s}

' % (className, value, value) + elif value == u'image': + return '' % (className, value) + elif typeText == u'URL': + return '%s' % (className, value, value) + elif typeText == u'Number': + return '

%s: {this.props.%s}

' % (className, value, value) + elif typeText == u'Integer': + return '

%s: {this.props.%s}

' % (className, value, value) + elif typeText == u'boolean': + return '

%s: {this.props.%s}

' % (className, value, value) + else: + return '
%s: {this.props.%s}
' % (className, value, value) + + @classmethod + def typeOfNonEntity(klass, typeText): + if typeText == u'Date': + return 'string' + if typeText == u'DateTime': + return 'string' + elif typeText == u'Text': + return 'string' + elif typeText == u'URL': + return 'string' + elif typeText == u'Number': + return 'number' + elif typeText == u'Integer': + return 'number' + elif typeText == u'boolean': + return 'bool' + else: + return 'string' + diff --git a/react_generator_python/react_writer.py b/react_generator_python/react_writer.py new file mode 100644 index 0000000..c79bcca --- /dev/null +++ b/react_generator_python/react_writer.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +from config import * +from helpers import Helpers +import os +import os.path as path + +class ReactWriter: + def __init__(self, componentName, topComments, dependencies, properties, defaultProps = None): + self.componentName = componentName + if not path.exists(outputPath): + os.makedirs(outputPath) + + self.lines = [] + self.lines.append( topComments ) + + self.useES6Snippet(dependencies, properties) + + self.writeLines() + + def useES6Snippet(self, dependencies, properties): + with open( + path.join( + path.dirname(path.realpath(__file__)), + 'ES6ClassSnippet.js' + ) + ) as snippet: + for line in snippet: + if '%imports%' in line: + # self.writeImports(dependencies) + pass + elif '%className%' in line: + addline = line.replace('%className%', self.componentName) + self.lines.append(addline) + elif '%render%' in line: + self.writeRender(properties) + elif '%propTypes%' in line: + self.writePropTypes(properties) + elif '%' in line: + pass + else: + self.lines.append(line) + # Add other snippets parts here + + def writeImports(self, dependencies): + for dependency in dependencies: + if dependency != self.componentName: + self.lines.append("\nimport %s from './%s.js';" % (dependency, dependency.lower())) + self.lines.append("\n\n") + + def writeRender(self, properties): + render = [ + " render() {\n" + ] + + for (propertyName, typeNameArray) in properties.iteritems(): + if propertyName == 'sameAs': + displayName = 'link' + else: + displayName = propertyName + renderProperty = [ + " let %s;\n" % propertyName, + " if (this.props.%s) {\n" % propertyName, + " if (this.props.%s instanceof Array) {\n" % propertyName, + " %s = (\n" % propertyName, + "
\n" % propertyName, + "
%ss
\n" % (displayName, propertyName), + " {this.props.%s.map((item, index) => {\n" % propertyName + ] + renderProperty += self.getPropertyReturn(propertyName, typeNameArray, 'return') + renderProperty += [ + " })}\n", + "
\n" % propertyName, + "
\n", + " );\n", + " } else {\n" + ] + renderProperty += self.getPropertyReturn(propertyName, typeNameArray, '=') + renderProperty += [ + " }\n", + " }\n\n", + ] + render += renderProperty + + render = render + [ + " return (\n", + "
\n" % self.componentName + ] + + for (propertyName, typeName) in properties.iteritems(): + render.append( + " {%s}\n" % propertyName + ) + + render = render + [ + "
\n", + " );\n", + " }\n" + ] + + self.lines = self.lines + render + + def getPropertyReturn(self, propertyName, typeNameArray, returnOrEqual): + if returnOrEqual == '=': + returnOrEqual = '%s =' % propertyName + tabs = '' + else: + tabs = ' ' + if len(typeNameArray) > 1: + propertyReturn = [] + number = 0 + for typeName in typeNameArray: + if number == 0: + propertyReturn.append( + " %sif (this.props['@type'] === '%s') {\n" % (tabs, typeName) + ) + elif number == len(typeNameArray): + propertyReturn.append( + " %selse {\n" % tabs + ) + else: + propertyReturn.append( + " %selse if (this.props['@type'] === '%s') {\n" % (tabs, typeName) + ) + number += 1 + propertyReturn += [ + " %s%s %s;\n" % (tabs, returnOrEqual, self.getPropertyRepresentation(propertyName, typeName, True)), + " %s}\n" % tabs + ] + else: + propertyReturn = [ + " %s%s %s;\n" % (tabs, returnOrEqual, self.getPropertyRepresentation(propertyName, typeNameArray[0], True, True)), + ] + return propertyReturn + + def writePropTypes(self, properties): + for (propertyName, typeName) in properties.iteritems(): + if Helpers.isEntity(typeName): + self.lines.append(" %s: React.PropTypes.object,\n" % propertyName) + else: + self.lines.append(" %s: React.PropTypes.%s,\n" % (propertyName, Helpers.typeOfNonEntity(typeName))) + + def getPropertyRepresentation(self, propertyName, typeName, isElementOfArray=False, isElse=False): + key = '' + item = '' + if isElementOfArray: + key = 'key={index}' + item = '{item}' + if isElse: + key = '' + if Helpers.isEntity(typeName) and ' ' not in typeName: + return "(<%s %s {...this.props.%s} />)" % (typeName, key ,propertyName) + else: + return "(
%s
)" % (propertyName, propertyName, typeName.replace('\n', ' '), Helpers.getNonEntityRepresentation(typeName, propertyName, self.componentName)) + + def writeLines(self): + safeLines = [] + + for line in self.lines: + safeLines.append( Helpers.safe_unicode( Helpers.safe_str( line ) ) ) + + pathOfNewFile = path.join(outputPath, self.componentName.lower() + '.js') + print "Writing %s ..." % pathOfNewFile + + f = open( pathOfNewFile, 'w' ) + f.writelines( safeLines ) + f.close() + + print "Done writing %s.js" % self.componentName.lower() + diff --git a/source.json b/source.json new file mode 100644 index 0000000..0c7a2d8 --- /dev/null +++ b/source.json @@ -0,0 +1,57 @@ +{ + "@context": { + "schema": "http://schema.org/" + }, + "@graph": [ + { + "@id": "person", + "@type": "schema:Person", + "schema:givenName": "Name", + "schema:familyName": "Family", + "schema:gender": "Male", + "schema:nationality": "CA", + "schema:worksFor": "ReelyActive", + "schema:jobTitle": "Mascot", + "schema:url": "http://context.reelyactive.com/", + "schema:sameAs": [ + "https://twitter.com/reelyActive", + "https://www.facebook.com/reelyActive", + "http://reelyactive.github.io/", + "http://www.linkedin.com/company/reelyactive" + ], + "schema:image": "http://reelyactive.com/images/barnowl.jpg" + }, + { + "@id": "product", + "@type": "schema:Product", + "schema:name": "Nexus 5", + "schema:manufacturer": { + "@type": "schema:Organization", + "schema:name": "LG Electronics" + }, + "schema:model": "D82x", + "schema:url": "http://www.google.com/nexus/5/", + "schema:image": "http://reelyactive.com/images/Nexus5.jpg" + }, + { + "@id": "place", + "@type": "schema:Place", + "schema:name": "Notman", + "schema:address": { + "@type": "schema:PostalAddress", + "schema:streetAddress": "51 rue sherbrooke ouest", + "schema:addressLocality": "Montréal", + "schema:addressRegion": "Québec", + "schema:postalCode": "H2X 1X2", + "schema:addressCountry": "CA" + }, + "schema:url": "http://notman.org", + "schema:image": "http://reelyactive.com/images/json-silo.jpg", + "schema:logo": "http://reelyactive.org/images/notman.png", + "schema:sameAs": [ + "https://twitter.com/notman", + "https://www.facebook.com/NotmanHouse/" + ] + } + ] +} diff --git a/src/es6/Organization.js b/src/es6/Organization.js new file mode 100644 index 0000000..7e97192 --- /dev/null +++ b/src/es6/Organization.js @@ -0,0 +1,25 @@ + +import React, { + Component +} from 'react'; + +export default class Organization extends Component { + render() { + var name; + if (this.props.name) { + name = (

Name: {this.props.name}

); + } + + return ( +
+ {name} +
+ ); + } +}; + +Organization.propTypes = { + name: React.PropTypes.string, +}; + + diff --git a/src/es6/Person.js b/src/es6/Person.js new file mode 100644 index 0000000..48b26fc --- /dev/null +++ b/src/es6/Person.js @@ -0,0 +1,86 @@ + +import React, { + Component +} from 'react'; + +export default class Person extends Component { + render() { + var givenName; + if (this.props.givenName) { + givenName = (

GivenName: {this.props.givenName}

); + } + + var familyName; + if (this.props.familyName) { + familyName = (

FamilyName: {this.props.familyName}

); + } + + var gender; + if (this.props.gender) { + gender = (

Gender: {this.props.gender}

); + } + + var nationality; + if (this.props.nationality) { + nationality = (

Nationality: {this.props.nationality}

); + } + + var worksFor; + if (this.props.worksFor) { + worksFor = (

WorksFor: {this.props.worksFor}

); + } + + var jobTitle; + if (this.props.jobTitle) { + jobTitle = (

JobTitle: {this.props.jobTitle}

); + } + + var url; + if (this.props.url) { + url = (Url); + } + + var sameAs; + if (this.props.sameAs) { + sameAs = (this.props.sameAs + ? this.props.sameAs.map(function(item, index) { + return (SameAs); + }) + : null + ); + } + + var image; + if (this.props.image) { + image = (Image); + } + + return ( +
+ {givenName} + {familyName} + {gender} + {nationality} + {worksFor} + {jobTitle} + {url} + {sameAs} + {image} +
+ ); + } +}; + +Person.propTypes = { + givenName: React.PropTypes.string, + familyName: React.PropTypes.string, + gender: React.PropTypes.string, + nationality: React.PropTypes.string, + worksFor: React.PropTypes.string, + jobTitle: React.PropTypes.string, + url: React.PropTypes.string, + sameAs: React.PropTypes.arrayOf(React.PropTypes.string), + image: React.PropTypes.string, +}; + + diff --git a/src/es6/Place.js b/src/es6/Place.js new file mode 100644 index 0000000..16c504d --- /dev/null +++ b/src/es6/Place.js @@ -0,0 +1,64 @@ + +import React, { + Component +} from 'react'; + +export default class Place extends Component { + render() { + var name; + if (this.props.name) { + name = (

Name: {this.props.name}

); + } + + var address; + if (this.props.address) { + address = (); + } + + var url; + if (this.props.url) { + url = (Url); + } + + var image; + if (this.props.image) { + image = (Image); + } + + var logo; + if (this.props.logo) { + logo = (Logo); + } + + var sameAs; + if (this.props.sameAs) { + sameAs = (this.props.sameAs + ? this.props.sameAs.map(function(item, index) { + return (SameAs); + }) + : null + ); + } + + return ( +
+ {name} + {address} + {url} + {image} + {logo} + {sameAs} +
+ ); + } +}; + +Place.propTypes = { + name: React.PropTypes.string, + url: React.PropTypes.string, + image: React.PropTypes.string, + logo: React.PropTypes.string, + sameAs: React.PropTypes.arrayOf(React.PropTypes.string), +}; + + diff --git a/src/es6/PostalAddress.js b/src/es6/PostalAddress.js new file mode 100644 index 0000000..18ef69d --- /dev/null +++ b/src/es6/PostalAddress.js @@ -0,0 +1,53 @@ + +import React, { + Component +} from 'react'; + +export default class PostalAddress extends Component { + render() { + var streetAddress; + if (this.props.streetAddress) { + streetAddress = (

StreetAddress: {this.props.streetAddress}

); + } + + var addressLocality; + if (this.props.addressLocality) { + addressLocality = (

AddressLocality: {this.props.addressLocality}

); + } + + var addressRegion; + if (this.props.addressRegion) { + addressRegion = (

AddressRegion: {this.props.addressRegion}

); + } + + var postalCode; + if (this.props.postalCode) { + postalCode = (

PostalCode: {this.props.postalCode}

); + } + + var addressCountry; + if (this.props.addressCountry) { + addressCountry = (

AddressCountry: {this.props.addressCountry}

); + } + + return ( +
+ {streetAddress} + {addressLocality} + {addressRegion} + {postalCode} + {addressCountry} +
+ ); + } +}; + +PostalAddress.propTypes = { + streetAddress: React.PropTypes.string, + addressLocality: React.PropTypes.string, + addressRegion: React.PropTypes.string, + postalCode: React.PropTypes.string, + addressCountry: React.PropTypes.string, +}; + + diff --git a/src/es6/Product.js b/src/es6/Product.js new file mode 100644 index 0000000..e2edae3 --- /dev/null +++ b/src/es6/Product.js @@ -0,0 +1,52 @@ + +import React, { + Component +} from 'react'; + +export default class Product extends Component { + render() { + var name; + if (this.props.name) { + name = (

Name: {this.props.name}

); + } + + var manufacturer; + if (this.props.manufacturer) { + manufacturer = (); + } + + var model; + if (this.props.model) { + model = (

Model: {this.props.model}

); + } + + var url; + if (this.props.url) { + url = (Url); + } + + var image; + if (this.props.image) { + image = (Image); + } + + return ( +
+ {name} + {manufacturer} + {model} + {url} + {image} +
+ ); + } +}; + +Product.propTypes = { + name: React.PropTypes.string, + model: React.PropTypes.string, + url: React.PropTypes.string, + image: React.PropTypes.string, +}; + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..f3a3434 --- /dev/null +++ b/src/main.js @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +window.cuttlefish = { + // Get data as argument as an object + // This file should know automatically which react component to send back + represent: function (jsonld_data, nodeID) { + var component = eval(jsonld_data['@type']); + ReactDOM.render( + React.createElement(component, jsonld_data), + document.getElementById(nodeID) + ); + } +}; diff --git a/tasks/babel.coffee b/tasks/babel.coffee new file mode 100644 index 0000000..3be5f6a --- /dev/null +++ b/tasks/babel.coffee @@ -0,0 +1,19 @@ +gulp = require 'gulp' +babel = require 'gulp-babel' +plumber = require 'gulp-plumber' +concat = require 'gulp-concat' + +gulp.task 'babel', -> + files = [ + './src/es6/*.js', + './src/main.js' + ] + gulp.src files + .pipe plumber() + .pipe babel + presets: [ + 'es2015', + 'react' + ] + .pipe concat 'app.js' + .pipe gulp.dest './src/' diff --git a/tasks/browserify.coffee b/tasks/browserify.coffee new file mode 100644 index 0000000..1d7f679 --- /dev/null +++ b/tasks/browserify.coffee @@ -0,0 +1,9 @@ +gulp = require 'gulp' +browserify = require 'browserify' +source = require 'vinyl-source-stream' + +gulp.task 'browserify', -> + return browserify './src/app.js' + .bundle() + .pipe(source('cuttlefish.js')) + .pipe(gulp.dest('./dist/')) diff --git a/tasks/clean.coffee b/tasks/clean.coffee new file mode 100644 index 0000000..0d1d3f8 --- /dev/null +++ b/tasks/clean.coffee @@ -0,0 +1,16 @@ +gulp = require 'gulp' + +vinylPaths = require 'vinyl-paths' +del = require 'del' + +# Clean the served directory +gulp.task 'clean', -> + gulp.src './dist/*.js' + .pipe vinylPaths(del) + +gulp.task 'clean-end', -> + gulp.src [ + './src/components.js', + './src/app.js' + ] + .pipe vinylPaths(del) diff --git a/tasks/concat.coffee b/tasks/concat.coffee new file mode 100644 index 0000000..10f9555 --- /dev/null +++ b/tasks/concat.coffee @@ -0,0 +1,15 @@ +gulp = require 'gulp' + +concat = require 'gulp-concat' +debug = require 'gulp-debug' +path = require 'path' +plumber = require 'gulp-plumber' + +gulp.task 'concat', -> + files = [ + path.join('./src/*.js') + ] + gulp.src files + .pipe plumber() + .pipe concat('app.js') + .pipe gulp.dest './src/' diff --git a/tasks/minify.coffee b/tasks/minify.coffee new file mode 100644 index 0000000..bcb3bde --- /dev/null +++ b/tasks/minify.coffee @@ -0,0 +1,10 @@ +gulp = require 'gulp' + +uglify = require 'gulp-uglify' +rename = require 'gulp-rename' + +gulp.task 'minify', -> + gulp.src "./dist/cuttlefish.js" + .pipe uglify() + .pipe rename("cuttlefish.min.js") + .pipe gulp.dest "./dist/"