├── .babelrc ├── .eslintrc ├── .gitignore ├── README.md ├── package.json └── src ├── actions └── index.js ├── containers └── App.js ├── index.html ├── index.js ├── lovutils.js ├── reducers └── index.js └── standalone.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-runtime", "transform-strict-mode"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "browser": true 6 | }, 7 | "ecmaFeatures": { 8 | "jsx": true, 9 | "experimentalObjectRestSpread": true 10 | }, 11 | "plugins": [ 12 | "react" 13 | ], 14 | "extends": "eslint:recommended", 15 | "rules": { 16 | "comma-dangle": 0, 17 | "curly": [2, "multi-line"], 18 | "eol-last": 2, 19 | "jsx-quotes": 2, 20 | "no-console": 1, 21 | "no-fallthrough": 0, 22 | "no-multiple-empty-lines": [2, {"max": 1}], 23 | "no-trailing-spaces": [2], 24 | "no-use-before-define": [2, "nofunc"], 25 | "semi": [2, "never"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | lib/ 3 | node_modules/ 4 | standalone/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | An example of using [react-jsonld-editor](https://github.com/editorsnotes/react-jsonld-editor) with dynamically loaded vocabuaries from [Linked Open Vocabularies](http://lov.okfn.org/dataset/lov/): 2 | 3 | 1. First, add some vocabularies by typing their names into the autocompleting input. 4 | 1. Then, use the classes and properties from those vocabularies to create a valid JSON-LD object. 5 | 1. When you're done, save it to a file. 6 | 7 | [Try it out.](https://editorsnotes.github.io/edit-with-lov/) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edit-with-lov", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Demo of editing JSON-LD with LOV vocabs", 6 | "main": "lib/index.js", 7 | "files": "lib", 8 | "scripts": { 9 | "lint": "eslint src", 10 | "transpile": "babel src -d lib --source-maps inline", 11 | "prebuild": "npm -s run lint", 12 | "build": "npm -s run transpile", 13 | "precss": "mkdir -p lib", 14 | "css": "cat node_modules/react-jsonld-editor/src/style.css node_modules/highlight.js/styles/default.css > lib/style.css", 15 | "predevelop": "npm -s run css", 16 | "develop": "budo src/standalone.js --css lib/style.css --live -- -t [ babelify --sourceMaps inline ]", 17 | "prestandalone": "npm -s run build && npm -s run css && mkdir -p standalone", 18 | "standalone": "NODE_ENV=production browserify lib/standalone.js | uglifyjs -c > standalone/bundle.js", 19 | "poststandalone": "cp src/index.html lib/style.css standalone && cd standalone && zip ../standalone.zip *" 20 | }, 21 | "author": "Ryan Shaw", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "babel-cli": "^6.1.2", 25 | "babel-plugin-transform-runtime": "^6.1.2", 26 | "babel-plugin-transform-strict-mode": "^6.8.0", 27 | "babel-preset-es2015": "^6.1.2", 28 | "babelify": "^7.2.0", 29 | "budo": "^8.0.4", 30 | "eslint": "^2.12.0", 31 | "uglify": "^0.1.5" 32 | }, 33 | "dependencies": { 34 | "file-saver": "^1.3.3", 35 | "highland": "^2.8.1", 36 | "highlight.js": "^9.7.0", 37 | "immutable": "^3.8.1", 38 | "n3": "^0.4.5", 39 | "rdf-ns": "0.0.2", 40 | "react": "^15.2.1", 41 | "react-dom": "^15.2.1", 42 | "react-hyperscript": "^2.4.0", 43 | "react-jsonld-editor": "^5.1.2", 44 | "react-lowlight": "^1.0.2", 45 | "react-redux": "^4.4.5", 46 | "rebass": "^0.4.0-beta.8", 47 | "redux": "^3.5.2", 48 | "redux-thunk": "^2.1.0", 49 | "reselect": "^2.5.3" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | const {getVocabularies, getVocabulary} = require('../lovutils') 2 | 3 | const UPDATE_INPUT = 'UPDATE_INPUT' 4 | const UPDATE_SUGGESTIONS = 'UPDATE_SUGGESTIONS' 5 | const REQUEST_VOCABS = 'REQUEST_VOCABS' 6 | const RECEIVE_VOCABS = 'RECEIVE_VOCABS' 7 | const REQUEST_VOCAB = 'REQUEST_VOCAB' 8 | const RECEIVE_VOCAB = 'RECEIVE_VOCAB' 9 | const RECEIVE_ERROR = 'RECEIVE_ERROR' 10 | const UPDATE_NODE = 'UPDATE_NODE' 11 | 12 | const updateInput = input => ( 13 | { type: UPDATE_INPUT 14 | , input 15 | } 16 | ) 17 | 18 | const updateSuggestions = suggestions => ( 19 | { type: UPDATE_SUGGESTIONS 20 | , suggestions 21 | } 22 | ) 23 | 24 | const requestVocabs = () => ( 25 | { type: REQUEST_VOCABS } 26 | ) 27 | 28 | const receiveVocabs = list => ( 29 | { type: RECEIVE_VOCABS 30 | , list 31 | , receivedAt: Date.now() 32 | } 33 | ) 34 | 35 | const requestVocab = vocab => ( 36 | { type: REQUEST_VOCAB 37 | , vocab 38 | } 39 | ) 40 | 41 | const receiveVocab = (vocab, {info, classes, properties}) => ( 42 | { type: RECEIVE_VOCAB 43 | , vocab 44 | , info 45 | , classes 46 | , properties 47 | , receivedAt: Date.now() 48 | } 49 | ) 50 | 51 | const receiveError = error => ( 52 | { type: RECEIVE_ERROR 53 | , error 54 | , receivedAt: Date.now() 55 | } 56 | ) 57 | 58 | const fetchVocab = vocab => dispatch => { 59 | dispatch(requestVocab(vocab)) 60 | return getVocabulary(vocab) 61 | .then(o => dispatch(receiveVocab(vocab, o))) 62 | .catch(error => dispatch(receiveError(error))) 63 | } 64 | 65 | const fetchVocabs = () => dispatch => { 66 | dispatch(requestVocabs()) 67 | return getVocabularies() 68 | .then(list => dispatch(receiveVocabs(list))) 69 | .catch(error => dispatch(receiveError(error))) 70 | } 71 | 72 | const updateNode = node => ( 73 | { type: UPDATE_NODE 74 | , node: node 75 | } 76 | ) 77 | 78 | module.exports = 79 | { UPDATE_INPUT 80 | , UPDATE_SUGGESTIONS 81 | , REQUEST_VOCABS 82 | , RECEIVE_VOCABS 83 | , REQUEST_VOCAB 84 | , RECEIVE_VOCAB 85 | , RECEIVE_ERROR 86 | , UPDATE_NODE 87 | , updateInput 88 | , updateSuggestions 89 | , requestVocabs 90 | , receiveVocabs 91 | , requestVocab 92 | , receiveVocab 93 | , receiveError 94 | , fetchVocab 95 | , fetchVocabs 96 | , updateNode 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | const React = require('react') // eslint-disable-line no-unused-vars 2 | , h = require('react-hyperscript') 3 | , {connect} = require('react-redux') 4 | , {bindActionCreators} = require('redux') 5 | , {Block, Heading, List, Button} = require('rebass') 6 | , Editor = require('react-jsonld-editor') 7 | , Autosuggest = require('rebass-autosuggest') 8 | , Lowlight = require('react-lowlight') 9 | , {saveAs} = require('file-saver') 10 | , { updateInput 11 | , updateSuggestions 12 | , updateNode 13 | , fetchVocab 14 | } = require('../actions') 15 | 16 | Lowlight.registerLanguage('json', require('highlight.js/lib/languages/json')) 17 | 18 | const App = ( 19 | { input 20 | , suggestions 21 | , getSuggestions 22 | , vocabularies 23 | , node 24 | , classes 25 | , properties 26 | , updateInput 27 | , updateSuggestions 28 | , updateNode 29 | , fetchVocab 30 | }) => ( 31 | 32 | h(Block, 33 | [ h('p', 34 | [ 'An example of using ' 35 | , h('a', {href: 'https://github.com/editorsnotes/react-jsonld-editor'}, 36 | 'react-jsonld-editor') 37 | , ' with dynamically loaded vocabuaries from ' 38 | , h('a', {href: 'http://lov.okfn.org'}, 'Linked Open Vocabularies') 39 | , '. First, add some vocabularies by typing their names into the input below. Then, use the classes and properties from those vocabularies to create a JSON-LD object.' 40 | ]) 41 | , h('p', 42 | [ 'See the ' 43 | , h('a', {href: 'https://github.com/editorsnotes/edit-with-lov'}, 44 | 'source') 45 | , ' for this demo.' 46 | ]) 47 | 48 | , vocabularies.isEmpty() 49 | ? h(Heading, {size: 4, mb: 1}, ['No vocabularies loaded']) 50 | : h(Block, 51 | [ h(Heading, {size: 4, mb: 1}, ['Loaded vocabularies:']) 52 | , h(List, vocabularies 53 | .map(({title, url}) => h( 54 | 'a', {href: url, target: '_blank'}, title)) 55 | .toArray() 56 | ) 57 | ]) 58 | 59 | , h(Autosuggest, 60 | { name: 'vocabulary' 61 | , label: 'Add vocabulary' 62 | , hideLabel: true 63 | , suggestions 64 | , onSuggestionsFetchRequested: 65 | ({value}) => updateSuggestions(getSuggestions(value)) 66 | , onSuggestionsClearRequested: 67 | () => updateSuggestions([]) 68 | , getSuggestionValue: 69 | suggestion => suggestion.label 70 | , renderSuggestion: 71 | suggestion => suggestion.label 72 | , onSuggestionSelected: 73 | (e, {suggestion}) => fetchVocab(suggestion.id) 74 | , inputProps: 75 | { value: input 76 | , placeholder: 77 | 'Type the name of a vocabulary to add here, e.g. FOAF' 78 | , onChange: (e, {newValue, method}) => { 79 | if (method === 'type') { updateInput(newValue) } 80 | } 81 | } 82 | } 83 | ) 84 | 85 | , ...(vocabularies.isEmpty() 86 | ? [] 87 | : [ h(Editor, 88 | { node 89 | , classes 90 | , properties 91 | , onSave: node => updateNode(node) 92 | }) 93 | , h(Lowlight, 94 | { language: 'json' 95 | , value: JSON.stringify(node.toJS(), null, 2) 96 | }) 97 | , h(Button, 98 | {onClick: () => saveAs( 99 | new Blob([JSON.stringify(node.toJS(), null, 2)], 100 | {type: 'text/plain;charset=utf-8'}), 101 | 'exported.json', true) 102 | }, 'Save') 103 | ]) 104 | ] 105 | ) 106 | ) 107 | 108 | const matches = (inputValue, inputLength) => prefix => prefix 109 | ? prefix.toLowerCase().slice(0, inputLength) === inputValue 110 | : false 111 | 112 | const getSuggester = vocabs => input => { 113 | const inputValue = String(input).trim().toLowerCase() 114 | const inputLength = inputValue.length 115 | const matchesInput = matches(inputValue, inputLength) 116 | return inputLength === 0 117 | ? [] 118 | : vocabs 119 | .filter(vocab => ( 120 | matchesInput(vocab.prefix) || 121 | matchesInput(vocab.titles[0].value))) 122 | .map(vocab => ( 123 | { id: vocab.uri 124 | , label: vocab.titles[0].value 125 | })) 126 | .toArray() 127 | } 128 | 129 | const mapStateToProps = (state) => ( 130 | { input: state.input 131 | , suggestions: state.suggestions 132 | , getSuggestions: getSuggester(state.availableVocabs) 133 | , vocabularies: 134 | state.loadedVocabs 135 | .valueSeq() 136 | .filterNot(vocab => vocab.isFetching) 137 | .map(vocab => ( 138 | { title: vocab.info.get('titles').first().get('value') 139 | , url: vocab.info.get('uri') 140 | } 141 | )) 142 | , node: state.node 143 | , classes: state.classes 144 | , properties: state.properties 145 | } 146 | ) 147 | 148 | const mapDispatchToProps = dispatch => bindActionCreators( 149 | {updateInput, updateSuggestions, updateNode, fetchVocab}, dispatch) 150 | 151 | module.exports = connect(mapStateToProps, mapDispatchToProps)(App) 152 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | edit with LOV 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | , h = require('react-hyperscript') 3 | , {createStore, applyMiddleware, compose} = require('redux') 4 | , {Provider} = require('react-redux') 5 | , thunk = require('redux-thunk').default 6 | , {fetchVocabs} = require('./actions') 7 | , reducer = require('./reducers') 8 | , App = require('./containers/App') 9 | 10 | module.exports = React.createClass({ 11 | displayName: 'LOVLinkedDataEditor', 12 | 13 | getDefaultProps() { 14 | return { 15 | store: createStore( 16 | reducer, 17 | compose( 18 | applyMiddleware(thunk), 19 | global.devToolsExtension ? global.devToolsExtension() : f => f 20 | ) 21 | ) 22 | } 23 | }, 24 | 25 | componentDidMount() { 26 | const {store} = this.props 27 | 28 | store.dispatch(fetchVocabs()) 29 | }, 30 | 31 | render() { 32 | return h(Provider, this.props, h(App)) 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/lovutils.js: -------------------------------------------------------------------------------- 1 | const _ = require('highland') 2 | , N3 = require('n3') 3 | , { isBlank 4 | , isLiteral 5 | , getLiteralValue 6 | , getLiteralLanguage 7 | } = N3.Util 8 | , request = require('request') 9 | , {Map, List, Set, fromJS} = require('immutable') 10 | , {JSONLDNode, JSONLDValue} = require('immutable-jsonld') 11 | , ns = require('rdf-ns') 12 | 13 | const LOV_V2 = 'https://lov.okfn.org/dataset/lov/api/v2' 14 | const VOCAB_LIST = `${LOV_V2}/vocabulary/list` 15 | const VOCAB_INFO = `${LOV_V2}/vocabulary/info?vocab=` 16 | 17 | const owl = ns('http://www.w3.org/2002/07/owl#') 18 | , rdf = ns('http://www.w3.org/1999/02/22-rdf-syntax-ns#') 19 | , rdfs = ns('http://www.w3.org/2000/01/rdf-schema#') 20 | , skos = ns('http://www.w3.org/2004/02/skos/core#') 21 | 22 | const CLASS_TYPES = Set.of( 23 | owl('Class'), 24 | rdfs('Class'), 25 | rdfs('Datatype') 26 | ) 27 | 28 | const PROPERTY_TYPES = Set.of( 29 | owl('ObjectProperty'), 30 | owl('DatatypeProperty'), 31 | owl('AnnotationProperty'), 32 | owl('OntologyProperty'), 33 | rdf('Property') 34 | ) 35 | 36 | const rollup = predicates => (results, triple) => ( 37 | predicates.has(triple.predicate) 38 | ? results.update(triple.subject, JSONLDNode({'@id': triple.subject}), 39 | node => node.update( 40 | triple.predicate === rdf('type') ? '@type' : triple.predicate, 41 | List(), 42 | list => list.push(isLiteral(triple.object) 43 | ? JSONLDValue( 44 | { '@value': getLiteralValue(triple.object) 45 | , '@language': getLiteralLanguage(triple.object) 46 | }) 47 | : triple.predicate === rdf('type') 48 | ? triple.object 49 | : JSONLDNode({'@id': triple.object}) 50 | ) 51 | ) 52 | ) 53 | : results 54 | ) 55 | 56 | const isClass = node => node.types.intersect(CLASS_TYPES).size > 0 57 | const isProperty = node => node.types.intersect(PROPERTY_TYPES).size > 0 58 | 59 | const PREDICATES = Set.of( 60 | rdf('type'), rdfs('range'), rdfs('label'), skos('prefLabel') 61 | ) 62 | 63 | const requestJSON = uri => _(request(uri)) 64 | // collect chunks of data into an array 65 | .collect() 66 | // concatenate into a single buffer 67 | .map(Buffer.concat) 68 | // parse JSON 69 | .map(buffer => JSON.parse(buffer.toString('utf8'))) 70 | 71 | const hasInRange = (node, id) => node 72 | .get(rdfs('range'), List()) 73 | .some(node => node.id === id) 74 | 75 | const inferDatatypeProperties = node => hasInRange(node, rdfs('Literal')) 76 | ? node.push('@type', owl('DatatypeProperty')) 77 | : node 78 | 79 | const parseClassesAndProperties = url => _(request(url).pipe(N3.StreamParser())) 80 | // ignore blank nodes 81 | .reject(triple => isBlank(triple.subject)) 82 | // group into JSON-LD nodes with specified predicates 83 | .reduce(Map(), rollup(PREDICATES)) 84 | // stream over values 85 | .flatMap(map => _(map.valueSeq())) 86 | // ignore resources without labels 87 | .reject(node => node.preferredLabel() === undefined) 88 | // infer datatype properties 89 | .map(inferDatatypeProperties) 90 | // split into classes and properties 91 | .reduce({}, ({classes = Map(), properties = Map()}, node) => ( 92 | isClass(node) 93 | ? ({classes: classes.set(node.id, node), properties}) 94 | : isProperty(node) 95 | ? ({classes, properties: properties.set(node.id, node)}) 96 | : {classes, properties} 97 | )) 98 | 99 | const getLatestVersionURL = info => List(info.versions) 100 | .first() 101 | .fileURL.replace(/^http:/, 'https:') 102 | 103 | exports.getVocabulary = vocab => new Promise((resolve, reject) => { 104 | requestJSON(`${VOCAB_INFO}${encodeURIComponent(vocab)}`) 105 | .stopOnError(reject) 106 | .apply(info => parseClassesAndProperties(getLatestVersionURL(info)) 107 | .stopOnError(reject) 108 | .apply(o => resolve(Object.assign({}, {info: fromJS(info)}, o))) 109 | ) 110 | }) 111 | 112 | exports.getVocabularies = () => new Promise((resolve, reject) => { 113 | requestJSON(VOCAB_LIST) 114 | .stopOnError(reject) 115 | .apply(vocabularies => resolve(List(vocabularies))) 116 | }) 117 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | const {combineReducers} = require('redux') 2 | , {Map, List} = require('immutable') 3 | , {JSONLDNode} = require('immutable-jsonld') 4 | , { UPDATE_INPUT 5 | , UPDATE_SUGGESTIONS 6 | , REQUEST_VOCABS 7 | , RECEIVE_VOCABS 8 | , REQUEST_VOCAB 9 | , RECEIVE_VOCAB 10 | , RECEIVE_ERROR 11 | , UPDATE_NODE 12 | } = require('../actions') 13 | 14 | const isFetchingVocabs = (isFetchingVocabs = false, action) => { 15 | switch (action.type) { 16 | case REQUEST_VOCABS: 17 | return true 18 | default: 19 | return isFetchingVocabs 20 | } 21 | } 22 | 23 | const availableVocabs = (availableVocabs = List(), action) => { 24 | switch (action.type) { 25 | case RECEIVE_VOCABS: 26 | return action.list 27 | default: 28 | return availableVocabs 29 | } 30 | } 31 | 32 | const input = (input = '', action) => { 33 | switch (action.type) { 34 | case UPDATE_INPUT: 35 | return action.input || '' 36 | case REQUEST_VOCAB: 37 | return '' 38 | default: 39 | return input 40 | } 41 | } 42 | 43 | const suggestions = (suggestions = [], action) => { 44 | switch (action.type) { 45 | 46 | case UPDATE_SUGGESTIONS: 47 | return action.suggestions 48 | 49 | default: 50 | return suggestions 51 | } 52 | } 53 | 54 | const vocab = (vocab = {info: Map(), isFetching: false}, action) => { 55 | switch (action.type) { 56 | case REQUEST_VOCAB: 57 | return Object.assign({}, vocab, {isFetching: true}) 58 | case RECEIVE_VOCAB: 59 | return Object.assign({}, vocab, {info: action.info, isFetching: false}) 60 | } 61 | } 62 | 63 | const loadedVocabs = (loadedVocabs = Map(), action) => { 64 | switch (action.type) { 65 | case RECEIVE_VOCAB: 66 | case REQUEST_VOCAB: 67 | return loadedVocabs.set( 68 | action.vocab, vocab(loadedVocabs[action.vocab], action)) 69 | case RECEIVE_ERROR: 70 | console.log(action.error) 71 | default: 72 | return loadedVocabs 73 | } 74 | } 75 | 76 | const classes = (classes = Map(), action) => { 77 | switch (action.type) { 78 | case RECEIVE_VOCAB: 79 | return classes.merge(action.classes) 80 | 81 | default: 82 | return classes 83 | } 84 | } 85 | 86 | const properties = (properties = Map(), action) => { 87 | switch (action.type) { 88 | case RECEIVE_VOCAB: 89 | return properties.merge(action.properties) 90 | 91 | default: 92 | return properties 93 | } 94 | } 95 | 96 | const node = (node = JSONLDNode(), action) => { 97 | switch (action.type) { 98 | case UPDATE_NODE: 99 | return action.node 100 | default: 101 | return node 102 | } 103 | } 104 | 105 | module.exports = combineReducers( 106 | { isFetchingVocabs 107 | , availableVocabs 108 | , input 109 | , suggestions 110 | , loadedVocabs 111 | , classes 112 | , properties 113 | , node 114 | } 115 | ) 116 | -------------------------------------------------------------------------------- /src/standalone.js: -------------------------------------------------------------------------------- 1 | const h = require('react-hyperscript') 2 | , {render} = require('react-dom') 3 | , LOVLinkedDataEditor = require('./') 4 | 5 | const mount = document.createElement('div') 6 | document.body.appendChild(mount) 7 | 8 | render(h(LOVLinkedDataEditor), mount) 9 | --------------------------------------------------------------------------------