diff --git a/css/custom.css b/css/custom.css index 8478a08d..21fcf093 100644 --- a/css/custom.css +++ b/css/custom.css @@ -1726,18 +1726,6 @@ header { text-align: center; } -.container img, .container-narrow img { - max-width: 100%; -} - - - - - - - - - /* replace w github */ table { @@ -1986,19 +1974,23 @@ pre code::before, pre code::after { white-space: normal; } -#feature-support .img-container { - width: 32px; - height: 32px; +#feature-support td, #feature-support th { + position: relative; /* for tooltip */ } -#feature-support td { - position: relative; /* for tooltip */ +#feature-support th img { + height: 32px; + padding: 2px 0; } #feature-support td:hover, #feature-support td:focus, #feature-support td:focus-within { background: rgba(0, 0, 0, .04); } +#feature-support time { + white-space: nowrap; +} + .feature-cell { position: relative; height: 24px; /* height of the icon inside */ @@ -2052,6 +2044,8 @@ pre code::before, pre code::after { list-style: lower-alpha; font-size: 0.7em; margin: 0 0 1em 0; + columns: 32em auto; + column-gap: 2em; } #feature-support-scrollbox + ol > li { @@ -2068,6 +2062,7 @@ pre code::before, pre code::after { white-space: normal; background: #fefefe; font-size: 0.8em; + font-weight: normal; border-radius: 2px; outline: none; @@ -2107,3 +2102,36 @@ pre code::before, pre code::after { [data-placement="bottom"] > .feature-tooltip-arrow { top: 0; transform: translateY(-50%) rotate(45deg); } [data-placement="left"] > .feature-tooltip-arrow { right: 0; transform: translateX(50%) rotate(135deg); } [data-placement="right"] > .feature-tooltip-arrow { left: 0; transform: translateX(-50%) rotate(-45deg); } + +#feature-support-error { + background: #d0305029; + color: #ab1f3f; + margin-bottom: 1em; + display: flex; + align-items: center; +} + +#feature-support-error .alert-icon { + font-size: 42px; + line-height: 1; + margin: 0 16px; + font-variant-emoji: text; +} + +#feature-support-error .alert-body { + padding: 1em 0; +} + +#feature-support-error .alert-title { + font-size: 1.25em; + font-weight: 500; + line-height: 1.5; +} + +#feature-support-error .alert-subtitle { + font-size: .875em; +} + +#feature-support-error a { + color: #0a5497; +} diff --git a/css/webassembly.svg b/css/webassembly.svg index 9c081290..4d3009bc 100644 --- a/css/webassembly.svg +++ b/css/webassembly.svg @@ -1,35 +1 @@ - - - - - web-assembly-logo - - - - - - - - - - - - - - - - - - - - - - +WebAssembly logo \ No newline at end of file diff --git a/features.json b/features.json index 3e450e46..d7875b24 100644 --- a/features.json +++ b/features.json @@ -4,12 +4,21 @@ "bigInt": { "description": "JS BigInt to Wasm i64 integration", "url": "https://github.com/WebAssembly/JS-BigInt-integration", - "phase": 5 + "phase": 5, + "stdznDate": "2020-06-09", + "specVersion": "2.0" }, "bulkMemory": { "description": "Bulk memory operations", "url": "https://github.com/WebAssembly/bulk-memory-operations/blob/master/proposals/bulk-memory-operations/Overview.md", - "phase": 5 + "phase": 5, + "stdznDate": "2021-02-10", + "specVersion": "2.0" + }, + "gc": { + "description": "Garbage collection", + "url":"https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md", + "phase": 3 }, "exceptions": { "description": "Exception handling", @@ -19,16 +28,18 @@ "extendedConst": { "description": "Extended constant expressions", "url": "https://github.com/WebAssembly/extended-const/blob/master/proposals/extended-const/Overview.md", - "phase": 4 + "phase": 4, + "stdznDate": "2023-01-31", + "specVersion": "2.0" }, - "gc": { - "description": "Garbage collection", - "url":"https://github.com/WebAssembly/gc", + "threads": { + "description": "Threads and atomics", + "url": "https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md", "phase": 3 }, - "memory64": { - "description": "Memory64", - "url": "https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md", + "typeReflection": { + "description": "Type reflection", + "url": "https://github.com/WebAssembly/js-types/blob/main/proposals/js-types/Overview.md", "phase": 3 }, "multiMemory": { @@ -36,55 +47,64 @@ "url": "https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md", "phase": 3 }, + "memory64": { + "description": "Memory64", + "url": "https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md", + "phase": 3 + }, "multiValue": { "description": "Multi-value", "url": "https://github.com/WebAssembly/spec/blob/master/proposals/multi-value/Overview.md", - "phase": 4 + "phase": 4, + "stdznDate": "2020-03-11", + "specVersion": "2.0" }, "mutableGlobals": { "description": "Mutable globals", "url": "https://github.com/WebAssembly/mutable-global/blob/master/proposals/mutable-global/Overview.md", - "phase": 5 + "phase": 5, + "stdznDate": "2018-06-06", + "specVersion": "1.0" }, "referenceTypes": { "description": "Reference types", "url": "https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md", - "phase": 5 + "phase": 5, + "stdznDate": "2021-02-10", + "specVersion": "2.0" }, "relaxedSimd": { "description": "Relaxed SIMD", - "url": "https://github.com/WebAssembly/relaxed-simd/tree/main/proposals/relaxed-simd", + "url": "https://github.com/WebAssembly/relaxed-simd/blob/main/proposals/relaxed-simd/Overview.md", "phase": 3 }, - "saturatedFloatToInt": { - "description": "Non-trapping float-to-int conversions", - "url": "https://github.com/WebAssembly/spec/blob/master/proposals/nontrapping-float-to-int-conversion/Overview.md", - "phase": 5 - }, "signExtensions": { "description": "Sign-extension operations", "url": "https://github.com/WebAssembly/spec/blob/master/proposals/sign-extension-ops/Overview.md", - "phase": 5 + "phase": 5, + "stdznDate": "2020-03-11", + "specVersion": "2.0" + }, + "saturatedFloatToInt": { + "description": "Non-trapping float-to-int conversions", + "url": "https://github.com/WebAssembly/spec/blob/master/proposals/nontrapping-float-to-int-conversion/Overview.md", + "phase": 5, + "stdznDate": "2020-03-11", + "specVersion": "2.0" }, "simd": { "description": "Fixed-width SIMD", "url": "https://github.com/WebAssembly/simd/blob/master/proposals/simd/SIMD.md", - "phase": 5 + "phase": 5, + "stdznDate": "2021-07-14", + "specVersion": "2.0" }, "tailCall": { "description": "Tail calls", "url": "https://github.com/WebAssembly/tail-call/blob/master/proposals/tail-call/Overview.md", - "phase": 4 - }, - "threads": { - "description": "Threads and atomics", - "url": "https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md", - "phase": 3 - }, - "typeReflection": { - "description": "Type reflection", - "url": "https://github.com/WebAssembly/js-types/blob/main/proposals/js-types/Overview.md", - "phase": 3 + "phase": 4, + "stdznDate": "2023-01-17", + "specVersion": "2.0" } }, "browsers": { @@ -147,39 +167,6 @@ "threads": ["14.1", "Supported in desktop Safari since 14.1 and iOS Safari since 14.5"] } }, - "Wasmtime": { - "url": "https://wasmtime.dev/", - "logo": "/images/bca.svg", - "features": { - "bigInt": null, - "bulkMemory": "0.20", - "memory64": ["flag", "Requires flag `--wasm-features=memory64`"], - "multiMemory": ["flag", "Requires flag `--wasm-features=multi-memory`"], - "multiValue": "0.17", - "mutableGlobals": true, - "referenceTypes": "0.20", - "saturatedFloatToInt": true, - "signExtensions": true, - "simd": "0.33", - "threads": null - } - }, - "Wasmer": { - "url": "https://wasmer.io/", - "logo": "/images/wasmer.svg", - "features": { - "bigInt": null, - "bulkMemory": "1.0", - "multiValue": "1.0", - "mutableGlobals": "0.7", - "referenceTypes": "2.0", - "saturatedFloatToInt": true, - "signExtensions": true, - "simd": "2.0", - "threads": null, - "typeReflection": "2.0" - } - }, "Node.js": { "url": "https://nodejs.org/", "logo": "/images/nodejs.svg", @@ -219,7 +206,40 @@ "simd": "1.9", "tailCall": ["flag", "Requires flag `--v8-flags=--experimental-wasm-return-call`"], "threads": "1.9", - "typeReflection": ["flag", "Requires corresponding v8 flag (`--v8-flags=\"...\"`)"] + "typeReflection": ["flag", "Requires flag `--v8-flags=--experimental-wasm-type-reflection`"] + } + }, + "Wasmtime": { + "url": "https://wasmtime.dev/", + "logo": "/images/bca.svg", + "features": { + "bigInt": null, + "bulkMemory": "0.20", + "memory64": ["flag", "Requires flag `--wasm-features=memory64`"], + "multiMemory": ["flag", "Requires flag `--wasm-features=multi-memory`"], + "multiValue": "0.17", + "mutableGlobals": true, + "referenceTypes": "0.20", + "saturatedFloatToInt": true, + "signExtensions": true, + "simd": "0.33", + "threads": null + } + }, + "Wasmer": { + "url": "https://wasmer.io/", + "logo": "/images/wasmer.svg", + "features": { + "bigInt": null, + "bulkMemory": "1.0", + "multiValue": "1.0", + "mutableGlobals": "0.7", + "referenceTypes": "2.0", + "saturatedFloatToInt": true, + "signExtensions": true, + "simd": "2.0", + "threads": null, + "typeReflection": "2.0" } }, "wasm2c": { diff --git a/features.schema.json b/features.schema.json index c79b8ab9..43f80770 100644 --- a/features.schema.json +++ b/features.schema.json @@ -7,13 +7,25 @@ "feature-info": { "type": "object", "properties": { - "description": { "type": "string" }, + "description": { "type": "string", "minLength": 1 }, "url": { "type": "string", "format": "uri-reference" }, - "phase": { "type": "integer", "minimum": 1 } + "phase": { "enum": [1, 2, 3] } }, "additionalProperties": false, "required": ["description", "url", "phase"] }, + "feature-info-standardized": { + "type": "object", + "properties": { + "description": { "type": "string", "minLength": 1 }, + "url": { "type": "string", "format": "uri-reference" }, + "phase": { "enum": [4, 5] }, + "stdznDate": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, + "specVersion": { "type": "string", "minLength": 1 } + }, + "additionalProperties": false, + "required": ["description", "url", "phase", "stdznDate", "specVersion"] + }, "browser-features": { "type": "object", "properties": { @@ -66,7 +78,12 @@ "$schema": { "type": "string", "format": "uri-reference" }, "features": { "type": "object", - "additionalProperties": { "$ref": "#/definitions/feature-info" } + "additionalProperties": { + "oneOf": [ + { "$ref": "#/definitions/feature-info" }, + { "$ref": "#/definitions/feature-info-standardized" } + ] + } }, "browsers": { "type": "object", diff --git a/images/wasm2c.svg b/images/wasm2c.svg index e6f68671..0f9ddb82 100644 --- a/images/wasm2c.svg +++ b/images/wasm2c.svg @@ -1,32 +1 @@ - - - - - - - - - - web-assembly-logo - - - - - - - - - - - - web-assembly-logo - - - - +wasm2c logo \ No newline at end of file diff --git a/roadmap.js b/roadmap.js index 1bc598c1..31a1cc38 100644 --- a/roadmap.js +++ b/roadmap.js @@ -1,6 +1,15 @@ -(async () => { - 'use strict'; +'use strict'; + +// This variable is unfortunately in the global scope, hence the weird name +const __feature_table_error_handler = (e) => { + const banner = document.getElementById('feature-support-error'); + if (banner.style.display) { + console.error('Failed to load feature table:', e); + banner.style.display = ''; + } +} +(async () => { function partitionArray(arr, condition) { const matched = []; const unmatched = []; @@ -65,7 +74,7 @@ h('th', { scope: 'col', id: idMap['table-col'](name) }, [ h('a', { href: url, target: '_blank' }, [ // https://www.w3.org/WAI/WCAG22/Techniques/html/H2 - h('img', { src: logo, width: 48, height: 32, alt: '' }), + h('img', { src: logo, alt: '' }), h('br'), name ]) @@ -77,7 +86,13 @@ ); let featureGroups = partitionArray( - Object.entries(features).map(([name, feature]) => Object.assign(feature, { name })), + Object.entries(features) + .map(([name, feature]) => Object.assign(feature, { name })) + .sort((a, b) => { // Sort by phase descending then date ascending + let i; + if (i = b.phase - a.phase) return i; + return (a.stdznDate || '9999-99-99').localeCompare(b.stdznDate || '9999-99-99') + }), feature => feature.phase >= 4 ); @@ -86,37 +101,24 @@ { name: 'In-progress proposals', features: featureGroups.unmatched }, ]; - // Collect all notes and assign an index to each unique item - // { "First unique note": 0, "Second unique note": 1, ...} + // Collect all footnotes const notes = Object.values(browsers).flatMap(b => Object.values(b.features) .filter(s => Array.isArray(s)) .map(s => s[1]) ); - const note2index = new Map(); - let noteIndex = 0; - for (const note of notes) { - if (!note2index.has(note)) { - note2index.set(note, noteIndex++); - } - } + const noteCache = new Map(); + let nextNoteId = 0; - // Generate the footnote list. They are later referenced in the actual table. + // List containing all footnotes const noteList = document.createElement('ol'); // Place footnote list outside of the scolling area scrollbox.parentNode.insertBefore(noteList, scrollbox.nextSibling); - for (const [note, index] of note2index) { - const item = h('li', { id: `feature-note-${index}` }); - noteList.appendChild(item).appendChild(renderNote(note)); - } - - // Create an element that links to the specified footnote. - // Also returns the HTML id of the footnote it refers to. - function createNoteRef(index) { - const id = `feature-note-${index}`; - return [id, h('a', { href: `#${id}` }, [`[${toAlphabet(index)}]`])]; - } + // Clip the tooltips to both and the scrollbox. + // the former is to avoid blocking out the headers; + // the latter is to keep the tooltip inside the scrollable area + const tooltipBoundary = [tBody, scrollbox]; const columnCount = 2 + Object.keys(browsers).length; for (const { name: groupName, features } of featureGroups) { @@ -137,28 +139,34 @@ }, [groupName]) ]) ); - for (const { name: featName, description, url } of features) { + for (const feat of features) { + // Feature detection for "Your browser" const detectResult = h('td', { - headers: [idMap['table-col']('Your browser'), idMap['table-row'](featName)].join(' ') + headers: [idMap['table-col']('Your browser'), idMap['table-row'](feat.name)].join(' ') }, [buildCellInner('loading')]); - detectWasmFeature(featName).then(supported => { + detectWasmFeature(feat.name).then(supported => { detectResult.textContent = ''; detectResult.appendChild(buildCellInner(supported ? 'yes' : 'no')); - addTooltip(detectResult, supported ? '✓ Supported' : '✗ Not supported', [tBody, scrollbox]); + return addTooltip(detectResult, supported ? '✓ Supported' : '✗ Not supported', tooltipBoundary); }, _err => { detectResult.textContent = ''; detectResult.appendChild(buildCellInner('unknown')); - addTooltip(detectResult, 'Detection unavailable for this feature', [tBody, scrollbox]); + return addTooltip(detectResult, 'Detection unavailable for this feature', tooltipBoundary); }); + // Feature name and it's tooltip + const featureLink = h('a', { href: feat.url, target: '_blank' }, [feat.description]); + const featureHeader = h('th', { + scope: 'row', + id: idMap['table-row'](feat.name), + headers: idMap['table-group'](groupName) + }, [featureLink]); + addTooltip(featureLink, buildFeatureTooltip(feat), [scrollbox], featureHeader); + tBody.append( h('tr', {}, [ - h('th', { - scope: 'row', - id: idMap['table-row'](featName), - headers: idMap['table-group'](groupName) - }, [h('a', { href: url, target: '_blank' }, [description])]), + featureHeader, detectResult, ...Object.entries(browsers).map(([browserName, { features }]) => { // Meaning of each entry: @@ -171,7 +179,7 @@ // ...and any combination thereof /** @type {null|boolean|string|[boolean|string,string]} */ - let support = features[featName]; + let support = features[feat.name]; let box, note; // First extract the footnote part if it's an array @@ -201,12 +209,17 @@ } else { if (support !== true) throw new TypeError(); box = buildCellInner('yes'); - // Magic value, keep in sync with `renderNote` - note ||= '✓ Supported, introduced in unknown version'; + if (!note) { + note = document.createDocumentFragment(); + note.append('✓ Supported, introduced in unknown version ', h('a', { + href: 'https://github.com/WebAssembly/website/blob/main/features.json', + target: '_blank' + }, ['(contribute data)'])); + } } const cell = h('td', { - headers: [idMap['table-col'](browserName), idMap['table-row'](featName)].join(' ') + headers: [idMap['table-col'](browserName), idMap['table-row'](feat.name)].join(' ') }, [box]); // Give the cell itself an `aria-lebel` to avoid screen readers calling it "empty cell". @@ -216,46 +229,66 @@ icon.removeAttribute('aria-label'); } - if (note && note2index.has(note)) { + if (note && notes.includes(note)) { cell.tabIndex = 0; // focusable - const index = note2index.get(note); - const [noteId, refLink] = createNoteRef(index); - box.appendChild(h('sup', {}, [refLink])); - - const noteItem = document.getElementById(noteId); - if (noteItem) { - cell.addEventListener('mouseenter', () => noteItem.classList.add('ref-highlight')); - cell.addEventListener('mouseleave', () => noteItem.classList.remove('ref-highlight')); + + // If we already have a
  • associated with this note, just use that. + let cache = noteCache.get(note); + if (!cache) { + const index = nextNoteId++; + const item = h('li', { id: `feature-note-${index}` }); + noteCache.set(note, cache = { index, item }); + noteList.appendChild(item).appendChild(renderNote(note)); } + + const { index, item } = cache; + const noteRef = h('a', { href: `#${item.id}` }, [`[${toAlphabet(index)}]`]); + box.appendChild(h('sup', {}, [noteRef])); + + cell.addEventListener('mouseenter', () => item.classList.add('ref-highlight')); + cell.addEventListener('mouseleave', () => item.classList.remove('ref-highlight')); } - // Clip to both and the scrollbox. - // the former is to avoid blocking out the headers; - // the latter is to keep the tooltip inside the scrollable area - addTooltip(cell, note, [tBody, scrollbox]); + addTooltip(cell, note, tooltipBoundary); return cell; }) ]) ); - tBody.lastElementChild.setAttribute('aria-describedby', idMap['table-row'](featName)); + tBody.lastElementChild.setAttribute('aria-describedby', idMap['table-row'](feat.name)); } } function buildCellInner(type, text) { const content = text || icon(type); - return h('div', { className: `feature-cell icon-${type}`}, [content]); + return h('div', { className: `feature-cell icon-${type}` }, [content]); + } + + function buildFeatureTooltip(feat) { + if (feat.stdznDate) { + const fragment = document.createDocumentFragment(); + fragment.append(`Phase ${feat.phase} proposal, standardized on `); + fragment.append(h('time', { dateTime: feat.stdznDate }, [feat.stdznDate])); + fragment.append(` as part of WebAssembly ${feat.specVersion}`); + return fragment; + } else { + return `Phase ${feat.phase} proposal` + } } function renderNote(note) { - const fragment = document.createDocumentFragment(); - const isMissingData = note.includes('introduced in unknown version'); - - // Transform markdown-like backticks into html - while (note) { - const [head, body, tail] = splitParts(note, '`'); - head && fragment.append(head); - body && fragment.appendChild(h('code', {}, [body])); - note = tail; + let fragment; + if (typeof note === 'string') { + fragment = document.createDocumentFragment(); + + // Transform markdown-like backticks into html + while (note) { + const [head, body, tail] = splitParts(note, '`'); + head && fragment.append(head); + body && fragment.appendChild(h('code', {}, [body])); + note = tail; + } + } else { + fragment = note; } const firstNode = fragment.firstChild; @@ -274,13 +307,6 @@ } } - if (isMissingData) { - fragment.appendChild(h('a', { - href: 'https://github.com/WebAssembly/website/blob/master/features.json', - target: '_blank' - }, [' (contribute data)'])) - } - return fragment; } @@ -299,9 +325,7 @@ // Lazy-loading function _loadTooltipModule() { - // Be sure to change the preloads in markdown when updating url. - // The ESM bundle of this package doesn't work with unpkg.com. - const module = import('https://cdn.jsdelivr.net/npm/@floating-ui/dom@1/+esm'); + const module = import(document.getElementById('preload-tooltip').href); const subscribers = new Set(); const updateAll = () => { for (const fn of subscribers) fn(); }; @@ -311,7 +335,7 @@ window.addEventListener('resize', updateAll, { passive: true }); let counter = 0; - return (reference, note, boundary) => + return (reference, note, boundary, parent = reference) => module.then(({ computePosition, offset, flip, shift, arrow }) => { const tooltipId = `tooltip-${counter++}`; const tooltip = h('div', { id: tooltipId, className: 'feature-tooltip', role: 'tooltip' }); @@ -377,16 +401,15 @@ timeout = setTimeout(() => setVisible(false), 80); }); - reference.appendChild(tooltip); + parent.appendChild(tooltip); reference.setAttribute('aria-describedby', tooltipId); return tooltip; - }); + }).catch(__feature_table_error_handler); } function _loadFeatureDetectModule() { - // Be sure to change the preloads in markdown when updating url. - const module = import('https://cdn.jsdelivr.net/npm/wasm-feature-detect@1.5/dist/esm/index.js'); + const module = import(document.getElementById('preload-detect').href); return (featureName) => module .then(wasmFeatureDetect => wasmFeatureDetect[featureName]()); } -})(); +})().catch(__feature_table_error_handler); diff --git a/roadmap.md b/roadmap.md index ee201218..fad2edb3 100644 --- a/roadmap.md +++ b/roadmap.md @@ -27,12 +27,22 @@ After the initial release, WebAssembly has been gaining new features through the The table below aims to track implemented features in popular engines: +
    +
    + + - - To detect supported features at runtime from JavaScript, check out the [`wasm-feature-detect` library](https://github.com/GoogleChromeLabs/wasm-feature-detect), which powers the "Your browser" column above.