JAL-1894 add licensing
[jalview.git] / examples / biojson-doc / docson.js
1 /*
2  * Copyright 2013 Laurent Bovet <laurent.bovet@windmaster.ch>
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 var docson = docson || {};
18
19 docson.templateBaseUrl="templates";
20
21 define(["lib/jquery", "lib/handlebars", "lib/highlight", "lib/jsonpointer", "lib/marked", "lib/traverse"], function(jquery, handlebars, highlight, jsonpointer, marked) {
22
23     var ready = $.Deferred();
24     var boxTemplate;
25     var signatureTemplate;
26     var source;
27     var stack = [];
28     var boxes=[];
29
30     Handlebars.registerHelper('scope', function(schema, options) {
31         var result;
32         boxes.push([]);
33         if(schema && (schema.id || schema.root)) {
34             stack.push( schema );
35             result = options.fn(this);
36             stack.pop();
37         } else {
38             result = options.fn(this);
39         }
40         boxes.pop();
41         return result;
42     });
43
44     Handlebars.registerHelper('source', function(schema) {
45         delete schema.root;
46         delete schema.__boxId;
47         delete schema.__name;
48         delete schema.__ref;
49         return JSON.stringify(schema, null, 2);
50     });
51
52     Handlebars.registerHelper('desc', function(schema) {
53         var description = schema.description;
54
55         if( !description ) return "";
56         var text = description;
57         if(marked) {
58             marked.setOptions({gfm: true, breaks: true})
59             return new Handlebars.SafeString(marked(text));
60         } else {
61             return text;
62         }
63     });
64
65     Handlebars.registerHelper('equals', function(lvalue, rvalue, options) {
66         if (arguments.length < 3)
67             throw new Error("Handlebars Helper equals needs 2 parameters");
68         if( lvalue!=rvalue ) {
69             return options.inverse(this);
70         } else {
71             return options.fn(this);
72         }
73     });
74
75     Handlebars.registerHelper('contains', function(arr, item, options) {;
76         if(arr && arr instanceof Array && arr.indexOf(item) != -1) {
77             return options.fn(this);
78         }
79     });
80
81     Handlebars.registerHelper('primitive', function(schema, options) {
82         if(schema.type && schema.type != "object" && schema.type != "array" || schema.enum) {
83             return withType(this, options, true)
84         }
85     });
86
87     Handlebars.registerHelper('exists', function(value, options) {
88         if(value !== undefined) {
89             value = value === null ? "null": value;
90             value = value === true ? "true": value;
91             value = value === false ? "false": value;
92             value = typeof value === "object" ? JSON.stringify(value): value;
93             this.__default = value;
94             var result = options.fn(this);
95             delete this.__default;
96             return result;
97         }
98     });
99
100     Handlebars.registerHelper('range', function(from, to, replFrom, replTo, exclFrom, exclTo, sep) {
101         var result = "";
102         if(from !== undefined || to !== undefined) {
103             result += exclFrom ? "]" : "[";
104             result += from !== undefined ? from : replFrom;
105             if( (from || replFrom) !== (to || replTo)) {
106                 result += (from !== undefined || replFrom !== null) && (to !== undefined || replTo !== null) ? sep : "";
107                 result += to !== undefined ? to : replTo;
108             }
109             result += exclTo ? "[" : "]";
110             return result;
111         }
112     });
113
114     var sub = function(schema) {
115         return schema.type == "array" || schema.allOf || schema.anyOf || schema.oneOf || schema.not;
116     }
117
118     Handlebars.registerHelper('sub', function(schema, options) {
119         if(sub(schema) || (schema.type && schema.type != "object" && schema.type != "array") || schema.enum) {
120             return options.fn(this);
121         }
122     });
123
124     Handlebars.registerHelper('main', function(schema, options) {
125         if(!sub(schema)) {
126             return options.fn(this);
127         }
128     });
129
130     var simpleSchema = function(schema) {
131         var result = schema.description===undefined && schema.title===undefined && schema.id===undefined;
132         result &= schema.properties===undefined;
133         return result;
134     };
135
136     Handlebars.registerHelper('simple', function(schema, options) {
137         if(simpleSchema(schema) && !schema.$ref) {
138             return withType(schema, options, true);
139         }
140     });
141
142     var withType = function(schema, options, hideAny) {
143         schema.__type = schema.type;
144         if(!schema.type && !hideAny) {
145             schema.__type="any";
146         }
147         if(schema.format) {
148             schema.__type=schema.format;
149         }
150         if( (schema.__type == "any" || schema.__type == "object") && schema.title) {
151             schema.__type = schema.title;
152         }
153         var result = options.fn(schema);
154         delete schema.__type;
155         return result;
156     }
157
158     Handlebars.registerHelper('complex', function(schema, options) {
159         if(!simpleSchema(schema) && !schema.$ref || schema.properties) {
160             return withType(schema, options);
161         }
162     });
163
164     Handlebars.registerHelper('enum', function(schema) {
165         if(schema.enum) {
166             return (schema.enum.length > 1) ? "enum": "constant";
167         }
168     });
169
170     Handlebars.registerHelper('obj', function(schema, options) {
171         if(schema.properties || schema.type == "object") {
172             return withType(schema, options);
173         }
174     });
175
176     var pushBox = function(schema) {
177         boxes[boxes.length-1].push(schema);
178     }
179
180     Handlebars.registerHelper('box', function(schema, options) {
181         if(schema) {
182             pushBox(schema);
183             return options.fn(schema);
184         }
185     });
186
187     Handlebars.registerHelper('boxId', function() {
188         return boxes[boxes.length-1].length
189     });
190
191     Handlebars.registerHelper('boxes', function(options) {
192         var result="";
193         $.each(boxes[boxes.length-1], function(k, box) {
194             box.__boxId = k+1;
195             result=result+options.fn(box);
196         });
197         boxes[boxes.length-1] = []
198         return result;
199     });
200
201     var resolveIdRef = function(ref) {
202         if(stack) {
203             var i;
204             for(i=stack.length-1; i>=0; i--) {
205                 if(stack[i][ref]) {
206                     return stack[i][ref];
207                 }
208             }
209         }
210         return null;
211     }
212
213     var resolvePointerRef = function(ref) {
214         var root = stack[1];
215         if(ref=="#") {
216             return root;
217         }
218         try {
219             return jsonpointer.get(stack[1], ref);
220         } catch(e) {
221             console.log(e);
222             return null;
223         }
224     }
225
226     var resolveRef = function(ref) {
227         if(ref.indexOf("#") == 0) {
228             return resolvePointerRef(ref);
229         } else {
230             return resolveIdRef(ref);
231         }
232     }
233
234     var getName = function(schema) {
235         if(!schema) {
236             return "<error>";
237         }
238         var name = schema.title;
239         name = !name && schema.id ? schema.id: name;
240         name = !name ? schema.__name: name;
241         return name;
242     }
243
244     Handlebars.registerHelper('name', function(schema, options) {
245         schema.__name = getName(schema);
246         if(schema.__name) {
247             return options.fn(schema);
248         }
249     });
250
251     var refName = function(ref) {
252         var name = getName(resolveRef(ref));
253         if(!name) {
254             if(ref == "#") {
255                 name = "<root>";
256             } else {
257                 name = ref.replace("#", "/")
258             }
259         }
260         var segments = name.split("/");
261         name = segments[segments.length-1];
262         return name;
263     }
264
265     function renderSchema(schema) {
266         if(stack.indexOf(schema) == -1) { // avoid recursion
267             stack.push(schema);
268             var ret = new Handlebars.SafeString(boxTemplate(schema));
269             stack.pop();
270             return ret;
271         } else {
272             return new Handlebars.SafeString(boxTemplate({"description": "_circular reference_"}));
273         }
274     }
275
276     Handlebars.registerHelper('ref', function(schema, options) {
277         if(schema.$ref) {
278             var target = resolveRef(schema.$ref);
279             if(target) {
280                 target.__name = refName(schema.$ref);
281                 target.__ref = schema.$ref.replace("#", "");
282             }
283             var result;
284             if(target) {
285                 result = options.fn(target);
286             } else {
287                 result = new Handlebars.SafeString("<span class='signature-type-ref'>"+schema.$ref+"</span>");
288             }
289             if(target) {
290                 delete target.__ref;
291             }
292             return result;
293         }
294     });
295
296     Handlebars.registerHelper('schema', function(schema) {
297         return renderSchema(schema);
298     });
299
300     Handlebars.registerHelper('signature', function(schema, keyword, schemas) {
301         if(!schemas) {
302             schemas = []
303         }
304         schemas = schemas instanceof Array ? schemas : [schemas];
305         return new Handlebars.SafeString(signatureTemplate({ schema: schema, keyword: keyword, schemas: schemas}));
306     });
307
308     Handlebars.registerHelper('l', function(context) {
309         console.log(context);
310     });
311
312     function init() {
313         $.when( $.get(docson.templateBaseUrl+"/box.html").done(function(content) {
314             source = content
315             boxTemplate = Handlebars.compile(source);
316         }), $.get(docson.templateBaseUrl+"/signature.html").done(function(content) {
317             source = content
318             signatureTemplate = Handlebars.compile(source);
319         })).always(function() {
320             ready.resolve();
321         });
322     };
323
324     docson.doc = function(element, schema, ref, baseUrl) {
325         var d = $.Deferred();
326         if(baseUrl === undefined) baseUrl='';
327         init();
328         ready.done(function() {
329             if(typeof element == "string") {
330                 element = $("#"+element);
331             }
332             if(typeof schema == "string") {
333                 schema = JSON.parse(schema);
334             }
335
336             var refsPromise = $.Deferred().resolve().promise();
337             var refs = {};
338
339
340             var renderBox = function() {
341                 stack.push(refs);
342                 var target = schema;
343                 if(ref) {
344                     ref = ref[0] !== '/' ? '/'+ref : ref;
345                     target = jsonpointer.get(schema, ref);
346                     stack.push( schema );
347                 }
348                 target.root = true;
349                 target.__ref = "<root>";
350                 var html = boxTemplate(target);
351
352                 if(ref) {
353                     stack.pop();
354                 }
355                 stack.pop();
356
357                 element.addClass("docson").html(html);
358
359                 var resizeHandler = element.get(0).onresize;
360                 function resized() {
361                     if(resizeHandler) {
362                         var box = element.find(".box").first();
363                         element.get(0).onresize(box.outerWidth(), box.outerHeight());
364                     }
365                 }
366                 element.get(0).resized = resized;
367                 resized();
368
369                 if(highlight) {
370                     element.find(".json-schema").each(function(k, schemaElement) {
371                         highlight.highlightSchema(schemaElement);
372                     });
373                 }
374                 element.find(".box-title").each(function() {
375                    var ref = $(this).attr("ref");
376                    if(ref) {
377                        if(window.location.href.indexOf("docson/index.html") > -1) {
378                            $(this).find(".box-name").css("cursor", "pointer").attr("title", "Open in new window")
379                            .hover(
380                                function(){ $(this).addClass('link') },
381                                function(){ $(this).removeClass('link') })
382                            .click(function() {
383                                 var url = window.location.href+"$$expand";
384                                 if(ref !=="<root>") {
385                                    url = url.replace(/(docson\/index.html#[^\$]*).*/, "$1$"+ref+"$$expand");
386                                 }
387                                 var w;
388                                 function receiveMessage(event) {
389                                    if (event.data.id && event.data.id == "docson" && event.data.action == "ready") {
390                                        w.postMessage({ id: "docson", action: "load", definitions: schema, type: event.data.url.split("$")[1], expand: true}, "*");
391                                    }
392                                 }
393                                 window.addEventListener("message", receiveMessage, false);
394                                 w = window.open(url, "_blank");
395                            });
396                        }
397                    }
398                 });
399                 element.find(".box").mouseenter(function() {
400                     $(this).children(".source-button").fadeIn(300);
401                     $(this).children(".box-body").children(".expand-button").fadeIn(300);
402                 });
403                 element.find(".box").mouseleave(function() {
404                     $(this).children(".source-button").fadeOut(300);
405                     $(this).children(".box-body").children(".expand-button").fadeOut(300);
406                 });
407                 element.find(".signature-type-expandable").click(function() {
408                     var boxId = $(this).attr("boxid");
409                     $(this).toggleClass("signature-type-expanded");
410                     $(this).parent().parent().parent().children(".signature-box-container").
411                         children("[boxid='"+boxId+"']").toggle(resizeHandler ? 0 : 300);
412                     resized();
413                 });
414                 element.find(".expand-button").click(function() {
415                     if($(this).attr("expanded")) {
416                         $(this).parent().parent().find(".expand-button").html(" + ").attr("title", "Expand all");
417                         $(this).parent().parent().find(".signature-type-expandable").removeClass("signature-type-expanded");
418                         $(this).parent().parent().find(".box-container").hide( resizeHandler ? 0 : 300);
419                         $(this).parent().parent().find(".expand-button").removeAttr("expanded");
420                         resized();
421                     } else {
422                         $(this).parent().parent().find(".expand-button").html(" - ").attr("title", "Collapse all");
423                         $(this).parent().parent().find(".signature-type-expandable").addClass("signature-type-expanded");
424                         $(this).parent().parent().find(".box-container").show(resizeHandler ? 0 : 300);
425                         $(this).parent().parent().find(".expand-button").attr("expanded", true);
426                         resized();
427                     }
428                 });
429                 element.find(".source-button").click(function() {
430                     $(this).parent().children(".box-body").toggle();
431                     $(this).parent().children(".source").toggle();
432                     resized();
433                 });
434             };
435
436             var resolveRefsReentrant = function(schema){
437                 traverse(schema).forEach(function(item) {
438                     // Fix Swagger weird generation for array.
439                     if(item && item.$ref == "array") {
440                         delete item.$ref;
441                         item.type ="array";
442                     }
443
444                     // Fetch external schema
445                     if(this.key === "$ref") {
446                         var external = false;
447                         //Local meaning local to this server, but not in this file.
448                         var local = false;
449                         if((/^https?:\/\//).test(item)) {
450                             external = true;
451                         }
452                         else if((/^[^#]/).test(item)) {
453                             local = true;
454                         } else if(item.indexOf('#') > 0) {
455                             //Internal reference
456                             //Turning relative refs to absolute ones
457                             external = true;
458                             item = baseUrl + item;
459                             this.update(item);
460                         }
461                         if(external){
462                             //External reference, fetch it.
463                             var segments = item.split("#");
464                             refs[item] = null;
465                             var p = $.get(segments[0]).then(function(content) {
466                                 if(typeof content != "object") {
467                                     try {
468                                         content = JSON.parse(content);
469                                     } catch(e) {
470                                         console.error("Unable to parse "+segments[0], e);
471                                     }
472                                 }
473                                 if(content) {
474                                     refs[item] = content;
475                                     renderBox();
476                                     resolveRefsReentrant(content); 
477                                 }
478                             });
479                         }
480                         else if(local) {
481                             //Local to this server, fetch relative
482                             var segments = item.split("#");
483                             refs[item] = null;
484                             var p = $.get(baseUrl + segments[0]).then(function(content) {
485                                 if(typeof content != "object") {
486                                     try {
487                                         content = JSON.parse(content);
488                                     } catch(e) {
489                                         console.error("Unable to parse "+segments[0], e);
490                                     }
491                                 }
492                                 if(content) {
493                                     refs[item] = content;
494                                     renderBox();
495                                     resolveRefsReentrant(content);
496                                 }
497                             });
498                         }
499                     }
500                 });
501             };
502             
503             resolveRefsReentrant(schema);
504             renderBox();
505             
506             d.resolve();
507         })
508         return d.promise();
509     }
510
511     return docson;
512 });