hello
├── .ackrc
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── calorimetre.js
├── index.html
├── karma.conf-ci.js
├── karma.conf.js
├── package-lock.json
├── package.json
├── src
└── index.js
└── test
├── _utils.js
├── child-offsets.js
├── create-hash.js
├── create-key.js
├── find-key.js
├── get-scope.js
├── normalize-offset.js
├── normalize-text.js
├── offsets.js
├── range-from-text.js
├── range-offsets.js
└── serialize-range.js
/.ackrc:
--------------------------------------------------------------------------------
1 | --ignore-dir=static
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # Compiled binary addons (http://nodejs.org/api/addons.html)
20 | build/Release
21 |
22 | # Dependency directory
23 | # Commenting this out is preferred by some people, see
24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
25 | node_modules
26 |
27 | # Users Environment Variables
28 | .lock-wscript
29 |
30 | bundle.*
31 | scratch/
32 | sauce.json
33 | dist
34 | web-verse.min.js
35 | web-verse.js
36 | coverage/
37 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 | scratch
4 | .ackrc
5 | .babelrc
6 | .eslintrc
7 | .gitignore
8 | circle.yml
9 | .travis.yml
10 | index.html
11 | sauce.json
12 | coverage/
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Web Verse
2 |
3 | [](https://github.com/prettier/prettier)
4 |
5 | Web Verse enables deep-linking into HTML text, without requiring specific coöperation from the
6 | content (such as adding `id` attributes everywhere). It can be used to generate locator keys for
7 | content inside of a page that are reasonably resilient to markup modifications as well as to edits
8 | to the text itself. As such, it can be used to build an annotation system for text that is likely
9 | to be edited over time. Obviously it is not
10 | [altogether unstoppable][no-power-in-the-verse] but it offers good enough resilience to
11 | be used in production systems.
12 |
13 | It was inspired by [Emphasis][nyt] by Michael Donohoe and [Ted Nelson parallel markup](https://www.xml.com/pub/a/w3j/s3.nelson.html), but leverages the [Range interface][ranges]
14 | and [selection object][selections].
15 |
16 | We do not provide direct support for instance for mapping a URL's hash containing a Web Verse key
17 | into a specific paragraph or the such. Rather, the expectation is that one can build one's own
18 | preferred annotation system (or more generally deep, resilient linking system) very easily on top
19 | of Web Verse.
20 |
21 | We fingerprint a block-level element (e.g. a paragraph) by:
22 |
23 | 1. Normalising the text to abstract away from markup and formatting differences.
24 |
25 | 2. Breaking the text into sentences. We attempt to be smart about handling full-stops. We'll ignore
26 | things like "Dr. Who" and a number of similar cases. It is generally enough to avoid getting
27 | single word nonsense for our sentences.
28 | 3. Taking the first and last sentences. It's OK if the first and last sentences are the same, the
29 | key is still meaningful.
30 | 4. Taking the first character from the first three words of each sentence. Words are defined as
31 | tokens composed of a run of non-white-space characters.
32 |
33 | These fingerprints [have been shown][jsconf] to provide reasonable uniqueness for reasonably-sized
34 | documents. Since it's deterministic yet not dependent on all the content, this method is tolerant to
35 | smaller changes in the content. Furthermore, finding keys can take edit-distance into account, which
36 | enables additional resilience to change.
37 |
38 | Regions of text more specific than a block-level element can be referenced from within a block using
39 | character ranges. For instance, in the following paragraph:
40 |
41 |
42 | `I` `a`m `a` paragraph with 2 **sentences**.
43 | `I` `a`m `t`he second sentence.
44 |
45 |
46 | We can refer to the word `sentences` in the first sentence by using the range, `24-33`. Altogether
47 | with the paragraph's fingerprint, this gives us an address selecting just that word of
48 | `IaaIat:25-33`. (Note that the text offsets are zero-based, and apply to normalised text.)
49 |
50 | ## Installation
51 |
52 | `npm install web-verse`
53 |
54 | ### In the browser
55 |
56 | This is primarily a client-side library (~7k minizipped), just include the `web-verse.min.js` script
57 | that comes with the distribution.
58 |
59 | ### In Node
60 |
61 | Web Verse works with Node, but you have to bring your own DOM. Currently, the best option is likely
62 | to be `jsdom`, but it has limitations due to it not supporting `Range`s.
63 |
64 | The following subset of methods works with Node and `jsdom`:
65 |
66 | * `createKey()`
67 | * `createHash()`
68 | * `getScope()`, but only with a `node` argument
69 | * `serializeNode()`
70 | * `findKey()`
71 | * `getChildOffsets()`
72 | * `normalizeText()`
73 | * `normalizeOffset()`
74 | * `denormalizeOffset()`
75 |
76 | These should normally be more than enough to carry out the sort of operations that you are likely to
77 | want to do on the server (as opposed to, say, getting the user's selection and producing a link from
78 | it).
79 |
80 | ## API
81 |
82 | When loaded in a Web context, Web Verse exposes itself as a global `WebVerse` object, on which the
83 | following methods are available.
84 |
85 | ### `key = WebVerse.createKey($el)`
86 |
87 | Given an element, returns a 6-char key that summarises it for the purposes of deep, resilient
88 | linking.
89 |
90 | ### `result = WebVerse.findKey(targetKey, candidateKeys)`
91 |
92 | Given a key that is being searched for, and a list of candidate keys (for instance, all the keys for
93 | block elements in the document), this will return the best match it can find.
94 |
95 | The returned object has fields for `index` (the index in `candidateKeys` that best matched), `value`
96 | (the value that actually matched, which may differ slightly from the `targetKey`), and `lev` (an
97 | indication of the Levenshtein edit distance of the match). If no match was found, all of those
98 | fields will be `undefined`.
99 |
100 | The match works by first attempting an exact match, then by choosing the candidate with the smallest
101 | edit distance. No edit distance can be greater than or equal to 3.
102 |
103 | ### `hash = WebVerse.createHash($el)`
104 |
105 | Given an element, it will return a hash for it that is invariant to numerous markup changes inside
106 | of it, looking only at its normalised text content. Such a hash can also be used to generate
107 | resilient identifiers.
108 |
109 | ### `details = WebVerse.serializeRange(range, $el)`
110 |
111 | Given a range and optionally a scoping element (which defaults to `getScope(range)`), it will return
112 | the details one needs in order to create a resilient pointer to that range. The returned object
113 | contains:
114 |
115 | * `$scope`: The scoping element (which was used for key and hash generation).
116 | * `hash`: The hash of the scoping element, can be used as an ID that is resilient to markup and
117 | white space changes but not to text edits.
118 | * `key`: The key for the scoping element; can *also* be used as an ID. It is resilient to markup and
119 | white space changes, as well as to a certain amount of text editing; but it is less unique than
120 | the `hash`.
121 | * `startOffset`, `endOffset`: The normalised offsets into the text for that range.
122 |
123 | So if you were to wish to use the key+offsets fingerpint that is discussed in this README's
124 | introduction in order to obtain a resilient pointer into what a given range captures, you would:
125 |
126 | ```js
127 | var details = WebVerse.serializeRange(range);
128 | var fingerprint = details. key + ':' + details.startOffset + '-' + details.endOffset;
129 | ```
130 |
131 | ### `details = WebVerse.serializeSelection()`
132 |
133 | Returns the same details as `serializeRange()` but for the current selection. If there is no
134 | selection (or if it is collapsed) it returns `undefined`.
135 |
136 | ### `details = WebVerse.serializeNode($node, $el)`
137 |
138 | The same as `serializeRange()` but instead of a `Range` it uses a node, taking its own text content
139 | as the offsets into the given scope. If no scoping `$el` is given, it will use `getScope($node)`.
140 |
141 | ### `range = WebVerse.rangeFromOffsets($scope, startOffset, endOffset)`
142 |
143 | Given a scope and normalised start/end offsets (that you may have stored in a fingerprint), returns
144 | a `range` object suitable to use directly on the DOM (i.e. applying to the raw content).
145 |
146 | If you start with a fingerprint such as the `IaaIat:24-33` example you would use the `IaaIat` part
147 | to find the `$scope` (typically with `findKey()`) and then this method using the scope and the
148 | offsets. It returns a `Range` that you could wrap to highlight, etc.
149 |
150 | ### `ranges = WebVerse.getRangesFromText($el, searchText)`
151 |
152 | Given an element to scope the search in, and a string, it will find all instances of that string
153 | (in a normalised, white-space-invariant manner) inside the textual content of that element, and
154 | return an array of `Range` elements pointing into the matches.
155 |
156 | This can be used to find an highlight a specific string. Or, for instance, if a user is creating a
157 | link around a given string in a text this can offer the option of linking all other occurrences of
158 | the same string.
159 |
160 | Since it returns `Range`s, it can be easily used with [`Range.surroundContents`][surround-contents].
161 |
162 | ### `WebVerse.citeable`
163 |
164 | An array of element `tagName`s (i.e. uppercase) that are considered acceptable scopes (block-level
165 | elements). You can modify this to alter Web Verse's behaviour.
166 |
167 | ### `text = WebVerse.normalizeText(text)`
168 |
169 | Given a string, returns a version normalised according to Web Verse's internal normalising
170 | algorithm. This is essentially `str.trim().replace(/\s+/g, ' ')` but with its behaviour made
171 | resistant to browser vagaries.
172 |
173 | ### `offset = WebVerse.normalizeOffset(rawOffset, rawText)`
174 |
175 | Web Verse hides away a lot of the complexity involved in dealing with normalised text internally but
176 | having to manipulate a DOM that has raw, unnormalised text content (obviously, without changing the
177 | DOM).
178 |
179 | This method returns the offset in the normalised text equivalent to the given raw offset into the
180 | unadulterated text. So calling it with `4, ' a b'` (which has the offset right before the `b`) will
181 | return `2`, since the normalised text is `a b`.
182 |
183 | This may seem cryptic, and in many ways it is. You should only need this if you are trying to
184 | manipulate the text in the same manner as Web Verse does, for instance to extend its functionality.
185 |
186 | ### `offset = WebVerse.denormalizeOffset(normalisedOffset, rawText)`
187 |
188 | Does the reverse of the previous one: given a normalised offset and the *raw* text, it will return
189 | the matching raw offset.
190 |
191 | ### `$element = WebVerse.getScope(range|$node)`
192 |
193 | Given a range or a `$node`, will return the closest enclosing element that may scope it (i.e. a
194 | block-level element from `citeable`). This can the range's `commonAncestorContainer` or any of its
195 | parents. If it goes up the tree without finding a valid candidate, it will return `undefined`.
196 |
197 | ### `details = WebVerse.getOffsets(range, $el)`
198 |
199 | Given a range and an element scope, return an object with `startOffset` and `endOffset` that are the
200 | offsets into the normalised text equivalent to that range, for that scope. Mostly of internal use.
201 |
202 | ### `details = WebVerse.getChildOffsets($parent, $child)`
203 |
204 | Same as `getOffsets()` but uses a `$child` text node (or a `$child` element containing text) as
205 | determining the offsets inside a `$parent` element. Returns `startOffset` and `endOffset` fields
206 | being the offsets normalised to the content of the `$parent`.
207 |
208 |
209 | ## Development
210 |
211 | The best thing when developing is to `npm run watch`. This will build both Node and browser versions
212 | continuously. It is also a good idea to `npm run test-local`, which will keep the Karma tests
213 | running (just in Chrome, so as not to be too invasive) whenever you make changes.
214 |
215 | ---
216 | [jsconf]: http://2014.jsconf.eu/speakers/michael-donohoe-deeplink-to-anything-on-the-web.html
217 | [nyt]: https://github.com/NYTimes/Emphasis
218 | [ranges]: https://developer.mozilla.org/en-US/docs/Web/API/Range
219 | [selections]: https://developer.mozilla.org/en-US/docs/Web/API/Selection
220 | [surround-contents]: https://developer.mozilla.org/en-US/docs/Web/API/Range/surroundContents
221 | [no-power-in-the-verse]: https://youtu.be/uRdbEY_YfV4?t=24s
222 |
--------------------------------------------------------------------------------
/calorimetre.js:
--------------------------------------------------------------------------------
1 |
2 | // a quick and dirty tool to know what is taking up space in a bundle
3 | // this should be made generic
4 | var exec = require('child_process').exec
5 | , chalk = require('chalk')
6 | , entryFile = 'src/index.js'
7 | ;
8 | exec(`browserify --deps ${entryFile}`, function (err, stdout, stderr) {
9 | var files = {}
10 | , incBy = {}
11 | , deps = JSON.parse(stdout)
12 | ;
13 | deps.forEach(function (dep) {
14 | files[dep.file] = {
15 | size: dep.source.replace(/^\/\/# sourceMap.*/m, '').length
16 | , entry: !!dep.entry
17 | };
18 | for (var k in dep.deps) {
19 | var includes = dep.deps[k];
20 | if (!incBy[includes]) incBy[includes] = {};
21 | incBy[includes][dep.file] = true;
22 | }
23 | });
24 | var keys = Object.keys(files)
25 | .sort(function (a, b) {
26 | if (files[a].entry) return -1;
27 | if (files[b].entry) return 1;
28 | if (files[a].size > files[b].size) return -1;
29 | if (files[a].size < files[b].size) return 1;
30 | return 0;
31 | })
32 | ;
33 | keys.forEach(function (k) {
34 | console.log(chalk.bold.red(k + (files[k].entry ? ' [entry]' : '')));
35 | console.log(chalk.green(' chars: ') + files[k].size);
36 | console.log(chalk.green(' included by:'));
37 | for (var inc in incBy[k]) console.log(' ' + inc);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
40 | One sentence only. 41 |
42 | 43 |44 | Dr. N.I.H police. 45 |
46 | 47 |48 | One non terminated sentence 49 |
50 | 51 |52 | 53 | a 54 | b 55 | 56 | c 57 |
58 | 59 |60 | Lorem ipsum dolor sit amet, 61 | consectetur adipiscing 62 | elit. Curabitur porta nulla nec 63 | volutpat iaculis. Vivamus sed tellus sed tortor ultrices 64 | posuere. Praesent porta, felis at congue tincidunt, eros dui 65 | hendrerit massa, vitae tincidunt enim diam a diam. Vivamus 66 | ultrices nibh dolor, et accumsan nisl tincidunt et. Mauris 67 | ornare tellus quis enim gravida, id tempor augue porta. Nunc 68 | ultrices felis rhoncus vulputate egestas. Etiam nisi magna, 69 | bibendum ut facilisis ornare, suscipit a risus. Maecenas 70 | semper lacinia arcu, eu convallis turpis. Etiam pharetra 71 | elementum gravida. In vitae aliquet odio, non ultricies 72 | risus. Quisque ut urna sit amet velit pharetra vestibulum et 73 | sed tellus. 74 |
75 | 76 |
77 | Duis bibendum nulla dictum tempor euismod. Fusce sed dui in
78 | magna gravida fermentum in sed felis. Nulla eu leo molestie,
79 | luctus massa consequat, aliquet lorem. Vivamus nec euismod
80 |
81 | nulla. Maecenas pulvinar semper pharetra. Aenean vitae dolor
82 | ornare, congue nunc eu, lobortis arcu. In ac magna lacinia,
83 | ultrices quam ultricies, volutpat augue.
84 |
a
', 7 | parent: 'p', 8 | child: 'span', 9 | offsets: [0, 1] 10 | }, 11 | { 12 | desc: 'should work with prefix text', 13 | html: 'aha abc
', 14 | parent: 'p', 15 | child: 'span', 16 | offsets: [4, 7] 17 | }, 18 | { 19 | desc: 'should work on second child', 20 | html: 'aha abc hmm x
', 21 | parent: 'p', 22 | child: 'strong', 23 | offsets: [12, 14] 24 | }, 25 | { 26 | desc: 'should work when first text node is not a direct child of the child', 27 | html: 'hello
world
' + c.text + '
'); 38 | var $p = document.getElementsByTagName('p')[0] 39 | , key = WebVerse.createKey($p) 40 | ; 41 | assert.equal(key, c.key, 'created key matches'); 42 | }); 43 | }); 44 | }); 45 | })(); 46 | -------------------------------------------------------------------------------- /test/find-key.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var cases = [ 3 | { 4 | desc: 'should find a direct match', 5 | target: 'abcdef', 6 | candidates: ['qwerty', 'asdfgh', 'abcdef'], 7 | resIndex: 2, 8 | resValue: 'abcdef', 9 | resLev: 0 10 | }, 11 | { 12 | desc: 'should find no match', 13 | target: 'abcdef', 14 | candidates: ['qwerty', 'asdfgh'], 15 | resIndex: undefined, 16 | resValue: undefined, 17 | resLev: undefined 18 | }, 19 | { 20 | desc: 'should find no match: target=3, cands=6', 21 | target: 'abc', 22 | candidates: ['qwerty', 'asdfgh', 'abcdef'], 23 | resIndex: undefined, 24 | resValue: undefined, 25 | resLev: undefined 26 | }, 27 | { 28 | desc: 'should find with: target=6, cands=3+one good', 29 | target: 'abcdef', 30 | candidates: ['qwe', 'asd', 'abcdef'], 31 | resIndex: 2, 32 | resValue: 'abcdef', 33 | resLev: 0 34 | }, 35 | { 36 | desc: 'should find no match: target=6, cands=3', 37 | target: 'abcdef', 38 | candidates: ['qwe', 'asd', 'abc'], 39 | resIndex: undefined, 40 | resValue: undefined, 41 | resLev: undefined 42 | }, 43 | { 44 | desc: 'should find match: target=2, cands=2', 45 | target: 'ab', 46 | candidates: ['ab', 'qwe', 'asd'], 47 | resIndex: 0, 48 | resValue: 'ab', 49 | resLev: 0 50 | }, 51 | { 52 | desc: 'should find match: target=1, cands=1', 53 | target: 'a', 54 | candidates: ['ab', 'qwe', 'a'], 55 | resIndex: 2, 56 | resValue: 'a', 57 | resLev: 0 58 | }, 59 | { 60 | desc: 'should find match: target=0, cands=0', 61 | target: '', 62 | candidates: ['ab', '', 'a'], 63 | resIndex: 1, 64 | resValue: '', 65 | resLev: 0 66 | }, 67 | { 68 | desc: 'should find with lev=1, sizes=6', 69 | target: 'abcdef', 70 | candidates: ['abcdeg', '', 'a'], 71 | resIndex: 0, 72 | resValue: 'abcdeg', 73 | resLev: 1 74 | }, 75 | { 76 | desc: 'should find with lev=2, sizes=6', 77 | target: 'abcdef', 78 | candidates: ['axxdeg', 'abcdfg', 'a'], 79 | resIndex: 1, 80 | resValue: 'abcdfg', 81 | resLev: 2 82 | }, 83 | { 84 | desc: 'should find with lev=2, sizes=5/6', 85 | target: 'abcde', 86 | candidates: ['abcdef', 'abcdfg', 'a'], 87 | resIndex: 0, 88 | resValue: 'abcdef', 89 | resLev: 2 90 | }, 91 | { 92 | desc: 'should find with lev=2, sizes=6/5', 93 | target: 'abcdef', 94 | candidates: ['abcde', 'abcdfg', 'a'], 95 | resIndex: 0, 96 | resValue: 'abcde', 97 | resLev: 2 98 | }, 99 | { 100 | desc: 'should find with lev=2, permutation', 101 | target: 'abcdef', 102 | candidates: ['abcdfe', 'abcdfg', 'a'], 103 | resIndex: 0, 104 | resValue: 'abcdfe', 105 | resLev: 2 106 | }, 107 | ]; 108 | describe('WebVerse findKey', function () { 109 | cases.forEach(function (c) { 110 | it(c.desc, function () { 111 | var res = WebVerse.findKey(c.target, c.candidates); 112 | assert.equal(res.index, c.resIndex, 'indices match'); 113 | assert.equal(res.value, c.resValue, 'values match'); 114 | assert.equal(res.lev, c.resLev, 'lev distances match'); 115 | }); 116 | }); 117 | }); 118 | })(); 119 | -------------------------------------------------------------------------------- /test/get-scope.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | function el (ln) { 4 | return document.getElementsByTagName(ln)[0]; 5 | } 6 | 7 | var cases = [ 8 | { 9 | desc: 'should bubble up from the text node', 10 | html: 'a
', 11 | node: function () { return el('p').firstChild; }, 12 | offsets: [0, 1], 13 | check: function ($node) { return $node.id; }, 14 | result: 'x' 15 | }, 16 | { 17 | desc: 'should bubble up from the text node, deeply', 18 | html: 'ab
', 19 | node: function () { return el('span').firstChild; }, 20 | offsets: [0, 1], 21 | check: function ($node) { return $node.id; }, 22 | result: 'x' 23 | }, 24 | { 25 | desc: 'should bubble up from the text node, deeper', 26 | html: 'abbherebb
', 27 | node: function () { return el('strong').firstChild; }, 28 | offsets: [0, 1], 29 | check: function ($node) { return $node.id; }, 30 | result: 'x' 31 | }, 32 | { 33 | desc: 'should stick to the given element', 34 | html: 'ab
', 35 | node: function () { return el('p'); }, 36 | offsets: [0, 1], 37 | check: function ($node) { return $node.id; }, 38 | result: 'x' 39 | }, 40 | { 41 | desc: 'should abort at root ', 42 | html: 'hello', 43 | node: function () { return el('title'); }, 44 | offsets: [0, 0], 45 | check: function ($node) { return typeof $node; }, 46 | result: 'undefined' 47 | }, 48 | ]; 49 | 50 | describe('WebVerse getScope', function () { 51 | cases.forEach(function (c) { 52 | it(c.desc, function () { 53 | htmlContent(c.html); 54 | var $node = c.node() 55 | , range = document.createRange() 56 | ; 57 | range.setStart($node, c.offsets[0]); 58 | range.setEnd($node, c.offsets[1]); 59 | var $scope = WebVerse.getScope(range); 60 | assert.equal(c.check($scope), c.result, 'check on range produces ' + c.result); 61 | $scope = WebVerse.getScope($node); 62 | assert.equal(c.check($scope), c.result, 'check on node produces ' + c.result); 63 | }); 64 | }); 65 | }); 66 | })(); 67 | -------------------------------------------------------------------------------- /test/normalize-offset.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | var cases = [ 4 | { 5 | desc: 'should handle a trivial string', 6 | text: 'a', 7 | raw: 0, 8 | norm: 0 9 | }, 10 | { 11 | desc: 'should handle a trivial string, inside', 12 | text: 'a', 13 | raw: 1, 14 | norm: 1 15 | }, 16 | { 17 | desc: 'should handle a little bit of space', 18 | text: ' a', 19 | raw: 1, 20 | norm: 0 21 | }, 22 | { 23 | desc: 'should handle trimming', 24 | text: ' \n\t\r\uFEFFa', 25 | raw: 8, 26 | norm: 0 27 | }, 28 | { 29 | desc: 'should handle normalising', 30 | text: ' \n\t\r\uFEFFa\n\t\r b', 31 | raw: 13, 32 | norm: 2 33 | }, 34 | { 35 | desc: 'should handle end', 36 | text: ' a b ', 37 | raw: 4, 38 | norm: 3 39 | }, 40 | { 41 | desc: 'should handle between', 42 | text: ' a b ', 43 | raw: 4, 44 | norm: 2 45 | }, 46 | { 47 | desc: 'should handle case from ranges', 48 | text: 'a b a b', 49 | raw: 5, 50 | norm: 4 51 | }, 52 | ]; 53 | 54 | describe('WebVerse normalizeOffset', function () { 55 | cases.forEach(function (c) { 56 | it(c.desc + ' [normalising]', function () { 57 | var norm = WebVerse.normalizeOffset(c.raw, c.text); 58 | assert.equal(norm, c.norm, 'offset normalised'); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('WebVerse denormalizeOffset', function () { 64 | cases.forEach(function (c) { 65 | it(c.desc + ' [denormalising]', function () { 66 | var raw = WebVerse.denormalizeOffset(c.norm, c.text); 67 | assert.equal(raw, c.raw, 'offset denormalised'); 68 | }); 69 | }); 70 | }); 71 | })(); 72 | -------------------------------------------------------------------------------- /test/normalize-text.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | var cases = [ 4 | { 5 | desc: 'should trim pre and post', 6 | in: ' a ', 7 | out: 'a' 8 | }, 9 | { 10 | desc: 'should normalise inside', 11 | in: 'a b', 12 | out: 'a b' 13 | }, 14 | { 15 | desc: 'should normalise repeatedly', 16 | in: 'a b c', 17 | out: 'a b c' 18 | }, 19 | { 20 | desc: 'hairy', 21 | in: '\uFEFF\n \ta\r \nb\xA0\t\r', 22 | out: 'a b' 23 | }, 24 | ]; 25 | 26 | describe('WebVerse normalizeText', function () { 27 | cases.forEach(function (c) { 28 | it(c.desc, function () { 29 | var out = WebVerse.normalizeText(c.in); 30 | assert.equal(out, c.out, 'strings match'); 31 | }); 32 | }); 33 | }); 34 | })(); 35 | -------------------------------------------------------------------------------- /test/offsets.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | var cases = [ 4 | { 5 | it: 'should handle simple offset into an h1', 6 | html: '', 23 | ' ', 24 | ' a', 25 | ' b', 26 | ' ', 27 | ' c', 28 | '
'].join('\n'), 29 | start: { el: 'span', elNum: 2, pos: 0 }, 30 | end: { el: 'span', elNum: 3, pos: 1 }, 31 | scope: { el: 'p' }, 32 | result: { startOffset: 2, endOffset: 5 } 33 | } 34 | , { 35 | it: 'should handle offsets over empty elements', 36 | html: [ '',
37 | ' Bibendum ',
38 | '',
39 | 'nulla.',
40 | '
a
', 7 | text: 'a', 8 | ranges: [ 9 | { startID: 'x', start: 0, endID: 'x', end: 1 } 10 | ] 11 | }, 12 | { 13 | desc: 'should handle a bit of space', 14 | html: '\n a\n\t
', 15 | text: 'a', 16 | ranges: [ 17 | { startID: 'x', start: 4, endID: 'x', end: 5 } 18 | ] 19 | }, 20 | { 21 | desc: 'should handle search string with space', 22 | html: 'a b
', 23 | text: 'a b', 24 | ranges: [ 25 | { startID: 'x', start: 1, endID: 'x', end: 4 } 26 | ] 27 | }, 28 | { 29 | desc: 'should handle search string with space', 30 | html: '\n a b\na\nb a\t b
', 31 | text: 'a b', 32 | ranges: [ 33 | { startID: 'x', start: 4, endID: 'x', end: 7 }, 34 | { startID: 'x', start: 8, endID: 'x', end: 11 }, 35 | { startID: 'x', start: 13, endID: 'x', end: 19 } 36 | ] 37 | }, 38 | ]; 39 | 40 | describe('WebVerse getRangesFromText', function () { 41 | cases.forEach(function (c) { 42 | it(c.desc, function () { 43 | htmlContent(c.html); 44 | var main = document.getElementsByTagName('main')[0]; 45 | var ranges = WebVerse.getRangesFromText(main, c.text); 46 | assert.equal(ranges.length, c.ranges.length, 'found ' + c.ranges.length + ' instances of ' + c.text); 47 | c.ranges.forEach(function (r, idx) { 48 | assert.equal(ranges[idx].startContainer.parentNode.id, r.startID, 'range ' + idx + ' starts in id=' + r.startID); 49 | assert.equal(ranges[idx].endContainer.parentNode.id, r.endID, 'range ' + idx + ' ends in id=' + r.endID); 50 | assert.equal(ranges[idx].startOffset, r.start, 'range ' + idx + ' starts at ' + r.start); 51 | assert.equal(ranges[idx].endOffset, r.end, 'range ' + idx + ' ends at ' + r.end); 52 | }); 53 | }); 54 | }); 55 | }); 56 | })(); 57 | -------------------------------------------------------------------------------- /test/range-offsets.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | var cases = [ 4 | { 5 | desc: 'should work on the simplest string', 6 | html: 'a
', 7 | offsets: [0, 1], 8 | range: { startID: 'x', start: 0, endID: 'x', end: 1 } 9 | }, 10 | { 11 | desc: 'should handle a bit of space', 12 | html: '\n a\n\t
', 13 | offsets: [0, 1], 14 | range: { startID: 'x', start: 4, endID: 'x', end: 5 } 15 | }, 16 | { 17 | desc: 'should handle inner space', 18 | html: '\n a\n\tb
', 19 | offsets: [0, 3], 20 | range: { startID: 'x', start: 4, endID: 'x', end: 8 } 21 | }, 22 | ]; 23 | 24 | describe('WebVerse rangeFromOffsets', function () { 25 | cases.forEach(function (c) { 26 | it(c.desc, function () { 27 | htmlContent(c.html); 28 | var $main = document.getElementsByTagName('main')[0]; 29 | var range = WebVerse.rangeFromOffsets($main, c.offsets[0], c.offsets[1]) 30 | , r = c.range 31 | ; 32 | assert.equal(range.startContainer.parentNode.id, r.startID, 'range starts in id=' + r.startID); 33 | assert.equal(range.endContainer.parentNode.id, r.endID, 'range ends in id=' + r.endID); 34 | assert.equal(range.startOffset, r.start, 'range starts at ' + r.start); 35 | assert.equal(range.endOffset, r.end, 'range ends at ' + r.end); 36 | }); 37 | }); 38 | }); 39 | 40 | describe('WebVerse getOffsets', function () { 41 | cases.forEach(function (c) { 42 | it(c.desc, function () { 43 | htmlContent(c.html); 44 | var $main = document.getElementsByTagName('main')[0]; 45 | document.normalize; 46 | var range = document.createRange() 47 | , r = c.range; 48 | range.setStart(document.getElementById(r.startID).firstChild, r.start); 49 | range.setEnd(document.getElementById(r.endID).firstChild, r.end); 50 | var offsets = WebVerse.getOffsets(range, $main); 51 | assert.equal(offsets.startOffset, c.offsets[0], 'start offset is ' + c.offsets[0]); 52 | assert.equal(offsets.endOffset, c.offsets[1], 'end offset is ' + c.offsets[1]); 53 | }); 54 | }); 55 | }); 56 | })(); 57 | -------------------------------------------------------------------------------- /test/serialize-range.js: -------------------------------------------------------------------------------- 1 | 2 | (function () { 3 | describe('WebVerse serializeRange', function () { 4 | it('should return nothing if no scope can be found', function () { 5 | var $tit = document.getElementsByTagName('title')[0] 6 | , range = document.createRange() 7 | ; 8 | range.setStart($tit, 0); 9 | range.setEnd($tit, 0); 10 | var res = WebVerse.serializeRange(range); 11 | assert.equal(res, undefined, 'returned undefined for range'); 12 | res = WebVerse.serializeNode($tit); 13 | assert.equal(res, undefined, 'returned undefined for node'); 14 | }); 15 | 16 | it('should default the scope', function () { 17 | htmlContent('b
'); 18 | var $span = document.getElementsByTagName('span')[0] 19 | , range = document.createRange() 20 | ; 21 | range.setStart($span, 0); 22 | range.setEnd($span, 0); 23 | var res = WebVerse.serializeRange(range); 24 | assert.equal(res.$scope.id, 'x', 'returned scope with id=x for range'); 25 | res = WebVerse.serializeNode($span); 26 | assert.equal(res.$scope.id, 'x', 'returned scope with id=x for node'); 27 | }); 28 | 29 | it('should return proper details', function () { 30 | htmlContent('\n\thello\nworld \n\f\v
'); 31 | var $p = document.getElementsByTagName('p')[0] 32 | , $main = document.getElementsByTagName('main')[0] 33 | , range = document.createRange() 34 | ; 35 | range.setStart($p.firstChild, 3); 36 | range.setEnd($p.childNodes.item(3).firstChild, 5); 37 | var res = WebVerse.serializeRange(range, $main); 38 | assert.equal(res.$scope, $main, 'used the given scope for range'); 39 | assert.equal(res.hash, hashedHW, 'hashed the content properly for range'); 40 | assert.equal(res.key, 'hwhw', 'generates the correct key for range'); 41 | assert.equal(res.startOffset, 0, 'normalised the start offset for range'); 42 | assert.equal(res.endOffset, 11, 'normalised the end offset for range'); 43 | res = WebVerse.serializeNode($p.childNodes.item(1), $main); 44 | assert.equal(res.$scope, $main, 'used the given scope for node'); 45 | assert.equal(res.hash, hashedHW, 'hashed the content properly for node'); 46 | assert.equal(res.key, 'hwhw', 'generates the correct key for node'); 47 | assert.equal(res.startOffset, 0, 'normalised the start offset for node'); 48 | assert.equal(res.endOffset, 5, 'normalised the end offset for node'); 49 | }); 50 | }); 51 | describe('WebVerse serializeSelection', function () { 52 | it('should ignore collapsed selections', function () { 53 | var res = WebVerse.serializeSelection(); 54 | assert.equal(res, undefined, 'returned undefined'); 55 | }); 56 | it('should serialise a selection', function () { 57 | htmlContent('\n\thello\nworld \n\f\v
'); 58 | var $p = document.getElementsByTagName('p')[0] 59 | , range = document.createRange() 60 | ; 61 | range.setStart($p.firstChild, 3); 62 | range.setEnd($p.childNodes.item(3).firstChild, 5); 63 | var sel = window.getSelection(); 64 | sel.addRange(range); 65 | var res = WebVerse.serializeSelection(); 66 | assert.equal(res.$scope, $p, 'defaulted the scope'); 67 | assert.equal(res.hash, hashedHW, 'hashed the content properly'); 68 | assert.equal(res.key, 'hwhw', 'generates the correct key'); 69 | assert.equal(res.startOffset, 0, 'normalised the start offset'); 70 | assert.equal(res.endOffset, 11, 'normalised the end offset'); 71 | }); 72 | }); 73 | })(); 74 | --------------------------------------------------------------------------------