From 03c2786be9e71f4df93b31409a48b002d28f891a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Daoust?= Date: Thu, 7 May 2020 00:26:17 +0200 Subject: [PATCH] Add groups view The view gets generated from all features referenced throughout the pages. It needs to be enabled in the toc.json file. This view matches #127. The Media & Entertainment Interest Group recently requested it again to create some sort of "heat map" of what's going on. The list is pretty raw for now: it features the name of the group, the relevant specifications (or features in these specifications) that the group is working on (or was working on), and links to pages where the specifications are mentioned for further details. Changes to data files were needed to align group names with their official names. README not yet updated. --- assets/css/theme.css | 18 +- assets/img/groups.svg | 89 +++++++++ data/css-3d.json | 4 +- data/css-color-space.json | 4 +- data/css-size-adjust.json | 4 +- data/me-media-timed-events.json | 2 +- data/mediaqueries5.json | 4 +- js/generate-utils.js | 309 +++++++++++++++++++++++++++++--- js/generate.js | 6 +- js/template-groups.html | 8 + js/translations.json | 4 +- media/groups.html | 14 ++ media/toc.json | 3 + package.json | 2 +- 14 files changed, 431 insertions(+), 40 deletions(-) create mode 100644 assets/img/groups.svg create mode 100644 js/template-groups.html create mode 100644 media/groups.html diff --git a/assets/css/theme.css b/assets/css/theme.css index b44ef1b9..8e049476 100644 --- a/assets/css/theme.css +++ b/assets/css/theme.css @@ -204,7 +204,7 @@ tbody th, tbody td { padding-left: 0.5em;} th.feature { max-width: 150px; } -th.feature[rowspan] { +th[rowspan], td[rowspan] { vertical-align: top; padding-top: 1.5em; } @@ -225,6 +225,10 @@ td.milestones { font-size: 90%; min-width: 150px; } +[data-col=spec] { + max-width: 250px; +} + /************************************************************ @@ -350,7 +354,7 @@ Styles for the "current implementations" column } /* UA filtering menu */ -[data-col|=impl] details { +th[data-col|=impl] details { display: none; position: relative; padding: 0 0.1em; @@ -358,18 +362,18 @@ Styles for the "current implementations" column font-weight: 400; } -[data-col|=impl] details.active { +th[data-col|=impl] details.active { display: block; } -[data-col|=impl] summary { +th[data-col|=impl] summary { font-size: smaller; padding-left: 1em; outline: none; cursor: pointer; } -[data-col|=impl] div { +th[data-col|=impl] div { position: absolute; left: 0; right: 0; @@ -378,13 +382,13 @@ Styles for the "current implementations" column z-index: 10; } -[data-col|=impl] ul { +th[data-col|=impl] ul { list-style: none; padding-left: 0; margin: 0.6em; } -[data-col|=impl] li { +th[data-col|=impl] li { color: inherit; } diff --git a/assets/img/groups.svg b/assets/img/groups.svg new file mode 100644 index 00000000..4b21ae6e --- /dev/null +++ b/assets/img/groups.svg @@ -0,0 +1,89 @@ + +image/svg+xml \ No newline at end of file diff --git a/data/css-3d.json b/data/css-3d.json index 59e2861b..54dfcb1d 100644 --- a/data/css-3d.json +++ b/data/css-3d.json @@ -4,8 +4,8 @@ "feature": "3D effects", "wgs": [ { - "url": "http://www.w3.org/Style/CSS/members", - "label": "CSS Working Group" + "url": "https://www.w3.org/Style/CSS/", + "label": "Cascading Style Sheets (CSS) Working Group" } ], "impl": { diff --git a/data/css-color-space.json b/data/css-color-space.json index 745316d5..aa08665a 100644 --- a/data/css-color-space.json +++ b/data/css-color-space.json @@ -2,8 +2,8 @@ "url": "https://www.w3.org/TR/css-color-4/", "wgs": [ { - "url": "http://www.w3.org/Style/CSS/members", - "label": "CSS Working Group" + "url": "https://www.w3.org/Style/CSS/", + "label": "Cascading Style Sheets (CSS) Working Group" } ], "impl": { diff --git a/data/css-size-adjust.json b/data/css-size-adjust.json index e4e3b269..a431c8e6 100644 --- a/data/css-size-adjust.json +++ b/data/css-size-adjust.json @@ -3,8 +3,8 @@ "title": "CSS Mobile Text Size Adjustment Module Level 1", "wgs": [ { - "url": "http://www.w3.org/Style/CSS/members", - "label": "CSS Working Group" + "url": "https://www.w3.org/Style/CSS/members", + "label": "Cascading Style Sheets (CSS) Working Group" } ], "impl": { diff --git a/data/me-media-timed-events.json b/data/me-media-timed-events.json index 0e56fbca..85f46ef6 100644 --- a/data/me-media-timed-events.json +++ b/data/me-media-timed-events.json @@ -4,7 +4,7 @@ "wgs": [ { "url": "https://www.w3.org/2011/webtv/", - "label": "Media & Entertainment Interest Group" + "label": "Media and Entertainment Interest Group" } ] } \ No newline at end of file diff --git a/data/mediaqueries5.json b/data/mediaqueries5.json index 3158a8fd..22e19d70 100644 --- a/data/mediaqueries5.json +++ b/data/mediaqueries5.json @@ -3,8 +3,8 @@ "title": "Media Queries Level 5", "wgs": [ { - "label": "CSS Working Group", - "url": "https://www.w3.org/Style/CSS/members" + "label": "Cascading Style Sheets (CSS) Working Group", + "url": "https://www.w3.org/Style/CSS/" } ], "features": { diff --git a/js/generate-utils.js b/js/generate-utils.js index 29b59937..565a9a74 100644 --- a/js/generate-utils.js +++ b/js/generate-utils.js @@ -145,7 +145,12 @@ const tableColumnsPerType = { 'well-deployed': ['feature', 'spec', 'maturity', 'impl'], 'in-progress': ['feature', 'spec', 'maturity', 'impl'], 'exploratory-work': ['feature', 'spec', 'impl-intents'], - 'versions': ['feature', 'spec', 'maturity', 'seeAlso'] + 'versions': ['feature', 'spec', 'maturity', 'seeAlso'], + 'groups': [ + 'group', + { type: 'spec', title: 'Specification', hideGroup: true }, + 'mention' + ] }; /** @@ -282,6 +287,7 @@ const getSpecFeatureUrl = function (spec, featureId, linkto) { */ const createFeatureCell = function (column, refId, featureName, specInfo, implInfo, translate, lang, pos) { let cell = document.createElement((pos === 0) ? 'th' : 'td'); + cell.setAttribute('data-col', column.type); cell.appendChild(document.createTextNode(featureName)); cell.classList.add('feature'); return cell; @@ -314,26 +320,29 @@ const createSpecCell = function (column, refId, featureName, specInfo, implInfo, } let cell = document.createElement('td'); + cell.setAttribute('data-col', column.type); fillCell(cell, { localizedLabel: localizedLabel, label: label, url: specUrl }); - specInfo.deliveredBy = specInfo.deliveredBy || []; - specInfo.deliveredBy.forEach((wg, w) => { - wg.label = wg.label || ''; - wg.label = wg.label - .replace(/Cascading Style Sheets \(CSS\)/, 'CSS') - .replace(/Technical Architecture Group/, 'TAG') - .replace(/Web Real-Time Communications/, 'WebRTC'); - wg.localizedLabel = translate('groups', wg.label); - cell.appendChild(document.createElement('br')); - let span = document.createElement('span'); - span.classList.add('group'); - fillCell(span, wg); - cell.appendChild(span); - }); + if (!column.hideGroup) { + specInfo.deliveredBy = specInfo.deliveredBy || []; + specInfo.deliveredBy.forEach((wg, w) => { + wg.label = wg.label || ''; + wg.label = wg.label + .replace(/Cascading Style Sheets \(CSS\)/, 'CSS') + .replace(/Technical Architecture Group/, 'TAG') + .replace(/Web Real-Time Communications/, 'WebRTC'); + wg.localizedLabel = translate('groups', wg.label); + cell.appendChild(document.createElement('br')); + let span = document.createElement('span'); + span.classList.add('group'); + fillCell(span, wg); + cell.appendChild(span); + }); + } return cell; }; @@ -341,6 +350,7 @@ const createSpecCell = function (column, refId, featureName, specInfo, implInfo, const createMaturityCell = function (column, refId, featureName, specInfo, implInfo, translate, lang, pos) { // Render maturity info let cell = document.createElement('td'); + cell.setAttribute('data-col', column.type); let maturityInfo = maturityData(specInfo, translate); fillCell(cell, maturityInfo.maturity, maturityInfo.maturityIcon); cell.classList.add('maturity'); @@ -349,6 +359,7 @@ const createMaturityCell = function (column, refId, featureName, specInfo, implI const createImplCell = function (column, refId, featureName, specInfo, implInfo, translate, lang, pos) { let cell = document.createElement('td'); + cell.setAttribute('data-col', column.type); cell.appendChild(formatImplInfo(implInfo, translate)); cell.classList.add('impl'); return cell; @@ -356,6 +367,7 @@ const createImplCell = function (column, refId, featureName, specInfo, implInfo, const createSeeAlsoCell = function (column, refId, featureName, specInfo, implInfo, translate, lang, pos) { let cell = document.createElement('td'); + cell.setAttribute('data-col', column.type); cell.classList.add('seeAlso'); if (column.class) { cell.classList.add(column.class); @@ -418,6 +430,7 @@ const createSeeAlsoCell = function (column, refId, featureName, specInfo, implIn const createMilestonesCell = function (column, refId, featureName, specInfo, implInfo, translate, lang, pos) { let cell = document.createElement('td'); + cell.setAttribute('data-col', column.type); cell.classList.add('milestones'); if (specInfo.milestones) { let milestones = Object.keys(specInfo.milestones).map(maturity => { @@ -457,6 +470,40 @@ const createMilestonesCell = function (column, refId, featureName, specInfo, imp }; +const createGroupCell = function (column, refId, featureName, specInfo, implInfo, translate, lang, pos) { + let cell = document.createElement((pos === 0) ? 'th' : 'td'); + cell.setAttribute('data-col', column.type); + + specInfo.deliveredBy = specInfo.deliveredBy || []; + specInfo.deliveredBy.forEach((wg, w) => { + wg.label = wg.label || ''; + wg.label = wg.label + .replace(/Cascading Style Sheets \(CSS\)/, 'CSS') + .replace(/Technical Architecture Group/, 'TAG') + .replace(/Web Real-Time Communications/, 'WebRTC'); + wg.localizedLabel = translate('groups', wg.label); + fillCell(cell, wg); + }); + return cell; +}; + +const createMentionCell = function (column, refId, featureName, specInfo, implInfo, translate, lang, pos) { + let cell = document.createElement((pos === 0) ? 'th' : 'td'); + cell.setAttribute('data-col', column.type); + const pages = implInfo || []; + const seeLabel = translate('labels', 'in %page'); + if (featureName) { + cell.innerHTML = featureName; + } + pages.forEach((page, pos) => { + cell.innerHTML += ((pos > 0 || featureName) ? '
' : '') + + seeLabel.replace('%page', + '' + page.title + ''); + }); + cell.classList.add('mention'); + return cell; +}; + const tableColumnCreators = { 'feature': createFeatureCell, 'spec': createSpecCell, @@ -464,7 +511,9 @@ const tableColumnCreators = { 'impl': createImplCell, 'impl-intents': createImplCell, 'seeAlso': createSeeAlsoCell, - 'milestones': createMilestonesCell + 'milestones': createMilestonesCell, + 'group': createGroupCell, + 'mention': createMentionCell }; @@ -566,6 +615,7 @@ const loadTemplatePage = function (lang, pagetype) { const hero = $(document, 'header > *'); const sections = $(document, 'main > *'); const aboutContents = !!document.querySelector('[data-contents=about]'); + const groupsContents = !!document.querySelector('[data-contents=groups]'); // Replace doc by template doc document.documentElement.innerHTML = responseText; @@ -578,9 +628,9 @@ const loadTemplatePage = function (lang, pagetype) { $(document, '[data-pagetype="' + type + '"]').forEach(el => el.parentNode.removeChild(el))); - const completeContents = function (aboutHTML) { - if (aboutContents) { - document.querySelector('.main-content .container').innerHTML = aboutHTML; + const completeContents = function (html) { + if (aboutContents || groupsContents) { + document.querySelector('.main-content .container').innerHTML = html; } headElements.forEach(el => document.querySelector('head').appendChild(el)); @@ -622,6 +672,10 @@ const loadTemplatePage = function (lang, pagetype) { return loadLocalizedUrl('../js/template-about.html', lang) .then(responseText => completeContents(responseText)); } + else if (groupsContents) { + return loadLocalizedUrl('../js/template-groups.html', lang) + .then(responseText => completeContents(responseText)); + } else { return completeContents(); } @@ -769,6 +823,9 @@ const applyToc = function (toc, translate, lang, pagetype) { if (!title && pagetype.about) { title = toc.about.title || translate('labels', 'About this document'); } + else if (!title && pagetype.groups) { + title = toc.groups.title || translate('labels', 'List of relevant groups'); + } document.querySelector('title').textContent = (pagetype.menu ? '' : title + ' - ') + toc.title; $(document, 'section.contribute .discourse').forEach(link => { @@ -782,6 +839,10 @@ const applyToc = function (toc, translate, lang, pagetype) { document.body.className += ' menu'; } + if (pagetype.groups) { + document.body.setAttribute('data-groups', 'data-groups'); + } + let currentPage = toc.pages.find(page => window.location.pathname.endsWith(page.url) || window.location.pathname.endsWith(page.url.replace(/\.([^\.]+)$/, '.' + lang + '.$1'))); @@ -791,6 +852,11 @@ const applyToc = function (toc, translate, lang, pagetype) { window.location.pathname.endsWith(toc.about.url.replace(/\.([^\.]+)$/, '.' + lang + '.$1')))) { iconUrl = toc.about.icon || '../assets/img/about.svg'; } + else if (!currentPage && toc.groups && toc.groups.url && + (window.location.pathname.endsWith(toc.groups.url) || + window.location.pathname.endsWith(toc.groups.url.replace(/\.([^\.]+)$/, '.' + lang + '.$1')))) { + iconUrl = toc.groups.icon || '../assets/img/groups.svg'; + } let titleContainer = document.createElement('div'); titleContainer.setAttribute('data-beforemetadata', 'true'); titleContainer.innerHTML = templatePageTitle; @@ -832,6 +898,13 @@ const applyToc = function (toc, translate, lang, pagetype) { icon: '../assets/img/home.svg' }]; pages = pages.concat(toc.pages); + if (toc.groups) { + pages.push({ + title: toc.groups.title || translate('labels', 'List of relevant groups'), + url: toc.groups.url, + icon: toc.groups.icon || '../assets/img/groups.svg' + }); + } if (toc.about) { pages.push({ title: toc.about.title || translate('labels', 'About this document'), @@ -843,7 +916,8 @@ const applyToc = function (toc, translate, lang, pagetype) { const localizedUrl = ((lang === 'en') ? page.url : page.url.replace(/\.([^\.]+)$/, '.' + lang + '.$1')); - if (mainNav && (!toc.about || (page.url !== toc.about.url))) { + if (mainNav && (!toc.about || (page.url !== toc.about.url)) && + (!toc.groups || (page.url !== toc.groups.url))) { let mainNavItem = document.createElement('li'); mainNavItem.innerHTML = templateItem; mainNavItem.querySelector('a').href = localizedUrl; @@ -1138,6 +1212,12 @@ const fillTables = function (specInfo, implInfo, toc, translate, lang) { } }); + // If we're on the groups page, we should rather be building the list of + // relevant groups + if (!!document.body.getAttribute('data-groups')) { + return fillGroupsTable(specInfo, implInfo, toc, translate, lang, columnsPerType.groups); + } + // Helper function to extract all specs referenced with a data-featureid. // Fills out referencedIds and complete the optional references table // (indexed by feature name) with ids of specs to list in the table. @@ -1511,3 +1591,192 @@ const addFilteringMenus = function (translate) { th.appendChild(menu); }); }; + + +/** + * Generates the list of groups mentioned across pages + * (may take a while as the document starts by loading each page in turn!) + */ +const fillGroupsTable = function (specInfo, implInfo, toc, translate, lang, columns) { + return Promise.all(toc.pages.map(page => new Promise((resolve, reject) => { + // Load page in the background and extract all referenced feature ids + const iframe = document.createElement('iframe'); + iframe.hidden = true; + iframe.src = page.url; + iframe.addEventListener('load', () => { + iframe.contentWindow.document.addEventListener('generate', _ => { + const featureids = $(iframe.contentWindow.document, '[data-featureid]') + .map(el => { + const featureEl = el.closest('[data-feature]'); + return { + id: el.dataset['featureid'], + feature: featureEl ? featureEl.dataset['feature'] : null, + linkonly: (el.dataset['linkonly'] !== undefined) && + (el.dataset['linkonly'] !== 'false'), + page: page + }; + }) + .filter((feature, idx, self) => + self.findIndex(f => (f.id === feature.id) && + (f.feature === feature.feature)) === idx) + .filter(feature => !feature.linkonly); + resolve(featureids); + }); + }); + document.body.appendChild(iframe); + }))).then(res => { + // Drop hidden iframes and flatten the list of feature ids + $(document, 'iframe').forEach(el => el.parentNode.removeChild(el)); + return res.reduce((acc, featureids) => acc.concat(featureids), []); + }).then(res => res.map(feature => { + // Retrieve information about each feature + const [specId, featureId] = feature.id.split('/'); + feature.info = specInfo[specId]; + return feature; + })).then(res => { + // Group things by groups, feature ids, feature names and pages + const groups = []; + res.forEach(ref => { + if (!ref.info || !ref.info.deliveredBy) { + return; + } + + ref.info.deliveredBy.forEach(deliveredBy => { + let group = groups.find(g => g.deliveredBy.label === deliveredBy.label); + if (!group) { + group = { + deliveredBy: deliveredBy, + features: [] + }; + groups.push(group); + } + + // Override delivered by info (we're grouping by groups and specs may + // be developed by more than one group) + const info = Object.assign({}, ref.info, { deliveredBy: [deliveredBy]}); + + let spec = group.features.find(f => f.id === ref.id); + if (!spec) { + spec = { + id: ref.id, + info: info, + features: [] + } + group.features.push(spec); + } + + let feature = spec.features.find(f => f.name === ref.feature); + if (!feature) { + feature = { + name: ref.feature, + pages: [] + }; + spec.features.push(feature); + } + + let page = feature.pages.find(f => f.url === ref.page.url); + if (!page) { + page = ref.page; + feature.pages.push(page); + } + }); + }); + + groups.forEach(group => { + group.features.forEach(spec => { + // Only keep entries with no feature names if pages do not already + // appear elsewhere + const nofeature = spec.features.find(f => !f.name); + if (nofeature) { + nofeature.pages = nofeature.pages.filter(page => + !spec.features.find(f => f.name && f.pages.find(p => p.url === page.url))); + if (nofeature.pages.length === 0) { + spec.features = spec.features.filter(f => f.name); + } + } + }); + }); + return groups; + }).then(groups => groups.sort((g1, g2) => { + return translate('groups', g1.deliveredBy.label) + .localeCompare(translate('groups', g2.deliveredBy.label)); + })).then(groups => groups.map(group => { + group.features.sort((s1, s2) => { + return translate('specifications', s1.info.title) + .localeCompare(translate('specifications', s2.info.title)); + }); + return group; + })).then(groups => { + const tableWrapper = document.getElementById('table'); + const table = document.createElement('table'); + + // Fill the table headers + const row = document.createElement('tr'); + columns.forEach(column => { + const cell = document.createElement('th'); + cell.setAttribute('data-col', column.type); + cell.appendChild(document.createTextNode(column.title)); + row.appendChild(cell); + }); + + let thead = document.createElement('thead'); + thead.appendChild(row); + table.appendChild(thead); + + const tbody = document.createElement('tbody'); + table.appendChild(tbody); + + groups.forEach((group, groupidx) => { + group.features.forEach((spec, specidx) => { + spec.features.forEach((feature, featureidx) => { + const row = document.createElement('tr'); + tbody.appendChild(row); + columns.forEach((column, columnidx) => { + // Group name cell and spec cell may span multiple rows + if ((column.type === 'group') && + ((specidx > 0) || (featureidx > 0))) { + return; + } + else if ((column.type === 'spec') && (featureidx > 0)) { + return; + } + + // Find implementation info if needed, or use that field to + // pass page info + const [specId, featureId] = spec.id.split('/'); + let impl = implInfo[specId]; + if (featureId) { + impl = (impl.features ? impl.features[featureId] : {}); + } + if (column.type === 'mention') { + impl = feature.pages; + } + + // Create the appropriate cell + const cell = column.createCell( + column, spec.id, feature.name, + spec.info, impl, + translate, lang, columnidx); + row.appendChild(cell); + + // Make cells span multiple rows when needed + if (column.type === 'group') { + const rowspan = group.features.reduce( + (tot, spec) => tot + spec.features.length, 0); + if (rowspan > 1) { + cell.setAttribute('rowspan', rowspan); + } + } + else if (column.type === 'spec') { + if (spec.features.length > 1) { + cell.setAttribute('rowspan', spec.features.length); + } + } + }); + }); + }); + }); + + tableWrapper.appendChild(table); + }); +}; \ No newline at end of file diff --git a/js/generate.js b/js/generate.js index f2a10624..5c432746 100644 --- a/js/generate.js +++ b/js/generate.js @@ -53,7 +53,8 @@ why not). let pagetype = { menu: !!document.querySelector('*[data-pagetype="menu"]'), page: !!document.querySelector('*[data-pagetype="page"]'), - about: !!document.querySelector('*[data-contents="about"]') + about: !!document.querySelector('*[data-contents="about"]'), + groups: !!document.querySelector('*[data-contents="groups"]') }; if (!pagetype.menu && !pagetype.page) { @@ -117,8 +118,9 @@ loadScript('../js/utils.js') translate ]); }).then(results => { - fillTables(results[2], results[3], results[0], results[4], lang); + let promise = fillTables(results[2], results[3], results[0], results[4], lang); addFilteringMenus(results[4]); + return promise; }).then(_ => { // Remove duplicate warnings and report them warnings = warnings.filter((warning, idx, self) => self.indexOf(warning) === idx); diff --git a/js/template-groups.html b/js/template-groups.html new file mode 100644 index 00000000..ba709a91 --- /dev/null +++ b/js/template-groups.html @@ -0,0 +1,8 @@ +
+

+ This page summarizes groups mentioned throughout this document's pages, along with the name of the specifications and features that these groups are (or were) responsible for. Please refer to individual pages for details about the relevance and status of each work. +

+
+
+

List of groups

+
diff --git a/js/translations.json b/js/translations.json index 6fc05cd9..1a64bb93 100644 --- a/js/translations.json +++ b/js/translations.json @@ -13,7 +13,9 @@ "impl": "Current implementations", "impl-intents": "Implementation intents", "seeAlso": "See also", - "milestones": "Milestones" + "milestones": "Milestones", + "group": "Group", + "mentioned": "Feature" }, "implstatus": { "shipped": "Shipped", diff --git a/media/groups.html b/media/groups.html new file mode 100644 index 00000000..8afa1ed5 --- /dev/null +++ b/media/groups.html @@ -0,0 +1,14 @@ + + + + + List of relevant groups + + +
+

List of relevant groups

+
+
+ + + \ No newline at end of file diff --git a/media/toc.json b/media/toc.json index 1bd1c0e0..860464da 100644 --- a/media/toc.json +++ b/media/toc.json @@ -52,5 +52,8 @@ ], "about": { "url": "about.html" + }, + "groups": { + "url": "groups.html" } } diff --git a/package.json b/package.json index e9f0a13a..bd0f8877 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "dependencies": { "ajv-cli": "^3.0.0", "fetch-filecache-for-crawling": "^3.0.2", - "jsdom": "^11.8.0", + "jsdom": "^16.2.2", "mdn-browser-compat-data": "^1.0.0", "mkdirp": "^0.5.1", "ncp": "^2.0.0"