-
-
Notifications
You must be signed in to change notification settings - Fork 419
Expand file tree
/
Copy pathmdn-annotation.js
More file actions
222 lines (205 loc) · 7.29 KB
/
mdn-annotation.js
File metadata and controls
222 lines (205 loc) · 7.29 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
// @ts-check
import { docLink, fetchAndCache, getIntlData, showError } from "./utils.js";
import css from "../styles/mdn-annotation.css.js";
import { html } from "./import-maps.js";
export const name = "core/mdn-annotation";
const BASE_JSON_PATH = "https://w3c.github.io/mdn-spec-links/";
const MDN_URL_BASE = "https://developer.mozilla.org/en-US/docs/Web/";
const MDN_BROWSERS = {
// The browser IDs here must match the ones in the imported JSON data.
// See the list of browser IDs at:
// https://github.com/mdn/browser-compat-data/blob/master/schemas/compat-data-schema.md#browser-identifiers.
chrome: "Chrome",
chrome_android: "Chrome Android",
edge: "Edge",
edge_mobile: "Edge Mobile",
firefox: "Firefox",
firefox_android: "Firefox Android",
// nodejs: "Node.js", // no data for features in HTML
opera: "Opera",
opera_android: "Opera Android",
// qq_android: "QQ Browser", // not enough data for features in HTML
safari: "Safari",
safari_ios: "Safari iOS",
samsunginternet_android: "Samsung Internet",
// uc_android: "UC browser", // not enough data for features in HTML
// uc_chinese_android: "Chinese UC Browser", // not enough data for features in HTML
webview_android: "WebView Android",
};
const localizationStrings = {
en: {
inAllEngines: "This feature is in all major engines.",
inSomeEngines: "This feature has limited support.",
},
zh: {
inAllEngines: "所有主要引擎均支持此特性。",
inSomeEngines: "此功能支持有限。",
},
cs: {
inAllEngines: "Tato funkce je podporována ve všech hlavních prohlížečích.",
inSomeEngines: "Tato funkce má omezenou podporu.",
},
};
const l10n = getIntlData(localizationStrings);
/**
* @param {HTMLElement} node
*/
function insertMDNBox(node) {
const targetAncestor = node.closest("section");
if (!targetAncestor) return;
const { previousElementSibling: targetSibling } = targetAncestor;
if (targetSibling && targetSibling.classList.contains("mdn")) {
// If the target ancestor already has a mdnBox inserted, we just use it
return targetSibling;
}
const mdnBox = html`<aside class="mdn"></aside>`;
targetAncestor.before(mdnBox);
return mdnBox;
}
/**
* @param {MdnEntry} mdnSpec
* @returns {HTMLDetailsElement}
*/
function attachMDNDetail(mdnSpec) {
const { name, slug, summary, support, engines } = mdnSpec;
const mdnSubPath = slug.slice(slug.indexOf("/") + 1);
const href = `${MDN_URL_BASE}${slug}`;
const label = `Expand MDN details for ${name}`;
const engineSupport = getEngineSupportIcons(engines);
return html`<details>
<summary aria-label="${label}"><span>MDN</span>${engineSupport}</summary>
<a title="${summary}" href="${href}">${mdnSubPath}</a>
${getEngineSupport(engines)}
${support
? buildBrowserSupportTable(support)
: html`<p class="nosupportdata">No support data.</p>`}
</details>`;
}
/**
* @param {MdnEntry['support']} support
* @returns {HTMLTableElement}
*/
function buildBrowserSupportTable(support) {
/**
* @param {string | keyof MDN_BROWSERS} browserId
* @param {"Yes" | "No" | "Unknown"} yesNoUnknown
* @param {string} version
* @returns {HTMLTableRowElement}
*/
function createRow(browserId, yesNoUnknown, version) {
const displayStatus = yesNoUnknown === "Unknown" ? "?" : yesNoUnknown;
const classList = `${browserId} ${yesNoUnknown.toLowerCase()}`;
return html`<tr class="${classList}">
<td>${MDN_BROWSERS[browserId]}</td>
<td>${version ? version : displayStatus}</td>
</tr>`;
}
/**
* @param {string | keyof MDN_BROWSERS} browserId
* @param {VersionDetails} versionData
*/
function createRowFromBrowserData(browserId, versionData) {
if (versionData.version_removed) {
return createRow(browserId, "No", "");
}
const versionAdded = versionData.version_added;
if (typeof versionAdded === "boolean") {
return createRow(browserId, versionAdded ? "Yes" : "No", "");
} else if (!versionAdded) {
return createRow(browserId, "Unknown", "");
} else {
return createRow(browserId, "Yes", `${versionAdded}+`);
}
}
return html`<table>
${Object.keys(MDN_BROWSERS).map(browserId => {
return support[browserId]
? createRowFromBrowserData(browserId, support[browserId])
: createRow(browserId, "Unknown", "");
})}
</table>`;
}
export async function run(conf) {
const mdnKey = getMdnKey(conf);
if (!mdnKey) return;
const mdnSpecJson = await getMdnData(mdnKey, conf.mdn);
if (!mdnSpecJson) return;
const style = document.createElement("style");
style.textContent = css;
document.head.append(style);
for (const elem of findElements(mdnSpecJson)) {
const mdnSpecArray = mdnSpecJson[elem.id];
const mdnBox = insertMDNBox(elem);
if (!mdnBox) continue;
for (const spec of mdnSpecArray) {
mdnBox.append(attachMDNDetail(spec));
}
}
}
/** @returns {string} */
function getMdnKey(conf) {
const { shortName, mdn } = conf;
if (!mdn) return;
if (typeof mdn === "string") return mdn;
return mdn.key || shortName;
}
/**
* @param {string} key MDN key
* @param {object} mdnConf
* @param {string} [mdnConf.specMapUrl]
* @param {string} [mdnConf.baseJsonPath]
* @param {number} [mdnConf.maxAge]
*
* @typedef {{ version_added: string|boolean|null, version_removed?: string }} VersionDetails
* @typedef {Record<string | keyof MDN_BROWSERS, VersionDetails>} MdnSupportEntry
* @typedef {{ name: string, title: string, slug: string, summary: string, support: MdnSupportEntry, engines: string[] }} MdnEntry
* @typedef {Record<string, MdnEntry[]>} MdnData
* @returns {Promise<MdnData|undefined>}
*/
async function getMdnData(key, mdnConf) {
const { baseJsonPath = BASE_JSON_PATH, maxAge = 60 * 60 * 24 * 1000 } =
mdnConf;
const url = new URL(`${key}.json`, baseJsonPath).href;
const res = await fetchAndCache(url, maxAge);
if (res.status === 404) {
const msg = `Could not find MDN data associated with key "${key}".`;
const hint = docLink`If using \`mdn: true\`, check that ${"[shortName]"} is set correctly — the MDN key defaults to the spec's shortName. Otherwise, search for your spec's URL in the [MDN spec links map](https://github.com/w3c/mdn-spec-links/blob/main/SPECMAP.json) to find the correct key, then set ${"[mdn]"} to it.`;
showError(msg, name, { hint });
return;
}
return await res.json();
}
/**
* Find elements that can have an annotation box attached.
* @param {MdnData} data
*/
function findElements(data) {
/** @type {NodeListOf<HTMLElement>} */
const elemsWithId = document.body.querySelectorAll("[id]:not(script)");
return [...elemsWithId].filter(({ id }) => Array.isArray(data[id]));
}
/**
* @param {MdnEntry['engines']} engines
* @returns {HTMLSpanElement}
*/
function getEngineSupportIcons(engines) {
if (engines.length === 3) {
return html`<span title="${l10n.inAllEngines}">✅</span>`;
}
if (engines.length < 2) {
return html`<span title="${l10n.inSomeEngines}">🚫</span>`;
}
return html`<span> </span>`;
}
/**
* @param {MdnEntry['engines']} engines
* @returns {HTMLParagraphElement|undefined}
*/
function getEngineSupport(engines) {
if (engines.length === 3) {
return html`<p class="engines-all">${l10n.inAllEngines}</p>`;
}
if (engines.length < 2) {
return html`<p class="engines-some">${l10n.inSomeEngines}</p>`;
}
}