Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Annotator can now de serialise annotation in XML documents. Fixes #54

Previously the annotator failed to parse the XPaths stored on the
annotations when loaded into an XHTML document served as
application/xhtml+xml. We now check to see if the current document is an
XML one and if it is try to resolve any namespaced elements first by using
a namespace resolver created with document.createNSResolver() then falling
back to a cruder method of defining a custom resolver and appending a
custom namespace to all nodes.

Also added a dev.xhtml file for specifically testing the injection of
annotations into an XHTML document. This can be removed once we have a
working browser test runner.
  • Loading branch information...
commit fbfaa1eb534c7f12f614b7f1d09a9b9a444a4ede 1 parent 8c1e946
@aron aron authored
Showing with 150 additions and 7 deletions.
  1. +81 −0 dev.xhtml
  2. +59 −7 src/range.coffee
  3. +10 −0 test/spec/range_spec.coffee
View
81 dev.xhtml
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>JS annotation test</title>
+
+ <script src="lib/vendor/jquery.js"></script>
+ <script src="lib/vendor/json2.js"></script>
+
+ <script src="lib/extensions.js"></script>
+ <script src="lib/console.js"></script>
+ <script src="lib/class.js"></script>
+ <script src="lib/range.js"></script>
+ <script src="lib/annotator.js"></script>
+ <script src="lib/widget.js"></script>
+ <script src="lib/editor.js"></script>
+ <script src="lib/viewer.js"></script>
+ <script src="lib/notification.js"></script>
+ <script src="lib/plugin/store.js"></script>
+ <script src="lib/plugin/permissions.js"></script>
+ <script src="lib/plugin/auth.js"></script>
+ <script src="lib/plugin/tags.js"></script>
+ <script src="lib/plugin/unsupported.js"></script>
+ <script src="lib/plugin/filter.js"></script>
+
+ <link rel="stylesheet" type="text/css" href="css/annotator.css" />
+ </head>
+
+ <body>
+ <header>
+ <h1>Javascript annotation service test</h1>
+ </header>
+
+ <div id="airlock">
+ <p><strong>Pellentesque habitant morbi tristique</strong> senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. <em>Aenean ultricies mi vitae est.</em> Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, <code>commodo vitae</code>, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. <a href="#">Donec non enim</a> in turpis pulvinar facilisis. Ut felis.</p>
+
+ <h2>Header Level 2</h2>
+
+ <ol>
+ <li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
+ <li>Aliquam tincidunt mauris eu risus.</li>
+ </ol>
+
+ <blockquote><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.</p></blockquote>
+
+ <h3>Header Level 3</h3>
+
+ <ul>
+ <li id="listone">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
+ <li id="listtwo">Aliquam tincidunt mauris eu risus.</li>
+ </ul>
+
+ <pre><code>
+ #header h1 a {
+ display: block;
+ width: 300px;
+ height: 80px;
+ }
+ </code></pre>
+ </div>
+
+ <script>
+ var devAnnotator
+ (function ($) {
+ var elem = document.getElementById('airlock');
+ devAnnotator = new Annotator(elem);
+ devAnnotator.setupAnnotation({
+ "ranges":[{
+ "start":"/blockquote/p",
+ "startOffset":28,
+ "end":"/blockquote/p",
+ "endOffset":55
+ }],
+ "text": "My Quote",
+ "quote":"consectetur adipiscing elit"
+ });
+ }(jQuery));
+ </script>
+ </body>
+</html>
View
66 src/range.coffee
@@ -11,7 +11,7 @@ Range = {}
# Range.sniff(selection.getRangeAt(0))
# # => Returns a BrowserRange instance.
#
-# Returns
+# Returns a Range object or false.
Range.sniff = (r) ->
if r.commonAncestorContainer?
new Range.BrowserRange(r)
@@ -231,7 +231,7 @@ class Range.SerializedRange
# startOffset: The offset to the start of the selection from obj.start.
# end: An xpath to the Element containing the last TextNode
# relative to the root Element.
- # startOffset: The offset to the end of the selection from obj.end.
+ # startOffset: The offset to the end of the selection from obj.end.
#
# Returns an instance of SerializedRange
constructor: (obj) ->
@@ -240,15 +240,67 @@ class Range.SerializedRange
@end = obj.end
@endOffset = obj.endOffset
+ # Finds an Element Node using an XPath relative to the document root.
+ #
+ # If the document is served as application/xhtml+xml it will try and resolve
+ # any namespaces within the XPath.
+ #
+ # xpath - An XPath String to query.
+ #
+ # Examples
+ #
+ # node = this._nodeFromXPath('/html/body/div/p[2]')
+ # if node
+ # # Do something with the node.
+ #
+ # Returns the Node if found otherwise null.
+ _nodeFromXPath: (xpath) ->
+ evaluateXPath = (xp, nsResolver=null) ->
+ document.evaluate(xp, document, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
+
+ if not $.isXMLDoc document.documentElement
+ evaluateXPath xpath
+ else
+ # We're in an XML document, create a namespace resolver function to try
+ # and resolve any namespaces in the current document.
+ # https://developer.mozilla.org/en/DOM/document.createNSResolver
+ customResolver = document.createNSResolver(
+ if document.ownerDocument == null
+ document.documentElement
+ else
+ document.ownerDocument.documentElement
+ )
+ node = evaluateXPath xpath, customResolver
+
+ unless node
+ # If the previous search failed to find a node then we must try to
+ # provide a custom namespace resolver to take into account the default
+ # namespace. We also prefix all node names with a custom xhtml namespace
+ # eg. 'div' => 'xhtml:div'.
+ xpath = (for segment in xpath.split '/'
+ if segment and segment.indexOf(':') == -1
+ segment.replace(/^([a-z]+)/, 'xhtml:$1')
+ else segment
+ ).join('/')
+
+ # Find the default document namespace.
+ namespace = document.lookupNamespaceURI null
+
+ # Try and resolve the namespace, first seeing if it is an xhtml node
+ # otherwise check the head attributes.
+ customResolver = (ns) ->
+ if ns == 'xhtml' then namespace
+ else document.documentElement.getAttribute('xmlns:' + ns)
+
+ node = evaluateXPath xpath, customResolver
+ node
+
# Public: Creates a NormalizedRange.
#
# root - The root Element from which the XPaths were generated.
#
# Returns a NormalizedRange instance.
normalize: (root) ->
- nodeFromXPath = (xpath) ->
- document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
-
parentXPath = $(root).xpath()[0]
startAncestry = @start.split("/")
endAncestry = @end.split("/")
@@ -264,7 +316,7 @@ class Range.SerializedRange
break
cacXPath = parentXPath + common.join("/")
- range.commonAncestorContainer = nodeFromXPath(cacXPath)
+ range.commonAncestorContainer = this._nodeFromXPath(cacXPath)
if not range.commonAncestorContainer
console.error("Error deserializing range: can't find XPath '" + cacXPath + "'. Is this the right document?")
@@ -276,7 +328,7 @@ class Range.SerializedRange
# matches the value of the offset.
for p in ['start', 'end']
length = 0
- for tn in $(nodeFromXPath(parentXPath + this[p])).textNodes()
+ for tn in $(this._nodeFromXPath(parentXPath + this[p])).textNodes()
if (length + tn.nodeValue.length >= this[p + 'Offset'])
range[p + 'Container'] = tn
range[p + 'Offset'] = this[p + 'Offset'] - length
View
10 test/spec/range_spec.coffee
@@ -58,6 +58,16 @@ describe 'Range', ->
expect(obj.endOffset).toEqual(27)
expect(JSON.stringify(obj)).toEqual('{"start":"/p/strong","startOffset":13,"end":"/p/strong","endOffset":27}')
+ describe "_nodeFromXPath", ->
+ it "should parse a standard xpath string", ->
+ node = r._nodeFromXPath "/html/body/p/strong"
+ expect(node).toBe($('strong')[0])
+
+ it "should parse an standard xpath string for an xml document", ->
+ Annotator.$.isXMLDoc = -> true
+ node = r._nodeFromXPath "/html/body/p/strong"
+ expect(node).toBe($('strong')[0])
+
describe "BrowserRange", ->
beforeEach ->
sel = mockSelection(0)
Please sign in to comment.
Something went wrong with that request. Please try again.