index.html (519 lines of code) (raw):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Mozilla Standards Positions</title>
<link rel="preload" as="image" href="asset/Mozilla-2024.svg">
<link rel="preload" as="image" href="asset/WHATWG.svg">
<link rel="preload" as="image" href="asset/W3C.svg">
<link rel="preload" as="image" href="asset/W3C-CG.svg">
<link rel="preload" as="image" href="asset/IETF.svg">
<link rel="preload" as="image" href="asset/Ecma.png">
<link rel="preload" as="image" href="asset/Unicode.svg">
<link rel="preload" as="image" href="asset/Proposal.svg">
<link rel="preload" as="image" href="asset/Other.svg">
<link rel="preload" as="image" href="asset/bugzilla.png">
<link rel="preload" as="image" href="asset/MDN.svg">
<link rel="icon" href="asset/mozilla-icon.png">
<style>
/* light scheme colors */
:root {
--root-color: #333;
--root-bg-color: #fff;
--header-bg-color: #eee;
--h1-color: black;
--highlight-bg-color: #ffffd7;
--highlight-color: inherit;
--link-color: #2268a4;
--row-hover-bg-color: #f5f5f5;
--positive-color: white;
--positive-bg-color: green;
--negative-color: white;
--negative-bg-color: maroon;
--neutral-color: inherit;
--neutral-bg-color: transparent;
--defer-color: white;
--defer-bg-color: #666;
--table-border-color: silver;
--position-border-color: gray;
--input-bg-color: white;
--input-color: black;
/* under consideration uses the same colors as defer */
}
/* dark scheme colors */
@media (prefers-color-scheme: dark) {
:root {
--root-color: #eee;
--root-bg-color: #222;
--header-bg-color: black;
--highlight-bg-color: #440;
--h1-color: white;
--link-color: #6fb6f5;
--row-hover-bg-color: black;
--table-border-color: #444;
--position-border-color: black;
--positive-bg-color: #00a000;
--positive-color: black;
--negative-bg-color: #c65050;
--negative-color: black;
--defer-bg-color: #999;
--defer-color: black;
--input-bg-color: #171717;
--input-color: white;
}
h1 img { filter: invert(1); }
table img { background-color: white; border-radius: 16px; }
}
html { font-family: system-ui, sans-serif; color: var(--root-color); background-color: var(--root-bg-color); line-height: 1.5; scrollbar-gutter: stable; scroll-padding-top: calc(2.5em + 1px); }
html.loading header ~ * { visibility: hidden; }
body { margin: 2em; }
header { background-color: var(--header-bg-color); margin: 0 auto 2em; padding: 3em; max-width: 66em; }
h1 { font-size: min(12vw, 4em); margin: 0; color: var(--h1-color); line-height: 1; }
h1 img { max-height: 0.83333333em; width: auto; }
mark { background-color: var(--highlight-bg-color); color: var(--highlight-color); padding: 0.5em; margin: -0.5em; line-height: 2; box-decoration-break: clone; }
:any-link { color: var(--link-color); padding: 0.25rem 0; }
table :any-link { text-decoration: none; }
table :any-link:hover { text-decoration: underline; }
search { margin: 0 auto 2em; padding-block: 3em; padding-inline: min(3%, 3em); max-width: 66em; }
search label { display: flex; gap: 1em; }
input[name="search"] { width: 100%; margin-right: min(5%, 5em); border: 1px solid gray; border-radius: 3px; background-color: var(--input-bg-color); color: var(--input-color); }
table { border-collapse: collapse; table-layout: fixed; width: 100%; margin: 0 auto 2em; max-width: 72em; min-width: 48em; }
thead { position: sticky; top: 0; background-color: var(--root-bg-color); z-index: 2; }
th { text-align: left; box-shadow: var(--table-border-color) 0 1px 0; white-space: nowrap; }
td.specification { word-wrap: break-word; }
td.concerns, td.topics { font-size: 0.75em; padding-bottom: 0.85em; text-wrap: balance; }
td.venues, td.more-info { word-spacing: 8px; padding-top: 0.1em; padding-bottom: 0.1em; font-size: 1.5em; }
td.more-info :any-link[title="Discussion"] { font-size: 0.75rem; vertical-align: 0.2rem; }
td.more-info :any-link:not([title="Discussion"]) { padding: 0; text-decoration: none; }
td.link :any-link { padding: 0.25rem; }
th.concerns { width: 7em; }
th.position { width: 4.25em; }
th.topics { width: 3.5em; }
th.venues { width: 4em; }
th.more-info { width: 9.5em; }
th, td { padding: 0.5rem; }
th.link { width: 3em; text-align: center; }
td.link { padding-left: 2.5em; }
tr.details > td { padding-top: 0; padding-bottom: 0; border-top: none; }
summary { position: relative; white-space: pre; top: calc(-2em - 1px); width: 0.9em; padding-left: 0.45em; overflow: hidden; cursor: default; user-select: none; }
details:not([open]) { height: 0; }
details dl { margin-top: -1em; grid-template-columns: 6em auto; }
details dt { font-weight: bold; }
dl { display: grid; grid-template-columns: max-content auto; row-gap: 1em; max-width: calc(100vw - 4em) }
dt { text-align: right; }
dd { margin-left: 1em; padding-left: 1px; }
code { font-family: monospace, monospace; font-size-adjust: 0.5; }
td { border-top: 1px solid var(--table-border-color); vertical-align: bottom; }
tbody > tr:hover,
tbody > tr:hover + tr.details,
tbody > tr:has( + :where(tr.details:hover)) { background-color: var(--row-hover-bg-color); }
tbody > tr:target,
tbody > tr:target + tr.details { background-color: var(--highlight-bg-color); color: var(--highlight-color); }
table img { width: auto; height: 24px; vertical-align: -2px; }
span.position { border-radius: 0.6em; padding: 0.25em 0.5em; vertical-align: 1px; font-size: 0.75em; border: 1px solid var(--position-border-color); white-space: nowrap; }
span.positive { background-color: var(--positive-bg-color); color: var(--positive-color); }
span.negative { background-color: var(--negative-bg-color); color: var(--negative-color); }
span.neutral { border-color: currentColor; }
span.defer, span.under-consideration { background-color: var(--defer-bg-color); color: var(--defer-color); }
/* sorting */
th.sortable { padding-top: 0; padding-bottom: 0; }
th button { appearance: none; padding: 0.25em 0; font: inherit; display: inline; background-color: transparent; border: none; cursor: default; user-select: none; }
th button::after { font-size: 0.8125em; content: " ▼"; opacity: 0.3; }
th button:hover::after, th button:focus::after { opacity: 0.6 }
th[aria-sort] button::after { opacity: 1; }
th[aria-sort="descending"] button::after { content: " ▲"; }
</style>
<script>
// Avoid a flash of empty table (FOET)
document.documentElement.classList.add('loading');
setTimeout(() => {
document.documentElement.classList.remove('loading');
}, 1000);
</script>
<meta name="description" content="This page tracks Mozilla's positions on open Web and Web-related specifications submitted to standards bodies like the IETF, W3C, WHATWG, and Ecma TC39.">
</head>
<body>
<header>
<h1><img src="asset/Mozilla-2024.svg" alt="Mozilla" width="240" height="50"> Standards Positions</h1>
<p>This page tracks Mozilla's positions on open Web and Web-related specifications submitted to standards
bodies like the <a href="https://www.ietf.org/">IETF</a>, <a href="https://www.w3.org/">W3C</a>,
<a href="https://whatwg.org/">WHATWG</a>, and <a href="https://tc39.github.io/">Ecma TC39</a>.</p>
<p>Please remember, this isn't a commitment to implement or participate;
<strong>it's just what we think right now</strong>. See <a
href="https://groups.google.com/a/mozilla.org/g/dev-platform/">dev-platform</a>
to find out what we're implementing.</p>
<p><mark>Want Mozilla's position on a specification? <a href="https://github.com/mozilla/standards-positions/">Find out more</a>.</mark></p>
</header>
<p class="noscript">Enable JavaScript to view the Mozilla standards positions, or see the <a href="https://github.com/mozilla/standards-positions/issues?q=is%3Aissue">issues in mozilla/standards-positions</a> on GitHub.</p>
<search>
<p><label>Search <input name="search"></label></p>
</search>
<table>
<thead>
<tr>
<th class="link">Link</th>
<th class="specification sortable">Specification</th>
<th class="position sortable">Position</th>
<th class="concerns sortable">Concerns</th>
<th class="topics sortable">Topics</th>
<th class="venues sortable">Venues</th>
<th class="more-info sortable">More info</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<h2>Legend</h2>
<p>The possible positions are:</p>
<dl>
<dt><span class="position positive">positive</span></dt><dd>Mozilla regards this work as a potential improvement to the web.</dd>
<dt><span class="position neutral">neutral</span></dt><dd>Mozilla is not convinced of the merits of this work, but does not see any significant negative potential.</dd>
<dt><span class="position negative">negative</span></dt><dd>Mozilla believes that pursuing this work in its current form would not be good for the web.</dd>
<dt><span class="position defer">defer</span></dt><dd>Mozilla takes no position on this work.</dd>
<dt><span class="position under-consideration">under consideration</span></dt><dd>Mozilla has not taken a position on this work and is gathering more information.</dd>
</dl>
<script>
function getData() {
// We need to delay the load event for URL fragments and text fragments to work reliably,
// so use <iframe> instead of fetch().
const iframe = document.createElement('iframe');
iframe.hidden = true;
iframe.src = "merged-data.json";
const p = new Promise(resolve => {
iframe.onload = e => {
const text = iframe.contentDocument.body.textContent;
const data = JSON.parse(text);
iframe.remove();
resolve(data);
};
});
document.body.append(iframe);
return p;
}
function expandInline(markdown) {
const fragment = document.createDocumentFragment();
const codePattern = /`([^`]+)`/;
while (true) {
const match = codePattern.exec(markdown);
if (!match) {
// No more code segments, just append remaining text (if any)
if (markdown) {
fragment.appendChild(document.createTextNode(markdown));
}
break;
}
const textBefore = markdown.slice(0, match.index);
if (textBefore) {
fragment.appendChild(document.createTextNode(textBefore));
}
// Create a <code> element for the matched segment
const codeElement = document.createElement('code');
codeElement.textContent = match[1];
fragment.appendChild(codeElement);
// Slice off what we've already processed
markdown = markdown.slice(match.index + match[0].length);
}
return fragment;
}
function expandLinks(markdown) {
const fragment = document.createDocumentFragment();
const linkPattern = /\[([^\]]+)\]\(((?:#|https:\/\/)[^\)]+)\)/;
while (true) {
const match = linkPattern.exec(markdown);
if (!match) {
// No more links, parse the remaining text as inline
if (markdown) {
fragment.appendChild(expandInline(markdown));
}
break;
}
// Text before the link
const textBefore = markdown.slice(0, match.index);
if (textBefore) {
fragment.appendChild(expandInline(textBefore));
}
// Create the <a> element for the matched link
const linkElement = document.createElement('a');
linkElement.href = match[2];
// The link text itself can contain inline code
linkElement.appendChild(expandInline(match[1]));
fragment.appendChild(linkElement);
// Slice the processed portion off
markdown = markdown.slice(match.index + match[0].length);
}
return fragment;
}
function cell(nodes, className, colSpan) {
const td = document.createElement('td');
if (nodes) {
if (!Array.isArray(nodes)) {
nodes = [nodes];
}
td.append(...nodes);
}
if (className) {
td.className = className;
}
if (colSpan) {
td.colSpan = colSpan;
}
return td;
}
function link(href, child, helpText) {
const a = document.createElement('a');
if (href) {
a.setAttribute('href', href);
}
a.append(child);
if (helpText) {
a.setAttribute('aria-label', helpText);
a.title = helpText;
}
return a;
}
function img(src, width, height, alt, title) {
const img = document.createElement('img');
img.src = src;
img.width = width;
img.height = height;
img.alt = alt;
if (title) {
img.title = title;
}
return img;
}
function positionSpan(position) {
if (!position) {
return null;
}
const span = document.createElement('span');
span.className = `position ${position.replace(/ /g, '-')}`;
span.textContent = position;
return span;
}
function dlItem(term, value) {
const dt = document.createElement('dt');
dt.textContent = term;
const dd = document.createElement('dd');
dd.append(expandLinks(value));
return [dt, dd];
}
function details(description, rationale, describedById) {
if (!description && !rationale) {
return null;
}
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.textContent = 'Details';
summary.setAttribute('aria-describedby', describedById);
details.append(summary);
const dl = document.createElement('dl');
details.append(dl);
if (description) {
dl.append(...dlItem('Description', description));
}
if (rationale) {
dl.append(...dlItem('Rationale', rationale));
}
return details;
}
const imageMapping = {
'WHATWG': ['WHATWG.svg', 32, 32],
'W3C': ['W3C.svg', 47, 32],
'W3C CG': ['W3C-CG.svg', 32, 32],
'IETF': ['IETF.svg', 60, 32],
'Ecma': ['Ecma.png', 32, 32],
'Unicode': ['Unicode.svg', 32, 32],
'Proposal': ['Proposal.svg', 32, 32],
'Other': ['Other.svg', 32, 32],
};
function venuesImages(venues) {
const rv = [];
for (const venue of venues) {
if (rv.length > 0) {
rv.push(" ");
}
const [filename, width, height] = imageMapping[venue];
rv.push(img(`asset/${filename}`, width, height, venue, venue));
}
return rv;
}
function row(issueNum, { id, url, explainer, title, concerns, position, topics, venues, caniuse, bug, mdn, webkit, description, rationale }) {
const tr = document.createElement('tr');
const rv = [tr];
// Link
let selfId = '';
if (id) {
selfId += id;
} else {
selfId += issueNum;
}
tr.id = selfId;
tr.append(cell(link(`#${selfId}`, '🔗', 'Self-link'), 'link'));
// Specification
if (!url) {
url = explainer;
}
const specLink = link(url, expandInline(title));
specLink.id = `speclink-${selfId}`;
tr.append(cell(specLink, 'specification'));
// Position
tr.append(cell(positionSpan(position), 'positions'));
tr.dataset.position = position;
// Concerns
tr.append(cell(concerns?.join(', '), 'concerns'));
// Topics
tr.append(cell(topics?.join(', '), 'topics'));
tr.dataset.topics = topics?.join(' ');
// Venues
tr.append(cell(venuesImages(venues), 'venues'));
tr.dataset.venues = venues?.join(' ');
// More info
const infoLinks = [];
infoLinks.push(link(`https://github.com/mozilla/standards-positions/issues/${issueNum}`, `#${issueNum}`, 'Discussion'));
if (bug) {
infoLinks.push(" ", link(bug, img('asset/bugzilla.png', 32, 32, 'bug'), 'Mozilla bug'));
}
if (mdn) {
infoLinks.push(" ", link(mdn, img('asset/MDN.svg', 32, 32, 'MDN'), 'MDN'));
}
if (caniuse) {
infoLinks.push(" ", link(caniuse, '📊', 'Can I use'));
}
tr.append(cell(infoLinks, 'more-info'));
// Details
if (description || rationale) {
const detailsTr = document.createElement('tr');
detailsTr.className = 'details';
detailsTr.append(cell(details(description, rationale, specLink.id), null, 7));
rv.push(detailsTr);
}
return rv;
}
// Expand details for :target
function expandDetailsForTarget(id) {
const tr = document.getElementById(id);
if (!tr) {
return;
}
if (tr.nextElementSibling.className === "details") {
const details = tr.nextElementSibling.firstChild.firstChild;
details.open = true;
}
}
// Filter/search
function filterTable(searchText) {
const tbody = document.querySelector('tbody');
for (const tr of tbody.children) {
if (tr.classList.contains('details')) {
continue;
}
const show = shouldShowRow(searchText, tr);
tr.hidden = !show;
const nextTr = tr.nextElementSibling;
if (nextTr.classList.contains('details')) {
nextTr.hidden = !show;
}
}
}
function shouldShowRow(searchText, tr) {
const specTitle = tr.firstChild.nextSibling.textContent;
const position = tr.dataset.position;
const concerns = tr.firstChild.nextSibling.nextSibling.nextSibling.textContent;
const topics = tr.dataset.topics;
const venues = tr.dataset.venues;
const issueNum = tr.lastChild.firstChild.textContent;
const allText = [specTitle, position, concerns, topics, venues, issueNum].filter(Boolean).join(' ');
const links = tr.querySelectorAll('a[href]');
return allText.toLowerCase().includes(searchText.toLowerCase()) ||
[...links].find(a => a.getAttribute('href').includes(searchText));
}
function setupSearch() {
const searchInput = document.querySelector('input[name="search"]');
searchInput.oninput = e => filterTable(searchInput.value);
if (searchInput.value) {
filterTable(searchInput.value);
}
}
// Sorting
function setupSorting() {
const table = document.querySelector("table");
const headers = table.querySelectorAll("th");
headers.forEach((header, columnIndex) => {
if (!header.classList.contains('sortable')) {
return;
}
const button = document.createElement("button");
// "more-info" is the column that starts out being sorted; in reverse order.
const isInitDescending = header.classList.contains("more-info");
const sortNumerically = isInitDescending; // Coincidentally, this is the only numeric column.
button.setAttribute("aria-label", `Sort by ${header.textContent}`);
button.textContent = header.textContent;
header.setAttribute("aria-label", header.textContent);
header.textContent = '';
header.append(button);
if (isInitDescending) {
header.setAttribute("aria-sort", "descending");
}
button.onclick = () => {
const sortAscending = header.getAttribute("aria-sort") !== "ascending";
sortTable(table, columnIndex, sortAscending, sortNumerically);
headers.forEach((h) => h.removeAttribute("aria-sort"));
header.setAttribute("aria-sort", sortAscending ? "ascending" : "descending");
};
});
}
function sortTable(table, columnIndex, ascending, sortNumerically) {
const tbody = table.querySelector("tbody");
const rows = Array.from(tbody.querySelectorAll("tr[id]")); // Don't include .details rows
rows.sort((rowA, rowB) => {
const cellA = rowA.cells[columnIndex];
const cellB = rowB.cells[columnIndex];
let a = cellA.firstChild instanceof HTMLImageElement ? cellA.firstChild.alt : cellA.textContent;
let b = cellB.firstChild instanceof HTMLImageElement ? cellB.firstChild.alt : cellB.textContent;
// Handle empty cells: Always place them at the end
if (a === "" || b === "") {
if (a === "" && b === "") {
return 0;
}
return a === "" ? 1 : -1;
}
[a, b] = ascending ? [a, b] : [b, a];
return a.localeCompare(b, undefined, { numeric: sortNumerically });
});
for (const row of rows) {
const detailsRow = row.nextElementSibling?.className === 'details' ? row.nextElementSibling : null;
tbody.append(row);
if (detailsRow) {
tbody.append(detailsRow);
}
}
}
async function init() {
const data = await getData();
const tbody = document.querySelector('tbody');
for (const issueNum in data) {
const item = data[issueNum];
// Remove the below if check to include issues without a position.
if (item.position) {
// Newest issue first
tbody.prepend(...row(issueNum, item));
}
}
if (location.hash) {
expandDetailsForTarget(location.hash.substr(1));
}
onhashchange = e => {
expandDetailsForTarget(e.newURL.split('#')[1]);
};
setupSearch();
setupSorting();
document.querySelector('.noscript').remove();
document.documentElement.classList.remove('loading');
}
init();
</script>
</body>
</html>