Template - My Book Notes template
Installation
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).
Script
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());
}
Template
---
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}}