Skip to content

Commit eb1234d

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

File tree

7 files changed

+275
-1
lines changed

7 files changed

+275
-1
lines changed

source/_extensions/pwa_service.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 = parse_result.netloc
50+
51+
# enables RTD multilanguage support
52+
if os.getenv("READTHEDOCS"):
53+
url = "https://" + url + "/" + os.getenv("READTHEDOCS_LANGUAGE") + "/" + os.getenv("READTHEDOCS_VERSION") + "/"
54+
55+
if config["html_baseurl"] is None and not os.getenv("CI"):
56+
logger.warning(
57+
red(f"html_baseurl is not configured. This can be ignored if deployed in RTD environments.")
58+
)
59+
url = ""
60+
61+
if dirpath == "":
62+
resource_url = urljoin(
63+
url, name
64+
)
65+
files_to_cache.append(resource_url)
66+
else:
67+
resource_url = url + dirpath + "/" + name
68+
files_to_cache.append(resource_url)
69+
70+
return files_to_cache
71+
72+
73+
def build_finished(app: Sphinx, exception: Exception):
74+
outDir = app.outdir
75+
outDirStatic = outDir + os.sep + "_static" + os.sep
76+
files_to_cache = get_files_to_cache(outDir, app.config)
77+
78+
# dumps a json file with our cache
79+
with open(outDirStatic + "cache.json", "w") as f:
80+
json.dump(files_to_cache, f)
81+
82+
# copies over our service worker
83+
shutil.copyfile(
84+
os.path.dirname(__file__) + os.sep + "pwa_service_files" + os.sep + "sw.js",
85+
outDir + os.sep + "sw.js",
86+
)
87+
88+
89+
def html_page_context(
90+
app: Sphinx,
91+
pagename: str,
92+
templatename: str,
93+
context: Dict[str, Any],
94+
doctree: nodes.document,
95+
) -> None:
96+
if pagename == "index":
97+
context[
98+
"metatags"
99+
] += '<script>"serviceWorker"in navigator&&navigator.serviceWorker.register("sw.js").catch((e) => window.alert(e));</script>'
100+
context[
101+
"metatags"
102+
] += f'<link rel="manifest" href="_static/frcdocs.webmanifest"/>'
103+
104+
if app.config["pwa_apple_icon"] is not None:
105+
context[
106+
"metatags"
107+
] += f'<link rel="apple-touch-icon" href="{app.config["pwa_apple_icon"]}">'
108+
109+
110+
def setup(app: Sphinx) -> Dict[str, Any]:
111+
app.add_config_value("pwa_name", "", "html")
112+
app.add_config_value("pwa_short_name", "", "html")
113+
app.add_config_value("pwa_theme_color", "", "html")
114+
app.add_config_value("pwa_background_color", "", "html")
115+
app.add_config_value("pwa_display", "standalone", "html")
116+
app.add_config_value("pwa_icons", [], "html")
117+
app.add_config_value("pwa_apple_icon", "", "html")
118+
119+
app.connect("html-page-context", html_page_context)
120+
app.connect("build-finished", build_finished)
121+
122+
return {
123+
"parallel_read_safe": True,
124+
"parallel_write_safe": True,
125+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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+
}());
18+
});
19+
20+
// opt for a cache first response, for quickest load times
21+
// we'll still update the page assets in the background
22+
self.addEventListener('fetch', function (event) {
23+
event.respondWith(async function () {
24+
let request_url = event.request.url;
25+
26+
try {
27+
await addKeys([request_url]) //put in format our addKeys function expects
28+
} catch (error) {
29+
console.error("Error downloading from remote:", error)
30+
}
31+
32+
let res = await getKey(request_url)
33+
34+
console.log("Fetching:", event.request.url)
35+
return res;
36+
}());
37+
});
38+
39+
let dbPromise;
40+
41+
async function getDB() {
42+
if (dbPromise) {
43+
return dbPromise;
44+
} else {
45+
let request = indexedDB.open("frc-docs", "1")
46+
47+
dbPromise = new Promise((resolve, reject) => {
48+
request.onsuccess = function (event) {
49+
console.log("Successfully opened database!")
50+
resolve(event.target.result)
51+
}
52+
53+
request.onerror = function (event) {
54+
console.error("Error opening database for getKey():", request.error)
55+
reject()
56+
}
57+
58+
request.onupgradeneeded = function (event) {
59+
let db = event.target.result;
60+
db.createObjectStore("urls", { keyPath: 'key' })
61+
}
62+
});
63+
64+
return dbPromise;
65+
}
66+
}
67+
68+
async function getKey(key) {
69+
let db = await getDB()
70+
console.log("Grabbing key", key)
71+
return new Promise((resolve, reject) => {
72+
try {
73+
let transaction = db.transaction("urls").objectStore("urls");
74+
let request = transaction.get(key)
75+
76+
request.onsuccess = function (event) {
77+
let res = request.result;
78+
console.log("Successfully retrieved result:", res)
79+
resolve(new Response(res.value));
80+
}
81+
82+
request.onerror = function (event) {
83+
console.error("Error on retrieving blob:", key, request.error)
84+
reject()
85+
}
86+
87+
} catch (ex) {
88+
console.error(ex.message);
89+
reject()
90+
}
91+
})
92+
}
93+
94+
async function addKeys(datas) {
95+
let db = await getDB()
96+
return Promise.all(
97+
datas.map(async (data) => {
98+
let fetchedData = await fetch(data)
99+
.then(x => x.blob())
100+
.catch((error) => {
101+
console.error("Error fetching", data)
102+
return new Promise((resolve, reject) => {
103+
reject();
104+
})
105+
})
106+
let transaction = db.transaction("urls", "readwrite").objectStore("urls")
107+
let request = transaction.put({key: data, value: fetchedData})
108+
109+
return new Promise((resolve, reject) => {
110+
request.onsuccess = function() {
111+
resolve()
112+
}
113+
request.onerror = function () {
114+
console.log(request.error)
115+
reject(request.error)
116+
}
117+
});
118+
})
119+
);
120+
// data is already a key/value object with url/data
121+
}

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/_static/touch-icon.png

30.1 KB
Loading

source/conf.py

Lines changed: 8 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
@@ -177,6 +178,9 @@
177178
# Use MathJax3 for better page loading times
178179
mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
179180

181+
# PWA Specific Settings
182+
pwa_apple_icon = "_static/touch-icon.png"
183+
180184

181185
# -- Options for HTML output -------------------------------------------------
182186

@@ -198,7 +202,10 @@
198202

199203
# Specify canonical root
200204
# This tells search engines that this domain is preferred
201-
html_baseurl = "https://docs.wpilib.org/en/stable/"
205+
if os.getenv("TESTING"):
206+
html_baseurl = "http://localhost:8000/"
207+
else:
208+
html_baseurl = "https://frc-docs--1704.org.readthedocs.build/en/1704/"
202209

203210
html_theme_options = {
204211
"collapse_navigation": True,

0 commit comments

Comments
 (0)