Repository navigation
Expand file tree
/
Copy pathgithub-source.ts
More file actions
117 lines (109 loc) · 3.53 KB
/
Copy pathgithub-source.ts
File metadata and controls
117 lines (109 loc) · 3.53 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
// Reads the current file through GitHub's page payload or authenticated raw URL.
(() => {
'use strict';
const RAW_BUTTON_SELECTOR = '[data-testid="raw-button"]';
const EMBEDDED_DATA_SELECTOR =
'script[type="application/json"][data-target="react-app.embeddedData"]';
const MAX_SOURCE_BYTES = 5 * 1024 * 1024;
const MAX_CACHED_SOURCES = 8;
const sourceCache = new Map<string, string>();
async function loadHtmlSource(): Promise<HtmlSource> {
const rawLink =
document.querySelector<HTMLAnchorElement>(RAW_BUTTON_SELECTOR);
const rawUrl = rawLink
? NativeHtmlPreviewSecurity.trustedRawUrl(
rawLink.href,
window.location.href
)
: '';
const embedded = readEmbeddedSource();
if (embedded) {
assertSourceSize(embedded);
return { html: embedded, baseUrl: rawUrl || window.location.href };
}
if (!rawUrl) {
throw new Error('GitHub did not provide the HTML source URL');
}
const cached = sourceCache.get(rawUrl);
if (cached !== undefined) {
return { html: cached, baseUrl: rawUrl };
}
const response = await fetch(rawUrl, { credentials: 'same-origin' });
if (!response.ok) {
throw new Error(`Could not load HTML source (HTTP ${response.status})`);
}
const declaredSize = Number(response.headers.get('Content-Length'));
if (Number.isFinite(declaredSize) && declaredSize > MAX_SOURCE_BYTES) {
throw new Error('HTML source exceeds the 5 MB preview limit');
}
const html = await response.text();
assertSourceSize(html);
if (sourceCache.size >= MAX_CACHED_SOURCES) {
sourceCache.clear();
}
sourceCache.set(rawUrl, html);
return {
html,
baseUrl: NativeHtmlPreviewSecurity.trustedRawUrl(
response.url || rawUrl,
window.location.href
),
};
}
function assertSourceSize(html: string): void {
if (new Blob([html]).size > MAX_SOURCE_BYTES) {
throw new Error('HTML source exceeds the 5 MB preview limit');
}
}
function readEmbeddedSource(): string | null {
for (const script of document.querySelectorAll(EMBEDDED_DATA_SELECTOR)) {
if (!script.textContent) {
continue;
}
try {
const payload: unknown = JSON.parse(script.textContent);
const path = findProperty(
payload,
'path',
(value): value is string => typeof value === 'string'
);
if (!path || !window.location.pathname.endsWith(`/${path}`)) {
continue;
}
const lines = findProperty(payload, 'rawLines', Array.isArray);
if (lines?.every((line) => typeof line === 'string')) {
return lines.join('\n');
}
} catch {}
}
return null;
}
function findProperty<T>(
payload: unknown,
property: string,
accepts: (value: unknown) => value is T
): T | null {
let found: T | null = null;
const visit = (node: unknown): void => {
if (found !== null || !node || typeof node !== 'object') {
return;
}
const record = node as Record<string, unknown>;
if (property in record && accepts(record[property])) {
found = record[property];
return;
}
for (const value of Object.values(record)) {
visit(value);
}
};
visit(payload);
return found;
}
const api: NativeHtmlPreviewGitHubApi = Object.freeze({ loadHtmlSource });
(
globalThis as typeof globalThis & {
NativeHtmlPreviewGitHub: NativeHtmlPreviewGitHubApi;
}
).NativeHtmlPreviewGitHub = api;
})();