├── .gitattributes ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── hast-annotations-match ├── add-parent-props.js ├── add-props-to-node.js ├── css-selector.js ├── full-html-schema.json ├── generate-full-html-schema.js ├── get-node.js ├── index.js ├── process-positions.js ├── process-quotations.js ├── range-selector.js ├── render-templates.js └── simple-xpath-selector.js ├── index.js ├── logger.js ├── package-lock.json ├── package.json ├── tap-snapshots ├── tests-index.js-TAP.test.js └── tests │ └── index.js.test.cjs └── tests ├── fixtures ├── annotations-template.json ├── css-single.annotations.json ├── css-single.input.html ├── css-single.output.html ├── fragment-multibody.annotations.json ├── fragment-multibody.input.html ├── fragment-multibody.output.html ├── poetry-linebreaks.annotations.json ├── poetry-linebreaks.input.html ├── poetry-linebreaks.output.html ├── range.annotations.json ├── range.input.html ├── range.output.html ├── refinedby.annotations.json ├── refinedby.input.html ├── refinedby.output.html ├── template.html ├── text-position-values.reference.txt ├── text-position.annotations.json ├── text-position.input.html ├── text-position.output.html ├── text-position.string.reference.txt ├── text-quote.annotations.json ├── text-quote.input.html ├── text-quote.output.html ├── xpath-single.annotations.json ├── xpath-single.input.html └── xpath-single.output.html └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm run build --if-present 28 | - run: npm test 29 | - uses: codecov/codecov-action@v1 30 | with: 31 | fail_ci_if_error: false # optional (default = false) 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `rehype-annotate` 2 | 3 | ## Version 1.0 and ESM support 4 | 5 | With version 1.0+ this module is [ESM only](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c): Node 12+ is needed to use it and it must be imported instead of required. 6 | 7 | ## Introduction 8 | 9 | This [`rehype`](https://github.com/rehypejs/rehype) plugin matches [W3C-style annotations](https://www.w3.org/TR/annotation-model/) to their targets in the parsed HTML file. It wraps text range selections in `mark` elements and adds attributes and hooks to matched locations that can be used in other processors or browser scripts to implement further behaviours. 10 | 11 | Note: this modifies the original tree and in some cases can add class attributes. Make sure to sanitise the tree afterwards. 12 | 13 | The script _does not_ embed annotation-provided styles, although that is on the roadmap. We do assign the values of the `styleClass` property to the annotated nodes so the client can provide their own stylesheet. 14 | 15 | ## License 16 | 17 | Apache 2.0 18 | 19 | ## Install 20 | 21 | We haven't yet published this package on `npm` but you can install it directly from the GitHub repository. 22 | 23 | [`npm`](https://docs.npmjs.com/cli/install): 24 | 25 | ```sh 26 | npm install RebusFoundation/rehype-annotate 27 | ``` 28 | 29 | ## Use 30 | 31 | `rehype-annotate` should be used as a `rehype` or `unified` plugin to match annotations to a `hast` syntax tree. 32 | 33 | ```js 34 | const vfile = require("to-vfile"); 35 | const unified = require("unified"); 36 | const annotate = require("rehype-annotate"); 37 | const parse = require("rehype-parse"); 38 | const stringify = require("rehype-stringify"); 39 | const report = require("vfile-reporter"); 40 | const glob = require("glob"); 41 | const path = require("path"); 42 | 43 | async function process(file, options) { 44 | return unified() 45 | .use(parse) 46 | .use(annotate, options) 47 | .use(stringify) 48 | .process(await vfile.read(file)); 49 | } 50 | 51 | const options = { 52 | // Should be an array of W3C Web Annotations 53 | annotations: require("./path/to/annotations/json"), 54 | // the base url for the original html 55 | url: "https://syndicated.example.com/annotated.html", 56 | // the canonical url for the html 57 | canonical: "https://example.org/annotated.html", 58 | }; 59 | 60 | process("path/to/example/htmlfile.html", options) 61 | .then((file) => { 62 | console.log(report(file)); 63 | console.log(String(file)); 64 | }) 65 | .catch((err) => console.error(err)); 66 | ``` 67 | 68 | The above code will print whatever issues are found out to the console, followed by the processed HTML. 69 | 70 | The `file.data.annotations` property will contain the annotations that have been matched to the HTML, in the order that the appear in the HTML file itself. 71 | 72 | ## API 73 | 74 | ### `processor.use(annotate[, options])` 75 | 76 | Configure `processor` to modify the [**hast**][hast] syntax tree to match annotations to their target locations in the HTML. 77 | 78 | The following attributes are added when an element node is matched by a selector: 79 | 80 | - `data-annotation-id`: the id of the matched annotation 81 | - `data-annotation-motivation`: space-separated list of the motivations from the annotation [`motivation`](https://www.w3.org/TR/annotation-model/#motivation-and-purpose) property. 82 | - `data-annotation-purpose` space-separated list of the purposes from the annotation _body's_ [`purpose`](https://www.w3.org/TR/annotation-model/#motivation-and-purpose) property. 83 | - `class` the value of the annotation's [`styleClass`](https://www.w3.org/TR/annotation-model/#styles) property is added when present. 84 | 85 | #### Example (single node match): 86 | 87 | If the source HTML is as follows: 88 | 89 | ```html 90 |
j'adore !
", 120 | "format": "text/html", 121 | "language": "fr", 122 | "purpose": "describing" 123 | } 124 | ], 125 | "target": { 126 | "source": "https://example.com/tests/fixtures/fragment-multibody.input.html", 127 | "styleClass": "Bookmarked", 128 | "selector": { 129 | "type": "FragmentSelector", 130 | "value": "test-id" 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | Then the result should be (provided the `url` or `canonical` options match the `source`): 137 | 138 | ```html 139 |156 | Resilient Garulf key quest abandon knives lifted niceties tonight disappeared 157 | strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes 158 | Shadowfax nearly lesser south deceive hates 22nd missing others! 159 |
160 | ``` 161 | 162 | And `rehype-annotate` is run with the following annotation: 163 | 164 | ```json 165 | { 166 | "id": "http://example.com/annotations1", 167 | "type": "Annotation", 168 | "motivation": "highlighting", 169 | "created": "2015-10-13T13:00:00Z", 170 | "body": [ 171 | { 172 | "type": "TextualBody", 173 | "value": "j'adore !
", 174 | "format": "text/html", 175 | "language": "fr", 176 | "purpose": "commenting" 177 | } 178 | ], 179 | "target": { 180 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 181 | "styleClass": "Bookmarked", 182 | "selector": { 183 | "type": "TextQuoteSelector", 184 | "exact": "Resilient Garulf key quest abandon knives" 185 | } 186 | } 187 | } 188 | ``` 189 | 190 | Then the result should be (provided the `url` or `canonical` options match the `source`): 191 | 192 | ```html 193 |194 | Resilient Garulf key quest abandon knives 201 | lifted niceties tonight disappeared strongest plates. Farthing ginger large. 202 | Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd 203 | missing others! 204 |
205 | ``` 206 | 207 | #### `options` 208 | 209 | ##### `options.annotations` 210 | 211 | An array of annotations that conform to the [W3C Web Annotations Data Model](https://www.w3.org/TR/annotation-model/). See note below on selector support. 212 | 213 | ##### `options.canonical` and `options.url` 214 | 215 | The annotation is only matched to the html source if the `annotation.target.source` property matches either `canonical` or `url`. 216 | 217 | ## Selector Support 218 | 219 | - `CssSelector`: limited to the selectors supported by [`hast-util-select`](https://github.com/syntax-tree/hast-util-select#support) 220 | - `XPathSelector`: because `rehype`/`hast` doesn't come with built-in `xpath` support, these selectors only work when they are very simple. E.g. `/html/body/p[1]` 221 | - `FragmentSelector`: supports only HTML fragment ids. 222 | - `RangeSelector`: supported when both `startSelector` and `endSelector` resolve to element nodes. 223 | - `TextQuoteSelector`: implementation is loosely based on the excellent [`dom-anchor-text-quote`](https://github.com/tilgovi/dom-anchor-text-quote) by Randall Leeds. 224 | - `TextPositionSelector` 225 | 226 | For performance reasons text quote and text position selectors that overlap each other in the document are not supported. 227 | 228 | ### `refinedBy` 229 | 230 | You can use the `refinedBy` property on a selector that resolves to a single element node (`CssSelector`, `XPathSelector`, `FragmentSelector`) to create a new scope or root for a another selector, including the `TextPositionSelector` or the `TextQuoteSelector`. 231 | -------------------------------------------------------------------------------- /hast-annotations-match/add-parent-props.js: -------------------------------------------------------------------------------- 1 | import { find, svg } from "property-information"; 2 | import pixelWidth from "string-pixel-width"; 3 | import toString from "hast-util-to-string"; 4 | 5 | /* 6 | ## Props 7 | * `data-annotation-id` 8 | * `data-selector-index`: index of mark in total number of marks for this selector 9 | * `data-annotation-motivation`: annotations motivations 10 | * `class` : styleClass 11 | * `data-annotation-purpose`: body purposes 12 | */ 13 | const props = [ 14 | "x", 15 | "y", 16 | "width", 17 | "height", 18 | "textLength", 19 | "font-size", 20 | "data-annotation-x", 21 | "data-annotation-y", 22 | "data-annotation-width", 23 | "data-annotation-height", 24 | "data-annotation-offset", 25 | "data-annotation-highlight", 26 | "data-annotation-transform", 27 | "transform", 28 | ]; 29 | const attributes = {}; 30 | for (const prop of props) { 31 | attributes[prop] = find(svg, prop).property; 32 | } 33 | 34 | export function addParentProps(svg, node, parent) { 35 | if (!svg) return; 36 | const fontSize = Number.parseFloat( 37 | parent.properties[attributes["font-size"]] || 16 38 | ); 39 | const parentPixelWidth = pixelWidth(toString(parent), { 40 | size: fontSize, 41 | font: "helvetica", 42 | }); 43 | const nodePixelWidth = pixelWidth(toString(node), { 44 | size: fontSize, 45 | font: "helvetica", 46 | }); 47 | // const offset = parentLength - nodeLength; 48 | const width = Number.parseFloat( 49 | parent.properties[attributes.textLength] || parentPixelWidth 50 | ); 51 | const nodeWidth = (nodePixelWidth / parentPixelWidth) * width; 52 | const offsetWidth = width - nodeWidth; 53 | const height = fontSize + 40; 54 | const x = Number.parseFloat(parent.properties[attributes.x] || 0); 55 | const y = 56 | Number.parseFloat(parent.properties[attributes.y] || 0) - 20 - fontSize; 57 | node.properties[attributes["data-annotation-x"]] = String( 58 | x + offsetWidth - 20 59 | ); 60 | node.properties[attributes["data-annotation-y"]] = String(y); 61 | node.properties[attributes["data-annotation-width"]] = String(nodeWidth + 40); 62 | node.properties[attributes["data-annotation-height"]] = String(height); 63 | node.properties[attributes["data-annotation-transform"]] = 64 | node.properties[attributes.transform]; 65 | } 66 | -------------------------------------------------------------------------------- /hast-annotations-match/add-props-to-node.js: -------------------------------------------------------------------------------- 1 | import { find, html } from "property-information"; 2 | 3 | /* 4 | ## Props 5 | * `data-annotation-id` 6 | * `data-selector-index`: index of mark in total number of marks for this selector 7 | * `data-annotation-motivation`: annotations motivations 8 | * `class` : styleClass 9 | * `data-annotation-purpose`: body purposes 10 | */ 11 | const props = [ 12 | "data-annotation-id", 13 | "data-controller", 14 | "data-annotation-motivation", 15 | "class", 16 | "data-annotation-purpose", 17 | "data-annotation-type", 18 | "data-target", 19 | ]; 20 | const attributes = {}; 21 | for (const prop of props) { 22 | attributes[prop] = find(html, prop).property; 23 | } 24 | 25 | export function addPropsToNode(node, annotation) { 26 | // console.log("does this get called?: ", node, annotation); 27 | const { target } = annotation; 28 | const { body = [] } = annotation; 29 | let purposes = body.map((item) => item.purpose); 30 | purposes = [].concat(...purposes); 31 | node.properties[attributes["data-annotation-id"]] = annotation.id; 32 | node.properties[attributes["data-annotation-motivation"]] = [] 33 | .concat(annotation.motivation) 34 | .join(" ,"); 35 | if (target.styleClass) { 36 | const classes = node.properties[attributes.class] || []; 37 | node.properties[attributes.class] = classes 38 | .concat(target.styleClass) 39 | .filter((item) => item); 40 | } 41 | if (purposes.length !== 0) { 42 | node.properties[attributes["data-annotation-purpose"]] = purposes; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /hast-annotations-match/css-selector.js: -------------------------------------------------------------------------------- 1 | import { select } from "hast-util-select"; 2 | import { addPropsToNode } from "./add-props-to-node.js"; 3 | 4 | /** 5 | * 6 | * @param {{tree: Object, value: string, annotation: Object, addProps: undefined | boolean}} param0 - selector options 7 | */ 8 | export function nodeSelector({ tree, value, annotation, addProps = true }) { 9 | const node = select(value, tree); 10 | if (node && addProps) { 11 | addPropsToNode(node, annotation); 12 | } 13 | return node; 14 | } 15 | -------------------------------------------------------------------------------- /hast-annotations-match/full-html-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "strip": ["script", "object", "applet", "foreignObject"], 3 | "clobberPrefix": "user-content-", 4 | "clobber": ["name", "id"], 5 | "ancestors": { 6 | "li": ["ol", "ul"], 7 | "tbody": ["table"], 8 | "tfoot": ["table"], 9 | "thead": ["table"], 10 | "td": ["table"], 11 | "th": ["table"], 12 | "tr": ["table"] 13 | }, 14 | "protocols": { 15 | "href": ["http", "https", "mailto", "xmpp", "irc", "ircs"], 16 | "cite": ["http", "https"], 17 | "src": ["http", "https"], 18 | "longDesc": ["http", "https"] 19 | }, 20 | "tagNames": [ 21 | "a", 22 | "abbr", 23 | "acronym", 24 | "address", 25 | "area", 26 | "article", 27 | "aside", 28 | "audio", 29 | "b", 30 | "bdi", 31 | "bdo", 32 | "big", 33 | "blink", 34 | "blockquote", 35 | "body", 36 | "br", 37 | "button", 38 | "canvas", 39 | "caption", 40 | "center", 41 | "cite", 42 | "code", 43 | "col", 44 | "colgroup", 45 | "content", 46 | "data", 47 | "datalist", 48 | "dd", 49 | "decorator", 50 | "del", 51 | "details", 52 | "dfn", 53 | "dir", 54 | "div", 55 | "dl", 56 | "dt", 57 | "element", 58 | "em", 59 | "fieldset", 60 | "figcaption", 61 | "figure", 62 | "font", 63 | "footer", 64 | "form", 65 | "h1", 66 | "h2", 67 | "h3", 68 | "h4", 69 | "h5", 70 | "h6", 71 | "head", 72 | "header", 73 | "hgroup", 74 | "hr", 75 | "html", 76 | "i", 77 | "img", 78 | "input", 79 | "ins", 80 | "kbd", 81 | "label", 82 | "legend", 83 | "li", 84 | "main", 85 | "map", 86 | "mark", 87 | "marquee", 88 | "menu", 89 | "menuitem", 90 | "meter", 91 | "nav", 92 | "nobr", 93 | "ol", 94 | "optgroup", 95 | "option", 96 | "output", 97 | "p", 98 | "pre", 99 | "progress", 100 | "q", 101 | "rp", 102 | "rt", 103 | "ruby", 104 | "s", 105 | "samp", 106 | "section", 107 | "select", 108 | "shadow", 109 | "small", 110 | "source", 111 | "spacer", 112 | "span", 113 | "strike", 114 | "strong", 115 | "style", 116 | "sub", 117 | "summary", 118 | "sup", 119 | "table", 120 | "tbody", 121 | "td", 122 | "template", 123 | "textarea", 124 | "tfoot", 125 | "th", 126 | "thead", 127 | "time", 128 | "tr", 129 | "track", 130 | "tt", 131 | "u", 132 | "ul", 133 | "var", 134 | "video", 135 | "wbr", 136 | "svg", 137 | "altglyph", 138 | "altglyphdef", 139 | "altglyphitem", 140 | "animatecolor", 141 | "animatemotion", 142 | "animatetransform", 143 | "circle", 144 | "clippath", 145 | "defs", 146 | "desc", 147 | "ellipse", 148 | "filter", 149 | "g", 150 | "glyph", 151 | "glyphref", 152 | "hkern", 153 | "image", 154 | "line", 155 | "lineargradient", 156 | "marker", 157 | "mask", 158 | "metadata", 159 | "mpath", 160 | "path", 161 | "pattern", 162 | "polygon", 163 | "polyline", 164 | "radialgradient", 165 | "rect", 166 | "stop", 167 | "switch", 168 | "symbol", 169 | "text", 170 | "textpath", 171 | "title", 172 | "tref", 173 | "tspan", 174 | "view", 175 | "vkern", 176 | "feBlend", 177 | "feColorMatrix", 178 | "feComponentTransfer", 179 | "feComposite", 180 | "feConvolveMatrix", 181 | "feDiffuseLighting", 182 | "feDisplacementMap", 183 | "feDistantLight", 184 | "feFlood", 185 | "feFuncA", 186 | "feFuncB", 187 | "feFuncG", 188 | "feFuncR", 189 | "feGaussianBlur", 190 | "feMerge", 191 | "feMergeNode", 192 | "feMorphology", 193 | "feOffset", 194 | "fePointLight", 195 | "feSpecularLighting", 196 | "feSpotLight", 197 | "feTile", 198 | "feTurbulence", 199 | "math", 200 | "menclose", 201 | "merror", 202 | "mfenced", 203 | "mfrac", 204 | "mglyph", 205 | "mi", 206 | "mlabeledtr", 207 | "mmultiscripts", 208 | "mn", 209 | "mo", 210 | "mover", 211 | "mpadded", 212 | "mphantom", 213 | "mroot", 214 | "mrow", 215 | "ms", 216 | "mspace", 217 | "msqrt", 218 | "mstyle", 219 | "msub", 220 | "msup", 221 | "msubsup", 222 | "mtable", 223 | "mtd", 224 | "mtext", 225 | "mtr", 226 | "munder", 227 | "munderover" 228 | ], 229 | "attributes": { 230 | "a": ["href"], 231 | "img": ["src", "longDesc"], 232 | "input": [ 233 | ["type", "checkbox"], 234 | ["disabled", true] 235 | ], 236 | "li": [["className", "task-list-item"]], 237 | "div": ["itemScope", "itemType"], 238 | "blockquote": ["cite"], 239 | "del": ["cite"], 240 | "ins": ["cite"], 241 | "q": ["cite"], 242 | "*": [ 243 | "abbr", 244 | "accept", 245 | "acceptCharset", 246 | "accessKey", 247 | "action", 248 | "align", 249 | "alt", 250 | "ariaDescribedBy", 251 | "ariaHidden", 252 | "ariaLabel", 253 | "ariaLabelledBy", 254 | "axis", 255 | "border", 256 | "cellPadding", 257 | "cellSpacing", 258 | "char", 259 | "charOff", 260 | "charSet", 261 | "checked", 262 | "clear", 263 | "cols", 264 | "colSpan", 265 | "color", 266 | "compact", 267 | "coords", 268 | "dateTime", 269 | "dir", 270 | "disabled", 271 | "encType", 272 | "htmlFor", 273 | "frame", 274 | "headers", 275 | "height", 276 | "hrefLang", 277 | "hSpace", 278 | "isMap", 279 | "id", 280 | "label", 281 | "lang", 282 | "maxLength", 283 | "media", 284 | "method", 285 | "multiple", 286 | "name", 287 | "noHref", 288 | "noShade", 289 | "noWrap", 290 | "open", 291 | "prompt", 292 | "readOnly", 293 | "rel", 294 | "rev", 295 | "rows", 296 | "rowSpan", 297 | "rules", 298 | "scope", 299 | "selected", 300 | "shape", 301 | "size", 302 | "span", 303 | "start", 304 | "summary", 305 | "tabIndex", 306 | "target", 307 | "title", 308 | "type", 309 | "useMap", 310 | "vAlign", 311 | "value", 312 | "vSpace", 313 | "width", 314 | "itemProp", 315 | "className", 316 | "data*", 317 | "about", 318 | "content", 319 | "datatype", 320 | "id", 321 | "lang", 322 | "property", 323 | "rel", 324 | "resource", 325 | "rev", 326 | "tabindex", 327 | "typeof", 328 | "ariaActiveDescendant", 329 | "ariaAtomic", 330 | "ariaAutoComplete", 331 | "ariaBusy", 332 | "ariaChecked", 333 | "ariaColCount", 334 | "ariaColIndex", 335 | "ariaColSpan", 336 | "ariaControls", 337 | "ariaCurrent", 338 | "ariaDescribedBy", 339 | "ariaDetails", 340 | "ariaDisabled", 341 | "ariaDropEffect", 342 | "ariaErrorMessage", 343 | "ariaExpanded", 344 | "ariaFlowTo", 345 | "ariaGrabbed", 346 | "ariaHasPopup", 347 | "ariaHidden", 348 | "ariaInvalid", 349 | "ariaKeyShortcuts", 350 | "ariaLabel", 351 | "ariaLabelledBy", 352 | "ariaLevel", 353 | "ariaLive", 354 | "ariaModal", 355 | "ariaMultiLine", 356 | "ariaMultiSelectable", 357 | "ariaOrientation", 358 | "ariaOwns", 359 | "ariaPlaceholder", 360 | "ariaPosInSet", 361 | "ariaPressed", 362 | "ariaReadOnly", 363 | "ariaRelevant", 364 | "ariaRequired", 365 | "ariaRoleDescription", 366 | "ariaRowCount", 367 | "ariaRowIndex", 368 | "ariaRowSpan", 369 | "ariaSelected", 370 | "ariaSetSize", 371 | "ariaSort", 372 | "ariaValueMax", 373 | "ariaValueMin", 374 | "ariaValueNow", 375 | "ariaValueText", 376 | "role" 377 | ], 378 | "foreignObject": [ 379 | "alignment-baseline", 380 | "baseline-shift", 381 | "clip", 382 | "clip-path", 383 | "clip-rule", 384 | "color", 385 | "color-interpolation", 386 | "color-interpolation-filters", 387 | "color-profile", 388 | "color-rendering", 389 | "cursor", 390 | "direction", 391 | "display", 392 | "dominant-baseline", 393 | "enable-background", 394 | "externalResourcesRequired", 395 | "fill", 396 | "fill-opacity", 397 | "fill-rule", 398 | "filter", 399 | "flood-color", 400 | "flood-opacity", 401 | "focusHighlight", 402 | "focusable", 403 | "font-family", 404 | "font-size", 405 | "font-size-adjust", 406 | "font-stretch", 407 | "font-style", 408 | "font-variant", 409 | "font-weight", 410 | "glyph-orientation-horizontal", 411 | "glyph-orientation-vertical", 412 | "height", 413 | "image-rendering", 414 | "kerning", 415 | "letter-spacing", 416 | "lighting-color", 417 | "marker-end", 418 | "marker-mid", 419 | "marker-start", 420 | "mask", 421 | "nav-down", 422 | "nav-down-left", 423 | "nav-down-right", 424 | "nav-left", 425 | "nav-next", 426 | "nav-prev", 427 | "nav-right", 428 | "nav-up", 429 | "nav-up-left", 430 | "nav-up-right", 431 | "opacity", 432 | "overflow", 433 | "pointer-events", 434 | "requiredExtensions", 435 | "requiredFeatures", 436 | "requiredFonts", 437 | "requiredFormats", 438 | "shape-rendering", 439 | "stop-color", 440 | "stop-opacity", 441 | "stroke", 442 | "stroke-dasharray", 443 | "stroke-dashoffset", 444 | "stroke-linecap", 445 | "stroke-linejoin", 446 | "stroke-miterlimit", 447 | "stroke-opacity", 448 | "stroke-width", 449 | "systemLanguage", 450 | "text-anchor", 451 | "text-decoration", 452 | "text-rendering", 453 | "transform", 454 | "unicode-bidi", 455 | "visibility", 456 | "width", 457 | "word-spacing", 458 | "writing-mode", 459 | "x", 460 | "y" 461 | ], 462 | "script": [ 463 | "async", 464 | "charSet", 465 | "crossOrigin", 466 | "defer", 467 | "integrity", 468 | "language", 469 | "noModule", 470 | "nonce", 471 | "referrerPolicy", 472 | "src", 473 | "type" 474 | ], 475 | "applet": [ 476 | "align", 477 | "alt", 478 | "archive", 479 | "code", 480 | "codeBase", 481 | "height", 482 | "hSpace", 483 | "name", 484 | "object", 485 | "vSpace", 486 | "width" 487 | ], 488 | "object": [ 489 | "align", 490 | "archive", 491 | "border", 492 | "classId", 493 | "codeBase", 494 | "codeType", 495 | "data", 496 | "declare", 497 | "form", 498 | "height", 499 | "hSpace", 500 | "name", 501 | "standby", 502 | "tabIndex", 503 | "type", 504 | "typeMustMatch", 505 | "useMap", 506 | "vSpace", 507 | "width" 508 | ] 509 | }, 510 | "required": { 511 | "input": { 512 | "type": "checkbox", 513 | "disabled": true 514 | } 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /hast-annotations-match/generate-full-html-schema.js: -------------------------------------------------------------------------------- 1 | const merge = require("deepmerge"); 2 | const gh = require("hast-util-sanitize/lib/github"); 3 | const info = require("property-information"); 4 | const svgElementAttributes = require("svg-element-attributes"); 5 | const ariaAttributes = require("aria-attributes").map( 6 | (attr) => info.find(info.html, attr).property 7 | ); 8 | const htmlElementAttributes = require("html-element-attributes"); 9 | const fs = require("fs"); 10 | 11 | let schema = gh; 12 | schema.tagNames = [ 13 | "a", 14 | "abbr", 15 | "acronym", 16 | "address", 17 | "area", 18 | "article", 19 | "aside", 20 | "audio", 21 | "b", 22 | "bdi", 23 | "bdo", 24 | "big", 25 | "blink", 26 | "blockquote", 27 | "body", 28 | "br", 29 | "button", 30 | "canvas", 31 | "caption", 32 | "center", 33 | "cite", 34 | "code", 35 | "col", 36 | "colgroup", 37 | "content", 38 | "data", 39 | "datalist", 40 | "dd", 41 | "decorator", 42 | "del", 43 | "details", 44 | "dfn", 45 | "dir", 46 | "div", 47 | "dl", 48 | "dt", 49 | "element", 50 | "em", 51 | "fieldset", 52 | "figcaption", 53 | "figure", 54 | "font", 55 | "footer", 56 | "form", 57 | "h1", 58 | "h2", 59 | "h3", 60 | "h4", 61 | "h5", 62 | "h6", 63 | "head", 64 | "header", 65 | "hgroup", 66 | "hr", 67 | "html", 68 | "i", 69 | "img", 70 | "input", 71 | "ins", 72 | "kbd", 73 | "label", 74 | "legend", 75 | "li", 76 | "main", 77 | "map", 78 | "mark", 79 | "marquee", 80 | "menu", 81 | "menuitem", 82 | "meter", 83 | "nav", 84 | "nobr", 85 | "ol", 86 | "optgroup", 87 | "option", 88 | "output", 89 | "p", 90 | "pre", 91 | "progress", 92 | "q", 93 | "rp", 94 | "rt", 95 | "ruby", 96 | "s", 97 | "samp", 98 | "section", 99 | "select", 100 | "shadow", 101 | "small", 102 | "source", 103 | "spacer", 104 | "span", 105 | "strike", 106 | "strong", 107 | "style", 108 | "sub", 109 | "summary", 110 | "sup", 111 | "table", 112 | "tbody", 113 | "td", 114 | "template", 115 | "textarea", 116 | "tfoot", 117 | "th", 118 | "thead", 119 | "time", 120 | "tr", 121 | "track", 122 | "tt", 123 | "u", 124 | "ul", 125 | "var", 126 | "video", 127 | "wbr", 128 | "svg", 129 | "a", 130 | "altglyph", 131 | "altglyphdef", 132 | "altglyphitem", 133 | "animatecolor", 134 | "animatemotion", 135 | "animatetransform", 136 | "audio", 137 | "canvas", 138 | "circle", 139 | "clippath", 140 | "defs", 141 | "desc", 142 | "ellipse", 143 | "filter", 144 | "font", 145 | "g", 146 | "glyph", 147 | "glyphref", 148 | "hkern", 149 | "image", 150 | "line", 151 | "lineargradient", 152 | "marker", 153 | "mask", 154 | "metadata", 155 | "mpath", 156 | "path", 157 | "pattern", 158 | "polygon", 159 | "polyline", 160 | "radialgradient", 161 | "rect", 162 | "stop", 163 | "style", 164 | "switch", 165 | "symbol", 166 | "text", 167 | "textpath", 168 | "title", 169 | "tref", 170 | "tspan", 171 | "video", 172 | "view", 173 | "vkern", 174 | "feBlend", 175 | "feColorMatrix", 176 | "feComponentTransfer", 177 | "feComposite", 178 | "feConvolveMatrix", 179 | "feDiffuseLighting", 180 | "feDisplacementMap", 181 | "feDistantLight", 182 | "feFlood", 183 | "feFuncA", 184 | "feFuncB", 185 | "feFuncG", 186 | "feFuncR", 187 | "feGaussianBlur", 188 | "feMerge", 189 | "feMergeNode", 190 | "feMorphology", 191 | "feOffset", 192 | "fePointLight", 193 | "feSpecularLighting", 194 | "feSpotLight", 195 | "feTile", 196 | "feTurbulence", 197 | "math", 198 | "menclose", 199 | "merror", 200 | "mfenced", 201 | "mfrac", 202 | "mglyph", 203 | "mi", 204 | "mlabeledtr", 205 | "mmultiscripts", 206 | "mn", 207 | "mo", 208 | "mover", 209 | "mpadded", 210 | "mphantom", 211 | "mroot", 212 | "mrow", 213 | "ms", 214 | "mspace", 215 | "msqrt", 216 | "mstyle", 217 | "msub", 218 | "msup", 219 | "msubsup", 220 | "mtable", 221 | "mtd", 222 | "mtext", 223 | "mtr", 224 | "munder", 225 | "munderover", 226 | ]; 227 | 228 | schema = merge(schema, { 229 | attributes: { 230 | "*": Array.from( 231 | new Set( 232 | [ 233 | "className", 234 | "data*", 235 | "about", 236 | "content", 237 | "datatype", 238 | "id", 239 | "lang", 240 | "property", 241 | "rel", 242 | "resource", 243 | "rev", 244 | "tabindex", 245 | "typeof", 246 | ].concat(ariaAttributes) 247 | ) 248 | ), 249 | }, 250 | }); 251 | 252 | schema.strip = ["script", "object", "applet", "foreignObject"]; 253 | for (const element of Object.keys(svgElementAttributes)) { 254 | if (element !== "*" && schema.strip.indexOf(element) !== -1) { 255 | schema.attributes[element] = svgElementAttributes[element].map( 256 | (attr) => info.find(info.html, attr).property 257 | ); 258 | } 259 | } 260 | for (const element of Object.keys(htmlElementAttributes)) { 261 | if (element !== "*" && schema.strip.indexOf(element) !== -1) { 262 | schema.attributes[element] = htmlElementAttributes[element].map( 263 | (attr) => info.find(info.html, attr).property 264 | ); 265 | } 266 | } 267 | 268 | schema.tagNames = Array.from(new Set(schema.tagNames)); 269 | 270 | module.exports = schema; 271 | 272 | fs.writeFileSync( 273 | "hast-annotations-match/full-html-schema.json", 274 | JSON.stringify(schema, null, 4) 275 | ); 276 | -------------------------------------------------------------------------------- /hast-annotations-match/get-node.js: -------------------------------------------------------------------------------- 1 | // const debug = require("../logger")("hast-annotations-match"); 2 | import { nodeSelector } from "./css-selector.js"; 3 | import { simpleXpathSelector } from "./simple-xpath-selector.js"; 4 | import { processPositions } from "./process-positions.js"; 5 | import { processQuotations } from "./process-quotations.js"; 6 | const selectors = { 7 | XPathSelector: simpleXpathSelector, 8 | CssSelector: nodeSelector, 9 | FragmentSelector: ({ tree, value, annotation, addProps = true }) => { 10 | return nodeSelector({ 11 | tree, 12 | value: "#" + value, 13 | annotation, 14 | addProps, 15 | }); 16 | }, 17 | }; 18 | 19 | /** 20 | * 21 | * @param {{tree: Object, selector: Object, annotation: Object}} param0 - selector options 22 | */ 23 | export function getNode({ tree, selector, annotation }) { 24 | // Need to check `refinedBy`. If so and refining selector is quote or text-position then process using node as root tree 25 | if (selector.refinedBy) { 26 | const node = selectors[selector.type]({ 27 | tree, 28 | value: selector.value, 29 | annotation, 30 | addProps: false, 31 | }); 32 | const target = { ...annotation.target, selector: selector.refinedBy }; 33 | if (selector.refinedBy.type === "TextQuoteSelector") { 34 | processQuotations(node, [{ ...annotation, target }]); 35 | } else if (selector.refinedBy.type === "TextPositionSelector") { 36 | processPositions(node, [{ ...annotation, target }]); 37 | } else { 38 | return selectors[selector.refinedBy.type]({ 39 | tree: node, 40 | value: selector.refinedBy.value, 41 | annotation: { 42 | ...annotation, 43 | target, 44 | }, 45 | }); 46 | } 47 | } else { 48 | return selectors[selector.type]({ 49 | tree, 50 | value: selector.value, 51 | annotation, 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hast-annotations-match/index.js: -------------------------------------------------------------------------------- 1 | // const debug = require("../logger")("hast-annotations-match"); 2 | import { rangeSelector } from "./range-selector.js"; 3 | import { getNode } from "./get-node.js"; 4 | import { processPositions } from "./process-positions.js"; 5 | import { processQuotations } from "./process-quotations.js"; 6 | import { selectAll } from "hast-util-select"; 7 | import { find, svg } from "property-information"; 8 | import { h } from "hastscript"; 9 | 10 | function debug(...args) { 11 | // console.log(...args); 12 | } 13 | 14 | const props = [ 15 | "x", 16 | "y", 17 | "width", 18 | "height", 19 | "textLength", 20 | "font-size", 21 | "data-annotation-id", 22 | "data-annotation-x", 23 | "data-annotation-y", 24 | "data-annotation-width", 25 | "data-annotation-height", 26 | "data-annotation-offset", 27 | "data-annotation-highlight", 28 | "data-annotation-transform", 29 | "data-annotation-highlight-box", 30 | "pointer-events", 31 | "transform", 32 | "fill", 33 | "class", 34 | ]; 35 | const attributes = {}; 36 | for (const prop of props) { 37 | attributes[prop] = find(svg, prop).property; 38 | } 39 | 40 | /* 41 | ## `hast-annotations-match` 42 | _called with -> (tree, annotations) -> modified tree with annotations data prop_ 43 | 44 | Modifies the hast tree to include annotations properties and marks for highlights. Returns a modified tree with a sorted annotations collection under `data.annotations` on the root element. 45 | 46 | For each selector: 47 | 1. Match to node: 48 | * Node-based: select node and add properties (id, index, motivation, styleClass). 49 | * If has refinedBy, call Quote-based processing with selected node as root. 50 | * Quote-based: convert to text positions 51 | 2. Visit tree with parents: 52 | 1. If highlight motivation 53 | 1. Node-based: if highlight motivation, add marks to all text children. Add props to marks 54 | 2. If text position, split at offsets and wrap selected in marks, then links (if it has linking purpose). Do not wrap Text nodes that already have mark parents. Add props to marks. 55 | 3. Add included stylesheets to head. 56 | 4. Add default annotations stylesheet to head. 57 | 5. Add link to annotations collection id in head 58 | 59 | ## Props 60 | 61 | * `data-annotations-id` 62 | * `data-selector-index`: index of mark in total number of marks for this selector 63 | * `data-annotations-motivation`: annotations motivations 64 | * `class` : styleClass 65 | * `data-annotations-purpose`: body purposes 66 | * `data-annotations-creator` 67 | 68 | */ 69 | 70 | export function matchAnnotations( 71 | tree, 72 | file, 73 | { annotations, url, canonical, notes } 74 | ) { 75 | // iterate through annotations 76 | let positionAnnotations = []; 77 | let quoteAnnotations = []; 78 | for (const annotation of annotations) { 79 | const { target } = annotation; 80 | if (testSource(target.source)) { 81 | const { selector } = target; 82 | switch (selector.type) { 83 | case "TextQuoteSelector": 84 | debug("using TextQuoteSelector"); 85 | quoteAnnotations = quoteAnnotations.concat(annotation); 86 | break; 87 | case "TextPositionSelector": 88 | debug("using TextPositionSelector"); 89 | positionAnnotations = positionAnnotations.concat(annotation); 90 | break; 91 | case "RangeSelector": 92 | debug("using RangeSelector"); 93 | rangeSelector({ tree, selector, annotation }); 94 | break; 95 | default: 96 | getNode({ tree, selector, annotation }); 97 | break; 98 | } 99 | } 100 | } 101 | processPositions(tree, positionAnnotations); 102 | processQuotations(tree, quoteAnnotations); 103 | selectAll("svg", tree).forEach((svg) => { 104 | const svgHighlights = selectAll("tspan[data-annotation-id]", svg).map( 105 | (node) => { 106 | const rect = h("rect"); 107 | rect.properties[attributes["data-annotation-highlight-box"]] = 108 | node.properties[attributes["data-annotation-id"]]; 109 | rect.properties[attributes.x] = 110 | node.properties[attributes["data-annotation-x"]]; 111 | rect.properties[attributes.y] = 112 | node.properties[attributes["data-annotation-y"]]; 113 | rect.properties[attributes.width] = 114 | node.properties[attributes["data-annotation-width"]]; 115 | rect.properties[attributes.height] = 116 | node.properties[attributes["data-annotation-height"]]; 117 | rect.properties[attributes.transform] = 118 | node.properties[attributes["data-annotation-transform"]]; 119 | rect.properties[attributes.fill] = "rgba(255, 255, 0, 0.35)"; 120 | const classes = []; 121 | rect.properties[attributes.class] = classes.concat( 122 | node.properties[attributes.class] 123 | ); 124 | rect.properties[attributes["pointer-events"]] = "none"; 125 | return rect; 126 | } 127 | ); 128 | svg.children = svg.children.concat(svgHighlights); 129 | }); 130 | const sortedAnnotationsId = selectAll("[data-annotation-id]", tree).map( 131 | (node) => node.properties.dataAnnotationId 132 | ); 133 | const sortedAnnotations = Array.from(new Set(sortedAnnotationsId)).map((id) => 134 | annotations.find((annotation) => annotation.id === id) 135 | ); 136 | file.data.annotations = sortedAnnotations; 137 | function testSource(source) { 138 | return source === url || source === canonical; 139 | } 140 | return tree; 141 | } 142 | 143 | // function addHighlights (node, ancestors) { 144 | // if (node.tagName === "tspan" && node.properties[attributes["data-annotation-id"]]) { 145 | // const rect = h("rect[data-annotation-highlight-box]") 146 | // rect.properties[attributes.x] = node.properties[attributes["data-annotation-x"]]; 147 | // rect.properties[attributes.y] = node.properties[attributes["data-annotation-y"]]; 148 | // rect.properties[attributes.width] = node.properties[attributes["data-annotation-width"]]; 149 | // rect.properties[attributes.height] = node.properties[attributes["data-annotation-height"]]; 150 | // rect.properties[attributes.transform] = node.properties[attributes["data-annotation-transform"]]; 151 | // rect.properties[attributes.fill] = "rgba(255, 255, 0, 0.5)" 152 | // const classes = []; 153 | // node.properties[attributes.class] = classes 154 | // .concat(node.properties[attributes.class]) 155 | // } 156 | // } 157 | 158 | export default matchAnnotations; 159 | -------------------------------------------------------------------------------- /hast-annotations-match/process-positions.js: -------------------------------------------------------------------------------- 1 | import { visitParents as visit } from "unist-util-visit-parents"; 2 | import { addPropsToNode } from "./add-props-to-node.js"; 3 | import { h } from "hastscript"; 4 | import { addParentProps } from "./add-parent-props.js"; 5 | import { encode } from "universal-base64url"; 6 | 7 | function getId(id) { 8 | return `id-${encode(id)}`; 9 | } 10 | // const debug = require("../logger")("process-positions"); 11 | 12 | /* 13 | ## `hast-annotations-match` 14 | _called with -> (tree, annotations) -> modified tree with annotations data prop_ 15 | 16 | Modifies the hast tree to include annotations properties and marks for highlights. Returns a modified tree with a sorted annotations collection under `data.annotations` on the root element. 17 | 18 | For each selector: 19 | 1. Match to node: 20 | * Node-based: select node and add properties (id, index, motivation, styleClass). 21 | * If has refinedBy, call Quote-based processing with selected node as root. 22 | * Quote-based: convert to text positions 23 | 2. Visit tree with parents: 24 | 1. If highlight motivation 25 | 1. Node-based: if highlight motivation, add marks to all text children. Add props to marks 26 | 2. If text position, split at offsets and wrap selected in marks, then links (if it has linking purpose). Do not wrap Text nodes that already have mark parents. Add props to marks. 27 | 3. Add included stylesheets to head. 28 | 4. Add default annotations stylesheet to head. 29 | 5. Add link to annotations collection id in head 30 | */ 31 | 32 | export function processPositions(tree, positionAnnotations) { 33 | // Sort annotations based on selector.start 34 | positionAnnotations.sort( 35 | (a, b) => a.target.selector.start - b.target.selector.start 36 | ); 37 | // shift first annotation 38 | let annotation = getAnnotation(positionAnnotations); 39 | // visit tree with parents, filtering on text nodes, maintaining character count 40 | let count = 0; 41 | const replacementActions = []; 42 | visit(tree, "text", visitor); 43 | replacementActions.forEach((fn) => fn()); 44 | function visitor(node, ancestors) { 45 | if (!annotation) return; 46 | visitNode({ 47 | count, 48 | currentAnnotation: annotation, 49 | node, 50 | ancestors, 51 | }); 52 | count = count + node.value.length; 53 | } 54 | function visitNode({ count, currentAnnotation, node, ancestors }) { 55 | const parent = ancestors[ancestors.length - 1]; 56 | const textElement = ancestors.find((node) => node.tagName === "text"); 57 | // One thing that would be useful here is the ability to select elements using the position/quotation selector 58 | // The way this would work could be to have two new options: highlightingPurposes and highlightingSelectors. 59 | // The selector only highlights if both are true. Export a config.js module that lists all known purposes and selectors. 60 | // Also need an injectAnnotation option that injects the HTML commenting body adjacent to the selected node. 61 | // How would it work 62 | const { end } = currentAnnotation.target.selector; 63 | const startInNode = startIsInNode(count, currentAnnotation, node); 64 | const endInNode = endIsInNode(count, currentAnnotation, node); 65 | const { replacement, suffix } = processNode({ 66 | startInNode, 67 | endInNode, 68 | count, 69 | node, 70 | svg: ancestors[ancestors.map((node) => node.tagName).lastIndexOf("svg")], 71 | currentAnnotation, 72 | parent: textElement, 73 | }); 74 | if (replacement) { 75 | replacementActions.push(() => { 76 | const newIndex = parent.children.indexOf(node); 77 | parent.children.splice(newIndex, 1, ...replacement); 78 | }); 79 | } 80 | if (endInNode) { 81 | annotation = getAnnotation(positionAnnotations); 82 | if (suffix && annotation) { 83 | visitNode({ 84 | count: end, 85 | currentAnnotation: annotation, 86 | node: suffix, 87 | ancestors, 88 | }); 89 | } 90 | } 91 | } 92 | } 93 | 94 | function wrapNode(text, annotation, svg, parent) { 95 | // If we decide to support linking purposes by rendering actual links then we need to change this and make sure we don't render nested links. 96 | // It's actually simpler in the meantime to support linking purposes by rendering a link button either after highlight or in sidebar. 97 | const node = h(svg ? "tspan" : "mark", text); 98 | addPropsToNode(node, annotation); 99 | addParentProps(svg, node, parent); 100 | return node; 101 | } 102 | function getAnnotation(positionAnnotations) { 103 | const annotation = positionAnnotations.shift(); 104 | return annotation; 105 | } 106 | 107 | function startIsInNode(count, currentAnnotation, node) { 108 | const { start } = currentAnnotation.target.selector; 109 | let startInNode = false; 110 | if (count <= start && start <= count + node.value.length) { 111 | startInNode = true; 112 | // debug("start is in node"); 113 | } 114 | return startInNode; 115 | } 116 | 117 | function endIsInNode(count, currentAnnotation, node) { 118 | const { end } = currentAnnotation.target.selector; 119 | let endInNode = false; 120 | if (count <= end && end <= count + node.value.length) { 121 | endInNode = true; 122 | // debug("end is in node"); 123 | } 124 | return endInNode; 125 | } 126 | 127 | function processNode({ 128 | startInNode, 129 | endInNode, 130 | count, 131 | node, 132 | currentAnnotation, 133 | svg, 134 | parent, 135 | }) { 136 | const { start, end } = currentAnnotation.target.selector; 137 | let replacement; 138 | let suffix; 139 | if (startInNode && endInNode) { 140 | const firstSplit = start - count; 141 | const secondSplit = end - count; 142 | const prefix = { type: "text", value: node.value.slice(0, firstSplit) }; 143 | const nodeValue = node.value.slice(firstSplit, secondSplit); 144 | if (nodeValue.trim()) { 145 | const wrappedNode = wrapNode( 146 | node.value.slice(firstSplit, secondSplit), 147 | currentAnnotation, 148 | svg, 149 | parent 150 | ); 151 | const suffixValue = node.value.slice(secondSplit); 152 | suffix = { type: "text", value: suffixValue }; 153 | wrappedNode.properties.id = getId(currentAnnotation.id); 154 | replacement = [prefix, wrappedNode, suffix]; 155 | } 156 | // else if (start) split at start, wrap the rest 157 | } else if (startInNode) { 158 | const firstSplit = start - count; 159 | const prefix = { type: "text", value: node.value.slice(0, firstSplit) }; 160 | const nodeValue = node.value.slice(firstSplit); 161 | if (nodeValue.trim()) { 162 | const wrappedNode = wrapNode( 163 | node.value.slice(firstSplit), 164 | currentAnnotation, 165 | svg, 166 | parent 167 | ); 168 | wrappedNode.properties.id = getId(currentAnnotation.id); 169 | replacement = [prefix, wrappedNode]; 170 | } 171 | // else if (end) split at end, wrap beginning 172 | } else if (endInNode) { 173 | const secondSplit = end - count; 174 | const wrappedNode = wrapNode( 175 | node.value.slice(0, secondSplit), 176 | currentAnnotation, 177 | svg, 178 | parent 179 | ); 180 | suffix = { type: "text", value: node.value.slice(secondSplit) }; 181 | replacement = [wrappedNode, suffix]; 182 | // if node is entirely within selector range, wrap contents 183 | } else if ( 184 | start < count && 185 | count + node.value.length < end && 186 | node.value.trim() 187 | ) { 188 | // debug("whitespace: ", !node.value.trim()); 189 | replacement = [wrapNode(node.value, currentAnnotation, svg, parent)]; 190 | } 191 | return { replacement, suffix }; 192 | } 193 | -------------------------------------------------------------------------------- /hast-annotations-match/process-quotations.js: -------------------------------------------------------------------------------- 1 | import { processPositions } from "./process-positions.js"; 2 | import DiffMatchPatch from "diff-match-patch"; 3 | import toString from "hast-util-to-string"; 4 | 5 | // Based on https://github.com/tilgovi/dom-anchor-text-quote/blob/master/src/index.js MIT Licensed 6 | 7 | // The DiffMatchPatch bitap has a hard 32-character pattern length limit. 8 | const SLICE_LENGTH = 32; 9 | const SLICE_RE = new RegExp("(.|[\r\n]){1," + String(SLICE_LENGTH) + "}", "g"); 10 | 11 | export function processQuotations(tree, quoteAnnotations) { 12 | let positionAnnotations = quoteAnnotations.map(processQuote); 13 | function processQuote(annotation, index) { 14 | return processor(tree, annotation); 15 | } 16 | positionAnnotations = positionAnnotations.filter((item) => item); 17 | processPositions(tree, positionAnnotations); 18 | } 19 | 20 | function processor(tree, annotation, options = {}) { 21 | const { selector } = annotation.target; 22 | const { prefix, exact, suffix } = selector; 23 | 24 | const dmp = new DiffMatchPatch(); 25 | 26 | const textContent = toString(tree); 27 | dmp.Match_Distance = textContent.length * 2; 28 | 29 | // Work around a hard limit of the DiffMatchPatch bitap implementation. 30 | // The search pattern must be no more than SLICE_LENGTH characters. 31 | const slices = exact.match(SLICE_RE); 32 | let loc = 0; 33 | let start = Number.POSITIVE_INFINITY; 34 | let end = Number.NEGATIVE_INFINITY; 35 | let result = -1; 36 | const havePrefix = prefix !== undefined; 37 | const haveSuffix = suffix !== undefined; 38 | let foundPrefix = false; 39 | 40 | // If the prefix is known then search for that first. 41 | if (havePrefix) { 42 | result = dmp.match_main(textContent, prefix, loc); 43 | if (result > -1) { 44 | loc = result + prefix.length; 45 | foundPrefix = true; 46 | } 47 | } 48 | 49 | // If we have a suffix, and the prefix wasn't found, then search for it. 50 | if (haveSuffix && !foundPrefix) { 51 | result = dmp.match_main(textContent, suffix, loc + exact.length); 52 | if (result > -1) { 53 | loc = result - exact.length; 54 | } 55 | } 56 | 57 | // Search for the first slice. 58 | const firstSlice = slices.shift(); 59 | result = dmp.match_main(textContent, firstSlice, loc); 60 | if (result > -1) { 61 | start = result; 62 | loc = end = start + firstSlice.length; 63 | } else { 64 | return null; 65 | } 66 | 67 | // Create a fold function that will reduce slices to positional extents. 68 | const foldSlices = (acc, slice) => { 69 | if (!acc) { 70 | // A search for an earlier slice of the pattern failed to match. 71 | return null; 72 | } 73 | 74 | const result = dmp.match_main(textContent, slice, acc.loc); 75 | if (result === -1) { 76 | return null; 77 | } 78 | 79 | // The next slice should follow this one closely. 80 | acc.loc = result + slice.length; 81 | 82 | // Expand the start and end to a quote that includes all the slices. 83 | acc.start = Math.min(acc.start, result); 84 | acc.end = Math.max(acc.end, result + slice.length); 85 | 86 | return acc; 87 | }; 88 | // Use the fold function to establish the full quote extents. 89 | // Expect the slices to be close to one another. 90 | // This distance is deliberately generous for now. 91 | dmp.Match_Distance = 64; 92 | const acc = slices.reduce(foldSlices, { start, end, loc }); 93 | if (!acc) { 94 | return null; 95 | } 96 | const target = { 97 | ...annotation.target, 98 | selector: { 99 | type: "TextPositionSelector", 100 | start: acc.start, 101 | end: acc.end, 102 | }, 103 | }; 104 | return { ...annotation, target }; 105 | } 106 | -------------------------------------------------------------------------------- /hast-annotations-match/range-selector.js: -------------------------------------------------------------------------------- 1 | import { visit } from "unist-util-visit"; 2 | import { is } from "unist-util-is"; 3 | import { getNode } from "./get-node.js"; 4 | import { addPropsToNode } from "./add-props-to-node.js"; 5 | 6 | /** 7 | * 8 | * @param {{tree: Object, selector: Object, annotation: Object}} param0 - selector options 9 | */ 10 | export function rangeSelector({ tree, selector, annotation }) { 11 | const { startSelector, endSelector } = selector; 12 | const startNode = getNode({ 13 | tree, 14 | selector: startSelector, 15 | annotation, 16 | }); 17 | const endNode = getNode({ 18 | tree, 19 | selector: endSelector, 20 | annotation, 21 | }); 22 | let controlStart = false; 23 | let controlEnd = false; 24 | let selectedNodes = [startNode]; 25 | if (!startNode || !endNode) return; 26 | visit(tree, "element", (node, index, parent) => { 27 | if (controlStart && !controlEnd) { 28 | selectedNodes = selectedNodes.concat(node); 29 | } 30 | if (is(node, startNode)) { 31 | controlStart = true; 32 | } 33 | if (is(node, endNode)) { 34 | controlEnd = true; 35 | } 36 | }); 37 | selectedNodes = [...selectedNodes, endNode]; 38 | selectedNodes.map((node) => { 39 | addPropsToNode(node, annotation); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /hast-annotations-match/render-templates.js: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | const gh = require("hast-util-sanitize/lib/github"); 3 | const sanitize = require("rehype-sanitize"); 4 | const unified = require("unified"); 5 | const parse = require("rehype-parse"); 6 | const info = require("property-information"); 7 | const markdown = require("remark-parse"); 8 | const remark2rehype = require("remark-rehype"); 9 | const stringify = require("rehype-stringify"); 10 | const raw = require("rehype-raw"); 11 | const h = require("hastscript"); 12 | 13 | /* 14 | ## Props 15 | * `data-annotation-id` 16 | * `data-selector-index`: index of mark in total number of marks for this selector 17 | * `data-annotation-motivation`: annotations motivations 18 | * `class` : styleClass 19 | * `data-annotation-purpose`: body purposes 20 | */ 21 | const props = [ 22 | "data-template-id", 23 | "data-controller", 24 | "data-template-purpose", 25 | "class", 26 | "lang", 27 | "data-target", 28 | ]; 29 | const attributes = {}; 30 | for (const prop of props) { 31 | attributes[prop] = info.find(info.html, prop).property; 32 | } 33 | const htmlProcessor = unified() 34 | .use(parse, { fragment: true }) 35 | .use(sanitize, gh); 36 | 37 | const markdownProcessor = unified() 38 | .use(markdown, { commonmark: true, footnotes: true }) 39 | .use(remark2rehype, { allowDangerousHTML: true, commonmark: true }) 40 | .use(raw) 41 | .use(stringify); 42 | 43 | module.exports = function renderTemplates(annotations) { 44 | // This line flattens the resulting map and filters out undefineds/nulls. 45 | return Array.prototype.concat 46 | .apply([], annotations.map(renderTemplate)) 47 | .filter((item) => item); 48 | }; 49 | 50 | function renderTemplate(annotation) { 51 | if (annotation.body) { 52 | return annotation.body.map(renderBody); 53 | } 54 | function renderBody(body, index) { 55 | if (body.type === "TextualBody") { 56 | let wrapper; 57 | if (body.format === "text/html") { 58 | wrapper = htmlProcessor.parse(`${body.value}`) 59 | .children[0]; 60 | } else if (body.format === "text/markdown") { 61 | const html = markdownProcessor.processSync(`${body.value}`); 62 | wrapper = htmlProcessor.parse(`${String(html)}`) 63 | .children[0]; 64 | } else { 65 | wrapper = h("template", body.value); 66 | } 67 | wrapper.properties[attributes["data-template-id"]] = `${annotation.id}`; 68 | wrapper.properties[attributes["data-controller"]] = "template"; 69 | if (body.purpose) { 70 | wrapper.properties[attributes["data-template-purpose"]] = body.purpose; 71 | } 72 | if (body.language) { 73 | wrapper.properties[attributes.lang] = body.language; 74 | } 75 | wrapper.properties[attributes["data-target"]] = "annotations.template"; 76 | return wrapper; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /hast-annotations-match/simple-xpath-selector.js: -------------------------------------------------------------------------------- 1 | import { addPropsToNode } from "./add-props-to-node.js"; 2 | 3 | // Based on simple-xpath-selector from https://github.com/tilgovi/simple-xpath-position/blob/master/src/xpath.js MIT license 4 | 5 | /** 6 | * 7 | * @param {{tree: Object, value: string, annotation: Object, addProps: undefined | boolean}} param0 - selector options 8 | */ 9 | export function simpleXpathSelector({ 10 | tree, 11 | value, 12 | annotation, 13 | addProps = true, 14 | }) { 15 | const node = fallbackResolve(value, tree); 16 | if (node && addProps) { 17 | addPropsToNode(node, annotation); 18 | } 19 | return node; 20 | } 21 | 22 | /** 23 | * 24 | * @param {string} path - the xpath string 25 | * @param {*} root 26 | */ 27 | function fallbackResolve(path, root) { 28 | const steps = path.split("/"); 29 | let node = root; 30 | while (node) { 31 | const step = steps.shift(); 32 | if (step === undefined) break; 33 | if (step === ".") continue; 34 | // eslint-disable-next-line 35 | let [name, xpathPosition] = step.split(/[\[\]]/); // prettier-ignore 36 | name = name.replace("_default_:", ""); 37 | const position = xpathPosition ? parseInt(xpathPosition) : 1; 38 | node = findChild(node, name, position); 39 | } 40 | return node; 41 | } 42 | function findChild(node, name, position) { 43 | const parent = node; 44 | while (node) { 45 | if (nodeName(node) === name && --position === 0) break; 46 | node = nextNode(node, parent); 47 | } 48 | return node; 49 | } 50 | 51 | function nextNode(node, parent) { 52 | const index = parent.children.indexOf(node); 53 | if (parent.children[index + 1]) { 54 | return parent.children[index + 1]; 55 | } else { 56 | return null; 57 | } 58 | } 59 | 60 | function nodeName(node) { 61 | if (node.tagName) { 62 | return node.tagName.toLowerCase(); 63 | } 64 | return ""; 65 | } 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { matchAnnotations } from "./hast-annotations-match/index.js"; 2 | 3 | export function attacher(options) { 4 | return transformer; 5 | function transformer(node, file) { 6 | return matchAnnotations(node, file, options); 7 | } 8 | } 9 | 10 | export default attacher; 11 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | module.exports = function log(path) { 2 | return function (...args) { 3 | if (process.env.NODE_ENV !== "production") { 4 | console.log(`${path}: `, ...args); 5 | } 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rehype-annotate", 3 | "version": "1.0.0", 4 | "description": "ReHype plugin to match W3C Web Annotations to source locations", 5 | "license": "Apache-2.0", 6 | "type": "module", 7 | "scripts": { 8 | "lint": "tsc --module es2020 --checkJs --allowJs --noEmit --lib es2015 --moduleResolution node index.js && healthier && prettier --write --plugin-search-dir=. '**/*.{js,json,css,yml,svelte}'", 9 | "pretest": "npm run lint", 10 | "prepublishOnly": "npm run lint", 11 | "test": "c8 tap --no-cov tests/index.js && c8 report --reporter=lcov", 12 | "snapshots": "TAP_SNAPSHOT=1 npm run test" 13 | }, 14 | "engines": { 15 | "node": ">14.0.0" 16 | }, 17 | "healthier": { 18 | "ignore": [ 19 | "__sapper__", 20 | "rollup.config.js" 21 | ] 22 | }, 23 | "dependencies": { 24 | "@types/node": "^16.0.1", 25 | "aria-attributes": "^2.0.0", 26 | "deepmerge": "^4.2.2", 27 | "diff-match-patch": "^1.0.5", 28 | "glob": "^7.1.7", 29 | "hast-util-sanitize": "^4.0.0", 30 | "hast-util-select": "^5.0.0", 31 | "hast-util-to-string": "^1.0.4", 32 | "hastscript": "^7.0.1", 33 | "html-element-attributes": "^3.0.0", 34 | "property-information": "^6.0.1", 35 | "rehype-parse": "^7.0.1", 36 | "rehype-raw": "^5.1.0", 37 | "rehype-sanitize": "^4.0.0", 38 | "rehype-stringify": "^8.0.0", 39 | "remark-parse": "^9.0.0", 40 | "remark-rehype": "^8.1.0", 41 | "string-pixel-width": "^1.10.0", 42 | "svg-element-attributes": "^2.0.0", 43 | "to-vfile": "^7.1.0", 44 | "unified": "^9.2.1", 45 | "unist-util-is": "^5.1.0", 46 | "unist-util-visit": "^3.1.0", 47 | "unist-util-visit-parents": "^4.1.1", 48 | "universal-base64url": "^1.1.0", 49 | "vfile-reporter": "^7.0.1" 50 | }, 51 | "devDependencies": { 52 | "c8": "^7.7.3", 53 | "healthier": "^4.0.0", 54 | "prettier": "^2.3.2", 55 | "tap": "^15.0.9", 56 | "typescript": "^4.3.5" 57 | }, 58 | "private": true 59 | } 60 | -------------------------------------------------------------------------------- /tests/fixtures/annotations-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "http://www.w3.org/ns/anno.jsonld", 3 | "id": "http://example.org/collection1", 4 | "type": "AnnotationCollection", 5 | "label": "Annotations Collection", 6 | "total": 2, 7 | "first": { 8 | "id": "http://example.org/page1", 9 | "type": "AnnotationPage", 10 | "label": "Annotations Page", 11 | "items": [ 12 | { 13 | "id": "http://example.com/annotations1", 14 | "type": "Annotation", 15 | "motivation": "bookmarking", 16 | "creator": { 17 | "id": "http://example.org/user1", 18 | "type": "Person", 19 | "name": "A. Person", 20 | "nickname": "user1" 21 | }, 22 | "created": "2015-10-13T13:00:00Z", 23 | "stylesheet": { 24 | "id": "http://example.org/stylesheet1", 25 | "type": "CssStylesheet" 26 | }, 27 | "target": { 28 | "source": "http://example.com/document1", 29 | "styleClass": "Bookmark", 30 | "selector": { 31 | "type": "CssSelector", 32 | "value": "h3:first-of-type" 33 | } 34 | } 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/fixtures/css-single.annotations.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://example.org/page1", 3 | "type": "AnnotationPage", 4 | "label": "Annotations Page", 5 | "items": [ 6 | { 7 | "id": "http://example.com/annotations1", 8 | "type": "Annotation", 9 | "motivation": "bookmarking", 10 | "creator": { 11 | "id": "http://example.org/user1", 12 | "type": "Person", 13 | "name": "A. Person", 14 | "nickname": "user1" 15 | }, 16 | "created": "2015-10-13T13:00:00Z", 17 | "stylesheet": { 18 | "id": "http://example.org/stylesheet1", 19 | "type": "CssStylesheet" 20 | }, 21 | "target": { 22 | "source": "https://example.com/tests/fixtures/css-single.input.html", 23 | "selector": { 24 | "type": "CssSelector", 25 | "value": "h3:first-of-type" 26 | } 27 | } 28 | }, 29 | { 30 | "id": "http://example.com/annotations1", 31 | "type": "Annotation", 32 | "motivation": "bookmarking", 33 | "creator": { 34 | "id": "http://example.org/user1", 35 | "type": "Person", 36 | "name": "A. Person", 37 | "nickname": "user1" 38 | }, 39 | "created": "2015-10-13T13:00:00Z", 40 | "stylesheet": { 41 | "id": "http://example.org/stylesheet1", 42 | "type": "CssStylesheet" 43 | }, 44 | "target": { 45 | "source": "https://example.com/tests/fixtures/test-that-unmatched-sources-are-not-processed", 46 | "selector": { 47 | "type": "CssSelector", 48 | "value": "h3:first-of-type" 49 | } 50 | } 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /tests/fixtures/css-single.input.html: -------------------------------------------------------------------------------- 1 | 2 |Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
6 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
8 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
10 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
26 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
42 |46 | | Defeated | 47 |Concludes | 48 |Ceremonial | 49 |Hang | 50 |Rue | 51 |
---|---|---|---|---|---|
Invitations | 56 |branch | 57 |firm | 58 |evil | 59 |grumbling | 60 |dressed | 61 |
Relight | 64 |wearied | 65 |ignoring | 66 |came | 67 |safety | 68 |give | 69 |
Profit | 72 |linger | 73 |else | 74 |enemy's | 75 |fitted | 76 |motion | 77 |
Warrior | 80 |gathered | 81 |any | 82 |secure | 83 |daylight | 84 |razor-sharp | 85 |
Racket | 88 |travel | 89 |astride | 90 |firm | 91 |courage | 92 |named | 93 |
Long-forgotten | 96 |frightening | 97 |stayed | 98 |change | 99 |outer | 100 |evacuate | 101 |
pony | 106 |pauses | 107 |board | 108 |bite | 109 |again | 110 |up | 111 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
116 |117 |120 |You shall be the Fellowship of the Ring.
118 | 119 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
122 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!123 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
5 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
7 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
9 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
25 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
41 |45 | | Defeated | 46 |Concludes | 47 |Ceremonial | 48 |Hang | 49 |Rue | 50 |
---|---|---|---|---|---|
Invitations | 55 |branch | 56 |firm | 57 |evil | 58 |grumbling | 59 |dressed | 60 |
Relight | 63 |wearied | 64 |ignoring | 65 |came | 66 |safety | 67 |give | 68 |
Profit | 71 |linger | 72 |else | 73 |enemy's | 74 |fitted | 75 |motion | 76 |
Warrior | 79 |gathered | 80 |any | 81 |secure | 82 |daylight | 83 |razor-sharp | 84 |
Racket | 87 |travel | 88 |astride | 89 |firm | 90 |courage | 91 |named | 92 |
Long-forgotten | 95 |frightening | 96 |stayed | 97 |change | 98 |outer | 99 |evacuate | 100 |
pony | 105 |pauses | 106 |board | 107 |bite | 108 |again | 109 |up | 110 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
115 |116 |119 |You shall be the Fellowship of the Ring.
117 | 118 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
121 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!122 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
j'adore !
", 30 | "format": "text/html", 31 | "language": "fr", 32 | "purpose": "describing" 33 | } 34 | ], 35 | "target": { 36 | "source": "https://example.com/tests/fixtures/fragment-multibody.input.html", 37 | "styleClass": "Bookmarked", 38 | "selector": { 39 | "type": "FragmentSelector", 40 | "value": "test-id" 41 | } 42 | } 43 | }, 44 | { 45 | "id": "http://example.com/annotations2-does-not-exist", 46 | "type": "Annotation", 47 | "motivation": "bookmarking", 48 | "creator": { 49 | "id": "http://example.org/user1", 50 | "type": "Person", 51 | "name": "A. Person", 52 | "nickname": "user1" 53 | }, 54 | "created": "2015-10-13T13:00:00Z", 55 | "stylesheet": { 56 | "id": "http://example.org/stylesheet1", 57 | "type": "CssStylesheet" 58 | }, 59 | "body": [ 60 | { 61 | "type": "TextualBody", 62 | "purpose": "tagging", 63 | "value": "love" 64 | }, 65 | { 66 | "type": "TextualBody", 67 | "value": "j'adore !
", 68 | "format": "text/html", 69 | "language": "fr", 70 | "purpose": "describing" 71 | } 72 | ], 73 | "target": { 74 | "source": "https://example.com/tests/fixtures/fragment-multibody.input.html", 75 | "styleClass": "Bookmarked", 76 | "selector": { 77 | "type": "FragmentSelector", 78 | "value": "this-id-does-not-exist-in-the-text" 79 | } 80 | } 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /tests/fixtures/fragment-multibody.input.html: -------------------------------------------------------------------------------- 1 | 2 |Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
6 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
8 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
10 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
26 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
42 |46 | | Defeated | 47 |Concludes | 48 |Ceremonial | 49 |Hang | 50 |Rue | 51 |
---|---|---|---|---|---|
Invitations | 56 |branch | 57 |firm | 58 |evil | 59 |grumbling | 60 |dressed | 61 |
Relight | 64 |wearied | 65 |ignoring | 66 |came | 67 |safety | 68 |give | 69 |
Profit | 72 |linger | 73 |else | 74 |enemy's | 75 |fitted | 76 |motion | 77 |
Warrior | 80 |gathered | 81 |any | 82 |secure | 83 |daylight | 84 |razor-sharp | 85 |
Racket | 88 |travel | 89 |astride | 90 |firm | 91 |courage | 92 |named | 93 |
Long-forgotten | 96 |frightening | 97 |stayed | 98 |change | 99 |outer | 100 |evacuate | 101 |
pony | 106 |pauses | 107 |board | 108 |bite | 109 |again | 110 |up | 111 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
116 |117 |120 |You shall be the Fellowship of the Ring.
118 | 119 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
122 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!123 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
5 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
7 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
9 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
25 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
41 |45 | | Defeated | 46 |Concludes | 47 |Ceremonial | 48 |Hang | 49 |Rue | 50 |
---|---|---|---|---|---|
Invitations | 55 |branch | 56 |firm | 57 |evil | 58 |grumbling | 59 |dressed | 60 |
Relight | 63 |wearied | 64 |ignoring | 65 |came | 66 |safety | 67 |give | 68 |
Profit | 71 |linger | 72 |else | 73 |enemy's | 74 |fitted | 75 |motion | 76 |
Warrior | 79 |gathered | 80 |any | 81 |secure | 82 |daylight | 83 |razor-sharp | 84 |
Racket | 87 |travel | 88 |astride | 89 |firm | 90 |courage | 91 |named | 92 |
Long-forgotten | 95 |frightening | 96 |stayed | 97 |change | 98 |outer | 99 |evacuate | 100 |
pony | 105 |pauses | 106 |board | 107 |bite | 108 |again | 109 |up | 110 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
115 |116 |119 |You shall be the Fellowship of the Ring.
117 | 118 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
121 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!122 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
6 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
8 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
10 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
26 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
42 |46 | | Defeated | 47 |Concludes | 48 |Ceremonial | 49 |Hang | 50 |Rue | 51 |
---|---|---|---|---|---|
Invitations | 56 |branch | 57 |firm | 58 |evil | 59 |grumbling | 60 |dressed | 61 |
Relight | 64 |wearied | 65 |ignoring | 66 |came | 67 |safety | 68 |give | 69 |
Profit | 72 |linger | 73 |else | 74 |enemy's | 75 |fitted | 76 |motion | 77 |
Warrior | 80 |gathered | 81 |any | 82 |secure | 83 |daylight | 84 |razor-sharp | 85 |
Racket | 88 |travel | 89 |astride | 90 |firm | 91 |courage | 92 |named | 93 |
Long-forgotten | 96 |frightening | 97 |stayed | 98 |change | 99 |outer | 100 |evacuate | 101 |
pony | 106 |pauses | 107 |board | 108 |bite | 109 |again | 110 |up | 111 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
116 |117 |120 |You shall be the Fellowship of the Ring.
118 | 119 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
122 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!123 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
5 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
7 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
9 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
25 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
41 |45 | | Defeated | 46 |Concludes | 47 |Ceremonial | 48 |Hang | 49 |Rue | 50 |
---|---|---|---|---|---|
Invitations | 55 |branch | 56 |firm | 57 |evil | 58 |grumbling | 59 |dressed | 60 |
Relight | 63 |wearied | 64 |ignoring | 65 |came | 66 |safety | 67 |give | 68 |
Profit | 71 |linger | 72 |else | 73 |enemy's | 74 |fitted | 75 |motion | 76 |
Warrior | 79 |gathered | 80 |any | 81 |secure | 82 |daylight | 83 |razor-sharp | 84 |
Racket | 87 |travel | 88 |astride | 89 |firm | 90 |courage | 91 |named | 92 |
Long-forgotten | 95 |frightening | 96 |stayed | 97 |change | 98 |outer | 99 |evacuate | 100 |
pony | 105 |pauses | 106 |board | 107 |bite | 108 |again | 109 |up | 110 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
115 |116 |119 |You shall be the Fellowship of the Ring.
117 | 118 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
121 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!122 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
j'adore !
", 15 | "format": "text/html", 16 | "language": "fr", 17 | "purpose": "commenting" 18 | } 19 | ], 20 | "target": { 21 | "source": "https://example.com/tests/fixtures/refinedby.input.html", 22 | "styleClass": "Bookmarked", 23 | "selector": { 24 | "type": "CssSelector", 25 | "value": "h3:first-of-type", 26 | "refinedBy": { 27 | "type": "TextPositionSelector", 28 | "start": 0, 29 | "end": 12 30 | } 31 | } 32 | } 33 | }, 34 | { 35 | "id": "http://example.com/annotations2", 36 | "type": "Annotation", 37 | "motivation": "highlighting", 38 | "created": "2015-10-13T13:00:00Z", 39 | "body": [ 40 | { 41 | "type": "TextualBody", 42 | "value": "j'adore !
", 43 | "format": "text/html", 44 | "language": "fr", 45 | "purpose": "commenting" 46 | } 47 | ], 48 | "target": { 49 | "source": "https://example.com/tests/fixtures/refinedby.input.html", 50 | "styleClass": "Bookmarked", 51 | "selector": { 52 | "type": "XPathSelector", 53 | "value": "/html[1]/body[1]/h1[1]", 54 | "refinedBy": { 55 | "type": "TextQuoteSelector", 56 | "exact": "entire bygone figure" 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | "id": "http://example.com/annotations3-no-refine-match", 63 | "type": "Annotation", 64 | "motivation": "highlighting", 65 | "created": "2015-10-13T13:00:00Z", 66 | "body": [ 67 | { 68 | "type": "TextualBody", 69 | "value": "j'adore !
", 70 | "format": "text/html", 71 | "language": "fr", 72 | "purpose": "commenting" 73 | } 74 | ], 75 | "target": { 76 | "source": "https://example.com/tests/fixtures/refinedby.input.html", 77 | "styleClass": "Bookmarked", 78 | "selector": { 79 | "type": "CssSelector", 80 | "value": "ul:first-of-type", 81 | "refinedBy": { 82 | "type": "CssSelector", 83 | "value": "li:first-of-type" 84 | } 85 | } 86 | } 87 | } 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /tests/fixtures/refinedby.input.html: -------------------------------------------------------------------------------- 1 | 2 |Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
6 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
8 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
10 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
26 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
42 |46 | | Defeated | 47 |Concludes | 48 |Ceremonial | 49 |Hang | 50 |Rue | 51 |
---|---|---|---|---|---|
Invitations | 56 |branch | 57 |firm | 58 |evil | 59 |grumbling | 60 |dressed | 61 |
Relight | 64 |wearied | 65 |ignoring | 66 |came | 67 |safety | 68 |give | 69 |
Profit | 72 |linger | 73 |else | 74 |enemy's | 75 |fitted | 76 |motion | 77 |
Warrior | 80 |gathered | 81 |any | 82 |secure | 83 |daylight | 84 |razor-sharp | 85 |
Racket | 88 |travel | 89 |astride | 90 |firm | 91 |courage | 92 |named | 93 |
Long-forgotten | 96 |frightening | 97 |stayed | 98 |change | 99 |outer | 100 |evacuate | 101 |
pony | 106 |pauses | 107 |board | 108 |bite | 109 |again | 110 |up | 111 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
116 |117 |120 |You shall be the Fellowship of the Ring.
118 | 119 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
122 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!123 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
5 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
7 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
9 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
25 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
41 |45 | | Defeated | 46 |Concludes | 47 |Ceremonial | 48 |Hang | 49 |Rue | 50 |
---|---|---|---|---|---|
Invitations | 55 |branch | 56 |firm | 57 |evil | 58 |grumbling | 59 |dressed | 60 |
Relight | 63 |wearied | 64 |ignoring | 65 |came | 66 |safety | 67 |give | 68 |
Profit | 71 |linger | 72 |else | 73 |enemy's | 74 |fitted | 75 |motion | 76 |
Warrior | 79 |gathered | 80 |any | 81 |secure | 82 |daylight | 83 |razor-sharp | 84 |
Racket | 87 |travel | 88 |astride | 89 |firm | 90 |courage | 91 |named | 92 |
Long-forgotten | 95 |frightening | 96 |stayed | 97 |change | 98 |outer | 99 |evacuate | 100 |
pony | 105 |pauses | 106 |board | 107 |bite | 108 |again | 109 |up | 110 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
115 |116 |119 |You shall be the Fellowship of the Ring.
117 | 118 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
121 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!122 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
j'adore !
", 15 | "format": "text/html", 16 | "language": "fr", 17 | "purpose": "commenting" 18 | } 19 | ], 20 | "target": { 21 | "source": "https://example.com/tests/fixtures/text-position.input.html", 22 | "styleClass": "Bookmarked", 23 | "selector": { 24 | "type": "TextPositionSelector", 25 | "start": 5, 26 | "end": 6 27 | } 28 | } 29 | }, 30 | { 31 | "id": "http://example.com/annotations1", 32 | "type": "Annotation", 33 | "motivation": "highlighting", 34 | "created": "2015-10-13T13:00:00Z", 35 | "body": [ 36 | { 37 | "type": "TextualBody", 38 | "value": "j'adore !
", 39 | "format": "text/html", 40 | "language": "fr", 41 | "purpose": "commenting" 42 | } 43 | ], 44 | "target": { 45 | "source": "https://example.com/tests/fixtures/text-position.input.html", 46 | "styleClass": "Bookmarked", 47 | "selector": { 48 | "type": "TextPositionSelector", 49 | "start": 176, 50 | "end": 368 51 | } 52 | } 53 | }, 54 | { 55 | "id": "http://example.com/annotations2", 56 | "type": "Annotation", 57 | "motivation": "highlighting", 58 | "created": "2015-10-13T13:00:00Z", 59 | "body": [ 60 | { 61 | "type": "TextualBody", 62 | "value": "j'adore !
", 63 | "format": "text/html", 64 | "language": "fr", 65 | "purpose": "commenting" 66 | } 67 | ], 68 | "target": { 69 | "source": "https://example.com/tests/fixtures/text-position.input.html", 70 | "styleClass": "Bookmarked", 71 | "selector": { 72 | "type": "TextPositionSelector", 73 | "start": 559, 74 | "end": 586 75 | } 76 | } 77 | }, 78 | { 79 | "id": "http://example.com/annotations3", 80 | "type": "Annotation", 81 | "motivation": "highlighting", 82 | "created": "2015-10-13T13:00:00Z", 83 | "body": [ 84 | { 85 | "type": "TextualBody", 86 | "value": "j'adore !
", 87 | "format": "text/html", 88 | "language": "fr", 89 | "purpose": "commenting" 90 | } 91 | ], 92 | "target": { 93 | "source": "https://example.com/tests/fixtures/text-position.input.html", 94 | "styleClass": "Bookmarked", 95 | "selector": { 96 | "type": "TextPositionSelector", 97 | "start": 1664, 98 | "end": 1684 99 | } 100 | } 101 | }, 102 | { 103 | "id": "http://example.com/annotations4", 104 | "type": "Annotation", 105 | "motivation": "highlighting", 106 | "created": "2015-10-13T13:00:00Z", 107 | "body": [ 108 | { 109 | "type": "TextualBody", 110 | "value": "j'adore !
", 111 | "format": "text/html", 112 | "language": "fr", 113 | "purpose": "commenting" 114 | } 115 | ], 116 | "target": { 117 | "source": "https://example.com/tests/fixtures/text-position.input.html", 118 | "styleClass": "Bookmarked", 119 | "selector": { 120 | "type": "TextPositionSelector", 121 | "start": 1779, 122 | "end": 1792 123 | } 124 | } 125 | } 126 | ] 127 | } 128 | -------------------------------------------------------------------------------- /tests/fixtures/text-position.input.html: -------------------------------------------------------------------------------- 1 | 2 |Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
6 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
8 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
10 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
26 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
42 |46 | | Defeated | 47 |Concludes | 48 |Ceremonial | 49 |Hang | 50 |Rue | 51 |
---|---|---|---|---|---|
Invitations | 56 |branch | 57 |firm | 58 |evil | 59 |grumbling | 60 |dressed | 61 |
Relight | 64 |wearied | 65 |ignoring | 66 |came | 67 |safety | 68 |give | 69 |
Profit | 72 |linger | 73 |else | 74 |enemy's | 75 |fitted | 76 |motion | 77 |
Warrior | 80 |gathered | 81 |any | 82 |secure | 83 |daylight | 84 |razor-sharp | 85 |
Racket | 88 |travel | 89 |astride | 90 |firm | 91 |courage | 92 |named | 93 |
Long-forgotten | 96 |frightening | 97 |stayed | 98 |change | 99 |outer | 100 |evacuate | 101 |
pony | 106 |pauses | 107 |board | 108 |bite | 109 |again | 110 |up | 111 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
116 |117 |120 |You shall be the Fellowship of the Ring.
118 | 119 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
122 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!123 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
5 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
7 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
9 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
25 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
41 |45 | | Defeated | 46 |Concludes | 47 |Ceremonial | 48 |Hang | 49 |Rue | 50 |
---|---|---|---|---|---|
Invitations | 55 |branch | 56 |firm | 57 |evil | 58 |grumbling | 59 |dressed | 60 |
Relight | 63 |wearied | 64 |ignoring | 65 |came | 66 |safety | 67 |give | 68 |
Profit | 71 |linger | 72 |else | 73 |enemy's | 74 |fitted | 75 |motion | 76 |
Warrior | 79 |gathered | 80 |any | 81 |secure | 82 |daylight | 83 |razor-sharp | 84 |
Racket | 87 |travel | 88 |astride | 89 |firm | 90 |courage | 91 |named | 92 |
Long-forgotten | 95 |frightening | 96 |stayed | 97 |change | 98 |outer | 99 |evacuate | 100 |
pony | 105 |pauses | 106 |board | 107 |bite | 108 |again | 109 |up | 110 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
115 |116 |119 |You shall be the Fellowship of the Ring.
117 | 118 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
121 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!122 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
j'adore !
", 14 | "format": "text/html", 15 | "language": "fr", 16 | "purpose": "commenting" 17 | } 18 | ], 19 | "target": { 20 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 21 | "styleClass": "Bookmarked", 22 | "selector": { 23 | "type": "TextQuoteSelector", 24 | "prefix": "well-earned machine?\n", 25 | "exact": "Skins fouler Rhosgobel you've about values blingon heirloom?", 26 | "suffix": "\nFeeling panic" 27 | } 28 | } 29 | }, 30 | { 31 | "id": "http://example.com/annotations2-inexact", 32 | "type": "Annotation", 33 | "motivation": "highlighting", 34 | "created": "2015-10-13T13:00:00Z", 35 | "body": [ 36 | { 37 | "type": "TextualBody", 38 | "value": "\n\nj'adore !\n\n", 39 | "format": "text/markdown", 40 | "language": "fr", 41 | "purpose": "commenting" 42 | } 43 | ], 44 | "target": { 45 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 46 | "styleClass": "Bookmarked", 47 | "selector": { 48 | "type": "TextQuoteSelector", 49 | "prefix": "bogomips", 50 | "exact": "quarry Bilbo captive", 51 | "suffix": "bogomips" 52 | } 53 | } 54 | }, 55 | { 56 | "id": "http://example.com/annotations1-non-existent", 57 | "type": "Annotation", 58 | "motivation": "highlighting", 59 | "created": "2015-10-13T13:00:00Z", 60 | "body": [ 61 | { 62 | "type": "TextualBody", 63 | "value": "j'adore !
", 64 | "format": "text/html", 65 | "language": "fr", 66 | "purpose": "commenting" 67 | } 68 | ], 69 | "target": { 70 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 71 | "styleClass": "Bookmarked", 72 | "selector": { 73 | "type": "TextQuoteSelector", 74 | "exact": "gaudeamus igitur" 75 | } 76 | } 77 | }, 78 | { 79 | "id": "http://example.com/annotations2-non-existent", 80 | "type": "Annotation", 81 | "motivation": "highlighting", 82 | "created": "2015-10-13T13:00:00Z", 83 | "body": [ 84 | { 85 | "type": "TextualBody", 86 | "value": "j'adore !
", 87 | "format": "text/html", 88 | "language": "fr", 89 | "purpose": "commenting" 90 | } 91 | ], 92 | "target": { 93 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 94 | "styleClass": "Bookmarked", 95 | "selector": { 96 | "type": "TextQuoteSelector", 97 | "exact": "Sweeps there's nightshade Gandalyabba dabba dooo" 98 | } 99 | } 100 | }, 101 | { 102 | "id": "http://example.com/annotations3-non-existent", 103 | "type": "Annotation", 104 | "motivation": "highlighting", 105 | "created": "2015-10-13T13:00:00Z", 106 | "body": [ 107 | { 108 | "type": "TextualBody", 109 | "value": "j'adore !
", 110 | "format": "text/html", 111 | "language": "fr", 112 | "purpose": "commenting" 113 | } 114 | ], 115 | "target": { 116 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 117 | "styleClass": "Bookmarked", 118 | "selector": { 119 | "type": "TextQuoteSelector", 120 | "exact": "Example Use Case: Megan has very limited ability to hear sound, and prefers to read captions when interacting with videos. She uses her annotation client to make a comment on such a video, and to help others in the same situation, the client includes that the video has this accessibility feature." 121 | } 122 | } 123 | }, 124 | { 125 | "id": "http://example.com/annotations4-non-existent", 126 | "type": "Annotation", 127 | "motivation": "highlighting", 128 | "created": "2015-10-13T13:00:00Z", 129 | "body": [ 130 | { 131 | "type": "TextualBody", 132 | "value": "j'adore !
", 133 | "format": "text/html", 134 | "language": "fr", 135 | "purpose": "commenting" 136 | } 137 | ], 138 | "target": { 139 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 140 | "styleClass": "Bookmarked", 141 | "selector": { 142 | "type": "TextQuoteSelector", 143 | "exact": "Flavor balls Tilda rolls nab beat stammer. *** DOES NOT MATCH*** Ropes ash though guarding Thrain Bucklebury Kili's Ranger? Reaches W wit father's Dwarf-city. *** DOES NOT MATCH*** Mouse dwelt regained lippy Noldorin." 144 | } 145 | } 146 | }, 147 | { 148 | "id": "http://example.com/annotations1", 149 | "type": "Annotation", 150 | "motivation": "highlighting", 151 | "created": "2015-10-13T13:00:00Z", 152 | "body": [ 153 | { 154 | "type": "TextualBody", 155 | "value": "j'adore !
", 156 | "format": "text/plain", 157 | "language": "fr", 158 | "purpose": "commenting" 159 | } 160 | ], 161 | "target": { 162 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 163 | "styleClass": "Bookmarked", 164 | "selector": { 165 | "type": "TextQuoteSelector", 166 | "exact": "pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.\nStirs ending exceeding fond muster fall Bagshot.\nResilient Garulf key quest abandon knives" 167 | } 168 | } 169 | }, 170 | { 171 | "id": "http://example.com/annotations2", 172 | "type": "Annotation", 173 | "motivation": "highlighting", 174 | "created": "2015-10-13T13:00:00Z", 175 | "body": [ 176 | { 177 | "type": "TextualBody", 178 | "value": "j'adore !
", 179 | "format": "text/html", 180 | "language": "fr", 181 | "purpose": "commenting" 182 | } 183 | ], 184 | "target": { 185 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 186 | "styleClass": "Bookmarked", 187 | "selector": { 188 | "type": "TextQuoteSelector", 189 | "exact": " tobacco chicken ridiculous", 190 | "prefix": "World bodies gifted" 191 | } 192 | } 193 | }, 194 | { 195 | "id": "http://example.com/annotations3", 196 | "type": "Annotation", 197 | "motivation": "highlighting", 198 | "created": "2015-10-13T13:00:00Z", 199 | "body": [ 200 | { 201 | "type": "TextualBody", 202 | "value": "j'adore !
", 203 | "format": "text/html", 204 | "language": "fr", 205 | "purpose": "commenting" 206 | } 207 | ], 208 | "target": { 209 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 210 | "styleClass": "Bookmarked", 211 | "selector": { 212 | "type": "TextQuoteSelector", 213 | "exact": "chap. Big grey beard", 214 | "prefix": "Elderly ", 215 | "suffix": ", pointy hat." 216 | } 217 | } 218 | }, 219 | { 220 | "id": "http://example.com/annotations4", 221 | "type": "Annotation", 222 | "motivation": "highlighting", 223 | "created": "2015-10-13T13:00:00Z", 224 | "body": [ 225 | { 226 | "type": "TextualBody", 227 | "value": "I adore!
", 228 | "format": "text/html" 229 | } 230 | ], 231 | "target": { 232 | "source": "https://example.com/tests/fixtures/text-quote.input.html", 233 | "styleClass": "Bookmarked", 234 | "selector": { 235 | "type": "TextQuoteSelector", 236 | "exact": "pon windlance", 237 | "suffix": ". Didn't stage" 238 | } 239 | } 240 | } 241 | ] 242 | } 243 | -------------------------------------------------------------------------------- /tests/fixtures/text-quote.input.html: -------------------------------------------------------------------------------- 1 | 2 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
8 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
10 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
26 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
42 |46 | | Defeated | 47 |Concludes | 48 |Ceremonial | 49 |Hang | 50 |Rue | 51 |
---|---|---|---|---|---|
Invitations | 56 |branch | 57 |firm | 58 |evil | 59 |grumbling | 60 |dressed | 61 |
Relight | 64 |wearied | 65 |ignoring | 66 |came | 67 |safety | 68 |give | 69 |
Profit | 72 |linger | 73 |else | 74 |enemy's | 75 |fitted | 76 |motion | 77 |
Warrior | 80 |gathered | 81 |any | 82 |secure | 83 |daylight | 84 |razor-sharp | 85 |
Racket | 88 |travel | 89 |astride | 90 |firm | 91 |courage | 92 |named | 93 |
Long-forgotten | 96 |frightening | 97 |stayed | 98 |change | 99 |outer | 100 |evacuate | 101 |
pony | 106 |pauses | 107 |board | 108 |bite | 109 |again | 110 |up | 111 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
116 |117 |120 |You shall be the Fellowship of the Ring.
118 | 119 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
122 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!123 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
6 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
8 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
10 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
26 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
42 |46 | | Defeated | 47 |Concludes | 48 |Ceremonial | 49 |Hang | 50 |Rue | 51 |
---|---|---|---|---|---|
Invitations | 56 |branch | 57 |firm | 58 |evil | 59 |grumbling | 60 |dressed | 61 |
Relight | 64 |wearied | 65 |ignoring | 66 |came | 67 |safety | 68 |give | 69 |
Profit | 72 |linger | 73 |else | 74 |enemy's | 75 |fitted | 76 |motion | 77 |
Warrior | 80 |gathered | 81 |any | 82 |secure | 83 |daylight | 84 |razor-sharp | 85 |
Racket | 88 |travel | 89 |astride | 90 |firm | 91 |courage | 92 |named | 93 |
Long-forgotten | 96 |frightening | 97 |stayed | 98 |change | 99 |outer | 100 |evacuate | 101 |
pony | 106 |pauses | 107 |board | 108 |bite | 109 |again | 110 |up | 111 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
116 |117 |120 |You shall be the Fellowship of the Ring.
118 | 119 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
122 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!123 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.
Hinges lingered Orthanc raid vast tune. Were-worms expert thrush spilt condone terrible makes finds recover come pottery on? It must be taken deep into Mordor and cast back into the fiery chasm from whence it came.
5 |Resilient Garulf key quest abandon knives lifted niceties tonight disappeared strongest plates. Farthing ginger large. Nobody tosses a Dwarf. Makes Shadowfax nearly lesser south deceive hates 22nd missing others!
7 |Forces group relative five her Gamgee master lift river-folk pale. Swords are no more use here. Seeks hanging unpredictable Sam fair.
9 |Rest peace enemy overlook liege person Gibbets year afford. Let the Ring-bearer decide. Gentlemen put shot.
25 |Elderly chap. Big grey beard, pointy hat. Smallest meadow corrupt Khazad-dum cave dragons children taste wriggling pouring pon windlance. Didn't stage Muil to juice foundations fouler two frightened rightfully hoot.
41 |45 | | Defeated | 46 |Concludes | 47 |Ceremonial | 48 |Hang | 49 |Rue | 50 |
---|---|---|---|---|---|
Invitations | 55 |branch | 56 |firm | 57 |evil | 58 |grumbling | 59 |dressed | 60 |
Relight | 63 |wearied | 64 |ignoring | 65 |came | 66 |safety | 67 |give | 68 |
Profit | 71 |linger | 72 |else | 73 |enemy's | 74 |fitted | 75 |motion | 76 |
Warrior | 79 |gathered | 80 |any | 81 |secure | 82 |daylight | 83 |razor-sharp | 84 |
Racket | 87 |travel | 88 |astride | 89 |firm | 90 |courage | 91 |named | 92 |
Long-forgotten | 95 |frightening | 96 |stayed | 97 |change | 98 |outer | 99 |evacuate | 100 |
pony | 105 |pauses | 106 |board | 107 |bite | 108 |again | 109 |up | 110 |
Way slinker snake sakes Luthien grasped reasonable! You cannot hide. I see you. There is no life in the void. Only death. Seduced avalanche anyone keeper Elf-maiden!
115 |116 |119 |You shall be the Fellowship of the Ring.
117 | 118 |
Maps Treebeard ranges breached strong forth malcontent reads eat birthright. Gandalf's death was not in vain. Nor would he have you give up hope. Farthing unfriendly tracked crawl whose bearer instance!
121 |Paths dagger rockets Cirith. Barter sailed ruse advantage sun's Garulf? Brew corks Argonath trumpets seem. Fled servant doubting winter-thickets commoners Azog!122 |
Sweeps there's nightshade Gandalf the Grey dragons guarding? Thrice delays boat speed Dwarf-city souls filth. Shattered Orthanc fiery victory broken choice. Flavor balls Tilda rolls nab beat stammer. Ropes ash though guarding Thrain Bucklebury Kili's Ranger
? Reaches W wit father's Dwarf-city. Mouse dwelt regained Noldorin. Selfish clearing well Goblin-men gloom. Goblin-town rests brown win throne act otherwise. Tune worry remove deceive Eorlingas. Guard care failed kept unoccupied lowly Sméagol distances brown. Golf coast either Minas Morgul Elfling Radagast Isildur's serpent. Mere chance craftsmen Uruk-hai cracked risked lord's exchanged depend
. Bracegirdles sowing loud girl jacketses. Deceit cheat tender behaving brace coneys 600 dragon's Guldur amazing industry. Asking Samwise cooking imaginable keen brewing ales prisoner. Prize spit thumping easy Radagast pass. One's o'clock 24th delayed sell Gorgoroth mushrooms.