I just published a new extension on Chrome and Firefox that allows anyone to run Code Tours from the Github UI. More information about Code Tours and the extension in this blog post.
Article No Longer Available
I thought it would be nice to write a series about how you could do exactly the same, step by step.
This fourth blog post will focus on keeping your state in a Background Script.
Our next feature
If you have followed the series until today, we have created an extension capable of requesting the content of the Code Tours. Now, we need to act on them.
What we'll build today is the ability to, from the list of tours, jump to the first step of any of them. And once we're there, we'll need to prepare to display the Code Tour.
Adding the link
Just so we're up to date, here is the state of the content script:
function forwardRequest(message) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
if (!response) return reject(chrome.runtime.lastError)
return resolve(response)
})
})
}
document.addEventListener("DOMContentLoaded", function(){
Array.from(
document.querySelectorAll('div[role=row] > div[role="rowheader"] > span > a').values(),
).map(
async (parentElement) => {
const title = parentElement.getAttribute('title')
const href = parentElement.getAttribute('href')
// Now we want to query the file content as a raw string.
// In github, this means fetching the file using “raw” instead of “blob”
const codeTourUrl = href.replace('blob', 'raw')
// A Code Tour is a json object, we can use the fetch API to receive an object
const content = await forwardRequest({ url: codeTourUrl })
console.log(title, content)
})
})
What we need to do now is, instead of logging the Code Tour content, to add a link to the right page. A basic Code Tour object looks like this:
{
title: "The tour name",
steps: [
{
file: "manifest.json",
line: 1,
description: "The text that describes the step"
}
]
}
We can definitely use it to generate the right link. For instance for this step, we want to go to:
https://github.com/<repository_owner>/<repository_name>/blob/main/manifest.json
For the sake of the exercise, let's consider that the base branch of a repository is always main
.
Generating this URL is fairly easy. We take the URL we're at up to the repository name, then add blob/main/
and the filename.
In order to have the line focused on Github we can even add #L1
to the URL to focus the line 1.
function getLink(codeTour) {
const currentRepo = /^\/([^/]+\/[^/]+)\//.exec(window.location.pathname)[1]
return `/${currentRepo}/blob/main/${codeTour.steps[0].file}#L${codeTour.steps[0].line}`
}
Now let's display our link. Just replace the log in the code to add a link to the page:
const link = document.createElement("a");
link.setAttribute("href", getLink(content));
link.setAttribute("style", "padding: 5px;");
link.text = "Go!";
parentElement.parentNode.prepend(link);
It doesn't look pretty but it will work for now.
Content Scripts have the memories of goldfish
Now we're able to navigate to the first step of the tour. That's great. Unfortunately, as the page loads, the Content Script comes to life again... With no memory of why it's here!
We need it to know what to do.
The first thing we're going to do is to add a few query parameters to pass some information.
Let's add the current step and the name of the Code Tour.
function getLink(codeTour) {
const currentRepo = /^\/([^/]+\/[^/]+)\//.exec(window.location.pathname)[1]
return `/${currentRepo}/blob/main/${codeTour.steps[0].file}?step=0&code-tour-title=${codeTour.title}#L${codeTour.steps[0].line}`
}
This way, when the content script gets loaded, we know that we are currently playing a Code Tour, which one, and at which step we're at. For now we always return 0
as the step number but it'll be easy to make it generic later.
Keep the Code Tours in memory, in the Background Script
Now that we know which Code Tour we have to play, and which step we're at, let's save all the Code Tours in memory in the Background Script. Since the Background Script is already the one querying the data for the Content Script, we can just keep an index:
const tours = {}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
fetch(`https://github.com/${request.url}`)
.then((response) => response.json())
.then((codeTourContent) => {
// Let's save the content of the code tour
tours[codeTourContent.title] = codeTourContent
sendResponse(codeTourContent)
})
return true
})
Retrieving the Code Tour from the Content Script
Now that we store the Code Tours, we need a way to be able to retrieve them from the Content Script. Let's do it this way:
- if the message received by the Background Script contains a URL, it fetches the Code Tour
- if it contains a title, it returns the cached version
We just need to add this line at the beginning of the function:
if (request.title) return sendResponse(tours[request.title])
Now, let query it from the content script:
function forwardRequest(message) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(message, (response) => {
if (!response) return reject(chrome.runtime.lastError)
return resolve(response)
})
})
}
document.addEventListener("DOMContentLoaded", async () => {
const urlParams = new URLSearchParams(window.location.search)
const title = urlParams.get('code-tour-title')
if (!title) return
const tour = await forwardRequest({ title })
const step = urlParams.get('step')
// Here we can retrieve the current step to be displayed
console.log(tour.steps[step])
})
And voilà. We can now display the Code Tour, and continue linking to the next and previous steps.
Conclusion
We've just learned how to store the state of our extension in the Background Script so we can keep the Content Script aware of what it should do. That's a great milestone! With what we know now, you can definitely build a basic version of the Code Tour extension.
In the next post, we will have a look at how to make our Code Tour feature appear as though it was native to Github and how to securely inject the html needed. Feel free to follow me here if you want to check the next one when it's out:
Photo by Ricardo Gomez Angel on Unsplash
Top comments (0)