Skip to content

Commit 1825352

Browse files
committed
Add helpers for AnnotationDocumentInfo
1 parent 7572f23 commit 1825352

File tree

6 files changed

+345
-23
lines changed

6 files changed

+345
-23
lines changed

src/components/AnnotationDocumentInfo.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
import { Link } from '@hypothesis/frontend-shared';
22

3+
import type { Annotation } from '../helpers/annotation-metadata';
4+
import { domainAndTitle } from '../helpers/annotation-metadata';
5+
36
export type AnnotationDocumentInfoProps = {
4-
/** The domain associated with the document */
5-
domain?: string;
6-
/** A direct link to the document */
7-
link?: string;
8-
title: string;
7+
annotation: Annotation;
98
};
9+
1010
/**
1111
* Render some metadata about an annotation's document and link to it
1212
* if a link is available.
1313
*/
1414
export default function AnnotationDocumentInfo({
15-
domain,
16-
link,
17-
title,
15+
annotation,
1816
}: AnnotationDocumentInfoProps) {
17+
const { domain, titleText: title, titleLink } = domainAndTitle(annotation);
18+
const annotationURL = annotation.links?.html || '';
19+
// There are some cases at present in which linking directly to an
20+
// annotation's document is not immediately feasible—e.g in an LMS context
21+
// where the original document might not be available outside of an
22+
// assignment (e.g. Canvas files), and/or wouldn't be able to present
23+
// any associated annotations.
24+
// For the present, disable links to annotation documents for all third-party
25+
// annotations until we have a more nuanced way of making linking determinations.
26+
// The absence of a link to a single-annotation view is a signal that this
27+
// is a third-party annotation.
28+
// Also, of course, verify that there is a URL to the document (titleLink)
29+
const link = annotationURL && titleLink ? titleLink : '';
30+
1931
return (
2032
<div className="flex gap-x-1">
2133
<div className="text-color-text-light">
Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
1-
import { checkAccessibility } from '@hypothesis/frontend-testing';
1+
import {
2+
checkAccessibility,
3+
mockImportedComponents,
4+
} from '@hypothesis/frontend-testing';
25
import { mount } from '@hypothesis/frontend-testing';
36

4-
import AnnotationDocumentInfo from '../AnnotationDocumentInfo';
7+
import AnnotationDocumentInfo, { $imports } from '../AnnotationDocumentInfo';
58

69
describe('AnnotationDocumentInfo', () => {
7-
const createAnnotationDocumentInfo = props => {
8-
return mount(
9-
<AnnotationDocumentInfo
10-
domain="www.foo.bar"
11-
link="http://www.baz"
12-
title="Turtles"
13-
{...props}
14-
/>,
15-
);
10+
let fakeAnnotation;
11+
let fakeDomainAndTitle;
12+
13+
beforeEach(() => {
14+
fakeAnnotation = {
15+
links: { html: 'https://example.com' },
16+
};
17+
fakeDomainAndTitle = sinon.stub().returns({
18+
titleText: 'Turtles',
19+
titleLink: 'http://www.baz',
20+
domain: 'www.foo.com',
21+
});
22+
23+
$imports.$mock(mockImportedComponents());
24+
$imports.$mock({
25+
'../helpers/annotation-metadata': {
26+
domainAndTitle: fakeDomainAndTitle,
27+
},
28+
});
29+
});
30+
31+
const createAnnotationDocumentInfo = () => {
32+
return mount(<AnnotationDocumentInfo annotation={fakeAnnotation} />);
1633
};
1734

1835
it('should render the document title', () => {
@@ -32,7 +49,8 @@ describe('AnnotationDocumentInfo', () => {
3249
it('does not link to document when no link available', () => {});
3350

3451
it('should render domain if available', () => {
35-
const wrapper = createAnnotationDocumentInfo({ link: '' });
52+
fakeAnnotation.links.html = '';
53+
const wrapper = createAnnotationDocumentInfo();
3654

3755
const link = wrapper.find('a');
3856
assert.include(wrapper.text(), '"Turtles"');
@@ -42,9 +60,7 @@ describe('AnnotationDocumentInfo', () => {
4260
it(
4361
'should pass a11y checks',
4462
checkAccessibility({
45-
content: () => {
46-
return createAnnotationDocumentInfo();
47-
},
63+
content: () => createAnnotationDocumentInfo(),
4864
}),
4965
);
5066
});

src/helpers/annotation-metadata.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
export type Annotation = {
2+
uri: string;
3+
document: { title: string };
4+
links: {
5+
/** A "bouncer" URL that takes the user to see the annotation in context */
6+
incontext?: string;
7+
/** URL to view the annotation by itself. */
8+
html?: string;
9+
};
10+
};
11+
12+
export type DocumentMetadata = {
13+
uri: string;
14+
domain: string;
15+
title: string;
16+
};
17+
18+
/**
19+
* Extract document metadata from an annotation.
20+
*/
21+
export function documentMetadata(annotation: Annotation): DocumentMetadata {
22+
const uri = annotation.uri;
23+
24+
let domain;
25+
try {
26+
domain = new URL(uri).hostname;
27+
} catch {
28+
// Annotation URI parsing on the backend is very liberal compared to the URL
29+
// constructor. There is also some historic invalid data in h (eg [1]).
30+
// Hence, we must handle URL parsing failures in the client.
31+
//
32+
// [1] https://github.com/hypothesis/client/issues/3666
33+
domain = '';
34+
}
35+
if (domain === 'localhost') {
36+
domain = '';
37+
}
38+
39+
let title = domain;
40+
if (annotation.document && annotation.document.title) {
41+
title = annotation.document.title[0];
42+
}
43+
44+
return { uri, domain, title };
45+
}
46+
47+
export type DomainAndTitle = {
48+
domain: string;
49+
titleText: string;
50+
titleLink: string | null;
51+
};
52+
53+
/**
54+
* Return the domain and title of an annotation for display on an annotation
55+
* card.
56+
*/
57+
export function domainAndTitle(annotation: Annotation): DomainAndTitle {
58+
return {
59+
domain: domainTextFromAnnotation(annotation),
60+
titleText: titleTextFromAnnotation(annotation),
61+
titleLink: titleLinkFromAnnotation(annotation),
62+
};
63+
}
64+
65+
function titleLinkFromAnnotation(annotation: Annotation): string | null {
66+
let titleLink: string | null = annotation.uri;
67+
68+
if (
69+
titleLink &&
70+
!(titleLink.indexOf('http://') === 0 || titleLink.indexOf('https://') === 0)
71+
) {
72+
// We only link to http(s) URLs.
73+
titleLink = null;
74+
}
75+
76+
if (annotation.links && annotation.links.incontext) {
77+
titleLink = annotation.links.incontext;
78+
}
79+
80+
return titleLink;
81+
}
82+
/**
83+
* Returns the domain text from an annotation.
84+
*/
85+
function domainTextFromAnnotation(annotation: Annotation): string {
86+
const document = documentMetadata(annotation);
87+
88+
let domainText = '';
89+
if (document.uri && document.uri.indexOf('file://') === 0 && document.title) {
90+
const parts = document.uri.split('/');
91+
const filename = parts[parts.length - 1];
92+
if (filename) {
93+
domainText = filename;
94+
}
95+
} else if (document.domain && document.domain !== document.title) {
96+
domainText = document.domain;
97+
}
98+
99+
return domainText;
100+
}
101+
102+
/**
103+
* Returns the title text from an annotation and crops it to 30 chars
104+
* if needed.
105+
*/
106+
function titleTextFromAnnotation(annotation: Annotation): string {
107+
const document = documentMetadata(annotation);
108+
109+
let titleText = document.title;
110+
if (titleText.length > 30) {
111+
titleText = titleText.slice(0, 30) + '…';
112+
}
113+
114+
return titleText;
115+
}

src/helpers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
export { domainAndTitle, documentMetadata } from './annotation-metadata';
12
export { processAndReplaceMentionElements } from './mentions';
23

4+
export type { DocumentMetadata, DomainAndTitle } from './annotation-metadata';
35
export type { Group, GroupType } from './groups';
46
export type { Mention, MentionMode, InvalidMentionContent } from './mentions';

0 commit comments

Comments
 (0)