├── src ├── css │ ├── options.css │ ├── popup.css │ └── tooltip.css ├── js │ ├── background.js │ ├── hypothesis │ │ ├── index.js │ │ └── hypothesis.js │ ├── options.js │ └── popup.js ├── img │ ├── icon-128.png │ └── icon-34.png ├── background.html ├── manifest.json ├── popup.html └── options.html ├── .gitignore ├── utils ├── env.js ├── build.js └── webserver.js ├── package.json ├── LICENSE.md ├── README.md └── webpack.config.js /src/css/options.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | secrets.*.js 4 | -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | import '../img/icon-128.png' 2 | import '../img/icon-34.png' 3 | -------------------------------------------------------------------------------- /src/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalmo3/hypothesis-to-bullets-chrome-extension/HEAD/src/img/icon-128.png -------------------------------------------------------------------------------- /src/img/icon-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalmo3/hypothesis-to-bullets-chrome-extension/HEAD/src/img/icon-34.png -------------------------------------------------------------------------------- /src/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /utils/env.js: -------------------------------------------------------------------------------- 1 | // tiny wrapper with default env vars 2 | module.exports = { 3 | NODE_ENV: (process.env.NODE_ENV || "development"), 4 | PORT: (process.env.PORT || 3000) 5 | }; 6 | -------------------------------------------------------------------------------- /src/css/popup.css: -------------------------------------------------------------------------------- 1 | .btnrow { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .btnrow button { 7 | background-color: beige; 8 | border: 1px solid lightgray; 9 | margin: 5px 5px; 10 | padding: 5px 5px; 11 | } -------------------------------------------------------------------------------- /utils/build.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"), 2 | config = require("../webpack.config"); 3 | 4 | delete config.chromeExtensionBoilerplate; 5 | 6 | webpack( 7 | config, 8 | function (err) { if (err) throw err; } 9 | ); 10 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Fetch Hypothesis Annotations", 3 | "description": "", 4 | "version": "0.2.0", 5 | "options_page": "options.html", 6 | "background": { 7 | "page": "background.html" 8 | }, 9 | "permissions": ["activeTab", "storage"], 10 | "browser_action": { 11 | "default_popup": "popup.html", 12 | "default_icon": "icon-34.png" 13 | }, 14 | "icons": { 15 | "128": "icon-128.png" 16 | }, 17 | "manifest_version": 2, 18 | "content_security_policy": "script-src 'self' 'unsafe-eval' ; object-src 'self' " 19 | } 20 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Fetch annotations!

9 | 10 |
11 | 12 |
13 | 14 | Copied! 15 |
16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-extension-webpack", 3 | "version": "0.2.1", 4 | "description": "A boilerplate to chrome extension with webpack", 5 | "scripts": { 6 | "build": "node utils/build.js", 7 | "start": "node utils/webserver.js" 8 | }, 9 | "devDependencies": { 10 | "clean-webpack-plugin": "3.0.0", 11 | "copy-webpack-plugin": "5.0.5", 12 | "css-loader": "3.2.0", 13 | "file-loader": "4.3.0", 14 | "fs-extra": "8.1.0", 15 | "html-loader": "0.5.5", 16 | "html-webpack-plugin": "3.2.0", 17 | "lodash": "^4.17.15", 18 | "pdfjs-dist": "^2.2.228", 19 | "query-string": "^6.10.1", 20 | "style-loader": "1.0.0", 21 | "webpack": "4.41.2", 22 | "webpack-dev-server": "3.9.0", 23 | "write-file-webpack-plugin": "4.5.1", 24 | "webpack-cli": "3.3.10" 25 | }, 26 | "dependencies": { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | Hypothes.is username 10 | 11 | 12 |
13 |
14 |
15 | API token 16 | (get it here) 17 | 18 | 19 |
20 |
21 |
22 | Auto fetch when opening extension: 23 | 24 |
25 |
26 |
27 | Auto copy to clipboard on fetch: 28 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/css/tooltip.css: -------------------------------------------------------------------------------- 1 | /* Tooltip container */ 2 | .tooltip { 3 | position: relative; 4 | display: inline-block; 5 | border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ 6 | } 7 | 8 | /* Tooltip text */ 9 | .tooltiptext { 10 | visibility: hidden; 11 | /* width: 120px; */ 12 | background-color: #555; 13 | color: #fff; 14 | text-align: center; 15 | padding: 5px 5px; 16 | border-radius: 6px; 17 | 18 | /* Position the tooltip text */ 19 | position: absolute; 20 | z-index: 1; 21 | /* bottom: 48%; */ 22 | /* bottom: 125%; */ 23 | left: 50%; 24 | /* margin-left: -60px; */ 25 | margin-top: -25px; 26 | /* Fade in tooltip */ 27 | /* opacity: 0; */ 28 | transition: opacity 0.3s; 29 | } 30 | 31 | /* Tooltip arrow */ 32 | .tooltiptext::after { 33 | content: ""; 34 | position: absolute; 35 | top: 100%; 36 | left: 50%; 37 | margin-left: -5px; 38 | border-width: 5px; 39 | border-style: solid; 40 | border-color: #555 transparent transparent transparent; 41 | } 42 | -------------------------------------------------------------------------------- /src/js/hypothesis/index.js: -------------------------------------------------------------------------------- 1 | import hypothesis from './hypothesis.js'; 2 | 3 | const months = [ 4 | 'January', 5 | 'February', 6 | 'March', 7 | 'April', 8 | 'May', 9 | 'June', 10 | 'July', 11 | 'August', 12 | 'September', 13 | 'October', 14 | 'November', 15 | 'December' 16 | ]; 17 | const nth = function(d) { 18 | if (d > 3 && d < 21) return 'th'; 19 | switch (d % 10) { 20 | case 1: 21 | return 'st'; 22 | case 2: 23 | return 'nd'; 24 | case 3: 25 | return 'rd'; 26 | default: 27 | return 'th'; 28 | } 29 | }; 30 | 31 | export const getRoamDate = dateString => { 32 | const d = new Date(dateString); 33 | const year = d.getFullYear(); 34 | const date = d.getDate(); 35 | const month = months[d.getMonth()]; 36 | const nthStr = nth(date); 37 | return `${month} ${date}${nthStr}, ${year}`; 38 | }; 39 | 40 | // if (url.startsWith("https://twitter")) { 41 | // twitter(url); 42 | // } else { 43 | export default (url,user,token) => hypothesis(url, user, token); 44 | // } 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dalmo Mendonça 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/webserver.js: -------------------------------------------------------------------------------- 1 | var WebpackDevServer = require("webpack-dev-server"), 2 | webpack = require("webpack"), 3 | config = require("../webpack.config"), 4 | env = require("./env"), 5 | path = require("path"); 6 | 7 | var options = (config.chromeExtensionBoilerplate || {}); 8 | var excludeEntriesToHotReload = (options.notHotReload || []); 9 | 10 | for (var entryName in config.entry) { 11 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) { 12 | config.entry[entryName] = 13 | [ 14 | ("webpack-dev-server/client?http://localhost:" + env.PORT), 15 | "webpack/hot/dev-server" 16 | ].concat(config.entry[entryName]); 17 | } 18 | } 19 | 20 | config.plugins = 21 | [new webpack.HotModuleReplacementPlugin()].concat(config.plugins || []); 22 | 23 | delete config.chromeExtensionBoilerplate; 24 | 25 | var compiler = webpack(config); 26 | 27 | var server = 28 | new WebpackDevServer(compiler, { 29 | hot: true, 30 | contentBase: path.join(__dirname, "../build"), 31 | sockPort: env.PORT, 32 | headers: { 33 | "Access-Control-Allow-Origin": "*" 34 | }, 35 | disableHostCheck: true 36 | }); 37 | 38 | server.listen(env.PORT); 39 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | import "../css/options.css"; 2 | 3 | const syncBox = (boxId) => { 4 | const checkbox = document.getElementById(boxId) 5 | const prop = checkbox.name; 6 | 7 | console.log(prop) 8 | chrome.storage.sync.get(prop, (data) => { 9 | checkbox.checked = data[prop] 10 | }) 11 | 12 | checkbox.onclick = function (e) { 13 | chrome.storage.sync.set({[prop]: checkbox.checked}, function() { 14 | console.log(boxId + ' changed to ' + checkbox.checked); 15 | }) 16 | } 17 | } 18 | 19 | const syncInput = (inputId, buttonId) => { 20 | 21 | const input = document.getElementById(inputId); 22 | const button = document.getElementById(buttonId); 23 | const prop = input.name; 24 | console.log(prop) 25 | chrome.storage.sync.get(prop, (data) => { 26 | input.value = data[prop] || "None set"; 27 | }) 28 | 29 | button.onclick = (e) => { 30 | chrome.storage.sync.set({[prop]: input.value}, function() { 31 | console.log('token is ' + input.value); 32 | }) 33 | } 34 | } 35 | 36 | 37 | const init = () => { 38 | syncInput('hypTokenInput', 'hypTokenSubmit'); 39 | syncInput('hypUserInput', 'hypUserSubmit'); 40 | syncBox('autoFetch'); 41 | syncBox('autoCopy'); 42 | } 43 | 44 | init(); -------------------------------------------------------------------------------- /src/js/hypothesis/hypothesis.js: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | // import fetch from "isomorphic-fetch"; 3 | import lodash from "lodash"; 4 | import { getRoamDate } from "./index.js"; 5 | 6 | const parseAnnotation = a => { 7 | const textRaw = a.text; 8 | const quotationRaw = 9 | lodash.get(a, "target[0]selector") && 10 | a.target[0].selector.find(x => x.exact) && 11 | a.target[0].selector.find(x => x.exact).exact; 12 | let result = ""; 13 | const quotation = (quotationRaw || "").replace(/\n/g, " "); 14 | const text = (textRaw || "").replace(/\n/g, " "); 15 | const extraIndent = text ? " " : ""; 16 | const quoteString = quotation ? ` - ${quotation}` : ""; 17 | const textString = text ? extraIndent + ` - ^^${text}^^` : ""; 18 | return [quoteString, textString].join("\n"); 19 | }; 20 | 21 | const getAnnotations = async (token, annotatedUrl, user) => { 22 | const query = queryString.stringify({ 23 | url: annotatedUrl, 24 | limit: 200, 25 | user 26 | }); 27 | const url = "https://hypothes.is/api/search?" + query; 28 | const queryHeaders = token && { 29 | headers: { 30 | Authorization: "Bearer " + token 31 | } 32 | }; 33 | try { 34 | return await fetch(url, queryHeaders) 35 | .then(async e => { 36 | console.log(e); 37 | const json = await e.json(); 38 | console.log(json) 39 | return json 40 | }) 41 | .then(e => { 42 | 43 | if (!e.rows.length){ 44 | return 'No annotations found' 45 | } 46 | const article = lodash.get(e, "rows[0].document.title[0]"); 47 | const updated = lodash.get(e, "rows[0].updated"); 48 | const annotations = lodash 49 | .orderBy(e.rows, f => { 50 | try { 51 | return lodash 52 | .get(f, "target[0].selector") 53 | .filter(x => x.type === "TextPositionSelector")[0].start; 54 | } catch (e) { 55 | return 0; 56 | } 57 | }) 58 | .map(x => parseAnnotation(x)) 59 | .join("\n"); 60 | const dateStr = getRoamDate(updated); 61 | const bulletedAnnotations = 62 | `- ${article}\n - Source: ${annotatedUrl}\n${annotations}` 63 | // console.log(bulletedAnnotations); 64 | return bulletedAnnotations; 65 | }); 66 | } catch (e) { 67 | console.error(e); 68 | } 69 | }; 70 | 71 | export default (url, user, token) => { 72 | return getAnnotations(token, url, user); 73 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hypothesis to bullets chrome extension 2 | 3 | Browser extension for fetching and formatting [Hypothes.is](https://web.hypothes.is/about/) annotations into markdown bullet points, ready for copying into Roam, Notion or similar apps. 4 | 5 | ## Install 6 | 7 | ### From zip 8 | 1. Download the [latest release](https://github.com/dalmo3/hypothesis-to-bullets-chrome-extension/releases) and extract into a new folder 9 | 2. Load your extension on Chrome following: 10 | 1. Access `chrome://extensions/` 11 | 2. Check `Developer mode` 12 | 3. Click on `Load unpacked extension` 13 | 4. Select the folder you created. 14 | 15 | ### Using `npm` 16 | 1. Run `npm run build` 17 | 2. Load your extension on Chrome following: 18 | 1. Access `chrome://extensions/` 19 | 2. Check `Developer mode` 20 | 3. Click on `Load unpacked extension` 21 | 4. Select the `build` folder. 22 | 23 | More info on the upstream project: https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate 24 | 25 | ## Usage 26 | 27 | 1. Open options page (Right-click on the extension) and add your details and preferences. 28 | 2. Visit the page where you have annotations. 29 | 3. Open the extension 30 | 4. For local PDF support, Right-click -> Manage Extensions -> Allow access to file URLs 31 | 32 | ## Changelog 33 | ``` 34 | 0.2.1 35 | Added some debug messages 36 | 0.2.0 37 | Added PDF support 38 | 0.1.0 39 | Initial Version 40 | ``` 41 | 42 | ## Troubleshooting 43 | 44 | If you're having trouble to fetch annotations or highlights on a specific page, 45 | 1. Right-click the extension button -> Inspect popup; 46 | 1. On the Console: 47 | 1. Check if `URL` is correct 48 | 1. Check for `{total: 0, rows: Array(0)}`. Zero means nothing was found on Hypothes.is servers. 49 | 1. Check for general errror messages. Ignore `Uncaught (in promise) DOMException: Document is not focused.` for now; 50 | 1. Open an [issue](https://github.com/dalmo3/hypothesis-to-bullets-chrome-extension/issues) 51 | 52 | ## Known Issues 53 | - Only fetches the first 200 annotations from a page 54 | - Won't fetch annotations from PDFs while the official Hypothes.is browser extension is open 55 | 56 | ## Credits 57 | 58 | This project was made possible thanks to: 59 | - Samuel Simões for [chrome-extension-webpack-boilerplate](https://github.com/samuelsimoes/chrome-extension-webpack-boilerplate) 60 | - Stian Håklev for the [annotations fetching and processing code](https://github.com/houshuang/hypothesis-to-bullet) 61 | - And of course the people at [Hypothes.is](https://web.hypothes.is/about/) 62 | 63 | ------------- 64 | Dalmo Mendonça ~ [@dalmo3](https://twitter.com/dalmo3) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"), 2 | path = require("path"), 3 | fileSystem = require("fs"), 4 | env = require("./utils/env"), 5 | CleanWebpackPlugin = require("clean-webpack-plugin").CleanWebpackPlugin, 6 | CopyWebpackPlugin = require("copy-webpack-plugin"), 7 | HtmlWebpackPlugin = require("html-webpack-plugin"), 8 | WriteFilePlugin = require("write-file-webpack-plugin"); 9 | 10 | // load the secrets 11 | var alias = {}; 12 | 13 | var secretsPath = path.join(__dirname, ("secrets." + env.NODE_ENV + ".js")); 14 | 15 | var fileExtensions = ["jpg", "jpeg", "png", "gif", "eot", "otf", "svg", "ttf", "woff", "woff2"]; 16 | 17 | if (fileSystem.existsSync(secretsPath)) { 18 | alias["secrets"] = secretsPath; 19 | } 20 | 21 | var options = { 22 | mode: process.env.NODE_ENV || "development", 23 | entry: { 24 | popup: path.join(__dirname, "src", "js", "popup.js"), 25 | options: path.join(__dirname, "src", "js", "options.js"), 26 | background: path.join(__dirname, "src", "js", "background.js") 27 | }, 28 | output: { 29 | path: path.join(__dirname, "build"), 30 | filename: "[name].bundle.js" 31 | }, 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.css$/, 36 | loader: "style-loader!css-loader", 37 | exclude: /node_modules/ 38 | }, 39 | { 40 | test: new RegExp('.(' + fileExtensions.join('|') + ')$'), 41 | loader: "file-loader?name=[name].[ext]", 42 | exclude: /node_modules/ 43 | }, 44 | { 45 | test: /\.html$/, 46 | loader: "html-loader", 47 | exclude: /node_modules/ 48 | } 49 | ] 50 | }, 51 | resolve: { 52 | alias: alias 53 | }, 54 | plugins: [ 55 | // clean the build folder 56 | new CleanWebpackPlugin({ 57 | cleanAfterEveryBuildPatterns: ['!manifest.json'] 58 | }), 59 | // expose and write the allowed env vars on the compiled bundle 60 | new webpack.EnvironmentPlugin(["NODE_ENV"]), 61 | new CopyWebpackPlugin([{ 62 | from: "src/manifest.json", 63 | transform: function (content, path) { 64 | // generates the manifest file using the package.json informations 65 | return Buffer.from(JSON.stringify({ 66 | description: process.env.npm_package_description, 67 | version: process.env.npm_package_version, 68 | ...JSON.parse(content.toString()) 69 | })) 70 | } 71 | }]), 72 | new HtmlWebpackPlugin({ 73 | template: path.join(__dirname, "src", "popup.html"), 74 | filename: "popup.html", 75 | chunks: ["popup"] 76 | }), 77 | new HtmlWebpackPlugin({ 78 | template: path.join(__dirname, "src", "options.html"), 79 | filename: "options.html", 80 | chunks: ["options"] 81 | }), 82 | new HtmlWebpackPlugin({ 83 | template: path.join(__dirname, "src", "background.html"), 84 | filename: "background.html", 85 | chunks: ["background"] 86 | }), 87 | new WriteFilePlugin() 88 | ] 89 | }; 90 | 91 | if (env.NODE_ENV === "development") { 92 | options.devtool = "cheap-module-eval-source-map"; 93 | } 94 | 95 | module.exports = options; 96 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | import '../css/popup.css'; 2 | import '../css/tooltip.css'; 3 | import hyp from './hypothesis/index'; 4 | 5 | import pdfjsLib from 'pdfjs-dist'; 6 | 7 | // console.log(pdfjsLib) 8 | 9 | const getHypothesisURI = async () => 10 | new Promise(res => { 11 | chrome.tabs.query({ currentWindow: true, active: true }, async tabs => { 12 | // chrome.tabs.query({ lastFocusedWindow: true, active: true }, async tabs => { 13 | console.log('Tabs: :', tabs); 14 | const urlObject = new URL(tabs[0].url); 15 | console.log('URL: ', urlObject.href); 16 | 17 | let url = decodeURI(urlObject.href.toString().replace(/#.*/, '')); 18 | if ( 19 | urlObject.href.endsWith('.pdf') || 20 | urlObject.pathname.endsWith('.pdf') || 21 | urlObject.pathname === '/pdfjs/web/viewer.html' 22 | ) { 23 | if (urlObject.protocol === 'chrome-extension:') { 24 | url = decodeURIComponent(urlObject.search.slice(6)); 25 | } 26 | const doc = await pdfjsLib.getDocument(url).promise; 27 | console.log('PDF fingerprint: ', doc.fingerprint); 28 | res(`urn:x-pdf:${doc.fingerprint}`); 29 | } else { 30 | res(url); 31 | } 32 | }); 33 | }) 34 | 35 | 36 | 37 | // getHypothesisURI().then(console.log); 38 | // console.log(pdfjsLib.PDFJS.PDFViewer) 39 | 40 | // const pdfaPath = 41 | 42 | // console.log( 43 | // pdfjsLib.getDocument(url) 44 | // ) 45 | 46 | // main function 47 | const run = data => { 48 | const { autoCopy, autoFetch, hypUser, hypToken } = data; 49 | // console.log(data); 50 | 51 | const fetchBtn = document.getElementById('fetchBtn'); 52 | const copyBtn = document.getElementById('copyBtn'); 53 | 54 | fetchBtn.onclick = async e => { 55 | console.log('clicked'); 56 | // get current tab url 57 | 58 | // chrome.tabs.query({ lastFocusedWindow: true, active: true }, async tabs => { 59 | // const url = tabs[0].url; 60 | 61 | // console.log(url); 62 | // console.log(hypUser, hypToken); 63 | const url = await getHypothesisURI(); 64 | const annotations = await hyp(url, hypUser, hypToken); 65 | 66 | const textArea = document.getElementById('annotationArea'); 67 | textArea.value = annotations; 68 | // console.log(annotations); 69 | 70 | // auto copy to clipboard 71 | autoCopy && copyBtn.click(); 72 | // }); 73 | }; 74 | 75 | copyBtn.onclick = e => { 76 | //copy to clipboard 77 | const textArea = document.getElementById('annotationArea'); 78 | navigator.clipboard.writeText(textArea.value).then(); 79 | 80 | //display tooltip 81 | document.getElementById('copyTt').style.visibility = 'visible'; 82 | setTimeout( 83 | () => (document.getElementById('copyTt').style.visibility = 'hidden'), 84 | 1000 85 | ); 86 | }; 87 | 88 | // auto fetch 89 | autoFetch && fetchBtn.click(); 90 | }; 91 | 92 | const init = callback => { 93 | console.log('opened'); 94 | // get user data 95 | chrome.storage.sync.get( 96 | ['autoCopy', 'autoFetch', 'hypUser', 'hypToken'], 97 | callback // fetched data is passed to callback 98 | ); 99 | }; 100 | 101 | init(run); 102 | --------------------------------------------------------------------------------