diff --git a/src/annotator/config.js b/src/annotator/config.js index 2f0d3339ea2..a487f083203 100644 --- a/src/annotator/config.js +++ b/src/annotator/config.js @@ -1,6 +1,6 @@ 'use strict'; -var annotationIDs = require('./util/annotation-ids'); +var annotationQuery = require('./util/extract-annotation-query'); var settings = require('../shared/settings'); var docs = 'https://h.readthedocs.io/en/latest/embedding.html'; @@ -33,7 +33,7 @@ function config(window_) { } } - // Extract the direct linked ID from the URL. + // Extract the default query from the URL. // // The Chrome extension or proxy may already have provided this config // via a tag injected into the DOM, which avoids the problem where the page's @@ -41,9 +41,9 @@ function config(window_) { // // In environments where the config has not been injected into the DOM, // we try to retrieve it from the URL here. - var directLinkedID = annotationIDs.extractIDFromURL(window_.location.href); + var directLinkedID = annotationQuery.extractAnnotationQuery(window_.location.href); if (directLinkedID) { - options.annotations = directLinkedID; + Object.assign(options, directLinkedID); } return options; } diff --git a/src/annotator/sidebar.coffee b/src/annotator/sidebar.coffee index 623dcefda89..31e18454577 100644 --- a/src/annotator/sidebar.coffee +++ b/src/annotator/sidebar.coffee @@ -26,7 +26,7 @@ module.exports = class Sidebar extends Host super this.hide() - if options.openSidebar || options.annotations + if options.openSidebar || options.annotations || options.query this.on 'panelReady', => this.show() if @plugins.BucketBar? diff --git a/src/annotator/util/annotation-ids.js b/src/annotator/util/annotation-ids.js deleted file mode 100644 index 2964a4f7a88..00000000000 --- a/src/annotator/util/annotation-ids.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -/** - * Extracts a direct-linked annotation ID from the fragment of a URL. - * - * @param {string} url - The URL which may contain a '#annotations:' - * fragment. - * @return {string?} The annotation ID if present - */ -function extractIDFromURL(url) { - try { - // Annotation IDs are url-safe-base64 identifiers - // See https://tools.ietf.org/html/rfc4648#page-7 - var annotFragmentMatch = url.match(/#annotations:([A-Za-z0-9_-]+)$/); - if (annotFragmentMatch) { - return annotFragmentMatch[1]; - } else { - return null; - } - } catch (err) { - return null; - } -} - -module.exports = { - extractIDFromURL: extractIDFromURL, -}; diff --git a/src/annotator/util/extract-annotation-query.js b/src/annotator/util/extract-annotation-query.js new file mode 100644 index 00000000000..872965ade07 --- /dev/null +++ b/src/annotator/util/extract-annotation-query.js @@ -0,0 +1,33 @@ +'use strict'; + +/** + * Extracts an annotation selection or default filter from a url. + * + * @param {string} url - The URL which may contain a '#annotations:' + * fragment. + * @return {Object} - An object with either an annotation ID or a filter string. + */ +function extractAnnotationQuery(url) { + var filter = {}; + try { + // Annotation IDs are url-safe-base64 identifiers + // See https://tools.ietf.org/html/rfc4648#page-7 + var annotFragmentMatch = url.match(/#annotations:([A-Za-z0-9_-]+)$/); + var queryFragmentMatch = url.match(/#annotations:(query|q):(.+)$/i); + if (queryFragmentMatch) { + filter.query = decodeURI(queryFragmentMatch[2]); + } else if (annotFragmentMatch) { + filter.annotations = annotFragmentMatch[1]; + } else { + filter = null; + } + } catch (err) { + // URI Error should return the page unfiltered. + filter = null; + } + return filter; +} + +module.exports = { + extractAnnotationQuery: extractAnnotationQuery, +}; diff --git a/src/annotator/util/test/extract-annotation-query-test.js b/src/annotator/util/test/extract-annotation-query-test.js new file mode 100644 index 00000000000..f52179f2b86 --- /dev/null +++ b/src/annotator/util/test/extract-annotation-query-test.js @@ -0,0 +1,38 @@ +'use strict'; + +var unroll = require('../../../shared/test/util').unroll; + +var annotationIds = require('../extract-annotation-query'); + +describe('annotation queries', function () { + + it ('returns null on invalid fragment', function () { + assert.equal(annotationIds.extractAnnotationQuery( + 'http://localhost:3000#annotations:\"TRYINGTOGETIN\");EVILSCRIPT()'), + null); + }); + + unroll('accepts annotation fragment from urls', function (testCase) { + assert.equal(annotationIds.extractAnnotationQuery(testCase.url).annotations, testCase.result); + }, [{ + url: 'http://localhost:3000#annotations:alphanum3ric_-only', + result: 'alphanum3ric_-only', + }, + ]); + + unroll('accepts query from annotation fragment', function(testCase) { + assert.equal(annotationIds.extractAnnotationQuery(testCase.url).query, testCase.result); + }, [{ + url: 'http://localhost:3000#annotations:q:user:USERNAME', + result: 'user:USERNAME', + }, + { + url: 'http://localhost:3000#annotations:QuerY:user:USERNAME', + result: 'user:USERNAME', + }, { + url: 'http://localhost:3000#annotations:q:user:USERNAME%20tag:KEYWORD', + result: 'user:USERNAME tag:KEYWORD', + }]); +}); + + diff --git a/src/sidebar/host-config.js b/src/sidebar/host-config.js index 89c527a95f6..bfeaeb787ed 100644 --- a/src/sidebar/host-config.js +++ b/src/sidebar/host-config.js @@ -16,6 +16,9 @@ function hostPageConfig(window) { var paramWhiteList = [ // Direct-linked annotation ID 'annotations', + + // Default query passed by url + 'query', // Config param added by the extension, Via etc. indicating how Hypothesis // was added to the page. diff --git a/src/sidebar/reducers/selection.js b/src/sidebar/reducers/selection.js index 5f731a61406..fa3655b0a1a 100644 --- a/src/sidebar/reducers/selection.js +++ b/src/sidebar/reducers/selection.js @@ -16,6 +16,7 @@ var uiConstants = require('../ui-constants'); var util = require('./util'); + /** * Default starting tab. */ @@ -37,9 +38,10 @@ TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_ANNOTATIONS] = ['Newest', 'Oldest', 'Loca TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_NOTES] = ['Newest', 'Oldest']; TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_ORPHANS] = ['Newest', 'Oldest', 'Location']; + function initialSelection(settings) { var selection = {}; - if (settings.annotations) { + if (settings.annotations && !settings.query) { selection[settings.annotations] = true; } return freeze(selection); @@ -81,7 +83,7 @@ function init(settings) { // IDs of annotations that should be highlighted highlighted: [], - filterQuery: null, + filterQuery: settings.query || null, selectedTab: TAB_DEFAULT,