TYPE_GROUP_PAGE
or TYPE_USER_PAGE
.
30 | *
31 | * @alias type
32 | * @memberOf Apiary
33 | */
34 | var type;
35 |
36 | /**
37 | * ID of the entity being used on the page.
38 | *
39 | * @alias id
40 | * @memberOf Apiary
41 | */
42 | var id;
43 |
44 | /**
45 | * “Mode” (either “debug” or “production”; the latter by default).
46 | *
47 | * @alias mode
48 | * @memberOf Apiary
49 | */
50 | var mode = MODE_PRODUCTION;
51 |
52 | /**
53 | * Dictionary of placeholders found on the page, and all DOM elements associated to each one of them.
54 | *
55 | * @alias placeholders
56 | * @memberOf Apiary
57 | */
58 | var placeholders = {};
59 |
60 | /**
61 | * Simple cache of HTTP calls to the API, to avoid redundancy and save on requests.
62 | *
63 | * @alias cache
64 | * @memberOf Apiary
65 | */
66 | var cache = {};
67 |
68 | /**
69 | * Main function, invoked once after the document is completely loaded.
70 | *
71 | * @alias process
72 | * @memberOf Apiary
73 | */
74 | var process = function() {
75 | if (window.removeEventListener)
76 | window.removeEventListener('load', process);
77 | else if (window.detachEvent)
78 | window.detachEvent('onload', process);
79 | console.log(`Apiary version ${VERSION}`);
80 | inferTypeAndId();
81 | if (type && id) {
82 | findPlaceholders();
83 | getDataForType();
84 | } else {
85 | window.alert('Apiary ' + VERSION + '\n' +
86 | 'ERROR: could not get all necessary metadata.\n' +
87 | 'type: “' + type + '”\n' +
88 | 'id: “' + id + '”');
89 | }
90 | };
91 |
92 | /**
93 | * Infer the type of page (group, user…) and the ID of the corresponding entity.
94 | *
95 | * After this function is done, variables type
and id
should have their right values set.
96 | *
97 | * @alias inferTypeAndId
98 | * @memberOf Apiary
99 | */
100 | var inferTypeAndId = function() {
101 | if (document.querySelectorAll('[data-apiary-group]').length > 0) {
102 | type = TYPE_GROUP_PAGE;
103 | id = document.querySelectorAll('[data-apiary-group]')[0].getAttribute('data-apiary-group');
104 | } else if (document.querySelectorAll('[data-apiary-user]').length > 0) {
105 | type = TYPE_USER_PAGE;
106 | id = document.querySelectorAll('[data-apiary-user]')[0].getAttribute('data-apiary-user');
107 | }
108 | if (1 === document.querySelectorAll('html[data-apiary-mode]').length) {
109 | mode = document.querySelectorAll('html[data-apiary-mode]')[0].getAttribute('data-apiary-mode');
110 | }
111 | };
112 |
113 | /**
114 | * Traverse the DOM in search of all elements with class apiary-*
.
115 | *
116 | * After this function is done, placeholders
should be an object containing all keys found in the DOM;
117 | * and for every key, an array of all elements mentioning that key.
118 | *
119 | * @example
120 | * {
121 | * name: [
122 | * element
124 | * ],
125 | * lead: [ element],
126 | * groups: [ element]
127 | * }
128 | *
129 | * @alias findPlaceholders
130 | * @memberOf Apiary
131 | */
132 | var findPlaceholders = function() {
133 | var candidates = document.querySelectorAll(APIARY_SELECTOR);
134 | var expression;
135 | for (var c = 0; c < candidates.length; c ++) {
136 | expression = candidates[c].getAttribute(APIARY_ATTRIBUTE);
137 | if (!placeholders[expression])
138 | placeholders[expression] = [];
139 | placeholders[expression].push(candidates[c]);
140 | }
141 | if (MODE_DEBUG === mode)
142 | console.debug(`placeholders:\n${JSON.stringify(placeholders)}`);
143 | };
144 |
145 | /**
146 | * Get basic data for a particular entity from the W3C API, given a type of item and its value.
147 | *
148 | * @alias getDataForType
149 | * @memberOf Apiary
150 | */
151 | var getDataForType = function() {
152 | if (Object.keys(placeholders).length > 0) {
153 | if (TYPE_GROUP_PAGE === type) {
154 | get(BASE_URL + 'groups/' + id);
155 | } else if (TYPE_USER_PAGE === type) {
156 | get(BASE_URL + 'users/' + id);
157 | }
158 | }
159 | };
160 |
161 | /**
162 | * Crawl the API dynamically, traversing segments in placeholders.
163 | *
164 | * @param {Object} json JSON coming from an API call.
165 | *
166 | * @alias crawl
167 | * @memberOf Apiary
168 | */
169 | var crawl = function(json) {
170 | var i, keys, key, prefix, rest;
171 | keys = Object.keys(placeholders);
172 | for (key in keys) {
173 | i = keys[key];
174 | if (json.hasOwnProperty(i)) {
175 | if ('object' === typeof json[i] && 1 === Object.keys(json[i]).length && json[i].hasOwnProperty('href')) {
176 | get(json[i].href);
177 | } else {
178 | injectValues(i, json[i]);
179 | }
180 | } else if (i.indexOf(' ') > -1) {
181 | prefix = i.substr(0, i.indexOf(' '));
182 | rest = i.substr(i.indexOf(' ') + 1);
183 | if (MODE_DEBUG === mode)
184 | console.debug(i, prefix, rest);
185 | if (json.hasOwnProperty(prefix)) {
186 | if ('object' === typeof json[prefix] && 1 === Object.keys(json[prefix]).length && json[prefix].hasOwnProperty('href'))
187 | get(json[prefix].href);
188 | else
189 | injectValues(i, json[prefix], rest);
190 | }
191 | }
192 | }
193 | };
194 |
195 | var interpolateString = function(expression) {
196 | const expregex = /\$\{([^\}]+)\}/g;
197 | var exp, result = expression;
198 | while (exp = expregex.exec(expression))
199 | if (this.hasOwnProperty(exp[1]))
200 | result = result.replace(exp[0], this[exp[1]]);
201 | return result;
202 | };
203 |
204 | var escapeHTML = function(string) {
205 | const tags = /<[^>]*>/g,
206 | ampersands = /&/g,
207 | dquotes = /'/g,
208 | squotes = /"/g;
209 | return string.replace(tags, '').replace(ampersands, '&').replace(dquotes, '"').replace(squotes, ''');
210 | };
211 |
212 | var formatEntity = function(entity, expression) {
213 | var result;
214 | // @TODO: get rid of these special checks when there's a smarter algorithm for hyperlinks.
215 | if (expression) {
216 | result = '' + interpolateString.call(entity, expression) + ' ';
217 | } else if (entity.hasOwnProperty('_links') && entity._links.hasOwnProperty('homepage') &&
218 | entity._links.homepage.hasOwnProperty('href') && entity.hasOwnProperty('name')) {
219 | // It's a group.
220 | result = '' + entity.name + ' ';
221 | } else if (entity.hasOwnProperty('discr') && 'user' === entity.discr &&
222 | entity.hasOwnProperty('id') && entity.hasOwnProperty('name')) {
223 | // It's a user.
224 | result = '' + (entity['work-title'] ? entity['work-title'] + ' ' : '') + entity.name + ' ';
225 | } else if (entity.hasOwnProperty('shortlink') && entity.hasOwnProperty('title')) {
226 | // Spec:
227 | result = '' + entity.title;
230 | result += (entity.shortname ? ' (' + entity.shortname + '
)' : '');
231 | result += ' ';
232 | } else if (entity.hasOwnProperty('name')) {
233 | result = '' + entity.name + ' ';
234 | } else if (entity.hasOwnProperty('title')) {
235 | result = '' + entity.title + ' ';
236 | } else if (entity.hasOwnProperty('href') && entity.hasOwnProperty('title')) {
237 | result = '' + entity.title + ' ';
238 | } else if (entity.hasOwnProperty('href') && entity.hasOwnProperty('name')) {
239 | result = '' + entity.name + '';
240 | }
241 | return result;
242 | };
243 |
244 | /*
245 | * Function: discr="function", name, staging, is-closed, _links.{lead.title, homepage.href?}.
246 | * User: discr="user", family, given, id, name, work-title?, _links.self.href.
247 | * Participation: created, individual, _links.(group.(href, title), self.href, user.(href, title)?, organization.(href, title)?).
248 | * Group: discr="w3cgroup", description, id, name, type.
249 | * Affiliation: discr="affiliation"|"organization", id, name.
250 | * Spec: shortname, title, description?.
251 | * Version: {_embedded.versions}[date, status, _links.self.href].
252 | * Service: link, type, shortdesc?.
253 | * Charter: start, end, initial-end.
254 | *
255 | * f=all: functions.
256 | * g=all: groups.
257 | * s=all: specs.
258 | * a=all: affiliations.
259 | * f=6823109: users, services.
260 | * g=68239: users x 3, services, specs, charters, participations.
261 | * g=46300&c=155: (none).
262 | * s=dwbp: versions.
263 | * s=2dcontext&v=20110525: groups, users, versions x 2.
264 | * u=ggdj8tciu9kwwc4o4ww888ggkwok0c8: participations, groups, specs, affiliations.
265 | * x=1913: groups.
266 | * p=1503: users.
267 | * a=52794: users, groups.
268 | */
269 |
270 | /**
271 | * @TODO
272 | */
273 |
274 | var renderItem = function(entity, type) {
275 | if (!entity)
276 | return window.alert('Error: tried to render an undefined item')
277 | var result;
278 | if ('function' === entity.discr) {
279 | // Function:
280 | result = `
281 |
282 | ${entity.name}, led by ${entity._links.lead.title}
283 |
284 | `;
285 | } else if ('user' === entity.discr) {
286 | // User:
287 | var prefix = entity['work-title'] ? `, ${entity['work-title']}` : '';
288 | result = `\
289 | \
290 | ${entity.name}${prefix}\
291 | \
292 | `;
293 | } else if (entity.hasOwnProperty('created') && entity.hasOwnProperty('individual')) {
294 | // Participation:
295 | var label;
296 | if (TYPE_GROUP === type) {
297 | // We're interested in organisations and users:
298 | if (entity.individual)
299 | // Person:
300 | label = `${entity._links.user.title} (individual)`;
301 | else
302 | // Organisation:
303 | label = `${entity._links.organization.title} (organization)`;
304 | } else
305 | // TYPE_USER === type || TYPE_AFFILIATION === type; we're interested in groups:
306 | label = entity._links.group.title;
307 | result = `\
308 | \
309 | ${label}\
310 | \
311 | `;
312 | } else if ('w3cgroup' === entity.discr) {
313 | // Group:
314 | var descr = entity.description ? ` title="${escapeHTML(entity.description)}"` : '',
315 | type = '';
316 | result = `\
317 | \
318 | ${entity.name}${type}\
319 | \
320 | `;
321 | } else if (entity.hasOwnProperty('discr') && ('affiliation' === entity.discr || 'organization' === entity.discr)) {
322 | // Affiliation:
323 | result = `\
324 | \
325 | ${entity.name}\
326 | \
327 | `;
328 | } else if (entity.hasOwnProperty('shortname') && entity.hasOwnProperty('title')) {
329 | // Spec:
330 | var descr = entity.description ? ` title="${escapeHTML(entity.description)}"` : '';
331 | result = `\
332 | \
333 | ${entity.title} (${entity.shortname}
)\
334 | \
335 | `;
336 | } else if (entity.hasOwnProperty('date') && entity.hasOwnProperty('status')) {
337 | // Version:
338 | result = `\
339 | \
340 | ${entity.date} (${entity.status})\
341 | \
342 | `;
343 | } else if (entity.hasOwnProperty('link') && entity.hasOwnProperty('type')) {
344 | // Service:
345 | if ('lists' === entity.type && entity.hasOwnProperty('shortdesc')) {
346 | // Mailing list:
347 | result = `
348 |
349 | ${entity.shortdesc}
350 | (mailing list)
351 |
352 | `;
353 | } else if ('blog' === entity.type && entity.hasOwnProperty('shortdesc')) {
354 | // Blog:
355 | result = `
356 |
357 | ${entity.shortdesc}
358 | (blog)
359 |
360 | `;
361 | } else if ('tracker' === entity.type || 'repository' === entity.type || 'wiki' === entity.type || 'chat' === entity.type) {
362 | // Tracker, repo, wiki or chat:
363 | result = `
364 |
365 | ${normaliseURI(entity.link)}
366 | (${entity.type})
367 |
368 | `;
369 | } else if ('rss' === entity.type) {
370 | // RSS:
371 | result = `
372 |
373 | ${normaliseURI(entity.link)}
374 | (RSS)
375 |
376 | `;
377 | } else {
378 | result = `[Unknown type of service] \n`;
379 | }
380 | } else if (entity.hasOwnProperty('start') && entity.hasOwnProperty('end')) {
381 | // Charter:
382 | result = `\
383 | \
384 | ${entity.start} – ${entity.end}\
385 | \
386 | `;
387 | } else
388 | return '[Type of item not supported yet] \n';
389 | return result;
390 | };
391 |
392 | /**
393 | * Inject values retrieved from the API into the relevant elements of the DOM.
394 | *
395 | * @param {String} key ID of the placeholder.
396 | * @param {Object} value actual value for that piece of data.
397 | * @param {Array} expression list of fields to use (optional).
398 | *
399 | * @alias injectValues
400 | * @memberOf Apiary
401 | */
402 | var injectValues = function(key, value, expression) {
403 | if (MODE_DEBUG === mode)
404 | console.debug(`injectValues:\n${JSON.stringify(key)}\n${JSON.stringify(value)}\n${JSON.stringify(expression)}`);
405 | var chunk;
406 | if ('string' === typeof value || 'number' === typeof value) {
407 | chunk = String(value);
408 | } else if (value instanceof Array) {
409 | chunk = getLargestPhoto(value);
410 | if (!chunk) {
411 | chunk = '';
412 | for (var i = 0; i < value.length; i ++)
413 | chunk += formatEntity(value[i], expression);
414 | chunk += '
';
415 | }
416 | } else if ('object' === typeof value) {
417 | chunk = formatEntity(value, expression);
418 | }
419 | for (var i in placeholders[key]) {
420 | placeholders[key][i].innerHTML = chunk;
421 | placeholders[key][i].classList.add('apiary-done');
422 | }
423 | delete placeholders[key];
424 | };
425 |
426 | /**
427 | * GET data from the API, using the API key, and process the flattened version.
428 | *
429 | * @param {String} url target URL, including base URL and parameters, but not an API key.
430 | *
431 | * @alias get
432 | * @memberOf Apiary
433 | */
434 | var get = function(url) {
435 | if (MODE_DEBUG === mode)
436 | console.debug('get:\n' + JSON.stringify(url));
437 | var newUrl = url;
438 | if (-1 === newUrl.indexOf('?')) {
439 | newUrl += '?embed=true';
440 | } else {
441 | newUrl += '&embed=true';
442 | }
443 | if (cache.hasOwnProperty(newUrl)) {
444 | crawl(cache[newUrl]);
445 | } else {
446 | var xhr = new XMLHttpRequest();
447 | xhr.open('GET', newUrl);
448 | xhr.addEventListener('loadend', function(event) {
449 | var result = JSON.parse(xhr.response);
450 | var i, j;
451 | for (i of ['_links', '_embedded']) {
452 | if (result.hasOwnProperty(i)) {
453 | for (j in result[i]) {
454 | if (result[i].hasOwnProperty(j)) {
455 | result[j] = result[i][j];
456 | }
457 | }
458 | delete result[i];
459 | }
460 | }
461 | cache[newUrl] = result;
462 | crawl(result);
463 | });
464 | xhr.send();
465 | }
466 | };
467 |
468 | /**
469 | * Find the largest photo available from an array of them, and return an IMG element.
470 | *
471 | * @param {Array} data list of photos provided.
472 | * @returns {String} chunk of text corresponding to a new <img>
node with the photo.
473 | *
474 | * @alias getLargestPhoto
475 | * @memberOf Apiary
476 | */
477 | var getLargestPhoto = function(data) {
478 | var largest, result;
479 | if (data && data.length > 0) {
480 | for (var i = 0; i < data.length; i ++) {
481 | if (data[i].href && data[i].name && (!largest || PHOTO_VALUE[data[i].name] > PHOTO_VALUE[largest.name])) {
482 | largest = data[i];
483 | }
484 | }
485 | if (largest) {
486 | result = '
';
487 | }
488 | }
489 | return result;
490 | };
491 |
492 | // Process stuff!
493 | if (window.addEventListener)
494 | window.addEventListener('load', process);
495 | else if (window.attachEvent)
496 | window.attachEvent('onload', process);
497 |
498 | })(window);
499 |
--------------------------------------------------------------------------------
/examples/group.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | [Loading…]
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | [Loading…]
21 |
22 | [Loading…]
23 |
24 |
25 | Chartered –
26 |
27 | [Loading…]
28 | Chairs
29 | [Loading…]
30 | Team contacts
31 | [Loading…]
32 | Specs
33 | Hover over specs to view descriptions.
34 | [Loading…]
35 | Members
36 | [Loading…]
37 | Participants
38 | [Loading…]
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/examples/highlight.css:
--------------------------------------------------------------------------------
1 |
2 | * {
3 | box-sizing: border-box;
4 | margin: 0;
5 | border: 0;
6 | padding: 0;
7 | font-family: 'Lucida Sans Unicode', 'Lucida Grande', 'Trebuchet MS', Helvetica, Verdana, Geneva, Calibri, sans-serif;
8 | }
9 |
10 | body > div > * {
11 | margin: 0;
12 | border: 0;
13 | padding: 0.25em 8rem;
14 | }
15 |
16 | code {
17 | font-family: monospace;
18 | }
19 |
20 | [data-apiary] {
21 | background-color: #ffe0e0;
22 | }
23 |
24 | .apiary-done {
25 | background-color: #e0ffe0;
26 | }
27 |
--------------------------------------------------------------------------------
/examples/user.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | [Loading…]
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | [Loading…],
21 | [Loading…]
22 | [Loading…]
23 |
24 | [Loading…]
25 | Specs contributed to
26 | Hover over specs to view descriptions.
27 | [Loading…]
28 | Member of groups
29 | [Loading…]
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
151 |
--------------------------------------------------------------------------------
/w3c.json:
--------------------------------------------------------------------------------
1 | {
2 | "contacts": ["deniak", "jean-gui", "vivienlacourba"],
3 | "repo-type": "tool"
4 | }
5 |
--------------------------------------------------------------------------------
' + entity.shortname + '
)' : '');
231 | result += '${entity.shortname}
)\
334 | \
335 | ${entity.shortdesc}
350 | (mailing list)
351 |
352 | ${normaliseURI(entity.link)}
366 | (${entity.type})
367 |
368 | ${normaliseURI(entity.link)}
374 | (RSS)
375 |
376 | - ';
412 | for (var i = 0; i < value.length; i ++)
413 | chunk += formatEntity(value[i], expression);
414 | chunk += '
<img>
node with the photo.
473 | *
474 | * @alias getLargestPhoto
475 | * @memberOf Apiary
476 | */
477 | var getLargestPhoto = function(data) {
478 | var largest, result;
479 | if (data && data.length > 0) {
480 | for (var i = 0; i < data.length; i ++) {
481 | if (data[i].href && data[i].name && (!largest || PHOTO_VALUE[data[i].name] > PHOTO_VALUE[largest.name])) {
482 | largest = data[i];
483 | }
484 | }
485 | if (largest) {
486 | result = '
20 | [Loading…]
21 |
22 | [Loading…]
23 |
24 | 25 | Chartered – 26 |
27 |[Loading…]
28 |Chairs
29 |Team contacts
31 |Specs
33 |Hover over specs to view descriptions.
34 |Members
36 |Participants
38 |20 | [Loading…], 21 | [Loading…] 22 | [Loading…] 23 |
24 |Specs contributed to
26 |Hover over specs to view descriptions.
27 |