Commit 33067d0d authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Add config support for Notebook feature

Add env NOTEBOOK_APP_URL and support for it in
build and config for annotator.
parent fdc5a1bd
......@@ -224,6 +224,10 @@ let isFirstBuild = true;
function generateBootScript(manifest, { usingDevServer = false } = {}) {
const { version } = require('./package.json');
const defaultNotebookAppUrl = process.env.NOTEBOOK_APP_URL
? `${process.env.NOTEBOOK_APP_URL}`
: '{current_scheme}://{current_host}:5000/notebook';
const defaultSidebarAppUrl = process.env.SIDEBAR_APP_URL
? `${process.env.SIDEBAR_APP_URL}`
: '{current_scheme}://{current_host}:5000/app.html';
......@@ -239,6 +243,7 @@ function generateBootScript(manifest, { usingDevServer = false } = {}) {
if (isFirstBuild) {
log(`Sidebar app URL: ${defaultSidebarAppUrl}`);
log(`Notebook app URL: ${defaultNotebookAppUrl}`);
log(`Client asset root URL: ${defaultAssetRoot}`);
isFirstBuild = false;
}
......@@ -247,6 +252,7 @@ function generateBootScript(manifest, { usingDevServer = false } = {}) {
.src('build/scripts/boot.bundle.js')
.pipe(replace('__MANIFEST__', JSON.stringify(manifest)))
.pipe(replace('__ASSET_ROOT__', defaultAssetRoot))
.pipe(replace('__NOTEBOOK_APP_URL__', defaultNotebookAppUrl))
.pipe(replace('__SIDEBAR_APP_URL__', defaultSidebarAppUrl))
// Strip sourcemap link. It will have been invalidated by the previous
// replacements and the bundle is so small that it isn't really valuable.
......
......@@ -39,6 +39,7 @@ export default function configFrom(window_) {
requestConfigFromFrame: settings.hostPageSetting('requestConfigFromFrame'),
services: settings.hostPageSetting('services'),
showHighlights: settings.showHighlights,
notebookAppUrl: settings.notebookAppUrl,
sidebarAppUrl: settings.sidebarAppUrl,
// Subframe identifier given when a frame is being embedded into
// by a top level client
......
......@@ -8,33 +8,33 @@ export default function settingsFrom(window_) {
const configFuncSettings = configFuncSettingsFrom(window_);
/**
* Return the href URL of the first annotator sidebar link in the given document.
* Return the href of the first annotator link in the given
* document with this `rel` attribute.
*
* Return the value of the href attribute of the first
* `<link type="application/annotator+html" rel="sidebar">` element in the given document.
*
* This URL is used as the src of the sidebar's iframe.
*
* @return {string} - The URL to use for the sidebar's iframe.
*
* @throws {Error} - If there's no annotator link or the first annotator has
* no href.
*
* `<link type="application/annotator+html" rel="${rel}">`
* element in the given document. This URL is used as the `src` for sidebar
* or notebook iframes.
*
* @param {string} rel - The `rel` attribute to match
* @return {string} - The URL to use for the iframe
* @throws {Error} - If there's no link with the `rel` indicated, or the first
* matching link has no `href`
*/
function sidebarAppUrl() {
function urlFromLinkTag(rel) {
const link = window_.document.querySelector(
'link[type="application/annotator+html"][rel="sidebar"]'
`link[type="application/annotator+html"][rel="${rel}"]`
);
if (!link) {
throw new Error(
'No application/annotator+html (rel="sidebar") link in the document'
`No application/annotator+html (rel="${rel}") link in the document`
);
}
if (!link.href) {
throw new Error(
'application/annotator+html (rel="sidebar") link has no href'
`application/annotator+html (rel="${rel}") link has no href`
);
}
......@@ -45,13 +45,13 @@ export default function settingsFrom(window_) {
* Return the href URL of the first annotator client link in the given document.
*
* Return the value of the href attribute of the first
* `<link type="application/annotator+html" rel="hypothesis-client">` element in the given document.
* `<link type="application/annotator+javascript" rel="hypothesis-client">`
* element in the given document.
*
* This URL is used to identify where the client is from and what url should be
* used inside of subframes
* This URL is used to identify where the client is from and what url should
* be used inside of subframes.
*
* @return {string} - The URL that the client is hosted from
*
* @throws {Error} - If there's no annotator link or the first annotator has
* no href.
*
......@@ -177,7 +177,7 @@ export default function settingsFrom(window_) {
const coerceValue =
typeof options.coerce === 'function' ? options.coerce : name => name;
if (!allowInBrowserExt && isBrowserExtension(sidebarAppUrl())) {
if (!allowInBrowserExt && isBrowserExtension(urlFromLinkTag('sidebar'))) {
return hasDefaultValue ? options.defaultValue : null;
}
......@@ -206,11 +206,14 @@ export default function settingsFrom(window_) {
get group() {
return group();
},
get notebookAppUrl() {
return urlFromLinkTag('notebook');
},
get showHighlights() {
return showHighlights();
},
get sidebarAppUrl() {
return sidebarAppUrl();
return urlFromLinkTag('sidebar');
},
get query() {
return query();
......
import settingsFrom from '../settings';
import { $imports } from '../settings';
describe('annotator.config.settingsFrom', function () {
describe('annotator/config/settingsFrom', () => {
let fakeConfigFuncSettingsFrom;
let fakeIsBrowserExtension;
let fakeParseJsonConfig;
......@@ -22,90 +22,169 @@ describe('annotator.config.settingsFrom', function () {
$imports.$restore();
});
describe('#sidebarAppUrl', function () {
function appendSidebarLinkToDocument(href) {
describe('app frame URLs from link tags', () => {
function appendLink(href, rel) {
const link = document.createElement('link');
link.type = 'application/annotator+html';
link.rel = 'sidebar';
link.rel = rel;
if (href) {
link.href = href;
}
document.head.appendChild(link);
return link;
}
describe('#notebookAppUrl', () => {
context(
"when there's an application/annotator+html notebook link",
() => {
let link;
beforeEach(
'add an application/annotator+html notebook <link>',
() => {
link = appendLink('http://example.com/app.html', 'notebook');
}
);
context("when there's an application/annotator+html link", function () {
let link;
afterEach('tidy up the notebook link', () => {
link.remove();
});
beforeEach('add an application/annotator+html <link>', function () {
link = appendSidebarLinkToDocument('http://example.com/app.html');
});
it('returns the href from the notebook link', () => {
assert.equal(
settingsFrom(window).notebookAppUrl,
'http://example.com/app.html'
);
});
}
);
context('when there are multiple annotator+html notebook links', () => {
let link1;
let link2;
beforeEach('add two notebook links to the document', () => {
link1 = appendLink('http://example.com/app1', 'notebook');
link2 = appendLink('http://example.com/app2', 'notebook');
});
afterEach('tidy up the link', function () {
document.head.removeChild(link);
afterEach('tidy up the notebook links', () => {
link1.remove();
link2.remove();
});
it('returns the href from the first notebook link found', () => {
assert.equal(
settingsFrom(window).notebookAppUrl,
'http://example.com/app1'
);
});
});
it('returns the href from the link', function () {
assert.equal(
settingsFrom(window).sidebarAppUrl,
'http://example.com/app.html'
context('when the annotator+html notebook link has no href', () => {
let link;
beforeEach(
'add an application/annotator+html notebook <link> with no href',
() => {
link = appendLink(undefined, 'notebook');
}
);
});
});
context('when there are multiple annotator+html links', function () {
let link1;
let link2;
afterEach('tidy up the notebook link', () => {
link.remove();
});
beforeEach('add two links to the document', function () {
link1 = appendSidebarLinkToDocument('http://example.com/app1');
link2 = appendSidebarLinkToDocument('http://example.com/app2');
it('throws an error', () => {
assert.throws(() => {
settingsFrom(window).notebookAppUrl; // eslint-disable-line no-unused-expressions
}, 'application/annotator+html (rel="notebook") link has no href');
});
});
afterEach('tidy up the links', function () {
document.head.removeChild(link1);
document.head.removeChild(link2);
context("when there's no annotator+html notebook link", () => {
it('throws an error', () => {
assert.throws(() => {
settingsFrom(window).notebookAppUrl; // eslint-disable-line no-unused-expressions
}, 'No application/annotator+html (rel="notebook") link in the document');
});
});
});
it('returns the href from the first one', function () {
assert.equal(
settingsFrom(window).sidebarAppUrl,
'http://example.com/app1'
);
describe('#sidebarAppUrl', () => {
context("when there's an application/annotator+html sidebar link", () => {
let link;
beforeEach('add an application/annotator+html sidebar <link>', () => {
link = appendLink('http://example.com/app.html', 'sidebar');
});
afterEach('tidy up the sidebar link', () => {
link.remove();
});
it('returns the href from the sidebar link', () => {
assert.equal(
settingsFrom(window).sidebarAppUrl,
'http://example.com/app.html'
);
});
});
});
context('when the annotator+html link has no href', function () {
let link;
context('when there are multiple annotator+html sidebar links', () => {
let link1;
let link2;
beforeEach(
'add an application/annotator+html <link> with no href',
function () {
link = appendSidebarLinkToDocument();
}
);
beforeEach('add two sidebar links to the document', () => {
link1 = appendLink('http://example.com/app1', 'sidebar');
link2 = appendLink('http://example.com/app2', 'sidebar');
});
afterEach('tidy up the sidebar links', () => {
link1.remove();
link2.remove();
});
afterEach('tidy up the link', function () {
document.head.removeChild(link);
it('returns the href from the first one', () => {
assert.equal(
settingsFrom(window).sidebarAppUrl,
'http://example.com/app1'
);
});
});
it('throws an error', function () {
assert.throws(function () {
settingsFrom(window).sidebarAppUrl; // eslint-disable-line no-unused-expressions
}, 'application/annotator+html (rel="sidebar") link has no href');
context('when the annotator+html sidebar link has no href', () => {
let link;
beforeEach(
'add an application/annotator+html sidebar <link> with no href',
() => {
link = appendLink(null, 'sidebar');
}
);
afterEach('tidy up the sidebar link', () => {
link.remove();
});
it('throws an error', () => {
assert.throws(() => {
settingsFrom(window).sidebarAppUrl; // eslint-disable-line no-unused-expressions
}, 'application/annotator+html (rel="sidebar") link has no href');
});
});
});
context("when there's no annotator+html link", function () {
it('throws an error', function () {
assert.throws(function () {
settingsFrom(window).sidebarAppUrl; // eslint-disable-line no-unused-expressions
}, 'No application/annotator+html (rel="sidebar") link in the document');
context("when there's no annotator+html sidebar link", () => {
it('throws an error', () => {
assert.throws(() => {
settingsFrom(window).sidebarAppUrl; // eslint-disable-line no-unused-expressions
}, 'No application/annotator+html (rel="sidebar") link in the document');
});
});
});
});
describe('#clientUrl', function () {
describe('#clientUrl', () => {
function appendClientUrlLinkToDocument(href) {
const link = document.createElement('link');
link.type = 'application/annotator+javascript';
......@@ -117,74 +196,68 @@ describe('annotator.config.settingsFrom', function () {
return link;
}
context(
"when there's an application/annotator+javascript link",
function () {
let link;
context("when there's an application/annotator+javascript link", () => {
let link;
beforeEach(
'add an application/annotator+javascript <link>',
function () {
link = appendClientUrlLinkToDocument('http://example.com/app.html');
}
);
beforeEach('add an application/annotator+javascript <link>', () => {
link = appendClientUrlLinkToDocument('http://example.com/app.html');
});
afterEach('tidy up the link', function () {
document.head.removeChild(link);
});
afterEach('tidy up the link', () => {
link.remove();
});
it('returns the href from the link', function () {
assert.equal(
settingsFrom(window).clientUrl,
'http://example.com/app.html'
);
});
}
);
it('returns the href from the link', () => {
assert.equal(
settingsFrom(window).clientUrl,
'http://example.com/app.html'
);
});
});
context('when there are multiple annotator+javascript links', function () {
context('when there are multiple annotator+javascript links', () => {
let link1;
let link2;
beforeEach('add two links to the document', function () {
beforeEach('add two links to the document', () => {
link1 = appendClientUrlLinkToDocument('http://example.com/app1');
link2 = appendClientUrlLinkToDocument('http://example.com/app2');
});
afterEach('tidy up the links', function () {
document.head.removeChild(link1);
document.head.removeChild(link2);
afterEach('tidy up the links', () => {
link1.remove();
link2.remove();
});
it('returns the href from the first one', function () {
it('returns the href from the first one', () => {
assert.equal(settingsFrom(window).clientUrl, 'http://example.com/app1');
});
});
context('when the annotator+javascript link has no href', function () {
context('when the annotator+javascript link has no href', () => {
let link;
beforeEach(
'add an application/annotator+javascript <link> with no href',
function () {
() => {
link = appendClientUrlLinkToDocument();
}
);
afterEach('tidy up the link', function () {
document.head.removeChild(link);
afterEach('tidy up the link', () => {
link.remove();
});
it('throws an error', function () {
assert.throws(function () {
it('throws an error', () => {
assert.throws(() => {
settingsFrom(window).clientUrl; // eslint-disable-line no-unused-expressions
}, 'application/annotator+javascript (rel="hypothesis-client") link has no href');
});
});
context("when there's no annotator+javascript link", function () {
it('throws an error', function () {
assert.throws(function () {
context("when there's no annotator+javascript link", () => {
it('throws an error', () => {
assert.throws(() => {
settingsFrom(window).clientUrl; // eslint-disable-line no-unused-expressions
}, 'No application/annotator+javascript (rel="hypothesis-client") link in the document');
});
......@@ -202,20 +275,17 @@ describe('annotator.config.settingsFrom', function () {
};
}
describe('#annotations', function () {
describe('#annotations', () => {
context(
'when the host page has a js-hypothesis-config with an annotations setting',
function () {
beforeEach(
'add a js-hypothesis-config annotations setting',
function () {
fakeParseJsonConfig.returns({
annotations: 'annotationsFromJSON',
});
}
);
() => {
beforeEach('add a js-hypothesis-config annotations setting', () => {
fakeParseJsonConfig.returns({
annotations: 'annotationsFromJSON',
});
});
it('returns the annotations from the js-hypothesis-config script', function () {
it('returns the annotations from the js-hypothesis-config script', () => {
assert.equal(
settingsFrom(fakeWindow()).annotations,
'annotationsFromJSON'
......@@ -223,11 +293,11 @@ describe('annotator.config.settingsFrom', function () {
});
context(
"when there's also an annotations in the URL fragment",
function () {
"when there's also an `annotations` in the URL fragment",
() => {
specify(
'js-hypothesis-config annotations override URL ones',
function () {
() => {
const window_ = fakeWindow(
'http://localhost:3000#annotations:annotationsFromURL'
);
......@@ -269,8 +339,8 @@ describe('annotator.config.settingsFrom', function () {
returns: null,
},
].forEach(function (test) {
describe(test.describe, function () {
it(test.it, function () {
describe(test.describe, () => {
it(test.it, () => {
assert.deepEqual(
settingsFrom(fakeWindow(test.url)).annotations,
test.returns
......@@ -303,31 +373,28 @@ describe('annotator.config.settingsFrom', function () {
});
});
describe('#query', function () {
describe('#query', () => {
context(
'when the host page has a js-hypothesis-config with a query setting',
function () {
beforeEach('add a js-hypothesis-config query setting', function () {
() => {
beforeEach('add a js-hypothesis-config query setting', () => {
fakeParseJsonConfig.returns({
query: 'queryFromJSON',
});
});
it('returns the query from the js-hypothesis-config script', function () {
it('returns the query from the js-hypothesis-config script', () => {
assert.equal(settingsFrom(fakeWindow()).query, 'queryFromJSON');
});
context("when there's also a query in the URL fragment", function () {
specify(
'js-hypothesis-config queries override URL ones',
function () {
const window_ = fakeWindow(
'http://localhost:3000#annotations:query:queryFromUrl'
);
context("when there's also a query in the URL fragment", () => {
specify('js-hypothesis-config queries override URL ones', () => {
const window_ = fakeWindow(
'http://localhost:3000#annotations:query:queryFromUrl'
);
assert.equal(settingsFrom(window_).query, 'queryFromJSON');
}
);
assert.equal(settingsFrom(window_).query, 'queryFromJSON');
});
});
}
);
......@@ -376,8 +443,8 @@ describe('annotator.config.settingsFrom', function () {
returns: null,
},
].forEach(function (test) {
describe(test.describe, function () {
it(test.it, function () {
describe(test.describe, () => {
it(test.it, () => {
assert.deepEqual(
settingsFrom(fakeWindow(test.url)).query,
test.returns
......@@ -386,8 +453,8 @@ describe('annotator.config.settingsFrom', function () {
});
});
describe('when the URL contains an invalid fragment', function () {
it('returns null', function () {
describe('when the URL contains an invalid fragment', () => {
it('returns null', () => {
// An invalid escape sequence which will cause decodeURIComponent() to
// throw a URIError.
const invalidFrag = '%aaaaa';
......@@ -399,7 +466,7 @@ describe('annotator.config.settingsFrom', function () {
});
});
describe('#showHighlights', function () {
describe('#showHighlights', () => {
[
{
it: 'returns an "always" setting from the host page unmodified',
......@@ -465,7 +532,7 @@ describe('annotator.config.settingsFrom', function () {
output: /regex/,
},
].forEach(function (test) {
it(test.it, function () {
it(test.it, () => {
fakeParseJsonConfig.returns({
showHighlights: test.input,
});
......@@ -474,7 +541,7 @@ describe('annotator.config.settingsFrom', function () {
assert.deepEqual(settings.showHighlights, test.output);
});
it(test.it, function () {
it(test.it, () => {
fakeConfigFuncSettingsFrom.returns({
showHighlights: test.input,
});
......@@ -484,16 +551,16 @@ describe('annotator.config.settingsFrom', function () {
});
});
it("defaults to 'always' if there's no showHighlights setting in the host page", function () {
it("defaults to 'always' if there's no showHighlights setting in the host page", () => {
assert.equal(settingsFrom(fakeWindow()).showHighlights, 'always');
});
context('when the client is in a browser extension', function () {
beforeEach('configure a browser extension client', function () {
context('when the client is in a browser extension', () => {
beforeEach('configure a browser extension client', () => {
fakeIsBrowserExtension.returns(true);
});
it("doesn't read the setting from the host page, defaults to 'always'", function () {
it("doesn't read the setting from the host page, defaults to 'always'", () => {
fakeParseJsonConfig.returns({
showHighlights: 'never',
});
......@@ -506,7 +573,7 @@ describe('annotator.config.settingsFrom', function () {
});
});
describe('#hostPageSetting', function () {
describe('#hostPageSetting', () => {
[
{
when: 'the client is embedded in a web page',
......@@ -621,8 +688,8 @@ describe('annotator.config.settingsFrom', function () {
expected: 'the default value',
},
].forEach(function (test) {
context(test.when, function () {
specify(test.specify, function () {
context(test.when, () => {
specify(test.specify, () => {
fakeIsBrowserExtension.returns(test.isBrowserExtension);
fakeConfigFuncSettingsFrom.returns(test.configFuncSettings);
fakeParseJsonConfig.returns(test.jsonSettings);
......
......@@ -21,6 +21,7 @@ const commonPolyfills = [
/**
* @typedef AnnotatorConfig
* @prop {string} assetRoot - The root URL to which URLs in `manifest` are relative
* @prop {string} notebookAppUrl - The URL of the sidebar's notebook
* @prop {string} sidebarAppUrl - The URL of the sidebar's HTML page
* @prop {Object.<string,string>} manifest -
* A mapping from canonical asset path to cache-busted asset path
......@@ -101,6 +102,13 @@ export function bootHypothesisClient(doc, config) {
sidebarUrl.type = 'application/annotator+html';
doc.head.appendChild(sidebarUrl);
// Register the URL of the notebook app which the Hypothesis client should load.
const notebookUrl = doc.createElement('link');
notebookUrl.rel = 'notebook';
notebookUrl.href = config.notebookAppUrl;
notebookUrl.type = 'application/annotator+html';
doc.head.appendChild(notebookUrl);
// Register the URL of the annotation client which is currently being used to drive
// annotation interactions.
const clientUrl = doc.createElement('link');
......
......@@ -26,10 +26,18 @@ if (isBrowserSupported()) {
if (document.querySelector('hypothesis-app')) {
bootSidebarApp(document, { assetRoot, manifest });
} else {
const notebookAppUrl = processUrlTemplate(
settings.notebookAppUrl || '__NOTEBOOK_APP_URL__'
);
const sidebarAppUrl = processUrlTemplate(
settings.sidebarAppUrl || '__SIDEBAR_APP_URL__'
);
bootHypothesisClient(document, { assetRoot, manifest, sidebarAppUrl });
bootHypothesisClient(document, {
assetRoot,
manifest,
notebookAppUrl,
sidebarAppUrl,
});
}
} else {
// Show a "quiet" warning to avoid being disruptive on non-Hypothesis sites
......
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