hal-browser
Advanced tools
Comparing version 0.3.0 to 0.4.0
@@ -0,1 +1,9 @@ | ||
0.4.0 (2018-08-09) | ||
================== | ||
* Support for `text/markdown` | ||
* Support for `text/csv` | ||
* Added default `code-repository` link to navigation. | ||
0.3.0 (2018-07-08) | ||
@@ -2,0 +10,0 @@ ================== |
import { Context } from '@curveball/core'; | ||
import { SureOptions } from './types'; | ||
export default function generateHtmlIndex(ctx: Context, body: any, options: SureOptions): void; | ||
export default function generateHtmlIndex(ctx: Context, body: any, options: SureOptions): Promise<void>; |
@@ -6,16 +6,23 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const highlight_js_1 = __importDefault(require("highlight.js")); | ||
const querystring_1 = __importDefault(require("querystring")); | ||
const url_1 = __importDefault(require("url")); | ||
function generateHtmlIndex(ctx, body, options) { | ||
const links = fetchLinks(body, options); | ||
const navHtml = generateNavigation(links, options); | ||
const pagerHtml = generatePager(links, options); | ||
const linksHtml = generateLinks(links, options); | ||
const csv_body_1 = __importDefault(require("./components/csv-body")); | ||
const hal_body_1 = __importDefault(require("./components/hal-body")); | ||
const links_table_1 = __importDefault(require("./components/links-table")); | ||
const markdown_body_1 = __importDefault(require("./components/markdown-body")); | ||
const navigation_1 = __importDefault(require("./components/navigation")); | ||
const pager_1 = __importDefault(require("./components/pager")); | ||
const search_1 = __importDefault(require("./components/search")); | ||
const util_1 = require("./util"); | ||
async function generateHtmlIndex(ctx, body, options) { | ||
const links = util_1.fetchLinks(body, options); | ||
const navHtml = navigation_1.default(links, options); | ||
const pagerHtml = pager_1.default(links, options); | ||
const linksHtml = links_table_1.default(links, options); | ||
const [headTitle, bodyTitle] = generateTitle(links, ctx, options); | ||
const bodyHtml = generateBody(body); | ||
const searchHtml = generateSearch(links, options); | ||
const bodyHtml = await parseBody(ctx); | ||
const searchHtml = search_1.default(links, options); | ||
const stylesheets = options.stylesheets.map(ss => { | ||
return ` <link rel="stylesheet" href="${h(url_1.default.resolve(options.assetBaseUrl, ss))}" type="text/css" />\n`; | ||
return ` <link rel="stylesheet" href="${util_1.h(url_1.default.resolve(options.assetBaseUrl, ss))}" type="text/css" />\n`; | ||
}).join(''); | ||
ctx.response.type = 'text/html; charset=utf-8'; | ||
ctx.response.body = ` | ||
@@ -40,8 +47,4 @@ <!DOCTYPE html> | ||
${linksHtml} | ||
<h2>Contents</h2> | ||
<code class="hljs"><pre>${bodyHtml}</pre></code> | ||
${bodyHtml} | ||
${pagerHtml} | ||
</main> | ||
@@ -54,117 +57,2 @@ | ||
exports.default = generateHtmlIndex; | ||
function h(input) { | ||
const map = { | ||
'&': '&', | ||
'<': '<', | ||
'>': '>', | ||
'"': '"' | ||
}; | ||
return input.replace(/&<>"/g, s => map[s]); | ||
} | ||
function syntaxHighlightJson(body) { | ||
return highlight_js_1.default.highlight('json', JSON.stringify(body, undefined, ' ')).value; | ||
} | ||
function generateBody(body) { | ||
const tmpBody = Object.assign(body); | ||
delete tmpBody._links; | ||
return syntaxHighlightJson(tmpBody); | ||
} | ||
function generateLinks(links, options) { | ||
let linkHtml = ''; | ||
// Grouping links by rel. | ||
const groups = {}; | ||
for (const link of links) { | ||
if (options.hiddenRels.includes(link.rel) || link.rel in options.navigationLinks) { | ||
continue; | ||
} | ||
if (groups[link.rel]) { | ||
groups[link.rel].push(link); | ||
} | ||
else { | ||
groups[link.rel] = [link]; | ||
} | ||
} | ||
for (const group of Object.values(groups)) { | ||
const linkCount = group.length; | ||
let first = true; | ||
for (const link of group) { | ||
linkHtml += '<tr>'; | ||
if (first) { | ||
linkHtml += `<td rowspan="${linkCount}">${h(link.rel)}</td>`; | ||
first = false; | ||
} | ||
linkHtml += `<td><a href="${h(link.href)}">${h(link.href)}</a></td>`; | ||
linkHtml += '<td>' + (link.title ? h(link.title) : '') + '</td>'; | ||
linkHtml += '</tr>\n'; | ||
} | ||
} | ||
if (!linkHtml) { | ||
// No links | ||
return ''; | ||
} | ||
return ` | ||
<h2>Links</h2> | ||
<table> | ||
<tr> | ||
<th>Relationship</th><th>Url</th><th>Title</th> | ||
</tr> | ||
${linkHtml} | ||
</table> | ||
`; | ||
} | ||
/** | ||
* Returns the list of links for a section. | ||
* | ||
* This function sorts and normalizes the link. | ||
*/ | ||
function getNavLinks(links, options, position) { | ||
const result = []; | ||
for (const link of links) { | ||
// Don't handle templated links. | ||
if (link.templated) { | ||
continue; | ||
} | ||
if (options.navigationLinks[link.rel] === undefined) { | ||
continue; | ||
} | ||
const nl = options.navigationLinks[link.rel]; | ||
if ((typeof nl.position === 'undefined' && position !== 'header') || | ||
(typeof nl.position !== 'undefined' && nl.position !== position)) { | ||
continue; | ||
} | ||
result.push({ | ||
rel: link.rel, | ||
href: link.href, | ||
title: link.title ? link.title : (nl.defaultTitle ? nl.defaultTitle : link.rel), | ||
icon: url_1.default.resolve(options.assetBaseUrl, nl.icon ? nl.icon : 'icon/' + link.rel + '.svg'), | ||
priority: nl.priority ? nl.priority : 0, | ||
showLabel: nl.showLabel, | ||
}); | ||
} | ||
return result.sort((a, b) => a.priority - b.priority); | ||
} | ||
function generateNavigation(links, options) { | ||
const html = []; | ||
for (const link of getNavLinks(links, options, 'header')) { | ||
html.push(`<a href="${h(link.href)}" rel="${h(link.rel)}" title="${h(link.title)}">` + | ||
`<img src="${link.icon}" />${link.showLabel ? ' ' + h(link.title) : ''}</a>`); | ||
} | ||
if (!html.length) { | ||
return ''; | ||
} | ||
const result = ' <ul>\n <li>' + html.join('</li>\n <li>') + ' </li>\n </ul>\n'; | ||
return result; | ||
} | ||
function generatePager(links, options) { | ||
const html = []; | ||
for (const link of getNavLinks(links, options, 'pager')) { | ||
html.push(`<a href="${h(link.href)}" rel="${h(link.rel)}" title="${h(link.title)}">` + | ||
`<img src="${h(link.icon)}" /> ${h(link.title)}</a>`); | ||
} | ||
if (!html.length) { | ||
return ''; | ||
} | ||
const result = ' <nav class="pager">\n <ul>\n <li>' + html.join('</li>\n <li>') + ' </li>\n </ul>\n </nav>\n'; | ||
return result; | ||
} | ||
function generateTitle(links, ctx, options) { | ||
@@ -191,72 +79,20 @@ const selfLink = links.find(link => link.rel === 'self'); | ||
return [ | ||
`${h(title)} - ${options.title}`, | ||
`<a href="${h(href)}" rel="self">${h(title)}</a> - ${options.title}` | ||
`${util_1.h(title)} - ${options.title}`, | ||
`<a href="${util_1.h(href)}" rel="self">${util_1.h(title)}</a> - ${options.title}` | ||
]; | ||
} | ||
function generateSearch(links, options) { | ||
const searchRel = links.find(link => link.rel === 'search'); | ||
if (searchRel === undefined) { | ||
return ''; | ||
async function parseBody(ctx) { | ||
switch (ctx.response.type) { | ||
case 'application/json': | ||
case 'application/problem+json': | ||
case 'application/hal+json': | ||
return hal_body_1.default(ctx.response.body); | ||
case 'text/markdown': | ||
return markdown_body_1.default(ctx.response.body); | ||
case 'text/csv': | ||
return csv_body_1.default(ctx.response.body); | ||
default: | ||
return ''; | ||
} | ||
if (!searchRel.templated) { | ||
return ''; | ||
} | ||
// We only support a very specific format. The link must be | ||
// templated, have at most 1 templated field, and that field must | ||
// appear in the query parameter. | ||
// | ||
// Sample format: | ||
// { | ||
// href: "/search?language=nl{?q}", | ||
// templated: true | ||
// } | ||
// | ||
// This regex might capture invalid templates, but it's not up to us to | ||
// validate it. | ||
const matches = searchRel.href.match(/^([^{]+){\?([a-zA-z0-9]+)}([^{]*)$/); | ||
if (matches === null) { | ||
return ''; | ||
} | ||
// Url with the template variable stripped. | ||
const newUrl = matches[1] + matches[3]; | ||
const [action, queryStr] = newUrl.split('?'); | ||
const query = querystring_1.default.parse(queryStr); | ||
let html = `<form method="GET" action="${h(action)}" class="search">`; | ||
html += '<img src="' + url_1.default.resolve(options.assetBaseUrl, 'icon/search.svg') + '" />'; | ||
for (const pair of Object.entries(query)) { | ||
html += `<input type="hidden" name="${h(pair[0])}" value="${h(pair[1])}" />`; | ||
} | ||
html += `<input type="search" placeholder="Search" name="${h(matches[2])}" />`; | ||
html += '</form>'; | ||
return html; | ||
} | ||
function fetchLinks(body, options) { | ||
const result = Array.from(options.defaultLinks); | ||
if (!body._links) { | ||
return result; | ||
} | ||
for (const rel of Object.keys(body._links)) { | ||
let linksTmp; | ||
if (Array.isArray(body._links[rel])) { | ||
linksTmp = body._links[rel]; | ||
} | ||
else { | ||
linksTmp = [body._links[rel]]; | ||
} | ||
for (const link of linksTmp) { | ||
if (!link.href) { | ||
// tslint:disable:no-console | ||
console.warn('Incorrect format for HAL link with rel: ' + rel); | ||
} | ||
result.push({ | ||
rel: rel, | ||
href: link.href, | ||
type: link.type, | ||
title: link.title, | ||
templated: link.templated, | ||
}); | ||
} | ||
} | ||
return result; | ||
} | ||
//# sourceMappingURL=html-index.js.map |
@@ -12,2 +12,4 @@ "use strict"; | ||
'application/problem+json', | ||
'text/markdown', | ||
'text/csv', | ||
]; | ||
@@ -30,3 +32,2 @@ /* | ||
* http://microformats.org/wiki/existing-rel-values | ||
* | ||
* - icon | ||
@@ -38,3 +39,3 @@ */ | ||
}, | ||
'core-repository': true, | ||
'code-repository': true, | ||
'create-form': { | ||
@@ -95,4 +96,3 @@ showLabel: true, | ||
if (ctx.request.accepts('text/html', ...parsedContentTypes) === 'text/html') { | ||
ctx.response.headers.set('Content-Type', 'text/html'); | ||
html_index_1.default(ctx, ctx.response.body, newOptions); | ||
await html_index_1.default(ctx, ctx.response.body, newOptions); | ||
} | ||
@@ -99,0 +99,0 @@ }; |
{ | ||
"name": "hal-browser", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "A HAL browser middleware", | ||
@@ -40,2 +40,3 @@ "main": "dist/index.js", | ||
"@types/highlight.js": "^9.12.3", | ||
"@types/markdown-it": "0.0.4", | ||
"@types/mocha": "^5.2.3", | ||
@@ -60,4 +61,6 @@ "@types/node": "^10.3.6", | ||
"@curveball/core": "^0.4.1", | ||
"highlight.js": "^9.12.0" | ||
"csv-parse": "^2.5.0", | ||
"highlight.js": "^9.12.0", | ||
"markdown-it": "^8.4.1" | ||
} | ||
} |
@@ -12,3 +12,10 @@ HAL browser | ||
It automatically decorates the following formats: | ||
* `application/json` | ||
* `application/problem+json` | ||
* `application/hal+json` | ||
* `text/markdown` | ||
Screenshot | ||
@@ -150,3 +157,2 @@ ---------- | ||
* Add a link to allow the user to see the raw format. | ||
* Support displaying `text/csv` as a table. | ||
* Support HTTP Link headers. | ||
@@ -153,0 +159,0 @@ * A better interface for `_embedded`. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
69728
56
1003
165
4
13
1
+ Addedcsv-parse@^2.5.0
+ Addedmarkdown-it@^8.4.1
+ Addedargparse@1.0.10(transitive)
+ Addedcsv-parse@2.5.0(transitive)
+ Addedentities@1.1.2(transitive)
+ Addedlinkify-it@2.2.0(transitive)
+ Addedmarkdown-it@8.4.2(transitive)
+ Addedmdurl@1.0.1(transitive)
+ Addedsprintf-js@1.0.3(transitive)
+ Addeduc.micro@1.0.6(transitive)