├── .gitignore ├── LICENSE ├── README.md ├── comment-out-imports.py ├── iframe.html ├── iframe.ts ├── index.html ├── index.ts ├── make.sh ├── server.py ├── tsconfig.json ├── use-debug-lib.py └── use-production-lib.py /.gitignore: -------------------------------------------------------------------------------- 1 | index.js 2 | iframe.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {2019} {Jon Udell} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## View and export Hypothesis annotations 2 | 3 | Use remotely at https://jonudell.info/h/facet 4 | 5 | Or clone and rehost elsewhere 6 | 7 | Uses [hlib](https://github.com/judell/hlib) 8 | 9 | Uses [showdown](https://github.com/showdownjs/showdown) 10 | 11 | More Hypothesis tools 12 | 13 | -------------------------------------------------------------------------------- /comment-out-imports.py: -------------------------------------------------------------------------------- 1 | filenames = [ 2 | 'index.js', 3 | 'iframe.js', 4 | ] 5 | 6 | for filename in filenames: 7 | with open(filename, 'r+') as f: 8 | text = f.read() 9 | text = text.replace('import *','// import *') 10 | f.seek(0) 11 | f.write(text) 12 | f.truncate() -------------------------------------------------------------------------------- /iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 105 | 106 | 107 | 108 | 109 | 110 |
111 | 112 |
113 | 114 |

115 |

116 |

117 | 118 |
119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /iframe.ts: -------------------------------------------------------------------------------- 1 | import * as hlib from '../hlib/hlib' // this will be commented out in the shipping bundle 2 | 3 | let params:any = decodeURIComponent(hlib.gup('params')) 4 | params = JSON.parse(params) 5 | 6 | const widget = hlib.getById('widget') as HTMLElement 7 | const controlsContainer = hlib.getById('controlsContainer') as HTMLElement 8 | 9 | const format = params.format 10 | delete params.format 11 | 12 | const sortBy = params.sortBy 13 | delete params.sortBy 14 | 15 | const iconColor = '#2c1409b5' 16 | const exportControlStyle = `style="display:inline; width:1.8em; height:1.8em; margin-left:1em; fill:${iconColor}"` 17 | const externalLinkStyle = `style="display:inline; width:.6em; height:.6em; margin-left:2px;margin-top:3px; fill:${iconColor}"` 18 | 19 | let htmlBuffer = '' 20 | 21 | const subjectUserTokens = hlib.getSubjectUserTokensFromLocalStorage() 22 | 23 | hlib.getById('svgDefs').outerHTML = hlib.svgIcons 24 | 25 | enum annoOrReplyCounterId { 26 | annoCount = 'annoCount', 27 | replyCount = 'replyCount' 28 | } 29 | 30 | enum counterDirection { 31 | up, 32 | down 33 | } 34 | 35 | Object.keys(params).forEach(function (key) { 36 | if (params[key] === '') { 37 | delete params[key] 38 | } 39 | if (params.group && params.group === 'all') { 40 | delete params.group 41 | } 42 | }) 43 | 44 | showParams() 45 | 46 | hlib.getById('progress').innerText = 'fetching annotations ' 47 | 48 | hlib.search(params, 'progress') 49 | .then( data => { 50 | processSearchResults(data[0], data[1]) 51 | }) 52 | .catch( _ => { 53 | alert(`Cannot search for those parameters: ${JSON.stringify(params)}`) 54 | }) 55 | 56 | function showParams() { 57 | let _params = Object.assign({}, params) 58 | const excluded = [ 59 | '_separate_replies', 60 | 'controlledTags', 61 | 'exactTagSearch', 62 | 'expanded', 63 | 'group', 64 | 'searchReplies', 65 | 'service', 66 | 'subjectUserTokens', 67 | ] 68 | excluded.forEach(param => { 69 | delete _params[param] 70 | }) 71 | let title = hlib.syntaxColorParams(_params, excluded) 72 | title = title.slice(0, -1) 73 | if (title) { 74 | hlib.getById('title').innerHTML += title 75 | } else { 76 | hlib.getById('title').style.display = 'none' 77 | } 78 | } 79 | 80 | function exactTagSearch(annos:any[]) { 81 | if (params.exactTagSearch==='false') { 82 | return annos 83 | } 84 | if (!params.tag) { 85 | return annos 86 | } 87 | const checkedAnnos:any[] = [] 88 | const queryTag = params.tag 89 | annos.forEach(anno => { 90 | const _tags = anno.tags.map(function(t:string) { return t.toLowerCase() }) 91 | if (_tags.indexOf(queryTag.toLowerCase()) > -1) { 92 | checkedAnnos.push(anno) 93 | } else { 94 | const counterId = anno.isReply ? annoOrReplyCounterId.replyCount : annoOrReplyCounterId.annoCount 95 | decrementAnnoOrReplyCount(counterId) 96 | } 97 | }) 98 | return checkedAnnos 99 | } 100 | 101 | async function processSearchResults (annoRows:any[], replyRows:any[]) { 102 | 103 | hlib.getById('title').innerHTML += ` 104 | annotations ${annoRows.length}, 105 | replies ${replyRows.length}` 106 | 107 | if ( annoRows.length == 0 && replyRows.length == 0 ) { 108 | hlib.getById('progress').innerText = '' 109 | hlib.getById('widget').innerHTML = ` 110 |

Nothing found for this query. 111 |

Please try removing or altering one or more filters. 112 | ` 113 | hlib.getById('widget').style.display= 'block' 114 | return 115 | } 116 | 117 | annoRows = exactTagSearch(annoRows) 118 | replyRows = exactTagSearch(replyRows) 119 | 120 | let csv = '' 121 | const json:any[] = [] 122 | const combined = annoRows.concat(replyRows) 123 | 124 | const gatheredResults = hlib.gatherAnnotationsByUrl(combined) 125 | const reversedUrls = reverseChronUrls(gatheredResults) 126 | 127 | let cardCounter = 0 128 | 129 | for (let i = 0; i < reversedUrls.length; i++ ) { 130 | const url = reversedUrls[i] 131 | await renderCardsForUrl(url) 132 | } 133 | 134 | styleWidget(csv, json) 135 | 136 | showToggleAndDownloadControls() 137 | 138 | if (format === 'html') { 139 | const expanded = isExpanded() 140 | if (expanded) { 141 | setExpanderCollapse() 142 | hlib.getById('expander').click() 143 | } else { 144 | hlib.collapseAll() 145 | } 146 | } 147 | 148 | widget.style.display = 'block' 149 | hlib.getById('progress').innerHTML = '' 150 | 151 | async function renderCardsForUrl(url: string) { 152 | cardCounter++ 153 | const resultsForUrl = gatheredResults.get(url) ! 154 | const annosForUrl: hlib.annotation[] = resultsForUrl.annos 155 | const repliesForUrl: hlib.annotation[] = resultsForUrl.replies 156 | const perUrlCount = annosForUrl.length + repliesForUrl.length 157 | if (format === 'html') { 158 | htmlBuffer += showUrlResults(cardCounter, 'widget', url, perUrlCount, resultsForUrl.title) 159 | } 160 | let cardsHTMLBuffer = '' 161 | let promises = missingReplyPromises(annosForUrl.concat(repliesForUrl)) 162 | promises = promises.map(p => p.catch( e => e )) 163 | let missingAnnoOrReplyResults:hlib.httpResponse[] = await Promise.all(promises) 164 | missingAnnoOrReplyResults.forEach(result => { 165 | if (result && result.status == 200 ) { 166 | const annoOrReply = hlib.parseAnnotation(JSON.parse(result.response)) 167 | if ( annoOrReply.isReply) { 168 | repliesForUrl.push(annoOrReply) 169 | adjustAnnoOrReplyCount(annoOrReplyCounterId.replyCount, counterDirection.up) 170 | } else { 171 | annosForUrl.push(annoOrReply) 172 | adjustAnnoOrReplyCount(annoOrReplyCounterId.annoCount, counterDirection.up) 173 | } 174 | } 175 | }) 176 | let all = organizeReplies(annosForUrl, repliesForUrl) 177 | all.forEach(anno => { 178 | let level = anno.isReply ? anno.refs.length : 0 179 | if (format === 'html') { 180 | const externalLinkIcon = renderIcon('icon-external-link', externalLinkStyle) 181 | const externalLink = `${externalLinkIcon}` 182 | let cardsHTML = hlib.showAnnotation( 183 | anno, 184 | level, 185 | { 186 | externalLink: externalLink, 187 | addQuoteContext: hlib.getSettings().addQuoteContext === 'true' 188 | }) 189 | if (params.any) { 190 | const regex = new RegExp(params.any, 'g') 191 | cardsHTML = cardsHTML.replace(regex, `${params.any}`) 192 | } 193 | cardsHTMLBuffer += cardsHTML 194 | } 195 | else if (format === 'csv') { 196 | let _row = document.createElement('div') 197 | _row.innerHTML = hlib.csvRow(level, anno) 198 | csv += _row.innerText + '\n' 199 | } 200 | else if (format === 'json') { 201 | anno.text = anno.text.replace(/[] { 209 | const allIds:string[] = all.map(function (anno: hlib.annotation) { 210 | return anno.id 211 | }) 212 | let refIds = [] as string[] 213 | all.forEach(function (anno: hlib.annotation) { 214 | anno.refs.forEach(refId =>{ 215 | if (refIds.indexOf(refId) < 0) { 216 | refIds.push(refId) 217 | } 218 | }) 219 | }) 220 | 221 | const promises = [] as Promise[] 222 | for (let refId of refIds) { 223 | if (allIds.indexOf(refId) < 0) { 224 | promises.push(hlib.getAnnotation(refId, hlib.getToken())) 225 | } 226 | } 227 | return promises 228 | } 229 | 230 | function styleWidget(csv: string, json: any[]) { 231 | if (format === 'html') { 232 | hlib.getById('widget').innerHTML = htmlBuffer 233 | } 234 | else if (format === 'csv') { 235 | widget.style.whiteSpace = 'pre' 236 | widget.style.overflowX = 'scroll' 237 | widget.innerText = csv 238 | } 239 | else if (format === 'json') { 240 | widget.style.whiteSpace = 'pre' 241 | widget.innerText = JSON.stringify(json, null, 2) 242 | } 243 | } 244 | 245 | function showUrlResults (counter:number, eltId:string, url:string, count:number, doctitle:string):string { 246 | const host = new URL(url).host 247 | const headingCounter = `counter_${counter}` 248 | const { togglerTitle, togglerUnicodeChar } = getToggler() 249 | const output = `

250 | ${togglerUnicodeChar} 251 |  ${count}  252 | ${doctitle} 253 | ${host} 254 |

255 |
256 | CARDS_${counter} 257 |
` 258 | return output 259 | } 260 | 261 | function reverseChronUrls (results: hlib.gatheredResults) { 262 | function reverseByUpdate(a:string, b:string) { 263 | const resultA = results.get(a) ! 264 | const resultB = results.get(b) ! 265 | return new Date(resultB.updated).getTime() - new Date(resultA.updated).getTime() 266 | } 267 | const urls = Array.from(results.keys()) 268 | urls.sort(reverseByUpdate) 269 | return urls 270 | } 271 | 272 | function findRepliesForId(id: string, replies: any[]) { 273 | const _replies = replies.filter( _reply => { 274 | return _reply.refs.indexOf(id) != -1 275 | }) 276 | return _replies 277 | } 278 | 279 | function organizeReplies(annosForUrl:hlib.annotation[], repliesForUrl:hlib.annotation[]) : hlib.annotation[] { 280 | function ascendingByUpdate(a:hlib.annotation, b:hlib.annotation) { 281 | return new Date(a.updated).getTime() - new Date(b.updated).getTime() 282 | } 283 | function reverseByUpdate(a:hlib.annotation, b:hlib.annotation) { 284 | return new Date(b.updated).getTime() - new Date(a.updated).getTime() 285 | } 286 | function byLocation(a:hlib.annotation, b:hlib.annotation) { 287 | const aStart = a.start ? a.start : 9999999999 288 | const bStart = b.start ? b.start : -9999999999 289 | return aStart - bStart 290 | } 291 | annosForUrl.sort(sortBy === 'recency' ? reverseByUpdate: byLocation) 292 | const _annos = [] as hlib.annotation[] 293 | annosForUrl.forEach(function (annoForUrl) { 294 | _annos.push(annoForUrl) 295 | const repliesForAnno = findRepliesForId(annoForUrl.id, repliesForUrl) 296 | repliesForAnno.sort(ascendingByUpdate) 297 | repliesForAnno.forEach(function (reply:hlib.annotation) { 298 | _annos.push(reply) 299 | }) 300 | }) 301 | repliesForUrl.forEach(function (reply:hlib.annotation) { 302 | const ids = _annos.map(_anno => { return _anno.id } ) 303 | const index = ids.indexOf(reply.id) 304 | if ( index < 0 ) { 305 | _annos.splice(index, 0, reply) 306 | } 307 | }) 308 | return _annos 309 | } 310 | 311 | } 312 | 313 | function isExpanded() { 314 | return hlib.getSettings().expanded === 'true' 315 | } 316 | 317 | function setExpanderExpand() { 318 | const expander = hlib.getById('expander') 319 | expander.onclick = setExpanderCollapse 320 | expander.innerText = hlib.expandToggler.togglerUnicodeChar 321 | expander.title = hlib.expandToggler.togglerTitle 322 | hlib.expandAll() 323 | } 324 | 325 | function setExpanderCollapse() { 326 | const expander = hlib.getById('expander') 327 | expander.onclick = setExpanderExpand 328 | expander.innerText = hlib.collapseToggler.togglerUnicodeChar 329 | expander.title = hlib.collapseToggler.togglerTitle 330 | hlib.collapseAll() 331 | } 332 | 333 | function getToggler() : hlib.toggler { 334 | const togglerTitle = isExpanded() ? hlib.expandToggler.togglerTitle : hlib.collapseToggler.togglerTitle 335 | const togglerUnicodeChar = isExpanded() ? hlib.expandToggler.togglerUnicodeChar : hlib.collapseToggler.togglerUnicodeChar 336 | return { 337 | togglerTitle: togglerTitle, 338 | togglerUnicodeChar: togglerUnicodeChar 339 | } 340 | } 341 | 342 | function showToggleAndDownloadControls() { 343 | const downloaderIcon = renderIcon('icon-floppy', exportControlStyle) 344 | if (format === 'html') { 345 | const { togglerTitle, togglerUnicodeChar } = getToggler() 346 | controlsContainer.innerHTML = ` 347 | ${togglerUnicodeChar} 348 | ${downloaderIcon}` 349 | const expander = hlib.getById('expander') as HTMLSpanElement 350 | expander.onclick = isExpanded() ? setExpanderCollapse : setExpanderExpand 351 | const downloader = document.querySelector('.downloader') as HTMLSpanElement 352 | downloader.onclick = downloadHTML 353 | } else { 354 | controlsContainer.innerHTML = `${downloaderIcon}` 355 | const downloader = document.querySelector('.downloader') as HTMLSpanElement 356 | downloader.onclick = format === 'csv' ? downloadCSV : downloadJSON 357 | } 358 | } 359 | 360 | function downloadHTML () { 361 | function rebaseLinks(links: NodeListOf) { 362 | links.forEach(link => { 363 | link.href = link.href 364 | }) 365 | } 366 | const head = document.head 367 | const body = document.body 368 | const controlsContainer = body.querySelector('#controlsContainer') as HTMLElement 369 | controlsContainer.remove() 370 | const pencils = body.querySelectorAll('.icon-pencil') 371 | pencils.forEach(pencil => { pencil.remove() }) 372 | rebaseLinks(body.querySelectorAll('.user a')) 373 | rebaseLinks(body.querySelectorAll('.annotationTags a')) 374 | const html = `${head.outerHTML}${body.outerHTML}` 375 | hlib.download(html, 'html') 376 | location.href = location.href 377 | } 378 | 379 | function downloadCSV () { 380 | let csvOutput = '"level","created","updated","url","user","id","group","tags","quote","text","relay link","direct link"\n' 381 | csvOutput += widget.innerText 382 | hlib.download(csvOutput, 'csv') 383 | } 384 | 385 | function downloadJSON () { 386 | const jsonOutput = '[' + widget.innerText + ']' 387 | hlib.download(jsonOutput, 'json') 388 | } 389 | 390 | function renderIcon(iconClass:string, style?: string) { 391 | const _style = style ? style : `style="display:block"` 392 | return `` 393 | } 394 | 395 | function deleteAnnotation(domAnnoId: string) { 396 | if (! window.confirm("Really delete this annotation?")) { 397 | return 398 | } 399 | const card = hlib.getById(domAnnoId) as HTMLElement 400 | const userElement = card.querySelector('.user') as HTMLElement 401 | const username = getUserName(userElement) 402 | const token = subjectUserTokens.get(username) || '' 403 | async function _delete() { 404 | const annoId = annoIdFromDomAnnoId(domAnnoId) 405 | const r = await hlib.deleteAnnotation(annoId, token) 406 | const response = JSON.parse(r.response) 407 | if (response.deleted) { 408 | const cardCounter = card.closest('div[id*="cards_counter"') as HTMLElement 409 | const urlCounter = cardCounter.previousElementSibling as HTMLHeadingElement 410 | hlib.getById(domAnnoId).remove() 411 | decrementPerUrlCount(urlCounter.id) 412 | } else { 413 | alert (`unable to delete, ${r.response}`) 414 | } 415 | } 416 | _delete() 417 | } 418 | 419 | function annoIdFromDomAnnoId(domAnnoId:string) { 420 | return domAnnoId.replace(/^_/,'') 421 | } 422 | 423 | function getUserName(userElement: HTMLElement) { 424 | return userElement.innerText.trim() 425 | } 426 | 427 | function adjustAnnoOrReplyCount(id:annoOrReplyCounterId, direction:counterDirection) { 428 | const counterElement = hlib.getById(id) as HTMLSpanElement 429 | let count:number = parseInt(counterElement.innerText) 430 | if (direction === counterDirection.up) { 431 | count++ 432 | } else { 433 | count-- 434 | } 435 | counterElement.innerText = count.toString() 436 | } 437 | 438 | function incrementAnnoOrReplyCount(id:annoOrReplyCounterId) { 439 | adjustAnnoOrReplyCount(id, counterDirection.up) 440 | } 441 | 442 | function decrementAnnoOrReplyCount(id:annoOrReplyCounterId) { 443 | adjustAnnoOrReplyCount(id, counterDirection.down) 444 | } 445 | 446 | function adjustPerUrlCount(urlCounterId:string, direction:counterDirection) { 447 | const urlHeading = hlib.getById(urlCounterId) 448 | const counterElement = urlHeading.querySelector('.counter') as HTMLElement 449 | let counter = parseInt(counterElement.innerText) 450 | if (direction === counterDirection.up) { 451 | counter++ 452 | } else { 453 | counter-- 454 | } 455 | if (counter == 0) { 456 | urlHeading.remove() 457 | } else { 458 | counterElement.innerText = ` ${counter}` 459 | } 460 | } 461 | 462 | function incrementPerUrlCount(urlCounterId:string) { 463 | adjustPerUrlCount(urlCounterId, counterDirection.up) 464 | } 465 | 466 | function decrementPerUrlCount(urlCounterId:string) { 467 | adjustPerUrlCount(urlCounterId, counterDirection.down) 468 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | View and export Hypothesis annotations 7 | 8 | 9 | 116 | 117 | 118 | 119 | 120 |
121 | 122 |
123 | 124 |
125 | 126 |
127 |
128 | 129 |
130 |
131 | 132 |
133 |
134 | 135 |
136 |
137 | 138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | sort top-level annotations by 153 | 157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | 165 |
166 |
167 |
168 |
169 | 170 |
171 |
172 |
173 | 174 |
175 | 176 |
177 | 178 |
179 | 180 |
181 | 182 |
183 | 184 |
185 | 186 |

187 | Fill in your Hypothesis API token to access your private groups and annotations. 188 | 189 | Click HTML, CSV, or JSON to search for matching Hypothesis annotations and display them 190 | in one of those formats. 191 | 192 | Click to save the current HTML, CSV, or JSON view. 193 | 194 | Fill in one or more facets to filter results. The facets are 195 | username, group, url (or wildcard_uri), tag, and any. 196 | 197 | If you need more than 50 results, set max to a larger number. 198 | 199 | If you specify no facets other than the default group All, you'll get recent Hypothesis 200 | annotations and replies in any group you're authorized to see. 201 | 202 | Click to launch the Hypothesis thread viewer/editor. 203 | 204 | Use exactTagSearch to match tags exactly. 205 | 206 | Use addQuoteContext to show the prefix and suffix captured with each highlight. 207 | 208 | Results are grouped by URL, ordered by most-recently-updated annotation for each URL, and displayed as threads. 209 | 210 | Use subject user tokens to enable in-place editing of annotations by users who 211 | have shared their tokens with you, and controlled tags to constrain the choices for the first 212 | tag belonging to each in-situ-editable annotation. 213 | 214 | Support: https://github.com/judell/facet. 215 | 216 | Icons by way of https://www.svgrepo.com licensed under https://creativecommons.org/licenses/by/4.0/. 217 | 218 | More Hypothesis tools. 219 |

220 | 221 | 222 | 223 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as hlib from '../hlib/hlib' // this will be commented out in the shipping bundle 2 | 3 | const syncedParams = ['user', 'group', 'url', 'wildcard_uri', 'tag', 'any', 'max'] as string[] 4 | const settings = ['subjectUserTokens', 'expanded', 'searchReplies', 'exactTagSearch'] as string[] 5 | 6 | if ( ! localStorage.getItem('h_settings') ) { 7 | hlib.settingsToLocalStorage(hlib.getSettings()) // initialize settings 8 | } 9 | 10 | hlib.getById('svgDefs').outerHTML = hlib.svgIcons 11 | 12 | updateSettingsFromUrl() // incoming url params override remembered params 13 | 14 | hlib.settingsToUrl(hlib.getSettings()) // add non-overridden remembered params to url 15 | 16 | hlib.createUserInputForm(hlib.getById('userContainer')) 17 | 18 | hlib.createGroupInputForm(hlib.getById('groupContainer')) 19 | .then( _ => { 20 | const selectId = 'groupsList' 21 | const groupsList = hlib.getById(selectId) as HTMLSelectElement 22 | const option = new Option('All','all') 23 | groupsList.insertBefore(option, groupsList.firstChild) 24 | groupsList.onchange = groupChangeHandler 25 | if (hlib.getSettings().group === 'all') { 26 | groupsList.selectedIndex = 0 27 | } 28 | function groupChangeHandler() { 29 | hlib.setSelectedGroup(selectId) 30 | hlib.getById('buttonHTML').click() 31 | } 32 | }) 33 | 34 | hlib.createUrlInputForm(hlib.getById('urlContainer')) 35 | 36 | hlib.createWildcardUriInputForm(hlib.getById('wildcard_uriContainer')) 37 | 38 | hlib.createTagInputForm(hlib.getById('tagContainer'), 'View/rename tags here') 39 | 40 | hlib.createAnyInputForm(hlib.getById('anyContainer'), 'Freetext search') 41 | 42 | hlib.createMaxInputForm(hlib.getById('maxContainer'), 'Approximate limit') 43 | 44 | hlib.createApiTokenInputForm(hlib.getById('tokenContainer')) 45 | 46 | hlib.createExactTagSearchCheckbox(hlib.getById('exactTagSearchContainer')) 47 | 48 | hlib.createAddQuoteContextCheckbox(hlib.getById('addQuoteContextContainer')) 49 | 50 | hlib.createExpandedCheckbox(hlib.getById('expandedContainer')) 51 | 52 | function updateSettingsFromUrl() { 53 | const params = syncedParams.concat(settings) 54 | params.forEach(param => { 55 | if (hlib.gup(param) !== '') { 56 | const value = decodeURIComponent(hlib.gup(param)) 57 | hlib.updateSetting(param, value) 58 | hlib.settingsToLocalStorage(hlib.getSettings()) 59 | } 60 | }) 61 | } 62 | 63 | function getCSV () { 64 | search('csv') 65 | } 66 | 67 | function getHTML () { 68 | search('html') 69 | } 70 | 71 | function getJSON () { 72 | search('json') 73 | } 74 | 75 | function search (format:string) { 76 | let params:any = {} 77 | params = Object.assign(params, hlib.getSettings()) 78 | params.format = format 79 | params._separate_replies = 'false' 80 | params.group = hlib.getSelectedGroup('groupsList') 81 | params.groupName = hlib.getSelectedGroupName('groupsList') 82 | const maxInput = document.querySelector('#maxForm') as HTMLInputElement 83 | params.max = maxInput.value ? maxInput.value : hlib.getSettings().max 84 | const sortByElement = hlib.getById('sortBy') as HTMLSelectElement 85 | const sortByOption = sortByElement[sortByElement.selectedIndex] as HTMLOptionElement 86 | params.sortBy = sortByOption.value 87 | document.title = 'Hypothesis activity for the query ' + JSON.stringify(params) 88 | params = encodeURIComponent(JSON.stringify(params)) 89 | const iframeUrl = `iframe.html?params=${params}` 90 | hlib.getById('iframe').setAttribute('src', iframeUrl) 91 | } 92 | 93 | function dropHandler(e:DragEvent) { 94 | const target = e.target as HTMLInputElement 95 | target.focus() 96 | target.click() 97 | setTimeout( () => { 98 | if (target.id === 'urlForm' || target.id === 'wildcard_uriForm') { 99 | if (target.id === 'wildcard_uriForm' && ! target.value.endsWith('/*') ) { 100 | target.value += '/*' 101 | } 102 | if (target.id === 'wildcard_uriForm' && ! target.value.startsWith('http') ) { 103 | target.value = `http://${target.value}` 104 | } 105 | } 106 | }, 0) 107 | } 108 | 109 | const activeFields = syncedParams.filter(x => {return x !== 'group'}) 110 | 111 | activeFields.forEach(field => { 112 | const fieldElement = hlib.getById(`${field}Container`) as HTMLInputElement 113 | fieldElement.addEventListener('formUrlStorageSync', function (e) { 114 | getHTML() 115 | }) 116 | fieldElement.addEventListener('clearInput', function (e) { 117 | getHTML() 118 | }) 119 | }) 120 | 121 | hlib.getById('sortBy').onchange = function() { 122 | getHTML() 123 | } -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | # The TypeScript compiler converts index.ts and iframe.ts to index.js and iframe.js 2 | 3 | rm index.js 4 | rm iframe.js 5 | 6 | tsc 7 | 8 | # The TypeScript files begin with this import: 9 | 10 | # import * as hlib from '../../hlib/hlib' 11 | 12 | # That makes hlib, now written in TypeScript, available to the editor. 13 | 14 | # But apps that use hlib are simple-minded and just want to include it with a script tag. 15 | 16 | # So this step transforms the tsc outputs, inserting comments before the import statement 17 | 18 | python comment-out-imports.py 19 | 20 | # For local testing, three files need reference adjustment: 21 | # index.html 22 | # iframe.html 23 | # 24 | # These are the options: 25 | # dev: http://10.0.0.9:8000/hlib.bundle.js 26 | # prd: https://jonudell.info/hlib/hlib.bundle.js 27 | 28 | if [[ $1 = "dev" ]]; then 29 | python use-debug-lib.py 30 | else 31 | python use-production-lib.py 32 | fi 33 | 34 | read -rsp $'Press enter to continue...\n' 35 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | import http.server 4 | from http.server import SimpleHTTPRequestHandler 5 | 6 | class CORSRequestHandler (SimpleHTTPRequestHandler): 7 | def end_headers (self): 8 | self.send_header('Access-Control-Allow-Origin', '*') 9 | SimpleHTTPRequestHandler.end_headers(self) 10 | 11 | def run(server_class=http.server.HTTPServer, 12 | handler_class=CORSRequestHandler): 13 | server_address = ('', 8001) 14 | httpd = server_class(server_address, handler_class) 15 | httpd.serve_forever() 16 | 17 | if __name__ == '__main__': 18 | run() -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | //"module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | //"outFile": "./hlib.js", /* Concatenate and emit output to single file. */ 13 | // "outDir": "./", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | //"strictNullChecks": true, /* Enable strict null checks. */ 25 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 26 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 27 | "noImplicitThis": false, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | //"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | 47 | /* Source Map Options */ 48 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 49 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 50 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 51 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 52 | 53 | /* Experimental Options */ 54 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 55 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 56 | } 57 | } -------------------------------------------------------------------------------- /use-debug-lib.py: -------------------------------------------------------------------------------- 1 | filenames = [ 2 | 'index.html', 3 | 'iframe.html', 4 | ] 5 | 6 | for filename in filenames: 7 | with open(filename, 'r+') as f: 8 | text = f.read() 9 | text = text.replace('https://jonudell.info/hlib/hlib3.bundle.js', 'http://localhost:8000/hlib3.bundle.js') 10 | text = text.replace('https://jonudell.info/hlib/hlib.css', 'http://localhost:8000/hlib.css') 11 | f.seek(0) 12 | f.write(text) 13 | f.truncate() 14 | 15 | 16 | -------------------------------------------------------------------------------- /use-production-lib.py: -------------------------------------------------------------------------------- 1 | filenames = [ 2 | 'index.html', 3 | 'iframe.html', 4 | ] 5 | 6 | for filename in filenames: 7 | with open(filename, 'r+') as f: 8 | text = f.read() 9 | text = text.replace('http://localhost:8000/hlib3.bundle.js', 'https://jonudell.info/hlib/hlib3.bundle.js') 10 | text = text.replace('http://localhost:8000/hlib.css', 'https://jonudell.info/hlib/hlib.css') 11 | f.seek(0) 12 | f.write(text) 13 | 14 | 15 | --------------------------------------------------------------------------------