From 653cb129bafd0f4190ef7acf8b1f923878b304e1 Mon Sep 17 00:00:00 2001 From: Shu-yu Guo Date: Thu, 22 Jul 2021 16:49:29 -0700 Subject: [PATCH 01/20] Initial support for effects --- css/elements.css | 16 ++++++++++++++ js/menu.js | 40 ++++++++++++++++++++++------------ src/Clause.ts | 29 ++++++++++++++++++++++++- src/Spec.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++ src/Xref.ts | 43 +++++++++++++++++++++++++++++++------ src/autolinker.ts | 13 +++++++++-- src/header-parser.ts | 12 +++++++++-- 7 files changed, 179 insertions(+), 25 deletions(-) diff --git a/css/elements.css b/css/elements.css index 183c1af7..2ee7af10 100644 --- a/css/elements.css +++ b/css/elements.css @@ -35,6 +35,22 @@ a:hover { color: #239dee; } +a.e-uc { + position: relative; +} + +a.e-uc::before { + display: none; + color: brown; + position: absolute; + bottom: -0.9em; + left: 0; + content: 'UC'; + font-style: italic; + font-weight: bold; + font-size: small; +} + code { font-weight: bold; font-family: Consolas, Monaco, monospace; diff --git a/js/menu.js b/js/menu.js index 260b2fbb..8283ec71 100644 --- a/js/menu.js +++ b/js/menu.js @@ -964,6 +964,8 @@ function makeLinkToId(id) { return (targetSec === 'index' ? './' : targetSec + '.html') + hash; } +let stylesheetWorkaroundForCanCallUserCodeAnnotation; + function doShortcut(e) { if (!(e.target instanceof HTMLElement)) { return; @@ -973,22 +975,30 @@ function doShortcut(e) { if (name === 'textarea' || name === 'input' || name === 'select' || target.isContentEditable) { return; } - if (e.key === 'm' && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && usesMultipage) { - let pathParts = location.pathname.split('/'); - let hash = location.hash; - if (pathParts[pathParts.length - 2] === 'multipage') { - if (hash === '') { - let sectionName = pathParts[pathParts.length - 1]; - if (sectionName.endsWith('.html')) { - sectionName = sectionName.slice(0, -5); - } - if (idToSection['sec-' + sectionName] !== undefined) { - hash = '#sec-' + sectionName; + if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) { + if (e.key === 'm' && usesMultipage) { + let pathParts = location.pathname.split('/'); + let hash = location.hash; + if (pathParts[pathParts.length - 2] === 'multipage') { + if (hash === '') { + let sectionName = pathParts[pathParts.length - 1]; + if (sectionName.endsWith('.html')) { + sectionName = sectionName.slice(0, -5); + } + if (idToSection['sec-' + sectionName] !== undefined) { + hash = '#sec-' + sectionName; + } } + location = pathParts.slice(0, -2).join('/') + '/' + hash; + } else { + location = 'multipage/' + hash; + } + } else if (e.key === 'u') { + if (stylesheetWorkaroundForCanCallUserCodeAnnotation.innerText === '') { + stylesheetWorkaroundForCanCallUserCodeAnnotation.textContent = 'a.e-uc::before { display: block !important; }'; + } else { + stylesheetWorkaroundForCanCallUserCodeAnnotation.textContent = ''; } - location = pathParts.slice(0, -2).join('/') + '/' + hash; - } else { - location = 'multipage/' + hash; } } } @@ -1017,4 +1027,6 @@ document.addEventListener('keypress', doShortcut); document.addEventListener('DOMContentLoaded', () => { Toolbox.init(); referencePane.init(); + stylesheetWorkaroundForCanCallUserCodeAnnotation = document.createElement('style'); + document.head.appendChild(stylesheetWorkaroundForCanCallUserCodeAnnotation); }); diff --git a/src/Clause.ts b/src/Clause.ts index bfe04b5b..925aeb3b 100644 --- a/src/Clause.ts +++ b/src/Clause.ts @@ -20,9 +20,11 @@ export default class Clause extends Builder { subclauses: Clause[]; number: string; aoid: string | null; + type: string | null; notes: Note[]; editorNotes: Note[]; examples: Example[]; + effects: string[]; constructor(spec: Spec, node: HTMLElement, parent: Clause, number: string) { super(spec, node); @@ -33,6 +35,7 @@ export default class Clause extends Builder { this.notes = []; this.editorNotes = []; this.examples = []; + this.effects = []; // namespace is either the entire spec or the parent clause's namespace. let parentNamespace = spec.namespace; @@ -53,6 +56,12 @@ export default class Clause extends Builder { this.aoid = node.id; } + this.type = node.getAttribute('type'); + if (this.type === '') { + this.type = null; + } + node.removeAttribute('type'); // TODO maybe leave it in; this is just to minimize the diff + let header = this.node.firstElementChild; while (header != null && header.tagName === 'SPAN' && header.children.length === 0) { // skip oldids @@ -117,7 +126,7 @@ export default class Clause extends Builder { header.innerHTML = formattedHeader; } - const { description, for: _for } = parseStructuredHeaderDl(this.spec, type, dl); + const { description, for: _for, effects } = parseStructuredHeaderDl(this.spec, type, dl); const paras = formatPreamble( this.spec, @@ -154,6 +163,14 @@ export default class Clause extends Builder { this.node.setAttribute('aoid', name); this.aoid = name; } + + this.effects = effects; + for (let effect of effects) { + if (!this.spec._effectWorklist.has(effect)) { + this.spec._effectWorklist.set(effect, []); + } + this.spec._effectWorklist.get(effect)!.push(this); + } } buildNotes() { @@ -180,6 +197,16 @@ export default class Clause extends Builder { } } + isEffectApplicable(effectName: string) { + // The following effects are runtime only: + // + // uc: Only runtime can call user code. + if (this.type === 'static sdo') { + if (effectName == 'uc') return false; + } + return true; + } + static async enter({ spec, node, clauseStack, clauseNumberer }: Context) { if (!node.id) { spec.warn({ diff --git a/src/Spec.ts b/src/Spec.ts index e1e3130f..b04ed3e2 100644 --- a/src/Spec.ts +++ b/src/Spec.ts @@ -290,6 +290,8 @@ export default class Spec { }[]; _prodRefs: ProdRef[]; _textNodes: { [s: string]: [TextNodeContext] }; + _effectWorklist: Map; + _effectfulAOs: Map; refsByClause: { [refId: string]: [string] }; private _fetch: (file: string, token: CancellationToken) => PromiseLike; @@ -332,6 +334,8 @@ export default class Spec { this._ntStringRefs = []; this._prodRefs = []; this._textNodes = {}; + this._effectWorklist = new Map(); + this._effectfulAOs = new Map(); this.refsByClause = Object.create(null); this.processMetadata(); @@ -455,6 +459,8 @@ export default class Spec { this.autolink(); + this.log('Propagating can-call-user-code annotations...'); + this.propagateEffects(); this.log('Linking xrefs...'); this._xrefs.forEach(xref => xref.build()); this.log('Linking non-terminal references...'); @@ -652,6 +658,51 @@ export default class Spec { return true; } + private propagateEffects() { + for (let [effectName, worklist] of this._effectWorklist) { + this.propagateEffect(effectName, worklist); + } + } + + private propagateEffect(effectName: string, worklist: Clause[]) { + let usersOfAoid : Map> = new Map(); + for (let xref of this._xrefs) { + if (xref.clause == null) { + continue; + } + let usedAoid = xref.aoid; + if (!usersOfAoid.has(usedAoid)) { + usersOfAoid.set(usedAoid, new Set()); + } + usersOfAoid.get(usedAoid)!.add(xref.clause); + } + + while (worklist.length != 0) { + let clause = worklist.shift() as Clause; + let aoid = clause.aoid; + if (aoid == null || !usersOfAoid.has(aoid)) { + continue; + } + + this._effectfulAOs.set(aoid, clause); + for (let userClause of usersOfAoid.get(aoid)!) { + if (userClause.isEffectApplicable(effectName) && + userClause.effects.indexOf(effectName) === -1) { + userClause.effects.push(effectName); + worklist.push(userClause); + } + } + // TODO(syg): Support "of" form of SDO invocation + } + } + + public getEffectsByAoid(aoid: string): string[] | null { + if (this._effectfulAOs.has(aoid)) { + return this._effectfulAOs.get(aoid)!.effects; + } + return null; + } + private async buildMultipage(wrapper: Element, tocEles: Element[], jsSha: string) { let stillIntro = true; const introEles = []; diff --git a/src/Xref.ts b/src/Xref.ts index 84d679f9..41e556f3 100644 --- a/src/Xref.ts +++ b/src/Xref.ts @@ -4,12 +4,14 @@ import type * as Biblio from './Biblio'; import type Clause from './Clause'; import Builder from './Builder'; +import * as utils from './utils'; /*@internal*/ export default class Xref extends Builder { namespace: string; href: string; aoid: string; + isInvocation: boolean; clause: Clause | null; id: string; entry: Biblio.BiblioEntry | undefined; @@ -30,6 +32,7 @@ export default class Xref extends Builder { this.aoid = aoid; this.clause = clause; this.id = node.getAttribute('id')!; + this.isInvocation = node.hasAttribute('is-invocation'); } static async enter({ node, spec, clauseStack }: Context) { @@ -141,7 +144,34 @@ export default class Xref extends Builder { this.entry = spec.biblio.byAoid(aoid, namespace); if (this.entry) { - buildAOLink(node, this.entry); + let effects = null; + + // Check if there's metadata overriding the effects, otherwise look up + // the effects. + if (node.parentElement && + node.parentElement.tagName === 'EMU-META' && + node.parentElement.hasAttribute('effects')) { + effects = node.parentElement.getAttribute('effects')!.split(','); + if (effects.length === 0) { + effects = null; + } + } else if (this.isInvocation) { + effects = spec.getEffectsByAoid(aoid); + } + + let classNames = null; + if (effects) { + classNames = ''; + let parentClause = this.clause; + let isFirst = true; + for (let effect of effects) { + if (!parentClause || parentClause.isEffectApplicable(effect)) { + classNames += (isFirst ? '' : ' ') + 'e-' + effect; + isFirst = false; + } + } + } + buildAOLink(node, this.entry, classNames); return; } @@ -179,11 +209,11 @@ function buildProductionLink(xref: Element, entry: Biblio.ProductionBiblioEntry) } } -function buildAOLink(xref: Element, entry: Biblio.BiblioEntry) { +function buildAOLink(xref: Element, entry: Biblio.BiblioEntry, classNames: string | null) { if (xref.textContent!.trim() === '') { - xref.innerHTML = buildXrefLink(entry, xref.getAttribute('aoid')); + xref.innerHTML = buildXrefLink(entry, xref.getAttribute('aoid'), classNames); } else { - xref.innerHTML = buildXrefLink(entry, xref.innerHTML); + xref.innerHTML = buildXrefLink(entry, xref.innerHTML, classNames); } } @@ -276,6 +306,7 @@ function buildStepLink(spec: Spec, xref: Element, entry: Biblio.StepBiblioEntry) xref.innerHTML = buildXrefLink(entry, text); } -function buildXrefLink(entry: Biblio.BiblioEntry, contents: string | number | undefined | null) { - return '' + contents + ''; +function buildXrefLink(entry: Biblio.BiblioEntry, contents: string | number | undefined | null, classNames: string | null = null) { + let classSnippet = classNames == null ? '' : (' class="' + classNames + '"'); + return '' + contents + ''; } diff --git a/src/autolinker.ts b/src/autolinker.ts index ba8bf5ee..f43a9e63 100644 --- a/src/autolinker.ts +++ b/src/autolinker.ts @@ -42,7 +42,7 @@ export function autolink( const spec = clause.spec; const template = spec.doc.createElement('template'); const content = escape(node.textContent!); - const autolinked = content.replace(replacer, match => { + const autolinked = content.replace(replacer, (match, offset) => { const entry = autolinkmap[narrowSpace(match)]; if (!entry) { return match; @@ -56,7 +56,16 @@ export function autolink( } if (entry.aoid) { - return '' + match + ''; + let isInvocationAttribute = ''; + // Matches function-style invocation with parentheses and SDO-style 'of' + // invocation. + if (content[offset + match.length] === '(' || + (content[offset + match.length] === ' ' && + content[offset + match.length + 1] === 'o' && + content[offset + match.length + 2] === 'f')) { + isInvocationAttribute = ' is-invocation'; + } + return '' + match + ''; } else { return '' + match + ''; } diff --git a/src/header-parser.ts b/src/header-parser.ts index c7286db5..401730de 100644 --- a/src/header-parser.ts +++ b/src/header-parser.ts @@ -199,9 +199,10 @@ export function parseStructuredHeaderDl( spec: Spec, type: string | null, dl: Element -): { description: Element | null; for: Element | null } { +): { description: Element | null; for: Element | null; effects: string[] } { let description = null; let _for = null; + let effects = null; for (let i = 0; i < dl.children.length; ++i) { const dt = dl.children[i]; if (dt.tagName !== 'DT') { @@ -260,6 +261,13 @@ export function parseStructuredHeaderDl( } break; } + case 'effects': { + // The dd contains a comma-separated list of effects. + if (dd.textContent !== null) { + effects = dd.textContent.split(','); + } + break; + } case '': { spec.warn({ type: 'node', @@ -280,7 +288,7 @@ export function parseStructuredHeaderDl( } } } - return { description, for: _for }; + return { description, for: _for, effects: effects == null ? [] : effects }; } export function formatPreamble( From 2df0c245d6b3dede670385d5d3f2eb82edf486c6 Mon Sep 17 00:00:00 2001 From: Shu-yu Guo Date: Wed, 15 Sep 2021 17:35:49 -0700 Subject: [PATCH 02/20] Remove stale TODO --- src/Spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Spec.ts b/src/Spec.ts index b04ed3e2..df5c8cfc 100644 --- a/src/Spec.ts +++ b/src/Spec.ts @@ -692,7 +692,6 @@ export default class Spec { worklist.push(userClause); } } - // TODO(syg): Support "of" form of SDO invocation } } From 4574ef7184973321b912c0146e6673c2e861547a Mon Sep 17 00:00:00 2001 From: Shu-yu Guo Date: Wed, 15 Sep 2021 17:37:45 -0700 Subject: [PATCH 03/20] JavaScript --- src/Spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spec.ts b/src/Spec.ts index df5c8cfc..853f1057 100644 --- a/src/Spec.ts +++ b/src/Spec.ts @@ -677,7 +677,7 @@ export default class Spec { usersOfAoid.get(usedAoid)!.add(xref.clause); } - while (worklist.length != 0) { + while (worklist.length !== 0) { let clause = worklist.shift() as Clause; let aoid = clause.aoid; if (aoid == null || !usersOfAoid.has(aoid)) { From baf9b471723547cf57d96782f8f2b354d78bef4b Mon Sep 17 00:00:00 2001 From: Shu-yu Guo Date: Wed, 15 Sep 2021 17:41:01 -0700 Subject: [PATCH 04/20] Remove unnecessary import --- src/Xref.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Xref.ts b/src/Xref.ts index 41e556f3..2928bd10 100644 --- a/src/Xref.ts +++ b/src/Xref.ts @@ -4,7 +4,6 @@ import type * as Biblio from './Biblio'; import type Clause from './Clause'; import Builder from './Builder'; -import * as utils from './utils'; /*@internal*/ export default class Xref extends Builder { From f6cc1eeeacd0ab024777914d00802d727b3b5d44 Mon Sep 17 00:00:00 2001 From: Shu-yu Guo Date: Wed, 15 Sep 2021 18:05:29 -0700 Subject: [PATCH 05/20] Fix bad rebase --- src/Clause.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Clause.ts b/src/Clause.ts index 925aeb3b..9bfd2242 100644 --- a/src/Clause.ts +++ b/src/Clause.ts @@ -60,7 +60,6 @@ export default class Clause extends Builder { if (this.type === '') { this.type = null; } - node.removeAttribute('type'); // TODO maybe leave it in; this is just to minimize the diff let header = this.node.firstElementChild; while (header != null && header.tagName === 'SPAN' && header.children.length === 0) { @@ -96,7 +95,7 @@ export default class Clause extends Builder { } // if we find such a DL, treat this as a structured header - const type = this.node.getAttribute('type'); + const type = this.type; const { name, formattedHeader, formattedParams } = parseStructuredHeaderH1(this.spec, header); if (type === 'numeric method' && name != null && !name.includes('::')) { From 2e894a5f64fca22a6db504383b7ee7e807a19020 Mon Sep 17 00:00:00 2001 From: Shu-yu Guo Date: Wed, 15 Sep 2021 18:05:36 -0700 Subject: [PATCH 06/20] Rebaseline --- test/baselines/generated-reference/algorithms.html | 2 +- test/baselines/generated-reference/autolinking.html | 2 +- test/baselines/generated-reference/emd-in-grammar.html | 2 +- test/baselines/generated-reference/eqn.html | 10 +++++----- test/baselines/generated-reference/imports.html | 2 +- .../multipage.html/multipage/index.html | 2 +- .../multipage.html/multipage/second.html | 2 +- .../multipage.html/multipage/third.html | 2 +- test/baselines/generated-reference/test.html | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/baselines/generated-reference/algorithms.html b/test/baselines/generated-reference/algorithms.html index 4fbc06ef..b59ad4af 100644 --- a/test/baselines/generated-reference/algorithms.html +++ b/test/baselines/generated-reference/algorithms.html @@ -1,7 +1,7 @@
-
  1. Can call abstract operations in this spec: Internal();
  2. Can call abstract operations in ES6: ReturnIfAbrupt(completion);
  3. Can call abstract operations in a biblio file: Biblio();
  4. Unfound abstract operations just don't link: Unfound();
  5. Can prefix with ! and ?.
    1. Let foo be ? Internal();
    2. Let bar be ! Internal();
+
  1. Can call abstract operations in this spec: Internal();
  2. Can call abstract operations in ES6: ReturnIfAbrupt(completion);
  3. Can call abstract operations in a biblio file: Biblio();
  4. Unfound abstract operations just don't link: Unfound();
  5. Can prefix with ! and ?.
    1. Let foo be ? Internal();
    2. Let bar be ! Internal();

1 Internal Function

diff --git a/test/baselines/generated-reference/autolinking.html b/test/baselines/generated-reference/autolinking.html index da8378ec..20ca8650 100644 --- a/test/baselines/generated-reference/autolinking.html +++ b/test/baselines/generated-reference/autolinking.html @@ -2,7 +2,7 @@

1 Autolinking

-

Type, type, Type(), type()

+

Type, type, Type(), type()

%Array% and %ArrayPrototype% from ES6 should link (but not %Arrayprototype%).

Lowercase

diff --git a/test/baselines/generated-reference/emd-in-grammar.html b/test/baselines/generated-reference/emd-in-grammar.html index 731ade32..35b0a591 100644 --- a/test/baselines/generated-reference/emd-in-grammar.html +++ b/test/baselines/generated-reference/emd-in-grammar.html @@ -6,7 +6,7 @@

1 Example

Foo :: Bar - but only if the AbstractOperation of Bar is ≤ variable + but only if the AbstractOperation of Bar is ≤ variable diff --git a/test/baselines/generated-reference/eqn.html b/test/baselines/generated-reference/eqn.html index 2a560881..7d0f9c36 100644 --- a/test/baselines/generated-reference/eqn.html +++ b/test/baselines/generated-reference/eqn.html @@ -4,16 +4,16 @@

1 Header

Can refer to eqns inside paragraph text via autolinking: DateValue.

-
  1. Return Value(val);
+
  1. Return Value(val);
-
Value2(t)
= DateValue(t) if Type(t) is string
= t
+
Value2(t)
= DateValue(t) if Type(t) is string
= t
DateValue(t)
= 0 if t = 0
= 1 if t = 1
= 2
-
Value(t)
= DateValue(t) if Type(t) is string
= t
+
Value(t)
= DateValue(t) if Type(t) is string
= t
-
  1. Return Value(val);
+
  1. Return Value(val);
-

Inline eqns are a thing, for example DateValue(n) or 1 + 1 = 2

+

Inline eqns are a thing, for example DateValue(n) or 1 + 1 = 2

\ No newline at end of file diff --git a/test/baselines/generated-reference/imports.html b/test/baselines/generated-reference/imports.html index 5e513a01..90e415b0 100644 --- a/test/baselines/generated-reference/imports.html +++ b/test/baselines/generated-reference/imports.html @@ -18,5 +18,5 @@

1.1 Import 3

2 Import 2

-
  1. Ensure we can auto-link to imported aoids: Baz()
+
  1. Ensure we can auto-link to imported aoids: Baz()
\ No newline at end of file diff --git a/test/baselines/generated-reference/multipage.html/multipage/index.html b/test/baselines/generated-reference/multipage.html/multipage/index.html index d19f787a..42a3b526 100644 --- a/test/baselines/generated-reference/multipage.html/multipage/index.html +++ b/test/baselines/generated-reference/multipage.html/multipage/index.html @@ -1,7 +1,7 @@ - +