diff --git a/bikeshed/mdn/mdn-anno.css b/bikeshed/mdn/mdn-anno.css
index bae5f910b9..4c875e5579 100644
--- a/bikeshed/mdn/mdn-anno.css
+++ b/bikeshed/mdn/mdn-anno.css
@@ -71,11 +71,10 @@
vertical-align: top;
}
.mdn-anno > .feature {
- margin-top: 20px;
+ margin-top: 6px;
}
.mdn-anno > .feature:not(:first-of-type) {
border-top: 1px solid #999;
- margin-top: 6px;
padding-top: 2px;
}
.mdn-anno > .feature > .less-than-two-engines-text {
diff --git a/bikeshed/mdn/mdn-anno.js b/bikeshed/mdn/mdn-anno.js
new file mode 100644
index 0000000000..689d953759
--- /dev/null
+++ b/bikeshed/mdn/mdn-anno.js
@@ -0,0 +1,202 @@
+'use strict';
+// File copied from https://github.com/whatwg/whatwg.org/blob/main/resources.whatwg.org/standard-mdn-annos.js
+
+// Builds MDN annotation panels on demand from the compact `data-mdn` attribute written by
+// the spec generator (wattsi's EncodeMDNAnnotations; Bikeshed's mdnspeclinks.py builds its
+// panels eagerly for now). Only the wire format is shared: the browser labels, the
+// engine-support strings and the render grouping all live here.
+//
+// Archived commit snapshots load this file forever, so every format version this script
+// has ever understood has to keep working. Evolve the format by adding a branch in
+// `hydrate`, never by redefining a version that has already shipped.
+
+const MDN_URL = 'https://developer.mozilla.org/en-US/docs/Web/';
+const CANIUSE_URL = 'https://caniuse.com/#feat=';
+
+// Render groups, in the order they appear in a panel, separated by
.
+const CURRENT_ENGINES = 0;
+const BORROWED_ENGINES = 1;
+const RETIRED_ENGINES = 2;
+const MOBILE = 3;
+const NON_BROWSER = 4;
+const GROUP_COUNT = 5;
+
+// Cell slots, in the order they appear in `data-mdn`. APPEND-ONLY: a new browser goes at
+// the end and is never inserted, so that old data still lines up and old copies of this
+// script can keep ignoring the cells they don't know about. `group` is what decides where
+// a row is rendered, so appending to this list doesn't constrain where the row shows up.
+const SLOTS = [
+ { id: 'firefox', label: 'Firefox', group: CURRENT_ENGINES },
+ { id: 'safari', label: 'Safari', group: CURRENT_ENGINES },
+ { id: 'chrome', label: 'Chrome', group: CURRENT_ENGINES },
+ { id: 'opera', label: 'Opera', group: BORROWED_ENGINES, mirrors: 'Chrome' },
+ { id: 'edge_blink', label: 'Edge', group: BORROWED_ENGINES },
+ { id: 'edge', label: 'Edge (Legacy)', group: RETIRED_ENGINES },
+ { id: 'ie', label: 'IE', group: RETIRED_ENGINES },
+ { id: 'firefox_android', label: 'Firefox for Android', group: MOBILE, mirrors: 'Firefox' },
+ { id: 'safari_ios', label: 'iOS Safari', group: MOBILE, mirrors: 'Safari' },
+ { id: 'chrome_android', label: 'Chrome for Android', group: MOBILE, mirrors: 'Chrome' },
+ { id: 'webview_android', label: 'Android WebView', group: MOBILE, mirrors: 'Chrome' },
+ { id: 'samsunginternet_android', label: 'Samsung Internet', group: MOBILE, mirrors: 'Chrome' },
+ { id: 'opera_android', label: 'Opera Mobile', group: MOBILE, mirrors: 'Chrome' },
+ { id: 'nodejs', label: 'Node.js', group: NON_BROWSER },
+];
+
+// One mnemonic character per engine-support note.
+const LEVELS = {
+ '0': { class: 'less-than-two-engines-text', text: 'In no current engines.' },
+ '1': { class: 'less-than-two-engines-text', text: 'In only one current engine.' },
+ '9': { class: 'all-engines-text', text: 'In all current engines.' },
+ 's': { class: 'less-than-two-engines-text', text: 'Support in one engine under other name.' },
+ 'S': { class: 'less-than-two-engines-text', text: 'Support in some engines under other name.' },
+ 'v': { class: 'less-than-two-engines-text', text: 'Prefixed support in one engine.' },
+ 'V': { class: 'less-than-two-engines-text', text: 'Prefixed support in some engines.' },
+ 'p': { class: 'less-than-two-engines-text', text: 'Partial support in one engine.' },
+ 'P': { class: 'less-than-two-engines-text', text: 'Partial support in some engines.' },
+};
+
+// Leading character of a cell, qualifying how the version after it should be read. `flag`
+// is whether to mark the version with đ° as well as explaining it in a tooltip.
+const CAVEATS = {
+ '*': { flag: true, title: () => 'Partial implementation.' },
+ '^': { flag: true, title: () => 'Requires setting a user preference or runtime flag.' },
+ '$': { flag: true, title: () => 'Requires a prefix or alternative name.' },
+ // Mirrored support is far too common to flag every occurrence; the tooltip is enough.
+ '@': { flag: false, title: (slot) => `Support is mirrored from ${slot.mirrors ?? 'the upstream browser'}.` },
+};
+
+const FLAG = '\u{1F530}'; // đ°, marks a version that comes with a caveat
+const EN_DASH = 'â';
+
+function element(name, attributes, ...children) {
+ const node = document.createElement(name);
+ for (const [attribute, value] of Object.entries(attributes)) {
+ if (value !== undefined) {
+ node.setAttribute(attribute, value);
+ }
+ }
+ node.append(...children);
+ return node;
+}
+
+function splitOnce(string, separator) {
+ const at = string.indexOf(separator);
+ return at === -1 ? [string, undefined] : [string.slice(0, at), string.slice(at + 1)];
+}
+
+// A version body says both whether the browser supports the feature and since when.
+function readVersion(body) {
+ if (body === '?') {
+ return { state: 'unknown', version: '?' };
+ }
+ if (body === '-') {
+ return { state: 'no', version: 'None' };
+ }
+ if (body === '!') {
+ return { state: 'yes', version: 'Yes' };
+ }
+ if (body.startsWith('=')) {
+ // A version that will never be superseded, so no trailing "+".
+ return { state: 'yes', version: body.slice(1) };
+ }
+ const [added, removed] = body.split('-');
+ if (removed !== undefined) {
+ return { state: 'no', version: added + EN_DASH + removed };
+ }
+ return { state: 'yes', version: added + '+' };
+}
+
+function readCell(cell, slot) {
+ const caveat = CAVEATS[cell[0]];
+ const { state, version } = readVersion(caveat ? cell.slice(1) : cell);
+ return {
+ state,
+ version: caveat?.flag ? `${FLAG} ${version}` : version,
+ title: caveat?.title(slot),
+ };
+}
+
+// A browser row is the browser's name in one cell and its version in the next. The logo
+// comes from CSS, keyed off the browser id in the row's class.
+function browserRow(slot, cell) {
+ const { state, version, title } = readCell(cell, slot);
+ return element('span', { class: `${slot.id} ${state}` },
+ element('span', {}, slot.label),
+ element('span', { title }, version));
+}
+
+// The caniuse row has a single cell instead, since its logo needs no label beside it.
+function caniuseRow(field) {
+ const [feature, title] = splitOnce(field, ',');
+ const link = element('a', { href: CANIUSE_URL + feature, title }, 'caniuse.com table');
+ return element('span', { class: 'caniuse' }, element('span', {}, link));
+}
+
+function supportTable(cells, caniuse) {
+ const groups = Array.from({ length: GROUP_COUNT }, () => []);
+ SLOTS.forEach((slot, slotIndex) => {
+ // An empty cell means this browser gets no row at all. Cells beyond SLOTS are
+ // browsers added after this copy of the script was written; ignore them.
+ if (cells[slotIndex]) {
+ groups[slot.group].push(browserRow(slot, cells[slotIndex]));
+ }
+ });
+ if (caniuse !== undefined) {
+ groups.push([caniuseRow(caniuse)]);
+ }
+
+ const table = element('div', { class: 'support' });
+ for (const rows of groups.filter((rows) => rows.length > 0)) {
+ if (table.hasChildNodes()) {
+ table.append(element('hr', {}));
+ }
+ table.append(...rows);
+ }
+ return table;
+}
+
+function featurePanel(record) {
+ const [slug, level, cells, caniuse] = record.split('|');
+
+ const article = element('a', { href: MDN_URL + slug }, slug.slice(slug.indexOf('/') + 1));
+ const panel = element('div', { class: 'feature' }, element('p', {}, article));
+
+ if (level in LEVELS) {
+ panel.append(element('p', { class: LEVELS[level].class }, LEVELS[level].text));
+ }
+ if (cells) {
+ panel.append(supportTable(cells.split(','), caniuse));
+ }
+ return panel;
+}
+
+function hydrate(anno) {
+ const [version, features] = splitOnce(anno.dataset.mdn, '|');
+ delete anno.dataset.mdn; // Only ever hydrate a panel once.
+
+ if (version !== 'v1') {
+ console.warn(`Unsupported MDN annotation data version "${version}".`, anno);
+ return;
+ }
+ anno.append(...features.split('~').map(featurePanel));
+}
+
+function hydrateIfNeeded(node) {
+ if (node instanceof Element && node.matches('.mdn-anno[data-mdn]')) {
+ hydrate(node);
+ }
+}
+
+// A click on the summary arrives before the browser opens the , so hydrating
+// from it means the panel never flashes empty. That covers pointers and the keyboard,
+// since activating a summary dispatches a click either way.
+document.addEventListener('click', (event) => {
+ if (event.target instanceof Element) {
+ hydrateIfNeeded(event.target.closest('summary')?.parentElement);
+ }
+}, true);
+
+// And `toggle` is the catch-all for a panel opened some other way, such as find-in-page
+// or a fragment navigation into it. It fires a task after the state has already changed,
+// and it doesn't bubble, so it has to be a capture listener.
+document.addEventListener('toggle', (event) => hydrateIfNeeded(event.target), true);
\ No newline at end of file
diff --git a/bikeshed/mdn/mdnspeclinks.py b/bikeshed/mdn/mdnspeclinks.py
index 7e402850e0..90121e68dc 100644
--- a/bikeshed/mdn/mdnspeclinks.py
+++ b/bikeshed/mdn/mdnspeclinks.py
@@ -16,16 +16,62 @@ class MdnFeatureT(t.TypedDict, total=False):
slug: str
summary: str
mdn_url: str
- support: dict[str, t.Any]
+ support: dict[str, MdnSupportEntry | list[MdnSupportEntry] | t.Literal["mirror"]]
# the support values are wild
+ class MdnSupportEntry(t.TypedDict, total=False):
+ version_added: bool | str
+ version_removed: str
+ prefix: str
+ alternative_name: str
+ partial_implementation: t.Literal[True]
+ flags: t.Any
+
MdnDataT: t.TypeAlias = dict[str, list[MdnFeatureT]]
+BROWSER_DATA = {
+ "current": ["firefox", "safari", "chrome"],
+ "retired": {"edge":"18", "ie":"11"},
+ "cell_order": [
+ "firefox",
+ "safari",
+ "chrome", # current
+ "opera",
+ "edge_blink", # borrowed
+ "edge",
+ "ie", # retired
+ "firefox_android",
+ "safari_ios",
+ "chrome_android",
+ "webview_android",
+ "samsunginternet_android",
+ "opera_android", # mobile
+ "nodejs", # JS
+ ],
+ "mirrors": {
+ "chrome_android": "chrome",
+ "webview_android": "chrome",
+ "samsunginternet_android": "chrome_android",
+ "opera": "chrome",
+ "opera_android": "chrome_android",
+ "firefox_android": "firefox",
+ "safari_ios": "safari",
+ },
+}
+
def addMdnPanels(doc: t.SpecT) -> list[t.ElementT]:
if not doc.md.includeMdnPanels:
return []
+ data = loadMdnData(doc)
+ panels = panelsFromData(doc, data)
+ if panels:
+ doc.extraJC.addMdn()
+ return panels
+
+
+def loadMdnData(doc: t.SpecT) -> MdnDataT:
try:
filename = f"{doc.md.vshortname}.json"
datafile = doc.dataFile.fetch("mdn", filename, str=True)
@@ -36,119 +82,67 @@ def addMdnPanels(doc: t.SpecT) -> list[t.ElementT]:
except OSError:
if doc.md.includeMdnPanels == "maybe":
# if "maybe", failure is fine, don't complain
- pass
+ return {}
else:
m.die(f"Couldn't find the MDN data for '{doc.md.vshortname}' nor '{doc.md.shortname}'.")
- return []
+ return {}
try:
data = t.cast("MdnDataT", json.loads(datafile, object_pairs_hook=OrderedDict))
+ return data
except Exception as e:
- m.die(f"Couldn't load MDN Spec Links data for this spec.\n{e}")
- return []
-
- panels = panelsFromData(doc, data)
- if panels:
- doc.extraJC.addMdn()
-
- return panels
+ m.die(f"Couldn't parse MDN data for this spec.\n{e}")
+ return {}
-def createAnno(className: str, mdnButton: t.ElementT, featureDivs: list[t.ElementT]) -> t.ElementT:
- return h.E.div({"class": className}, mdnButton, *featureDivs)
+# MDN annotation panels do not ship their body as markup. Each panel carries a compact
+# "data-mdn" attribute instead, and
+# builds the support table from it the first time the reader opens the panel. That script
+# owns the browser labels, the engine-support strings and the render grouping; what is
+# shared between it and the code below is only this wire format:
+#
+# data-mdn = "v1|" feature ("~" feature)*
+# feature = slug "|" level "|" cells ["|" caniuse-feature "," caniuse-title]
+# level = "" no remark | "0" no engines | "1" one engine | "9" all engines
+# | "s"/"S" one/some engines under another name
+# | "v"/"V" one/some engines prefixed
+# | "p"/"P" one/some engines partially
+# cells = cell ("," cell)* one per entry of MDNBrowserSlots, in that order. An
+# empty cell means "no row for this browser", and trailing
+# empty cells may be left out entirely.
+# cell = [caveat] body
+# caveat = "*" partial | "^" needs a flag | "$" needs a prefix or alternative name
+# body = "?" unknown | "-" unsupported | "!" supported, version unknown
+# | "=" version supported since exactly this version, no trailing "+"
+# | version "-" version supported over a range, rendered with an en dash
+# | version supported since this version, rendered with a "+"
+#
+# None of "|" and "~" occurs in any field of the current data, so no escaping is needed;
+# the two free-text fields are squashed defensively rather than escaped.
+#
+# New browsers must be APPENDED to MDNBrowserSlots and never inserted. Archived commit
+# snapshots keep loading the script above forever, and older copies of it simply ignore
+# the cells they do not know about, which only works while the order is append-only.
+# Anything that this scheme cannot express needs a new version prefix, not a redefinition
+# of "v1".
def panelsFromData(doc: t.SpecT, data: MdnDataT) -> list[t.ElementT]:
- mdnBaseUrl = "https://developer.mozilla.org/en-US/docs/Web/"
-
- browsersProvidingCurrentEngines = ["firefox", "safari", "chrome"]
- browsersWithBorrowedEngines = ["opera", "edge_blink"]
- browsersWithRetiredEngines = ["edge", "ie"]
- browsersForMobileDevices = [
- "firefox_android",
- "safari_ios",
- "chrome_android",
- "webview_android",
- "samsunginternet_android",
- "opera_android",
- ]
-
- # BCD/mdn-spec-links shortnames to full names
- nameFromCodeName = {
- "chrome": "Chrome",
- "chrome_android": "Chrome for Android",
- "edge": "Edge (Legacy)",
- "edge_blink": "Edge",
- "firefox": "Firefox",
- "firefox_android": "Firefox for Android",
- "ie": "IE",
- "nodejs": "Node.js",
- "opera": "Opera",
- "opera_android": "Opera Mobile",
- "safari": "Safari",
- "samsunginternet_android": "Samsung Internet",
- "safari_ios": "iOS Safari",
- "webview_android": "Android WebView",
- }
-
panels = []
missingIds = []
docIds = h.collectIds(doc.body)
for elementId, features in data.items():
- lessThanTwoEngines = 0
- onlyTwoEngines = 0
- allEngines = 0
- featureDivs = []
targetElement = docIds.get(elementId)
if targetElement is None and elementId not in doc.md.ignoreMDNFailure:
missingIds.append(elementId)
continue
- for feature in features:
- if "engines" in feature:
- engines = len(feature["engines"])
- if engines < 2:
- lessThanTwoEngines = lessThanTwoEngines + 1
- elif engines == 2:
- onlyTwoEngines = onlyTwoEngines + 1
- elif engines >= len(browsersProvidingCurrentEngines):
- allEngines = allEngines + 1
- featureDivs.append(
- mdnPanelFor(
- feature,
- mdnBaseUrl,
- nameFromCodeName,
- browsersProvidingCurrentEngines,
- browsersWithBorrowedEngines,
- browsersWithRetiredEngines,
- browsersForMobileDevices,
- ),
- )
-
- summary = h.E.summary()
- if lessThanTwoEngines > 0:
- h.appendChild(
- summary,
- h.E.b(
- {
- "class": "less-than-two-engines-flag",
- "title": _t("This feature is in less than two current engines."),
- },
- "\u26a0",
- ),
- )
- elif allEngines > 0 and lessThanTwoEngines == 0 and onlyTwoEngines == 0:
- h.appendChild(
- summary,
- h.E.b(
- {
- "class": "all-engines-flag",
- "title": _t("This feature is in all current engines."),
- },
- "\u2714",
- ),
- )
- h.appendChild(summary, h.E.span("MDN"))
- anno = h.E.details({"class": "mdn-anno unpositioned", "data-anno-for": elementId}, summary, *featureDivs)
+ featureCode = encodeFeatureData(features)
+
+ summary = buildSummary(features, numEngines=len(BROWSER_DATA["current"]))
+ anno = h.E.details(
+ {"class": "mdn-anno unpositioned", "data-anno-for": elementId, "data-mdn": featureCode},
+ summary,
+ )
panels.append(anno)
h.appendChild(doc.body, anno)
@@ -160,148 +154,186 @@ def panelsFromData(doc: t.SpecT, data: MdnDataT) -> list[t.ElementT]:
return panels
-def addSupportRow(
+def encodeFeatureData(features: list[MdnFeatureT]) -> str:
+ fullCode = "v1|" + ("~".join(codeFromFeature(x) for x in features))
+ return fullCode
+
+
+def codeFromFeature(feature: MdnFeatureT) -> str:
+ # feature = slug "|" level "|" cells ["|" caniuse-feature "," caniuse-title]
+ code = cleanCodeText(feature["slug"]) + "|"
+ code += levelFromFeature(feature) + "|"
+ code += cellsFromFeature(feature)
+ return code
+
+
+def levelFromFeature(feature: MdnFeatureT) -> str:
+ # level = "" no remark | "0" no engines | "1" one engine | "9" all engines
+ # | "s"/"S" one/some engines under another name
+ # | "v"/"V" one/some engines prefixed
+ # | "p"/"P" one/some engines partially
+ if "engines" in feature:
+ numEngines = len(feature["engines"])
+ if numEngines == 0:
+ return "0"
+ elif numEngines == 1:
+ return "1"
+ elif numEngines >= len(BROWSER_DATA["current"]):
+ return "9"
+ return ""
+
+
+def cellsFromFeature(feature: MdnFeatureT) -> str:
+ # cells = cell ("," cell)* one per entry of BROWSER_DATA["cell_order"], in that order. An
+ # empty cell means "no row for this browser", and trailing
+ # empty cells may be left out entirely.
+ cells = []
+ for browserCodeName in BROWSER_DATA["cell_order"]:
+ cells.append(cellFromFeature(feature, browserCodeName))
+ return ",".join(cells)
+
+
+def cleanCodeText(text: str) -> str:
+ # | and ~ are used in the data-mdn code as separators, so can't appear in the code's contents.
+ # They're not used in the text of *any* current features, so just defensively blank them out
+ # in case they ever do appear.
+ return text.replace("|", " ").replace("~", " ")
+
+
+def buildSummary(features: list[MdnFeatureT], numEngines: int) -> t.ElementT:
+ lessThanTwoEngines = 0
+ onlyTwoEngines = 0
+ allEngines = 0
+ for feature in features:
+ if "engines" in feature:
+ engines = len(feature["engines"])
+ if engines < 2:
+ lessThanTwoEngines = lessThanTwoEngines + 1
+ elif engines == 2:
+ onlyTwoEngines = onlyTwoEngines + 1
+ elif engines >= numEngines:
+ allEngines = allEngines + 1
+
+ summary = h.E.summary()
+ if lessThanTwoEngines > 0:
+ h.appendChild(
+ summary,
+ h.E.b(
+ {
+ "class": "less-than-two-engines-flag",
+ "title": _t("This feature is in less than two current engines."),
+ },
+ "\u26a0",
+ ),
+ )
+ elif allEngines > 0 and lessThanTwoEngines == 0 and onlyTwoEngines == 0:
+ h.appendChild(
+ summary,
+ h.E.b(
+ {
+ "class": "all-engines-flag",
+ "title": _t("This feature is in all current engines."),
+ },
+ "\u2714",
+ ),
+ )
+ h.appendChild(summary, h.E.span("MDN"))
+ return summary
+
+
+def cellFromFeature(
+ feature: MdnFeatureT,
browserCodeName: str,
- nameFromCodeName: dict[str, str],
- support: dict[str, t.Any],
- supportData: t.ElementT,
-) -> None:
- if browserCodeName not in support:
- return
- isEdgeLegacy = browserCodeName == "edge"
- isIE = browserCodeName == "ie"
- needsFlag = False
+) -> str:
+ # cell = [caveat] body
+ # caveat = "*" partial | "^" needs a flag | "$" needs a prefix or alternative name | "@" mirrored
+ # body = "?" unknown | "-" unsupported | "!" supported, version unknown
+ # | "=" version supported since exactly this version, no trailing "+"
+ # | version "-" version supported over a range, rendered with an en dash
+ # | version supported since this version, rendered with a "+"
+
+ # Documentation for the "support" data:
+ # https://github.com/mdn/browser-compat-data/blob/main/schemas/compat-data-schema.md
+ # Documentation misses:
+ # * apparently feature.support can be `null`
+ # * apparently feature.support.version_removed can be `true`
+
+ if feature["support"] is None or browserCodeName not in feature["support"]:
+ return ""
+ support = feature["support"][browserCodeName]
+
+ if support == "mirror":
+ sourceCodeName, support = followMirrors(feature, browserCodeName)
+ if sourceCodeName is None:
+ return "@?"
+ _, version = distillSupport(support, sourceCodeName)
+ if version in "?-!":
+ return "@" + version
+ else:
+ return "@!"
+
+ caveat, version = distillSupport(support, browserCodeName)
+ return caveat+version
+
+def distillSupport(support: MdnSupportEntry | list[MdnSupportEntry], browserCodeName: str): -> tuple[str, str]
versionAdded = None
versionRemoved = None
- thisBrowserSupport = support[browserCodeName]
- if isinstance(thisBrowserSupport, dict):
- if "version_added" in thisBrowserSupport:
- versionAdded = thisBrowserSupport["version_added"]
- if "flags" in thisBrowserSupport:
- needsFlag = True
- if (
- "prefix" in thisBrowserSupport
- or "alternative_name" in thisBrowserSupport
- or "partial_implementation" in thisBrowserSupport
- ):
- versionAdded = False
- if "version_removed" in thisBrowserSupport:
- versionRemoved = thisBrowserSupport["version_removed"]
- if isinstance(thisBrowserSupport, list):
- for versionDetails in thisBrowserSupport:
+ caveat = ""
+ if isinstance(support, dict):
+ if "version_added" in support:
+ versionAdded = support["version_added"]
+ caveat = getCaveat(support)
+ if "version_removed" in support:
+ versionRemoved = support["version_removed"]
+ else isinstance(support, list):
+ # List of support objects, documenting different levels of support over time.
+ # Ordered recent-first, so stop when I hit the first version_added
+ for versionDetails in support:
if "version_removed" in versionDetails:
versionRemoved = versionDetails["version_removed"]
- continue
if "version_added" in versionDetails:
- if versionDetails["version_added"] is False:
- versionAdded = False
- continue
- if versionDetails["version_added"] is None:
- versionAdded = None
- continue
- if (
- "prefix" in versionDetails
- or "alternative_name" in versionDetails
- or "partial_implementation" in versionDetails
- ):
- continue
- if "flags" in thisBrowserSupport:
- needsFlag = True
versionAdded = versionDetails["version_added"]
- versionRemoved = None
+ caveat = getCaveat(versionDetails)
break
- statusCode = "n"
- if versionAdded is None:
- minVersion = "?"
- elif versionAdded is False:
- minVersion = "None"
- elif versionAdded is True:
- minVersion = "Yes"
- statusCode = "y"
- else:
- if versionRemoved is None:
- statusCode = "y"
- minVersion = versionAdded + "+"
- if isEdgeLegacy and versionAdded == "18":
- minVersion = "18"
- if isIE and versionAdded == "11":
- minVersion = "11"
- else:
- statusCode = "n"
- if versionAdded is not None:
- minVersion = versionAdded + "\u2013" + versionRemoved
- else:
- minVersion = "None"
- browserFullName = nameFromCodeName[browserCodeName]
- h.appendChild(
- supportData,
- browserCompatSpan(browserCodeName, browserFullName, statusCode, minVersion, needsFlag),
- )
+ version = getVersionString(versionAdded, versionRemoved, browserCodeName)
-def mdnPanelFor(
- feature: MdnFeatureT,
- mdnBaseUrl: str,
- nameFromCodeName: dict[str, str],
- browsersProvidingCurrentEngines: list[str],
- browsersWithBorrowedEngines: list[str],
- browsersWithRetiredEngines: list[str],
- browsersForMobileDevices: list[str],
-) -> t.ElementT:
- featureDiv = h.E.div({"class": "feature"})
- if "slug" in feature:
- slug = feature["slug"]
- displaySlug = slug.split("/", 1)[1]
- title = feature.get("summary", "")
- mdnURL = mdnBaseUrl + slug
- h.appendChild(featureDiv, h.E.p({}, h.E.a({"href": mdnURL, "title": title}, displaySlug)))
- if "engines" in feature:
- engines = len(feature["engines"])
- enginesPara = None
- if engines == 0:
- enginesPara = h.E.p({"class": "less-than-two-engines-text"}, _t("In no current engines."))
- elif engines == 1:
- enginesPara = h.E.p({"class": "less-than-two-engines-text"}, _t("In only one current engine."))
- elif engines >= len(browsersProvidingCurrentEngines):
- enginesPara = h.E.p({"class": "all-engines-text"}, _t("In all current engines."))
- if enginesPara is not None:
- h.appendChild(featureDiv, enginesPara)
- supportData = h.E.div({"class": "support"})
- h.appendChild(featureDiv, supportData)
- support = feature["support"]
- for browserCodeName in browsersProvidingCurrentEngines:
- addSupportRow(browserCodeName, nameFromCodeName, support, supportData)
- h.appendChild(supportData, h.E.hr())
- for browserCodeName in browsersWithBorrowedEngines:
- addSupportRow(browserCodeName, nameFromCodeName, support, supportData)
- h.appendChild(supportData, h.E.hr())
- for browserCodeName in browsersWithRetiredEngines:
- addSupportRow(browserCodeName, nameFromCodeName, support, supportData)
- h.appendChild(supportData, h.E.hr())
- for browserCodeName in browsersForMobileDevices:
- addSupportRow(browserCodeName, nameFromCodeName, support, supportData)
- if "nodejs" in support:
- h.appendChild(supportData, h.E.hr())
- addSupportRow("nodejs", nameFromCodeName, support, supportData)
- return featureDiv
-
-
-def browserCompatSpan(
- browserCodeName: str,
- browserFullName: str,
- statusCode: str,
- minVersion: str,
- needsFlag: bool,
-) -> t.ElementT:
- # browserCodeName: e.g. "chrome"
- # browserFullName: e.g. "Chrome for Android"
- minVersionAttributes = {}
- flagSymbol = ""
- if needsFlag:
- flagSymbol = "\U0001f530 "
- minVersionAttributes["title"] = _t("Requires setting a user preference or runtime flag.")
- statusClass = {"y": "yes", "n": "no"}[statusCode]
- outer = h.E.span({"class": browserCodeName + " " + statusClass})
- h.appendChild(outer, h.E.span({}, browserFullName))
- h.appendChild(outer, h.E.span(minVersionAttributes, flagSymbol + minVersion))
- return outer
+ return caveat, version
+
+
+def followMirrors(feature: MdnFeatureT, browserCodeName: str, ) -> tuple[str, MdnSupportEntry | list[MdnSupportEntry]] | tuple[None, None]:
+ if browserCodeName not in BROWSER_DATA["mirrors"]:
+ return None, None
+ mirroredBrowser = BROWSER_DATA["mirrors"]
+ if mirroredBrowser not in feature["support"]:
+ return None, None
+ support = feature["support"][mirroredBrowser]
+ if support == "mirror":
+ return followMirrors(feature, mirroredBrowser)
+ return mirroredBrowser, support
+
+
+def getCaveat(supportEntry: MdnSupportEntry) -> str:
+ if "flags" in support:
+ return "^"
+ if "prefix" in support or "alternative_name" in support:
+ return "$"
+ if "partial_implementation" in support:
+ return "*"
+ return ""
+
+
+def getVersionString(versionAdded: str | bool | None, versionRemoved: str | bool | None, browserCodeName: str) -> str:
+ if versionAdded is False:
+ return "-"
+ elif versionAdded is True:
+ return "!"
+ elif versionRemoved is True:
+ return "-"
+ elif browserCodeName in BROWSER_DATA["retired"] and versionAdded == BROWSER_DATA["retired"][browserCodeName]:
+ return "=" + versionAdded
+ elif versionAdded:
+ if versionRemoved:
+ return versionAdded + "-" + versionRemoved
+ return versionAdded
+ return "?"
\ No newline at end of file
diff --git a/bikeshed/stylescript/manager.py b/bikeshed/stylescript/manager.py
index 68800176cf..abc13301fc 100644
--- a/bikeshed/stylescript/manager.py
+++ b/bikeshed/stylescript/manager.py
@@ -24,8 +24,9 @@ def getStyles(self, allowList: t.BoolSet) -> list[Style]:
for script in self.scripts.values():
if not script.insertable(allowList):
continue
- if script.style and script.style.insertable(allowList):
- styles[script.style.name] = script.style
+ for style in script.styles:
+ if style.insertable(allowList):
+ styles[style.name] = style
return sorted(styles.values(), key=lambda x: x.name)
def getScripts(self, allowList: t.BoolSet) -> list[Library | Script]:
@@ -34,8 +35,9 @@ def getScripts(self, allowList: t.BoolSet) -> list[Library | Script]:
for style in self.styles.values():
if not style.insertable(allowList):
continue
- if style.script and style.script.insertable(allowList):
- scripts[style.script.name] = style.script
+ for script in style.scripts:
+ if script.insertable(allowList):
+ scripts[script.name] = script
for script in self.scripts.values():
if not script.insertable(allowList):
continue
@@ -76,13 +78,16 @@ def addIssues(self) -> None:
def addMdn(self) -> None:
style = self._addCSS("mdn-anno", "mdn")
- if not style.script:
- style.script = Script("position-annos", Path(config.scriptPath("stylescript/position-annos.js")))
+ if not style.scripts:
+ style.scripts = [
+ Script("mdn-anno", Path(config.scriptPath("mdn/mdn-anno.js"))),
+ Script("position-annos", Path(config.scriptPath("stylescript/position-annos.js"))),
+ ]
def addCiu(self) -> None:
style = self._addCSS("caniuse-panel", "caniuse")
- if not style.script:
- style.script = Script("position-annos", Path(config.scriptPath("stylescript/position-annos.js")))
+ if not style.scripts:
+ style.scripts = [Script("position-annos", Path(config.scriptPath("stylescript/position-annos.js")))]
def addDomintro(self) -> None:
self._addCSS("domintro")
@@ -104,16 +109,18 @@ def addHidedel(self) -> None:
def addVarClickHighlighting(self) -> None:
script = self._addJS("var-click-highlighting")
- if not script.style:
- script.style = Style(
- "var-click-highlighting",
- Path(config.scriptPath("stylescript/var-click-highlighting.css")),
- )
+ if not script.styles:
+ script.styles = [
+ Style(
+ "var-click-highlighting",
+ Path(config.scriptPath("stylescript/var-click-highlighting.css")),
+ ),
+ ]
def addRefHints(self) -> dict[str, t.Any]:
script = self._addJS("ref-hints", "refs")
- if not script.style:
- script.style = Style("ref-hints", Path(config.scriptPath("refs/ref-hints.css")))
+ if not script.styles:
+ script.styles = [Style("ref-hints", Path(config.scriptPath("refs/ref-hints.css")))]
if "dom-helper" not in script.libraries:
script.libraries["dom-helper"] = Library("dom-helper", Path(config.scriptPath("stylescript/dom-helper.js")))
if not script.data:
@@ -140,8 +147,8 @@ def addWptCSS(self) -> None:
def addWpt(self, paths: list[str] | None) -> None:
script = self._addJS("wpt", "wpt")
- if not script.style:
- script.style = Style("wpt", Path(config.scriptPath("wpt/wpt.css")))
+ if not script.styles:
+ script.styles = [Style("wpt", Path(config.scriptPath("wpt/wpt.css")))]
if "dom-helper" not in script.libraries:
script.libraries["dom-helper"] = Library("dom-helper", Path(config.scriptPath("stylescript/dom-helper.js")))
if not script.data:
@@ -151,8 +158,8 @@ def addWpt(self, paths: list[str] | None) -> None:
def addDfnPanels(self) -> dict[str, t.Any]:
script = self._addJS("dfn-panel", "dfnpanels")
- if not script.style:
- script.style = Style("dfn-panel", Path(config.scriptPath("dfnpanels/dfn-panel.css")))
+ if not script.styles:
+ script.styles = [Style("dfn-panel", Path(config.scriptPath("dfnpanels/dfn-panel.css")))]
if "dom-helper" not in script.libraries:
script.libraries["dom-helper"] = Library("dom-helper", Path(config.scriptPath("stylescript/dom-helper.js")))
if not script.data:
@@ -177,7 +184,7 @@ def toElement(self) -> t.ElementT:
class Script(JCResource):
path: Path
libraries: dict[str, Library] = dataclasses.field(default_factory=dict)
- style: Style | None = None
+ styles: list[Style] = dataclasses.field(default_factory=list)
data: tuple[str, dict[str, t.Any]] | None = None
def insertable(self, allowList: t.BoolSet) -> bool:
@@ -224,7 +231,7 @@ def toElement(self) -> t.ElementT:
@dataclasses.dataclass
class Style(JCResource):
textPath: Path
- script: Script | None = None
+ scripts: list[Script] = dataclasses.field(default_factory=list)
def insertable(self, allowList: t.BoolSet) -> bool:
return f"style-{self.name}" in allowList
diff --git a/tests/github/WICG/background-fetch/index.html b/tests/github/WICG/background-fetch/index.html
index e22fa1f70e..5098c54e97 100644
--- a/tests/github/WICG/background-fetch/index.html
+++ b/tests/github/WICG/background-fetch/index.html
@@ -3182,501 +3182,98 @@ Parsing as an integer infra/189. âľ
-
+
â MDN
-
-
BackgroundFetchEvent/BackgroundFetchEvent
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchEvent/registration
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchEvent
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchManager/fetch
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchManager/get
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchManager/getIds
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchManager
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRecord/request
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRecord/responseReady
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRecord
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/abort
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/downloaded
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/downloadTotal
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/failureReason
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/id
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/match
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/matchAll
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/progress_event
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/recordsAvailable
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/result
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/uploaded
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration/uploadTotal
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchRegistration
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
+
â MDN
-
-
BackgroundFetchUpdateUIEvent/updateUI
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
BackgroundFetchUpdateUIEvent
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
ServiceWorkerGlobalScope/backgroundfetchabort_event
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
ServiceWorkerGlobalScope/backgroundfetchclick_event
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
ServiceWorkerGlobalScope/backgroundfetchfail_event
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
ServiceWorkerGlobalScope/backgroundfetchsuccess_event
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
-
+
â MDN
-
-
ServiceWorkerRegistration/backgroundFetch
-
In only one current engine.
-
- FirefoxNoneSafariNoneChrome74+
-
- Opera?Edge79+
-
- Edge (Legacy)?IENone
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebViewNoneSamsung Internet?Opera Mobile?
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
âMDN
-
-
@charset
-
In all current engines.
-
- Firefox1.5+Safari4+Chrome2+
-
- Opera9+Edge79+
-
- Edge (Legacy)12+IE5.5+
-
- Firefox for Android4+iOS Safari4+Chrome for Android?Android WebView2+Samsung Internet?Opera Mobile10.1+
-
-
-
+
âMDN
-
-
border-collapse
-
In all current engines.
-
- Firefox1+Safari1.2+Chrome1+
-
- Opera4+Edge79+
-
- Edge (Legacy)12+IE5+
-
- Firefox for Android?iOS Safari3+Chrome for Android?Android WebView2.2+Samsung Internet?Opera Mobile10.1+
-
-
-
+
âMDN
-
-
border-spacing
-
In all current engines.
-
- Firefox1+Safari1+Chrome1+
-
- Opera4+Edge79+
-
- Edge (Legacy)12+IE8+
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebView37+Samsung Internet?Opera Mobile?
-
-
-
+
âMDN
-
-
caption-side
-
In all current engines.
-
- Firefox1+Safari1+Chrome1+
-
- Opera4+Edge79+
-
- Edge (Legacy)12+IE8+
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebView37+Samsung Internet?Opera Mobile?
-
-
-
+
âMDN
-
-
clear
-
In all current engines.
-
- Firefox1+Safari1+Chrome1+
-
- Opera3.5+Edge79+
-
- Edge (Legacy)12+IE4+
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile10.1+
-
-
-
+
âMDN
-
-
empty-cells
-
In all current engines.
-
- Firefox1+Safari1.2+Chrome1+
-
- Opera4+Edge79+
-
- Edge (Legacy)12+IE8+
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile10.1+
-
-
-
+
âMDN
-
-
float
-
In all current engines.
-
- Firefox1+Safari1+Chrome1+
-
- Opera7+Edge79+
-
- Edge (Legacy)12+IE4+
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile10.1+
-
-
-
+
âMDN
-
-
table-layout
-
In all current engines.
-
- Firefox1+Safari1+Chrome14+
-
- Opera7+Edge79+
-
- Edge (Legacy)12+IE5+
-
- Firefox for Android?iOS Safari3+Chrome for Android?Android WebView1.5+Samsung Internet?Opera Mobile10.1+
-
-
-
+
âMDN
-
-
vertical-align
-
In all current engines.
-
- Firefox1+Safari1+Chrome1+
-
- Opera4+Edge79+
-
- Edge (Legacy)12+IE4+
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebView?Samsung Internet?Opera Mobile?
-
-
-
+
âMDN
-
-
z-index
-
In all current engines.
-
- Firefox1+Safari1+Chrome1+
-
- Opera4+Edge79+
-
- Edge (Legacy)12+IE4+
-
- Firefox for Android?iOS Safari?Chrome for Android?Android WebView37+Samsung Internet?Opera Mobile?
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+