Skip to content

Commit f2e6063

Browse files
committed
Implement PWA Support
1 parent 989e680 commit f2e6063

File tree

6 files changed

+282
-1
lines changed

6 files changed

+282
-1
lines changed

source/_extensions/pwa_service.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import sphinx as Sphinx
2+
from typing import Any, Dict, List
3+
import os
4+
from docutils import nodes
5+
import json
6+
import shutil
7+
from urllib.parse import urljoin, urlparse, urlunparse
8+
from sphinx.util import logging
9+
from sphinx.util.console import green, red, yellow # pylint: disable=no-name-in-module
10+
11+
manifest = {
12+
"name": "",
13+
"short_name": "",
14+
"theme_color": "",
15+
"background_color": "",
16+
"display": "standalone",
17+
"scope": "/",
18+
"start_url": "/index.html",
19+
"icons": [],
20+
}
21+
22+
logger = logging.getLogger(__name__)
23+
24+
def get_files_to_cache(outDir: str, config: Dict[str, Any]):
25+
files_to_cache = []
26+
for (dirpath, dirname, filenames) in os.walk(outDir):
27+
dirpath = dirpath.split(outDir)[1]
28+
29+
# skip adding sources to cache
30+
if os.sep + "_sources" + os.sep in dirpath:
31+
continue
32+
33+
# add files to cache
34+
for name in filenames:
35+
if "sw.js" in name:
36+
continue
37+
38+
dirpath = dirpath.replace("\\", "/")
39+
dirpath = dirpath.lstrip("/")
40+
41+
# we have to use absolute urls in our cache resource, because fetch will return an absolute url
42+
# this means that we cannot accurately cache resources that are in PRs because RTD does not give us
43+
# the url
44+
if config["html_baseurl"] is not None:
45+
# readthedocs uses html_baseurl for sphinx > 1.8
46+
parse_result = urlparse(config["html_baseurl"])
47+
48+
# Grab root url from canonical url
49+
url = urlunparse(
50+
(
51+
parse_result.scheme,
52+
parse_result.netloc,
53+
parse_result.path,
54+
"",
55+
"",
56+
"",
57+
)
58+
)
59+
60+
# enables RTD multilanguage support
61+
if os.getenv("READTHEDOCS"):
62+
url = urljoin(parse_result.netloc, os.getenv("READTHEDOCS_LANGUAGE"), os.getenv("READTHEDOCS_PROJECT"))
63+
64+
if config["html_baseurl"] is None and not os.getenv("CI"):
65+
logger.warning(
66+
red(f"html_baseurl is not configured. This can be ignored if deployed in RTD environments.")
67+
)
68+
url = ""
69+
70+
if dirpath == "":
71+
resource_url = urljoin(
72+
url, name
73+
)
74+
logger.info(f"Caching {resource_url}")
75+
files_to_cache.append(resource_url)
76+
else:
77+
resource_url = url + dirpath + "/" + name
78+
79+
logger.info(f"Name is {name}")
80+
logger.info(f"Caching {resource_url}")
81+
files_to_cache.append(resource_url)
82+
83+
return files_to_cache
84+
85+
86+
def build_finished(app: Sphinx, exception: Exception):
87+
outDir = app.outdir
88+
outDirStatic = outDir + os.sep + "_static" + os.sep
89+
files_to_cache = get_files_to_cache(outDir, app.config)
90+
91+
# dumps a json file with our cache
92+
with open(outDirStatic + "cache.json", "w") as f:
93+
json.dump(files_to_cache, f)
94+
95+
# copies over our service worker
96+
shutil.copyfile(
97+
os.path.dirname(__file__) + os.sep + "pwa_service_files" + os.sep + "sw.js",
98+
outDir + os.sep + "sw.js",
99+
)
100+
101+
102+
def html_page_context(
103+
app: Sphinx,
104+
pagename: str,
105+
templatename: str,
106+
context: Dict[str, Any],
107+
doctree: nodes.document,
108+
) -> None:
109+
if pagename == "index":
110+
context[
111+
"metatags"
112+
] += '<script>"serviceWorker"in navigator&&navigator.serviceWorker.register("sw.js").catch((e) => window.alert(e));</script>'
113+
context[
114+
"metatags"
115+
] += f'<link rel="manifest" href="_static/frcdocs.webmanifest"/>'
116+
117+
118+
def setup(app: Sphinx) -> Dict[str, Any]:
119+
app.add_config_value("name", "", "html")
120+
app.add_config_value("short_name", "", "html")
121+
app.add_config_value("theme_color", "", "html")
122+
app.add_config_value("background_color", "", "html")
123+
app.add_config_value("display", "standalone", "html")
124+
app.add_config_value("icons", [], "html")
125+
126+
app.connect("html-page-context", html_page_context)
127+
app.connect("build-finished", build_finished)
128+
129+
return {
130+
"parallel_read_safe": True,
131+
"parallel_write_safe": True,
132+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"use strict";
2+
// extend this to update the service worker every push
3+
// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Offline_Service_workers
4+
let cacheName = 'js13kPWA-v1';
5+
6+
// todo test
7+
self.addEventListener('install', function (e) {
8+
e.waitUntil(async function() {
9+
await fetch('_static/cache.json')
10+
.then(response => response.json())
11+
.then(async (data) => {
12+
for (let i = 0; i < data.length / 10; i++) {
13+
const tofetch = data.slice(i * 10, i * 10 + 10);
14+
await addKeys(tofetch)
15+
}
16+
})
17+
.catch(error => {
18+
console.error(error);
19+
});
20+
}());
21+
});
22+
23+
// opt for a cache first response, for quickest load times
24+
// we'll still update the page assets in the background
25+
self.addEventListener('fetch', function (event) {
26+
event.respondWith(async function () {
27+
let request_url = event.request.url;
28+
29+
try {
30+
await addKeys([request_url]) //put in format our addKeys function expects
31+
} catch (error) {
32+
console.error("Error downloading from remote:", error.message)
33+
}
34+
35+
let res = await getKey(request_url)
36+
37+
console.log("Fetching:", event.request.url)
38+
return res;
39+
}());
40+
});
41+
42+
let dbPromise;
43+
44+
async function getDB() {
45+
if (dbPromise) {
46+
return dbPromise;
47+
} else {
48+
let request = indexedDB.open("frc-docs", "1")
49+
50+
dbPromise = new Promise((resolve, reject) => {
51+
request.onsuccess = function (event) {
52+
console.log("Successfully opened database!")
53+
resolve(event.target.result)
54+
}
55+
56+
request.onerror = function (event) {
57+
console.error("Error opening database for getKey():", request.error)
58+
reject()
59+
}
60+
61+
request.onupgradeneeded = function (event) {
62+
let db = event.target.result;
63+
db.createObjectStore("urls", { keyPath: 'key' })
64+
}
65+
});
66+
67+
return dbPromise;
68+
}
69+
}
70+
71+
async function getKey(key) {
72+
let db = await getDB()
73+
console.log("Grabbing key", key)
74+
return new Promise((resolve, reject) => {
75+
try {
76+
let transaction = db.transaction("urls").objectStore("urls");
77+
let request = transaction.get(key)
78+
79+
request.onsuccess = function (event) {
80+
let res = request.result;
81+
console.log("Successfully retrieved result:", res)
82+
resolve(new Response(res.value));
83+
}
84+
85+
request.onerror = function (event) {
86+
console.error("Error on retrieving blob:", key, request.error)
87+
reject()
88+
}
89+
90+
} catch (ex) {
91+
console.error(ex.message);
92+
reject()
93+
}
94+
})
95+
}
96+
97+
async function addKeys(datas) {
98+
let db = await getDB()
99+
return Promise.all(
100+
datas.map(async (data) => {
101+
let fetchedData = await fetch(data)
102+
.then(x => x.blob())
103+
.catch((error) => {
104+
console.error("Error fetching", data)
105+
return new Promise((resolve, reject) => {
106+
reject();
107+
})
108+
})
109+
let transaction = db.transaction("urls", "readwrite").objectStore("urls")
110+
let request = transaction.put({key: data, value: fetchedData})
111+
112+
return new Promise((resolve, reject) => {
113+
request.onsuccess = function() {
114+
resolve()
115+
}
116+
request.onerror = function () {
117+
console.log(request.error)
118+
reject(request.error)
119+
}
120+
});
121+
})
122+
);
123+
// data is already a key/value object with url/data
124+
}

source/_static/first-logo-256px.png

19.7 KB
Loading

source/_static/first-logo-512px.png

43.3 KB
Loading

source/_static/frcdocs.webmanifest

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "FRC Docs",
3+
"short_name": "FRC Docs",
4+
"theme_color": "#003974",
5+
"background_color": "#003974",
6+
"display": "standalone",
7+
"scope": "../",
8+
"start_url": "../index.html",
9+
"icons": [
10+
{
11+
"src": "/_static/first-logo-256px.png",
12+
"type": "image/png",
13+
"sizes": "256x256"
14+
},
15+
{
16+
"src": "/_static/first-logo-512px.png",
17+
"type": "image/png",
18+
"sizes": "512x512"
19+
}
20+
]
21+
}

source/conf.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"_extensions.post_process",
6060
"_extensions.rtd_patch",
6161
"_extensions.localization",
62+
"_extensions.pwa_service",
6263
]
6364

6465
extensions += local_extensions
@@ -198,7 +199,10 @@
198199

199200
# Specify canonical root
200201
# This tells search engines that this domain is preferred
201-
html_baseurl = "https://docs.wpilib.org/en/stable/"
202+
if os.getenv("TESTING"):
203+
html_baseurl = "http://localhost:8000/"
204+
else:
205+
html_baseurl = "https://frc-docs--1704.org.readthedocs.build/en/1704/"
202206

203207
html_theme_options = {
204208
"collapse_navigation": True,

0 commit comments

Comments
 (0)