Here's a video guide for installing user scripts in QuickAdd.
Basically, you'll want to create a new JavaScript file (file extension is .js
) with the contents of the script. Then, in the script, you see the YOUR_READWISE_TOKEN
, which is where you'll want to insert your Readwise token (find it here).
Now you need to create a new macro. To do so, open the Macro Manager, enter a name for it (I use Readwise
), and then click Add
. Then click Configure
on that macro. Once a modal opens, select the user script you've created and click Add
.
Once that's done, you can use the template provided below. If you have your own, then you can just use the {{MACRO:Readwise::instaFetchBook}}
to insert the highlights. If you called your macro something else than Readwise
, replace Readwise
with that.
This template should be added to a Template choice, and should be given values that resemble this:
The template path should be the path the template made based on the one here.
Notably, the book's name would be the one selected. I have chosen to write prepend a {
before it, as I use this to denote literature notes in my vault.
The remaining settings are up to you. Activating this choice will open a menu which allows you to choose a book, and the book notes will be used appended to the template.
You can customize the template as much as you like, but make sure to keep the {{MACRO:Readwise::instaFetchBook}}
, as that is what gets the highlights (and where they'll be inserted).
Most of the setup is shown in the gif.
module.exports = {start, getDailyQuote, instaFetchBook, getBooks};
const apiUrl = "https://readwise.io/api/v2/";
const books = "π Books", articles = "π° Articles", tweets = "π€ Tweets", supplementals = "π Supplementals", podcasts = "π Podcasts", searchAll = "π Search All Highlights (slow!)";
const categories = {books, articles, tweets, supplementals, podcasts, searchAll};
const randomNumberInRange = (max) => Math.floor(Math.random() * max);
const token = "YOUR_READWISE_TOKEN";
let quickAddApi;
async function start(params) {
({quickAddApi} = params);
let highlights;
const category = await categoryPromptHandler();
if (!category) return;
if (category === "searchAll") {
highlights = await getAllHighlights();
} else {
let res = await getHighlightsByCategory(category);
if (!res) return;
const {results} = res;
const item = await quickAddApi.suggester(results.map(item => item.title), results);
if (!item) return;
params.variables["author"] = `[[${item.author}]]`;
const res2 = await getHighlightsForElement(item);
if (!res2) return;
highlights = res2.results.reverse();
}
const textToAppend = await highlightsPromptHandler(highlights);
return !textToAppend ? "" : textToAppend;
}
async function getBooks(params) {
const {results: books} = await getHighlightsByCategory("books");
const bookNames = books.map(book => book.title);
const selectedBook = await params.quickAddApi.suggester(bookNames, bookNames);
params.variables["Book Title"] = selectedBook;
return selectedBook;
}
async function instaFetchBook(params) {
const bookTitle = params.variables["Book Title"];
if (!bookTitle) return await start(params);
const {results: books} = await getHighlightsByCategory("books");
const book = books.find(b => b.title.toLowerCase().contains(bookTitle.toLowerCase()));
if (!book) throw new Error("Book " + bookTitle + " not found.");
params.variables["author"] = `[[${book.author}]]`;
const highlights = (await getHighlightsForElement(book)).results.reverse();
return writeAllHandler(highlights);
}
async function getDailyQuote(params) {
const category = "supplementals";
const res = await getHighlightsByCategory(category);
if (!res) return;
const {results} = res;
const targetItem = results[randomNumberInRange(results.length)];
const {results: highlights} = await getHighlightsForElement(targetItem);
if (!highlights) return;
const randomHighlight = highlights[randomNumberInRange(highlights.length)];
const quote = formatDailyQuote(randomHighlight.text, targetItem);
return `${quote}`
}
async function categoryPromptHandler() {
const choice = await quickAddApi.suggester(Object.values(categories), Object.keys(categories));
if (!choice) return null;
return choice;
}
async function highlightsPromptHandler(highlights) {
const writeAll = "Write all highlights to page", writeOne = "Write one highlight to page";
const choices = [writeAll, writeOne];
const choice = await quickAddApi.suggester(choices, choices);
if (!choice) return null;
if (choice == writeAll)
return writeAllHandler(highlights);
else
return await writeOneHandler(highlights);
}
function writeAllHandler(highlights) {
return highlights.map(hl => {
if (hl.text == "No title") return;
const {quote, note} = textFormatter(hl.text, hl.note);
return `${quote}${note}`;
}).join("\n\n");
}
async function writeOneHandler(highlights) {
const chosenHighlight = await quickAddApi.suggester(highlights.map(hl => hl.text), highlights);
if (!chosenHighlight) return null;
const {quote, note} = textFormatter(chosenHighlight.text, chosenHighlight.note);
return `${quote}${note}`;
}
function formatDailyQuote(sourceText, sourceItem) {
let quote = sourceText.split("\n").filter(line => line != "").map(line => {
return `> ${line}`;
});
const attr = `\n>\\- ${sourceItem.author}, _${sourceItem.title}_`;
return `${quote}${attr}`;
}
function textFormatter(sourceText, sourceNote) {
let quote = sourceText.split("\n").filter(line => line != "").map(line => {
if (sourceNote.includes(".h1"))
return `## ${line}`;
else
return `> ${line}`;
}).join("\n");
let note;
if (sourceNote.includes(".h1") || sourceNote == "" || !sourceNote) {
note = "";
} else {
note = "\n\n" + sourceNote;
}
return {quote, note};
}
async function getHighlightsByCategory(category) {
return apiGet(`${apiUrl}books`, {category, "page_size": 1000});
}
async function getHighlightsForElement(element) {
return apiGet(`${apiUrl}highlights`, {book_id: element.id, page_size: 1000});
}
async function getAllHighlights() {
const MAX_PAGE_SIZE = 1000;
const URL = `${apiUrl}highlights`;
let promises = [];
const {count} = await apiGet(URL);
const requestsToMake = Math.ceil(count / MAX_PAGE_SIZE);
for (let i = 1; i <= requestsToMake; i++) {
promises.push(apiGet(URL, { page_size: MAX_PAGE_SIZE, page: i }));
}
const allHighlights = (await Promise.all(promises)).map(hl => hl.results);
return allHighlights;
}
async function apiGet(url, data) {
let finalURL = new URL(url);
if (data)
Object.keys(data).forEach(key => finalURL.searchParams.append(key, data[key]));
return await fetch(finalURL, {
method: 'GET', cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
Authorization: `Token ${token}`
},
}).then(async (res) => await res.json());
}
---
image:
tags: in/books
aliases:
- <% tp.file.title.replace('{ ', '') %>
cssclass:
---
# Title: [[<%tp.file.title%>]]
## Metadata
Tags::
Type:: [[{]]
Author:: {{VALUE:author}}
Reference::
Rating::
Reviewed Date:: [[<%tp.date.now("gggg-MM-DD - ddd MMM D")%>]]
Finished Year:: [[<%tp.date.now("gggg")%>]]
# Thoughts
# Actions Taken / Changes
# Summary of Key Points
# Highlights & Notes
{{MACRO:Readwise::instaFetchBook}}