diff --git a/api/GPUDevice.json b/api/GPUDevice.json index beed6402f2003b..59d885775ffd91 100644 --- a/api/GPUDevice.json +++ b/api/GPUDevice.json @@ -384,7 +384,7 @@ }, "storageTexture_access_read-write_read-only": { "__compat": { - "description": "read-write and read-only storageTexture.access", + "description": "`read-write` and `read-only` `storageTexture.access`", "spec_url": "https://gpuweb.github.io/gpuweb/wgsl/#memory-access-mode", "tags": [ "web-features:webgpu" @@ -427,7 +427,7 @@ }, "texture_rgb10a2uint": { "__compat": { - "description": "rgb10a2uint texture format", + "description": "`rgb10a2uint` texture format", "spec_url": "https://gpuweb.github.io/gpuweb/#dom-gputextureformat-rgb10a2uint", "tags": [ "web-features:webgpu" @@ -1320,7 +1320,7 @@ }, "texture_rgb10a2uint": { "__compat": { - "description": "rgb10a2uint texture format", + "description": "`rgb10a2uint` texture format", "spec_url": "https://gpuweb.github.io/gpuweb/#dom-gputextureformat-rgb10a2uint", "tags": [ "web-features:webgpu" @@ -1408,7 +1408,7 @@ }, "vertex_unorm10-10-10-2": { "__compat": { - "description": "unorm10-10-10-2 vertex format", + "description": "`unorm10-10-10-2` vertex format", "spec_url": "https://gpuweb.github.io/gpuweb/#dom-gpuvertexformat-unorm10-10-10-2", "tags": [ "web-features:webgpu" @@ -1652,7 +1652,7 @@ }, "texture_rgb10a2uint": { "__compat": { - "description": "rgb10a2uint texture format", + "description": "`rgb10a2uint` texture format", "spec_url": "https://gpuweb.github.io/gpuweb/#dom-gputextureformat-rgb10a2uint", "tags": [ "web-features:webgpu" @@ -1740,7 +1740,7 @@ }, "vertex_unorm10-10-10-2": { "__compat": { - "description": "unorm10-10-10-2 vertex format", + "description": "`unorm10-10-10-2` vertex format", "spec_url": "https://gpuweb.github.io/gpuweb/#dom-gpuvertexformat-unorm10-10-10-2", "tags": [ "web-features:webgpu" @@ -2100,7 +2100,7 @@ }, "texture_rgb10a2uint": { "__compat": { - "description": "rgb10a2uint texture format", + "description": "`rgb10a2uint` texture format", "spec_url": "https://gpuweb.github.io/gpuweb/#dom-gputextureformat-rgb10a2uint", "tags": [ "web-features:webgpu" @@ -2330,7 +2330,7 @@ }, "color_space_display-p3": { "__compat": { - "description": "display-p3 color space", + "description": "`display-p3` color space", "spec_url": "https://html.spec.whatwg.org/multipage/canvas.html#dom-predefinedcolorspace-display-p3", "tags": [ "web-features:webgpu" diff --git a/api/GPUTexture.json b/api/GPUTexture.json index 0b5757ca682aaf..85c784e767b755 100644 --- a/api/GPUTexture.json +++ b/api/GPUTexture.json @@ -582,7 +582,7 @@ }, "texture_rgb10a2uint": { "__compat": { - "description": "rgb10a2uint texture format", + "description": "`rgb10a2uint` texture format", "spec_url": "https://gpuweb.github.io/gpuweb/#dom-gputextureformat-rgb10a2uint", "tags": [ "web-features:webgpu" diff --git a/api/Notification.json b/api/Notification.json index 63adc0f24e85c4..109d62a5afe365 100644 --- a/api/Notification.json +++ b/api/Notification.json @@ -18,7 +18,7 @@ "chrome_android": { "version_added": "42", "partial_implementation": true, - "notes": "A notification can only be sent from a service worker. To show a notification, see ServiceWorkerRegistration.showNotification()." + "notes": "A notification can only be sent from a service worker. To show a notification, see `ServiceWorkerRegistration.showNotification()`." }, "edge": { "version_added": "14" @@ -56,8 +56,8 @@ "version_added": "16.4", "partial_implementation": true, "notes": [ - "The Notification interface is undefined, unless the page is a web app saved to the home screen. The app's manifest must have a non-default display value.", - "A notification can only be sent from a service worker. To show a notification, see ServiceWorkerRegistration.showNotification()." + "The `Notification` interface is undefined, unless the page is a web app saved to the home screen. The app's manifest must have a non-default `display` value.", + "A notification can only be sent from a service worker. To show a notification, see `ServiceWorkerRegistration.showNotification()`." ] }, "samsunginternet_android": { @@ -98,8 +98,8 @@ "version_added": "42", "partial_implementation": true, "notes": [ - "A notification can only be sent from a service worker. To show a notification, see ServiceWorkerRegistration.showNotification().", - "This constructor always throws a TypeError exception." + "A notification can only be sent from a service worker. To show a notification, see `ServiceWorkerRegistration.showNotification()`.", + "This constructor always throws a `TypeError` exception." ] }, "edge": { @@ -127,8 +127,8 @@ "version_added": "16.4", "partial_implementation": true, "notes": [ - "This constructor throws a ReferenceError exception, unless the page is a web app saved to the home screen. The app's manifest must have a non-default display value.", - "A notification can only be sent from a service worker. To show a notification, see ServiceWorkerRegistration.showNotification()." + "This constructor throws a `ReferenceError` exception, unless the page is a web app saved to the home screen. The app's manifest must have a non-default `display` value.", + "A notification can only be sent from a service worker. To show a notification, see `ServiceWorkerRegistration.showNotification()`." ] }, "samsunginternet_android": "mirror", @@ -893,7 +893,7 @@ "safari_ios": { "version_added": "16.4", "partial_implementation": true, - "notes": "The parent Notification interface is undefined unless the page is a web app saved to the home screen. The app's manifest must have a non-default display value." + "notes": "The parent `Notification` interface is undefined unless the page is a web app saved to the home screen. The app's manifest must have a non-default `display` value." }, "samsunginternet_android": "mirror", "webview_android": { @@ -1000,7 +1000,7 @@ "safari_ios": { "version_added": "16.4", "partial_implementation": true, - "notes": "The parent Notification interface is undefined unless the page is a web app saved to the home screen. The app's manifest must have a non-default display value." + "notes": "The parent `Notification` interface is undefined unless the page is a web app saved to the home screen. The app's manifest must have a non-default `display` value." }, "samsunginternet_android": "mirror", "webview_android": { diff --git a/api/_globals/performance.json b/api/_globals/performance.json index d512477e91fc8d..dec96f5a428151 100644 --- a/api/_globals/performance.json +++ b/api/_globals/performance.json @@ -36,7 +36,7 @@ "version_added": "8.5.0", "version_removed": "16.0.0", "partial_implementation": true, - "notes": "Available as a part of the perf_hooks module." + "notes": "Available as a part of the `perf_hooks` module." } ], "oculus": "mirror", @@ -88,7 +88,7 @@ "version_added": "11.7.0", "version_removed": "16.0.0", "partial_implementation": true, - "notes": "Available as a part of the perf_hooks module." + "notes": "Available as a part of the `perf_hooks` module." } ], "oculus": "mirror", diff --git a/lint/fix.js b/lint/fix.js index a6a23cba1d3787..7512de002d0456 100644 --- a/lint/fix.js +++ b/lint/fix.js @@ -18,6 +18,7 @@ import fixFeatureOrder from './fixer/feature-order.js'; import fixPropertyOrder from './fixer/property-order.js'; import fixStatementOrder from './fixer/statement-order.js'; import fixDescriptions from './fixer/descriptions.js'; +import fixNotes from './fixer/notes.js'; import fixFlags from './fixer/flags.js'; import fixLinks from './fixer/links.js'; import fixMDNURLs from './fixer/mdn-urls.js'; @@ -35,6 +36,7 @@ const dirname = fileURLToPath(new URL('.', import.meta.url)); /** @type {Readonly | string>>} */ const FIXES = Object.freeze({ descriptions: fixDescriptions, + notes: fixNotes, common_errors: fixCommonErrors, flags: fixFlags, links: fixLinks, diff --git a/lint/fixer/notes.js b/lint/fixer/notes.js new file mode 100644 index 00000000000000..84452f52aa75ce --- /dev/null +++ b/lint/fixer/notes.js @@ -0,0 +1,48 @@ +/* This file is a part of @mdn/browser-compat-data + * See LICENSE file for more information. */ + +import { replaceCodeTagsWithBackticks } from '../utils.js'; +import walk from '../../utils/walk.js'; + +/** + * @param {string | string[]} notes + * @returns {string | string[]} + */ +export const fixNotes = (notes) => { + if (Array.isArray(notes)) { + return notes.map(replaceCodeTagsWithBackticks); + } + return replaceCodeTagsWithBackticks(notes); +}; + +/** + * Fixes HTML in notes that should use Markdown syntax instead. + * @param {string} filename The filename containing compatibility info + * @param {string} actual The current content of the file + * @returns {string} expected content of the file + */ +const fixNotesFixer = (filename, actual) => { + if (filename.includes('/browsers/')) { + return actual; + } + + const data = JSON.parse(actual); + const walker = walk(undefined, data); + + for (const feature of walker) { + for (const support of Object.values(feature.compat.support)) { + for (const statement of Array.isArray(support) ? support : [support]) { + if (statement.notes) { + statement.notes = + /** @type {string | [string, string, ...string[]]} */ ( + fixNotes(statement.notes) + ); + } + } + } + } + + return JSON.stringify(data, null, 2); +}; + +export default fixNotesFixer; diff --git a/lint/fixer/notes.test.js b/lint/fixer/notes.test.js new file mode 100644 index 00000000000000..b55241527fc854 --- /dev/null +++ b/lint/fixer/notes.test.js @@ -0,0 +1,36 @@ +/* This file is a part of @mdn/browser-compat-data + * See LICENSE file for more information. */ + +import assert from 'node:assert/strict'; + +import { fixNotes } from './notes.js'; + +describe('fix -> notes', () => { + it('replaces tags with backticks in a string note', () => { + assert.equal( + fixNotes('Before version 90, foo was required.'), + 'Before version 90, `foo` was required.', + ); + }); + + it('replaces tags in each note in an array', () => { + assert.deepEqual( + fixNotes([ + 'Before version 90, foo was required.', + 'Use `bar` instead.', + ]), + ['Before version 90, `foo` was required.', 'Use `bar` instead.'], + ); + }); + + it('leaves notes without tags unchanged', () => { + assert.equal(fixNotes('Use `foo` instead.'), 'Use `foo` instead.'); + }); + + it('does not replace inside backticks', () => { + assert.equal( + fixNotes('The `` element is not supported.'), + 'The `` element is not supported.', + ); + }); +}); diff --git a/lint/linter/test-descriptions.js b/lint/linter/test-descriptions.js index 81178416cd02be..f2149e9211254a 100644 --- a/lint/linter/test-descriptions.js +++ b/lint/linter/test-descriptions.js @@ -3,8 +3,12 @@ import { styleText } from 'node:util'; +import { replaceCodeTagsWithBackticks } from '../utils.js'; + import { validateHTML } from './test-notes.js'; +export { replaceCodeTagsWithBackticks }; + /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ /** @import {CompatStatement} from '../../types/types.js' */ @@ -117,6 +121,16 @@ export const processData = (data, category, path) => { } if (data.description) { + const converted = replaceCodeTagsWithBackticks(data.description); + if (converted !== data.description) { + errors.push({ + ruleName: 'no_code_tag_in_description', + path, + actual: data.description, + expected: converted, + }); + } + errors.push(...validateHTML(data.description)); } diff --git a/lint/linter/test-descriptions.test.js b/lint/linter/test-descriptions.test.js index ba4449e8adcd14..e7f8ecddac5c69 100644 --- a/lint/linter/test-descriptions.test.js +++ b/lint/linter/test-descriptions.test.js @@ -113,4 +113,41 @@ describe('test-descriptions', () => { assert.equal(errors.length, 1); }); }); + + describe('HTML in descriptions', () => { + it('flags tags as no_code_tag_in_description', () => { + /** @type {CompatStatement} */ + const data = { + description: 'transient_attachment usage', + support: {}, + }; + const errors = processData(data, 'api', 'api.Foo.bar'); + const err = /** @type {DescriptionError} */ ( + errors.find( + (e) => + typeof e !== 'string' && + e.ruleName === 'no_code_tag_in_description', + ) + ); + assert.ok(err); + assert.equal(err.actual, 'transient_attachment usage'); + assert.equal(err.expected, '`transient_attachment` usage'); + }); + + it('does not flag descriptions without HTML', () => { + /** @type {CompatStatement} */ + const data = { + description: '`transient_attachment` usage', + support: {}, + }; + const errors = processData(data, 'api', 'api.Foo.bar'); + assert.ok( + !errors.some( + (e) => + typeof e !== 'string' && + e.ruleName === 'no_code_tag_in_description', + ), + ); + }); + }); }); diff --git a/lint/linter/test-notes.js b/lint/linter/test-notes.js index 070832e914ac2b..2488392d880eae 100644 --- a/lint/linter/test-notes.js +++ b/lint/linter/test-notes.js @@ -6,7 +6,7 @@ import { styleText } from 'node:util'; import HTMLParser from '@desertnet/html-parser'; import { marked } from 'marked'; -import { VALID_ELEMENTS } from '../utils.js'; +import { replaceCodeTagsWithBackticks, VALID_ELEMENTS } from '../utils.js'; /** @import {Linter, LinterData} from '../types.js' */ /** @import {Logger} from '../utils.js' */ @@ -112,6 +112,21 @@ const checkNotes = (notes, browser, feature, logger) => { logger.error(`Notes for ${styleText('bold', browser)} → ${error}`); } } + + for (const note of Array.isArray(notes) ? notes : [notes]) { + const converted = replaceCodeTagsWithBackticks(note); + if (converted !== note) { + logger.error( + styleText( + 'red', + `Notes for ${styleText('bold', browser)} use HTML code tags instead of backtick-quoted Markdown code + Actual: ${styleText('yellow', `"${note}"`)} + Expected: ${styleText('green', `"${converted}"`)}`, + ), + { fixable: true }, + ); + } + } }; /** diff --git a/lint/linter/test-notes.test.js b/lint/linter/test-notes.test.js new file mode 100644 index 00000000000000..620092edc16cca --- /dev/null +++ b/lint/linter/test-notes.test.js @@ -0,0 +1,88 @@ +/* This file is a part of @mdn/browser-compat-data + * See LICENSE file for more information. */ + +/** @import {CompatStatement} from '../../types/types.js' */ +/** @import {LinterMessage} from '../types.js' */ + +import assert from 'node:assert/strict'; + +import { Logger } from '../utils.js'; + +import testNotes from './test-notes.js'; + +/** + * Run the notes linter check and return logged messages. + * @param {CompatStatement} data + * @returns {Promise} + */ +const check = async (data) => { + const logger = new Logger('Notes', 'test.feature'); + await testNotes.check(logger, { + data, + path: { full: 'test.feature', category: 'api' }, + }); + return logger.messages; +}; + +describe('test-notes', () => { + describe('code tag in notes', () => { + it('flags a note with a tag', async () => { + /** @type {CompatStatement} */ + const data = { + support: { + chrome: { + version_added: '80', + notes: 'Before version 90, foo was required.', + }, + }, + }; + const messages = await check(data); + assert.ok(messages.some((m) => m.fixable)); + }); + + it('flags each note in an array with a tag', async () => { + /** @type {CompatStatement} */ + const data = { + support: { + chrome: { + version_added: '80', + notes: [ + 'Before version 90, foo was required.', + 'Use `bar` instead.', + ], + }, + }, + }; + const messages = await check(data); + assert.equal(messages.filter((m) => m.fixable).length, 1); + }); + + it('does not flag a note using backtick Markdown', async () => { + /** @type {CompatStatement} */ + const data = { + support: { + chrome: { + version_added: '80', + notes: 'Before version 90, `foo` was required.', + }, + }, + }; + const messages = await check(data); + assert.ok(!messages.some((m) => m.fixable)); + }); + + it('does not flag a tag inside backticks', async () => { + /** @type {CompatStatement} */ + const data = { + support: { + chrome: { + version_added: '80', + notes: 'The `` element is not supported.', + }, + }, + }; + const messages = await check(data); + assert.ok(!messages.some((m) => m.fixable)); + }); + }); +}); diff --git a/lint/utils.js b/lint/utils.js index 9f68684be88f17..f130f9513eb033 100644 --- a/lint/utils.js +++ b/lint/utils.js @@ -31,6 +31,14 @@ export const IS_WINDOWS = platform() === 'win32'; export const VALID_ELEMENTS = ['code', 'kbd', 'em', 'strong', 'a']; +/** + * Replace tags with backtick-quoted Markdown. + * @param {string} str The string to process + * @returns {string} The string with tags replaced by backticks + */ +export const replaceCodeTagsWithBackticks = (str) => + str.replace(/([^<]*)<\/code>/g, '`$1`'); + /** * Escapes common invisible characters. * @param {string} str The string to escape invisibles for diff --git a/lint/utils.test.js b/lint/utils.test.js index 3f331b9564aee2..c01ab372d9ff28 100644 --- a/lint/utils.test.js +++ b/lint/utils.test.js @@ -9,6 +9,7 @@ import { createStatementGroupKey, escapeInvisibles, jsonDiff, + replaceCodeTagsWithBackticks, } from './utils.js'; describe('utils', () => { @@ -79,6 +80,25 @@ describe('utils', () => { ); }); + it('`replaceCodeTagsWithBackticks()` works correctly', () => { + assert.equal( + replaceCodeTagsWithBackticks('transient_attachment usage'), + '`transient_attachment` usage', + ); + assert.equal( + replaceCodeTagsWithBackticks('foo and bar'), + '`foo` and `bar`', + ); + assert.equal( + replaceCodeTagsWithBackticks('`already` markdown'), + '`already` markdown', + ); + assert.equal( + replaceCodeTagsWithBackticks('Use `` element'), + 'Use `` element', + ); + }); + it('createStatementGroupKey() works correctly', () => { /** @type {Record} */ const tests = { diff --git a/webextensions/api/webRequest.json b/webextensions/api/webRequest.json index ed629cbdaa0c2c..7f30cba0a30022 100644 --- a/webextensions/api/webRequest.json +++ b/webextensions/api/webRequest.json @@ -2047,7 +2047,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false @@ -2598,7 +2598,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false @@ -3051,7 +3051,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false @@ -3506,7 +3506,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false @@ -4040,7 +4040,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false @@ -4508,7 +4508,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false @@ -5065,7 +5065,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false @@ -5595,7 +5595,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false @@ -6029,7 +6029,7 @@ "edge": "mirror", "firefox": { "version_added": "74", - "notes": "Classification flags emailtracking and emailtracking_content added in Firefox 104." + "notes": "Classification flags `emailtracking` and `emailtracking_content` added in Firefox 104." }, "firefox_android": { "version_added": false