Skip to content

Commit

Permalink
do not leave emu-import / emu-biblio in the generated DOM (#624)
Browse files Browse the repository at this point in the history
  • Loading branch information
bakkot authored Oct 30, 2024
1 parent d7dc06f commit 6c0fb1f
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 98 deletions.
2 changes: 1 addition & 1 deletion src/Context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type Spec from './Spec';
import type Import from './Import';
import type { Import } from './Import';
import type Clause from './Clause';
import type { ClauseNumberIterator } from './clauseNums';

Expand Down
96 changes: 38 additions & 58 deletions src/Import.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,64 @@
import type Spec from './Spec';

import Builder from './Builder';
import * as utils from './utils';
import * as path from 'path';
import type { JSDOM } from 'jsdom';

export default class Import extends Builder {
/** @internal */ public importLocation: string;
/** @internal */ public relativeRoot: string;
/** @internal */ public source: string;
export type Import = {
importLocation: string;
relativeRoot: string;
source: string;
};

constructor(
spec: Spec,
node: HTMLElement,
importLocation: string,
relativeRoot: string,
source: string,
) {
super(spec, node);
this.importLocation = importLocation;
this.relativeRoot = relativeRoot;
this.source = source;
}
export async function buildImports(spec: Spec, importNode: EmuImportElement, root: string) {
const href = importNode.getAttribute('href');
if (!href) throw new Error('Import missing href attribute.');
const importPath = path.join(root, href);
const relativeRoot = path.dirname(importPath);

static async build(spec: Spec, node: EmuImportElement, root: string) {
const href = node.getAttribute('href');
if (!href) throw new Error('Import missing href attribute.');
const importPath = path.join(root, href);
const relativeRoot = path.dirname(importPath);
const html = await spec.fetch(importPath);

const html = await spec.fetch(importPath);
spec.imports.push({ importLocation: importPath, relativeRoot, source: html });

const imp = new Import(spec, node, importPath, relativeRoot, html);
spec.imports.push(imp);
const importDom = utils.htmlToDom(html);
importNode.dom = importDom;
importNode.source = html;
importNode.importPath = importPath;

const importDom = utils.htmlToDom(html);
node.dom = importDom;
node.source = html;
node.importPath = importPath;
const importDoc = importDom.window.document;

const importDoc = importDom.window.document;
const nodes = importDoc.body.childNodes;
const frag = spec.doc.createDocumentFragment();
// clone this list so we can walk it after the replaceWith call
const importedNodes = [...importDoc.body.childNodes];
const frag = spec.doc.createDocumentFragment();

for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const importedNode = spec.doc.adoptNode(node);
frag.appendChild(importedNode);
}
for (let i = 0; i < importedNodes.length; i++) {
const importedNode = spec.doc.adoptNode(importedNodes[i]);
importedNodes[i] = importedNode;
frag.appendChild(importedNode);

const children = frag.childElementCount;
node.appendChild(frag);
spec.topLevelImportedNodes.set(importedNode, importNode);
}

// This is a bit gross.
// We want to do this check after adopting the elements into the main DOM, so the location-finding infrastructure works
// But `appendChild(documentFragment)` both empties out the original fragment and returns it.
// So we have to remember how many child elements we are adding and walk over each of them manually.
for (let i = node.children.length - children; i < node.children.length; ++i) {
const child = node.children[i];
const biblios = [
...child.querySelectorAll('emu-biblio'),
...(child.tagName === 'EMU-BIBLIO' ? [child] : []),
importNode.replaceWith(frag);

for (let i = 0; i < importedNodes.length; i++) {
const importedNode = importedNodes[i];
if (importedNode.nodeType === 1 /* Node.ELEMENT_NODE */) {
const importedImports = [
...(importedNode as HTMLElement).querySelectorAll('emu-import'),
// we have to do this because querySelectorAll can't return its `this`
...((importedNode as HTMLElement).tagName === 'EMU-IMPORT' ? [importedNode] : []),
];
for (const biblio of biblios) {
spec.warn({
type: 'node',
node: biblio,
ruleId: 'biblio-in-import',
message: 'emu-biblio elements cannot be used within emu-imports',
});
for (const childImport of importedImports) {
await buildImports(spec, childImport as EmuImportElement, relativeRoot);
}
}

return imp;
}
}

export interface EmuImportElement extends HTMLElement {
href: string;
dom?: JSDOM;
dom: JSDOM;
source?: string;
importPath?: string;
}
63 changes: 37 additions & 26 deletions src/Spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import * as yaml from 'js-yaml';
import * as utils from './utils';
import * as hljs from 'highlight.js';
// Builders
import type { EmuImportElement } from './Import';
import Import from './Import';
import { buildImports } from './Import';
import type { Import, EmuImportElement } from './Import';
import Clause from './Clause';
import ClauseNumbers from './clauseNums';
import Algorithm from './Algorithm';
Expand Down Expand Up @@ -273,10 +273,6 @@ function wrapWarn(source: string, spec: Spec, warn: (err: EcmarkupError) => void
};
}

function isEmuImportElement(node: Node): node is EmuImportElement {
return node.nodeType === 1 && node.nodeName === 'EMU-IMPORT';
}

export type WorklistItem = { aoid: string | null; effects: string[] };
export function maybeAddClauseToEffectWorklist(
effectName: string,
Expand Down Expand Up @@ -339,6 +335,7 @@ export default class Spec {
/** @internal */ _emuMetasToRender: Set<HTMLElement>;
/** @internal */ _emuMetasToRemove: Set<HTMLElement>;
/** @internal */ refsByClause: { [refId: string]: [string] };
/** @internal */ topLevelImportedNodes: Map<Node, EmuImportElement>;

private _fetch: (file: string, token: CancellationToken) => PromiseLike<string>;

Expand Down Expand Up @@ -384,6 +381,7 @@ export default class Spec {
this._emuMetasToRender = new Set();
this._emuMetasToRemove = new Set();
this.refsByClause = Object.create(null);
this.topLevelImportedNodes = new Map();

this.processMetadata();
Object.assign(this.opts, opts);
Expand Down Expand Up @@ -653,15 +651,22 @@ export default class Spec {
node: Element | Node,
): ({ file?: string; source: string } & ElementLocation) | undefined {
let pointer: Element | Node | null = node;
while (pointer != null) {
if (isEmuImportElement(pointer)) {
break;
let dom: JSDOM;
let file: string | undefined;
let source: string | undefined;
search: {
while (pointer != null) {
if (this.topLevelImportedNodes.has(pointer)) {
const importNode = this.topLevelImportedNodes.get(pointer)!;
dom = importNode.dom;
file = importNode.importPath;
source = importNode.source;
break search;
}
pointer = pointer.parentElement;
}
pointer = pointer.parentElement;
}
const dom = pointer == null ? this.dom : pointer.dom;
if (!dom) {
return;
// else
dom = this.dom;
}
const loc = dom.nodeLocation(node);
if (loc) {
Expand All @@ -679,8 +684,8 @@ export default class Spec {
endCol: loc.endCol,
};
if (pointer != null) {
out.file = pointer.importPath!;
out.source = pointer.source!;
out.file = file!;
out.source = source!;
}
return out;
}
Expand Down Expand Up @@ -1266,6 +1271,7 @@ ${this.opts.multipage ? `<li><span>Navigate to/from multipage</span><code>m</cod
const biblioPaths = [];
for (const biblioEle of this.doc.querySelectorAll('emu-biblio')) {
const href = biblioEle.getAttribute('href');
biblioEle.remove();
if (href == null) {
this.spec.warn({
type: 'node',
Expand Down Expand Up @@ -1305,7 +1311,21 @@ ${this.opts.multipage ? `<li><span>Navigate to/from multipage</span><code>m</cod
}

private async loadImports() {
await loadImports(this, this.spec.doc.body, this.rootDir);
const imports = this.doc.body.querySelectorAll('EMU-IMPORT');
for (let i = 0; i < imports.length; i++) {
await buildImports(this, imports[i] as EmuImportElement, this.rootDir);
}

// we've already removed biblio elements in the main document in loadBiblios
// so any which are here now were in an import, which is illegal
for (const biblioEle of this.doc.querySelectorAll('emu-biblio')) {
this.warn({
type: 'node',
node: biblioEle,
ruleId: 'biblio-in-import',
message: 'emu-biblio elements cannot be used within emu-imports',
});
}
}

public exportBiblio(): ExportedBiblio | null {
Expand Down Expand Up @@ -1895,15 +1915,6 @@ function getBoilerplate(file: string) {
return fs.readFileSync(boilerplateFile, 'utf8');
}

async function loadImports(spec: Spec, rootElement: HTMLElement, rootPath: string) {
const imports = rootElement.querySelectorAll('EMU-IMPORT');
for (let i = 0; i < imports.length; i++) {
const node = imports[i];
const imp = await Import.build(spec, node as EmuImportElement, rootPath);
await loadImports(spec, node as HTMLElement, imp.relativeRoot);
}
}

async function walk(walker: TreeWalker, context: Context) {
const previousInNoAutolink = context.inNoAutolink;
let previousInNoEmd = context.inNoEmd;
Expand Down
2 changes: 1 addition & 1 deletion src/lint/collect-spelling-diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Warning } from '../Spec';
import type { default as Import } from '../Import';
import type { Import } from '../Import';

import { offsetToLineAndColumn } from '../utils';

Expand Down
2 changes: 1 addition & 1 deletion test/baselines/generated-reference/algorithms.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<li><span>Toggle pinning of the current clause</span><code>p</code></li>
<li><span>Jump to <i>n</i>th pin</span><code>1-9</code></li>
</ul></div><div id="spec-container">
<emu-biblio href="./algorithmsBiblio.json"></emu-biblio>

<emu-alg><ol><li>Can call <emu-xref href="#sec-algorithm-conventions-abstract-operations"><a href="https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations">abstract operations</a></emu-xref> in this spec: <emu-xref aoid="Internal" id="_ref_0"><a href="#sec-internal">Internal</a></emu-xref>();</li><li>Can call <emu-xref href="#sec-algorithm-conventions-abstract-operations"><a href="https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations">abstract operations</a></emu-xref> in ES6: <emu-xref aoid="ReturnIfAbrupt"><a href="https://tc39.es/ecma262/#sec-returnifabrupt">ReturnIfAbrupt</a></emu-xref>(<var>completion</var>);</li><li>Can call <emu-xref href="#sec-algorithm-conventions-abstract-operations"><a href="https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations">abstract operations</a></emu-xref> in a biblio file: <emu-xref aoid="Biblio"><a href="http://example.com/fooSite.html#sec-biblio">Biblio</a></emu-xref>();</li><li>Unfound <emu-xref href="#sec-algorithm-conventions-abstract-operations"><a href="https://tc39.es/ecma262/#sec-algorithm-conventions-abstract-operations">abstract operations</a></emu-xref> just don't link: Unfound();</li><li>Can prefix with !&nbsp;and ?.<ol><li>Let <var>foo</var> be ?&nbsp;<emu-xref aoid="Internal" id="_ref_1"><a href="#sec-internal">Internal</a></emu-xref>();</li><li>Set <var>foo</var> to !&nbsp;<emu-xref aoid="Internal" id="_ref_2"><a href="#sec-internal">Internal</a></emu-xref>();</li><li>Set <var>foo</var> to !&nbsp;SDO of <var>operation</var>.</li><li>Set <var>foo</var> to !&nbsp;<var>operation</var>.<var class="field">[[MOP]]</var>().</li></ol></li><li>A <emu-xref href="#sec-list-and-record-specification-type"><a href="https://tc39.es/ecma262/#sec-list-and-record-specification-type">Record</a></emu-xref> looks like this: <emu-xref href="#sec-list-and-record-specification-type"><a href="https://tc39.es/ecma262/#sec-list-and-record-specification-type">Record</a></emu-xref> { <var class="field">[[Key]]</var>: 0&nbsp;}.</li><li>A <emu-xref href="#sec-list-and-record-specification-type"><a href="https://tc39.es/ecma262/#sec-list-and-record-specification-type">List</a></emu-xref> looks like this: « 0, 1&nbsp;».</li></ol></emu-alg>

<emu-clause id="sec-internal" aoid="Internal">
Expand Down
15 changes: 9 additions & 6 deletions test/baselines/generated-reference/imports.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,26 @@
<li><span>Toggle pinning of the current clause</span><code>p</code></li>
<li><span>Jump to <i>n</i>th pin</span><code>1-9</code></li>
</ul></div><div id="spec-container">
<emu-import href="imports/import1.html"><emu-clause id="Baz" aoid="Baz">
<emu-clause id="Baz" aoid="Baz">
<h1><span class="secnum">1</span> Header</h1>
<p>text</p>
<p><emu-nt>nonterminal</emu-nt> <code>code</code> <emu-val>value</emu-val>.</p>

<emu-import href="sub/import3.html"><emu-clause id="import3">
<emu-clause id="import3">
<h1><span class="secnum">1.1</span> Import 3</h1>
wtf??
<emu-grammar type="definition"><emu-production name="A" collapsed="" id="prod-A">
<emu-nt><a href="#prod-A">A</a></emu-nt> <emu-geq>:</emu-geq> <emu-rhs a="ayxoiyyq"><emu-t>b</emu-t></emu-rhs>
</emu-production>
</emu-grammar>
</emu-clause></emu-import>
</emu-clause></emu-import>
<emu-import href="imports/import2.html"><emu-clause id="import2">
</emu-clause>

</emu-clause>

<emu-clause id="import2">
<h1><span class="secnum">2</span> Import 2</h1>
</emu-clause></emu-import>
</emu-clause>


<emu-alg><ol><li>Ensure we can auto-link to imported aoids: <emu-xref aoid="Baz" id="_ref_0"><a href="#Baz">Baz</a></emu-xref>()</li></ol></emu-alg>
</div></body>
10 changes: 6 additions & 4 deletions test/baselines/generated-reference/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,22 @@ <h1><span class="secnum">1.1</span> Sub Clause</h1>
<h1><span class="secnum">1.2</span> Sub Clause</h1>
</emu-clause>

<emu-import href="imports/import1.html"><emu-clause id="Baz" aoid="Baz">
<emu-clause id="Baz" aoid="Baz">
<h1><span class="secnum">1.3</span> Header</h1>
<p>text</p>
<p><emu-nt>nonterminal</emu-nt> <code>code</code> <emu-val>value</emu-val>.</p>

<emu-import href="sub/import3.html"><emu-clause id="import3">
<emu-clause id="import3">
<h1><span class="secnum">1.3.1</span> Import 3</h1>
wtf??
<emu-grammar type="definition"><emu-production name="A" collapsed="" id="prod-A">
<emu-nt><a href="#prod-A">A</a></emu-nt> <emu-geq>:</emu-geq> <emu-rhs a="ayxoiyyq"><emu-t>b</emu-t></emu-rhs>
</emu-production>
</emu-grammar>
</emu-clause></emu-import>
</emu-clause></emu-import>
</emu-clause>

</emu-clause>


<emu-alg><ol><li>Call <emu-xref aoid="Foo" id="_ref_3"><a href="#Foo">Foo</a></emu-xref>(<var>a</var>).</li><li>Call <emu-xref aoid="Bar" id="_ref_4"><a href="#Bar">Bar</a></emu-xref>(<code>toString</code>).</li><li>Call <emu-xref aoid="Baz" id="_ref_5"><a href="#Baz">Baz</a></emu-xref>(<emu-val>true</emu-val>).<ol><li>Do something else.</li><li>And again.</li></ol></li></ol></emu-alg>

Expand Down
2 changes: 1 addition & 1 deletion test/baselines/generated-reference/xref.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<li><span>Toggle pinning of the current clause</span><code>p</code></li>
<li><span>Jump to <i>n</i>th pin</span><code>1-9</code></li>
</ul></div><div id="spec-container">
<emu-biblio href="./xrefTestBiblio.json"></emu-biblio>

<emu-clause id="sec-clause-id">
<h1><span class="secnum">1</span> Clause Title</h1>

Expand Down

0 comments on commit 6c0fb1f

Please sign in to comment.