├── 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 |
13 |
14 |
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 |
--------------------------------------------------------------------------------