Repository navigation
Expand file tree
/
Copy pathcustom.js
More file actions
346 lines (300 loc) · 11.4 KB
/
Copy pathcustom.js
File metadata and controls
346 lines (300 loc) · 11.4 KB
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
function getUrlParameter(name) {
name = name.replace(/[\[\]]/g, "\\$&");
let regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)");
let results = regex.exec(window.location.href);
if (!results) return null;
if (!results[2]) return "";
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
function initializeLanguage(forcedLang = null) {
let lang = forcedLang || getUrlParameter("lang");
if (lang !== "js") {
lang = "ts";
}
// set top-level CSS class based on current language
document.body.classList.remove("page-language-js", "page-language-ts"); // for later switching
document.body.classList.add("page-language-" + lang);
return lang;
}
/* this function finds all tags which have both CSS classes "language-ts" and "highlighter-rouge"
and have exactly one direct neighbor sibling tag, which has the classes "language-ts" and "highlighter-rouge".
It then wraps both tags in a new div tag. */
function boxJSTSCouples() {
const tsTags = document.querySelectorAll(".language-ts");
tsTags.forEach(function (tsTag) {
const nextSibling = getNextSibling(tsTag, "js");
const previousSibling = getPreviousSibling(tsTag, "js");
if (nextSibling && previousSibling) {
// three subsequent code blocks, not clear what belongs to what
return;
} else if (!nextSibling && !previousSibling) {
// no direct sibling code block with different language
return;
} else if (nextSibling && getNextSibling(nextSibling)) {
// three subsequent code blocks, not clear what belongs to what
return;
} else if (previousSibling && getPreviousSibling(previousSibling)) {
// three subsequent code blocks, not clear what belongs to what
return;
}
const jsTag = nextSibling || previousSibling;
// we have two direct sibling code blocks with different languages; wrap them in a new div tag with nice switch button
const wrapper = document.createElement("div");
wrapper.classList.add("code-couple");
const tsButton = document.createElement("button");
const jsButton = document.createElement("button");
tsButton.classList.add("code-couple-button");
tsButton.classList.add("code-couple-button-ts");
tsButton.textContent = "TypeScript";
tsButton.addEventListener("click", function () {
// TODO: lots of redundant code to the one below
switchCodeCouple(wrapper, "ts");
});
wrapper.appendChild(tsButton);
jsButton.classList.add("code-couple-button");
jsButton.classList.add("code-couple-button-js");
jsButton.textContent = "JavaScript";
jsButton.addEventListener("click", function () {
switchCodeCouple(wrapper, "js");
});
wrapper.appendChild(jsButton);
tsTag.parentNode.insertBefore(wrapper, tsTag); // do this before tsTag is moved inside the wrapper
const wrapperContainer = document.createElement("div");
wrapperContainer.classList.add("code-couple-container");
wrapperContainer.appendChild(tsTag);
wrapperContainer.appendChild(jsTag);
wrapper.appendChild(wrapperContainer);
});
}
function switchCodeCouple(wrapper, lang) {
const tsTag = wrapper.querySelector(".language-ts");
const jsTag = wrapper.querySelector(".language-js");
const tsButton = wrapper.querySelector(".code-couple-button-ts");
const jsButton = wrapper.querySelector(".code-couple-button-js");
tsTag.style.display = lang === "ts" ? "block" : "none";
jsTag.style.display = lang === "js" ? "block" : "none";
tsButton.classList.toggle("code-couple-button-active", lang === "ts");
tsButton.classList.toggle("code-couple-button-inactive", lang === "js");
jsButton.classList.toggle("code-couple-button-active", lang === "js");
jsButton.classList.toggle("code-couple-button-inactive", lang === "ts");
wrapper.dataset.activeLang = lang;
}
function resetCodeCoupleButtons() {
const buttons = document.querySelectorAll(".code-couple-button");
buttons.forEach((button) => {
button.classList.remove(
"code-couple-button-active",
"code-couple-button-inactive"
);
});
}
function getPreviousSibling(tag, lang) {
const previousSibling =
tag.previousElementSibling &&
(tag.previousElementSibling.classList.contains("highlighter-rouge") ||
tag.nextElementSibling.classList.contains("hljs"))
? tag.previousElementSibling
: null;
if (
!lang ||
(previousSibling &&
(previousSibling.classList.contains("language-" + lang) ||
previousSibling.classList.contains("highlight-source-" + lang)))
) {
// success if lang does not matter or lang is as requested
return previousSibling;
}
return null;
}
function getNextSibling(tag, lang) {
const nextSibling =
tag.nextElementSibling &&
(tag.nextElementSibling.classList.contains("highlighter-rouge") ||
tag.nextElementSibling.classList.contains("hljs"))
? tag.nextElementSibling
: null;
if (
!lang ||
(nextSibling &&
(nextSibling.classList.contains("language-" + lang) ||
nextSibling.classList.contains("highlight-source-" + lang)))
) {
// success if lang does not matter or lang is as requested
return nextSibling;
}
return null;
}
/**
* This function finds all <details> tags with either the CSS class "ts-only" or "js-only" and:
* 1. removes their <summary> tag
* 2. replaces the <details> tag with a <section> tagr
*/
function replaceDetailSections() {
const detailTags = document.querySelectorAll(
"details.ts-only, details.js-only"
);
detailTags.forEach(function (detailTag) {
// create a new section tag before the detail tag
const sectionTag = document.createElement("section");
detailTag.parentNode.insertBefore(sectionTag, detailTag);
// copy over the either ts-only or js-only class to the new section tag
const lang = detailTag.classList.contains("ts-only") ? "ts" : "js";
sectionTag.classList.add(lang + "-only");
// move all children of the detail tag to the new section tag, except the <summary> tag
const children = Array.from(detailTag.children);
children.forEach(function (child) {
if (child.tagName.toLocaleUpperCase() === "SUMMARY") {
return;
}
// only add the child if it has content
if (child.innerHTML.trim()) {
sectionTag.appendChild(child);
}
});
// remove the detail tag
detailTag.parentNode.removeChild(detailTag);
});
}
function replaceFileExtensions(lang) {
const replacement =
"<span class='ts-only'>.ts</span><span class='js-only'>.js</span>";
// The `.ts/.js` toggle token only represents a *complete* file extension.
// Requiring a non-word boundary after `.js` prevents a mis-authored token
// that is really part of a longer extension (e.g. `manifest.ts/.js` + `on`,
// a leftover from tokenizing `.json`) from being swapped into a corrupt
// filename like `manifest.tson` when TypeScript is active.
const tokenPattern = /\.ts\/\.js(?![A-Za-z0-9])/g;
// select all text nodes in the body
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
let node = walker.nextNode();
// iterate over each text node
while (node) {
let nextNode = walker.nextNode();
if (tokenPattern.test(node.nodeValue)) {
const temp = document.createElement("div");
temp.innerHTML = node.nodeValue.replace(tokenPattern, replacement);
const fragment = document.createDocumentFragment();
while (temp.firstChild) {
fragment.appendChild(temp.firstChild);
}
node.parentNode.replaceChild(fragment, node);
} else if (node.nodeValue.includes(".\\ts\\/\\.js")) {
node.nodeValue = node.nodeValue.replace(/\.\\ts\\\/\\\.js/g, ".ts/.js");
}
node = nextNode;
}
}
function updatePrevNextLinks(lang) {
const links = document.querySelectorAll('a[href$="README.md"], a[href$="/"], a[href$="README.md?lang=js"], a[href$="/?lang=js"]');
links.forEach(link => {
// Only handle relative links (not starting with http://, https://, or //)
if (/^(https?:)?\/\//.test(link.getAttribute("href"))) {
return;
}
let url = new URL(link.href, window.location.origin);
// Remove any existing lang param
url.searchParams.delete("lang");
if (lang === "js") {
url.searchParams.set("lang", "js");
}
link.href = url.pathname + url.search + url.hash;
});
}
// dynamic overall language switching
function addLanguageSwitchButtons() {
const buttonContainer = document.createElement("div");
buttonContainer.classList.add("language-switch-container");
const tsButton = createLanguageButton("TS", "ts");
const jsButton = createLanguageButton("JS", "js");
buttonContainer.appendChild(tsButton);
buttonContainer.appendChild(jsButton);
document.body.appendChild(buttonContainer);
}
function createLanguageButton(text, lang) {
const button = document.createElement("button");
button.textContent = text;
button.classList.add("language-switch-button");
button.classList.add("lang-" + lang);
button.addEventListener("click", () => switchLanguage(lang));
return button;
}
function switchLanguage(newLang) {
const lang = initializeLanguage(newLang);
replaceFileExtensions(lang);
resetCodeCoupleButtons();
updateAllCodeCouples(lang);
updatePrevNextLinks(lang);
}
function updateAllCodeCouples(globalLang) {
const codeCouples = document.querySelectorAll(".code-couple");
codeCouples.forEach((couple) => {
switchCodeCouple(couple, globalLang);
});
}
// initialization on startup
/**
* Render each content image's `alt` text as a visible caption beneath it,
* mirroring the SAP Demo Kit behavior that was lost when the tutorials moved
* to GitHub Pages. The caption text lives only in the image's `alt` attribute
* (also used by screen readers and as the broken-image fallback), so there is
* a single source of truth.
*
* Idempotent: an image already wrapped in <figure class="img-figure"> is
* skipped, so re-running (e.g. after a language switch) does not duplicate.
*/
function renderImageCaptions() {
const images = document.querySelectorAll("img");
images.forEach((img) => {
const caption = (img.getAttribute("alt") || "").trim();
if (!caption) {
return; // no alt text -> no caption
}
if (img.closest("figure.img-figure")) {
return; // already processed
}
// Skip link-wrapped images. In the tutorials these are badges / icons
// (e.g. the REUSE status badge `[](…)`), never a preview
// screenshot — content images are always plain ``. A
// linked image should not get a caption.
if (img.parentElement && img.parentElement.tagName === "A") {
return;
}
const host = img.parentElement;
if (!host) {
return;
}
// Only caption block-level "preview" images — those that are the sole
// meaningful content of their containing block. This skips inline
// images sitting inside a line of prose (badges, inline icons), which
// should not get a caption. `host.textContent` is empty for a
// standalone image because the <img> contributes no text.
if (host.textContent.trim() !== "") {
return;
}
const figure = document.createElement("figure");
figure.classList.add("img-figure");
const figcaption = document.createElement("figcaption");
figcaption.classList.add("img-caption");
figcaption.textContent = caption;
img.parentNode.insertBefore(figure, img);
figure.appendChild(img);
figure.appendChild(figcaption);
});
}
document.addEventListener("DOMContentLoaded", (event) => {
setTimeout(() => {
const lang = initializeLanguage();
replaceDetailSections();
boxJSTSCouples(); // should happen after replaceDetailSections, so all couples are recognized
replaceFileExtensions(lang);
addLanguageSwitchButtons();
updateAllCodeCouples(lang);
updatePrevNextLinks(lang);
renderImageCaptions();
});
});