Skip to content

Commit fbfaa1e

Browse files
committed
Annotator can now de serialise annotation in XML documents. Fixes openannotation#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.
1 parent 8c1e946 commit fbfaa1e

File tree

3 files changed

+150
-7
lines changed

3 files changed

+150
-7
lines changed

dev.xhtml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
3+
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
4+
<html xmlns="http://www.w3.org/1999/xhtml">
5+
<head>
6+
<title>JS annotation test</title>
7+
8+
<script src="lib/vendor/jquery.js"></script>
9+
<script src="lib/vendor/json2.js"></script>
10+
11+
<script src="lib/extensions.js"></script>
12+
<script src="lib/console.js"></script>
13+
<script src="lib/class.js"></script>
14+
<script src="lib/range.js"></script>
15+
<script src="lib/annotator.js"></script>
16+
<script src="lib/widget.js"></script>
17+
<script src="lib/editor.js"></script>
18+
<script src="lib/viewer.js"></script>
19+
<script src="lib/notification.js"></script>
20+
<script src="lib/plugin/store.js"></script>
21+
<script src="lib/plugin/permissions.js"></script>
22+
<script src="lib/plugin/auth.js"></script>
23+
<script src="lib/plugin/tags.js"></script>
24+
<script src="lib/plugin/unsupported.js"></script>
25+
<script src="lib/plugin/filter.js"></script>
26+
27+
<link rel="stylesheet" type="text/css" href="css/annotator.css" />
28+
</head>
29+
30+
<body>
31+
<header>
32+
<h1>Javascript annotation service test</h1>
33+
</header>
34+
35+
<div id="airlock">
36+
<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>
37+
38+
<h2>Header Level 2</h2>
39+
40+
<ol>
41+
<li>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
42+
<li>Aliquam tincidunt mauris eu risus.</li>
43+
</ol>
44+
45+
<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>
46+
47+
<h3>Header Level 3</h3>
48+
49+
<ul>
50+
<li id="listone">Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</li>
51+
<li id="listtwo">Aliquam tincidunt mauris eu risus.</li>
52+
</ul>
53+
54+
<pre><code>
55+
#header h1 a {
56+
display: block;
57+
width: 300px;
58+
height: 80px;
59+
}
60+
</code></pre>
61+
</div>
62+
63+
<script>
64+
var devAnnotator
65+
(function ($) {
66+
var elem = document.getElementById('airlock');
67+
devAnnotator = new Annotator(elem);
68+
devAnnotator.setupAnnotation({
69+
"ranges":[{
70+
"start":"/blockquote/p",
71+
"startOffset":28,
72+
"end":"/blockquote/p",
73+
"endOffset":55
74+
}],
75+
"text": "My Quote",
76+
"quote":"consectetur adipiscing elit"
77+
});
78+
}(jQuery));
79+
</script>
80+
</body>
81+
</html>

src/range.coffee

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Range = {}
1111
# Range.sniff(selection.getRangeAt(0))
1212
# # => Returns a BrowserRange instance.
1313
#
14-
# Returns
14+
# Returns a Range object or false.
1515
Range.sniff = (r) ->
1616
if r.commonAncestorContainer?
1717
new Range.BrowserRange(r)
@@ -231,7 +231,7 @@ class Range.SerializedRange
231231
# startOffset: The offset to the start of the selection from obj.start.
232232
# end: An xpath to the Element containing the last TextNode
233233
# relative to the root Element.
234-
# startOffset: The offset to the end of the selection from obj.end.
234+
# startOffset: The offset to the end of the selection from obj.end.
235235
#
236236
# Returns an instance of SerializedRange
237237
constructor: (obj) ->
@@ -240,15 +240,67 @@ class Range.SerializedRange
240240
@end = obj.end
241241
@endOffset = obj.endOffset
242242

243+
# Finds an Element Node using an XPath relative to the document root.
244+
#
245+
# If the document is served as application/xhtml+xml it will try and resolve
246+
# any namespaces within the XPath.
247+
#
248+
# xpath - An XPath String to query.
249+
#
250+
# Examples
251+
#
252+
# node = this._nodeFromXPath('/html/body/div/p[2]')
253+
# if node
254+
# # Do something with the node.
255+
#
256+
# Returns the Node if found otherwise null.
257+
_nodeFromXPath: (xpath) ->
258+
evaluateXPath = (xp, nsResolver=null) ->
259+
document.evaluate(xp, document, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
260+
261+
if not $.isXMLDoc document.documentElement
262+
evaluateXPath xpath
263+
else
264+
# We're in an XML document, create a namespace resolver function to try
265+
# and resolve any namespaces in the current document.
266+
# https://developer.mozilla.org/en/DOM/document.createNSResolver
267+
customResolver = document.createNSResolver(
268+
if document.ownerDocument == null
269+
document.documentElement
270+
else
271+
document.ownerDocument.documentElement
272+
)
273+
node = evaluateXPath xpath, customResolver
274+
275+
unless node
276+
# If the previous search failed to find a node then we must try to
277+
# provide a custom namespace resolver to take into account the default
278+
# namespace. We also prefix all node names with a custom xhtml namespace
279+
# eg. 'div' => 'xhtml:div'.
280+
xpath = (for segment in xpath.split '/'
281+
if segment and segment.indexOf(':') == -1
282+
segment.replace(/^([a-z]+)/, 'xhtml:$1')
283+
else segment
284+
).join('/')
285+
286+
# Find the default document namespace.
287+
namespace = document.lookupNamespaceURI null
288+
289+
# Try and resolve the namespace, first seeing if it is an xhtml node
290+
# otherwise check the head attributes.
291+
customResolver = (ns) ->
292+
if ns == 'xhtml' then namespace
293+
else document.documentElement.getAttribute('xmlns:' + ns)
294+
295+
node = evaluateXPath xpath, customResolver
296+
node
297+
243298
# Public: Creates a NormalizedRange.
244299
#
245300
# root - The root Element from which the XPaths were generated.
246301
#
247302
# Returns a NormalizedRange instance.
248303
normalize: (root) ->
249-
nodeFromXPath = (xpath) ->
250-
document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
251-
252304
parentXPath = $(root).xpath()[0]
253305
startAncestry = @start.split("/")
254306
endAncestry = @end.split("/")
@@ -264,7 +316,7 @@ class Range.SerializedRange
264316
break
265317

266318
cacXPath = parentXPath + common.join("/")
267-
range.commonAncestorContainer = nodeFromXPath(cacXPath)
319+
range.commonAncestorContainer = this._nodeFromXPath(cacXPath)
268320

269321
if not range.commonAncestorContainer
270322
console.error("Error deserializing range: can't find XPath '" + cacXPath + "'. Is this the right document?")
@@ -276,7 +328,7 @@ class Range.SerializedRange
276328
# matches the value of the offset.
277329
for p in ['start', 'end']
278330
length = 0
279-
for tn in $(nodeFromXPath(parentXPath + this[p])).textNodes()
331+
for tn in $(this._nodeFromXPath(parentXPath + this[p])).textNodes()
280332
if (length + tn.nodeValue.length >= this[p + 'Offset'])
281333
range[p + 'Container'] = tn
282334
range[p + 'Offset'] = this[p + 'Offset'] - length

test/spec/range_spec.coffee

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ describe 'Range', ->
5858
expect(obj.endOffset).toEqual(27)
5959
expect(JSON.stringify(obj)).toEqual('{"start":"/p/strong","startOffset":13,"end":"/p/strong","endOffset":27}')
6060

61+
describe "_nodeFromXPath", ->
62+
it "should parse a standard xpath string", ->
63+
node = r._nodeFromXPath "/html/body/p/strong"
64+
expect(node).toBe($('strong')[0])
65+
66+
it "should parse an standard xpath string for an xml document", ->
67+
Annotator.$.isXMLDoc = -> true
68+
node = r._nodeFromXPath "/html/body/p/strong"
69+
expect(node).toBe($('strong')[0])
70+
6171
describe "BrowserRange", ->
6272
beforeEach ->
6373
sel = mockSelection(0)

0 commit comments

Comments
 (0)