Commit 471cb769 authored by Randall Leeds's avatar Randall Leeds

PDF TextPosition and TextQuote starting to work

TextPositionAnchor can be generated and re-anchored within a PDF so
long as the selection doesn't span a page boundary.

TextQuoteAnchor is generated, but not re-anchored.
parent 7a09ea0e
...@@ -16,7 +16,11 @@ ...@@ -16,7 +16,11 @@
# :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 = (selectors) ->
options =
root: document.body
ignoreSelector: '[class^="annotator-"]'
# Selectors # Selectors
fragment = null fragment = null
position = null position = null
...@@ -48,34 +52,38 @@ exports.anchor = (selectors, options) -> ...@@ -48,34 +52,38 @@ exports.anchor = (selectors, options) ->
if fragment? if fragment?
promise = promise.catch => promise = promise.catch =>
Promise.resolve(FragmentAnchor.fromSelector(fragment, options)) Promise.resolve(FragmentAnchor.fromSelector(fragment, options))
.then((a) -> Promise.resolve(a.toRange(options))) .then((a) -> a.toRange(options))
.then(assertQuote) .then(assertQuote)
if range? if range?
promise = promise.catch => promise = promise.catch =>
Promise.resolve(RangeAnchor.fromSelector(range, options)) Promise.resolve(RangeAnchor.fromSelector(range, options))
.then((a) -> Promise.resolve(a.toRange(options))) .then((a) -> a.toRange(options))
.then(assertQuote) .then(assertQuote)
if position? if position?
promise = promise.catch => promise = promise.catch =>
Promise.resolve(TextPositionAnchor.fromSelector(position, options)) Promise.resolve(TextPositionAnchor.fromSelector(position, options))
.then((a) -> Promise.resolve(a.toRange(options))) .then((a) -> a.toRange(options))
.then(assertQuote) .then(assertQuote)
if quote? if quote?
promise = promise.catch => promise = promise.catch =>
Promise.resolve(TextQuoteAnchor.fromSelector(quote, options)) Promise.resolve(TextQuoteAnchor.fromSelector(quote, options))
.then((a) -> Promise.resolve(a.toRange(options))) .then((a) -> a.toRange(options))
return promise return promise
exports.describe = (range, options) -> exports.describe = (range) ->
options =
root: document.body
ignoreSelector: '[class^="annotator-"]'
maybeDescribeWith = (type) -> maybeDescribeWith = (type) ->
return Promise.resolve(type) return Promise.resolve(type)
.then((t) -> Promise.resolve(t.fromRange(range, options))) .then((t) -> t.fromRange(range, options))
.then((a) -> Promise.resolve(a.toSelector(options))) .then((a) -> a.toSelector(options))
.catch(-> null) .catch(-> null)
selectors = (maybeDescribeWith(type) for type in [ selectors = (maybeDescribeWith(type) for type in [
......
if (window.PDFViewerApplication)
module.exports = require('./pdf')
else
module.exports = require('./html')
seek = require('dom-seek')
Annotator = require('annotator')
xpathRange = Annotator.Range
{
TextPositionAnchor
TextQuoteAnchor
} = require('./types')
getSiblingIndex = (node) ->
siblings = Array.prototype.slice.call(node.parentNode.childNodes)
return siblings.indexOf(node)
getNodeTextLayer = (node) ->
until node.classList?.contains('page')
node = node.parentNode
return node.getElementsByClassName('textLayer')[0]
getPageTextLayer = (pageIndex) ->
return PDFViewerApplication.pdfViewer.pages[pageIndex].textLayer
getPageTextContent = (pageIndex) ->
return PDFViewerApplication.pdfViewer.getPageTextContent(pageIndex)
getPageOffset = (pageIndex) ->
index = -1
next = (offset) ->
if ++index is pageIndex
return Promise.resolve(offset)
return getPageTextContent(index)
.then(getTextContentLength)
.then((length) -> next(offset + length))
return next(0)
getTextContentLength = (textContent) ->
sum = 0
for item in textContent.items
sum += item.str.length
return sum
findPage = (offset) ->
index = 0
total = 0
next = (length) ->
if total + length >= offset
offset = total
return Promise.resolve({index, offset})
else
index++
total += length
return count()
count = ->
return getPageTextContent(index)
.then(getTextContentLength)
.then(next)
return count()
###*
# Anchor a set of selectors.
#
# This function converts a set of selectors into a document range using.
# It encapsulates the core anchoring algorithm, using the selectors alone or
# in combination to establish the best anchor within the document.
#
# :param Array selectors: The selectors to try.
# :return: A Promise that resolves to a Range on success.
# :rtype: Promise
####
exports.anchor = (selectors) ->
options =
root: document.getElementById('viewer')
ignoreSelector: '[class^="annotator-"]'
# Selectors
position = null
quote = null
# Collect all the selectors
for selector in selectors ? []
switch selector.type
when 'TextPositionSelector'
position = selector
when 'TextQuoteSelector'
quote = selector
# Until we successfully anchor, we fail.
promise = Promise.reject('unable to anchor')
# Assert the quote matches the stored quote, if applicable
assertQuote = (range) ->
if quote?.exact? and range.toString() != quote.exact
throw new Error('quote mismatch')
else
return range
if position?
promise = promise.catch =>
findPage(position.start)
.then(({index, offset}) ->
textLayer = getPageTextLayer(index)
start = position.start - offset
end = position.end - offset
root = textLayer.textLayerDiv
Promise.resolve(TextPositionAnchor.fromSelector({start, end}, {root}))
.then((a) -> Promise.resolve(a.toRange({root})))
)
.then(assertQuote)
return promise
exports.describe = (range) ->
range = new xpathRange.BrowserRange(range).normalize()
startTextLayer = getNodeTextLayer(range.start)
endTextLayer = getNodeTextLayer(range.end)
# XXX: range covers only one page
if startTextLayer isnt endTextLayer
throw new Error('selecting across page breaks is not supported')
startRange = range.limit(startTextLayer)
endRange = range.limit(endTextLayer)
startPageIndex = getSiblingIndex(startTextLayer.parentNode)
endPageIndex = getSiblingIndex(endTextLayer.parentNode)
iter = seek.createTextIterator(startTextLayer)
return iter.seek(range.start).then (start) ->
iter.seek(range.end).then (length) ->
end = start + length + range.end.textContent.length
getPageOffset(startPageIndex).then (offset) ->
# XXX: range covers only one page
start += offset
end += offset
position = new TextPositionAnchor(start, end).toSelector()
options = {root: startTextLayer}
r = document.createRange()
r.setStartBefore(startRange.start)
r.setEndAfter(endRange.end)
quote = Promise.resolve(TextQuoteAnchor.fromRange(r, options))
.then((a) -> a.toSelector())
return Promise.all([position, quote])
...@@ -3,7 +3,7 @@ Promise = global.Promise or require('es6-promise').Promise ...@@ -3,7 +3,7 @@ Promise = global.Promise or require('es6-promise').Promise
Annotator = require('annotator') Annotator = require('annotator')
$ = Annotator.$ $ = Annotator.$
anchoring = require('./anchoring/html') anchoring = require('./anchoring/main')
highlighter = require('./highlighter') highlighter = require('./highlighter')
...@@ -145,19 +145,17 @@ module.exports = class Guest extends Annotator ...@@ -145,19 +145,17 @@ module.exports = class Guest extends Annotator
this.removeEvents() this.removeEvents()
setupAnnotation: (annotation) -> setupAnnotation: (annotation) ->
{anchored, unanchored, plugins} = this {anchored, unanchored, element, plugins} = this
root = @element[0]
ignoreSelector = '[class^="annotator-"]'
maybeAnchor = (target) -> maybeAnchor = (target) ->
anchoring.anchor(target.selector, {root, ignoreSelector}) anchoring.anchor(target.selector)
.then(highlightRange) .then(highlightRange)
.then((highlights) -> {annotation, target, highlights}) .then((highlights) -> {annotation, target, highlights})
.catch((reason) -> {annotation, target, reason}) .catch((reason) -> {annotation, target, reason})
highlightRange = (range) -> highlightRange = (range) ->
return new Promise(raf).then -> return new Promise(raf).then ->
normedRange = Annotator.Range.sniff(range).normalize(root) normedRange = Annotator.Range.sniff(range).normalize(element[0])
return highlighter.highlightRange(normedRange) return highlighter.highlightRange(normedRange)
storeAndSync = (results) -> storeAndSync = (results) ->
...@@ -179,16 +177,12 @@ module.exports = class Guest extends Annotator ...@@ -179,16 +177,12 @@ module.exports = class Guest extends Annotator
annotation annotation
createAnnotation: (annotation = {}) -> createAnnotation: (annotation = {}) ->
root = @element[0]
ignoreSelector = '[class^="annotator-"]'
options = {root, ignoreSelector}
ranges = @selectedRanges ranges = @selectedRanges
@selectedRanges = null @selectedRanges = null
info = this.getDocumentInfo() info = this.getDocumentInfo()
Promise.all(ranges.map((r) -> anchoring.describe(r, options))) Promise.all(ranges.map(anchoring.describe))
.then((targets) -> .then((targets) ->
info.then((info) -> info.then((info) ->
if info.metadata? if info.metadata?
......
raf = require('raf')
Promise = require('es6-promise').Promise Promise = require('es6-promise').Promise
Annotator = require('annotator') Annotator = require('annotator')
...@@ -44,10 +45,21 @@ class PDF extends Annotator.Plugin ...@@ -44,10 +45,21 @@ class PDF extends Annotator.Plugin
return {title, link} return {title, link}
onpagerendered: => onpagerendered: (event) =>
unanchored = @annotator.unanchored.splice(0, @annotator.unanchored.length) annotator = @annotator
unanchored = @annotator.unanchored
page = PDFViewerApplication.pdfViewer.pages[event.detail.pageNumber - 1]
waitForTextLayer = ->
unless (page.textLayer.renderingDone)
return new Promise(raf).then(waitForTextLayer)
reanchor = ->
unanchored = unanchored.splice(0, unanchored.length)
for obj in unanchored for obj in unanchored
@annotator.setupAnnotation(obj.annotation) annotator.setupAnnotation(obj.annotation)
waitForTextLayer().then(reanchor)
Annotator.Plugin.PDF = PDF Annotator.Plugin.PDF = PDF
......
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