diff --git a/src/lib/study.js b/src/lib/study.js index 850b003c..b30bbc4a 100644 --- a/src/lib/study.js +++ b/src/lib/study.js @@ -241,9 +241,11 @@ function getAnomalyGroupFromType(type) { /** * Structure a flat list of anomalies to the requested structure */ -function structureResults(structure, anomalies, crawlResults) { +function structureResults(structure, sort, anomalies, crawlResults) { const levels = structure.split('/') .map(level => level.replace(/\s+/g, '')); + const sortKeys = sort.split('/') + .map(level => level.replace(/\s+/g, '')); const report = []; switch (levels[0]) { @@ -348,10 +350,25 @@ function structureResults(structure, anomalies, crawlResults) { break; } + switch (sortKeys[0] ?? 'default') { + case 'name': + report.sort((e1, e2) => e1.name.localeCompare(e2.name)); + break; + case 'title': + // Note: for actual anomalies, the title is the anomaly message. All + // other entries have title. + report.sort((e1, e2) => (e1.title ?? e1.message).localeCompare(e2.title ?? e2.message)); + break; + case 'default': + default: + break; + } + if (levels.length > 1) { const itemsStructure = levels.slice(1).join('/'); + const itemsSort = sortKeys.slice(1).join('/'); for (const entry of report) { - entry.items = structureResults(itemsStructure, entry.anomalies, crawlResults); + entry.items = structureResults(itemsStructure, itemsSort, entry.anomalies, crawlResults); delete entry.anomalies; } } @@ -556,6 +573,7 @@ export default async function study(specs, options = {}) { const what = options.what ?? ['all']; const structure = options.structure ?? 'type + spec'; + const sort = options.sort ?? 'default'; const format = options.format ?? 'issue'; if (!what.includes('all')) { @@ -600,7 +618,7 @@ export default async function study(specs, options = {}) { // Now that we have a flat report of anomalies, // let's structure and serialize it as requested - const report = structureResults(structure, anomalies, options.crawlResults); + const report = structureResults(structure, sort, anomalies, options.crawlResults); // And serialize it using the right format const result = { diff --git a/strudy.js b/strudy.js index bbd4bdd2..370bdaed 100644 --- a/strudy.js +++ b/strudy.js @@ -68,6 +68,7 @@ program .option('-i, --issues ', 'report issues as markdown files in the given folder') .option('-m, --max ', 'maximum number of issue files to create/update', myParseInt, 0) .option('-s, --spec ', 'restrict analysis to given specs') + .option('--sort ', 'key(s) to use to sort the structured report', 'default') .option('--structure ', 'report structure', 'type+spec') .option('--tr ', 'path/URL to crawl report on published specs') .option('--update-mode ', 'what issue files to update', 'new') @@ -136,6 +137,39 @@ Usage notes for some of the options: For instance: $ strudy inspect . --spec picture-in-picture https://w3c.github.io/mediasession/ +--sort + Specifies the key(s) to use to sort each level in the structured report. + Use "/" to separate levels. See --structure for details on the possible + report structure. + + Possible keys: + "default" follow the natural order of the underlying structures, e.g. + return specs in the order in which they appear in the initial + list, anomalies in extraction order (which usually follows the + document order) + "name" sort entries by the name. For a "spec" level, the name is the + spec's shortname. For a "type" level, the name is the anomaly + type name. For a "type+spec" level, the name is the name of the + file that would be created if --issues is set, meaning the spec's + shortname completed with the anomaly type name. + "title" sort entries by their title. For a "spec" level, the title is the + spec's title. For the final level, the title is the anomaly + message. Etc. + + If the --sort value contains more levels than there are in the structured + report, additional keys are ignored. If the value contails fewer levels than + there are in the structured report, the default order is used for unspecified + levels. + + For example, if the structure is "type/spec", the --sort option could be: + "default" to use the default order at all levels + "default/title" to use the default order for the root level, and to sort + specs by title + "name/title/title" to sort anomaly types by names, specs by title, and + anomalies by message. + + Sort is always ascending. + --structure Describes the hierarchy in the report(s) that Strudy returns. Possible values: "flat" no level, report anomalies one by one @@ -257,6 +291,7 @@ Format must be one of "json" or "markdown".`) const anomaliesReport = await study(edReport.results, { what: options.what, structure: options.structure, + sort: options.sort, format: options.format === 'json' ? 'json' : (options.issues ? 'issue' : 'full'), diff --git a/test/study.js b/test/study.js index d439997b..de633d5f 100644 --- a/test/study.js +++ b/test/study.js @@ -191,4 +191,22 @@ describe('The main study function', function () { See [Dealing with the event loop](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-for-spec-authors) in the HTML specification for guidance on how to deal with algorithm sections that run *in parallel*.` }); }); + + it('sorts entries as requested in the final report', async function() { + const crawlResult = [ + populateSpec(specUrl, { error: 'Boo' }), + populateSpec(specUrl2, { error: 'Borked' }) + ]; + const report = await study(crawlResult, { structure: 'type/spec', sort: 'default/title', htmlFragments: {} }); + assertNbAnomalies(report.results, 1); + assertAnomaly(report.results, 0, { + title: 'Crawl error', + content: +`The following crawl errors occurred: +* [Hello universe API](https://w3c.github.io/universe/) + * [ ] Borked +* [Hello world API](https://w3c.github.io/world/) + * [ ] Boo` + }); + }); }); \ No newline at end of file