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

Reply via email to