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')
} = require('./types')
querySelector = (type, selector, options) ->
querySelector = (type, root, selector, options) ->
doQuery = (resolve, reject) ->
try
anchor = type.fromSelector(selector, options)
anchor = type.fromSelector(root, selector, options)
range = anchor.toRange(options)
resolve(range)
catch error
......@@ -26,11 +26,13 @@ querySelector = (type, selector, options) ->
# It encapsulates the core anchoring algorithm, using the selectors alone or
# 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 Object options: Options to pass to the anchor implementations.
# :return: A Promise that resolves to a Range on success.
# :rtype: Promise
####
exports.anchor = (selectors, options = {}) ->
exports.anchor = (root, selectors, options = {}) ->
# Selectors
fragment = null
position = null
......@@ -44,7 +46,7 @@ exports.anchor = (selectors, options = {}) ->
fragment = selector
when 'TextPositionSelector'
position = selector
options.position = position # TextQuoteAnchor hint
options.hint = position.start # TextQuoteAnchor hint
when 'TextQuoteSelector'
quote = selector
when 'RangeSelector'
......@@ -62,33 +64,33 @@ exports.anchor = (selectors, options = {}) ->
if fragment?
promise = promise.catch ->
return querySelector(FragmentAnchor, fragment, options)
return querySelector(FragmentAnchor, root, fragment, options)
.then(maybeAssertQuote)
if range?
promise = promise.catch ->
return querySelector(RangeAnchor, range, options)
return querySelector(RangeAnchor, root, range, options)
.then(maybeAssertQuote)
if position?
promise = promise.catch ->
return querySelector(TextPositionAnchor, position, options)
return querySelector(TextPositionAnchor, root, position, options)
.then(maybeAssertQuote)
if quote?
promise = promise.catch ->
# Note: similarity of the quote is implied.
return querySelector(TextQuoteAnchor, quote, options)
return querySelector(TextQuoteAnchor, root, quote, options)
return promise
exports.describe = (range, options = {}) ->
exports.describe = (root, range, options = {}) ->
types = [FragmentAnchor, RangeAnchor, TextPositionAnchor, TextQuoteAnchor]
selectors = for type in types
try
anchor = type.fromRange(range, options)
anchor = type.fromRange(root, range, options)
selector = anchor.toSelector(options)
catch
continue
......
......@@ -72,11 +72,13 @@ findPage = (offset, cache = {}) ->
# It encapsulates the core anchoring algorithm, using the selectors alone or
# 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 Object options: Options to pass to the anchor implementations.
# :return: A Promise that resolves to a Range on success.
# :rtype: Promise
####
exports.anchor = (selectors, options = {}) ->
exports.anchor = (root, selectors, options = {}) ->
cache = options.cache ? {}
# Selectors
......@@ -106,8 +108,8 @@ exports.anchor = (selectors, options = {}) ->
renderingDone = page.textLayer?.renderingDone
if renderingState is RenderingStates.FINISHED and renderingDone
root = page.textLayer.textLayerDiv
selector = anchor.toSelector()
return html.anchor([selector], {root})
selector = anchor.toSelector(options)
return html.anchor(root, [selector])
else
div = page.div ? page.el
placeholder = div.getElementsByClassName('annotator-placeholder')[0]
......@@ -130,7 +132,7 @@ exports.anchor = (selectors, options = {}) ->
end = position.end - offset
length = end - start
assertQuote(textContent.substr(start, length))
anchor = new TextPositionAnchor(start, end)
anchor = new TextPositionAnchor(root, start, end)
return anchorByPosition(page, anchor)
if quote?
......@@ -142,12 +144,6 @@ exports.anchor = (selectors, options = {}) ->
{page, anchor} = cache.quotePosition[quote.exact][position.start]
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...]) ->
page = getPage(pageIndex)
content = getPageTextContent(pageIndex, cache.pageText)
......@@ -155,16 +151,15 @@ exports.anchor = (selectors, options = {}) ->
Promise.all([content, offset, page])
.then (results) ->
[content, offset, page] = results
pageOptions = {root: {textContent: content}}
root = {textContent: content}
pageOptions = {}
if position?
# XXX: must be on one page
start = position.start - offset
end = position.end - offset
pageOptions.position = {start, end}
anchor = new TextQuoteAnchor.fromSelector(quote, pageOptions)
return Promise.resolve(anchor)
.then((a) -> return a.toPositionAnchor(pageOptions))
.then((a) -> return storeAndAnchor(page, a))
pageOptions.hint = position.start - offset
anchor = new TextQuoteAnchor.fromSelector(root, quote)
anchor = anchor.toPositionAnchor(pageOptions)
cache.quotePosition[quote.exact] ?= {}
cache.quotePosition[quote.exact][position.start] = {page, anchor}
return anchorByPosition(page, anchor)
.catch ->
if rest.length
return findInPages(rest)
......@@ -191,7 +186,7 @@ exports.anchor = (selectors, options = {}) ->
return promise
exports.describe = (range, options = {}) ->
exports.describe = (root, range, options = {}) ->
cache = options.cache ? {}
range = new xpathRange.BrowserRange(range).normalize()
......@@ -218,13 +213,12 @@ exports.describe = (range, options = {}) ->
start += pageOffset
end += pageOffset
position = new TextPositionAnchor(start, end).toSelector()
position = new TextPositionAnchor(root, start, end).toSelector(options)
r = document.createRange()
r.setStartBefore(startRange.start)
r.setEndAfter(endRange.end)
pageOptions = {root: startTextLayer}
quote = TextQuoteAnchor.fromRange(r, pageOptions).toSelector()
quote = TextQuoteAnchor.fromRange(root, r, options).toSelector(options)
return Promise.all([position, quote])
......@@ -2,67 +2,12 @@ Annotator = require('annotator')
$ = Annotator.$
xpathRange = Annotator.Range
DiffMatchPatch = require('diff-match-patch')
seek = require('dom-seek')
# Helper functions for throwing common errors
# Helper function for throwing common errors
missingParameter = (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)
#
......@@ -70,34 +15,32 @@ class FragmentAnchor extends Anchor
#
# :param Range range: A range describing the anchor.
###
class RangeAnchor extends Anchor
constructor: (@range) ->
unless @range? then missingParameter('range')
class RangeAnchor
constructor: (root, range) ->
unless root? then missingParameter('root')
unless range? then missingParameter('range')
@root = root
@range = xpathRange.sniff(range).normalize(@root)
@fromRange: (range, options = {}) ->
root = options.root or document.body
range = xpathRange.sniff(range).normalize(root)
return new RangeAnchor(range)
@fromRange: (root, range) ->
return new RangeAnchor(root, range)
# Create and anchor using the saved Range selector.
@fromSelector: (selector, options = {}) ->
root = options.root or document.body
@fromSelector: (root, selector) ->
data = {
start: selector.startContainer
startOffset: selector.startOffset
end: selector.endContainer
endOffset: selector.endOffset
}
range = new xpathRange.SerializedRange(data).normalize(root)
return new RangeAnchor(range)
range = new xpathRange.SerializedRange(data)
return new RangeAnchor(root, range)
toRange: () ->
return @range.toRange()
toSelector: (options = {}) ->
root = options.root or document.body
ignoreSelector = options.ignoreSelector
range = @range.serialize(root, ignoreSelector)
range = @range.serialize(@root, options.ignoreSelector)
return {
type: 'RangeSelector'
startContainer: range.start
......@@ -106,160 +49,7 @@ class RangeAnchor extends Anchor
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.TextPositionAnchor = TextPositionAnchor
exports.TextQuoteAnchor = TextQuoteAnchor
exports.FragmentAnchor = require('dom-anchor-fragment')
exports.TextPositionAnchor = require('dom-anchor-text-position')
exports.TextQuoteAnchor = require('dom-anchor-text-quote')
......@@ -163,6 +163,7 @@ module.exports = class Guest extends Annotator
setupAnnotation: (annotation) ->
self = this
root = @element[0]
anchors = []
anchoredTargets = []
......@@ -175,7 +176,7 @@ module.exports = class Guest extends Annotator
cache: self.anchoringCache
ignoreSelector: '[class^="annotator-"]'
}
return self.anchoring.anchor(target.selector, options)
return self.anchoring.anchor(root, target.selector, options)
.then((range) -> {annotation, target, range})
.catch(-> {annotation, target})
......@@ -183,7 +184,7 @@ module.exports = class Guest extends Annotator
return anchor unless anchor.range?
return animationPromise ->
range = Annotator.Range.sniff(anchor.range)
normedRange = range.normalize(self.element[0])
normedRange = range.normalize(root)
highlights = highlighter.highlightRange(normedRange)
rect = highlighter.getBoundingClientRect(highlights)
......@@ -233,6 +234,7 @@ module.exports = class Guest extends Annotator
createAnnotation: (annotation = {}) ->
self = this
root = @element[0]
ranges = @selectedRanges ? []
@selectedRanges = null
......@@ -242,7 +244,7 @@ module.exports = class Guest extends Annotator
cache: self.anchoringCache
ignoreSelector: '[class^="annotator-"]'
}
return self.anchoring.describe(range, options)
return self.anchoring.describe(root, range, options)
setDocumentInfo = ({metadata, uri}) ->
annotation.uri = uri
......
......@@ -17,12 +17,12 @@ class PDF extends Annotator.Plugin
anchoring = require('../anchoring/pdf')
@annotator.anchoring = {
anchor: (selectors, options = {}) ->
anchor: (root, selectors, options = {}) ->
options = extend({}, options, {cache})
return anchoring.anchor(selectors, options)
describe: (range, options = {}) ->
return anchoring.anchor(root, selectors, options)
describe: (root, range, options = {}) ->
options = extend({}, options, {cache})
return anchoring.describe(range, options)
return anchoring.describe(root, range, options)
}
@pdfViewer = PDFViewerApplication.pdfViewer
......
......@@ -10,6 +10,9 @@
"coffee-script": "1.7.1",
"coffeeify": "^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",
"es6-promise": "^2.1.0",
"extend": "^2.0.0",
......@@ -57,6 +60,9 @@
"annotator-auth": "./h/static/scripts/vendor/annotator.auth.js",
"angular": "./h/static/scripts/vendor/angular.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",
"es6-promise": "./node_modules/es6-promise/dist/es6-promise.js",
"hammerjs": "./node_modules/hammerjs/hammer.js",
......@@ -85,6 +91,20 @@
]
},
"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",
"es6-promise": "ES6Promise",
"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