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 -
-

BackgroundFetchUpdateUIEvent/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 -
-

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? -
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +