Commit 4e247f6d authored by Nick Stenning's avatar Nick Stenning

Merge pull request #1859 from hypothesis/extract-anchoring-2

Move juicy anchoring bits into a plugin of Annotator
parents c0a05f89 cdfbf804
# Abstract anchor class.
class Anchor
constructor: (@annotator, @annotation, @target
@startPage, @endPage,
@quote, @diffHTML, @diffCaseOnly) ->
unless @annotator? then throw "annotator is required!"
unless @annotation? then throw "annotation is required!"
unless @target? then throw "target is required!"
unless @startPage? then "startPage is required!"
unless @endPage? then throw "endPage is required!"
unless @quote? then throw "quote is required!"
@highlight = {}
# Return highlights for the given page
_createHighlight: (page) ->
throw "Function not implemented"
# Create the missing highlights for this anchor
realize: () =>
return if @fullyRealized # If we have everything, go home
# Collect the pages that are already rendered
renderedPages = [@startPage .. @endPage].filter (index) =>
@annotator.domMapper.isPageMapped index
# Collect the pages that are already rendered, but not yet anchored
pagesTodo = renderedPages.filter (index) => not @highlight[index]?
return unless pagesTodo.length # Return if nothing to do
# Create the new highlights
created = for page in pagesTodo
@highlight[page] = @_createHighlight page
# Check if everything is rendered now
@fullyRealized = renderedPages.length is @endPage - @startPage + 1
# Announce the creation of the highlights
@annotator.publish 'highlightsCreated', created
# Remove the highlights for the given set of pages
virtualize: (pageIndex) =>
highlight = @highlight[pageIndex]
return unless highlight? # No highlight for this page
highlight.removeFromDocument()
delete @highlight[pageIndex]
# Mark this anchor as not fully rendered
@fullyRealized = false
# Announce the removal of the highlight
@annotator.publish 'highlightRemoved', highlight
# Virtualize and remove an anchor from all involved pages
remove: ->
# Go over all the pages
for index in [@startPage .. @endPage]
@virtualize index
anchors = @annotator.anchors[index]
# Remove the anchor from the list
i = anchors.indexOf this
anchors[i..i] = []
# Kill the list if it's empty
delete @annotator.anchors[index] unless anchors.length
# This is called when the underlying Annotator has been udpated
annotationUpdated: ->
# Notify the highlights
for index in [@startPage .. @endPage]
@highlight[index]?.annotationUpdated()
......@@ -41,16 +41,6 @@ util =
# Store a reference to the current Annotator object.
_Annotator = this.Annotator
# Fake two-phase / pagination support, used for HTML documents
class DummyDocumentAccess
@applicable: -> true
getPageIndex: -> 0
getPageCount: -> 1
getPageIndexForPos: -> 0
isPageMapped: -> true
scan: ->
class Annotator extends Delegator
# Events to be bound on Annotator#element.
events:
......@@ -104,75 +94,20 @@ class Annotator extends Delegator
constructor: (element, options) ->
super
@plugins = {}
@selectorCreators = []
@anchoringStrategies = []
# Return early if the annotator is not supported.
return this unless Annotator.supported()
this._setupDocumentEvents() unless @options.readOnly
this._setupAnchorEvents()
this._setupWrapper()
this._setupDocumentAccessStrategies()
this._setupViewer()._setupEditor()
this._setupDynamicStyle()
# Perform initial DOM scan, unless told not to.
this._scan() unless @options.noScan
#this._scan() unless @options.noScan
# Create adder
this.adder = $(this.html.adder).appendTo(@wrapper).hide()
# Initializes the available document access strategies
_setupDocumentAccessStrategies: ->
@documentAccessStrategies = [
# Default dummy strategy for simple HTML documents.
# The generic fallback.
name: "Dummy"
mapper: DummyDocumentAccess
]
this
# Initializes the components used for analyzing the document
_chooseAccessPolicy: ->
if @domMapper? then return
# Go over the available strategies
for s in @documentAccessStrategies
# Can we use this strategy for this document?
if s.mapper.applicable()
@documentAccessStrategy = s
@domMapper = new s.mapper()
@anchors = {}
addEventListener "docPageMapped", (evt) =>
@_realizePage evt.pageIndex
addEventListener "docPageUnmapped", (evt) =>
@_virtualizePage evt.pageIndex
s.init?()
return this
# Remove the current document access policy
_removeCurrentAccessPolicy: ->
return unless @domMapper?
list = @documentAccessStrategies
index = list.indexOf @documentAccessStrategy
list.splice(index, 1) unless index is -1
@domMapper.destroy?()
delete @domMapper
# Perform a scan of the DOM. Required for finding anchors.
_scan: ->
# Ensure that we have a document access strategy
this._chooseAccessPolicy()
try
@pendingScan = @domMapper.scan()
catch
@_removeCurrentAccessPolicy()
@_scan()
return
# Wraps the children of @element in a @wrapper div. NOTE: This method will also
# remove any script elements inside @element to prevent them re-executing.
#
......@@ -243,14 +178,6 @@ class Annotator extends Delegator
})
this
# Sets up handlers to anchor-related events
_setupAnchorEvents: ->
# When annotations are updated
@on 'annotationUpdated', (annotation) =>
# Notify the anchors
for anchor in annotation.anchors or []
anchor.annotationUpdated()
# Sets up any dynamically calculated CSS for the Annotator.
#
# Returns itself for chaining.
......@@ -336,37 +263,6 @@ class Annotator extends Delegator
this.publish('beforeAnnotationCreated', [annotation])
annotation
# Do some normalization to get a "canonical" form of a string.
# Used to even out some browser differences.
normalizeString: (string) -> string.replace /\s{2,}/g, " "
# Find the given type of selector from an array of selectors, if it exists.
# If it does not exist, null is returned.
findSelector: (selectors, type) ->
for selector in selectors
if selector.type is type then return selector
null
# Try to find the right anchoring point for a given target
#
# Returns an Anchor object if succeeded, null otherwise
createAnchor: (annotation, target) ->
unless target?
throw new Error "Trying to find anchor for null target!"
error = null
anchor = null
for s in @anchoringStrategies
try
a = s.code.call this, annotation, target
if a
return result: a
catch error
console.log "Strategy '" + s.name + "' has thrown an error.",
error.stack ? error
return error: "No strategies worked."
# Public: Initialises an annotation either from an object representation or
# an annotation created with Annotator#createAnnotation(). It finds the
# selected range and higlights the selection in the DOM, extracts the
......@@ -397,7 +293,7 @@ class Annotator extends Delegator
for t in annotation.target ? []
try
# Create an anchor for this target
result = this.createAnchor annotation, t
result = this.anchoring.createAnchor annotation, t
anchor = result.result
if result.error? instanceof Range.RangeError
this.publish 'rangeNormalizeFail', [annotation, result.error.range, result.error]
......@@ -408,14 +304,6 @@ class Annotator extends Delegator
# Store this anchor for the annotation
annotation.anchors.push anchor
# Store the anchor for all involved pages
for pageIndex in [anchor.startPage .. anchor.endPage]
@anchors[pageIndex] ?= []
@anchors[pageIndex].push anchor
# Realizing the anchor
anchor.realize()
catch exception
console.log "Error in setupAnnotation for", annotation.id,
":", exception.stack ? exception
......@@ -624,19 +512,6 @@ class Annotator extends Delegator
this.startViewerHideTimer()
@mouseIsDown = true
# This is called to create a target from a raw selection,
# using selectors created by the registered selector creators
_getTargetFromSelection: (selection) =>
selectors = []
for c in @selectorCreators
description = c.describe selection
for selector in description
selectors.push selector
# Create the target
source: @getHref()
selector: selectors
# This method is to be called by the mechanisms responsible for
# triggering annotation (and highlight) creation.
#
......@@ -673,6 +548,12 @@ class Annotator extends Delegator
true
# This is called to create a target from a raw selection,
# using selectors created by the registered selector creators
_getTargetFromSelection: (selection) ->
source: @getHref()
selector: @anchoring.getSelectorsFromSelection(selection)
onFailedSelection: (event) ->
@adder.hide()
@selectedTargets = []
......@@ -796,36 +677,6 @@ class Annotator extends Delegator
# Delete highlight elements.
this.deleteAnnotation annotation
# Collect all the highlights (optionally for a given set of annotations)
getHighlights: (annotations) ->
results = []
if annotations?
# Collect only the given set of annotations
for annotation in annotations
for anchor in annotation.anchors
for page, hl of anchor.highlight
results.push hl
else
# Collect from everywhere
for page, anchors of @anchors
$.merge results, (anchor.highlight[page] for anchor in anchors when anchor.highlight[page]?)
results
# Realize anchors on a given pages
_realizePage: (index) ->
# If the page is not mapped, give up
return unless @domMapper.isPageMapped index
# Go over all anchors related to this page
for anchor in @anchors[index] ? []
anchor.realize()
# Virtualize anchors on a given page
_virtualizePage: (index) ->
# Go over all anchors related to this page
for anchor in @anchors[index] ? []
anchor.virtualize index
onAnchorMouseover: (event) ->
# Cancel any pending hiding of the viewer.
this.clearViewerHideTimer()
......@@ -896,7 +747,6 @@ Annotator.Util = Util
Annotator._instances = []
Annotator.Highlight = Highlight
Annotator.Anchor = Anchor
# Bind gettext helper so plugins can use localisation.
Annotator._t = _t
......
......@@ -2,8 +2,9 @@
class Highlight
constructor: (@anchor, @pageIndex) ->
@annotator = @anchor.annotator
@annotation = @anchor.annotation
@anchoring = @anchor.anchoring
@annotator = @anchoring.annotator
# Mark/unmark this hl as temporary (while creating an annotation)
setTemporary: (value) ->
......@@ -80,3 +81,4 @@ class Highlight
# Scroll down to the highlight, with a comfortable margin.
paddedScrollDownTo: -> @paddedScrollTo "down"
......@@ -113,7 +113,7 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
_collectVirtualAnnotations: (startPage, endPage) ->
results = []
for page in [startPage .. endPage]
anchors = @annotator.anchors[page]
anchors = @annotator.anchoring.anchors[page]
if anchors?
$.merge results, (anchor.annotation for anchor in anchors when not anchor.fullyRealized)
results
......@@ -128,7 +128,7 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
dir = if direction is "up" then +1 else -1
{next} = annotations.reduce (acc, ann) ->
{start, next} = acc
anchor = ann.anchors[0]
anchor = ann.anchoring.anchors[0]
if not next? or start.page*dir < anchor.startPage*dir
# This one is obviously better
start:
......@@ -164,7 +164,7 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
startPage = anchor.startPage
# Is this rendered?
if @annotator.domMapper.isPageMapped startPage
if @annotator.anchoring.document.isPageMapped startPage
# If it was rendered, then we only have one result. Go there.
hl = anchor.highlight[startPage]
hl.paddedScrollTo direction
......@@ -175,11 +175,11 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
count: next.length
page: startPage
direction: direction
@annotator.domMapper.setPageIndex startPage
@annotator.anchoring.document.setPageIndex startPage
_update: =>
wrapper = @annotator.wrapper
highlights = @annotator.getHighlights()
highlights = @annotator.anchoring.getHighlights()
defaultView = wrapper[0].ownerDocument.defaultView
# Keep track of buckets of annotations above and below the viewport
......@@ -187,7 +187,7 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
below = []
# Get the page numbers
mapper = @annotator.domMapper
mapper = @annotator.anchoring.document
return unless mapper? # Maybe it's too soon to do this
firstPage = 0
currentPage = mapper.getPageIndex()
......@@ -315,7 +315,7 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
# TODO: This should use event delegation on the container.
.on 'mousemove', (event) =>
bucket = @tabs.index(event.currentTarget)
for hl in @annotator.getHighlights()
for hl in @annotator.anchoring.getHighlights()
if hl.annotation in @buckets[bucket]
hl.setFocused true
else
......@@ -323,7 +323,7 @@ class Annotator.Plugin.BucketBar extends Annotator.Plugin
# Gets rid of them after
.on 'mouseout', =>
for hl in @annotator.getHighlights()
for hl in @annotator.anchoring.getHighlights()
hl.setFocused false
# Does one of a few things when a tab is clicked depending on type
......
......@@ -6,9 +6,11 @@ class Annotator.Plugin.DomTextMapper extends Annotator.Plugin
console.log "Not registering DOM-Text-Mapper."
return
@annotator.documentAccessStrategies.unshift
@anchoring = @annotator.anchoring
@anchoring.documentAccessStrategies.unshift
# Document access strategy for simple HTML documents,
# with enhanced text extraction and mapping features.
name: "DOM-Text-Mapper"
mapper: window.DomTextMapper
init: => @annotator.domMapper.setRootNode @annotator.wrapper[0]
init: => @anchoring.document.setRootNode @annotator.wrapper[0]
# Fake two-phase / pagination support, used for HTML documents
class DummyDocumentAccess
@applicable: -> true
getPageIndex: -> 0
getPageCount: -> 1
getPageIndexForPos: -> 0
isPageMapped: -> true
scan: ->
# Abstract anchor class.
class Anchor
constructor: (@anchoring, @annotation, @target
@startPage, @endPage,
@quote, @diffHTML, @diffCaseOnly) ->
unless @anchoring? then throw "anchoring manager is required!"
unless @annotation? then throw "annotation is required!"
unless @target? then throw "target is required!"
unless @startPage? then "startPage is required!"
unless @endPage? then throw "endPage is required!"
unless @quote? then throw "quote is required!"
@highlight = {}
_getSegment: (page) ->
throw "Function not implemented"
# Create the missing highlights for this anchor
realize: () =>
return if @fullyRealized # If we have everything, go home
# Collect the pages that are already rendered
renderedPages = [@startPage .. @endPage].filter (index) =>
@anchoring.document.isPageMapped index
# Collect the pages that are already rendered, but not yet anchored
pagesTodo = renderedPages.filter (index) => not @highlight[index]?
return unless pagesTodo.length # Return if nothing to do
# Create the new highlights
created = for page in pagesTodo
# TODO: add a layer of abstraction here
# Don't call TextHighlight directly; instead, make a system
# For registering highlight creators, or publish an event, or
# whatever
@highlight[page] = @Annotator.TextHighlight.createFrom @_getSegment(page), this, page
# Check if everything is rendered now
@fullyRealized = renderedPages.length is @endPage - @startPage + 1
# Announce the creation of the highlights
@anchoring.annotator.publish 'highlightsCreated', created
# Remove the highlights for the given set of pages
virtualize: (pageIndex) =>
highlight = @highlight[pageIndex]
return unless highlight? # No highlight for this page
highlight.removeFromDocument()
delete @highlight[pageIndex]
# Mark this anchor as not fully rendered
@fullyRealized = false
# Announce the removal of the highlight
@anchoring.annotator.publish 'highlightRemoved', highlight
# Virtualize and remove an anchor from all involved pages
remove: ->
# Go over all the pages
for index in [@startPage .. @endPage]
@virtualize index
anchors = @anchoring.anchors[index]
# Remove the anchor from the list
i = anchors.indexOf this
anchors[i..i] = []
# Kill the list if it's empty
delete @anchoring.anchors[index] unless anchors.length
# This is called when the underlying Annotator has been udpated
annotationUpdated: ->
# Notify the highlights
for index in [@startPage .. @endPage]
@highlight[index]?.annotationUpdated()
Annotator.Anchor = Anchor
# This plugin contains the enhanced anchoring framework.
class Annotator.Plugin.EnhancedAnchoring extends Annotator.Plugin
constructor: ->
# Initializes the available document access strategies
_setupDocumentAccessStrategies: ->
@documentAccessStrategies = [
# Default dummy strategy for simple HTML documents.
# The generic fallback.
name: "Dummy"
mapper: DummyDocumentAccess
]
this
# Sets up handlers to anchor-related events
_setupAnchorEvents: ->
# When annotations are updated
@annotator.on 'annotationUpdated', (annotation) =>
# Notify the anchors
for anchor in annotation.anchors or []
anchor.annotationUpdated()
# Initializes the components used for analyzing the document
_chooseAccessPolicy: ->
if @document? then return
# Go over the available strategies
for s in @documentAccessStrategies
# Can we use this strategy for this document?
if s.mapper.applicable()
@documentAccessStrategy = s
@document = new s.mapper()
@anchors = {}
addEventListener "docPageMapped", (evt) =>
@_realizePage evt.pageIndex
addEventListener "docPageUnmapped", (evt) =>
@_virtualizePage evt.pageIndex
s.init?()
return this
# Remove the current document access policy
_removeCurrentAccessPolicy: ->
return unless @document?
list = @documentAccessStrategies
index = list.indexOf @documentAccessStrategy
list.splice(index, 1) unless index is -1
@document.destroy?()
delete @document
# Perform a scan of the DOM. Required for finding anchors.
_scan: ->
# Ensure that we have a document access strategy
this._chooseAccessPolicy()
try
@pendingScan = @document.scan()
catch
@_removeCurrentAccessPolicy()
@_scan()
return
# Plugin initialization
pluginInit: ->
@$ = Annotator.$
@selectorCreators = []
@strategies = []
@_setupDocumentAccessStrategies()
this._setupAnchorEvents()
@annotator.anchoring = this
# PUBLIC Try to find the right anchoring point for a given target
#
# Returns an Anchor object if succeeded, null otherwise
createAnchor: (annotation, target) ->
unless target?
throw new Error "Trying to find anchor for null target!"
error = null
anchor = null
for s in @strategies
try
a = s.code.call this, annotation, target
if a
# Store the anchor for all involved pages
for pageIndex in [a.startPage .. a.endPage]
@anchors[pageIndex] ?= []
@anchors[pageIndex].push a
# Realizing the anchor
a.realize()
return result: a
catch error
console.log "Strategy '" + s.name + "' has thrown an error.",
error.stack ? error
return error: "No strategies worked."
# Do some normalization to get a "canonical" form of a string.
# Used to even out some browser differences.
normalizeString: (string) -> string.replace /\s{2,}/g, " "
# Find the given type of selector from an array of selectors, if it exists.
# If it does not exist, null is returned.
findSelector: (selectors, type) ->
for selector in selectors
if selector.type is type then return selector
null
# Realize anchors on a given pages
_realizePage: (index) ->
# If the page is not mapped, give up
return unless @document.isPageMapped index
# Go over all anchors related to this page
for anchor in @anchors[index] ? []
anchor.realize()
# Virtualize anchors on a given page
_virtualizePage: (index) ->
# Go over all anchors related to this page
for anchor in @anchors[index] ? []
anchor.virtualize index
# Collect all the highlights (optionally for a given set of annotations)
getHighlights: (annotations) ->
results = []
if annotations?
# Collect only the given set of annotations
for annotation in annotations
for anchor in annotation.anchors
for page, hl of anchor.highlight
results.push hl
else
# Collect from everywhere
for page, anchors of @anchors
@$.merge results, (anchor.highlight[page] for anchor in anchors when anchor.highlight[page]?)
results
# PUBLIC entry point 1:
# This is called to create a target from a raw selection,
# using selectors created by the registered selector creators
getSelectorsFromSelection: (selection) =>
selectors = []
for c in @selectorCreators
description = c.describe selection
for selector in description
selectors.push selector
selectors
......@@ -3,24 +3,26 @@ class Annotator.Plugin.FuzzyTextAnchors extends Annotator.Plugin
pluginInit: ->
# Do we have the basic text anchors plugin loaded?
unless @annotator.plugins.TextAnchors
console.warn "The FuzzyTextAnchors Annotator plugin requires the TextAnchors plugin. Skipping."
unless @annotator.plugins.TextPosition
console.warn "The FuzzyTextAnchors Annotator plugin requires the TextPosition plugin. Skipping."
return
@Annotator = Annotator
@anchoring = @annotator.anchoring
# Initialize the text matcher library
@textFinder = new DomTextMatcher => @annotator.domMapper.getCorpus()
@textFinder = new DomTextMatcher => @anchoring.document.getCorpus()
# Register our fuzzy strategies
@annotator.anchoringStrategies.push
@anchoring.strategies.push
# Two-phased fuzzy text matching strategy. (Using context and quote.)
# This can handle document structure changes,
# and also content changes.
name: "two-phase fuzzy"
code: this.twoPhaseFuzzyMatching
@annotator.anchoringStrategies.push
@anchoring.strategies.push
# Naive fuzzy text matching strategy. (Using only the quote.)
# This can handle document structure changes,
# and also content changes.
......@@ -28,11 +30,14 @@ class Annotator.Plugin.FuzzyTextAnchors extends Annotator.Plugin
code: this.fuzzyMatching
twoPhaseFuzzyMatching: (annotation, target) =>
document = @anchoring.document
# This won't work without DTM
return unless @annotator.domMapper.getInfoForNode?
return unless document.getInfoForNode?
# Fetch the quote and the context
quoteSelector = @annotator.findSelector target.selector, "TextQuoteSelector"
quoteSelector = @anchoring.findSelector target.selector, "TextQuoteSelector"
prefix = quoteSelector?.prefix
suffix = quoteSelector?.suffix
quote = quoteSelector?.exact
......@@ -41,12 +46,12 @@ class Annotator.Plugin.FuzzyTextAnchors extends Annotator.Plugin
unless (prefix? and suffix?) then return null
# Fetch the expected start and end positions
posSelector = @annotator.findSelector target.selector, "TextPositionSelector"
posSelector = @anchoring.findSelector target.selector, "TextPositionSelector"
expectedStart = posSelector?.start
expectedEnd = posSelector?.end
options =
contextMatchDistance: @annotator.domMapper.getCorpus().length * 2
contextMatchDistance: document.getCorpus().length * 2
contextMatchThreshold: 0.5
patternMatchThreshold: 0.5
flexContext: true
......@@ -65,20 +70,23 @@ class Annotator.Plugin.FuzzyTextAnchors extends Annotator.Plugin
# OK, we have everything
# Create a TextPositionAnchor from this data
new @Annotator.TextPositionAnchor @annotator, annotation, target,
new @Annotator.TextPositionAnchor @anchoring, annotation, target,
match.start, match.end,
(@annotator.domMapper.getPageIndexForPos match.start),
(@annotator.domMapper.getPageIndexForPos match.end),
(document.getPageIndexForPos match.start),
(document.getPageIndexForPos match.end),
match.found,
unless match.exact then match.comparison.diffHTML,
unless match.exact then match.exactExceptCase
fuzzyMatching: (annotation, target) =>
document = @anchoring.document
# This won't work without DTM
return unless @annotator.domMapper.getInfoForNode?
return unless document.getInfoForNode?
# Fetch the quote
quoteSelector = @annotator.findSelector target.selector, "TextQuoteSelector"
quoteSelector = @anchoring.findSelector target.selector, "TextQuoteSelector"
quote = quoteSelector?.exact
# No quote, no joy
......@@ -89,11 +97,11 @@ class Annotator.Plugin.FuzzyTextAnchors extends Annotator.Plugin
return unless quote.length >= 32
# Get a starting position for the search
posSelector = @annotator.findSelector target.selector, "TextPositionSelector"
posSelector = @anchoring.findSelector target.selector, "TextPositionSelector"
expectedStart = posSelector?.start
# Get full document length
len = @annotator.domMapper.getCorpus().length
len = document.getCorpus().length
# If we don't have the position saved, start at the middle of the doc
expectedStart ?= Math.floor(len / 2)
......@@ -116,10 +124,11 @@ class Annotator.Plugin.FuzzyTextAnchors extends Annotator.Plugin
# OK, we have everything
# Create a TextPosutionAnchor from this data
new @Annotator.TextPositionAnchor @annotator, annotation, target,
new @Annotator.TextPositionAnchor @anchoring, annotation, target,
match.start, match.end,
(@annotator.domMapper.getPageIndexForPos match.start),
(@annotator.domMapper.getPageIndexForPos match.end),
(document.getPageIndexForPos match.start),
(document.getPageIndexForPos match.end),
match.found,
unless match.exact then match.comparison.diffHTML,
unless match.exact then match.exactExceptCase
......@@ -222,7 +222,9 @@ class Annotator.Plugin.PDF extends Annotator.Plugin
console.warn "The PDF Annotator plugin requires the DomTextMapper plugin. Skipping."
return
@annotator.documentAccessStrategies.unshift
@anchoring = @annotator.anchoring
@anchoring.documentAccessStrategies.unshift
# Strategy to handle PDF documents rendered by PDF.js
name: "PDF.js"
mapper: PDFTextMapper
......@@ -247,7 +249,7 @@ class Annotator.Plugin.PDF extends Annotator.Plugin
# Get a PDF fingerPrint-based URI
_getFingerPrintURI: ->
fingerprint = @annotator.domMapper.getDocumentFingerprint()
fingerprint = @anchoring.document.getDocumentFingerprint()
# This is an experimental URN,
# as per http://tools.ietf.org/html/rfc3406#section-3.0
......@@ -266,7 +268,7 @@ class Annotator.Plugin.PDF extends Annotator.Plugin
# Try to extract the title; first from metadata, then HTML header
_getTitle: ->
title = @annotator.domMapper.getDocumentInfo().Title?.trim()
title = @anchoring.document.getDocumentInfo().Title?.trim()
if title? and title isnt ""
title
else
......@@ -291,8 +293,8 @@ class Annotator.Plugin.PDF extends Annotator.Plugin
# Public: Get metadata (when the doc is loaded). Returns a promise.
getMetaData: =>
new Promise (resolve, reject) =>
if @annotator.domMapper.waitForInit?
@annotator.domMapper.waitForInit().then =>
if @anchoring.document.waitForInit?
@anchoring.document.waitForInit().then =>
try
resolve @_metadata()
catch error
......
......@@ -2,6 +2,12 @@
# required for annotating text.
class TextHighlight extends Annotator.Highlight
@createFrom: (segment, anchor, page) ->
return null if segment.type isnt "magic range"
new TextHighlight anchor, page, segment.data
# XXX: This is a temporay workaround until the Highlighter extension
# PR will be merged which will restore separation properly
@highlightClass = 'annotator-hl'
......@@ -110,7 +116,7 @@ class TextHighlight extends Annotator.Highlight
removeFromDocument: ->
for hl in @_highlights
# Is this highlight actually the part of the document?
if hl.parentNode? and @annotator.domMapper.isPageMapped @pageIndex
if hl.parentNode? and @anchoring.document.isPageMapped @pageIndex
# We should restore original state
child = hl.childNodes[0]
@$(hl).replaceWith hl.childNodes
......
......@@ -4,11 +4,11 @@ class TextPositionAnchor extends Annotator.Anchor
@Annotator = Annotator
constructor: (annotator, annotation, target,
constructor: (anchoring, annotation, target,
@start, @end, startPage, endPage,
quote, diffHTML, diffCaseOnly) ->
super annotator, annotation, target,
super anchoring, annotation, target,
startPage, endPage,
quote, diffHTML, diffCaseOnly
......@@ -20,10 +20,10 @@ class TextPositionAnchor extends Annotator.Anchor
@Annotator = TextPositionAnchor.Annotator
# This is how we create a highlight out of this kind of anchor
_createHighlight: (page) ->
_getSegment: (page) ->
# First we create the range from the stored stard and end offsets
mappings = @annotator.domMapper.getMappingsForCharRange @start, @end, [page]
mappings = @anchoring.document.getMappingsForCharRange @start, @end, [page]
# Get the wanted range out of the response of DTM
realRange = mappings.sections[page].realRange
......@@ -32,11 +32,10 @@ class TextPositionAnchor extends Annotator.Anchor
browserRange = new @Annotator.Range.BrowserRange realRange
# Get a NormalizedRange
normedRange = browserRange.normalize @annotator.wrapper[0]
# Create the highligh
new @Annotator.TextHighlight this, page, normedRange
normedRange = browserRange.normalize @anchoring.annotator.wrapper[0]
type: "magic range"
data: normedRange
# Annotator plugin for text position-based anchoring
class Annotator.Plugin.TextPosition extends Annotator.Plugin
......@@ -45,12 +44,14 @@ class Annotator.Plugin.TextPosition extends Annotator.Plugin
@Annotator = Annotator
@anchoring = @annotator.anchoring
# Register the creator for text quote selectors
@annotator.selectorCreators.push
@anchoring.selectorCreators.push
name: "TextPositionSelector"
describe: @_getTextPositionSelector
@annotator.anchoringStrategies.push
@anchoring.strategies.push
# Position-based strategy. (The quote is verified.)
# This can handle document structure changes,
# but not the content changes.
......@@ -65,11 +66,13 @@ class Annotator.Plugin.TextPosition extends Annotator.Plugin
# We only care about "text range" selections.
return [] unless selection.type is "text range"
document = @anchoring.document
# We need dom-text-mapper - style functionality
return [] unless @annotator.domMapper.getStartPosForNode?
return [] unless document.getStartPosForNode?
startOffset = @annotator.domMapper.getStartPosForNode selection.range.start
endOffset = @annotator.domMapper.getEndPosForNode selection.range.end
startOffset = document.getStartPosForNode selection.range.start
endOffset = document.getEndPosForNode selection.range.end
if startOffset? and endOffset?
[
......@@ -95,7 +98,7 @@ class Annotator.Plugin.TextPosition extends Annotator.Plugin
createFromPositionSelector: (annotation, target) =>
# We need the TextPositionSelector
selector = @annotator.findSelector target.selector, "TextPositionSelector"
selector = @anchoring.findSelector target.selector, "TextPositionSelector"
return unless selector?
unless selector.start?
......@@ -106,13 +109,15 @@ class Annotator.Plugin.TextPosition extends Annotator.Plugin
console.log "Warning: 'end' field is missing from TextPositionSelector. Skipping."
return null
corpus = @annotator.domMapper.getCorpus?()
document = @anchoring.document
corpus = document.getCorpus?()
# This won't work without d-t-m
return null unless corpus
content = corpus[selector.start ... selector.end].trim()
currentQuote = @annotator.normalizeString content
savedQuote = @annotator.getQuoteForTarget? target
currentQuote = @anchoring.normalizeString content
savedQuote = @anchoring.getQuoteForTarget? target
if savedQuote? and currentQuote isnt savedQuote
# We have a saved quote, let's compare it to current content
#console.log "Could not apply position selector" +
......@@ -123,8 +128,9 @@ class Annotator.Plugin.TextPosition extends Annotator.Plugin
return null
# Create a TextPositionAnchor from this data
new TextPositionAnchor @annotator, annotation, target,
new TextPositionAnchor @anchoring, annotation, target,
selector.start, selector.end,
(@annotator.domMapper.getPageIndexForPos selector.start),
(@annotator.domMapper.getPageIndexForPos selector.end),
(document.getPageIndexForPos selector.start),
(document.getPageIndexForPos selector.end),
currentQuote
......@@ -7,16 +7,18 @@ class Annotator.Plugin.TextQuote extends Annotator.Plugin
# Plugin initialization
pluginInit: ->
@anchoring = @annotator.anchoring
# Register the creator for text quote selectors
@annotator.selectorCreators.push
@anchoring.selectorCreators.push
name: "TextQuoteSelector"
describe: @_getTextQuoteSelector
# Register function to get quote from this selector
@annotator.getQuoteForTarget = (target) =>
selector = @annotator.findSelector target.selector, "TextQuoteSelector"
@anchoring.getQuoteForTarget = (target) =>
selector = @anchoring.findSelector target.selector, "TextQuoteSelector"
if selector?
@annotator.normalizeString selector.exact
@anchoring.normalizeString selector.exact
else
null
......@@ -24,6 +26,8 @@ class Annotator.Plugin.TextQuote extends Annotator.Plugin
_getTextQuoteSelector: (selection) =>
return [] unless selection.type is "text range"
document = @anchoring.document
unless selection.range?
throw new Error "Called getTextQuoteSelector() with null range!"
......@@ -34,15 +38,15 @@ class Annotator.Plugin.TextQuote extends Annotator.Plugin
unless rangeEnd?
throw new Error "Called getTextQuoteSelector() on a range with no valid end."
if @annotator.domMapper.getStartPosForNode?
if document.getStartPosForNode?
# Calculate the quote and context using DTM
startOffset = @annotator.domMapper.getStartPosForNode rangeStart
endOffset = @annotator.domMapper.getEndPosForNode rangeEnd
startOffset = document.getStartPosForNode rangeStart
endOffset = document.getEndPosForNode rangeEnd
if startOffset? and endOffset?
quote = @annotator.domMapper.getCorpus()[startOffset .. endOffset-1].trim()
[prefix, suffix] = @annotator.domMapper.getContextForCharRange startOffset, endOffset
quote = document.getCorpus()[startOffset .. endOffset-1].trim()
[prefix, suffix] = document.getContextForCharRange startOffset, endOffset
[
type: "TextQuoteSelector"
......@@ -61,3 +65,4 @@ class Annotator.Plugin.TextQuote extends Annotator.Plugin
type: "TextQuoteSelector"
exact: selection.range.text().trim()
]
......@@ -30,11 +30,9 @@ class TextRangeAnchor extends Annotator.Anchor
@Annotator = TextRangeAnchor.Annotator
# This is how we create a highlight out of this kind of anchor
_createHighlight: ->
# Create the highligh
new @Annotator.TextHighlight this, 0, @range
_getSegment: ->
type: "magic range"
data: @range
# Annotator plugin for creating, and anchoring based on text range
# selectors
......@@ -44,13 +42,15 @@ class Annotator.Plugin.TextRange extends Annotator.Plugin
@Annotator = Annotator
@anchoring = @annotator.anchoring
# Register the creator for range selectors
@annotator.selectorCreators.push
@anchoring.selectorCreators.push
name: "RangeSelector"
describe: @_getRangeSelector
# Register our anchoring strategies
@annotator.anchoringStrategies.push
@anchoring.strategies.push
# Simple strategy based on DOM Range
name: "range"
code: @createFromRangeSelector
......@@ -74,7 +74,10 @@ class Annotator.Plugin.TextRange extends Annotator.Plugin
# Create and anchor using the saved Range selector.
# The quote is verified.
createFromRangeSelector: (annotation, target) =>
selector = @annotator.findSelector target.selector, "RangeSelector"
document = @anchoring.document
selector = @anchoring.findSelector target.selector, "RangeSelector"
unless selector? then return null
# Try to apply the saved XPath
......@@ -85,24 +88,24 @@ class Annotator.Plugin.TextRange extends Annotator.Plugin
return null
# Get the text of this range
if @annotator.domMapper.getInfoForNode?
if document.getInfoForNode?
# Determine the current content of the given range using DTM
startInfo = @annotator.domMapper.getInfoForNode normedRange.start
startInfo = document.getInfoForNode normedRange.start
return null unless startInfo # Don't fret if page is not mapped
startOffset = startInfo.start
endInfo = @annotator.domMapper.getInfoForNode normedRange.end
endInfo = document.getInfoForNode normedRange.end
return null unless endInfo # Don't fret if page is not mapped
endOffset = endInfo.end
rawQuote = @annotator.domMapper.getCorpus()[startOffset .. endOffset-1].trim()
rawQuote = document.getCorpus()[startOffset .. endOffset-1].trim()
else
# Determine the current content of the given range directly
rawQuote = normedRange.text().trim()
currentQuote = @annotator.normalizeString rawQuote
currentQuote = @anchoring.normalizeString rawQuote
# Look up the saved quote
savedQuote = @annotator.getQuoteForTarget? target
savedQuote = @anchoring.getQuoteForTarget? target
if savedQuote? and currentQuote isnt savedQuote
#console.log "Could not apply XPath selector to current document, " +
# "because the quote has changed. (Saved quote is '#{savedQuote}'." +
......@@ -113,12 +116,13 @@ class Annotator.Plugin.TextRange extends Annotator.Plugin
# Create a TextPositionAnchor from the start and end offsets
# of this range
# (to be used with dom-text-mapper)
new @Annotator.TextPositionAnchor @annotator, annotation, target,
new @Annotator.TextPositionAnchor @anchoring, annotation, target,
startInfo.start, endInfo.end,
(startInfo.pageIndex ? 0), (endInfo.pageIndex ? 0),
currentQuote
else
# Create a TextRangeAnchor from this range
# (to be used whithout dom-text-mapper)
new TextRangeAnchor @annotator, annotation, target,
new TextRangeAnchor @anchoring, annotation, target,
normedRange, currentQuote
# This plugin implements the UI code for creating text annotations
class Annotator.Plugin.TextAnchors extends Annotator.Plugin
class Annotator.Plugin.TextSelection extends Annotator.Plugin
# Plugin initialization
pluginInit: ->
# We need text highlights
unless @annotator.plugins.TextHighlights
throw new Error "The TextAnchors Annotator plugin requires the TextHighlights plugin."
throw new Error "The TextSelection Annotator plugin requires the TextHighlights plugin."
@Annotator = Annotator
@$ = Annotator.$
......@@ -114,3 +113,4 @@ class Annotator.Plugin.TextAnchors extends Annotator.Plugin
@annotator.onFailedSelection event
# Strategies used for creating anchors from saved data
......@@ -16,8 +16,9 @@ class Annotator.Guest extends Annotator
# Plugin configuration
options:
TextHighlights: {}
EnhancedAnchoring: {}
DomTextMapper: {}
TextAnchors: {}
TextSelection: {}
TextRange: {}
TextPosition: {}
TextQuote: {}
......@@ -76,7 +77,7 @@ class Annotator.Guest extends Annotator
unless config.dontScan
# Scan the document text with the DOM Text libraries
this._scan()
this.anchoring._scan()
# Watch for newly rendered highlights, and update positions in sidebar
this.subscribe "highlightsCreated", (highlights) =>
......@@ -136,7 +137,7 @@ class Annotator.Guest extends Annotator
.bind('onEditorSubmit', this.onEditorSubmit)
.bind('focusAnnotations', (ctx, tags=[]) =>
for hl in @getHighlights()
for hl in @anchoring.getHighlights()
if hl.annotation.$$tag in tags
hl.setFocused true
else
......@@ -144,7 +145,7 @@ class Annotator.Guest extends Annotator
)
.bind('scrollToAnnotation', (ctx, tag) =>
for hl in @getHighlights()
for hl in @anchoring.getHighlights()
if hl.annotation.$$tag is tag
hl.scrollTo()
return
......
......@@ -40,7 +40,7 @@ class Annotator.Host extends Annotator.Guest
# Host frame dictates the toolbar options.
this.on 'panelReady', =>
this.setTool('comment')
this._scan() # Scan the document
this.anchoring._scan() # Scan the document
this.setVisibleHighlights(!!options.showHighlights)
if @plugins.BucketBar?
......
......@@ -38,6 +38,8 @@ module.exports = function(config) {
'h/static/scripts/annotator/plugin/bridge.js',
'h/static/scripts/annotator/plugin/bucket-bar.js',
'h/static/scripts/annotator/plugin/threading.js',
'h/static/scripts/vendor/dom_text_mapper.js',
'h/static/scripts/annotator/annotator.anchoring.js',
'h/static/scripts/app.js',
'h/static/scripts/account.js',
'h/static/scripts/helpers.js',
......
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