-
Notifications
You must be signed in to change notification settings - Fork 37
/
force-readthedocs-addons.js
233 lines (200 loc) · 9.58 KB
/
force-readthedocs-addons.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
/*
Script to inject the new Addons implementation on pages served by El Proxito.
This script is ran on a Cloudflare Worker and modifies the HTML with two different purposes:
1. remove the old implementation of our flyout (``readthedocs-doc-embed.js`` and others)
2. inject the new addons implementation (``readthedocs-addons.js``) script
Currently, we are doing 1) only when users opt-in into the new beta addons.
In the future, when our addons become stable, we will always remove the old implementation,
making all the projects to use the addons by default.
*/
// add "readthedocs-addons.js" inside the "<head>"
const addonsJs =
'<script async type="text/javascript" src="/_/static/javascript/readthedocs-addons.js"></script>';
// selectors we want to remove
// https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/#selectors
const analyticsJs =
'script[src="/_/static/javascript/readthedocs-analytics.js"]';
const docEmbedCss = 'link[href="/_/static/css/readthedocs-doc-embed.css"]';
const docEmbedJs =
'script[src="/_/static/javascript/readthedocs-doc-embed.js"]';
const analyticsJsAssets =
'script[src="https://assets.readthedocs.org/static/javascript/readthedocs-analytics.js"]';
const docEmbedCssAssets =
'link[href="https://assets.readthedocs.org/static/css/readthedocs-doc-embed.css"]';
const docEmbedJsAssets =
'script[src="https://assets.readthedocs.org/static/javascript/readthedocs-doc-embed.js"]';
const docEmbedJsAssetsCore =
'script[src="https://assets.readthedocs.org/static/core/js/readthedocs-doc-embed.js"]';
const docEmbedJsAssetsProxied =
'script[src="/_/static/core/js/readthedocs-doc-embed.js"]';
const badgeOnlyCssAssets =
'link[href="https://assets.readthedocs.org/static/css/badge_only.css"]';
const badgeOnlyCssAssetsProxied = 'link[href="/_/static/css/badge_only.css"]';
const readthedocsExternalVersionWarning =
"[role=main] > div:first-child > div:first-child.admonition.warning";
const readthedocsExternalVersionWarningFuroTheme =
"[role=main] > div:first-child.admonition.warning";
const readthedocsExternalVersionWarningBookTheme =
"#main-content > div > div > article > div:first-child.admonition.warning";
const readthedocsFlyout = "div.rst-versions";
// "readthedocsDataParse" is the "<script>" that calls:
//
// READTHEDOCS_DATA = JSON.parse(document.getElementById('READTHEDOCS_DATA').innerHTML);
//
const readthedocsDataParse = "script[id=READTHEDOCS_DATA]:first-of-type";
const readthedocsData = "script[id=READTHEDOCS_DATA]";
// do this on a fetch
addEventListener("fetch", (event) => {
const request = event.request;
event.respondWith(handleRequest(request));
});
async function handleRequest(request) {
// perform the original request
let originalResponse = await fetch(request);
// get the content type of the response to manipulate the content only if it's HTML
const contentType = originalResponse.headers.get("content-type") || "";
const injectHostingIntegrations =
originalResponse.headers.get("x-rtd-hosting-integrations") || "false";
const forceAddons =
originalResponse.headers.get("x-rtd-force-addons") || "false";
// Log some debugging data
console.log(`ContentType: ${contentType}`);
console.log(`X-RTD-Force-Addons: ${forceAddons}`);
console.log(`X-RTD-Hosting-Integrations: ${injectHostingIntegrations}`);
// get project/version slug from headers inject by El Proxito
const projectSlug = originalResponse.headers.get("x-rtd-project") || "";
const versionSlug = originalResponse.headers.get("x-rtd-version") || "";
const resolverFilename = originalResponse.headers.get("x-rtd-resolver-filename") || "";
// check to decide whether or not inject the new beta addons:
//
// - content type has to be "text/html"
// when all these conditions are met, we remove all the old JS/CSS files and inject the new beta flyout JS
// check if the Content-Type is HTML, otherwise do nothing
if (contentType.includes("text/html")) {
// Remove old implementation of our flyout and inject the new addons if the following conditions are met:
//
// - header `X-RTD-Force-Addons` is present (user opted-in into new beta addons)
// - header `X-RTD-Hosting-Integrations` is not present (added automatically when using `build.commands`)
//
if (forceAddons === "true" && injectHostingIntegrations === "false") {
return (
new HTMLRewriter()
.on(analyticsJs, new removeElement())
.on(docEmbedCss, new removeElement())
.on(docEmbedJs, new removeElement())
.on(analyticsJsAssets, new removeElement())
.on(docEmbedCssAssets, new removeElement())
.on(docEmbedJsAssets, new removeElement())
.on(docEmbedJsAssetsCore, new removeElement())
.on(docEmbedJsAssetsProxied, new removeElement())
.on(badgeOnlyCssAssets, new removeElement())
.on(badgeOnlyCssAssetsProxied, new removeElement())
.on(readthedocsExternalVersionWarning, new removeElement())
.on(readthedocsExternalVersionWarningFuroTheme, new removeElement())
.on(readthedocsExternalVersionWarningBookTheme, new removeElement())
.on(readthedocsFlyout, new removeElement())
// NOTE: I wasn't able to reliably remove the "<script>" that parses
// the "READTHEDOCS_DATA" defined previously, so we are keeping it for now.
//
// .on(readthedocsDataParse, new removeElement())
// .on(readthedocsData, new removeElement())
.on("head", new addPreloads())
.on("head", new addMetaTags(projectSlug, versionSlug, resolverFilename))
.transform(originalResponse)
);
}
// Inject the new addons if the following conditions are met:
//
// - header `X-RTD-Hosting-Integrations` is present (added automatically when using `build.commands`)
// - header `X-RTD-Force-Addons` is not present (user opted-in into new beta addons)
//
if (forceAddons === "false" && injectHostingIntegrations === "true") {
return new HTMLRewriter()
.on("head", new addPreloads())
.on("head", new addMetaTags(projectSlug, versionSlug, resolverFilename))
.transform(originalResponse);
}
}
// Modify `_static/searchtools.js` to re-enable Sphinx's default search
if (
(contentType.includes("text/javascript") ||
contentType.includes("application/javascript")) &&
(injectHostingIntegrations === "true" || forceAddons === "true") &&
originalResponse.url.endsWith("_static/searchtools.js")
) {
console.log("Modifying _static/searchtools.js");
return handleSearchToolsJSRequest(originalResponse);
}
// if none of the previous conditions are met,
// we return the response without modifying it
return originalResponse;
}
class removeElement {
element(element) {
console.log("Removing: " + element.tagName);
console.log("Attribute href: " + element.getAttribute("href"));
console.log("Attribute src: " + element.getAttribute("src"));
console.log("Attribute id: " + element.getAttribute("id"));
console.log("Attribute class: " + element.getAttribute("class"));
element.remove();
}
}
class addPreloads {
element(element) {
console.log("addPreloads");
element.append(addonsJs, { html: true });
}
}
class addMetaTags{
constructor(projectSlug, versionSlug, resolverFilename) {
this.projectSlug = projectSlug;
this.versionSlug = versionSlug;
this.resolverFilename = resolverFilename;
}
element(element) {
console.log(
`addProjectVersionSlug. projectSlug=${this.projectSlug} versionSlug=${this.versionSlug} resolverFilename=${this.resolverFilename}`,
);
if (this.projectSlug && this.versionSlug) {
const metaProject = `<meta name="readthedocs-project-slug" content="${this.projectSlug}" />`;
const metaVersion = `<meta name="readthedocs-version-slug" content="${this.versionSlug}" />`;
const metaResolverFilename = `<meta name="readthedocs-resolver-filename" content="${this.resolverFilename}" />`;
element.append(metaProject, { html: true });
element.append(metaVersion, { html: true });
element.append(metaResolverFilename, { html: true });
}
}
}
/*
Script to fix the old removal of the Sphinx search init.
Enabling addons breaks the default Sphinx search in old versions that are not possible to rebuilt.
This is because we solved the problem in the `readthedocs-sphinx-ext` extension,
but since those versions can't be rebuilt, the fix does not apply there.
To solve the problem in these old versions, we are using a CF worker to apply that fix on-the-fly
at serving time on those old versions.
The fix basically replaces a Read the Docs comment in file `_static/searchtools.js`,
introduced by `readthedocs-sphinx-ext` to _disable the initialization of Sphinx search_,
with the real JavaScript to initialize the search, as Sphinx does by default.
(in other words, it _reverts_ the manipulation done by `readthedocs-sphinx-ext`)
*/
const textToReplace = `/* Search initialization removed for Read the Docs */`;
const textReplacement = `
/* Search initialization manipulated by Read the Docs using Cloudflare Workers */
/* See https://github.com/readthedocs/addons/issues/219 for more information */
function initializeSearch() {
Search.init();
}
if (document.readyState !== "loading") {
initializeSearch();
}
else {
document.addEventListener("DOMContentLoaded", initializeSearch);
}
`;
async function handleSearchToolsJSRequest(originalResponse) {
const content = await originalResponse.text();
const modifiedResponse = new Response(
content.replace(textToReplace, textReplacement),
);
return modifiedResponse;
}