Skip to content

Commit

Permalink
Extract information on cancelability of events (#1534)
Browse files Browse the repository at this point in the history
* Extract information on cancelability of events

* Adapt schema and test suite

* Also add `cancelable` to the schema of the events post-processor

The `cancelable` property is copied as-is by the post-processor, which should
be what we want.

* Events extraction: report `cancelable` mismatches to log

In theory, specs could set or unset the `cancelable` property of a given event
differently whenever they fire the event. In practice, the cancelability of an
event does not depend on the firing conditions. In other words, whenever a spec
fires a given event, its `cancelable` property should always be the same.

That assumption seems true today. To ease debugging in case that changes in the
future, this update makes the extraction code report an error to the log when it
bumps into discrepancies for the `cancelable` property.

(Of course, ideally, we would have a better mechanism to report such extraction
problems than hiding them in the log...)
  • Loading branch information
dontcallmedom authored Apr 19, 2024
1 parent 2a37ea2 commit 650506d
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 15 deletions.
1 change: 1 addition & 0 deletions schemas/browserlib/extract-events.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"items": { "$ref": "../common.json#/$defs/interface" }
},
"bubbles": { "type": "boolean" },
"cancelable": { "type": "boolean" },
"isExtension": { "type": "boolean" },
"href": { "$ref": "../common.json#/$defs/url" },
"src": {
Expand Down
1 change: 1 addition & 0 deletions schemas/postprocessing/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"href": { "$ref": "../common.json#/$defs/url" }
}
},
"cancelable": { "type": "boolean" },
"extendedIn": {
"type": "array",
"items": {
Expand Down
59 changes: 53 additions & 6 deletions src/browserlib/extract-events.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ import extractWebIdl from './extract-webidl.mjs';
import {parse} from "../../node_modules/webidl2/index.js";
import getAbsoluteUrl from './get-absolute-url.mjs';

const isSameEvent = (e1, e2) => e1.type === e2.type &&
((e1.href && e1.href === e2.href ) ||
(e1.targets?.sort()?.join("|") === e2.targets?.sort()?.join("|")));

const singlePage = !document.querySelector('[data-reffy-page]');
const href = el => el?.getAttribute("id") ? getAbsoluteUrl(el, {singlePage}) : null;

Expand Down Expand Up @@ -37,6 +33,15 @@ export default function (spec) {
return acc;
}, {});

function isSameEvent(e1, e2) {
const res = e1.type === e2.type &&
((e1.href && e1.href === e2.href ) ||
(e1.targets?.sort()?.join("|") === e2.targets?.sort()?.join("|")));
if (res && e1.cancelable !== undefined && e2.cancelable !== undefined && e1.cancelable !== e2.cancelable) {
console.error(`[reffy] Found two occurrences of same event with different "cancelable" properties in ${spec.title}: type=${e1.type} targets=${e1.targets.join(', ')} href=${e1.href}`);
}
return res;
}

function fromEventElementToTargetInterfaces(eventEl) {
if (!eventEl) return;
Expand Down Expand Up @@ -74,6 +79,8 @@ export default function (spec) {
// Useful e.g. for pointerevents
const bubblingInfoColumn = [...table.querySelectorAll("thead th")]
.findIndex(n => n.textContent.trim().match(/^bubbl/i));
const cancelableInfoColumn = [...table.querySelectorAll("thead th")]
.findIndex(n => n.textContent.trim().match(/^cancel/i));
const interfaceColumn = [...table.querySelectorAll("thead th")]
.findIndex(n => n.textContent.trim().match(/^(dom )?interface/i));
const targetsColumn = [...table.querySelectorAll("thead th")]
Expand Down Expand Up @@ -115,6 +122,9 @@ export default function (spec) {
if (bubblingInfoColumn >= 0) {
event.bubbles = tr.querySelector(`td:nth-child(${bubblingInfoColumn + 1})`)?.textContent?.trim() === "Yes";
}
if (cancelableInfoColumn >= 0) {
event.cancelable = !!tr.querySelector(`td:nth-child(${cancelableInfoColumn + 1})`)?.textContent?.trim().match(/(yes)||(varies)/i);
}
if (interfaceColumn >= 0) {
event.interface =
tr.querySelector(`td:nth-child(${interfaceColumn + 1}) a`)?.textContent ??
Expand All @@ -134,14 +144,17 @@ export default function (spec) {
}
const eventTypeRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim().match(/^type/i));
const bubblingInfoRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim() === "Bubbles");
const cancelableInfoRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim() === "Cancelable");
const interfaceRow = [...table.querySelectorAll("tbody th")].findIndex(n => n.textContent.trim().match(/^interface/i));
const eventName = table.querySelector(`tr:nth-child(${eventTypeRow + 1}) td:nth-child(2)`)?.textContent?.trim();
const bubblesCell = table.querySelector(`tr:nth-child(${bubblingInfoRow + 1}) td:nth-child(2)`);
const bubbles = bubblesCell ? bubblesCell.textContent.trim() === "Yes" : null;
const cancelableCell = table.querySelector(`tr:nth-child(${cancelableInfoRow + 1}) td:nth-child(2)`);
const cancelable = cancelableCell ? cancelableCell.textContent.trim() === "Yes" : null;
const iface = table.querySelector(`tr:nth-child(${interfaceRow + 1}) td:nth-child(2)`)?.textContent?.trim();
if (eventName) {
events.push({
type: eventName, interface: iface, bubbles,
type: eventName, interface: iface, bubbles, cancelable,
src: { format: "css definition table", href: href(table.closest('*[id]')) },
href: href(table.closest('*[id]')) });
}
Expand Down Expand Up @@ -208,6 +221,7 @@ export default function (spec) {
phrasing = "fire functional event";
}
}

if (phrasing) {
const name = m.groups.eventName;
let newEvent = true;
Expand Down Expand Up @@ -263,6 +277,17 @@ export default function (spec) {
}
}
}
if (event.bubbles === undefined && event.cancelable === undefined) {
if (parsedText.match(/bubbles and cancelable attributes/)) {
if (parsedText.match(/true/)) {
event.bubbles = true;
event.cancelable = true;
} else if (parsedText.match(/false/)) {
event.bubbles = false;
event.cancelable = false;
}
}
}
if (event.bubbles === undefined) {
if (parsedText.match(/bubbles attribute/)) {
if (parsedText.match(/true/)) {
Expand All @@ -276,6 +301,19 @@ export default function (spec) {
event.bubbles = false;
}
}
if (event.cancelable === undefined) {
if (parsedText.match(/cancelable attribute/)) {
if (parsedText.match(/true/)) {
event.cancelable = true;
} else if (parsedText.match(/false/)) {
event.cancelable = false;
}
} else if (parsedText.match(/not cancelable/) || parsedText.match(/not be cancelable/)) {
event.cancelable = false;
} else if (parsedText.match(/cancelable/)) {
event.cancelable = true;
}
}
if (newEvent) {
events.push(event);
}
Expand Down Expand Up @@ -329,13 +367,18 @@ export default function (spec) {
};
// CSS Animations & Transitions uses dt/dd to describe events
// and uses a ul in the dd to describe bubbling behavior
let bubbles, iface;
let bubbles, iface, cancelable;
if (container.tagName === "DT") {
const bubbleItem = [...container.nextElementSibling.querySelectorAll("li")]
.find(li => li.textContent.startsWith("Bubbles:"));
if (bubbleItem) {
bubbles = !!bubbleItem.textContent.match(/yes/i);
}
const cancelableItem = [...container.nextElementSibling.querySelectorAll("li")]
.find(li => li.textContent.startsWith("Cancelable:"));
if (cancelableItem) {
cancelable = !!cancelableItem.textContent.match(/yes/i);
}
// CSS Animation & Transitions document the event in the heading
// of the section where the definitions are located
let currentEl = container.parentNode;
Expand All @@ -356,6 +399,7 @@ export default function (spec) {
event.interface = iface;
}
event.bubbles = bubbles;
event.cancelable = cancelable;
events.push(event);
if (!iface) {
console.error(`[reffy] No interface hint found for event definition ${event.type} in ${spec.title}`);
Expand All @@ -370,6 +414,9 @@ export default function (spec) {
if (bubbles !== undefined) {
ev.bubbles = bubbles;
}
if (cancelable !== undefined) {
ev.cancelable = cancelable;
}
}
});
return events
Expand Down
24 changes: 15 additions & 9 deletions tests/extract-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const defaultResults = (format, {successIface} = {successIface: "SuccessEvent"})
{
type: "success",
interface: successIface,
cancelable: true,
targets: [ "Example" ],
bubbles: true,
href: "about:blank#success",
Expand All @@ -19,6 +20,7 @@ const defaultResults = (format, {successIface} = {successIface: "SuccessEvent"})
{
type: "error",
interface: "ErrorEvent",
cancelable: false,
targets: [ "Example" ],
bubbles: false,
href: "about:blank#error",
Expand All @@ -39,11 +41,11 @@ const tests = [
title: "extracts events from a summary table with data spread across columns, completed by an IDL fragment",
html: `<table>
<thead>
<tr><th>Event type</th><th>Interface</th><th>Bubbles</th></tr>
<tr><th>Event type</th><th>Interface</th><th>Bubbles</th><th>Cancelable</th></tr>
</thead>
<tbody>
<tr><th><dfn id=success>success</dfn></th><td><a href=''>SuccessEvent</a></td><td>Yes</td></tr>
<tr><th><dfn id=error>error</dfn></th><td><a href=''>ErrorEvent</a></td><td>No</td></tr>
<tr><th><dfn id=success>success</dfn></th><td><a href=''>SuccessEvent</a></td><td>Yes</td><td>✓</td></tr>
<tr><th><dfn id=error>error</dfn></th><td><a href=''>ErrorEvent</a></td><td>No</td><td>No</td></tr>
</tbody></table>${defaultIdl}`,
res: defaultResults("summary table")
},
Expand All @@ -54,13 +56,15 @@ const tests = [
<tbody>
<tr><th>Type<td>success
<tr><th>Bubbles<td>Yes
<tr><th>Cancelable<td>Yes
<tr><th>Interface<td>SuccessEvent
</table>
<h3><code>error</code> Event</h3>
<table class="def" id='error'>
<tbody>
<tr><th>Type<td>error
<tr><th>Bubbles<td>no
<tr><th>Cancelable<td>no
<tr><th>Interface<td>ErrorEvent
</table>
${defaultIdl}`,
Expand All @@ -73,24 +77,26 @@ ${defaultIdl}`,
<dt><dfn data-dfn-for=Example data-dfn-type=event id=success>success</dfn></dt>
<dd><ul>
<li>Bubbles: Yes</li>
<li>Cancelable: Yes</li>
</ul></dd>
<dt><dfn data-dfn-for=Example data-dfn-type=event id=error>error</dfn></dt>
<dd><ul>
<li>Bubbles: No</li>
<li>Cancelable: No</li>
</ul></dd>
`,
res: defaultResults("dfn", {successIface: "ErrorEvent"})
},
{
title: "extracts events from an event mentioned in a 'Fire an event' context, completed by an IDL fragment",
html: `<p id=success><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a> named <code>success</code> using <a href=''>SuccessEvent</a> with the <code>bubbles</code> attribute initialized to <code>true</code></p>
<p id=error><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a> named <code>error</code> using <a href=''>ErrorEvent</a> with the <code>bubbles</code> attribute initialized to <code>false</code></p>${defaultIdl}`,
html: `<p id=success><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a> named <code>success</code> using <a href=''>SuccessEvent</a> with the <code>bubbles</code> and <code>cancelable</code> attributes initialized to <code>true</code></p>
<p id=error><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a> named <code>error</code> using <a href=''>ErrorEvent</a> with the <code>bubbles</code> attribute initialized to <code>false</code> and the <code>cancelable</code> attribute set to <code>false</code></p>${defaultIdl}`,
res: defaultResults("fire an event phrasing")
},
{
title: "extracts events from an event mentioned in a 'Fire Functional Event' context, completed by an IDL fragment",
html: `<p id=success><a href='https://w3c.github.io/ServiceWorker/#fire-functional-event'>Fire Functional Event</a> <code>success</code> with the <code>bubbles</code> attribute initialized to <code>true</code></p>
<p id=error><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a> named <code>error</code> using <a href=''>ErrorEvent</a> with the <code>bubbles</code> attribute initialized to <code>false</code></p>${defaultIdl}`,
html: `<p id=success><a href='https://w3c.github.io/ServiceWorker/#fire-functional-event'>Fire Functional Event</a> <code>success</code> with the <code>bubbles</code> attribute initialized to <code>true</code> and the <code>cancelable</code> attribute initialized to <code>true</code></p>
<p id=error><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a> named <code>error</code> using <a href=''>ErrorEvent</a> with the <code>bubbles</code> and <code>cancelable</code> attributes initialized to <code>false</code></p>${defaultIdl}`,
res: defaultResults("fire an event phrasing", {successIface: "ExtendableEvent"})
},
{
Expand Down Expand Up @@ -161,8 +167,8 @@ ${defaultIdl}`,
{
title: "does not get confused by asides",
html: `<p id=success><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a>
named <code>success</code><span><span class="mdn-anno">Info</span></span> using <a href=''>SuccessEvent</a> with the <code>bubbles</code> attribute initialized to <code>true</code>.</p>
<p id=error><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a> named <code>error</code> using <a href=''>ErrorEvent</a> with the <code>bubbles</code> attribute initialized to <code>false</code></p>
named <code>success</code><span><span class="mdn-anno">Info</span></span> using <a href=''>SuccessEvent</a> with the <code>bubbles</code> and <code>cancelable</code> attributes initialized to <code>true</code>.</p>
<p id=error><a href='https://dom.spec.whatwg.org/#concept-event-fire'>Fire an event</a> named <code>error</code> using <a href=''>ErrorEvent</a> with the <code>bubbles</code> attribute initialized to <code>false</code> and must not be cancelable</p>
${defaultIdl}`,
res: defaultResults("fire an event phrasing")
}
Expand Down

0 comments on commit 650506d

Please sign in to comment.