jenkins-bot has submitted this change and it was merged. Change subject: Introduces QUnit test for rdf hint ......................................................................
Introduces QUnit test for rdf hint Refactor rdf/sparql hint to be CodeMirror independent and testable. Create basic QUnit tests for rdf hint. Bug: T118596 Change-Id: Icea22f10d8a593bf39739faa40da7eaa3022fdd1 --- M .jshintignore M .jshintrc M index.html M package.json M style.css M vendor/codemirror/addon/hint/show-hint.css M vendor/codemirror/addon/hint/show-hint.js D wikibase/codemirror/addon/hint/wikibase-rdf-hint.js D wikibase/codemirror/addon/hint/wikibase-sparql-hint.js M wikibase/queryService/ui/App.js R wikibase/queryService/ui/editor/Editor.js A wikibase/queryService/ui/editor/hint/Rdf.js A wikibase/queryService/ui/editor/hint/Sparql.js A wikibase/tests/index.html A wikibase/tests/queryService/ui/editor/hint/Rdf.test.js 15 files changed, 518 insertions(+), 331 deletions(-) Approvals: Smalyshev: Looks good to me, approved jenkins-bot: Verified diff --git a/.jshintignore b/.jshintignore index d31ba83..170d6d2 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,3 +1,3 @@ node_modules/** vendor/** -wikibase/codemirror/addon/** +wikibase/codemirror/addon/tooltip/** diff --git a/.jshintrc b/.jshintrc index 26ad95f..f5811b4 100644 --- a/.jshintrc +++ b/.jshintrc @@ -34,6 +34,8 @@ "WikibaseRDFTooltip": false, "download": false, "EXPLORER": false, - "JSON": false + "JSON": false, + "QUnit": false, + "sinon": false } } diff --git a/index.html b/index.html index c35cccf..5258e9a 100644 --- a/index.html +++ b/index.html @@ -186,12 +186,12 @@ <script src="vendor/wdqs-explorer/wdqs-explorer.js"></script> <script src="vendor/lightbox/ekko-lightbox.min.js"></script> - <script src="wikibase/codemirror/addon/hint/wikibase-sparql-hint.js"></script> - <script src="wikibase/codemirror/addon/hint/wikibase-rdf-hint.js"></script> <script src="wikibase/codemirror/addon/tooltip/WikibaseRDFTooltip.js"></script> <script src="wikibase/queryService/ui/App.js"></script> - <script src="wikibase/queryService/ui/Editor.js"></script> + <script src="wikibase/queryService/ui/editor/hint/Sparql.js"></script> + <script src="wikibase/queryService/ui/editor/hint/Rdf.js"></script> + <script src="wikibase/queryService/ui/editor/Editor.js"></script> <script src="wikibase/queryService/ui/QueryExampleDialog.js"></script> <script src="wikibase/queryService/ui/resultBrowser/AbstractResultBrowser.js"></script> <script src="wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js"></script> diff --git a/package.json b/package.json index 8b670dc..bd2735c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "grunt-cli": "0.1.13", "grunt-contrib-jshint": "0.11.2", "grunt-jscs": "2.1.0", - "grunt-jsonlint": "1.0.4" + "grunt-jsonlint": "1.0.4", + "sinon": "~1.17.3" } } diff --git a/style.css b/style.css index d1df9b9..62e8cf7 100644 --- a/style.css +++ b/style.css @@ -168,6 +168,14 @@ } } +/* + editor hint style +*/ +.wikibase-rdf-hint{ + border-bottom: 1px solid gray; + white-space: normal; +} + /** Query example dialog **/ diff --git a/vendor/codemirror/addon/hint/show-hint.css b/vendor/codemirror/addon/hint/show-hint.css index 7cda762..877a822 100755 --- a/vendor/codemirror/addon/hint/show-hint.css +++ b/vendor/codemirror/addon/hint/show-hint.css @@ -35,9 +35,4 @@ li.CodeMirror-hint-active { background: #08f; color: white; -} - -.wikibase-rdf-hint{ - border-bottom: 1px solid gray; - white-space: normal; } \ No newline at end of file diff --git a/vendor/codemirror/addon/hint/show-hint.js b/vendor/codemirror/addon/hint/show-hint.js index a1e56c3..d6ed411 100755 --- a/vendor/codemirror/addon/hint/show-hint.js +++ b/vendor/codemirror/addon/hint/show-hint.js @@ -430,11 +430,11 @@ alignWithWord: true, closeCharacters: /[\s()\[\]{};:>,]/, closeOnUnfocus: true, - completeOnSingleClick: false, + completeOnSingleClick: true, container: null, customKeys: null, extraKeys: null }; CodeMirror.defineOption("hintOptions", null); -}); +}); \ No newline at end of file diff --git a/wikibase/codemirror/addon/hint/wikibase-rdf-hint.js b/wikibase/codemirror/addon/hint/wikibase-rdf-hint.js deleted file mode 100755 index 42b7786..0000000 --- a/wikibase/codemirror/addon/hint/wikibase-rdf-hint.js +++ /dev/null @@ -1,170 +0,0 @@ -/** - * Code completion for Wikibase entities RDF prefixes in SPARQL - * - * Determines entity type from list of prefixes and completes input - * based on search results from wikidata.org entities search API - * - * licence GNU GPL v2+ - * - * @author Jens Ohlig <jens.oh...@wikimedia.de> - * @author Jan Zerebecki - * @author Jonas Kress - */ - -( function ( mod ) { - 'use strict'; - if ( typeof exports === 'object' && typeof module === 'object' ) { // CommonJS - mod( require( '../../lib/codemirror' ) ); - } else if ( typeof define === 'function' && define.amd ) { // AMD - define( [ '../../lib/codemirror' ], mod ); - } else { // Plain browser env - mod( CodeMirror ); - } -} )( function ( CodeMirror ) { - 'use strict'; - - var ENTITY_TYPES = { - 'http://www.wikidata.org/prop/direct/': 'property', - 'http://www.wikidata.org/prop/': 'property', - 'http://www.wikidata.org/prop/novalue/': 'property', - 'http://www.wikidata.org/prop/statement/': 'property', - 'http://www.wikidata.org/prop/statement/value/': 'property', - 'http://www.wikidata.org/prop/qualifier/': 'property', - 'http://www.wikidata.org/prop/qualifier/value/': 'property', - 'http://www.wikidata.org/prop/reference/': 'property', - 'http://www.wikidata.org/prop/reference/value/': 'property', - 'http://www.wikidata.org/wiki/Special:EntityData/': 'item', - 'http://www.wikidata.org/entity/': 'item' - }, - ENTITY_SEARCH_API_ENDPOINT = 'https://www.wikidata.org/w/api.php?action=wbsearchentities&' - + 'search={term}&format=json&language=en&uselang=en&type={entityType}&continue=0'; - - CodeMirror.registerHelper( 'hint', 'sparql', function ( editor, callback, options ) { - if ( wikibase_sparqlhint ){ - wikibase_sparqlhint( editor, callback, options ); - } - - var currentWord = getCurrentWord( getCurrentLine( editor ), getCurrentCurserPosition( editor ) ), - prefix, - term, - entityPrefixes; - - if ( !currentWord.word.match( /\S+:\S*/ ) ) { - return; - } - - prefix = getPrefixFromWord( currentWord.word.trim() ); - term = getTermFromWord( currentWord.word.trim() ); - entityPrefixes = extractPrefixes( editor.doc.getValue() ); - - if ( !entityPrefixes[ prefix ] ) { // unknown prefix - var list = [{ text: term, displayText: 'Unknown prefix \'' + prefix + ':\'' }]; - return callback( getHintCompletion( editor, currentWord, prefix, list ) ); - } - - if ( term.length === 0 ) { // empty search term - var list = [{ text: term, displayText: 'Type to search for an entity' }]; - return callback( getHintCompletion( editor, currentWord, prefix, list ) ); - } - - if ( entityPrefixes[ prefix ] ) { // search entity - searchEntities( term, entityPrefixes[ prefix ] ).done( function ( list ) { - callback( getHintCompletion( editor, currentWord, prefix, list ) ); - } ); - } - } ); - - CodeMirror.hint.sparql.async = true; - CodeMirror.defaults.hintOptions = {}; - CodeMirror.defaults.hintOptions.closeCharacters = /[]/; - CodeMirror.defaults.hintOptions.completeSingle = false; - - function getPrefixFromWord( word ) { - return word.split( ':' ).shift(); - } - - function getTermFromWord( word ) { - return word.split( ':' ).pop(); - } - - function getCurrentLine( editor ) { - return editor.getLine( editor.getCursor().line ); - } - - function getCurrentCurserPosition( editor ) { - return editor.getCursor().ch; - } - - function getHintCompletion( editor, currentWord, prefix, list ) { - var completion = { list: [] }; - completion.from = CodeMirror.Pos( editor.getCursor().line, currentWord.start + prefix.length + 1 ); - completion.to = CodeMirror.Pos( editor.getCursor().line, currentWord.end ); - completion.list = list; - - return completion; - } - - function searchEntities( term, type ) { - var entityList = [], - deferred = $.Deferred(); - - $.ajax( { - url: ENTITY_SEARCH_API_ENDPOINT.replace( '{term}', term ).replace( '{entityType}', type ), - dataType: 'jsonp' - } ).done( function ( data ) { - $.each( data.search, function ( key, value ) { - entityList.push( { - className: 'wikibase-rdf-hint', - text: value.id, - displayText: value.label + ' (' + value.id + ') ' + value.description + '\n' - } ); - } ); - - deferred.resolve( entityList ); - } ); - - return deferred.promise(); - } - - function getCurrentWord( line, position ) { - var pos = position -1, - colon = false; - - while( line.charAt( pos ).match( /\w/ ) || - ( line.charAt( pos ) === ' ' && colon === false ) || - ( line.charAt( pos ) === ':' && colon === false ) ){ - - if( line.charAt( pos ) === ':' ) - colon = true; - pos--; - } - var left = pos + 1; - - pos = position; - while( line.charAt( pos ).match( /\w/ ) ){ - pos++; - } - var right = pos; - - var word = line.substring( left, right ); - return { word: word, start: left, end: right }; - } - - function extractPrefixes( text ) { - var prefixes = wikibase.queryService.RdfNamespaces.getPrefixMap(ENTITY_TYPES), - lines = text.split( '\n' ), - matches; - - $.each( lines, function ( index, line ) { - // PREFIX wd: <http://www.wikidata.org/entity/> - if ( matches = line.match( /(PREFIX) (\S+): <([^>]+)>/ ) ) { - if ( ENTITY_TYPES[ matches[ 3 ] ] ) { - prefixes[ matches[ 2 ] ] = ENTITY_TYPES[ matches[ 3 ] ]; - } - } - } ); - - return prefixes; - } - -} ); diff --git a/wikibase/codemirror/addon/hint/wikibase-sparql-hint.js b/wikibase/codemirror/addon/hint/wikibase-sparql-hint.js deleted file mode 100755 index 65b65de..0000000 --- a/wikibase/codemirror/addon/hint/wikibase-sparql-hint.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Code completion for Wikibase entities RDF prefixes in SPARQL - * completes SPARQL keywords and ?variables - * - * licence GNU GPL v2+ - * - * @author Jonas Kress - */ - -var wikibase_sparqlhint = null; - -( function ( mod ) { - if ( typeof exports == 'object' && typeof module == 'object' ) { // CommonJS - mod( require( '../../lib/codemirror' ) ); - } else if ( typeof define == 'function' && define.amd ) { // AMD - define( [ '../../lib/codemirror' ], mod ); - } else { // Plain browser env - mod( CodeMirror ); - } -} )( function ( CodeMirror ) { - 'use strict'; - - var SPARQL_KEYWORDS = [ - 'SELECT', - 'OPTIONAL', - 'WHERE', - 'ORDER', - 'ORDER BY', - 'DISTINCT', - 'WHERE {\n\n}', - 'SERVICE', - 'SERVICE wikibase:label {\n bd:serviceParam wikibase:language "en" .\n}', - 'BASE', - 'PREFIX', - 'REDUCED', - 'FROM', - 'LIMIT', - 'OFFSET', - 'HAVING', - 'UNION' - ]; - - wikibase_sparqlhint = function ( editor, callback, options ) { - var currentWord = getCurrentWord( getCurrentLine( editor ), getCurrentCurserPosition( editor ) ), - hintList = []; - - if ( currentWord.word.indexOf( '?' ) === 0 ) { - hintList = hintList.concat( getVariableHints( - currentWord.word, - getDefinedVariables( editor.doc.getValue() ) - ) ); - } - - hintList = hintList.concat( getSPARQLHints( currentWord.word ) ); - - if ( hintList.length > 0 ) { - callback( getHintCompletion( editor, currentWord, hintList ) ); - } - }; - - function getSPARQLHints( term ) { - var list = []; - - $.each( SPARQL_KEYWORDS, function ( key, keyword ) { - if ( keyword.toLowerCase().indexOf( term.toLowerCase() ) === 0 ) { - list.push( keyword ); - } - } ); - - return list; - } - - function getDefinedVariables( text ) { - var variables = []; - - $.each( text.split( ' ' ), function ( key, word ) { - if ( word.match( /^\?\w+$/ ) ) { - variables.push( word ); - } - } ); - - return $.unique( variables ); - } - - function getVariableHints( term, variables ) { - var list = []; - - if ( !term || term === '?' ) { - return variables; - } - - $.each( variables, function ( key, variable ) { - if ( variable.toLowerCase().indexOf( term.toLowerCase() ) === 0 ) { - list.push( variable ); - } - } ); - - return list; - } - - function getHintCompletion( editor, currentWord , list ) { - var completion = { list: [] }; - completion.from = CodeMirror.Pos( editor.getCursor().line, currentWord.start ); - completion.to = CodeMirror.Pos( editor.getCursor().line, currentWord.end ); - completion.list = list; - - return completion; - } - - function getCurrentWord( line, position ) { - var words = line.split( ' ' ), matchedWord = '', scannedPostion = 0; - - $.each( words, function ( key, word ) { - scannedPostion += word.length; - - if ( key > 0 ) { // add spaces to position - scannedPostion++; - } - - if ( scannedPostion >= position ) { - matchedWord = word; - return; - } - } ); - - return { - word: matchedWord, - start: scannedPostion - matchedWord.length, - end: scannedPostion - }; - } - - function getCurrentLine( editor ) { - return editor.getLine( editor.getCursor().line ); - } - - function getCurrentCurserPosition( editor ) { - return editor.getCursor().ch; - } - -} ); diff --git a/wikibase/queryService/ui/App.js b/wikibase/queryService/ui/App.js index 2c8f7bd..fea3615 100644 --- a/wikibase/queryService/ui/App.js +++ b/wikibase/queryService/ui/App.js @@ -25,7 +25,7 @@ * @constructor * * @param {jQuery} $element - * @param {wikibase.queryService.ui.Editor} + * @param {wikibase.queryService.ui.editor.Editor} * @param {wikibase.queryService.api.Sparql} */ function SELF( $element, editor, sparqlApi, querySamplesApi ) { @@ -57,8 +57,7 @@ SELF.prototype._querySamplesApi = null; /** - * @property {wikibase.queryService.ui.Editor} - * @type wikibase.queryService.ui.Editor + * @property {wikibase.queryService.ui.editor.Editor} * @private **/ SELF.prototype._editor = null; @@ -84,7 +83,7 @@ } if( !this._editor ){ - this._editor = new wikibase.queryService.ui.Editor(); + this._editor = new wikibase.queryService.ui.editor.Editor(); } this._initApp(); diff --git a/wikibase/queryService/ui/Editor.js b/wikibase/queryService/ui/editor/Editor.js similarity index 62% rename from wikibase/queryService/ui/Editor.js rename to wikibase/queryService/ui/editor/Editor.js index 3a9a40e..8580074 100644 --- a/wikibase/queryService/ui/Editor.js +++ b/wikibase/queryService/ui/editor/Editor.js @@ -1,8 +1,9 @@ var wikibase = wikibase || {}; wikibase.queryService = wikibase.queryService || {}; wikibase.queryService.ui = wikibase.queryService.ui || {}; +wikibase.queryService.ui.editor = wikibase.queryService.ui.editor || {}; -wikibase.queryService.ui.Editor = ( function( CodeMirror, WikibaseRDFTooltip, localStorage ) { +wikibase.queryService.ui.editor.Editor = ( function( $, CodeMirror, WikibaseRDFTooltip, localStorage ) { "use strict"; var CODEMIRROR_DEFAULTS = { @@ -10,7 +11,8 @@ "matchBrackets": true, "mode": 'sparql', "extraKeys": { 'Ctrl-Space': 'autocomplete' }, - "viewportMargin": Infinity + "viewportMargin": Infinity, + "hintOptions": { closeCharacters: /[]/, completeSingle: false} }, ERROR_LINE_MARKER = null, ERROR_CHARACTER_MARKER = null; @@ -38,6 +40,18 @@ SELF.prototype._editor = null; /** + * @property {wikibase.queryService.ui.editor.hint.Sparql} + * @private + **/ + SELF.prototype._sparqlHint = null; + + /** + * @property {wikibase.queryService.ui.editor.hint.Rdf} + * @private + **/ + SELF.prototype._rdfHint = null; + + /** * Construct an this._editor on the given textarea DOM element * * @param {Element} element @@ -56,6 +70,56 @@ this._editor.focus(); new WikibaseRDFTooltip(this._editor); + + this._registerHints(); + }; + + SELF.prototype._registerHints = function() { + var self = this; + + CodeMirror.registerHelper( 'hint', 'sparql', function ( editor, callback, options ) { + if( editor !== self._editor ){ + return; + } + var lineContent = editor.getLine( editor.getCursor().line ), + editorContent = editor.doc.getValue(), + cursorPos = editor.getCursor().ch, + lineNum = editor.getCursor().line; + + self._getHints( editorContent, lineContent, lineNum, cursorPos ).done( function( hint ){ + callback( hint ); + } ); + } ); + + CodeMirror.hint.sparql.async = true; + }; + + SELF.prototype._getHints = function( editorContent, lineContent, lineNum, cursorPos ) { + var deferred = new $.Deferred(), + self = this; + if( !this._sparqlHint ){ + this._sparqlHint = new wikibase.queryService.ui.editor.hint.Sparql(); + } + if( !this._rdfHint ){ + this._rdfHint = new wikibase.queryService.ui.editor.hint.Rdf(); + } + + this._rdfHint.getHint( editorContent, lineContent, lineNum, cursorPos ).done( function( hint ){ + hint.from = CodeMirror.Pos( hint.from.line, hint.from.char ); + hint.to = CodeMirror.Pos( hint.to.line, hint.to.char ); + + deferred.resolve( hint ); + + } ).fail( function(){//if rdf hint is rejected try sparql hint + self._sparqlHint.getHint( editorContent, lineContent, lineNum, cursorPos ).done( function( hint ){ + hint.from = CodeMirror.Pos( hint.from.line, hint.from.char ); + hint.to = CodeMirror.Pos( hint.to.line, hint.to.char ); + + deferred.resolve( hint ); + } ); + } ); + + return deferred.promise(); }; /** @@ -160,4 +224,4 @@ return SELF; -}( CodeMirror, WikibaseRDFTooltip, window.localStorage ) ); +}( jQuery, CodeMirror, WikibaseRDFTooltip, window.localStorage ) ); diff --git a/wikibase/queryService/ui/editor/hint/Rdf.js b/wikibase/queryService/ui/editor/hint/Rdf.js new file mode 100755 index 0000000..4547984 --- /dev/null +++ b/wikibase/queryService/ui/editor/hint/Rdf.js @@ -0,0 +1,185 @@ +var wikibase = wikibase || {}; +wikibase.queryService = wikibase.queryService || {}; +wikibase.queryService.ui = wikibase.queryService.ui || {}; +wikibase.queryService.ui.editor = wikibase.queryService.ui.editor || {}; +wikibase.queryService.ui.editor.hint = wikibase.queryService.ui.editor.hint || {}; + +( function( $, wb ) { + 'use strict'; + + var MODULE = wb.queryService.ui.editor.hint; + + var ENTITY_TYPES = { + 'http://www.wikidata.org/prop/direct/': 'property', + 'http://www.wikidata.org/prop/': 'property', + 'http://www.wikidata.org/prop/novalue/': 'property', + 'http://www.wikidata.org/prop/statement/': 'property', + 'http://www.wikidata.org/prop/statement/value/': 'property', + 'http://www.wikidata.org/prop/qualifier/': 'property', + 'http://www.wikidata.org/prop/qualifier/value/': 'property', + 'http://www.wikidata.org/prop/reference/': 'property', + 'http://www.wikidata.org/prop/reference/value/': 'property', + 'http://www.wikidata.org/wiki/Special:EntityData/': 'item', + 'http://www.wikidata.org/entity/': 'item' + }, + ENTITY_SEARCH_API_ENDPOINT = 'https://www.wikidata.org/w/api.php?action=wbsearchentities&' + + 'search={term}&format=json&language=en&uselang=en&type={entityType}&continue=0'; + + /** + * Code completion for Wikibase entities RDF prefixes in SPARQL + * completes SPARQL keywords and ?variables + * + * licence GNU GPL v2+ + * + * @author Jonas Kress + * @param {wikibase.queryService.RdfNamespace} rdfNamespace + * @constructor + */ + var SELF = MODULE.Rdf = function( rdfNamespaces ) { + this._rdfNamespaces = rdfNamespaces; + + if( !this._rdfNamespaces ){ + this._rdfNamespaces = wikibase.queryService.RdfNamespaces; + } + }; + + /** + * @property {wikibase.queryService.RdfNamespace} + * @private + **/ + SELF.prototype._rdfNamespaces = null; + + /** + * Get list of hints + * + * @return {jQuery.promise} Returns the completion as promise ({list:[], from:, to:}) + **/ + SELF.prototype.getHint = function( editorContent, lineContent, lineNum, cursorPos ) { + var deferred = new $.Deferred(), + currentWord = this._getCurrentWord( lineContent, cursorPos ), + list, + prefix, + term, + entityPrefixes, + self = this; + + if ( !currentWord.word.match( /\S+:\S*/ ) ) { + return deferred.reject().promise(); + } + + prefix = this._getPrefixFromWord( currentWord.word.trim() ); + term = this._getTermFromWord( currentWord.word.trim() ); + entityPrefixes = this._extractPrefixes( editorContent ); + + if ( !entityPrefixes[ prefix ] ) { // unknown prefix + list = [{ text: term, displayText: 'Unknown prefix \'' + prefix + ':\'' }]; + return deferred.resolve( this._getHintCompletion( lineNum, currentWord, prefix, list ) ).promise(); + } + + if ( term.length === 0 ) { // empty search term + list = [{ text: term, displayText: 'Type to search for an entity' }]; + return deferred.resolve( this._getHintCompletion( lineNum, currentWord, prefix, list ) ).promise(); + } + + if ( entityPrefixes[ prefix ] ) { // search entity + this._searchEntities( term, entityPrefixes[ prefix ] ).done( function ( list ) { + return deferred.resolve( self._getHintCompletion( lineNum, currentWord, prefix, list ) ); + } ); + } + + return deferred.promise(); + }; + + SELF.prototype._getPrefixFromWord = function( word ) { + return word.split( ':' ).shift(); + }; + + SELF.prototype._getTermFromWord = function( word ) { + return word.split( ':' ).pop(); + }; + + SELF.prototype._getHintCompletion = function( lineNum, currentWord, prefix, list ) { + var completion = { list: [] }; + completion.from = {line: lineNum, char: currentWord.start + prefix.length + 1 }; + completion.to = {line: lineNum, char: currentWord.end }; + completion.list = list; + + return completion; + }; + + SELF.prototype._searchEntities = function( term, type ) { + var entityList = [], + deferred = $.Deferred(); + + $.ajax( { + url: ENTITY_SEARCH_API_ENDPOINT.replace( '{term}', term ).replace( '{entityType}', type ), + dataType: 'jsonp' + } ).done( function ( data ) { + $.each( data.search, function ( key, value ) { + entityList.push( { + className: 'wikibase-rdf-hint', + text: value.id, + displayText: value.label + ' (' + value.id + ') ' + value.description + '\n' + } ); + } ); + + deferred.resolve( entityList ); + } ); + + return deferred.promise(); + }; + + SELF.prototype._getCurrentWord = function( line, position ) { + var pos = position -1, + colon = false; + + if( pos < 0 ){ + pos = 0; + } + + while( line.charAt( pos ).match( /\w/ ) || + ( line.charAt( pos ) === ' ' && colon === false ) || + ( line.charAt( pos ) === ':' && colon === false ) ){ + + if( line.charAt( pos ) === ':' ){ + colon = true; + } + pos--; + if( pos < 0 ){ + break; + } + } + var left = pos + 1; + + pos = position; + while( line.charAt( pos ).match( /\w/ ) ){ + pos++; + if( pos >= line.length ){ + break; + } + } + var right = pos; + + var word = line.substring( left, right ); + return { word: word, start: left, end: right }; + }; + + SELF.prototype._extractPrefixes = function( text ) { + var prefixes = this._rdfNamespaces.getPrefixMap(ENTITY_TYPES), + lines = text.split( '\n' ), + matches; + + $.each( lines, function ( index, line ) { + // PREFIX wd: <http://www.wikidata.org/entity/> + if ( ( matches = line.match( /(PREFIX) (\S+): <([^>]+)>/ ) ) ) { + if ( ENTITY_TYPES[ matches[ 3 ] ] ) { + prefixes[ matches[ 2 ] ] = ENTITY_TYPES[ matches[ 3 ] ]; + } + } + } ); + + return prefixes; + }; + +}( jQuery, wikibase ) ); + diff --git a/wikibase/queryService/ui/editor/hint/Sparql.js b/wikibase/queryService/ui/editor/hint/Sparql.js new file mode 100755 index 0000000..7e6ffb6 --- /dev/null +++ b/wikibase/queryService/ui/editor/hint/Sparql.js @@ -0,0 +1,137 @@ +var wikibase = wikibase || {}; +wikibase.queryService = wikibase.queryService || {}; +wikibase.queryService.ui = wikibase.queryService.ui || {}; +wikibase.queryService.ui.editor = wikibase.queryService.ui.editor || {}; +wikibase.queryService.ui.editor.hint = wikibase.queryService.ui.editor.hint || {}; + +( function( $, wb ) { + 'use strict'; + + var MODULE = wb.queryService.ui.editor.hint; + + var SPARQL_KEYWORDS = [ + 'SELECT', + 'OPTIONAL', + 'WHERE', + 'ORDER', + 'ORDER BY', + 'DISTINCT', + 'WHERE {\n\n}', + 'SERVICE', + 'SERVICE wikibase:label {\n bd:serviceParam wikibase:language "en" .\n}', + 'BASE', 'PREFIX', 'REDUCED', 'FROM', 'LIMIT', 'OFFSET', 'HAVING', + 'UNION' ]; + + /** + * Code completion for Wikibase entities RDF prefixes in SPARQL + * completes SPARQL keywords and ?variables + * + * licence GNU GPL v2+ + * + * @author Jonas Kress + * @constructor + */ + var SELF = MODULE.Sparql = function Sparql() { + }; + + /** + * Get list of hints + * + * @return {jQuery.promise} Returns the completion as promise ({list:[], from:, to:}) + **/ + SELF.prototype.getHint = function( editorContent, lineContent, lineNum, cursorPos ) { + var currentWord = this._getCurrentWord( lineContent, cursorPos ), + hintList = [], + deferred = new $.Deferred(); + + if ( currentWord.word.indexOf( '?' ) === 0 ) { + hintList = hintList.concat( this._getVariableHints( + currentWord.word, + this._getDefinedVariables( editorContent ) + ) ); + } + + hintList = hintList.concat( this._getSPARQLHints( currentWord.word ) ); + + if ( hintList.length > 0 ) { + var hint = this._getHintCompletion( currentWord, hintList, lineNum ); + return deferred.resolve( hint ).promise(); + } + + return deferred.reject().promise(); + }; + + SELF.prototype._getSPARQLHints = function( term ) { + var list = []; + + $.each( SPARQL_KEYWORDS, function ( key, keyword ) { + if ( keyword.toLowerCase().indexOf( term.toLowerCase() ) === 0 ) { + list.push( keyword ); + } + } ); + + return list; + }; + + SELF.prototype._getDefinedVariables = function( text ) { + var variables = []; + + $.each( text.split( ' ' ), function ( key, word ) { + if ( word.match( /^\?\w+$/ ) ) { + variables.push( word ); + } + } ); + + return $.unique( variables ); + }; + + SELF.prototype._getVariableHints = function( term, variables ) { + var list = []; + + if ( !term || term === '?' ) { + return variables; + } + + $.each( variables, function ( key, variable ) { + if ( variable.toLowerCase().indexOf( term.toLowerCase() ) === 0 ) { + list.push( variable ); + } + } ); + + return list; + }; + + SELF.prototype._getHintCompletion = function( currentWord, list, lineNumber ) { + var completion = { list: [] }; + completion.from = {line: lineNumber, char: currentWord.start }; + completion.to = {line: lineNumber, char: currentWord.end}; + completion.list = list; + + return completion; + }; + + SELF.prototype._getCurrentWord = function( line, position ) { + var words = line.split( ' ' ), matchedWord = '', scannedPostion = 0; + + $.each( words, function ( key, word ) { + scannedPostion += word.length; + + if ( key > 0 ) { // add spaces to position + scannedPostion++; + } + + if ( scannedPostion >= position ) { + matchedWord = word; + return; + } + } ); + + return { + word: matchedWord, + start: scannedPostion - matchedWord.length, + end: scannedPostion + }; + }; + + +}( jQuery, wikibase ) ); diff --git a/wikibase/tests/index.html b/wikibase/tests/index.html new file mode 100644 index 0000000..bee9026 --- /dev/null +++ b/wikibase/tests/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width"> + <title>QUnit Tests</title> + <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.22.0.css"> +</head> +<body> + <div id="qunit"></div> + <div id="qunit-fixture"></div> + <script src="//code.jquery.com/qunit/qunit-1.22.0.js"></script> + <script src="../../vendor/jquery/jquery-1.11.3.js"></script> + <script src="../../node_modules/sinon/pkg/sinon-1.17.3.js"></script> + <script src="../queryService/ui/editor/hint/Rdf.js"></script> + <script src="queryService/ui/editor/hint/Rdf.test.js"></script> +</body> +</html> \ No newline at end of file diff --git a/wikibase/tests/queryService/ui/editor/hint/Rdf.test.js b/wikibase/tests/queryService/ui/editor/hint/Rdf.test.js new file mode 100644 index 0000000..ced6067 --- /dev/null +++ b/wikibase/tests/queryService/ui/editor/hint/Rdf.test.js @@ -0,0 +1,89 @@ +( function( $, QUnit, sinon, wb ) { + 'use strict'; + + QUnit.module( 'wikibase.queryService.ui.editor.hint.Rdf' ); + var Rdf = wb.queryService.ui.editor.hint.Rdf; + + var HINT_UNKNOWN_PREFIX = {'list':[{'text':'','displayText':'Unknown prefix \'XXX:\''}],'from':{'line':1,'char':4},'to':{'line':1,'char':4}}; + var HINT_START_SEARCH = { 'from' : { 'char' : 7, 'line' : 1 }, 'list' : [ {'displayText' : 'Type to search for an entity', 'text' : '' } ],'to' : {'char' : 7, 'line' : 1}}; + + var VALID_SCENARIOS = [ + { scenario:'PREFIX0:TERM', prefix:'PREFIX0', content:'PREFIX0:TERM', line:'PREFIX0:TERM', y:1, x:8, + result: {'from':{'char':8,'line':1},'list':[{'className':'wikibase-rdf-hint','displayText':'LABEL (ID) DESCRIPTION\n','text':'ID'}],'to':{'char':12,'line':1}}}, + + { scenario:'PREFIX1:TERM',prefix:'PREFIX1', content:'PREFIX1:TERM', line:'PREFIX1:TERM', y:1, x:8 , + result: {'from':{'char':8,'line':1},'list':[{'className':'wikibase-rdf-hint','displayText':'LABEL (ID) DESCRIPTION\n','text':'ID'}],'to':{'char':12,'line':1}}}, + + { scenario:'Defined prefix PREFIXDEF',prefix:'', content:'PREFIX PREFIXDEF: <http://www.wikidata.org/entity/>\nPREFIXDEF:TERM', line:'PREFIXDEF:TERM', y:2, x:10, + result: {'from':{'char':10,'line':2},'list':[{'className':'wikibase-rdf-hint','displayText':'LABEL (ID) DESCRIPTION\n','text':'ID'}],'to':{'char':14,'line':2}}}, + + { scenario:'?p wdt:P31/^PREFIX1:TERM',prefix:'PREFIX1', content:'?p wdt:P31/^PREFIX1:TERM', line:'?p wdt:P31/PREFIX1:TERM', y:1, x:19, + result: {'from':{'char':19,'line':1},'list':[{'className':'wikibase-rdf-hint','displayText':'LABEL (ID) DESCRIPTION\n','text':'ID'}],'to':{'char':23,'line':1}}}, + + { scenario:'?p wdt:P31/|PREFIX1:TERM',prefix:'PREFIX1', content:'?p wdt:P31/|PREFIX1:TERM', line:'?p wdt:P31/PREFIX1:TERM', y:1, x:19, + result: {'from':{'char':19,'line':1},'list':[{'className':'wikibase-rdf-hint','displayText':'LABEL (ID) DESCRIPTION\n','text':'ID'}],'to':{'char':23,'line':1}}}, + + { scenario:'?p PREFIX:TERM/wdt:p1',prefix:'PREFIX', content:'?p PREFIX:TERM/wdt:p1', line:'?p PREFIX:TERM/wdt:p1', y:1, x:10, + result: {'from':{'char':10,'line':1},'list':[{'className':'wikibase-rdf-hint','displayText':'LABEL (ID) DESCRIPTION\n','text':'ID'}],'to':{'char':14,'line':1}}}, + + { scenario:'?p wdt:P31/PREFIX1:TERM',prefix:'PREFIX1', content:'?p wdt:P31/PREFIX1:TERM', line:'?p wdt:P31/PREFIX1:TERM', y:1, x:19, + result: {'from':{'char':19,'line':1},'list':[{'className':'wikibase-rdf-hint','displayText':'LABEL (ID) DESCRIPTION\n','text':'ID'}],'to':{'char':23,'line':1}}} + ]; + + var API_URL = 'https://www.wikidata.org/w/api.php?action=wbsearchentities&search=TERM&format=json&language=en&uselang=en&type=item&continue=0'; + + sinon.stub($, 'ajax').returns( $.Deferred().resolve( { search:[{label:'LABEL', id:'ID', description:'DESCRIPTION'}] } ).promise() ); + + + QUnit.test( 'is constructable', function( assert ) { + assert.expect( 1 ); + assert.ok( new Rdf() instanceof Rdf ); + } ); + + QUnit.test( 'When there is nothing to autocomplete', function( assert ) { + assert.expect( 1 ); + + var rdf = new Rdf( {getPrefixMap:sinon.stub().returns({})} ); + rdf.getHint('XXX', 'XXX:', 1, 3).done( function( hint ){ + assert.notOk( true, 'Hinting should not succed'); + } ).fail( function(){ + assert.ok( true, 'Hinting must fail' ); + } ); + } ); + + QUnit.test( 'When empty prefix map', function( assert ) { + assert.expect( 1 ); + + var rdf = new Rdf( {getPrefixMap:sinon.stub().returns({})} ); + rdf.getHint('XXX:', 'XXX:', 1, 4).done( function( hint ){ + assert.deepEqual( hint, HINT_UNKNOWN_PREFIX , 'Hint must be a unknown prefix hint'); + } ); + } ); + + QUnit.test( 'When prefix exist, but there is nothing to search for', function( assert ) { + assert.expect( 1 ); + + var rdf = new Rdf( {getPrefixMap:sinon.stub().returns({'PREFIX' : 'item'})} ); + rdf.getHint('PREFIX:', 'PREFIX:', 1, 7).done( function( hint ){ + assert.deepEqual( hint, HINT_START_SEARCH , 'Hint equals start search'); + } ); + } ); + + + $.each( VALID_SCENARIOS, function( key, test){ + QUnit.test( 'When running valid scenario: ' + this.scenario, function( assert ) { + assert.expect( 2 ); + + var prefix = {}; + prefix[test.prefix] = 'item'; + var rdf = new Rdf( {getPrefixMap:sinon.stub().returns( prefix )} ); + rdf.getHint( test.content, test.line, test.y, test.x ).done( function( hint ){ + assert.deepEqual($.ajax.args[0][0].url, API_URL, 'Hint trigger call API URL call'); + $.ajax.reset(); + assert.deepEqual( hint, test.result , 'Hint must return valid hint'); + } ); + } ); + } ); + + +}( jQuery, QUnit, sinon, wikibase ) ); -- To view, visit https://gerrit.wikimedia.org/r/273522 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Icea22f10d8a593bf39739faa40da7eaa3022fdd1 Gerrit-PatchSet: 5 Gerrit-Project: wikidata/query/gui Gerrit-Branch: master Gerrit-Owner: Jonas Kress (WMDE) <jonas.kr...@wikimedia.de> Gerrit-Reviewer: JanZerebecki <jan.wikime...@zerebecki.de> Gerrit-Reviewer: Jonas Kress (WMDE) <jonas.kr...@wikimedia.de> Gerrit-Reviewer: Smalyshev <smalys...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits