Commit 8a5ab37d authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #1 from hypothesis/direct-document-link-from-stream

Show direct links on stream page when available (FIRST!)
parents 91b9a22d c4dea460
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
* uri, domain and title. * uri, domain and title.
* *
*/ */
function extractDocumentMetadata(annotation) { function documentMetadata(annotation) {
var uri = annotation.uri; var uri = annotation.uri;
var domain = new URL(uri).hostname; var domain = new URL(uri).hostname;
var title = domain; var title = domain;
...@@ -21,8 +21,8 @@ function extractDocumentMetadata(annotation) { ...@@ -21,8 +21,8 @@ function extractDocumentMetadata(annotation) {
title = annotation.document.title[0]; title = annotation.document.title[0];
} }
if (title.length > 30) { if (domain === 'localhost') {
title = title.slice(0, 30) + '…'; domain = '';
} }
return { return {
...@@ -32,6 +32,61 @@ function extractDocumentMetadata(annotation) { ...@@ -32,6 +32,61 @@ function extractDocumentMetadata(annotation) {
}; };
} }
/**
* Return the domain and title of an annotation for display on an annotation
* card.
*/
function domainAndTitle(annotation) {
return {
domain: domainTextFromAnnotation(annotation),
titleText: titleTextFromAnnotation(annotation),
titleLink: titleLinkFromAnnotation(annotation),
};
}
function titleLinkFromAnnotation(annotation) {
var titleLink = annotation.uri;
if (titleLink && !(titleLink.indexOf('http://') === 0 || titleLink.indexOf('https://') === 0)) {
// We only link to http(s) URLs.
titleLink = null;
}
if (annotation.links && annotation.links.incontext) {
titleLink = annotation.links.incontext;
}
return titleLink;
}
function domainTextFromAnnotation(annotation) {
var document = documentMetadata(annotation);
var domainText = '';
if (document.uri && document.uri.indexOf('file://') === 0 && document.title) {
var parts = document.uri.split('/');
var filename = parts[parts.length - 1];
if (filename) {
domainText = filename;
}
} else if (document.domain && document.domain !== document.title) {
domainText = document.domain;
}
return domainText;
}
function titleTextFromAnnotation(annotation) {
var document = documentMetadata(annotation);
var titleText = document.title;
if (titleText.length > 30) {
titleText = titleText.slice(0, 30) + '…';
}
return titleText;
}
/** Return `true` if the given annotation is a reply, `false` otherwise. */ /** Return `true` if the given annotation is a reply, `false` otherwise. */
function isReply(annotation) { function isReply(annotation) {
return (annotation.references || []).length > 0; return (annotation.references || []).length > 0;
...@@ -78,7 +133,8 @@ function location(annotation) { ...@@ -78,7 +133,8 @@ function location(annotation) {
} }
module.exports = { module.exports = {
extractDocumentMetadata: extractDocumentMetadata, documentMetadata: documentMetadata,
domainAndTitle: domainAndTitle,
isAnnotation: isAnnotation, isAnnotation: isAnnotation,
isNew: isNew, isNew: isNew,
isPageNote: isPageNote, isPageNote: isPageNote,
......
...@@ -4,14 +4,11 @@ ...@@ -4,14 +4,11 @@
var angular = require('angular'); var angular = require('angular');
var annotationMetadata = require('../annotation-metadata'); var annotationMetadata = require('../annotation-metadata');
var documentDomain = require('../filter/document-domain');
var documentTitle = require('../filter/document-title');
var events = require('../events'); var events = require('../events');
var persona = require('../filter/persona'); var persona = require('../filter/persona');
var isNew = annotationMetadata.isNew; var isNew = annotationMetadata.isNew;
var isReply = annotationMetadata.isReply; var isReply = annotationMetadata.isReply;
var extractDocumentMetadata = annotationMetadata.extractDocumentMetadata;
/** Return a human-readable error message for the given server error. /** Return a human-readable error message for the given server error.
* *
...@@ -115,9 +112,7 @@ function updateViewModel($scope, domainModel, ...@@ -115,9 +112,7 @@ function updateViewModel($scope, domainModel,
vm.isPrivate = permissions.isPrivate( vm.isPrivate = permissions.isPrivate(
domainModel.permissions, domainModel.user); domainModel.permissions, domainModel.user);
var documentMetadata = extractDocumentMetadata(domainModel); vm.documentMeta = annotationMetadata.domainAndTitle(domainModel);
vm.documentTitle = documentTitle(documentMetadata);
vm.documentDomain = documentDomain(documentMetadata);
} }
/** /**
......
'use strict';
var escapeHtml = require('escape-html');
/**
* Return a nice displayable string representation of a document's domain.
*
* @returns {String} The document's domain in braces, e.g. '(example.com)'.
* Returns '' if the document has no domain or if the document's domain is
* the same as its title (because we assume that the title is already
* displayed elsewhere and displaying it twice would be redundant).
*
*/
module.exports = function documentDomain(document) {
var uri = escapeHtml(document.uri || '');
var domain = escapeHtml(document.domain || '');
var title = escapeHtml(document.title || '');
if (uri.indexOf('file://') === 0 && title) {
var parts = uri.split('/');
var filename = parts[parts.length - 1];
if (filename) {
return '(' + decodeURIComponent(filename) + ')';
}
}
if (domain && domain !== title) {
return '(' + decodeURIComponent(domain) + ')';
} else {
return '';
}
};
'use strict';
var escapeHtml = require('escape-html');
/**
* Return a nice displayable string representation of a document's title.
*
* @returns {String} The document's title preceded on "on " and hyperlinked
* to the document's URI. If the document has no http(s) URI then don't
* hyperlink the title. If the document has no title then return ''.
*
*/
module.exports = function documentTitle(document) {
var title = escapeHtml(document.title || '');
var uri = escapeHtml(document.uri || '');
if (uri && !(uri.indexOf('http://') === 0 || uri.indexOf('https://') === 0)) {
// We only link to http(s) URLs.
uri = null;
}
if (title && uri) {
return ('on &ldquo;<a target="_blank" href="' + uri + '">' + title +
'</a>&rdquo;');
} else if (title) {
return 'on &ldquo;' + title + '&rdquo;';
} else {
return '';
}
};
'use strict';
var documentDomain = require('../document-domain');
describe('documentDomain', function() {
it('returns the domain in braces', function() {
var domain = documentDomain({
domain: 'example.com'
});
assert(domain === '(example.com)');
});
it('returns an empty string if domain and title are the same', function() {
var domain = documentDomain({
domain: 'example.com',
title: 'example.com'
});
assert(domain === '');
});
it('returns an empty string if the document has no domain', function() {
var domain = documentDomain({
title: 'example.com'
});
assert(domain === '');
});
it('returns the filename for local documents with titles', function() {
var domain = documentDomain({
title: 'example.com',
uri: 'file:///home/seanh/MyFile.pdf'
});
assert(domain === '(MyFile.pdf)');
});
it('replaces %20 with " "', function() {
var domain = documentDomain({
title: 'example.com',
uri: 'file:///home/seanh/My%20File.pdf'
});
assert(domain === '(My File.pdf)');
});
it('escapes HTML in the document domain', function() {
var spamLink = '<a href="http://example.com/rubies">Buy rubies!!!</a>';
var domain = documentDomain({
title: 'title',
domain: '</a>' + spamLink
});
assert(domain.indexOf(spamLink) === -1);
});
it('escapes HTML in the document uri', function() {
var spamLink = '<a href="http://example.com/rubies">Buy rubies!!!</a>';
var domain = documentDomain({
title: 'title',
uri: 'file:///home/seanh/' + spamLink
});
assert(domain.indexOf(spamLink) === -1);
});
});
'use strict';
var documentTitle = require('../document-title');
describe('documentTitle', function() {
it('returns the title linked if the document has title and uri', function() {
var title = documentTitle({
title: 'title',
uri: 'http://example.com/example.html'
});
assert(title === 'on &ldquo;<a target="_blank" ' +
'href="http://example.com/example.html">' +
'title</a>&rdquo;');
});
it('returns the title linked if the document has an https uri', function() {
var title = documentTitle({
title: 'title',
uri: 'https://example.com/example.html'
});
assert(title === 'on &ldquo;<a target="_blank" '+
'href="https://example.com/example.html">' +
'title</a>&rdquo;');
});
it('returns the title unlinked if doc has title but no uri', function() {
var title = documentTitle({
title: 'title',
});
assert(title === 'on &ldquo;title&rdquo;');
});
it('returns the title unlinked if doc has non-http uri', function() {
var title = documentTitle({
title: 'title',
uri: 'file:///home/bob/Documents/example.pdf'
});
assert(title === 'on &ldquo;title&rdquo;');
});
it('returns an empty string if the document has no title', function() {
var title = documentTitle({
uri: 'http://example.com/example.html'
});
assert(title === '');
});
it('escapes HTML in the document title', function() {
var spamLink = '<a href="http://example.com/rubies">Buy rubies!!!</a>';
var title = documentTitle({
title: '</a>' + spamLink,
uri: 'http://example.com/example.html'
});
assert(title.indexOf(spamLink) === -1);
});
it('escapes HTML in the document URI', function() {
var spamLink = '<a href="http://example.com/rubies">Buy rubies!!!</a>';
var title = documentTitle({
uri: 'http://</a>' + spamLink,
title: 'title'
});
assert(title.indexOf(spamLink) === -1);
});
});
...@@ -2,10 +2,11 @@ ...@@ -2,10 +2,11 @@
var annotationMetadata = require('../annotation-metadata'); var annotationMetadata = require('../annotation-metadata');
var extractDocumentMetadata = annotationMetadata.extractDocumentMetadata; var documentMetadata = annotationMetadata.documentMetadata;
var domainAndTitle = annotationMetadata.domainAndTitle;
describe('annotation-metadata', function () { describe('annotation-metadata', function () {
describe('.extractDocumentMetadata', function() { describe('.documentMetadata', function() {
context('when the model has a document property', function() { context('when the model has a document property', function() {
it('returns the hostname from model.uri as the domain', function() { it('returns the hostname from model.uri as the domain', function() {
...@@ -14,7 +15,7 @@ describe('annotation-metadata', function () { ...@@ -14,7 +15,7 @@ describe('annotation-metadata', function () {
uri: 'http://example.com/' uri: 'http://example.com/'
}; };
assert.equal(extractDocumentMetadata(model).domain, 'example.com'); assert.equal(documentMetadata(model).domain, 'example.com');
}); });
context('when model.uri does not start with "urn"', function() { context('when model.uri does not start with "urn"', function() {
...@@ -25,7 +26,7 @@ describe('annotation-metadata', function () { ...@@ -25,7 +26,7 @@ describe('annotation-metadata', function () {
}; };
assert.equal( assert.equal(
extractDocumentMetadata(model).uri, 'http://example.com/'); documentMetadata(model).uri, 'http://example.com/');
}); });
}); });
...@@ -39,7 +40,7 @@ describe('annotation-metadata', function () { ...@@ -39,7 +40,7 @@ describe('annotation-metadata', function () {
}; };
assert.equal( assert.equal(
extractDocumentMetadata(model).title, model.document.title[0]); documentMetadata(model).title, model.document.title[0]);
}); });
}); });
...@@ -50,7 +51,7 @@ describe('annotation-metadata', function () { ...@@ -50,7 +51,7 @@ describe('annotation-metadata', function () {
uri: 'http://example.com/', uri: 'http://example.com/',
}; };
assert.equal(extractDocumentMetadata(model).title, 'example.com'); assert.equal(documentMetadata(model).title, 'example.com');
}); });
}); });
}); });
...@@ -59,24 +60,70 @@ describe('annotation-metadata', function () { ...@@ -59,24 +60,70 @@ describe('annotation-metadata', function () {
it('returns model.uri for the uri', function() { it('returns model.uri for the uri', function() {
var model = {uri: 'http://example.com/'}; var model = {uri: 'http://example.com/'};
assert.equal(extractDocumentMetadata(model).uri, model.uri); assert.equal(documentMetadata(model).uri, model.uri);
}); });
it('returns the hostname of model.uri for the domain', function() { it('returns the hostname of model.uri for the domain', function() {
var model = {uri: 'http://example.com/'}; var model = {uri: 'http://example.com/'};
assert.equal(extractDocumentMetadata(model).domain, 'example.com'); assert.equal(documentMetadata(model).domain, 'example.com');
}); });
it('returns the hostname of model.uri for the title', function() { it('returns the hostname of model.uri for the title', function() {
var model = {uri: 'http://example.com/'}; var model = {uri: 'http://example.com/'};
assert.equal(extractDocumentMetadata(model).title, 'example.com'); assert.equal(documentMetadata(model).title, 'example.com');
}); });
}); });
});
describe('.domainAndTitle', function() {
context('when an annotation has a non-http(s) uri', function () {
it('returns no title link', function () {
var model = {
uri: 'file:///example.pdf',
};
context('when the title is longer than 30 characters', function() { assert.equal(domainAndTitle(model).titleLink, null);
it('truncates the title with "…"', function() { });
});
context('when an annotation has a direct link', function () {
it('returns the direct link as a title link', function () {
var model = {
links: {
incontext: 'https://example.com',
}
};
assert.equal(domainAndTitle(model).titleLink, 'https://example.com');
});
});
context('when an annotation has no direct link but has a http(s) uri', function () {
it('returns the uri as title link', function () {
var model = {
uri: 'https://example.com',
};
assert.equal(domainAndTitle(model).titleLink, 'https://example.com');
});
});
context('when the annotation title is shorter than 30 characters', function () {
it('returns the annotation title as title text', function () {
var model = {
document: {
title: ['A Short Document Title'],
},
};
assert.equal(domainAndTitle(model).titleText, 'A Short Document Title');
});
});
context('when the annotation title is longer than 30 characters', function() {
it('truncates the title text with "…"', function() {
var model = { var model = {
uri: 'http://example.com/', uri: 'http://example.com/',
document: { document: {
...@@ -85,11 +132,62 @@ describe('annotation-metadata', function () { ...@@ -85,11 +132,62 @@ describe('annotation-metadata', function () {
}; };
assert.equal( assert.equal(
extractDocumentMetadata(model).title, domainAndTitle(model).titleText,
'My Really Really Long Document…' 'My Really Really Long Document…'
); );
}); });
}); });
context('when the document uri refers to a filename', function () {
it('returns the filename as domain text', function () {
var model = {
uri: 'file:///path/to/example.pdf',
document: {
title: ['Document Title'],
},
};
assert.equal(domainAndTitle(model).domain, 'example.pdf');
});
});
context('when domain and title are the same', function () {
it('returns an empty domain text string', function() {
var model = {
uri: 'https://example.com',
document : {
title: ['example.com'],
},
};
assert.equal(domainAndTitle(model).domain, '');
});
});
context('when the document has no domain', function () {
it('returns an empty domain text string', function() {
var model = {
document : {
title: ['example.com'],
},
};
assert.equal(domainAndTitle(model).domain, '');
});
});
context('when the document is a local file with a title', function () {
it('returns the filename', function() {
var model = {
uri: 'file:///home/seanh/MyFile.pdf',
document: {
title: ['example.com'],
},
};
assert.equal(domainAndTitle(model).domain, 'MyFile.pdf');
});
});
}); });
describe('.location', function () { describe('.location', function () {
......
...@@ -27,13 +27,15 @@ ...@@ -27,13 +27,15 @@
<i class="h-icon-lock"></i><span class="annotation-header__group-name" ng-show="!vm.group().url">Only me</span> <i class="h-icon-lock"></i><span class="annotation-header__group-name" ng-show="!vm.group().url">Only me</span>
</span> </span>
<i class="h-icon-border-color" ng-show="vm.isHighlight() && !vm.editing()" title="This is a highlight. Click 'edit' to add a note or tag."></i> <i class="h-icon-border-color" ng-show="vm.isHighlight() && !vm.editing()" title="This is a highlight. Click 'edit' to add a note or tag."></i>
<span class="annotation-citation" <span ng-if="::vm.showDocumentInfo">
ng-bind-html="vm.documentTitle" <span class="annotation-citation" ng-if="vm.documentMeta.titleLink">
ng-if="::vm.showDocumentInfo"> on "<a ng-href="{{vm.documentMeta.titleLink}}">{{vm.documentMeta.titleText}}</a>"
</span> </span>
<span class="annotation-citation-domain" <span class="annotation-citation" ng-if="!vm.documentMeta.titleLink">
ng-bind-html="vm.documentDomain" on "{{vm.documentMeta.titleText}}"
ng-if="::vm.showDocumentInfo"> </span>
<span class="annotation-citation-domain"
ng-if="vm.documentMeta.domain">({{vm.documentMeta.domain}})</span>
</span> </span>
</span> </span>
</span> </span>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment