Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate DOM for markdown links in help text #1979

Merged
merged 7 commits into from
Apr 12, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion lighthouse-core/report/v2/report-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ class ReportRenderer {
`lighthouse-score__value--${scoringMode}`);

element.querySelector('.lighthouse-score__title').textContent = title;
element.querySelector('.lighthouse-score__description').textContent = description;
element.querySelector('.lighthouse-score__description')
.appendChild(this._convertMarkdownLinksToElement(description));

return element;
}
Expand Down Expand Up @@ -180,6 +181,39 @@ class ReportRenderer {
}
}

/**
* @param {string} text
* @return {!HTMLSpanElement}
*/
_convertMarkdownLinksToElement(text) {
const element = this._createElement('span');

// Split on markdown links (e.g. [some link](https://...)).
const parts = text.split(/\[(.*?)\]\((https?:\/\/.*?)\)/g);

while (parts.length) {
// Pop off the same number of elements as there are capture groups.
const [preambleText, linkText, linkHref] = parts.splice(0, 3);
element.appendChild(this._document.createTextNode(preambleText));

// Append link if there are any.
if (linkText && linkHref) {
const a = this._createElement('a');
a.rel = 'noopener';
a.target = '_blank';
a.textContent = linkText;
try {
a.href = (new URL(linkHref)).href;
element.appendChild(a);
} catch (e) {
element.appendChild(this._document.createTextNode(linkText));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we fail silently here? Seems like a big enough mistake that it should complain loudly and end up in _renderException

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to fail silently here. If a link is busted for some reason, it shouldn't block the whole report from being generated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but why is the link busted? Right now, at least, they're all coming from helpText. An error in one of those should be fixed immediately

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

FWIW, b/c of the regex, the only time we'd get an invalid URL is if someone wrote https://. or used something crazy in help text like [boom](🍣🍺).

}
}
}

return element;
}

/**
* @param {!DetailsJSON} text
* @return {!Element}
Expand Down
110 changes: 76 additions & 34 deletions lighthouse-core/test/report/v2/report-renderer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,87 @@
const assert = require('assert');
const fs = require('fs');
const jsdom = require('jsdom');
const URL = require('../../../lib/url-shim');
const ReportRenderer = require('../../../report/v2/report-renderer.js');
const sampleResults = require('../../results/sample_v2.json');

const TEMPLATE_FILE = fs.readFileSync(__dirname + '/../../../report/v2/templates.html', 'utf8');

describe('ReportRenderer V2', () => {
describe('renderReport', () => {
const document = jsdom.jsdom(TEMPLATE_FILE);
const renderer = new ReportRenderer(document);
before(() => {
global.URL = URL;
});

after(() => {
global.URL = undefined;
});

const document = jsdom.jsdom(TEMPLATE_FILE);
const renderer = new ReportRenderer(document);

describe('createElement', () => {
it('creates a simple element using default values', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's new and what's not is very confusing when you move things around :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like to keep you on your toes.

const el = renderer._createElement('div');
assert.equal(el.localName, 'div');
assert.equal(el.className, '');
assert.equal(el.className, el.attributes.length);
});

it('creates an element from parameters', () => {
const el = renderer._createElement(
'div', 'class1 class2', {title: 'title attr', tabindex: 0});
assert.equal(el.localName, 'div');
assert.equal(el.className, 'class1 class2');
assert.equal(el.getAttribute('title'), 'title attr');
assert.equal(el.getAttribute('tabindex'), '0');
});
});

describe('cloneTemplate', () => {
it('should clone a template', () => {
const clone = renderer._cloneTemplate('#tmpl-lighthouse-audit-score');
assert.ok(clone.querySelector('.lighthouse-score'));
});

it('fails when template cannot be found', () => {
assert.throws(() => renderer._cloneTemplate('#unknown-selector'));
});
});

describe('_convertMarkdownLinksToElement', () => {
it('correctly converts links', () => {
let result = renderer._convertMarkdownLinksToElement(
'Some [link](https://example.com/foo). [Learn more](http://example.com).');
assert.equal(result.innerHTML,
'Some <a rel="noopener" target="_blank" href="https://example.com/foo">link</a>. ' +
'<a rel="noopener" target="_blank" href="http://example.com/">Learn more</a>.');

result = renderer._convertMarkdownLinksToElement('[link](https://example.com/foo)');
assert.equal(result.innerHTML,
'<a rel="noopener" target="_blank" href="https://example.com/foo">link</a>',
'just a link');

result = renderer._convertMarkdownLinksToElement(
'[ Link ](https://example.com/foo) and some text afterwards.');
assert.equal(result.innerHTML,
'<a rel="noopener" target="_blank" href="https://example.com/foo"> Link </a> ' +
'and some text afterwards.', 'link with spaces in brackets');
});

it('handles invalid urls', () => {
const text = 'Text has [bad](https:///) link.';
const result = renderer._convertMarkdownLinksToElement(text);
assert.equal(result.innerHTML, 'Text has bad link.');
});

it('ignores links that do not start with http', () => {
const text = 'Sentence with [link](/local/path).';
const result = renderer._convertMarkdownLinksToElement(text);
assert.equal(result.innerHTML, text);
});
});

describe('renderReport', () => {
it('should render a report', () => {
const output = renderer.renderReport(sampleResults);
assert.ok(output.classList.contains('lighthouse-report'));
Expand All @@ -44,35 +115,6 @@ describe('ReportRenderer V2', () => {
assert.ok(output.classList.contains('lighthouse-exception'));
});

describe('createElement', () => {
it('creates a simple element using default values', () => {
const el = renderer._createElement('div');
assert.equal(el.localName, 'div');
assert.equal(el.className, '');
assert.equal(el.className, el.attributes.length);
});

it('creates an element from parameters', () => {
const el = renderer._createElement(
'div', 'class1 class2', {title: 'title attr', tabindex: 0});
assert.equal(el.localName, 'div');
assert.equal(el.className, 'class1 class2');
assert.equal(el.getAttribute('title'), 'title attr');
assert.equal(el.getAttribute('tabindex'), '0');
});
});

describe('cloneTemplate', () => {
it('should clone a template', () => {
const clone = renderer._cloneTemplate('#tmpl-lighthouse-audit-score');
assert.ok(clone.querySelector('.lighthouse-score'));
});

it('fails when template cannot be found', () => {
assert.throws(() => renderer._cloneTemplate('#unknown-selector'));
});
});

it('renders an audit', () => {
const audit = sampleResults.reportCategories[0].audits[0];
const auditDOM = renderer._renderAudit(audit);
Expand All @@ -82,7 +124,7 @@ describe('ReportRenderer V2', () => {
const score = auditDOM.querySelector('.lighthouse-score__value');

assert.equal(title.textContent, audit.result.description);
assert.equal(description.textContent, audit.result.helpText);
assert.ok(description.querySelector('a'), 'audit help text contains coverted markdown links');
assert.equal(score.textContent, '0');
assert.ok(score.classList.contains('lighthouse-score__value--fail'));
assert.ok(score.classList.contains(`lighthouse-score__value--${audit.result.scoringMode}`));
Expand All @@ -102,7 +144,7 @@ describe('ReportRenderer V2', () => {
'category score is numeric');
assert.equal(value.textContent, Math.round(category.score), 'category score is rounded');
assert.equal(title.textContent, category.name, 'title is set');
assert.equal(description.textContent, category.description, 'description is set');
assert.ok(description.querySelector('a'), 'description contains converted markdown links');

const audits = categoryDOM.querySelectorAll('.lighthouse-category > .lighthouse-audit');
assert.equal(audits.length, category.audits.length, 'renders correct number of audits');
Expand Down