Commit 4d0019c9 authored by Randall Leeds's avatar Randall Leeds

Switch to new standalone anchoring libs

parent ef0214a6
...@@ -8,10 +8,10 @@ raf = require('raf') ...@@ -8,10 +8,10 @@ raf = require('raf')
} = require('./types') } = require('./types')
querySelector = (type, selector, options) -> querySelector = (type, root, selector, options) ->
doQuery = (resolve, reject) -> doQuery = (resolve, reject) ->
try try
anchor = type.fromSelector(selector, options) anchor = type.fromSelector(root, selector, options)
range = anchor.toRange(options) range = anchor.toRange(options)
resolve(range) resolve(range)
catch error catch error
...@@ -26,11 +26,13 @@ querySelector = (type, selector, options) -> ...@@ -26,11 +26,13 @@ querySelector = (type, selector, options) ->
# It encapsulates the core anchoring algorithm, using the selectors alone or # It encapsulates the core anchoring algorithm, using the selectors alone or
# in combination to establish the best anchor within the document. # in combination to establish the best anchor within the document.
# #
# :param Element root: The root element of the anchoring context.
# :param Array selectors: The selectors to try. # :param Array selectors: The selectors to try.
# :param Object options: Options to pass to the anchor implementations.
# :return: A Promise that resolves to a Range on success. # :return: A Promise that resolves to a Range on success.
# :rtype: Promise # :rtype: Promise
#### ####
exports.anchor = (selectors, options = {}) -> exports.anchor = (root, selectors, options = {}) ->
# Selectors # Selectors
fragment = null fragment = null
position = null position = null
...@@ -44,7 +46,7 @@ exports.anchor = (selectors, options = {}) -> ...@@ -44,7 +46,7 @@ exports.anchor = (selectors, options = {}) ->
fragment = selector fragment = selector
when 'TextPositionSelector' when 'TextPositionSelector'
position = selector position = selector
options.position = position # TextQuoteAnchor hint options.hint = position.start # TextQuoteAnchor hint
when 'TextQuoteSelector' when 'TextQuoteSelector'
quote = selector quote = selector
when 'RangeSelector' when 'RangeSelector'
...@@ -62,33 +64,33 @@ exports.anchor = (selectors, options = {}) -> ...@@ -62,33 +64,33 @@ exports.anchor = (selectors, options = {}) ->
if fragment? if fragment?
promise = promise.catch -> promise = promise.catch ->
return querySelector(FragmentAnchor, fragment, options) return querySelector(FragmentAnchor, root, fragment, options)
.then(maybeAssertQuote) .then(maybeAssertQuote)
if range? if range?
promise = promise.catch -> promise = promise.catch ->
return querySelector(RangeAnchor, range, options) return querySelector(RangeAnchor, root, range, options)
.then(maybeAssertQuote) .then(maybeAssertQuote)
if position? if position?
promise = promise.catch -> promise = promise.catch ->
return querySelector(TextPositionAnchor, position, options) return querySelector(TextPositionAnchor, root, position, options)
.then(maybeAssertQuote) .then(maybeAssertQuote)
if quote? if quote?
promise = promise.catch -> promise = promise.catch ->
# Note: similarity of the quote is implied. # Note: similarity of the quote is implied.
return querySelector(TextQuoteAnchor, quote, options) return querySelector(TextQuoteAnchor, root, quote, options)
return promise return promise
exports.describe = (range, options = {}) -> exports.describe = (root, range, options = {}) ->
types = [FragmentAnchor, RangeAnchor, TextPositionAnchor, TextQuoteAnchor] types = [FragmentAnchor, RangeAnchor, TextPositionAnchor, TextQuoteAnchor]
selectors = for type in types selectors = for type in types
try try
anchor = type.fromRange(range, options) anchor = type.fromRange(root, range, options)
selector = anchor.toSelector(options) selector = anchor.toSelector(options)
catch catch
continue continue
......
...@@ -72,11 +72,13 @@ findPage = (offset, cache = {}) -> ...@@ -72,11 +72,13 @@ findPage = (offset, cache = {}) ->
# It encapsulates the core anchoring algorithm, using the selectors alone or # It encapsulates the core anchoring algorithm, using the selectors alone or
# in combination to establish the best anchor within the document. # in combination to establish the best anchor within the document.
# #
# :param Element root: The root element of the anchoring context.
# :param Array selectors: The selectors to try. # :param Array selectors: The selectors to try.
# :param Object options: Options to pass to the anchor implementations.
# :return: A Promise that resolves to a Range on success. # :return: A Promise that resolves to a Range on success.
# :rtype: Promise # :rtype: Promise
#### ####
exports.anchor = (selectors, options = {}) -> exports.anchor = (root, selectors, options = {}) ->
cache = options.cache ? {} cache = options.cache ? {}
# Selectors # Selectors
...@@ -106,8 +108,8 @@ exports.anchor = (selectors, options = {}) -> ...@@ -106,8 +108,8 @@ exports.anchor = (selectors, options = {}) ->
renderingDone = page.textLayer?.renderingDone renderingDone = page.textLayer?.renderingDone
if renderingState is RenderingStates.FINISHED and renderingDone if renderingState is RenderingStates.FINISHED and renderingDone
root = page.textLayer.textLayerDiv root = page.textLayer.textLayerDiv
selector = anchor.toSelector() selector = anchor.toSelector(options)
return html.anchor([selector], {root}) return html.anchor(root, [selector])
else else
div = page.div ? page.el div = page.div ? page.el
placeholder = div.getElementsByClassName('annotator-placeholder')[0] placeholder = div.getElementsByClassName('annotator-placeholder')[0]
...@@ -130,7 +132,7 @@ exports.anchor = (selectors, options = {}) -> ...@@ -130,7 +132,7 @@ exports.anchor = (selectors, options = {}) ->
end = position.end - offset end = position.end - offset
length = end - start length = end - start
assertQuote(textContent.substr(start, length)) assertQuote(textContent.substr(start, length))
anchor = new TextPositionAnchor(start, end) anchor = new TextPositionAnchor(root, start, end)
return anchorByPosition(page, anchor) return anchorByPosition(page, anchor)
if quote? if quote?
...@@ -142,12 +144,6 @@ exports.anchor = (selectors, options = {}) -> ...@@ -142,12 +144,6 @@ exports.anchor = (selectors, options = {}) ->
{page, anchor} = cache.quotePosition[quote.exact][position.start] {page, anchor} = cache.quotePosition[quote.exact][position.start]
return anchorByPosition(page, anchor) return anchorByPosition(page, anchor)
storeAndAnchor = (page, anchor) ->
if position?
cache.quotePosition[quote.exact] ?= {}
cache.quotePosition[quote.exact][position.start] = {page, anchor}
return anchorByPosition(page, anchor)
findInPages = ([pageIndex, rest...]) -> findInPages = ([pageIndex, rest...]) ->
page = getPage(pageIndex) page = getPage(pageIndex)
content = getPageTextContent(pageIndex, cache.pageText) content = getPageTextContent(pageIndex, cache.pageText)
...@@ -155,16 +151,15 @@ exports.anchor = (selectors, options = {}) -> ...@@ -155,16 +151,15 @@ exports.anchor = (selectors, options = {}) ->
Promise.all([content, offset, page]) Promise.all([content, offset, page])
.then (results) -> .then (results) ->
[content, offset, page] = results [content, offset, page] = results
pageOptions = {root: {textContent: content}} root = {textContent: content}
pageOptions = {}
if position? if position?
# XXX: must be on one page pageOptions.hint = position.start - offset
start = position.start - offset anchor = new TextQuoteAnchor.fromSelector(root, quote)
end = position.end - offset anchor = anchor.toPositionAnchor(pageOptions)
pageOptions.position = {start, end} cache.quotePosition[quote.exact] ?= {}
anchor = new TextQuoteAnchor.fromSelector(quote, pageOptions) cache.quotePosition[quote.exact][position.start] = {page, anchor}
return Promise.resolve(anchor) return anchorByPosition(page, anchor)
.then((a) -> return a.toPositionAnchor(pageOptions))
.then((a) -> return storeAndAnchor(page, a))
.catch -> .catch ->
if rest.length if rest.length
return findInPages(rest) return findInPages(rest)
...@@ -191,7 +186,7 @@ exports.anchor = (selectors, options = {}) -> ...@@ -191,7 +186,7 @@ exports.anchor = (selectors, options = {}) ->
return promise return promise
exports.describe = (range, options = {}) -> exports.describe = (root, range, options = {}) ->
cache = options.cache ? {} cache = options.cache ? {}
range = new xpathRange.BrowserRange(range).normalize() range = new xpathRange.BrowserRange(range).normalize()
...@@ -218,13 +213,12 @@ exports.describe = (range, options = {}) -> ...@@ -218,13 +213,12 @@ exports.describe = (range, options = {}) ->
start += pageOffset start += pageOffset
end += pageOffset end += pageOffset
position = new TextPositionAnchor(start, end).toSelector() position = new TextPositionAnchor(root, start, end).toSelector(options)
r = document.createRange() r = document.createRange()
r.setStartBefore(startRange.start) r.setStartBefore(startRange.start)
r.setEndAfter(endRange.end) r.setEndAfter(endRange.end)
pageOptions = {root: startTextLayer} quote = TextQuoteAnchor.fromRange(root, r, options).toSelector(options)
quote = TextQuoteAnchor.fromRange(r, pageOptions).toSelector()
return Promise.all([position, quote]) return Promise.all([position, quote])
...@@ -2,67 +2,12 @@ Annotator = require('annotator') ...@@ -2,67 +2,12 @@ Annotator = require('annotator')
$ = Annotator.$ $ = Annotator.$
xpathRange = Annotator.Range xpathRange = Annotator.Range
DiffMatchPatch = require('diff-match-patch')
seek = require('dom-seek')
# Helper function for throwing common errors
# Helper functions for throwing common errors
missingParameter = (name) -> missingParameter = (name) ->
throw new Error('missing required parameter "' + name + '"') throw new Error('missing required parameter "' + name + '"')
notImplemented = ->
throw new Error('method not implemented')
###*
# class:: Abstract base class for anchors.
###
class Anchor
# Create an instance of the anchor from a Range.
@fromRange: notImplemented
# Create an instance of the anchor from a selector.
@fromSelector: notImplemented
# Create a Range from the anchor.
toRange: notImplemented
# Create a selector from the anchor.
toSelector: notImplemented
###*
# class:: FragmentAnchor(id)
#
# This anchor type represents a fragment identifier.
#
# :param String id: The id of the fragment for the anchor.
###
class FragmentAnchor extends Anchor
constructor: (@id) ->
unless @id? then missingParameter('id')
@fromRange: (range) ->
id = $(range.commonAncestorContainer).closest('[id]').attr('id')
return new FragmentAnchor(id)
@fromSelector: (selector) ->
return new FragmentAnchor(selector.value)
toSelector: ->
return {
type: 'FragmentSelector'
value: @id
}
toRange: ->
el = document.getElementById(@id)
range = document.createRange()
range.selectNode(el)
return range
###* ###*
# class:: RangeAnchor(range) # class:: RangeAnchor(range)
# #
...@@ -70,34 +15,32 @@ class FragmentAnchor extends Anchor ...@@ -70,34 +15,32 @@ class FragmentAnchor extends Anchor
# #
# :param Range range: A range describing the anchor. # :param Range range: A range describing the anchor.
### ###
class RangeAnchor extends Anchor class RangeAnchor
constructor: (@range) -> constructor: (root, range) ->
unless @range? then missingParameter('range') unless root? then missingParameter('root')
unless range? then missingParameter('range')
@root = root
@range = xpathRange.sniff(range).normalize(@root)
@fromRange: (range, options = {}) -> @fromRange: (root, range) ->
root = options.root or document.body return new RangeAnchor(root, range)
range = xpathRange.sniff(range).normalize(root)
return new RangeAnchor(range)
# Create and anchor using the saved Range selector. # Create and anchor using the saved Range selector.
@fromSelector: (selector, options = {}) -> @fromSelector: (root, selector) ->
root = options.root or document.body
data = { data = {
start: selector.startContainer start: selector.startContainer
startOffset: selector.startOffset startOffset: selector.startOffset
end: selector.endContainer end: selector.endContainer
endOffset: selector.endOffset endOffset: selector.endOffset
} }
range = new xpathRange.SerializedRange(data).normalize(root) range = new xpathRange.SerializedRange(data)
return new RangeAnchor(range) return new RangeAnchor(root, range)
toRange: () -> toRange: () ->
return @range.toRange() return @range.toRange()
toSelector: (options = {}) -> toSelector: (options = {}) ->
root = options.root or document.body range = @range.serialize(@root, options.ignoreSelector)
ignoreSelector = options.ignoreSelector
range = @range.serialize(root, ignoreSelector)
return { return {
type: 'RangeSelector' type: 'RangeSelector'
startContainer: range.start startContainer: range.start
...@@ -106,160 +49,7 @@ class RangeAnchor extends Anchor ...@@ -106,160 +49,7 @@ class RangeAnchor extends Anchor
endOffset: range.endOffset endOffset: range.endOffset
} }
###*
# class:: TextPositionAnchor(start, end)
#
# This anchor type represents a piece of text described by start and end
# character offsets.
#
# :param Number start: The start offset for the anchor text.
# :param Number end: The end offset for the anchor text.
###
class TextPositionAnchor extends Anchor
constructor: (@start, @end) ->
unless @start? then missingParameter('start')
unless @end? then missingParameter('end')
@fromRange: (range, options = {}) ->
root = options.root or document.body
filter = options.filter or null
range = new xpathRange.BrowserRange(range).normalize(root)
iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, filter)
start = seek(iter, range.start)
end = seek(iter, range.end) + start + range.end.textContent.length
new TextPositionAnchor(start, end)
@fromSelector: (selector) ->
return new TextPositionAnchor(selector.start, selector.end)
toRange: (options = {}) ->
root = options.root or document.body
filter = options.filter or null
range = document.createRange()
iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, filter)
{start, end} = this
count = seek(iter, start)
remainder = start - count
if iter.pointerBeforeReferenceNode
range.setStart(iter.referenceNode, remainder)
else
range.setStart(iter.nextNode(), remainder)
iter.previousNode()
length = (end - start) + remainder
count = seek(iter, length)
remainder = length - count
if iter.pointerBeforeReferenceNode
range.setEnd(iter.referenceNode, remainder)
else
range.setEnd(iter.nextNode(), remainder)
return range
toSelector: ->
return {
type: 'TextPositionSelector'
start: @start
end: @end
}
###*
# class:: TextQuoteAnchor(quote, [prefix, [suffix, [start, [end]]]])
#
# This anchor type represents a piece of text described by a quote. The quote
# may optionally include textual context and/or a position within the text.
#
# :param String quote: The anchor text to match.
# :param String prefix: A prefix that preceeds the anchor text.
# :param String suffix: A suffix that follows the anchor text.
###
class TextQuoteAnchor extends Anchor
constructor: (@quote, @prefix='', @suffix='') ->
unless @quote? then missingParameter('quote')
@fromRange: (range, options = {}) ->
root = options.root or document.body
filter = options.filter or null
range = new xpathRange.BrowserRange(range).normalize(root)
iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, filter)
start = seek(iter, range.start)
count = seek(iter, range.end)
end = start + count + range.end.textContent.length
corpus = root.textContent
prefixStart = Math.max(start - 32, 0)
exact = corpus.substr(start, end - start)
prefix = corpus.substr(prefixStart, start - prefixStart)
suffix = corpus.substr(end, 32)
return new TextQuoteAnchor(exact, prefix, suffix)
@fromSelector: (selector) ->
{exact, prefix, suffix} = selector
return new TextQuoteAnchor(exact, prefix, suffix)
toRange: (options = {}) ->
return this.toPositionAnchor(options).toRange()
toSelector: ->
selector = {
type: 'TextQuoteSelector'
exact: @quote
}
if @prefix? then selector.prefix = @prefix
if @suffix? then selector.suffix = @suffix
return selector
toPositionAnchor: (options = {}) ->
root = options.root or document.body
dmp = new DiffMatchPatch()
dmp.Match_Distance = root.textContent.length * 2
foldSlices = (acc, slice) ->
result = dmp.match_main(root.textContent, slice, acc.loc)
if result is -1
throw new Error('no match found')
acc.loc = result + slice.length
acc.start = Math.min(acc.start, result)
acc.end = Math.max(acc.end, result + slice.length)
return acc
slices = @quote.match(/(.|[\r\n]){1,32}/g)
loc = options.position?.start ? root.textContent.length / 2
start = -1
if @prefix?
result = dmp.match_main(root.textContent, @prefix, loc)
if result > -1
loc = end = start = result + @prefix.length
if start is -1
firstSlice = slices.shift()
result = dmp.match_main(root.textContent, firstSlice, loc)
if result > -1
start = result
loc = end = start + firstSlice.length
else
throw new Error('no match found')
dmp.Match_Distance = 64
{start, end} = slices.reduce(foldSlices, {start, end, loc})
return new TextPositionAnchor(start, end)
exports.Anchor = Anchor
exports.FragmentAnchor = FragmentAnchor
exports.RangeAnchor = RangeAnchor exports.RangeAnchor = RangeAnchor
exports.TextPositionAnchor = TextPositionAnchor exports.FragmentAnchor = require('dom-anchor-fragment')
exports.TextQuoteAnchor = TextQuoteAnchor exports.TextPositionAnchor = require('dom-anchor-text-position')
exports.TextQuoteAnchor = require('dom-anchor-text-quote')
...@@ -163,6 +163,7 @@ module.exports = class Guest extends Annotator ...@@ -163,6 +163,7 @@ module.exports = class Guest extends Annotator
setupAnnotation: (annotation) -> setupAnnotation: (annotation) ->
self = this self = this
root = @element[0]
anchors = [] anchors = []
anchoredTargets = [] anchoredTargets = []
...@@ -175,7 +176,7 @@ module.exports = class Guest extends Annotator ...@@ -175,7 +176,7 @@ module.exports = class Guest extends Annotator
cache: self.anchoringCache cache: self.anchoringCache
ignoreSelector: '[class^="annotator-"]' ignoreSelector: '[class^="annotator-"]'
} }
return self.anchoring.anchor(target.selector, options) return self.anchoring.anchor(root, target.selector, options)
.then((range) -> {annotation, target, range}) .then((range) -> {annotation, target, range})
.catch(-> {annotation, target}) .catch(-> {annotation, target})
...@@ -183,7 +184,7 @@ module.exports = class Guest extends Annotator ...@@ -183,7 +184,7 @@ module.exports = class Guest extends Annotator
return anchor unless anchor.range? return anchor unless anchor.range?
return animationPromise -> return animationPromise ->
range = Annotator.Range.sniff(anchor.range) range = Annotator.Range.sniff(anchor.range)
normedRange = range.normalize(self.element[0]) normedRange = range.normalize(root)
highlights = highlighter.highlightRange(normedRange) highlights = highlighter.highlightRange(normedRange)
rect = highlighter.getBoundingClientRect(highlights) rect = highlighter.getBoundingClientRect(highlights)
...@@ -233,6 +234,7 @@ module.exports = class Guest extends Annotator ...@@ -233,6 +234,7 @@ module.exports = class Guest extends Annotator
createAnnotation: (annotation = {}) -> createAnnotation: (annotation = {}) ->
self = this self = this
root = @element[0]
ranges = @selectedRanges ? [] ranges = @selectedRanges ? []
@selectedRanges = null @selectedRanges = null
...@@ -242,7 +244,7 @@ module.exports = class Guest extends Annotator ...@@ -242,7 +244,7 @@ module.exports = class Guest extends Annotator
cache: self.anchoringCache cache: self.anchoringCache
ignoreSelector: '[class^="annotator-"]' ignoreSelector: '[class^="annotator-"]'
} }
return self.anchoring.describe(range, options) return self.anchoring.describe(root, range, options)
setDocumentInfo = ({metadata, uri}) -> setDocumentInfo = ({metadata, uri}) ->
annotation.uri = uri annotation.uri = uri
......
...@@ -17,12 +17,12 @@ class PDF extends Annotator.Plugin ...@@ -17,12 +17,12 @@ class PDF extends Annotator.Plugin
anchoring = require('../anchoring/pdf') anchoring = require('../anchoring/pdf')
@annotator.anchoring = { @annotator.anchoring = {
anchor: (selectors, options = {}) -> anchor: (root, selectors, options = {}) ->
options = extend({}, options, {cache}) options = extend({}, options, {cache})
return anchoring.anchor(selectors, options) return anchoring.anchor(root, selectors, options)
describe: (range, options = {}) -> describe: (root, range, options = {}) ->
options = extend({}, options, {cache}) options = extend({}, options, {cache})
return anchoring.describe(range, options) return anchoring.describe(root, range, options)
} }
@pdfViewer = PDFViewerApplication.pdfViewer @pdfViewer = PDFViewerApplication.pdfViewer
......
...@@ -10,6 +10,9 @@ ...@@ -10,6 +10,9 @@
"coffee-script": "1.7.1", "coffee-script": "1.7.1",
"coffeeify": "^1.0.0", "coffeeify": "^1.0.0",
"diff-match-patch": "^1.0.0", "diff-match-patch": "^1.0.0",
"dom-anchor-fragment": "^1.0.0",
"dom-anchor-text-position": "^1.0.1",
"dom-anchor-text-quote": "^1.1.2",
"dom-seek": "^1.0.0", "dom-seek": "^1.0.0",
"es6-promise": "^2.1.0", "es6-promise": "^2.1.0",
"extend": "^2.0.0", "extend": "^2.0.0",
...@@ -57,6 +60,9 @@ ...@@ -57,6 +60,9 @@
"annotator-auth": "./h/static/scripts/vendor/annotator.auth.js", "annotator-auth": "./h/static/scripts/vendor/annotator.auth.js",
"angular": "./h/static/scripts/vendor/angular.js", "angular": "./h/static/scripts/vendor/angular.js",
"angular-mock": "./h/static/scripts/vendor/angular-mocks.js", "angular-mock": "./h/static/scripts/vendor/angular-mocks.js",
"dom-anchor-fragment": "./node_modules/dom-anchor-fragment/dist/dom-anchor-fragment.js",
"dom-anchor-text-position": "./node_modules/dom-anchor-text-position/dist/dom-anchor-text-position.js",
"dom-anchor-text-quote": "./node_modules/dom-anchor-text-quote/dist/dom-anchor-text-quote.js",
"dom-seek": "./node_modules/dom-seek/dist/seek.js", "dom-seek": "./node_modules/dom-seek/dist/seek.js",
"es6-promise": "./node_modules/es6-promise/dist/es6-promise.js", "es6-promise": "./node_modules/es6-promise/dist/es6-promise.js",
"hammerjs": "./node_modules/hammerjs/hammer.js", "hammerjs": "./node_modules/hammerjs/hammer.js",
...@@ -85,6 +91,20 @@ ...@@ -85,6 +91,20 @@
] ]
}, },
"angular-mock": "global:angular.mock", "angular-mock": "global:angular.mock",
"dom-anchor-fragment": "domAnchorFragment",
"dom-anchor-text-position": {
"depends": [
"dom-seek:seek"
],
"exports": "domAnchorTextPosition"
},
"dom-anchor-text-quote": {
"depends": [
"diff-match-patch:DiffMatchPatch",
"dom-anchor-text-position:TextPositionAnchor"
],
"exports": "domAnchorTextQuote"
},
"dom-seek": "seek", "dom-seek": "seek",
"es6-promise": "ES6Promise", "es6-promise": "ES6Promise",
"hammerjs": "Hammer", "hammerjs": "Hammer",
......
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