diff --git a/src/Clause.ts b/src/Clause.ts index b6ae5b2c..105a76ef 100644 --- a/src/Clause.ts +++ b/src/Clause.ts @@ -86,6 +86,10 @@ export default class Clause extends Builder { this.buildStructuredHeader(header); } this.header = header; + if (header == null) { + this.title = 'UNKNOWN'; + this.titleHTML = 'UNKNOWN'; + } } buildStructuredHeader(header: Element) { @@ -240,26 +244,6 @@ export default class Clause extends Builder { static exit({ node, spec, clauseStack, inAlg, currentId }: Context) { const clause = clauseStack[clauseStack.length - 1]; - const header = clause.header; - if (header == null) { - clause.title = 'UNKNOWN'; - clause.titleHTML = 'UNKNOWN'; - } else { - const headerClone = header.cloneNode(true) as Element; - for (const a of headerClone.querySelectorAll('a')) { - a.replaceWith(...a.childNodes); - } - clause.titleHTML = headerClone.innerHTML; - clause.title = headerClone.textContent; - if (clause.number) { - const numElem = clause.spec.doc.createElement('span'); - numElem.setAttribute('class', 'secnum'); - numElem.textContent = clause.number; - header.insertBefore(clause.spec.doc.createTextNode(' '), header.firstChild); - header.insertBefore(numElem, header.firstChild); - } - } - clause.buildExamples(); clause.buildNotes(); diff --git a/src/H1.ts b/src/H1.ts new file mode 100644 index 00000000..a47459d1 --- /dev/null +++ b/src/H1.ts @@ -0,0 +1,30 @@ +import Builder from './Builder'; +import type { Context } from './Context'; + +export default class H1 extends Builder { + static elements = ['H1']; + + static async enter() { + // do nothing + } + + static async exit({ spec, node, clauseStack }: Context) { + const parent = clauseStack[clauseStack.length - 1] || null; + if (parent === null || parent.header !== node) { + return; + } + const headerClone = node.cloneNode(true) as Element; + for (const a of headerClone.querySelectorAll('a')) { + a.replaceWith(...a.childNodes); + } + parent.titleHTML = headerClone.innerHTML; + parent.title = headerClone.textContent; + if (parent.number) { + const numElem = spec.doc.createElement('span'); + numElem.setAttribute('class', 'secnum'); + numElem.textContent = parent.number; + node.insertBefore(spec.doc.createTextNode(' '), node.firstChild); + node.insertBefore(numElem, node.firstChild); + } + } +} diff --git a/src/Meta.ts b/src/Meta.ts index f7dd996d..561cff0e 100644 --- a/src/Meta.ts +++ b/src/Meta.ts @@ -3,7 +3,8 @@ import type { Context } from './Context'; import Builder from './Builder'; -import { validateEffects } from './utils'; +import { validateEffects, doesEffectPropagateToParent } from './utils'; +import { maybeAddClauseToEffectWorklist } from './Spec'; export default class Meta extends Builder { static elements = ['EMU-META']; @@ -20,13 +21,13 @@ export default class Meta extends Builder { node ); for (const effect of effects) { - if (!parent.effects.includes(effect)) { - parent.effects.push(effect); - if (!spec._effectWorklist.has(effect)) { - spec._effectWorklist.set(effect, []); - } - spec._effectWorklist.get(effect)!.push(parent); + if (!doesEffectPropagateToParent(node, effect)) { + continue; } + if (!spec._effectWorklist.has(effect)) { + spec._effectWorklist.set(effect, []); + } + maybeAddClauseToEffectWorklist(effect, parent, spec._effectWorklist.get(effect)!); } } spec._emuMetasToRender.add(node); diff --git a/src/Spec.ts b/src/Spec.ts index ab774f9a..a1b5ef5a 100644 --- a/src/Spec.ts +++ b/src/Spec.ts @@ -29,6 +29,7 @@ import Xref from './Xref'; import Eqn from './Eqn'; import Biblio from './Biblio'; import Meta from './Meta'; +import H1 from './H1'; import { autolink, replacerForNamespace, @@ -78,6 +79,7 @@ const builders: BuilderInterface[] = [ ProdRef, Note, Meta, + H1, ]; const visitorMap = builders.reduce((map, T) => { @@ -258,7 +260,11 @@ function isEmuImportElement(node: Node): node is EmuImportElement { return node.nodeType === 1 && node.nodeName === 'EMU-IMPORT'; } -function maybeAddClauseToEffectWorklist(effectName: string, clause: Clause, worklist: Clause[]) { +export function maybeAddClauseToEffectWorklist( + effectName: string, + clause: Clause, + worklist: Clause[] +) { if ( !worklist.includes(clause) && clause.canHaveEffect(effectName) && diff --git a/src/Xref.ts b/src/Xref.ts index 16fb973c..ca1475be 100644 --- a/src/Xref.ts +++ b/src/Xref.ts @@ -4,7 +4,7 @@ import type * as Biblio from './Biblio'; import type Clause from './Clause'; import Builder from './Builder'; -import { validateEffects } from './utils'; +import { validateEffects, doesEffectPropagateToParent } from './utils'; /*@internal*/ export default class Xref extends Builder { @@ -91,36 +91,10 @@ export default class Xref extends Builder { shouldPropagateEffect(effectName: string) { if (!this.isInvocation) return false; if (this.clause) { - // Xrefs should not propagate past explicit fences in parent steps. Fences - // must be at the beginning of steps. - // - // Abstract Closures are considered automatic fences for the user-code - // effect, since those are effectively nested functions. - // - // Calls to Abstract Closures that can call user code must be explicitly - // marked as such with .... - for (let node = this.node; node.parentElement; node = node.parentElement) { - const parent = node.parentElement; - // This is super hacky. It's checking the output of ecmarkdown. - if (parent.tagName !== 'LI') continue; - - if ( - effectName === 'user-code' && - parent.textContent?.includes('be a new Abstract Closure') - ) { - return false; - } - - if ( - parent - .getAttribute('fence-effects') - ?.split(',') - .map(s => s.trim()) - .includes(effectName) - ) { - return false; - } + if (!doesEffectPropagateToParent(this.node, effectName)) { + return false; } + if (!this.clause.canHaveEffect(effectName)) { return false; } diff --git a/src/utils.ts b/src/utils.ts index 81fca391..7c1cd564 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -244,3 +244,33 @@ export function validateEffects(spec: Spec, effectsRaw: string[], node: Element) return effects; } + +export function doesEffectPropagateToParent(node: Element, effect: string) { + // Effects should not propagate past explicit fences in parent steps. + // + // Abstract Closures are considered automatic fences for the user-code + // effect, since those are effectively nested functions. + // + // Calls to Abstract Closures that can call user code must be explicitly + // marked as such with .... + for (; node.parentElement; node = node.parentElement) { + const parent = node.parentElement; + // This is super hacky. It's checking the output of ecmarkdown. + if (parent.tagName !== 'LI') continue; + + if (effect === 'user-code' && parent.textContent?.includes('be a new Abstract Closure')) { + return false; + } + + if ( + parent + .getAttribute('fence-effects') + ?.split(',') + .map(s => s.trim()) + .includes(effect) + ) { + return false; + } + } + return true; +} diff --git a/test/baselines/generated-reference/effect-user-code.html b/test/baselines/generated-reference/effect-user-code.html index 16204416..e2f7f1d8 100644 --- a/test/baselines/generated-reference/effect-user-code.html +++ b/test/baselines/generated-reference/effect-user-code.html @@ -125,7 +125,7 @@

20 ResultOfEvaluating()

21 FencedEffects()

The abstract operation FencedEffects takes no arguments. Effects don't propagate past fences in parent steps. A fence must be at the beginning of a step. It performs the following steps when called:

-
  1. Fence.
    1. UserCode().
+
  1. Fence.
    1. UserCode().
    2. Let foo be the result of evaluating someUserCode.
diff --git a/test/baselines/sources/effect-user-code.html b/test/baselines/sources/effect-user-code.html index ffcd5322..034b6c57 100644 --- a/test/baselines/sources/effect-user-code.html +++ b/test/baselines/sources/effect-user-code.html @@ -231,6 +231,7 @@

FencedEffects()

1. [fence-effects="user-code"] Fence. 1. UserCode(). + 1. Let _foo_ be the result of evaluating _someUserCode_.