├── .eslintignore ├── .npmrc ├── boilerplate ├── proposal-copyright.html ├── address.html ├── software-license.html ├── standard-copyright.html └── alternative-copyright.html ├── test ├── boilerplate-copyright.fixture ├── baselines │ ├── sources │ │ ├── malformed.bad.html │ │ ├── charset-absent.html │ │ ├── imports │ │ │ ├── import2.html │ │ │ ├── sub │ │ │ │ └── import3.html │ │ │ └── import1.html │ │ ├── charset-present.html │ │ ├── html-element-attributes.html │ │ ├── algorithmsBiblio.json │ │ ├── date.html │ │ ├── toc.html │ │ ├── proposal-copyright.html │ │ ├── xrefTestBiblio.json │ │ ├── copyright.html │ │ ├── ins-nonterminal.html │ │ ├── shortname.html │ │ ├── title.html │ │ ├── assets-inline.html │ │ ├── ecmarkdown.html │ │ ├── imports.html │ │ ├── boilerplate-address.html │ │ ├── boilerplate-license.html │ │ ├── boilerplate-copyright.html │ │ ├── duplicate-productions.html │ │ ├── boilerplate-all.html │ │ ├── escaping.html │ │ ├── oldids.html │ │ ├── nonterminal-used-before-definition.html │ │ ├── step-xrefs.html │ │ ├── notes.html │ │ ├── emd-in-grammar.html │ │ ├── namespaces-productions.html │ │ ├── multipage.html │ │ ├── example.html │ │ ├── max-clause-depth.html │ │ ├── eqn.html │ │ ├── code.html │ │ ├── grammar.html │ │ ├── algorithms.html │ │ ├── optional-parts.html │ │ ├── autolinking.html │ │ ├── dfn.html │ │ ├── algorithm-replacements.html │ │ ├── duplicate-ids.html │ │ ├── clauses.html │ │ ├── figure.html │ │ └── namespaces.html │ └── generated-reference │ │ ├── charset-absent.html │ │ ├── charset-present.html │ │ ├── html-element-attributes.html │ │ ├── date.html │ │ ├── ecmarkdown.html │ │ ├── title.html │ │ ├── shortname.html │ │ ├── ins-nonterminal.html │ │ ├── escaping.html │ │ ├── notes.html │ │ ├── oldids.html │ │ ├── step-xrefs.html │ │ ├── imports.html │ │ ├── duplicate-productions.html │ │ ├── nonterminal-used-before-definition.html │ │ ├── example.html │ │ ├── toc.html │ │ ├── emd-in-grammar.html │ │ ├── multipage.html │ │ ├── multipage │ │ │ ├── index.html │ │ │ ├── second.html │ │ │ └── third.html │ │ └── index.html │ │ ├── code.html │ │ ├── algorithm-replacements.html │ │ ├── eqn.html │ │ ├── boilerplate-all.html │ │ ├── duplicate-ids.html │ │ ├── optional-parts.html │ │ ├── namespaces-productions.html │ │ ├── figure.html │ │ ├── dfn.html │ │ ├── autolinking.html │ │ ├── proposal-copyright.html │ │ ├── max-clause-depth.html │ │ ├── boilerplate-copyright.html │ │ └── algorithms.html ├── boilerplate-address.fixture ├── format-good.html ├── format-bad.html ├── build.js ├── boilerplate-license.fixture ├── clauseIds.js ├── lint-tags.js └── cli.js ├── .prettierignore ├── ecma-logo.png ├── bin ├── ecmarkup.js └── emu-format.js ├── img └── ecma-header.png ├── fonts ├── IBMPlexMono-Bold-SlashedZero.woff2 ├── IBMPlexMono-Italic-SlashedZero.woff2 ├── IBMPlexSans-Bold-SlashedZero.woff2 ├── IBMPlexSans-Italic-SlashedZero.woff2 ├── IBMPlexSerif-Bold-SlashedZero.woff2 ├── IBMPlexMono-Regular-SlashedZero.woff2 ├── IBMPlexSans-Regular-SlashedZero.woff2 ├── IBMPlexSerif-Italic-SlashedZero.woff2 ├── IBMPlexSerif-Regular-SlashedZero.woff2 ├── IBMPlexMono-BoldItalic-SlashedZero.woff2 ├── IBMPlexSans-BoldItalic-SlashedZero.woff2 └── IBMPlexSerif-BoldItalic-SlashedZero.woff2 ├── .gitignore ├── .editorconfig ├── tsconfig.test.json ├── src ├── lint │ ├── algorithm-error-reporter-type.ts │ ├── rules │ │ ├── algorithm-step-numbering.ts │ │ ├── algorithm-step-labels.ts │ │ ├── enum-casing.ts │ │ ├── for-each-element.ts │ │ ├── step-attributes.ts │ │ └── if-else-consistency.ts │ ├── collect-tag-diagnostics.ts │ └── lint.ts ├── external.d.ts ├── Context.ts ├── Terminal.ts ├── GrammarAnnotation.ts ├── .eslintrc.json ├── H1.ts ├── Builder.ts ├── Dfn.ts ├── ecmarkup.ts ├── Toc.ts ├── Example.ts ├── Meta.ts ├── Figure.ts ├── Import.ts ├── NonTerminal.ts ├── Eqn.ts ├── formatter │ ├── header.ts │ └── text.ts ├── Note.ts ├── ProdRef.ts ├── RHS.ts ├── args.ts └── arg-parser.ts ├── spec └── biblio.json ├── tsconfig.json ├── README.md ├── scripts └── auto-deploy.sh ├── .github └── workflows │ ├── publish.yml │ ├── update-docs.yml │ ├── enforce-format.yml │ └── check.yml ├── js ├── multipage.js ├── sdoMap.js ├── superscripts.js └── print.js ├── filter-entities.js ├── LICENSE ├── PUBLISHING.md ├── .eslintrc.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | docs 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | allow-same-version=true 2 | message=v%s 3 | -------------------------------------------------------------------------------- /boilerplate/proposal-copyright.html: -------------------------------------------------------------------------------- 1 |

© !YEAR! !CONTRIBUTORS!

2 | -------------------------------------------------------------------------------- /test/boilerplate-copyright.fixture: -------------------------------------------------------------------------------- 1 |

© !YEAR! !CONTRIBUTORS!

2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.md 3 | *.json 4 | *.yml 5 | lib/ 6 | docs/ 7 | -------------------------------------------------------------------------------- /ecma-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/ecma-logo.png -------------------------------------------------------------------------------- /bin/ecmarkup.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | require('../lib/cli'); 4 | -------------------------------------------------------------------------------- /img/ecma-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/img/ecma-header.png -------------------------------------------------------------------------------- /bin/emu-format.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | require('../lib/formatter/cli'); 4 | -------------------------------------------------------------------------------- /test/baselines/sources/malformed.bad.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /fonts/IBMPlexMono-Bold-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexMono-Bold-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexMono-Italic-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexMono-Italic-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexSans-Bold-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexSans-Bold-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexSans-Italic-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexSans-Italic-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexSerif-Bold-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexSerif-Bold-SlashedZero.woff2 -------------------------------------------------------------------------------- /test/baselines/sources/charset-absent.html: -------------------------------------------------------------------------------- 1 |
2 | toc: false
3 | copyright: false
4 | assets: none
5 | 
6 | -------------------------------------------------------------------------------- /fonts/IBMPlexMono-Regular-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexMono-Regular-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexSans-Regular-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexSans-Regular-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexSerif-Italic-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexSerif-Italic-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexSerif-Regular-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexSerif-Regular-SlashedZero.woff2 -------------------------------------------------------------------------------- /test/baselines/sources/imports/import2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Import 2

4 |
5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /index.html 3 | lib 4 | test/baselines/generated-local 5 | package 6 | ecmarkup-*.tgz 7 | .vscode/ 8 | docs/ 9 | -------------------------------------------------------------------------------- /fonts/IBMPlexMono-BoldItalic-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexMono-BoldItalic-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexSans-BoldItalic-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexSans-BoldItalic-SlashedZero.woff2 -------------------------------------------------------------------------------- /fonts/IBMPlexSerif-BoldItalic-SlashedZero.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tc39/ecmarkup/HEAD/fonts/IBMPlexSerif-BoldItalic-SlashedZero.woff2 -------------------------------------------------------------------------------- /test/baselines/sources/charset-present.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | toc: false
4 | copyright: false
5 | assets: none
6 | 
7 | 8 | -------------------------------------------------------------------------------- /test/baselines/sources/html-element-attributes.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | toc: false
4 | copyright: false
5 | assets: none
6 | 
7 | Content 8 | 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /test/baselines/sources/imports/sub/import3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Import 3

4 | wtf?? 5 | 6 | A : `b` 7 | 8 |
9 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": ["lib/**/*"], 4 | "compilerOptions": { 5 | "outDir": "this needs to be set so it does not ignore lib", 6 | "noEmit": true, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/baselines/sources/algorithmsBiblio.json: -------------------------------------------------------------------------------- 1 | { 2 | "location": "http://example.com/fooSite.html", 3 | "entries": [ 4 | { 5 | "type": "op", 6 | "aoid": "Biblio", 7 | "refId": "sec-biblio" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/baselines/sources/date.html: -------------------------------------------------------------------------------- 1 |
 2 | title:     test title!
 3 | date:      2014-07-01
 4 | status:    standard
 5 | toc:       false
 6 | copyright: false
 7 | assets:    none
 8 | 
9 | 10 |

Some body content

11 | 12 | -------------------------------------------------------------------------------- /test/baselines/sources/toc.html: -------------------------------------------------------------------------------- 1 |
 2 | copyright: false
 3 | assets: none
 4 | 
5 | 6 | 7 |

Example Link Header

8 |

Example clause contents.

9 |
10 | -------------------------------------------------------------------------------- /test/baselines/sources/imports/import1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Header

4 |

text

5 |

|nonterminal| `code` *value*.

6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /test/baselines/sources/proposal-copyright.html: -------------------------------------------------------------------------------- 1 |
 2 | title:     test title!
 3 | toc:       false
 4 | date:      2014-09-26
 5 | assets:    none
 6 | stage:     0
 7 | contributors: Brian Terlson, Ecma International
 8 | 
9 | 10 | -------------------------------------------------------------------------------- /src/lint/algorithm-error-reporter-type.ts: -------------------------------------------------------------------------------- 1 | export type Reporter = (lintingError: LintingError) => void; 2 | 3 | // TODO rename 4 | export type LintingError = { 5 | ruleId: string; 6 | message: string; 7 | line: number; 8 | column: number; 9 | }; 10 | -------------------------------------------------------------------------------- /test/baselines/sources/xrefTestBiblio.json: -------------------------------------------------------------------------------- 1 | { 2 | "location": "http://example.com/fooSite.html", 3 | "entries": [ 4 | { 5 | "type": "clause", 6 | "title": "Foo Section", 7 | "number": "1", 8 | "id": "sec-foo" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/baselines/sources/copyright.html: -------------------------------------------------------------------------------- 1 |
 2 | title:     test title!
 3 | status:    draft
 4 | version:   Draft 1
 5 | toc:       false
 6 | date:      2014-09-26
 7 | assets:    none
 8 | 
9 | 10 |

Test Clause

11 |
12 | -------------------------------------------------------------------------------- /test/baselines/sources/ins-nonterminal.html: -------------------------------------------------------------------------------- 1 |
 2 | toc: false
 3 | copyright: false
 4 | assets: none
 5 | 
6 | 7 | 8 | a b c d e f 9 | 10 | -------------------------------------------------------------------------------- /test/baselines/sources/shortname.html: -------------------------------------------------------------------------------- 1 |
 2 | title:     test title!
 3 | shortname: ECMA-000
 4 | status:    draft
 5 | version:   Draft 1
 6 | toc:       false
 7 | date:      2015-09-26
 8 | copyright: false
 9 | assets:    none
10 | 
11 | 12 |

Some body content

13 | -------------------------------------------------------------------------------- /test/baselines/sources/title.html: -------------------------------------------------------------------------------- 1 |
 2 | title:     test title!
 3 | description: a test document
 4 | status:    draft
 5 | version:   Draft 1
 6 | toc:       false
 7 | copyright: false
 8 | date:      2015-09-26
 9 | assets:    none
10 | 
11 | 12 |

Some body content

13 | -------------------------------------------------------------------------------- /test/baselines/sources/assets-inline.html: -------------------------------------------------------------------------------- 1 |
 2 |     toc: true
 3 |     date: 1997-06-01
 4 |     copyright: true
 5 |     contributors: Ecma
 6 |     assets: inline
 7 | 
8 | 9 |

Intro

10 |

This is a section.

11 |
12 | -------------------------------------------------------------------------------- /test/baselines/sources/ecmarkdown.html: -------------------------------------------------------------------------------- 1 |
 2 | toc: false
 3 | copyright: false
 4 | assets: none
 5 | 
6 | 7 |

ecmarkdown basics

8 |

ecmarkdown like _v_ works before and after comments _v_.

9 |
10 | 11 | -------------------------------------------------------------------------------- /src/external.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'html-escape' { 2 | function escape(text: string): string; 3 | export = escape; 4 | } 5 | 6 | declare module 'promise-debounce' { 7 | function debounce Promise>(fn: TFunc, ctx?: any): TFunc; 8 | export = debounce; 9 | } 10 | -------------------------------------------------------------------------------- /spec/biblio.json: -------------------------------------------------------------------------------- 1 | { 2 | "location": "https://tc39.es/ecma262/", 3 | "entries": [ 4 | { 5 | "type": "op", 6 | "id": "sec-returnifabrupt", 7 | "aoid": "ReturnIfAbrupt" 8 | }, 9 | { 10 | "type": "op", 11 | "id": "sec-get-o-p", 12 | "aoid": "Get" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/baselines/sources/imports.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 1. Ensure we can auto-link to imported aoids: Baz() 11 | 12 | -------------------------------------------------------------------------------- /test/baselines/sources/boilerplate-address.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /test/baselines/sources/boilerplate-license.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /test/baselines/sources/boilerplate-copyright.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /boilerplate/address.html: -------------------------------------------------------------------------------- 1 |

Ecma International

2 |

Rue du Rhone 114

3 |

CH-1204 Geneva

4 |

Tel: +41 22 849 6000

5 |

Fax: +41 22 849 6001

6 |

Web: https://ecma-international.org/

7 | -------------------------------------------------------------------------------- /test/boilerplate-address.fixture: -------------------------------------------------------------------------------- 1 |

Tet Corporation

2 |

2 Hammarskjöld Plaza

3 |

New York City, New York

4 |

Tel: 123-456-7890

5 |

Fax: 123-456-7890

6 |

Web: http://www.tet-corporation.com/

7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "target": "es2019", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "declaration": true, 9 | "stripInternal": true, 10 | "outDir": "lib", 11 | "lib": ["es2020", "dom", "dom.iterable"], 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/baselines/sources/duplicate-productions.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | GMD :: 9 | Test `bar` 10 | 11 | 12 | 13 | 14 | GMD :: 15 | Test `bar` 16 | 17 | -------------------------------------------------------------------------------- /test/format-good.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Clause Foo(_a_, _b_)

8 | 9 | 10 | 1. Call Foo(_a_). 11 | 1. Call Bar(`toString`). 12 | 1. Call Baz(*true*). 13 | 1. Do something else. 14 | 1. And again. 15 | 16 |
17 | -------------------------------------------------------------------------------- /test/format-bad.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Clause Foo( _a_, _b_ )

9 | 10 | 11 | 1. Call Foo(_a_). 12 | 1. Call Bar(`toString`). 13 | 1. Call Baz(*true*). 14 | 1. Do something else. 15 | 1. And again. 16 | 17 |
18 | -------------------------------------------------------------------------------- /test/baselines/sources/boilerplate-all.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /test/baselines/sources/escaping.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

blah

8 |

U+200C (ZERO WIDTH NON-JOINER) is abbreviated "<ZWNJ>"

9 |

<b>also doesn't escape when *emd* and autolinks to ReturnIfAbrupt are present.

10 |

Escpaing inside xref is preserved: <b>.

11 |
12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ecmarkup 2 | ======== 3 | 4 | A web component-based source format for ECMAScript and related specifications. See [the project page](https://tc39.es/ecmarkup/) for details. 5 | 6 | 7 | ### Installing & Using 8 | 9 | Requires node 8 or above. 10 | 11 | ``` 12 | npm install ecmarkup 13 | ecmarkup in.html out.html 14 | ``` 15 | 16 | ### Formatter 17 | 18 | There is also an auto-formatter: 19 | 20 | ``` 21 | emu-format --write spec.html 22 | ``` 23 | 24 | See its `--help` for more details. 25 | -------------------------------------------------------------------------------- /test/baselines/sources/oldids.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

c1

8 |

This is some content and such.

9 | 10 |

c1.1

11 |
12 | 13 | 14 | 15 | 16 | 17 |

This is a note!

18 |
19 |
20 | -------------------------------------------------------------------------------- /src/Context.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type { Import } from './Import'; 3 | import type Clause from './Clause'; 4 | import type { ClauseNumberIterator } from './clauseNums'; 5 | 6 | export interface Context { 7 | spec: Spec; 8 | node: HTMLElement; 9 | importStack: Import[]; 10 | clauseStack: Clause[]; 11 | tagStack: HTMLElement[]; 12 | clauseNumberer: ClauseNumberIterator; 13 | inNoAutolink: boolean; 14 | inNoEmd: boolean; 15 | inAlg: boolean; 16 | followingEmd: Node | null; 17 | currentId: string | null; 18 | } 19 | -------------------------------------------------------------------------------- /test/baselines/sources/nonterminal-used-before-definition.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

Example SDO

8 | 9 | Example: `foo` 10 | 11 | 12 | 1. Return *false*. 13 | 14 |
15 | 16 | 17 |

Example Grammar

18 |

Syntax

19 | 20 | Example : `foo` 21 | 22 |
23 | -------------------------------------------------------------------------------- /test/baselines/sources/step-xrefs.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |

Title

9 | 10 |

You can refer to a step before it occurs in text, like step .

11 | 12 | 13 | 1. Step. 14 | 1. List: 15 | 1. List: 16 | 1. Step. 17 | 1. [id="example"] Step. 18 | 19 | 20 |

You can refer to it after as well, like step .

21 |
22 | -------------------------------------------------------------------------------- /scripts/auto-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euxo pipefail 4 | 5 | declare -r GH_USER_EMAIL="bot@tc39" 6 | declare -r GH_USER_NAME="Bot" 7 | declare -r COMMIT_MESSAGE="Update gh-pages" 8 | 9 | cd "$(dirname "$BASH_SOURCE")"/../docs 10 | 11 | git config --global user.email "${GH_USER_EMAIL}" 12 | git config --global user.name "${GH_USER_NAME}" 13 | git config --global init.defaultBranch gh-pages 14 | git init 15 | git add -A 16 | git commit --message "${COMMIT_MESSAGE}" 17 | git push --force "https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" gh-pages 18 | -------------------------------------------------------------------------------- /test/baselines/sources/notes.html: -------------------------------------------------------------------------------- 1 | 6 | This is a note. Since it is outside a clause it is unstyled. 7 | 8 | 9 |

This

10 | 11 | This is note 1, but since it is the only note, it is not numbered. 12 | 13 | 14 |

That

15 | This is note 1, should be numbered 16 | This is note 2, should also be numbered 17 |
18 |
19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | 14 | - name: Setup node 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: '18' 18 | registry-url: 'https://registry.npmjs.org' 19 | 20 | - name: Install dependencies 21 | run: npm ci 22 | 23 | - name: Publish 24 | run: npm publish 25 | env: 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_FOR_TC39_USER }} 27 | -------------------------------------------------------------------------------- /test/baselines/sources/emd-in-grammar.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |

Example

9 | 10 | Foo :: 11 | Bar [> but only if the AbstractOperation of |Bar| is ≤ _variable_] 12 | 13 | Bar :: `example` 14 | 15 | 16 | 17 |

Static Semantics: AbstractOperation

18 | Bar :: `example` 19 | 20 | 1. Return 0. 21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /js/multipage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let idToSection = Object.create(null); 3 | for (let [section, ids] of Object.entries(multipageMap)) { 4 | for (let id of ids) { 5 | if (!idToSection[id]) { 6 | idToSection[id] = section; 7 | } 8 | } 9 | } 10 | if (location.hash) { 11 | let targetSec = idToSection[location.hash.substring(1)]; 12 | if (targetSec != null) { 13 | let match = location.pathname.match(/([^/]+)\.html?$/); 14 | if ((match != null && match[1] !== targetSec) || location.pathname.endsWith('/multipage/')) { 15 | window.navigating = true; 16 | location = (targetSec === 'index' ? './' : targetSec + '.html') + location.hash; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/update-docs.yml: -------------------------------------------------------------------------------- 1 | name: 'update docs' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | docs: 10 | name: 'deploy github pages' 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup node 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: '18' 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Build 25 | run: npm run build && npm run build-spec 26 | 27 | - name: Deploy docs 28 | run: ./scripts/auto-deploy.sh 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /test/baselines/sources/namespaces-productions.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

Intro

8 | 9 | Foo :: 10 | `bar` 11 | `baz` 12 | 13 | Foo :: `bar` 14 | 15 |
16 | 17 | 18 |

C

19 | 20 | Foo :: 21 | `bar` 22 | `qux` 23 | 24 | Statement :: 25 | `overridden statement` 26 | 27 | Foo :: `bar` 28 | 29 |
30 | -------------------------------------------------------------------------------- /test/baselines/sources/multipage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

Intro

13 |

Some text.

14 |
15 | 16 | 17 |

Second Clause

18 |

A cross-section reference: Alg.

19 |

A relative image: .

20 |
21 | 22 | 23 |

Third Clause

24 |

Some text.

25 | 26 |

Algorithm

27 |

Pretend there's an algorithm here.

28 |
29 |
30 | -------------------------------------------------------------------------------- /.github/workflows/enforce-format.yml: -------------------------------------------------------------------------------- 1 | name: enforce-format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | check: 11 | name: Enforce spec format 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: '18' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Enforce spec format 29 | run: 'if ! node bin/emu-format.js --check spec/index.html; then echo "You need to run \`npm run format-spec\`"; exit 1; fi' 30 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/charset-absent.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/charset-present.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
    5 |
  • Toggle shortcuts help?
  • 6 |
  • Toggle "can call user code" annotationsu
  • 7 | 8 |
  • Jump to search box/
  • 9 |
  • Toggle pinning of the current clausep
  • 10 |
  • Jump to the nth pin1-9
  • 11 |
  • Jump to the 10th pin0
  • 12 |
  • Jump to the most recent link target`
  • 13 |
14 | 15 |
-------------------------------------------------------------------------------- /test/baselines/sources/example.html: -------------------------------------------------------------------------------- 1 | 6 |

Examples outside of clauses aren't processed specially.

7 | 8 | This is an example. 9 | 10 | 11 | 12 |

Example Section

13 | 14 | Examples inside clauses are numbered and have captions. Captions are optional. 15 | 16 | 17 | 18 |

Example Subclause

19 | 20 | Multiple examples are numbered similar to notes 21 | 22 | 23 | 24 | So this becomes example 2. 25 | 26 |
27 |
28 | 29 | -------------------------------------------------------------------------------- /src/lint/rules/algorithm-step-numbering.ts: -------------------------------------------------------------------------------- 1 | import type { OrderedListItemNode } from 'ecmarkdown'; 2 | import type { Reporter } from '../algorithm-error-reporter-type'; 3 | 4 | const ruleId = 'algorithm-step-numbering'; 5 | 6 | /* 7 | Checks that step numbers are all `1`. 8 | */ 9 | export default function (report: Reporter, node: OrderedListItemNode, algorithmSource: string) { 10 | const itemSource = algorithmSource.slice(node.location.start.offset, node.location.end.offset); 11 | const match = itemSource.match(/^(\s*)(\d+\.) /)!; 12 | if (match[2] !== '1.') { 13 | report({ 14 | ruleId, 15 | line: node.location.start.line, 16 | column: node.location.start.column + match[1].length, 17 | message: `expected step number to be "1." (found ${JSON.stringify(match[2])})`, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | check: 11 | name: Check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: '18' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Test 29 | run: npm test 30 | 31 | - name: Lint 32 | run: npm run lint 33 | 34 | - name: Test published files 35 | run: npm run test-published-files 36 | 37 | - name: Test declarations 38 | run: npm run test-declarations 39 | -------------------------------------------------------------------------------- /src/Terminal.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type Production from './Production'; 3 | 4 | import Builder from './Builder'; 5 | 6 | export default class Terminal extends Builder { 7 | /** @internal */ production: Production; 8 | /** @internal */ optional: boolean; 9 | 10 | constructor(spec: Spec, prod: Production, node: HTMLElement) { 11 | super(spec, node); 12 | this.production = prod; 13 | 14 | this.optional = node.hasAttribute('optional'); 15 | } 16 | 17 | build(): void { 18 | let modifiers = ''; 19 | 20 | if (this.optional) { 21 | modifiers += 'opt'; 22 | } 23 | 24 | if (modifiers === '') return; 25 | 26 | const el = this.spec.doc.createElement('emu-mods'); 27 | el.innerHTML = modifiers; 28 | 29 | this.node.appendChild(el); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/html-element-attributes.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | Content 14 | 15 |
-------------------------------------------------------------------------------- /src/GrammarAnnotation.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type Production from './Production'; 3 | 4 | import Builder from './Builder'; 5 | 6 | export default class GrammarAnnotation extends Builder { 7 | /** @internal */ production: Production; 8 | 9 | constructor(spec: Spec, prod: Production, node: HTMLElement) { 10 | super(spec, node); 11 | this.production = prod; 12 | } 13 | 14 | build() { 15 | if (!this.node.firstChild) return; 16 | 17 | if (this.node.firstChild.nodeType === 3) { 18 | this.node.firstChild.textContent = '[' + this.node.firstChild.textContent; 19 | } else { 20 | const pre = this.spec.doc.createTextNode('['); 21 | this.node.insertBefore(pre, this.node.children[0]); 22 | } 23 | 24 | const post = this.spec.doc.createTextNode(']'); 25 | this.node.appendChild(post); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/date.html: -------------------------------------------------------------------------------- 1 | 2 | test title!
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |

test title!

13 | 14 |

Some body content

15 | 16 |
-------------------------------------------------------------------------------- /test/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('assert'); 3 | 4 | const build = require('../lib/ecmarkup').build; 5 | 6 | const doc = 7 | '

hi

'; 8 | 9 | function fetch(file) { 10 | if (file.match(/\.json$/)) { 11 | return '{}'; 12 | } else { 13 | return doc; 14 | } 15 | } 16 | 17 | describe('ecmarkup#build', () => { 18 | it('takes a fetch callback that returns a promise', async () => { 19 | let spec = await build( 20 | 'root.html', 21 | file => 22 | new Promise(res => { 23 | process.nextTick(() => res(fetch(file))); 24 | }), 25 | ); 26 | let result = spec.toHTML(); 27 | assert.equal(typeof result, 'string'); 28 | assert(result.includes(`
`)); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "ecmaVersion": 2020, 6 | "//": "NB project is relative to the directory you run eslint from, see https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/parser#parseroptionsproject", 7 | "project": "./tsconfig.json" 8 | }, 9 | "extends": [ 10 | "../.eslintrc.json", 11 | "plugin:@typescript-eslint/eslint-recommended" 12 | ], 13 | "plugins": [ 14 | "@typescript-eslint" 15 | ], 16 | "rules": { 17 | "@typescript-eslint/no-use-before-define": [ 18 | "error", 19 | { 20 | "functions": false, 21 | "typedefs": false 22 | } 23 | ], 24 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 25 | "@typescript-eslint/no-unused-vars": "error", 26 | "no-unused-vars": "off" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lint/rules/algorithm-step-labels.ts: -------------------------------------------------------------------------------- 1 | import type { OrderedListItemNode } from 'ecmarkdown'; 2 | import type { Reporter } from '../algorithm-error-reporter-type'; 3 | 4 | const ruleId = 'algorithm-step-labels'; 5 | 6 | /* 7 | Checks that step labels all start with `step-`. 8 | */ 9 | export default function (report: Reporter, node: OrderedListItemNode, algorithmSource: string) { 10 | const idAttr = node.attrs.find(({ key }) => key === 'id'); 11 | if (idAttr != null && !/^step-/.test(idAttr.value)) { 12 | const itemSource = algorithmSource.slice( 13 | idAttr.location.start.offset, 14 | idAttr.location.end.offset, 15 | ); 16 | const offset = itemSource.match(/^id *= *"/)![0].length; 17 | report({ 18 | ruleId, 19 | line: idAttr.location.start.line, 20 | column: idAttr.location.start.column + offset, 21 | message: `step labels should start with "step-"`, 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lint/rules/enum-casing.ts: -------------------------------------------------------------------------------- 1 | import type { Reporter } from '../algorithm-error-reporter-type'; 2 | import type { OrderedListItemNode } from 'ecmarkdown'; 3 | import { offsetToLineAndColumn } from '../../utils'; 4 | 5 | const ruleId = 'enum-casing'; 6 | 7 | /* 8 | Checks that ~enum-values~ are kebab-cased. 9 | */ 10 | export default function (report: Reporter, step: OrderedListItemNode, algorithmSource: string) { 11 | for (const item of step.contents) { 12 | if (item.name !== 'tilde' || item.contents.length !== 1 || item.contents[0].name !== 'text') { 13 | continue; 14 | } 15 | const text = item.contents[0]; 16 | if (/[\p{Uppercase_Letter}\s]/u.test(text.contents)) { 17 | const location = offsetToLineAndColumn(algorithmSource, text.location.start.offset); 18 | report({ 19 | ruleId, 20 | message: 'enum values should be lowercase and kebab-cased', 21 | ...location, 22 | }); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /filter-entities.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // entities.json is from https://html.spec.whatwg.org/entities.json 4 | // which is built by https://github.com/whatwg/html-build/tree/main/entities 5 | // this processes it into a form more useful for us 6 | // no new entities will be added, so the output of this file is committed 7 | // rather than running it every time 8 | 9 | let fs = require('fs'); 10 | let entities = require('./entities.json'); 11 | 12 | let transformed = Object.fromEntries( 13 | Object.entries(entities).map(([k, v]) => { 14 | // whitespace, default-ignorable, combining characters, control characters 15 | if ( 16 | v.characters === '&' || 17 | v.characters === '<' || 18 | /\p{White_Space}|\p{DI}|\p{gc=M}|\p{gc=C}/u.test(v.characters) 19 | ) { 20 | return [k, null]; 21 | } 22 | return [k, v.characters]; 23 | }), 24 | ); 25 | fs.writeFileSync('./entities-processed.json', JSON.stringify(transformed), 'utf8'); 26 | -------------------------------------------------------------------------------- /test/baselines/sources/max-clause-depth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |

One

13 | 14 |

Two

15 | 16 | 17 |

Three

18 | 19 |

four

20 |
21 |
22 | 23 |

Three Again

24 | 25 |

Four Again

26 |
27 |
28 |
29 | 30 |

Two Again

31 |
32 |
33 | 34 |

One Again

35 |
36 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/ecmarkdown.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

1 ecmarkdown basics

15 |

ecmarkdown like v works before and after comments v.

16 |
17 | 18 |
-------------------------------------------------------------------------------- /test/baselines/sources/eqn.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 |

Header

9 |

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

10 | 11 | 1. Return Value(_val_); 12 | 13 | 14 | 15 | Value2(_t_) 16 | = DateValue(_t_) if Type(_t_) is string 17 | = _t_ 18 | 19 | 20 | 21 | DateValue(t) 22 | = 0 if _t_ = 0 23 | = 1 if _t_ = 1 24 | = 2 25 | 26 | 27 | 28 | Value(_t_) 29 | = DateValue(_t_) if Type(_t_) is string 30 | = _t_ 31 | 32 | 33 | 34 | 1. Return Value(_val_); 35 | 36 | 37 |

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

38 |
39 | -------------------------------------------------------------------------------- /test/baselines/sources/code.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |

Test Clause

9 |

Increases indentaion level.

10 |

11 |     global1.Zone.current; // succeeds
12 |     global2.Zone.current; // throws: currentRealm = realm1; thisRealm = functionRealm = realm2
13 | 
14 |     const getter1 = Object.getOwnPropertyDescriptor(global1.Zone, "current").get;
15 |     const getter2 = Object.getOwnPropertyDescriptor(global2.Zone, "current").get;
16 | 
17 |     getter1.call(global2.Zone); // throws: currentRealm = functionRealm = realm1; thisRealm = realm2
18 |     getter2.call(global1.Zone); // throws: currentRealm = thisRealm = realm1; functionRealm = realm2
19 |   
20 |
21 | -------------------------------------------------------------------------------- /src/H1.ts: -------------------------------------------------------------------------------- 1 | import Builder from './Builder'; 2 | import type { Context } from './Context'; 3 | 4 | export default class H1 extends Builder { 5 | static elements = ['H1']; 6 | 7 | static async enter() { 8 | // do nothing 9 | } 10 | 11 | static async exit({ node, clauseStack }: Context) { 12 | const parent = clauseStack[clauseStack.length - 1] || null; 13 | if (parent === null || parent.header !== node) { 14 | return; 15 | } 16 | const headerClone = node.cloneNode(true) as Element; 17 | for (const a of headerClone.querySelectorAll('a')) { 18 | a.replaceWith(...a.childNodes); 19 | } 20 | parent.titleHTML = headerClone.innerHTML; 21 | parent.title = headerClone.textContent; 22 | if (parent.number) { 23 | // we want to prepend some HTML but setting `innerHTML` on the node will blow away the existing children, 24 | // which messes up other stuff which expects those to keep their identity 25 | node.insertAdjacentHTML('afterbegin', parent.getSecnumHTML()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lint/rules/for-each-element.ts: -------------------------------------------------------------------------------- 1 | import type { Reporter } from '../algorithm-error-reporter-type'; 2 | import type { Seq } from '../../expr-parser'; 3 | import type { OrderedListItemNode } from 'ecmarkdown'; 4 | 5 | const ruleId = 'for-each-element'; 6 | 7 | /* 8 | Checks that "For each" loops name a type or say "element" before the variable. 9 | */ 10 | export default function ( 11 | report: Reporter, 12 | step: OrderedListItemNode, 13 | algorithmSource: string, 14 | parsedSteps: Map, 15 | ) { 16 | const stepSeq = parsedSteps.get(step); 17 | if (stepSeq == null || stepSeq.items.length < 2) { 18 | return; 19 | } 20 | const [first, second] = stepSeq.items; 21 | if (first.name === 'text' && first.contents === 'For each ' && second.name === 'underscore') { 22 | report({ 23 | ruleId, 24 | line: second.location.start.line, 25 | column: second.location.start.column, 26 | message: 'expected "for each" to have a type name or "element" before the loop variable', 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/boilerplate-license.fixture: -------------------------------------------------------------------------------- 1 |

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

2 | 3 |

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

4 | 5 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

6 | -------------------------------------------------------------------------------- /test/clauseIds.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const sectionNums = require('../lib/clauseNums').default; 5 | 6 | describe('clause id generation', () => { 7 | let iter; 8 | 9 | beforeEach(() => { 10 | iter = sectionNums({ opts: {} }); 11 | }); 12 | 13 | specify('generating clause ids', () => { 14 | const CLAUSE = { nodeName: 'EMU-CLAUSE', hasAttribute: () => false }; 15 | const ANNEX = { nodeName: 'EMU-ANNEX', hasAttribute: () => false }; 16 | assert.strictEqual(iter.next([], CLAUSE), '1'); 17 | assert.strictEqual(iter.next([{}], CLAUSE), '1.1'); 18 | assert.strictEqual(iter.next([{}], CLAUSE), '1.2'); 19 | assert.strictEqual(iter.next([{}, {}], CLAUSE), '1.2.1'); 20 | assert.strictEqual(iter.next([], CLAUSE), '2'); 21 | assert.strictEqual(iter.next([], ANNEX), 'A'); 22 | assert.strictEqual(iter.next([{}], ANNEX), 'A.1'); 23 | assert.strictEqual(iter.next([{}], ANNEX), 'A.2'); 24 | assert.strictEqual(iter.next([{}, {}], ANNEX), 'A.2.1'); 25 | assert.strictEqual(iter.next([], ANNEX), 'B'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/title.html: -------------------------------------------------------------------------------- 1 | 2 | test title!
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |

Draft 1 / September 26, 2015

test title!

13 | 14 |

Some body content

15 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/shortname.html: -------------------------------------------------------------------------------- 1 | 2 | test title!
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |

Draft ECMA-000

Draft 1 / September 26, 2015

test title!

13 | 14 |

Some body content

15 |
-------------------------------------------------------------------------------- /test/baselines/sources/grammar.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | FooTerminal : 10 | Inserting `anRHS` 11 | Deleting `anRHS` 12 | Modifying Existing Old Production 13 | 14 | 15 | NewTerminal : 16 | `t` 17 | 18 | 19 | LookaheadExample :: 20 | `&&` `n` [lookahead <! {`1`, `3`, `5`, `7`, `9`}] DecimalDigits 21 | DecimalDigit [lookahead <! DecimalDigit] 22 | 23 | HTMLEntitiesExample :: 24 | > This is technically a “production” 25 | 26 | ButOnlyExample :: 27 | DecimalEscape [> but only if the CapturingGroupNumber of |DecimalEscape| is <= _NcapturingParens_] 28 | 29 | 30 |

Can also have inline productions like Atom :: `(` Disjunction `)`.

31 | 32 | 33 | 1. Can also have productions in steps. 34 | 1. Example is Atom :: `(` Disjunction `)`. 35 | 36 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/ins-nonterminal.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 | 15 | A :: one of a b c d e f 16 | 17 |
-------------------------------------------------------------------------------- /src/lint/rules/step-attributes.ts: -------------------------------------------------------------------------------- 1 | import type { OrderedListItemNode } from 'ecmarkdown'; 2 | import type { Reporter } from '../algorithm-error-reporter-type'; 3 | 4 | import { SPECIAL_KINDS } from '../../Clause'; 5 | 6 | const ruleId = 'step-attribute'; 7 | 8 | const KNOWN_ATTRIBUTES = ['id', 'fence-effects', 'declared', ...SPECIAL_KINDS]; 9 | 10 | /* 11 | Checks for unknown attributes on steps. 12 | */ 13 | export default function (report: Reporter, node: OrderedListItemNode) { 14 | for (const attr of node.attrs) { 15 | if (!KNOWN_ATTRIBUTES.includes(attr.key)) { 16 | report({ 17 | ruleId, 18 | message: `unknown step attribute ${JSON.stringify(attr.key)}`, 19 | line: attr.location.start.line, 20 | column: attr.location.start.column, 21 | }); 22 | } else if (attr.value !== '' && SPECIAL_KINDS.includes(attr.key)) { 23 | report({ 24 | ruleId, 25 | message: `step attribute ${JSON.stringify(attr.key)} should not have a value`, 26 | line: attr.location.start.line, 27 | column: attr.location.start.column + attr.key.length + 2, // =" 28 | }); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2014 Brian Terlson and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/baselines/sources/algorithms.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 1. Can call abstract operations in this spec: Internal(); 9 | 1. Can call abstract operations in ES6: ReturnIfAbrupt(_completion_); 10 | 1. Can call abstract operations in a biblio file: Biblio(); 11 | 1. Unfound abstract operations just don't link: Unfound(); 12 | 1. Can prefix with ! and ?. 13 | 1. Let _foo_ be ? Internal(); 14 | 1. Set _foo_ to ! Internal(); 15 | 1. Set _foo_ to ! SDO of _operation_. 16 | 1. Set _foo_ to ! _operation_.[[MOP]](). 17 | 1. A Record looks like this: Record { [[Key]]: 0 }. 18 | 1. A List looks like this: « 0, 1 ». 19 | 20 | 21 | 22 |

Internal Function

23 |
24 | 25 |

Refs

26 |

Can refer to functions in prose, eg. Internal.

27 |

Internal works if Internal is in the middle of the prose.

28 |

Don't autolink references inside square brackets, eg: [[Internal]]. 29 | Likewise, percent things: %%Internal%%. 30 | Likewise, mentions of Internal.foo.

31 |
32 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/escaping.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

1 blah

15 |

U+200C (ZERO WIDTH NON-JOINER) is abbreviated "<ZWNJ>"

16 |

<b>also doesn't escape when emd and autolinks to ReturnIfAbrupt are present.

17 |

Escpaing inside xref is preserved: <b>.

18 |
19 |
-------------------------------------------------------------------------------- /test/baselines/sources/optional-parts.html: -------------------------------------------------------------------------------- 1 | 6 | This is a note. Since it is outside a clause it is unstyled. 7 | 8 | 9 |

Definitions of Normative Optional etc

10 |

Normative Optional is a defined term, as is Legacy. For comparison, "Deprecated" is not.

11 |
12 | 13 | 14 |

Example Normative Optional Clause

15 |

This clause is normative optional.

16 |
17 | 18 | 19 |

Example Legacy Clause

20 |

This clause is legacy.

21 |
22 | 23 | 24 |

Example algorithm with marked steps.

25 | 26 | 1. This step is required. 27 | 1. [normative-optional=""] This step is optional. 28 | 1. [normative-optional="", legacy=""] This step is legacy. 29 | 1. [legacy=""] This step is only legacy, but has substeps. 30 | 1. This is a substep. 31 | 1. [deprecated=""] This step is optional. 32 | 1. This step is required. 33 |
34 | -------------------------------------------------------------------------------- /src/Builder.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type { Context } from './Context'; 3 | 4 | // We use this instead of `typeof Builder` because using the class as the type also requires derived constructors to be subtypes of the base constructor, which is irritating. 5 | export type BuilderInterface = Omit; 6 | 7 | export default class Builder { 8 | public spec: Spec; 9 | public node: HTMLElement; 10 | 11 | constructor(spec: Spec, node: HTMLElement) { 12 | this.spec = spec; 13 | this.node = node; 14 | 15 | const nodeId = node.getAttribute('id')!; 16 | 17 | if (nodeId !== null) { 18 | if (spec.nodeIds.has(nodeId)) { 19 | spec.warn({ 20 | type: 'attr-value', 21 | attr: 'id', 22 | ruleId: 'duplicate-id', 23 | message: `<${node.tagName.toLowerCase()}> has duplicate id ${JSON.stringify(nodeId)}`, 24 | node, 25 | }); 26 | } 27 | spec.nodeIds.add(nodeId); 28 | } 29 | } 30 | 31 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 32 | static async enter(context: Context) { 33 | throw new Error('Builder not implemented'); 34 | } 35 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 36 | static exit(context: Context): void {} 37 | static readonly elements: readonly string[] = []; 38 | } 39 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/notes.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | This is a note. Since it is outside a clause it is unstyled. 14 | 15 | 16 |

1 This

17 | 18 | Note
This is note 1, but since it is the only note, it is not numbered.
19 | 20 | 21 |

1.1 That

22 | Note 1
This is note 1, should be numbered
23 | Note 2
This is note 2, should also be numbered
24 |
25 |
26 |
-------------------------------------------------------------------------------- /test/baselines/sources/autolinking.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

Autolinking

8 |

Type, type, Type(), type()

9 |

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

10 | 11 |

Lowercase

12 |

strict mode

13 |

%Percent%

14 |

extra spaces

15 |

Await

16 |

Variants

17 |

𝔽(_x_)

18 |
19 | 20 |

Autolinking 2

21 |

lowercase should not autolink. But Lowercase should. But not LowerCase.

22 |

Strict mode shoud link. But Strict Mode should not. Also, strict 23 | mode can be wrapped across lines and contain extra whitespace.

24 |

extra spaces in a dfn should be narrowed to one space.

25 |

%Percent% should autolink.

26 |

Vars to dfns should be vars not dfns: _Lowercase_.

27 |

Also, no autolinks in anchors: Lowercase.

28 |

Similarly, no autolinks for [Await].

29 |

Variants like vOne and vTwo should autolink, including when capitalized as in VOne.

30 | 31 | 1. Non-word-chars AOs still link when invoked, like 𝔽(_x_). 32 | 33 |
34 |

%Array% and %ArrayPrototype% outside of clauses is ok.

35 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/oldids.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

1 c1

15 |

This is some content and such.

16 | 17 |

1.1 c1.1

18 |
19 | 20 |
Table 1
21 |
22 | 23 | Note
24 |

This is a note!

25 |
26 |
27 |
-------------------------------------------------------------------------------- /test/baselines/sources/dfn.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

Intro

8 |

Forward references to dfn work.

9 |

Also terms from ES6 are auto-linked: Lexical Environment, Completion, etc.

10 |
11 | 12 |

dfn

13 | 14 |

The term dfn means the dfn tag. Other mentions of dfn in this clause should not be auto-linked.

15 | 16 |

Terms with ids are called id dfns. Since this dfn has an id, other occurences 17 | of id dfns may autolink.

18 | 19 | 20 |

dfn subclause

21 | 22 |

Dfn should be auto-linked here as well. The linking of dfn is case insensitive. But the header shouldn't auto-link.

23 |

Id dfns should link to #identifiers.

24 |
25 | 26 |

Also terms are auto-linked in algorithms. But not naked abstract ops!

27 | 28 | 29 | 30 | 1. Call the dfn algorithm. 31 | 2. Do something great with dfn. 32 | 3. dfn is now awesome. 33 | 34 | 35 | 36 |

dfn autolink

37 | 38 | 39 | 40 | 1. Call the dfn algorithm. 41 | 2. Do something great with dfn. 42 | 3. dfn is now awesome. 43 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/step-xrefs.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 | 15 |

1 Title

16 | 17 |

You can refer to a step before it occurs in text, like step 2.a.ii.

18 | 19 |
  1. Step.
  2. List:
    1. List:
      1. Step.
      2. Step.
20 | 21 |

You can refer to it after as well, like step 2.a.ii.

22 |
23 |
-------------------------------------------------------------------------------- /test/baselines/sources/algorithm-replacements.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |

Title

9 | 10 |

You can refer to a step in a replacement algorithm before it occurs in text, like step .

11 | 12 |

You can similarly refer to a step in a replacement algorithm which itself which replaces a step in a replacement algorithm, like step .

13 | 14 |

An algorithm can come before the step it replaces.

15 | 16 | 17 | 1. Two a: 18 | 1. Two a i: 19 | 1. Two a i one. 20 | 1. [id="example"] Two a i two. 21 | 22 | 23 |

This is our sample algorithm.

24 | 25 | 1. One. 26 | 1. Two: 27 | 1. [id="to-be-replaced"] Two a. 28 | 29 | 30 |

An algorithm can also come after the step it replaces.

31 | 32 | 33 | 1. Two a: 34 | 1. Two a i: 35 | 1. Two a i one. 36 | 1. [id="example"] Two a i two. 37 | 38 | 39 |

You can even replace steps in replacement algorithms.

40 | 41 | 42 | 1. Two a i two: 43 | 1. Two a i two a. 44 | 1. Two a i two b. 45 | 1. [id="nested-example"] Two a i two c: 46 | 1. Two a i two c i: 47 | 1. Two a i two c i i: 48 | 1. Two a i two c i i i. 49 | 1. Two a i two c i i ii. 50 | 51 |
52 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/imports.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

1 Header

15 |

text

16 |

nonterminal code value.

17 | 18 | 19 |

1.1 Import 3

20 | wtf?? 21 | 22 | A : b 23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 |

2 Import 2

31 |
32 | 33 | 34 |
  1. Ensure we can auto-link to imported aoids: Baz()
35 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/duplicate-productions.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | Prod : 14 | 15 | GMD :: 16 | Test 17 | bar 18 | 19 | 20 | 21 | 22 | Prod : 23 | 24 | GMD :: 25 | Test 26 | bar 27 | 28 | 29 | 30 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/nonterminal-used-before-definition.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

1 Example SDO

15 | 16 | Example : foo 17 | 18 | 19 |
  1. Return false.
20 |
21 | 22 | 23 |

2 Example Grammar

24 |

Syntax

25 | 26 | Example : foo 27 | 28 | 29 |
30 |
-------------------------------------------------------------------------------- /test/lint-tags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let { assertLint, assertLintFree, positioned, lintLocationMarker: M } = require('./utils.js'); 4 | 5 | describe('tags', () => { 6 | it('unknown "emu-" tags', async () => { 7 | await assertLint( 8 | positioned` 9 | ${M}Foo 10 | `, 11 | { 12 | ruleId: 'valid-tags', 13 | nodeType: 'emu-not-a-thing', 14 | message: 'unknown "emu-" tag "emu-not-a-thing"', 15 | }, 16 | ); 17 | }); 18 | 19 | it('oldid', async () => { 20 | await assertLint( 21 | positioned` 22 | 23 |

Example

24 |
25 | `, 26 | { 27 | ruleId: 'valid-tags', 28 | nodeType: 'emu-clause', 29 | message: '"oldid" isn\'t a thing; did you mean "oldids"?', 30 | }, 31 | ); 32 | }); 33 | 34 | it('missing closing tag', async () => { 35 | await assertLint( 36 | positioned` 37 | 38 |

Example

39 | ${M}

some text 40 |

some other text

41 |
42 | `, 43 | { 44 | ruleId: 'missing-closing-tag', 45 | nodeType: 'p', 46 | message: 'element

is missing its closing tag', 47 | }, 48 | ); 49 | }); 50 | 51 | it('negative', async () => { 52 | await assertLintFree(` 53 | 54 |

Example

55 |

some text

56 |
57 |

some other text

58 | 59 | `); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/baselines/sources/duplicate-ids.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

A

8 | 9 | 10 |

Sub A

11 | 12 | 13 | Multiple examples are numbered similar to notes 14 | 15 |
16 |
17 | 18 | 19 |

Section A: Extras

20 | 21 | 22 | 23 | 24 |
Column 1Column 2
ValueValue 2
25 |
26 |
27 | 28 | 29 |

Section A: Extras

30 | 31 | 32 | 33 | 34 | 35 |
Column 1Column 2
ValueValue 2
36 |
37 | 38 | 39 | Multiple examples are numbered similar to notes 40 | 41 |
42 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/example.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 |

Examples outside of clauses aren't processed specially.

14 | 15 | This is an example. 16 | 17 | 18 | 19 |

1 Example Section

20 |
Example (Informative)
21 | Examples inside clauses are numbered and have captions. Captions are optional. 22 |
23 | 24 | 25 |

1.1 Example Subclause

26 |
Example 1 (Informative): An example
27 | Multiple examples are numbered similar to notes 28 |
29 | 30 |
Example 2 (Informative): A second example
31 | So this becomes example 2. 32 |
33 |
34 |
35 | 36 |
-------------------------------------------------------------------------------- /src/Dfn.ts: -------------------------------------------------------------------------------- 1 | import type { PartialBiblioEntry, TermBiblioEntry } from './Biblio'; 2 | import { getKeys } from './Biblio'; 3 | import type { Context } from './Context'; 4 | 5 | import Builder from './Builder'; 6 | 7 | export default class Dfn extends Builder { 8 | static async enter({ spec, node, clauseStack }: Context) { 9 | if (!node.hasAttribute('tabindex')) { 10 | node.setAttribute('tabindex', '-1'); 11 | } 12 | 13 | const parentClause = clauseStack[clauseStack.length - 1]; 14 | if (!parentClause) return; 15 | 16 | const entry: PartialBiblioEntry = { 17 | type: 'term', 18 | term: node.textContent!, 19 | refId: parentClause.id, 20 | }; 21 | 22 | if (node.hasAttribute('id')) { 23 | entry.id = node.id; 24 | } 25 | 26 | if (node.hasAttribute('variants')) { 27 | entry.variants = node 28 | .getAttribute('variants')! 29 | .split(',') 30 | .map(v => v.trim()); 31 | } 32 | 33 | const keys = getKeys(entry as TermBiblioEntry); 34 | const existing = spec.biblio.keysForNamespace(parentClause.namespace); 35 | for (const [index, key] of keys.entries()) { 36 | if (keys.indexOf(key) !== index) { 37 | spec.warn({ 38 | type: 'node', 39 | node, 40 | ruleId: 'duplicate-definition', 41 | message: `${JSON.stringify(key)} is defined more than once in this definition`, 42 | }); 43 | } 44 | if (existing.has(key)) { 45 | spec.warn({ 46 | type: 'node', 47 | node, 48 | ruleId: 'duplicate-definition', 49 | message: `duplicate definition ${JSON.stringify(key)}`, 50 | }); 51 | } 52 | } 53 | 54 | spec.biblio.add(entry, parentClause.namespace); 55 | } 56 | 57 | static elements = ['DFN']; 58 | } 59 | -------------------------------------------------------------------------------- /src/ecmarkup.ts: -------------------------------------------------------------------------------- 1 | import type { BiblioEntry, ExportedBiblio } from './Biblio'; 2 | 3 | import Spec from './Spec'; 4 | import * as utils from './utils'; 5 | import { CancellationToken } from 'prex'; 6 | 7 | export type { Spec, BiblioEntry }; 8 | 9 | export class Boilerplate { 10 | address?: string; 11 | copyright?: string; 12 | license?: string; 13 | } 14 | 15 | export type EcmarkupError = { 16 | ruleId: string; 17 | message: string; 18 | file?: string; 19 | source?: string; 20 | line?: number; 21 | column?: number; 22 | nodeType?: string; 23 | }; 24 | 25 | export interface Options { 26 | status?: 'proposal' | 'draft' | 'standard'; 27 | version?: string; 28 | title?: string; 29 | shortname?: string; 30 | description?: string; 31 | stage?: string | null; 32 | copyright?: boolean; 33 | date?: Date; 34 | location?: string; 35 | maxClauseDepth?: number; 36 | multipage?: boolean; 37 | extraBiblios?: ExportedBiblio[]; 38 | contributors?: string; 39 | toc?: boolean; 40 | oldToc?: boolean; 41 | printable?: boolean; 42 | markEffects?: boolean; 43 | lintSpec?: boolean; 44 | cssOut?: never; 45 | jsOut?: never; 46 | assets?: 'none' | 'inline' | 'external'; 47 | assetsDir?: string; 48 | outfile?: string; 49 | boilerplate?: Boilerplate; 50 | log?: (msg: string) => void; 51 | warn?: (err: EcmarkupError) => void; 52 | committee?: number; 53 | } 54 | 55 | export async function build( 56 | path: string, 57 | fetch: (path: string, token: CancellationToken) => PromiseLike, 58 | opts?: Options, 59 | token = CancellationToken.none, 60 | ): Promise { 61 | const html = await fetch(path, token); 62 | const dom = utils.htmlToDom(html); 63 | const spec = new Spec(path, fetch, dom, opts ?? {}, /*sourceText*/ html, token); 64 | await spec.build(); 65 | return spec; 66 | } 67 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/toc.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
16 | 17 | 18 |

1 Example Link Header

19 |

Example clause contents.

20 |
21 |
-------------------------------------------------------------------------------- /test/baselines/sources/clauses.html: -------------------------------------------------------------------------------- 1 | 8 | 9 |

Intro

10 | 11 |

Sub Intro

12 |
13 |
14 | 15 | 16 | 17 |

Clause Foo(_a_, _b_)

18 | 19 | 20 |

Sub Clause

21 | 22 |
23 |
24 | 25 | 26 |

Explicit number

27 | 28 | 29 |

Automatic number inside explicit number

30 |
31 | 32 | 33 |

Nested explicit number

34 |
35 | 36 | 37 |

Automatic number after explicit number

38 |
39 | 40 | 41 |

Multi-step explicit numbers

42 | 43 |

Multi-step explicit numbers

44 |
45 | 46 | 47 |

Automatic number after explicit number

48 | 49 |

Nested clause after explicit multi-step number

50 |
51 |
52 | 53 | 54 |

Increasing multi-step explicit numbers

55 |
56 |
57 |
58 | 59 | 60 |

Annex

61 | 62 | 63 |

Sub-annex

64 | 65 |
66 |
67 | -------------------------------------------------------------------------------- /src/Toc.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type Clause from './Clause'; 3 | 4 | export default class Toc { 5 | /** @internal */ spec: Spec; 6 | constructor(spec: Spec) { 7 | this.spec = spec; 8 | } 9 | 10 | /** @internal */ 11 | build(maxDepth: number = Infinity) { 12 | if (this.spec.subclauses.length === 0) { 13 | return; 14 | } 15 | 16 | const html = Toc.build(this.spec, { maxDepth }); 17 | const tocContainer = this.spec.doc.createElement('div'); 18 | tocContainer.setAttribute('id', 'toc'); 19 | tocContainer.innerHTML = '

Contents

' + html; 20 | const intro = this.spec.doc.querySelector('emu-intro, emu-clause, emu-annex'); 21 | if (intro && intro.parentNode) { 22 | intro.parentNode.insertBefore(tocContainer, intro); 23 | } 24 | 25 | const bodyClass = this.spec.doc.body.getAttribute('class') || ''; 26 | this.spec.doc.body.setAttribute('class', bodyClass + ' oldtoc'); 27 | } 28 | 29 | static build(level: Spec | Clause, { maxDepth = Infinity, expandy = false } = {}) { 30 | if (maxDepth <= 0) { 31 | return ''; 32 | } 33 | 34 | let html = '
    '; 35 | 36 | level.subclauses.forEach(sub => { 37 | html += '
  1. '; 38 | 39 | if (expandy) { 40 | if (sub.subclauses.length > 0) { 41 | html += '+'; 42 | } else { 43 | html += ''; 44 | } 45 | } 46 | 47 | html += `${sub.getSecnumHTML()}${shorten(sub.titleHTML)}`; 48 | if (sub.subclauses.length > 0) html += Toc.build(sub, { maxDepth: maxDepth - 1, expandy }); 49 | html += '
  2. '; 50 | }); 51 | 52 | html += '
'; 53 | 54 | return html; 55 | } 56 | } 57 | 58 | function shorten(title: string) { 59 | return title.replace('Static Semantics:', 'SS:').replace('Runtime Semantics:', 'RS:'); 60 | } 61 | -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- 1 | # Generating a PDF from ecmarkup 2 | 3 | ## Required frontmatter 4 | 5 | - In order to produce a PDF, the front matter `title`, `shortname`, and `status` are **mandatory**. 6 | 7 | ```yaml 8 | title: Temporal proposal 9 | shortname: Temporal 10 | status: proposal 11 | stage: 3 12 | ``` 13 | 14 | - You can also specify various boilerplate content (see the boilerplate/ directory) For example: 15 | 16 | ```yaml 17 | title: ECMAScript® Language Specification 18 | shortname: ECMA-262 19 | status: draft 20 | boilerplate: 21 | copyright: alternative 22 | ``` 23 | 24 | - If generating a version for submission to the GA, `version` and `date` are mandatory. `date` should reflect the date of the Ecma GA which will ratify the Standard. 25 | 26 | ```yaml 27 | title: ECMAScript® 2025 Language Specification 28 | shortname: ECMA-262 29 | version: 16th Edition 30 | date: 2025-06-25 31 | status: standard 32 | boilerplate: 33 | copyright: alternative 34 | location: https://262.ecma-international.org/16.0/ 35 | ``` 36 | 37 | ## Build and print 38 | 39 | To generate markup for use in PDF conversion, make sure to include the options `--assets`, `--assets-dir`, and `--printable`. If you have images and styles to include, make sure to move them into your assets directory before running `ecmarkup`. For example: 40 | 41 | ```shell 42 | mkdir -p out && \ 43 | cp -R images out && \ 44 | ecmarkup --assets external --assets-dir out --printable spec.html out/index.html 45 | ``` 46 | 47 | Then, from your spec's working directory, run [`prince-books`](https://www.princexml.com/) to generate your PDF. 48 | 49 | ```shell 50 | cd path/to/spec 51 | prince-books --script ./node_modules/ecmarkup/js/print.js out/index.html -o path/to/output.pdf 52 | ``` 53 | 54 | This has been extensively tested with [Prince Books](https://www.princexml.com/books/), built off of Prince 15. Earlier and later editions not guaranteed. CSS rule-specific documentation available in css/print.css. 55 | -------------------------------------------------------------------------------- /src/Example.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type Clause from './Clause'; 3 | import type { Context } from './Context'; 4 | 5 | import Builder from './Builder'; 6 | 7 | export default class Example extends Builder { 8 | /** @internal */ clause: Clause; 9 | /** @internal */ caption: string | null; 10 | /** @internal */ id: string | undefined; 11 | static readonly elements = ['EMU-EXAMPLE'] as const; 12 | 13 | constructor(spec: Spec, node: HTMLElement, clause: Clause) { 14 | super(spec, node); 15 | this.clause = clause; 16 | this.caption = this.node.getAttribute('caption'); 17 | if (this.node.hasAttribute('id')) { 18 | this.id = this.node.getAttribute('id')!; 19 | } 20 | } 21 | 22 | static async enter({ spec, node, clauseStack }: Context) { 23 | const clause = clauseStack[clauseStack.length - 1]; 24 | if (!clause) return; // don't process examples outside of clauses 25 | clause.examples.push(new Example(spec, node, clause)); 26 | } 27 | 28 | build(number?: number) { 29 | if (this.id) { 30 | // biblio is added during the build step as we don't know 31 | // the number at build time. Could probably be fixed. 32 | this.spec.biblio.add({ 33 | type: 'example', 34 | id: this.id, 35 | number: number || 1, 36 | clauseId: this.clause.id, 37 | }); 38 | } 39 | 40 | const ele = this.spec.doc.createElement('figure'); 41 | 42 | ele.append(...this.node.childNodes); 43 | this.node.append(ele); 44 | 45 | let caption = 'Example'; 46 | if (number) { 47 | caption += ' ' + number; 48 | } 49 | 50 | caption += ' (Informative)'; 51 | 52 | if (this.caption) { 53 | caption += ': ' + this.caption; 54 | } 55 | 56 | const captionElem = this.spec.doc.createElement('figcaption'); 57 | captionElem.textContent = caption; 58 | this.node.childNodes[0].insertBefore(captionElem, this.node.childNodes[0].firstChild); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Meta.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type { Context } from './Context'; 3 | 4 | import Builder from './Builder'; 5 | 6 | import { validateEffects, doesEffectPropagateToParent } from './utils'; 7 | import { maybeAddClauseToEffectWorklist } from './Spec'; 8 | 9 | export default class Meta extends Builder { 10 | static elements = ['EMU-META']; 11 | 12 | static async enter({ spec, node, clauseStack }: Context) { 13 | const parent = clauseStack[clauseStack.length - 1] || null; 14 | if (node.hasAttribute('effects') && parent !== null) { 15 | const effects = validateEffects( 16 | spec, 17 | node 18 | .getAttribute('effects')! 19 | .split(',') 20 | .map(e => e.trim()), 21 | node, 22 | ); 23 | for (const effect of effects) { 24 | if (!doesEffectPropagateToParent(node, effect)) { 25 | continue; 26 | } 27 | if (!spec._effectWorklist.has(effect)) { 28 | spec._effectWorklist.set(effect, []); 29 | } 30 | maybeAddClauseToEffectWorklist(effect, parent, spec._effectWorklist.get(effect)!); 31 | } 32 | } 33 | spec._emuMetasToRender.add(node); 34 | } 35 | 36 | static render(spec: Spec, node: HTMLElement) { 37 | // This builder turns tags that aren't removed during effect 38 | // propagation on invocations into s so they are rendered. 39 | if (node.hasAttribute('effects') && spec.opts.markEffects) { 40 | const classNames = node 41 | .getAttribute('effects')! 42 | .split(',') 43 | .map(e => e.trim()) 44 | .map(e => `e-${e}`) 45 | .join(' '); 46 | const span = spec.doc.createElement('span'); 47 | span.setAttribute('class', classNames); 48 | while (node.firstChild) { 49 | span.appendChild(node.firstChild); 50 | } 51 | node.replaceWith(span); 52 | } else { 53 | // Nothing to render, strip it. 54 | node.replaceWith(...node.childNodes); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/lint/rules/if-else-consistency.ts: -------------------------------------------------------------------------------- 1 | import type { Reporter } from '../algorithm-error-reporter-type'; 2 | import type { Seq } from '../../expr-parser'; 3 | import type { OrderedListItemNode, OrderedListNode } from 'ecmarkdown'; 4 | import { offsetToLineAndColumn } from '../../utils'; 5 | 6 | const ruleId = 'if-else-consistency'; 7 | 8 | /* 9 | Checks that `if`/`else` statements are both single-line or both multi-line. 10 | */ 11 | export default function ( 12 | report: Reporter, 13 | step: OrderedListItemNode, 14 | algorithmSource: string, 15 | parsedSteps: Map, 16 | parent: OrderedListNode, 17 | ) { 18 | const stepSeq = parsedSteps.get(step); 19 | if (stepSeq == null) { 20 | return; 21 | } 22 | const firstSeqItem = stepSeq.items[0]; 23 | if (firstSeqItem?.name !== 'text' || !/^(?:If|Else if)\b/.test(firstSeqItem.contents)) { 24 | return; 25 | } 26 | const idx = parent.contents.indexOf(step); 27 | if (idx >= parent.contents.length - 1) { 28 | return; 29 | } 30 | const nextStep = parent.contents[idx + 1]; 31 | const nextSeq = parsedSteps.get(nextStep); 32 | if (nextSeq == null) { 33 | return; 34 | } 35 | const nextFirstSeqitem = nextSeq.items[0]; 36 | if ( 37 | nextFirstSeqitem?.name !== 'text' || 38 | !/^(?:Else|Otherwise)\b/.test(nextFirstSeqitem.contents) 39 | ) { 40 | return; 41 | } 42 | if (step.sublist != null && nextStep.sublist == null) { 43 | const location = offsetToLineAndColumn(algorithmSource, nextFirstSeqitem.location.start.offset); 44 | report({ 45 | ruleId, 46 | ...location, 47 | message: '"Else" steps should be multiline whenever their corresponding "If" is', 48 | }); 49 | } else if (step.sublist == null && nextStep.sublist != null) { 50 | const location = offsetToLineAndColumn(algorithmSource, firstSeqItem.location.start.offset); 51 | report({ 52 | ruleId, 53 | ...location, 54 | message: '"If" steps should be multiline whenever their corresponding "Else" is', 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Figure.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type { Context } from './Context'; 3 | 4 | import Builder from './Builder'; 5 | 6 | export default class Figure extends Builder { 7 | type: string; 8 | number: number; 9 | id: string | null; 10 | isInformative: boolean; 11 | captionElem: HTMLElement | null; 12 | caption: string; 13 | 14 | static elements = ['EMU-FIGURE', 'EMU-TABLE']; 15 | 16 | constructor(spec: Spec, node: HTMLElement) { 17 | super(spec, node); 18 | this.type = node.nodeName.split('-')[1].toLowerCase(); 19 | this.number = ++spec._figureCounts[this.type]; 20 | this.id = node.getAttribute('id'); 21 | 22 | this.isInformative = node.hasAttribute('informative'); 23 | this.captionElem = node.querySelector('emu-caption'); 24 | this.caption = this.type.charAt(0).toUpperCase() + this.type.slice(1) + ' ' + this.number; 25 | 26 | if (this.isInformative) { 27 | this.caption += ' (Informative)'; 28 | } 29 | 30 | if (this.captionElem) { 31 | this.caption += ': ' + this.captionElem.innerHTML; 32 | } else if (node.getAttribute('caption')) { 33 | this.caption += ': ' + node.getAttribute('caption'); 34 | } 35 | 36 | if (this.id) { 37 | spec.biblio.add({ 38 | type: this.type as 'table' | 'figure' | 'example' | 'note', 39 | id: this.id, 40 | number: this.number, 41 | caption: this.caption, 42 | }); 43 | } 44 | } 45 | 46 | static async enter({ spec, node }: Context) { 47 | const figure = new Figure(spec, node); 48 | if (figure.captionElem && figure.captionElem.parentNode) { 49 | figure.captionElem.parentNode.removeChild(figure.captionElem); 50 | } 51 | 52 | const ele = spec.doc.createElement('figure'); 53 | 54 | ele.append(...node.childNodes); 55 | node.append(ele); 56 | 57 | const captionElem = spec.doc.createElement('figcaption'); 58 | captionElem.innerHTML = figure.caption; 59 | node.childNodes[0].insertBefore(captionElem, node.childNodes[0].firstChild); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/baselines/sources/figure.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | this is a figure! 9 | 10 | 11 | 12 | 13 | this is a figure! 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Column 1Column 2
ValueValue 2
ValueValue 2
ValueValue 2
ValueValue 2
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
Column 1Column 2
ValueValue 2
ValueValue 2
ValueValue 2
ValueValue 2
36 |
37 | 38 | 39 | This is the caption 40 | this is a figure! 41 | 42 | 43 | 44 | This is a table 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
Column 1Column 2
ValueValue 2
ValueValue 2
ValueValue 2
ValueValue 2
54 |
55 | 56 | 57 | This is a second table 58 | 59 | 60 | 61 | 62 | 63 | 64 |
Column 1Column 2
ValueValue 2
ValueValue 2
ValueValue 2
ValueValue 2
65 |
66 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/emd-in-grammar.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 | 15 |

1 Example

16 | 17 | Foo :: 18 | Bar 19 | but only if the AbstractOperation of Bar is ≤ variable 20 | 21 | 22 | 23 | Bar :: example 24 | 25 | 26 | 27 | 28 |

1.1 Static Semantics: AbstractOperation

29 | 30 | Bar :: example 31 | 32 | 33 |
  1. Return 0.
34 |
35 |
36 |
-------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "prettier" 6 | ], 7 | "plugins": [ 8 | "prettier", 9 | "@typescript-eslint" 10 | ], 11 | "env": { 12 | "node": true, 13 | "es6": true 14 | }, 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "sourceType": "script", 18 | "ecmaVersion": 2020 19 | }, 20 | "overrides": [ 21 | { 22 | "files": ["js/**/*"], 23 | "env": { 24 | "node": false, 25 | "browser": true, 26 | "es6": true 27 | }, 28 | "rules": { 29 | "no-use-before-define": "off", 30 | "no-redeclare": "off", 31 | "no-unused-vars": "error" 32 | }, 33 | "globals": { 34 | "multipageMap": "readonly", 35 | "usesMultipage": "readonly", 36 | "idToSection": "readonly", 37 | "sdoMap": "readonly", 38 | "biblio": "readonly", 39 | "debounce": "writable", 40 | "menu": "writable", 41 | "referencePane": "writable", 42 | "sdoBox": "writable", 43 | "Toolbox": "writable" 44 | } 45 | }, 46 | { 47 | "files": ["test/**/*"], 48 | "env": { 49 | "mocha": true 50 | } 51 | } 52 | ], 53 | "rules": { 54 | "@typescript-eslint/consistent-type-imports": "error", 55 | "prettier/prettier": "error", 56 | "arrow-body-style": "error", 57 | "prefer-arrow-callback": "error", 58 | "object-shorthand": "error", 59 | "no-inner-declarations": "off", 60 | "no-loss-of-precision": "off", 61 | "consistent-return": "off", 62 | "no-floating-decimal": "error", 63 | "no-self-compare": "error", 64 | "no-throw-literal": "error", 65 | "no-void": "error", 66 | "strict": [ 67 | "error", 68 | "global" 69 | ], 70 | "no-underscore-dangle": "off", 71 | "no-constant-condition": "off", 72 | "camelcase": [ 73 | "error", 74 | { 75 | "properties": "never" 76 | } 77 | ], 78 | "no-empty": "error", 79 | "curly": [ 80 | "error", 81 | "multi-line" 82 | ], 83 | "no-var": "error" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /boilerplate/software-license.html: -------------------------------------------------------------------------------- 1 |

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

2 | 3 |

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

4 | 5 |
    6 |
  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. 7 |
  3. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  4. 8 |
  5. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.
  6. 9 |
10 | 11 |

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

12 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const execSync = require('child_process').execSync; 5 | const execPath = process.execPath.includes(' ') ? `"${process.execPath}"` : process.execPath; 6 | 7 | describe('ecmarkup#cli', function () { 8 | this.timeout(4000); // Increase timeout for CLI tests as startup time can be variable. 9 | 10 | it('exits cleanly on success', () => { 11 | execSync(`${execPath} ./bin/ecmarkup.js test/baselines/sources/example.html`, { 12 | encoding: 'utf8', 13 | }); 14 | }); 15 | 16 | it('exits cleanly on warning', () => { 17 | execSync(`${execPath} ./bin/ecmarkup.js test/baselines/sources/duplicate-ids.html`, { 18 | encoding: 'utf8', 19 | stdio: 'ignore', 20 | }); 21 | }); 22 | 23 | it('exits with an error on error', () => { 24 | assert.throws(() => { 25 | execSync(`${execPath} ./bin/ecmarkup.js test/baselines/sources/malformed.bad.html`, { 26 | encoding: 'utf8', 27 | stdio: 'ignore', 28 | }); 29 | }); 30 | }); 31 | 32 | it('exits with an error on warning when using --strict', () => { 33 | assert.throws(() => { 34 | execSync(`${execPath} ./bin/ecmarkup.js --strict test/baselines/sources/duplicate-ids.html`, { 35 | encoding: 'utf8', 36 | stdio: 'ignore', 37 | }); 38 | }); 39 | }); 40 | }); 41 | 42 | describe('emu-format --check', function () { 43 | this.timeout(4000); 44 | 45 | it('exits cleanly if the file needs no formatting', () => { 46 | execSync(`${execPath} ./bin/emu-format.js --check test/format-good.html`, { 47 | encoding: 'utf8', 48 | }); 49 | }); 50 | 51 | it('exits with an error if the file needs formatting', () => { 52 | assert.throws(() => { 53 | execSync(`${execPath} ./bin/emu-format.js --check test/format-bad.html`, { 54 | encoding: 'utf8', 55 | }); 56 | }); 57 | }); 58 | 59 | it('exits with an error if any files in the list need formatting', () => { 60 | assert.throws(() => { 61 | execSync( 62 | `${execPath} ./bin/emu-format.js --check test/format-good.html test/format-bad.html`, 63 | { 64 | encoding: 'utf8', 65 | }, 66 | ); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/Import.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | 3 | import * as utils from './utils'; 4 | import * as path from 'path'; 5 | import type { JSDOM } from 'jsdom'; 6 | 7 | export type Import = { 8 | importLocation: string; 9 | relativeRoot: string; 10 | source: string; 11 | }; 12 | 13 | export async function buildImports(spec: Spec, importNode: EmuImportElement, root: string) { 14 | const href = importNode.getAttribute('href'); 15 | if (!href) throw new Error('Import missing href attribute.'); 16 | const importPath = path.join(root, href); 17 | const relativeRoot = path.dirname(importPath); 18 | 19 | const html = await spec.fetch(importPath); 20 | 21 | spec.imports.push({ importLocation: importPath, relativeRoot, source: html }); 22 | 23 | const importDom = utils.htmlToDom(html); 24 | importNode.dom = importDom; 25 | importNode.source = html; 26 | importNode.importPath = importPath; 27 | 28 | const importDoc = importDom.window.document; 29 | 30 | // clone this list so we can walk it after the replaceWith call 31 | const importedNodes = [...importDoc.body.childNodes]; 32 | const frag = spec.doc.createDocumentFragment(); 33 | 34 | for (let i = 0; i < importedNodes.length; i++) { 35 | const importedNode = spec.doc.adoptNode(importedNodes[i]); 36 | importedNodes[i] = importedNode; 37 | frag.appendChild(importedNode); 38 | 39 | spec.topLevelImportedNodes.set(importedNode, importNode); 40 | } 41 | 42 | importNode.replaceWith(frag); 43 | 44 | for (let i = 0; i < importedNodes.length; i++) { 45 | const importedNode = importedNodes[i]; 46 | if (importedNode.nodeType === 1 /* Node.ELEMENT_NODE */) { 47 | const importedImports = [ 48 | ...(importedNode as HTMLElement).querySelectorAll('emu-import'), 49 | // we have to do this because querySelectorAll can't return its `this` 50 | ...((importedNode as HTMLElement).tagName === 'EMU-IMPORT' ? [importedNode] : []), 51 | ]; 52 | for (const childImport of importedImports) { 53 | await buildImports(spec, childImport as EmuImportElement, relativeRoot); 54 | } 55 | } 56 | } 57 | } 58 | 59 | export interface EmuImportElement extends HTMLElement { 60 | href: string; 61 | dom: JSDOM; 62 | source?: string; 63 | importPath?: string; 64 | } 65 | -------------------------------------------------------------------------------- /src/NonTerminal.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type { Context } from './Context'; 3 | import type { BiblioEntry } from './Biblio'; 4 | 5 | import Builder from './Builder'; 6 | 7 | export default class NonTerminal extends Builder { 8 | /** @internal */ params: string | null; 9 | /** @internal */ optional: boolean; 10 | /** @internal */ namespace: string; 11 | /** @internal */ entry?: BiblioEntry; 12 | 13 | static readonly elements = ['EMU-NT'] as const; 14 | 15 | constructor(spec: Spec, node: HTMLElement, namespace: string) { 16 | super(spec, node); 17 | 18 | this.params = node.getAttribute('params'); 19 | this.optional = node.hasAttribute('optional'); 20 | this.namespace = namespace; 21 | } 22 | 23 | static async enter({ spec, node, clauseStack }: Context) { 24 | const clause = clauseStack[clauseStack.length - 1]; 25 | const namespace = clause ? clause.namespace : spec.namespace; 26 | const nt = new NonTerminal(spec, node, namespace); 27 | spec._ntRefs.push(nt); 28 | 29 | if (spec.opts.lintSpec && spec.locate(node) != null && !node.hasAttribute('example')) { 30 | const clause = clauseStack[clauseStack.length - 1]; 31 | const namespace = clause ? clause.namespace : spec.namespace; 32 | spec._ntStringRefs = spec._ntStringRefs.concat({ 33 | name: node.textContent!, 34 | loc: { line: 1, column: 1 }, 35 | node, 36 | namespace, 37 | }); 38 | } 39 | } 40 | 41 | build() { 42 | const name = this.node.textContent!; 43 | // const id = 'prod-' + name; 44 | const entry = this.spec.biblio.byProductionName(name, this.namespace); 45 | 46 | if (entry) { 47 | this.node.innerHTML = '' + name + ''; 48 | this.entry = entry; 49 | } else { 50 | this.node.innerHTML = name; 51 | } 52 | let modifiers = ''; 53 | 54 | if (this.params) { 55 | modifiers += '[' + this.params + ']'; 56 | } 57 | 58 | if (this.optional) { 59 | modifiers += 'opt'; 60 | } 61 | 62 | if (modifiers.length > 0) { 63 | const el = this.spec.doc.createElement('emu-mods'); 64 | el.innerHTML = modifiers; 65 | this.node.appendChild(el); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /boilerplate/standard-copyright.html: -------------------------------------------------------------------------------- 1 |

COPYRIGHT NOTICE

2 | 3 |

© !YEAR! Ecma International

4 | 5 |

By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions.

6 | 7 |

This document may be copied, published and distributed to others, and certain derivative works of it may be prepared, copied, published, and distributed, in whole or in part, provided that the above copyright notice and this Copyright License and Disclaimer are included on all such copies and derivative works. The only derivative works that are permissible under this Copyright License and Disclaimer are:

8 | 9 |

(i) works which incorporate all or portion of this document for the purpose of providing commentary or explanation (such as an annotated version of the document),

10 | 11 |

(ii) works which incorporate all or portion of this document for the purpose of incorporating features that provide accessibility,

12 | 13 |

(iii) translations of this document into languages other than English and into different formats and

14 | 15 |

(iv) works by making use of this specification in standard conformant products by implementing (e.g. by copy and paste wholly or partly) the functionality therein.

16 | 17 |

However, the content of this document itself may not be modified in any way, including by removing the copyright notice or references to Ecma International, except as required to translate it into languages other than English or into a different format.

18 | 19 |

The official version of an Ecma International document is the English language version on the Ecma International website. In the event of discrepancies between a translated version and the official version, the official version shall govern.

20 | 21 |

The limited permissions granted above are perpetual and will not be revoked by Ecma International or its successors or assigns.

22 | 23 |

This document and the information contained herein is provided on an “AS IS” basis and ECMA INTERNATIONAL DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY OWNERSHIP RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.

24 | -------------------------------------------------------------------------------- /boilerplate/alternative-copyright.html: -------------------------------------------------------------------------------- 1 | 58 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/multipage.html/multipage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 |
12 |
    13 |
  • Toggle shortcuts help?
  • 14 |
  • Toggle "can call user code" annotationsu
  • 15 |
  • Navigate to/from multipagem
  • 16 |
  • Jump to search box/
  • 17 |
  • Toggle pinning of the current clausep
  • 18 |
  • Jump to the nth pin1-9
  • 19 |
  • Jump to the 10th pin0
  • 20 |
  • Jump to the most recent link target`
  • 21 |
22 |

Intro

23 |

Some text.

24 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/code.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 | 15 |

1 Test Clause

16 |

Increases indentaion level.

17 |
global1.Zone.current; // succeeds
18 | global2.Zone.current; // throws: currentRealm = realm1; thisRealm = functionRealm = realm2
19 | 
20 | const getter1 = Object.getOwnPropertyDescriptor(global1.Zone, "current").get;
21 | const getter2 = Object.getOwnPropertyDescriptor(global2.Zone, "current").get;
22 | 
23 | getter1.call(global2.Zone); // throws: currentRealm = functionRealm = realm1; thisRealm = realm2
24 | getter2.call(global1.Zone); // throws: currentRealm = thisRealm = realm1; functionRealm = realm2
25 |
26 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/algorithm-replacements.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 | 15 |

1 Title

16 | 17 |

You can refer to a step in a replacement algorithm before it occurs in text, like step 2.a.i.2.

18 | 19 |

You can similarly refer to a step in a replacement algorithm which itself which replaces a step in a replacement algorithm, like step 2.a.i.2.c.

20 | 21 |

An algorithm can come before the step it replaces.

22 | 23 |
  1. Two a:
    1. Two a i:
      1. Two a i one.
      2. Two a i two.
24 | 25 |

This is our sample algorithm.

26 |
  1. One.
  2. Two:
    1. Two a.
27 | 28 |

An algorithm can also come after the step it replaces.

29 | 30 |
  1. Two a:
    1. Two a i:
      1. Two a i one.
      2. Two a i two.
31 | 32 |

You can even replace steps in replacement algorithms.

33 | 34 |
  1. Two a i two:
    1. Two a i two a.
    2. Two a i two b.
    3. Two a i two c:
      1. Two a i two c i:
        1. Two a i two c i i:
          1. Two a i two c i i i.
          2. Two a i two c i i ii.
35 |
36 |
-------------------------------------------------------------------------------- /src/Eqn.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from './Context'; 2 | 3 | import Builder from './Builder'; 4 | import * as emd from 'ecmarkdown'; 5 | import * as utils from './utils'; 6 | 7 | export default class Eqn extends Builder { 8 | // @ts-ignore 9 | private constructor() { 10 | throw new Error('not actually constructible'); 11 | } 12 | 13 | static async enter(context: Context) { 14 | const { spec, node, clauseStack } = context; 15 | 16 | const aoid = node.getAttribute('aoid'); 17 | if (aoid) { 18 | const existing = spec.biblio.keysForNamespace(spec.namespace); 19 | if (existing.has(aoid)) { 20 | spec.warn({ 21 | type: 'attr-value', 22 | attr: 'aoid', 23 | node, 24 | ruleId: 'duplicate-definition', 25 | message: `duplicate definition ${JSON.stringify(aoid)}`, 26 | }); 27 | } 28 | 29 | const id = node.getAttribute('id'); 30 | if (id) { 31 | spec.biblio.add({ 32 | type: 'op', 33 | aoid, 34 | id, 35 | signature: null, 36 | effects: [], 37 | }); 38 | } else { 39 | const clause = clauseStack[clauseStack.length - 1]; 40 | const clauseId = clause ? clause.id : ''; // TODO: no eqns outside of clauses, eh? 41 | spec.biblio.add({ 42 | type: 'op', 43 | aoid, 44 | refId: clauseId, 45 | signature: null, 46 | effects: [], 47 | }); 48 | } 49 | } 50 | 51 | let contents; 52 | try { 53 | contents = emd.fragment(node.innerHTML); 54 | } catch (e: any) { 55 | utils.warnEmdFailure(spec.warn, node, e); 56 | node.innerHTML = utils.wrapEmdFailure(node.innerHTML); 57 | return; 58 | } 59 | 60 | if (utils.shouldInline(node)) { 61 | const classString = node.getAttribute('class'); 62 | let classes: string[]; 63 | 64 | if (classString) { 65 | classes = classString.split(' '); 66 | } else { 67 | classes = []; 68 | } 69 | 70 | if (classes.indexOf('inline') === -1) { 71 | node.setAttribute('class', classes.concat(['inline']).join(' ')); 72 | } 73 | } else { 74 | contents = 75 | '
' + 76 | contents 77 | .split(/\r?\n/g) 78 | .filter(s => s.trim().length > 0) 79 | .join('
') + 80 | '
'; 81 | } 82 | 83 | node.innerHTML = contents; 84 | } 85 | 86 | static elements = ['EMU-EQN']; 87 | } 88 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/eqn.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
    5 |
  • Toggle shortcuts help?
  • 6 |
  • Toggle "can call user code" annotationsu
  • 7 | 8 |
  • Jump to search box/
  • 9 |
  • Toggle pinning of the current clausep
  • 10 |
  • Jump to the nth pin1-9
  • 11 |
  • Jump to the 10th pin0
  • 12 |
  • Jump to the most recent link target`
  • 13 |
14 | 15 |

1 Header

16 |

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

17 |
  1. Return Value(val);
18 | 19 |
Value2(t)
= DateValue(t) if Type(t) is string
= t
20 | 21 |
DateValue(t)
= 0 if t = 0
= 1 if t = 1
= 2
22 | 23 |
Value(t)
= DateValue(t) if Type(t) is string
= t
24 | 25 |
  1. Return Value(val);
26 | 27 |

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

28 |
29 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/multipage.html/multipage/second.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 |
12 |
    13 |
  • Toggle shortcuts help?
  • 14 |
  • Toggle "can call user code" annotationsu
  • 15 |
  • Navigate to/from multipagem
  • 16 |
  • Jump to search box/
  • 17 |
  • Toggle pinning of the current clausep
  • 18 |
  • Jump to the nth pin1-9
  • 19 |
  • Jump to the 10th pin0
  • 20 |
  • Jump to the most recent link target`
  • 21 |
22 |

1 Second Clause

23 |

A cross-section reference: Alg.

24 |

A relative image: .

25 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/multipage.html/multipage/third.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 |
12 |
    13 |
  • Toggle shortcuts help?
  • 14 |
  • Toggle "can call user code" annotationsu
  • 15 |
  • Navigate to/from multipagem
  • 16 |
  • Jump to search box/
  • 17 |
  • Toggle pinning of the current clausep
  • 18 |
  • Jump to the nth pin1-9
  • 19 |
  • Jump to the 10th pin0
  • 20 |
  • Jump to the most recent link target`
  • 21 |
22 |

2 Third Clause

23 |

Some text.

24 | 25 |

2.1 Algorithm

26 |

Pretend there's an algorithm here.

27 |
28 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/boilerplate-all.html: -------------------------------------------------------------------------------- 1 | 2 | test title!
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |

test title!

13 | 14 | 15 |

Copyright & Software License

16 |

Tet Corporation

17 |

2 Hammarskjöld Plaza

18 |

New York City, New York

19 |

Tel: 123-456-7890

20 |

Fax: 123-456-7890

21 |

Web: http://www.tet-corporation.com/

22 | 23 | 26 |

Software License

27 |

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

28 | 29 |

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

30 | 31 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

32 | 33 |
-------------------------------------------------------------------------------- /src/lint/collect-tag-diagnostics.ts: -------------------------------------------------------------------------------- 1 | import type { default as Spec, Warning } from '../Spec'; 2 | 3 | const knownEmuTags = new Set([ 4 | 'emu-meta', 5 | 'emu-import', 6 | 'emu-example', 7 | 'emu-intro', 8 | 'emu-clause', 9 | 'emu-annex', 10 | 'emu-biblio', 11 | 'emu-xref', 12 | 'emu-prodref', 13 | 'emu-not-ref', 14 | 'emu-note', 15 | 'emu-eqn', 16 | 'emu-table', 17 | 'emu-figure', 18 | 'emu-caption', 19 | 'emu-grammar', 20 | 'emu-alg', 21 | 'emu-var', 22 | 'emu-val', 23 | 'emu-production', 24 | 'emu-rhs', 25 | 'emu-nt', 26 | 'emu-t', 27 | 'emu-gann', 28 | 'emu-gprose', 29 | 'emu-gmod', 30 | ]); 31 | 32 | // https://html.spec.whatwg.org/multipage/syntax.html#void-elements 33 | const voidElements = new Set([ 34 | 'area', 35 | 'base', 36 | 'br', 37 | 'col', 38 | 'embed', 39 | 'hr', 40 | 'img', 41 | 'input', 42 | 'link', 43 | 'meta', 44 | 'param', 45 | 'source', 46 | 'track', 47 | 'wbr', 48 | ]); 49 | 50 | export function collectTagDiagnostics( 51 | report: (e: Warning) => void, 52 | spec: Spec, 53 | document: Document, 54 | ) { 55 | const lintWalker = document.createTreeWalker(document.body, 1 /* elements */); 56 | function visit() { 57 | const node: Element = lintWalker.currentNode as Element; 58 | const name = node.tagName.toLowerCase(); 59 | 60 | if (name.startsWith('emu-') && !knownEmuTags.has(name)) { 61 | report({ 62 | type: 'node', 63 | ruleId: 'valid-tags', 64 | message: `unknown "emu-" tag "${name}"`, 65 | node, 66 | }); 67 | } 68 | 69 | if (node.hasAttribute('oldid')) { 70 | report({ 71 | type: 'attr', 72 | attr: 'oldid', 73 | ruleId: 'valid-tags', 74 | message: `"oldid" isn't a thing; did you mean "oldids"?`, 75 | node, 76 | }); 77 | } 78 | 79 | if (!voidElements.has(name)) { 80 | const location = spec.locate(node); 81 | if (location != null && location.endTag == null) { 82 | report({ 83 | type: 'node', 84 | ruleId: 'missing-closing-tag', 85 | message: `element <${name}> is missing its closing tag`, 86 | node, 87 | }); 88 | } 89 | } 90 | 91 | const firstChild = lintWalker.firstChild(); 92 | if (firstChild) { 93 | while (true) { 94 | visit(); 95 | const next = lintWalker.nextSibling(); 96 | if (!next) break; 97 | } 98 | lintWalker.parentNode(); 99 | } 100 | } 101 | visit(); 102 | } 103 | -------------------------------------------------------------------------------- /test/baselines/sources/namespaces.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

Intro

8 | 9 | Foo :: 10 | `bar` MainProd `baz` 11 | 12 |
13 | 14 |

Clause 1

15 | 16 | Foo :: 17 | `bar` MainProd `baz` 18 | 19 | MainProd :: 20 | Foo 21 | 22 | 23 |

Clause 1.1

24 | 25 | 26 | Foo :: 27 | `bar` MainProd `baz` 28 | 29 | 30 | 31 | 32 | 33 |

SomeAlg

34 |
35 | 36 |

SomeAlg does things.

37 |

Can still xref clauses inside of namespaces: .

38 |

Can xref prods in namespaces: .

39 |
40 |
41 | 42 |

Annex

43 | 44 | Foo :: 45 | `bar` MainProd `baz` 46 | 47 | 48 | 49 | 50 |

SomeAlg

51 |
52 | 53 | 54 | 55 | 56 |

Annex 1.2

57 |

SomeAlg should link to #annex12. |Foo| should link to the production in #annex1.

58 |
59 | 60 |

SomeAlg does things.

61 | 62 | 1. Let _x_ be the result of SomeAlg. 63 | 64 |
65 | 66 | 67 |

Annex 2

68 | 69 | 70 |

SomeAlg

71 |
72 | 73 | MainProd :: 74 | Foo 75 | 76 |

SomeAlg does things.

77 |
78 | -------------------------------------------------------------------------------- /src/formatter/header.ts: -------------------------------------------------------------------------------- 1 | import { printSimpleParamList } from '../header-parser'; 2 | import type { Param, ParsedHeaderOrFailure } from '../header-parser'; 3 | import { LineBuilder } from './line-builder'; 4 | 5 | function printTypedParam(param: Param, optional: boolean) { 6 | let p = (optional ? 'optional ' : '') + param.name + ': ' + (param.type ?? 'unknown') + ','; 7 | if (param.wrappingTag) { 8 | p = `<${param.wrappingTag}>${p}`; 9 | } 10 | return p; 11 | } 12 | 13 | function ensureUnderscores(param: Param) { 14 | if (!/^[a-zA-Z0-9]+$/.test(param.name)) { 15 | return param; 16 | } 17 | return { 18 | ...param, 19 | name: '_' + param.name + '_', 20 | }; 21 | } 22 | 23 | export function printHeader( 24 | parseResult: ParsedHeaderOrFailure & { type: 'single-line' | 'multi-line' }, 25 | clauseType: string | null, 26 | indent: number, 27 | ): LineBuilder { 28 | /* eslint-disable prefer-const */ 29 | let { 30 | type, 31 | wrappingTag, 32 | prefix, 33 | name, 34 | params, 35 | optionalParams, 36 | returnType, 37 | // errors is already handled 38 | } = parseResult; 39 | /* eslint-enable prefer-const */ 40 | 41 | const multiline = type === 'multi-line' && (params.length > 0 || optionalParams.length > 0); 42 | 43 | const result = new LineBuilder(indent); 44 | if (multiline) { 45 | result.firstLineIsPartial = false; 46 | } 47 | if (wrappingTag !== null) { 48 | result.appendText(`<${wrappingTag}>`); 49 | } 50 | if (prefix !== null) { 51 | result.appendText(prefix + ' '); 52 | } 53 | result.appendText(name); 54 | 55 | params = params.map(ensureUnderscores); 56 | optionalParams = optionalParams.map(ensureUnderscores); 57 | 58 | if ( 59 | clauseType === 'sdo' && 60 | params.length === 0 && 61 | optionalParams.length === 0 && 62 | returnType === null 63 | ) { 64 | // do not print a parameter list 65 | } else if (!multiline) { 66 | result.appendText(' ' + printSimpleParamList(params, optionalParams)); 67 | } else { 68 | result.appendText(' ('); 69 | ++result.indent; 70 | for (const param of params) { 71 | result.appendLine(printTypedParam(param, false)); 72 | } 73 | for (const param of optionalParams) { 74 | result.appendLine(printTypedParam(param, true)); 75 | } 76 | --result.indent; 77 | result.appendText(')'); 78 | } 79 | if (returnType !== null && returnType !== '') { 80 | result.appendText(': ' + returnType); 81 | } 82 | if (wrappingTag !== null) { 83 | result.appendText(``); 84 | } 85 | if (multiline) { 86 | result.linebreak(); 87 | } 88 | 89 | return result; 90 | } 91 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/duplicate-ids.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

1 A

15 | 16 | 17 |

1.1 Sub A

18 | 19 |
Example (Informative): An example
20 | Multiple examples are numbered similar to notes 21 |
22 |
23 |
24 | 25 | 26 |

2 Section A: Extras

27 |
Table 1 (Informative): A Table Of Stuff
28 | 29 | 30 | 31 |
Column 1Column 2
ValueValue 2
32 |
33 |
34 | 35 | 36 |

3 Section A: Extras

37 | 38 |
Table 2 (Informative): A Table Of Stuff
39 | 40 | 41 | 42 |
Column 1Column 2
ValueValue 2
43 |
44 | 45 |
Example (Informative): An example
46 | Multiple examples are numbered similar to notes 47 |
48 |
49 |
-------------------------------------------------------------------------------- /js/sdoMap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | let sdoBox = { 3 | init() { 4 | this.$alternativeId = null; 5 | this.$outer = document.createElement('div'); 6 | this.$outer.classList.add('toolbox-container'); 7 | this.$container = document.createElement('div'); 8 | this.$container.classList.add('toolbox'); 9 | this.$displayLink = document.createElement('a'); 10 | this.$displayLink.setAttribute('href', '#'); 11 | this.$displayLink.textContent = 'Syntax-Directed Operations'; 12 | this.$displayLink.addEventListener('click', e => { 13 | e.preventDefault(); 14 | e.stopPropagation(); 15 | referencePane.showSDOs(sdoMap[this.$alternativeId] || {}, this.$alternativeId); 16 | }); 17 | this.$container.appendChild(this.$displayLink); 18 | this.$outer.appendChild(this.$container); 19 | document.body.appendChild(this.$outer); 20 | }, 21 | 22 | activate(el) { 23 | clearTimeout(this.deactiveTimeout); 24 | Toolbox.deactivate(); 25 | this.$alternativeId = el.id; 26 | let numSdos = Object.keys(sdoMap[this.$alternativeId] || {}).length; 27 | this.$displayLink.textContent = 'Syntax-Directed Operations (' + numSdos + ')'; 28 | this.$outer.classList.add('active'); 29 | let top = el.offsetTop - this.$outer.offsetHeight; 30 | let left = el.offsetLeft + 50 - 10; // 50px = padding-left(=75px) + text-indent(=-25px) 31 | this.$outer.setAttribute('style', 'left: ' + left + 'px; top: ' + top + 'px'); 32 | if (top < document.body.scrollTop) { 33 | this.$container.scrollIntoView(); 34 | } 35 | }, 36 | 37 | deactivate() { 38 | clearTimeout(this.deactiveTimeout); 39 | this.$outer.classList.remove('active'); 40 | }, 41 | }; 42 | 43 | document.addEventListener('DOMContentLoaded', () => { 44 | if (typeof sdoMap == 'undefined') { 45 | console.error('could not find sdo map'); 46 | return; 47 | } 48 | sdoBox.init(); 49 | 50 | let insideTooltip = false; 51 | sdoBox.$outer.addEventListener('pointerenter', () => { 52 | insideTooltip = true; 53 | }); 54 | sdoBox.$outer.addEventListener('pointerleave', () => { 55 | insideTooltip = false; 56 | sdoBox.deactivate(); 57 | }); 58 | 59 | sdoBox.deactiveTimeout = null; 60 | [].forEach.call(document.querySelectorAll('emu-grammar[type=definition] emu-rhs'), node => { 61 | node.addEventListener('pointerenter', function () { 62 | sdoBox.activate(this); 63 | }); 64 | 65 | node.addEventListener('pointerleave', () => { 66 | sdoBox.deactiveTimeout = setTimeout(() => { 67 | if (!insideTooltip) { 68 | sdoBox.deactivate(); 69 | } 70 | }, 500); 71 | }); 72 | }); 73 | 74 | document.addEventListener( 75 | 'keydown', 76 | debounce(e => { 77 | if (e.code === 'Escape') { 78 | sdoBox.deactivate(); 79 | } 80 | }), 81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/multipage.html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
    8 |
  • Toggle shortcuts help?
  • 9 |
  • Toggle "can call user code" annotationsu
  • 10 |
  • Navigate to/from multipagem
  • 11 |
  • Jump to search box/
  • 12 |
  • Toggle pinning of the current clausep
  • 13 |
  • Jump to the nth pin1-9
  • 14 |
  • Jump to the 10th pin0
  • 15 |
  • Jump to the most recent link target`
  • 16 |
20 | 21 | 22 |

Intro

23 |

Some text.

24 |
25 | 26 | 27 |

1 Second Clause

28 |

A cross-section reference: Alg.

29 |

A relative image: .

30 |
31 | 32 | 33 |

2 Third Clause

34 |

Some text.

35 | 36 |

2.1 Algorithm

37 |

Pretend there's an algorithm here.

38 |
39 |
40 |
-------------------------------------------------------------------------------- /js/superscripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Update superscripts to not suffer misinterpretation when copied and pasted as plain text. 4 | // For example, 5 | // * Replace `103` with 6 | // `103` 7 | // so it gets pasted as `10**3` rather than `103`. 8 | // * Replace `10-x` with 9 | // `10-x` 10 | // so it gets pasted as `10**-x` rather than `10-x`. 11 | // * Replace `2a + 1` with 12 | // `2**(a + 1)` 13 | // so it gets pasted as `2**(a + 1)` rather than `2a + 1`. 14 | 15 | function makeExponentPlainTextSafe(sup) { 16 | // Change a only if it appears to be an exponent: 17 | // * text-only and contains only mathematical content (not e.g. `1st`) 18 | // * contains only s and internal links (e.g. 19 | // `2(_y_)`) 20 | const isText = [...sup.childNodes].every(node => node.nodeType === 3); 21 | const text = sup.textContent; 22 | if (isText) { 23 | if (!/^[0-9. 𝔽ℝℤ()=*×/÷±+\u2212-]+$/u.test(text)) { 24 | return; 25 | } 26 | } else { 27 | if (sup.querySelector('*:not(var, emu-xref, :scope emu-xref a)')) { 28 | return; 29 | } 30 | } 31 | 32 | let prefix = '**'; 33 | let suffix = ''; 34 | 35 | // Add wrapping parentheses unless they are already present 36 | // or this is a simple (possibly signed) integer or single-variable exponent. 37 | const skipParens = 38 | /^[±+\u2212-]?(?:[0-9]+|\p{ID_Start}\p{ID_Continue}*)$/u.test(text) || 39 | // Split on parentheses and remember them; the resulting parts must 40 | // start and end empty (i.e., with open/close parentheses) 41 | // and increase depth to 1 only at the first parenthesis 42 | // to e.g. wrap `(a+1)*(b+1)` but not `((a+1)*(b+1))`. 43 | text 44 | .trim() 45 | .split(/([()])/g) 46 | .reduce((depth, s, i, parts) => { 47 | if (s === '(') { 48 | return depth > 0 || i === 1 ? depth + 1 : NaN; 49 | } else if (s === ')') { 50 | return depth > 0 ? depth - 1 : NaN; 51 | } else if (s === '' || (i > 0 && i < parts.length - 1)) { 52 | return depth; 53 | } 54 | return NaN; 55 | }, 0) === 0; 56 | if (!skipParens) { 57 | prefix += '('; 58 | suffix += ')'; 59 | } 60 | 61 | sup.insertAdjacentHTML('beforebegin', ``); 62 | if (suffix) { 63 | sup.insertAdjacentHTML('afterend', ``); 64 | } 65 | } 66 | 67 | document.addEventListener('DOMContentLoaded', () => { 68 | document.querySelectorAll('sup:not(.text)').forEach(sup => { 69 | makeExponentPlainTextSafe(sup); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/Note.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type Clause from './Clause'; 3 | import type { Context } from './Context'; 4 | 5 | import Builder from './Builder'; 6 | 7 | export default class Note extends Builder { 8 | /** @internal */ clause: Clause; 9 | /** @internal */ id?: string; 10 | /** @internal */ type: string; // normal, editor 11 | static readonly elements = ['EMU-NOTE'] as const; 12 | 13 | constructor(spec: Spec, node: HTMLElement, clause: Clause) { 14 | super(spec, node); 15 | this.clause = clause; 16 | if (this.node.hasAttribute('type')) { 17 | this.type = this.node.getAttribute('type') as string; 18 | } else { 19 | this.type = 'normal'; 20 | } 21 | 22 | if (this.node.hasAttribute('id')) { 23 | this.id = node.getAttribute('id')!; 24 | } 25 | } 26 | 27 | static async enter({ spec, node, clauseStack }: Context) { 28 | const clause = clauseStack[clauseStack.length - 1]; 29 | 30 | // TODO reconsider 31 | if (!clause) return; // do nothing with top-level note 32 | 33 | const note = new Note(spec, node, clause); 34 | if (note.type === 'editor') { 35 | clause.editorNotes.push(note); 36 | } else { 37 | clause.notes.push(note); 38 | } 39 | } 40 | 41 | build(number?: number) { 42 | if (this.id) { 43 | // biblio is added during the build step as we don't know 44 | // the number at build time. Could probably be fixed. 45 | this.spec.biblio.add({ 46 | type: 'note', 47 | id: this.id, 48 | number: number || 1, 49 | clauseId: this.clause.id, 50 | }); 51 | } 52 | 53 | const noteContentContainer = this.spec.doc.createElement('div'); 54 | noteContentContainer.setAttribute('class', 'note-contents'); 55 | 56 | while (this.node.childNodes.length > 0) { 57 | noteContentContainer.appendChild(this.node.childNodes[0]); 58 | } 59 | 60 | this.node.appendChild(noteContentContainer); 61 | 62 | const noteSpan = this.spec.doc.createElement('span'); 63 | noteSpan.setAttribute('class', 'note'); 64 | let label = ''; 65 | 66 | if (this.type === 'normal') { 67 | label = 'Note'; 68 | } else if (this.type === 'editor') { 69 | label = "Editor's Note"; 70 | } else { 71 | this.spec.warn({ 72 | type: 'attr-value', 73 | attr: 'type', 74 | ruleId: 'invalid-note', 75 | message: `unknown note type ${JSON.stringify(this.type)}`, 76 | node: this.node, 77 | }); 78 | } 79 | 80 | if (number !== undefined) { 81 | label += ' ' + number; 82 | } 83 | 84 | if (this.id) { 85 | // create link to note 86 | noteSpan.innerHTML = `${label}`; 87 | } else { 88 | // just text 89 | noteSpan.textContent = label; 90 | } 91 | 92 | this.node.insertBefore(noteSpan, noteContentContainer); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/optional-parts.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | This is a note. Since it is outside a clause it is unstyled. 14 | 15 | 16 |

1 Definitions of Normative Optional etc

17 |

Normative Optional is a defined term, as is Legacy. For comparison, "Deprecated" is not.

18 |
19 | 20 | 21 |

2 Example Normative Optional Clause

22 |

This clause is normative optional.

23 |
24 | 25 | 26 |

3 Example Legacy Clause

27 |

This clause is legacy.

28 |
29 | 30 | 31 |

4 Example algorithm with marked steps.

32 |
  1. This step is required.
  2. This step is optional.
  3. This step is legacy.
  4. This step is only legacy, but has substeps.
    1. This is a substep.
  5. Deprecated
    This step is optional.
  6. This step is required.
33 |
-------------------------------------------------------------------------------- /src/ProdRef.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from './Context'; 2 | import type Spec from './Spec'; 3 | 4 | import Builder from './Builder'; 5 | import { shouldInline } from './utils'; 6 | 7 | export default class ProdRef extends Builder { 8 | /** @internal */ public namespace: string; 9 | /** @internal */ public name: string; 10 | 11 | static readonly elements = ['EMU-PRODREF'] as const; 12 | 13 | constructor(spec: Spec, node: HTMLElement, namespace: string) { 14 | super(spec, node); 15 | this.spec = spec; 16 | this.node = node; 17 | this.namespace = namespace; 18 | this.name = node.getAttribute('name')!; 19 | } 20 | 21 | static async enter({ node, spec, clauseStack }: Context) { 22 | const clause = clauseStack[clauseStack.length - 1]; 23 | const namespace = clause ? clause.namespace : spec.namespace; 24 | const prodref = new ProdRef(spec, node, namespace); 25 | spec._prodRefs.push(prodref); 26 | } 27 | 28 | build() { 29 | const entry = this.spec.biblio.byProductionName(this.name, this.namespace); 30 | const prod = entry ? entry._instance : null; 31 | 32 | let copy: HTMLElement; 33 | 34 | if (!prod) { 35 | console.error('Could not find production named ' + this.node.getAttribute('name')); 36 | return; 37 | } 38 | 39 | if (shouldInline(this.node)) { 40 | const cls = this.node.getAttribute('class') || ''; 41 | 42 | if (cls.indexOf('inline') === -1) { 43 | this.node.setAttribute('class', cls + ' inline'); 44 | } 45 | } 46 | 47 | if (this.node.hasAttribute('a')) { 48 | this.node.setAttribute('collapsed', ''); 49 | if (!prod.rhsesById[this.node.getAttribute('a')!]) { 50 | console.error( 51 | 'Could not find alternative ' + 52 | this.node.getAttribute('a') + 53 | ' of production ' + 54 | prod.name, 55 | ); 56 | return; 57 | } 58 | 59 | copy = prod.node.cloneNode(false) as HTMLElement; 60 | 61 | // copy nodes until the first RHS. This captures the production name and any annotations. 62 | for (let j = 0; j < prod.node.childNodes.length; j++) { 63 | if (prod.node.childNodes[j].nodeName === 'EMU-RHS') break; 64 | 65 | copy.appendChild(prod.node.childNodes[j].cloneNode(true)); 66 | } 67 | 68 | copy.appendChild(prod.rhsesById[this.node.getAttribute('a')!].node.cloneNode(true)); 69 | } else { 70 | copy = prod.node.cloneNode(true) as HTMLElement; 71 | } 72 | 73 | copy.removeAttribute('id'); 74 | 75 | if (this.node.parentNode) { 76 | this.node.parentNode.replaceChild(copy, this.node); 77 | } 78 | 79 | // copy attributes over (especially important for 'class'). 80 | for (let j = 0; j < this.node.attributes.length; j++) { 81 | const attr = this.node.attributes[j]; 82 | 83 | if (!copy.hasAttribute(attr.name)) { 84 | copy.setAttribute(attr.name, attr.value); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/namespaces-productions.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

Intro

15 | 16 | Foo :: 17 | bar 18 | 19 | 20 | baz 21 | 22 | 23 | 24 | 25 | Foo :: bar 26 | 27 | 28 | 29 | Foo :: 30 | bar 31 | 32 | 33 | baz 34 | 35 | 36 |
37 | 38 | 39 |

1 C

40 | 41 | Foo :: 42 | bar 43 | 44 | 45 | qux 46 | 47 | 48 | 49 | Statement :: 50 | overridden statement 51 | 52 | 53 | 54 | 55 | Foo :: bar 56 | 57 | 58 | 59 | Foo :: 60 | bar 61 | 62 | 63 | qux 64 | 65 | 66 |
67 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecmarkup", 3 | "version": "22.0.0", 4 | "description": "Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.", 5 | "main": "lib/ecmarkup.js", 6 | "typings": "lib/ecmarkup.d.ts", 7 | "scripts": { 8 | "build": "tsc -sourceMap -declarationMap", 9 | "build-release": "tsc", 10 | "build-spec": "mkdir -p docs && node bin/ecmarkup.js spec/index.html docs/index.html --assets-dir=docs && cp ecma-logo.png docs/", 11 | "prepack": "safe-publish-latest && npm run build-release", 12 | "format-spec": "node bin/emu-format.js --write spec/index.html", 13 | "test": "mocha", 14 | "test-baselines": "mocha --timeout 10000 test/baselines.js", 15 | "test-declarations": "tsc -p tsconfig.test.json", 16 | "update-baselines": "npm --update-baselines run test-baselines", 17 | "pretest-published-files": "rm -rf \"ecmarkup-$npm_package_version.tgz\" package", 18 | "test-published-files": "npm pack && tar zxvf \"ecmarkup-$npm_package_version.tgz\" && cp -r test package/test && cd package && npm test && cd ..", 19 | "posttest-published-files": "rm -rf \"ecmarkup-$npm_package_version.tgz\" package", 20 | "lint": "eslint --ext .js,.ts js src test", 21 | "format": "prettier --write ." 22 | }, 23 | "bin": { 24 | "ecmarkup": "bin/ecmarkup.js", 25 | "emu-format": "bin/emu-format.js" 26 | }, 27 | "files": [ 28 | "/bin", 29 | "/lib", 30 | "/js", 31 | "/css", 32 | "/img", 33 | "/fonts", 34 | "/boilerplate", 35 | "/entities-processed.json" 36 | ], 37 | "repository": "tc39/ecmarkup", 38 | "keywords": [ 39 | "ecmascript", 40 | "javascript", 41 | "specs", 42 | "specifications", 43 | "markup", 44 | "markdown", 45 | "html", 46 | "code" 47 | ], 48 | "author": "Brian Terlson", 49 | "license": "MIT", 50 | "dependencies": { 51 | "chalk": "^4.1.2", 52 | "command-line-args": "^5.2.0", 53 | "command-line-usage": "^6.1.1", 54 | "dedent-js": "^1.0.1", 55 | "ecmarkdown": "^8.1.0", 56 | "eslint-formatter-codeframe": "^7.32.1", 57 | "fast-glob": "^3.2.7", 58 | "grammarkdown": "^3.3.2", 59 | "highlight.js": "11.0.1", 60 | "html-escape": "^1.0.2", 61 | "js-yaml": "^3.13.1", 62 | "jsdom": "^25.0.1", 63 | "nwsapi": "2.2.0", 64 | "parse5": "^6.0.1", 65 | "prex": "^0.4.7", 66 | "promise-debounce": "^1.0.1" 67 | }, 68 | "devDependencies": { 69 | "@types/command-line-args": "^5.2.0", 70 | "@types/command-line-usage": "^5.0.2", 71 | "@types/js-yaml": "^3.12.1", 72 | "@types/jsdom": "^16.2.13", 73 | "@types/node": "^13.1.8", 74 | "@types/parse5": "^6.0.2", 75 | "@typescript-eslint/eslint-plugin": "^8.6.0", 76 | "@typescript-eslint/parser": "^8.6.0", 77 | "eslint": "^8.56.0", 78 | "eslint-config-prettier": "^9.1.0", 79 | "eslint-plugin-prettier": "^5.1.3", 80 | "mocha": "^5.2.0", 81 | "prettier": "^3.2.3", 82 | "safe-publish-latest": "^1.1.4", 83 | "typescript": "^5.5.4" 84 | }, 85 | "prettier": { 86 | "singleQuote": true, 87 | "arrowParens": "avoid", 88 | "printWidth": 100 89 | }, 90 | "engines": { 91 | "node": ">= 18" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/RHS.ts: -------------------------------------------------------------------------------- 1 | import type Spec from './Spec'; 2 | import type Production from './Production'; 3 | 4 | import Builder from './Builder'; 5 | 6 | export default class RHS extends Builder { 7 | /** @internal */ production: Production; 8 | /** @internal */ constraints: string | null; 9 | /** @internal */ alternativeId: string | null; 10 | 11 | constructor(spec: Spec, prod: Production, node: HTMLElement) { 12 | super(spec, node); 13 | this.production = prod; 14 | this.node = node; 15 | this.constraints = node.getAttribute('constraints'); 16 | this.alternativeId = node.getAttribute('a'); 17 | } 18 | 19 | build() { 20 | if (this.node.textContent === '') { 21 | this.node.textContent = '[empty]'; 22 | 23 | return; 24 | } 25 | 26 | if (this.constraints) { 27 | const cs = this.spec.doc.createElement('emu-constraints'); 28 | cs.textContent = '[' + this.constraints + ']'; 29 | 30 | this.node.insertBefore(cs, this.node.childNodes[0]); 31 | } 32 | 33 | this.terminalify(this.node); 34 | } 35 | 36 | terminalify(parentNode: Element) { 37 | // we store effects to perform later so the iteration doesn't get messed up 38 | const surrogateTags = ['INS', 'DEL', 'MARK']; 39 | const pairs: { parent: Element; child: Text }[] = []; 40 | for (const node of parentNode.childNodes) { 41 | if (node.nodeType === 3) { 42 | pairs.push({ parent: parentNode, child: node as Text }); 43 | } else if (surrogateTags.includes(node.nodeName)) { 44 | for (const child of node.childNodes) { 45 | if (child.nodeType === 3) { 46 | pairs.push({ parent: node as Element, child: child as Text }); 47 | } 48 | } 49 | } 50 | } 51 | let first = true; 52 | for (const { parent, child } of pairs) { 53 | if (!first && !/^\s+$/.test(child.textContent ?? '')) { 54 | if (parent === parentNode) { 55 | parentNode.insertBefore(this.spec.doc.createTextNode(' '), child); 56 | } else { 57 | // put the space outside of `` (etc) tags 58 | parentNode.insertBefore(this.spec.doc.createTextNode(' '), parent); 59 | } 60 | } 61 | first = false; 62 | this.wrapTerminal(parent, child); 63 | } 64 | } 65 | 66 | private wrapTerminal(parentNode: Element, node: Text) { 67 | const textContent = node.textContent!; 68 | const text = textContent.trim(); 69 | 70 | if (text === '' && textContent.length > 0) { 71 | // preserve intermediate whitespace 72 | return; 73 | } 74 | 75 | const pieces = text.split(/\s/); 76 | 77 | let first = true; 78 | pieces.forEach(p => { 79 | if (p.length === 0) { 80 | return; 81 | } 82 | const est = this.spec.doc.createElement('emu-t'); 83 | est.textContent = p; 84 | 85 | parentNode.insertBefore(est, node); 86 | if (!first) { 87 | parentNode.insertBefore(this.spec.doc.createTextNode(' '), est); 88 | } 89 | first = false; 90 | }); 91 | 92 | parentNode.removeChild(node); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/figure.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |
Figure 1
15 | this is a figure! 16 |
17 | 18 | 19 |
Figure 2 (Informative): Informative figure
20 | this is a figure! 21 |
22 | 23 |
Table 1: An example table
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Column 1Column 2
ValueValue 2
ValueValue 2
ValueValue 2
ValueValue 2
33 |
34 | 35 |
Table 2 (Informative): An example table 2
36 | 37 | 38 | 39 | 40 | 41 | 42 |
Column 1Column 2
ValueValue 2
ValueValue 2
ValueValue 2
ValueValue 2
43 |
44 | 45 |
Figure 3 (Informative): This is the caption
46 | 47 | this is a figure! 48 |
49 | 50 |
Table 3: This is a table
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
Column 1Column 2
ValueValue 2
ValueValue 2
ValueValue 2
ValueValue 2
61 |
62 | 63 |
Table 4 (Informative): This is a second table
64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
Column 1Column 2
ValueValue 2
ValueValue 2
ValueValue 2
ValueValue 2
72 |
73 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/dfn.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

Intro

15 |

Forward references to dfn work.

16 |

Also terms from ES6 are auto-linked: Lexical Environment, Completion, etc.

17 |
18 | 19 |

1 dfn

20 | 21 |

The term dfn means the dfn tag. Other mentions of dfn in this clause should not be auto-linked.

22 | 23 |

Terms with ids are called id dfns. Since this dfn has an id, other occurences 24 | of id dfns may autolink.

25 | 26 | 27 |

1.1 dfn subclause

28 | 29 |

Dfn should be auto-linked here as well. The linking of dfn is case insensitive. But the header shouldn't auto-link.

30 |

Id dfns should link to #identifiers.

31 |
32 | 33 |

Also terms are auto-linked in algorithms. But not naked abstract ops!

34 | 35 | 36 |
  1. Call the dfn algorithm.
  2. Do something great with dfn.
  3. dfn is now awesome.
37 | 38 | 39 |

1.2 dfn autolink

40 | 41 | 42 |
  1. Call the dfn algorithm.
  2. Do something great with dfn.
  3. dfn is now awesome.
43 |
44 |
45 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/autolinking.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |

1 Autolinking

15 |

Type, type, Type(), type()

16 |

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

17 | 18 |

Lowercase

19 |

strict mode

20 |

%Percent%

21 |

extra spaces

22 |

Await

23 |

Variants

24 |

𝔽(x)

25 |
26 | 27 |

2 Autolinking 2

28 |

lowercase should not autolink. But Lowercase should. But not LowerCase.

29 |

Strict mode shoud link. But Strict Mode should not. Also, strict 30 | mode can be wrapped across lines and contain extra whitespace.

31 |

extra spaces in a dfn should be narrowed to one space.

32 |

%Percent% should autolink.

33 |

Vars to dfns should be vars not dfns: Lowercase.

34 |

Also, no autolinks in anchors: Lowercase.

35 |

Similarly, no autolinks for [Await].

36 |

Variants like vOne and vTwo should autolink, including when capitalized as in VOne.

37 |
  1. Non-word-chars AOs still link when invoked, like 𝔽(x).
38 |
39 |

%Array% and %ArrayPrototype% outside of clauses is ok.

40 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/proposal-copyright.html: -------------------------------------------------------------------------------- 1 | 2 | test title!
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |

Stage 0 Draft / September 26, 2014

test title!

13 | 14 | 15 |

Copyright & Software License

16 | 17 | 20 |

Software License

21 |

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

22 | 23 |

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

24 | 25 |
    26 |
  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. 27 |
  3. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  4. 28 |
  5. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.
  6. 29 |
30 | 31 |

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

32 | 33 |
-------------------------------------------------------------------------------- /src/args.ts: -------------------------------------------------------------------------------- 1 | export const options = [ 2 | { 3 | name: 'help', 4 | alias: 'h', 5 | type: Boolean, 6 | description: 'Display this help message', 7 | }, 8 | { 9 | name: 'watch', 10 | alias: 'w', 11 | type: Boolean, 12 | description: 'Rebuild when files change', 13 | }, 14 | { 15 | name: 'load-biblio', 16 | type: String, 17 | lazyMultiple: true, 18 | typeLabel: '{underline path}', 19 | description: 20 | 'An external biblio.json to load; either a path prefixed with "." or "./", or a package name of an installed package that exports a biblio', 21 | }, 22 | { 23 | name: 'write-biblio', 24 | type: String, 25 | typeLabel: '{underline file}', 26 | description: 'Path to where the biblio.json should be written', 27 | }, 28 | { 29 | name: 'assets', 30 | type: String, 31 | typeLabel: 'none|inline|external', 32 | description: 33 | 'Omit assets, inline them, or add them as external. Default: inline, unless --multipage or --assets-dir are specified, in which case external.', 34 | }, 35 | { 36 | name: 'assets-dir', 37 | type: String, 38 | typeLabel: '{underline dir}', 39 | description: 40 | 'The directory in which to place generated assets when using --assets=external. Implies --assets=external. Defaults to [outfile]/assets.', 41 | }, 42 | { 43 | name: 'no-toc', 44 | type: Boolean, 45 | description: "Don't include the table of contents", 46 | }, 47 | { 48 | name: 'printable', 49 | type: Boolean, 50 | description: 'Make the output suitable for printing', 51 | }, 52 | { 53 | name: 'mark-effects', 54 | type: Boolean, 55 | description: 'Render markers for effects like "user code" [UC]', 56 | }, 57 | { 58 | name: 'lint-spec', 59 | type: Boolean, 60 | description: 'Enforce some style and correctness checks', 61 | }, 62 | { 63 | name: 'error-formatter', 64 | type: String, 65 | typeLabel: '{underline formatter}', 66 | defaultValue: 'eslint-formatter-codeframe', 67 | description: 68 | 'The formatter for warnings and errors; either a path prefixed with "." or "./", or package name, of an installed eslint compatible formatter (default: eslint-formatter-codeframe)', 69 | }, 70 | { 71 | name: 'max-clause-depth', 72 | type: Number, 73 | description: 74 | 'The maximum nesting depth for clauses; exceeding this will cause a warning. Defaults to no limit.', 75 | }, 76 | { 77 | name: 'multipage', 78 | type: Boolean, 79 | description: 'Generate a multipage version of the spec. Implies --assets=external.', 80 | }, 81 | { 82 | name: 'strict', 83 | type: Boolean, 84 | description: 'Exit with an error if there are warnings. Cannot be used with --watch.', 85 | }, 86 | { 87 | name: 'verbose', 88 | type: Boolean, 89 | description: 'Display document build progress', 90 | }, 91 | { 92 | name: 'version', 93 | alias: 'v', 94 | type: Boolean, 95 | description: 'Display version info', 96 | }, 97 | { 98 | name: 'files', 99 | type: String, 100 | multiple: true, 101 | defaultOption: true, 102 | }, 103 | 104 | // removed; still defined here so we can give better errors 105 | { 106 | name: 'css-out', 107 | type: String, 108 | }, 109 | { 110 | name: 'js-out', 111 | type: String, 112 | }, 113 | { 114 | name: 'old-toc', 115 | type: Boolean, 116 | }, 117 | ] as const; 118 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/max-clause-depth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
    8 |
  • Toggle shortcuts help?
  • 9 |
  • Toggle "can call user code" annotationsu
  • 10 | 11 |
  • Jump to search box/
  • 12 |
  • Toggle pinning of the current clausep
  • 13 |
  • Jump to the nth pin1-9
  • 14 |
  • Jump to the 10th pin0
  • 15 |
  • Jump to the most recent link target`
  • 16 |
20 | 21 | 22 |

1 One

23 | 24 |

1.1 Two

25 | 26 | 27 |

1.2 Three

28 | 29 |

1.3 four

30 |
31 |
32 | 33 |

1.4 Three Again

34 | 35 |

1.5 Four Again

36 |
37 |
38 |
39 | 40 |

1.6 Two Again

41 |
42 |
43 | 44 |

2 One Again

45 |
46 |
-------------------------------------------------------------------------------- /test/baselines/generated-reference/boilerplate-copyright.html: -------------------------------------------------------------------------------- 1 | 2 | test title!
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |

test title!

13 | 14 | 15 |

Copyright & Software License

16 |

Ecma International

17 |

Rue du Rhone 114

18 |

CH-1204 Geneva

19 |

Tel: +41 22 849 6000

20 |

Fax: +41 22 849 6001

21 |

Web: https://ecma-international.org/

22 | 23 | 26 |

Software License

27 |

All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.

28 | 29 |

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

30 | 31 |
    32 |
  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  2. 33 |
  3. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  4. 34 |
  5. Neither the name of the authors nor Ecma International may be used to endorse or promote products derived from this software without specific prior written permission.
  6. 35 |
36 | 37 |

THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

38 | 39 |
-------------------------------------------------------------------------------- /src/arg-parser.ts: -------------------------------------------------------------------------------- 1 | import type { OptionDefinition } from 'command-line-usage'; 2 | import * as commandLineArgs from 'command-line-args'; 3 | 4 | function fail(msg: string): never { 5 | console.error(msg); 6 | process.exit(1); 7 | } 8 | 9 | export function parse( 10 | options: T, 11 | printHelp: () => void, 12 | ): ArgsFromOptions { 13 | const def = options.find(o => o.defaultOption); 14 | if (!def?.multiple) { 15 | // this is just so I don't have to think about how to handle `--` in other cases 16 | // not an inherent limitation 17 | throw new Error('ecmarkup arg-parser requires a default option for now'); 18 | } 19 | 20 | const argv = process.argv.slice(2); 21 | 22 | let notParsed: string[] = []; 23 | const dashDashIndex = argv.indexOf('--'); 24 | if (dashDashIndex !== -1) { 25 | notParsed = argv.splice(dashDashIndex + 1); 26 | argv.pop(); 27 | } 28 | 29 | let args: ArgsFromOptions; 30 | try { 31 | // @ts-ignore the types are wrong about mutability 32 | args = commandLineArgs(options, { argv }); 33 | } catch (e: any) { 34 | if (e?.name === 'UNKNOWN_OPTION') { 35 | fail(`Unknown option ${e.optionName}`); 36 | } 37 | throw e; 38 | } 39 | 40 | // @ts-ignore it's fine 41 | args[def.name] = (args[def.name] || []).concat(notParsed); 42 | 43 | if ( 44 | (args as unknown as { help?: boolean }).help || 45 | (argv.length === 0 && notParsed.length === 0) 46 | ) { 47 | printHelp(); 48 | process.exit(0); 49 | } 50 | 51 | return args; 52 | } 53 | 54 | // here follows some typescript shenanigans for inferring the type of of the parsed arguments from the argument specification 55 | 56 | type Multiples = T extends unknown 57 | ? Extract 58 | : never; 59 | type Defaulted = T extends unknown ? Extract : never; 60 | type Optional = T extends unknown ? Exclude | Defaulted> : never; 61 | 62 | type ReturnTypeOrNull = T extends (...args: unknown[]) => unknown ? ReturnType : null; 63 | // prettier-ignore 64 | type ArgTypeForKey = 65 | ReturnTypeOrNull['type']>; 66 | 67 | // spread is from https://stackoverflow.com/a/49683575 68 | type OptionalPropertyNames = { 69 | [K in keyof T]-?: {} extends { [P in K]: T[K] } ? K : never; 70 | }[keyof T]; 71 | 72 | type SpreadProperties = { 73 | [P in K]: L[P] | Exclude; 74 | }; 75 | 76 | type Id = T extends infer U ? { [K in keyof U]: U[K] } : never; 77 | 78 | type SpreadTwo = Id< 79 | Pick> & 80 | Pick>> & 81 | Pick, keyof L>> & 82 | SpreadProperties & keyof L> 83 | >; 84 | 85 | type Spread = A extends [infer L, ...infer R] 86 | ? SpreadTwo> 87 | : unknown; 88 | 89 | // this type is wrong: if you specify the flag but omit the argument you get `null`, not the default value 90 | // you should check those manually 91 | type ArgsFromOptions = Spread< 92 | [ 93 | { 94 | [key in Multiples['name']]: ArgTypeForKey[]; 95 | }, 96 | { 97 | [key in Defaulted['name']]: ArgTypeForKey; 98 | }, 99 | { 100 | [key in Optional['name']]?: ArgTypeForKey; 101 | }, 102 | ] 103 | >; 104 | -------------------------------------------------------------------------------- /test/baselines/generated-reference/algorithms.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
    4 |
  • Toggle shortcuts help?
  • 5 |
  • Toggle "can call user code" annotationsu
  • 6 | 7 |
  • Jump to search box/
  • 8 |
  • Toggle pinning of the current clausep
  • 9 |
  • Jump to the nth pin1-9
  • 10 |
  • Jump to the 10th pin0
  • 11 |
  • Jump to the most recent link target`
  • 12 |
13 | 14 |
  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. Set foo to ! Internal();
    3. Set foo to ! SDO of operation.
    4. Set foo to ! operation.[[MOP]]().
  6. A Record looks like this: Record { [[Key]]: 0 }.
  7. A List looks like this: « 0, 1 ».
15 | 16 | 17 |

1 Internal Function

18 |
19 | 20 |

2 Refs

21 |

Can refer to functions in prose, eg. Internal.

22 |

Internal works if Internal is in the middle of the prose.

23 |

Don't autolink references inside square brackets, eg: [[Internal]]. 24 | Likewise, percent things: %%Internal%%. 25 | Likewise, mentions of Internal.foo.

26 |
27 |
-------------------------------------------------------------------------------- /src/formatter/text.ts: -------------------------------------------------------------------------------- 1 | import { LineBuilder } from './line-builder'; 2 | const entities = require('../../entities-processed.json'); 3 | 4 | function isBadNumericReference(codePoint: number) { 5 | // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state 6 | if ( 7 | // NULL character 8 | !codePoint || 9 | // out of range 10 | codePoint > 0x10ffff || 11 | // surrogate 12 | (0xd800 <= codePoint && codePoint <= 0xdfff) || 13 | // noncharacter 14 | (0xfdd0 <= codePoint && codePoint <= 0xfdef) || 15 | (codePoint & 0xfffe) === 0xfffe || 16 | // control character (but without exceptions for tab/line feed/form feed/space) 17 | (0 <= codePoint && codePoint <= 0x1f) || 18 | (0x7f <= codePoint && codePoint <= 0x9f) 19 | ) { 20 | return true; 21 | } 22 | return false; 23 | } 24 | 25 | export function printText(text: string, indent: number, commonIndent: string = ''): LineBuilder { 26 | const output: LineBuilder = new LineBuilder(indent); 27 | if (text === '') { 28 | return output; 29 | } 30 | text = text.replace(/&(?:[a-zA-Z0-9]+|#[Xx]([0-9a-fA-F]+)|#([0-9]+));?/g, (m, hex, decimal) => { 31 | if (hex || decimal) { 32 | // pass through bad references, 33 | // normalize '&' and '<' into named references, 34 | // and transform everything else 35 | const codePoint = parseInt(hex || decimal, hex ? 16 : 10); 36 | if (isBadNumericReference(codePoint)) { 37 | return m; 38 | } 39 | const ch = String.fromCodePoint(codePoint); 40 | if (/\p{White_Space}|\p{DI}|\p{gc=M}|\p{gc=C}/u.test(ch)) { 41 | return m; 42 | } 43 | if (ch === '&') { 44 | return '&'; 45 | } else if (ch === '<') { 46 | return '<'; 47 | } 48 | return ch; 49 | } 50 | 51 | // entities[m] is null if the entity expands to '&', '<', or a string which has blank/control/etc characters 52 | if ({}.hasOwnProperty.call(entities, m) && entities[m] !== null) { 53 | return entities[m]; 54 | } 55 | const lower = m.toLowerCase(); 56 | if (lower === '<' || lower === '&') { 57 | return lower; 58 | } else if (lower === '<' || lower === '&') { 59 | return lower + ';'; 60 | } 61 | return m; 62 | }); 63 | 64 | const leadingSpace = text[0] === ' ' || text[0] === '\t'; 65 | const trailingSpace = text[text.length - 1] === ' ' || text[text.length - 1] === '\t'; 66 | 67 | const lines = text.split('\n').map((l, i) => { 68 | if (i === 0 || commonIndent === '' || !l.startsWith(commonIndent)) { 69 | return l.trim(); 70 | } 71 | const withoutIndent = l.substring(commonIndent.length); 72 | const moreIndent = withoutIndent.match(/^ +/)?.[0]; 73 | if (moreIndent) { 74 | return { indent: moreIndent, line: l.trim() }; 75 | } 76 | return l.trim(); 77 | }); 78 | 79 | if (leadingSpace) { 80 | output.appendText(' '); 81 | } 82 | if (lines.length === 1) { 83 | if (lines[0] !== '') { 84 | output.appendText(lines[0] as string); 85 | if (trailingSpace) { 86 | output.appendText(' '); 87 | } 88 | } 89 | return output; 90 | } 91 | for (let i = 0; i < lines.length; ++i) { 92 | const line = lines[i]; 93 | if (typeof line === 'string') { 94 | output.appendText(line); 95 | } else { 96 | output.last = line.indent; 97 | output.appendText(line.line); 98 | } 99 | if (i < lines.length - 1) { 100 | output.linebreak(); 101 | } 102 | } 103 | if (trailingSpace && output.last !== '') { 104 | output.appendText(' '); 105 | } 106 | 107 | return output; 108 | } 109 | -------------------------------------------------------------------------------- /src/lint/lint.ts: -------------------------------------------------------------------------------- 1 | import type { default as Spec, Warning } from '../Spec'; 2 | 3 | import { collectNodes } from './collect-nodes'; 4 | import { collectGrammarDiagnostics } from './collect-grammar-diagnostics'; 5 | import { collectSpellingDiagnostics } from './collect-spelling-diagnostics'; 6 | import { collectAlgorithmDiagnostics } from './collect-algorithm-diagnostics'; 7 | import { collectHeaderDiagnostics } from './collect-header-diagnostics'; 8 | import { collectTagDiagnostics } from './collect-tag-diagnostics'; 9 | import type { AugmentedGrammarEle } from '../Grammar'; 10 | import type { AlgorithmElementWithTree } from '../Algorithm'; 11 | 12 | /* 13 | Currently this checks 14 | - grammarkdown's built-in sanity checks 15 | - the productions in the definition of each early error and SDO are defined in the main grammar 16 | - those productions do not include `[no LineTerminator here]` restrictions or `[+flag]` gating 17 | - the algorithm linting rules imported above 18 | - headers of abstract operations have consistent spacing 19 | - certain common spelling errors 20 | 21 | There's more to do: 22 | https://github.com/tc39/ecmarkup/issues/173 23 | */ 24 | export async function lint( 25 | report: (err: Warning) => void, 26 | sourceText: string, 27 | spec: Spec, 28 | document: Document, 29 | ) { 30 | collectSpellingDiagnostics(report, sourceText, spec.imports); 31 | 32 | collectTagDiagnostics(report, spec, document); 33 | 34 | const collection = collectNodes(report, sourceText, spec, document); 35 | if (!collection.success) { 36 | return; 37 | } 38 | const { mainGrammar, headers, sdos, earlyErrors, algorithms } = collection; 39 | 40 | const { grammar, oneOffGrammars } = await collectGrammarDiagnostics( 41 | report, 42 | spec, 43 | sourceText, 44 | mainGrammar, 45 | sdos, 46 | earlyErrors, 47 | ); 48 | 49 | collectAlgorithmDiagnostics(report, spec, sourceText, algorithms); 50 | 51 | collectHeaderDiagnostics(report, headers); 52 | 53 | // Stash intermediate results for later use 54 | // This isn't actually necessary for linting, but we might as well avoid redoing work later when we can. 55 | 56 | await grammar.emit(undefined, (file, source) => { 57 | const name = +file.split('.')[0]; 58 | const node = mainGrammar[name].element; 59 | if ('grammarkdownOut' in node) { 60 | throw new Error('unexpectedly regenerating grammarkdown output for node ' + name); 61 | } 62 | if (name !== +grammar.sourceFiles[name].filename) { 63 | throw new Error( 64 | `grammarkdown file mismatch: ${name} vs ${grammar.sourceFiles[name].filename}. This is a bug in ecmarkup; please report it.`, 65 | ); 66 | } 67 | (node as AugmentedGrammarEle).grammarkdownOut = source; 68 | (node as AugmentedGrammarEle).grammarSource = grammar.sourceFiles[name]; 69 | }); 70 | for (const { grammarEle, grammar } of oneOffGrammars) { 71 | await grammar.emit(undefined, (file, source) => { 72 | if ('grammarkdownOut' in grammarEle) { 73 | throw new Error('unexpectedly regenerating grammarkdown output'); 74 | } 75 | if (grammar.rootFiles.length !== 1) { 76 | throw new Error( 77 | `grammarkdown file count mismatch: ${grammar.rootFiles.length}. This is a bug in ecmarkup; please report it.`, 78 | ); 79 | } 80 | (grammarEle as AugmentedGrammarEle).grammarkdownOut = source; 81 | (grammarEle as AugmentedGrammarEle).grammarSource = grammar.rootFiles[0]; 82 | }); 83 | } 84 | 85 | for (const pair of algorithms) { 86 | if ('tree' in pair) { 87 | const element = pair.element as AlgorithmElementWithTree; 88 | element.ecmarkdownTree = pair.tree ?? null; 89 | element.originalHtml = pair.source ?? pair.element.innerHTML; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /js/print.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Some notes: 3 | * - Prince cant grok trailing commas, so prettier is disabled anywhere it tries to enforce wrapping with trailing comma 4 | * - Prince doesn't support template strings yet 5 | * - Set Prince.trackboxes to true for advanced debugging, see https://www.princexml.com/doc/cookbook/#how-and-where-is-my-box 6 | * */ 7 | 8 | /* eslint-disable no-undef */ 9 | 'use strict'; 10 | 11 | // Prince.trackBoxes = true; 12 | 13 | const shortname = document.querySelector('h1.shortname'); 14 | const version = document.querySelector('h1.version'); 15 | const title = document.querySelector('h1.title'); 16 | 17 | shortname.innerHTML = restoreSuperScripts(shortname.innerHTML); 18 | version.innerHTML = restoreSuperScripts(version.innerHTML); 19 | title.innerHTML = restoreSuperScripts(title.innerHTML); 20 | 21 | rearrangeTables(); 22 | 23 | PDF.pageLayout = 'two-column-right'; 24 | PDF.pageMode = 'show-bookmarks'; 25 | PDF.duplex = 'duplex-flip-long-edge'; 26 | PDF.title = document.title; 27 | PDF.author = 'Ecma International'; 28 | PDF.subject = shortname.innerHTML + (version ? ', ' + version.innerHTML : ''); 29 | 30 | /** 31 | * Terms and definitions section should not have every term listed in the table of contents. 32 | * */ 33 | const terms = document.querySelector('#toc a[href="#sec-terms-and-definitions"]'); 34 | 35 | if (terms) { 36 | (terms.parentElement.querySelector('ol.toc') || document.createElement('i')).remove(); 37 | } 38 | 39 | function restoreSuperScripts(string) { 40 | if (!string) return false; 41 | 42 | return string 43 | .replace(/(\d)st/, '$1st') 44 | .replace(/(\d)nd/, '$1nd') 45 | .replace(/(\d)rd/, '$1rd') 46 | .replace(/(\d)th/, '$1th') 47 | .replace('®', '®') 48 | .replace('®', '®') 49 | .replace('™', '') 50 | .replace('™', ''); 51 | } 52 | 53 | /** 54 | * Sets up table captions and figcaptions for tables, which provides for 55 | * continuation table captions. 56 | * */ 57 | function rearrangeTables() { 58 | const tables = Array.from(document.getElementsByTagName('emu-table')); 59 | 60 | tables.forEach(emuTable => { 61 | const figcaption = emuTable.getElementsByTagName('figcaption')[0]; 62 | const tableCaptionText = figcaption.innerHTML; 63 | const table = emuTable.getElementsByTagName('table')[0]; 64 | const captionElement = document.createElement('caption'); 65 | 66 | captionElement.innerHTML = tableCaptionText; 67 | 68 | table.insertBefore(captionElement, table.getElementsByTagName('thead')[0]); 69 | table.appendChild(figcaption); 70 | }); 71 | } 72 | 73 | /** 74 | * @typedef {Object} PrinceBox 75 | * @property {string} type 76 | * @property {number} pageNum 77 | * @property {number} x 78 | * @property {number} y 79 | * @property {number} w 80 | * @property {number} h 81 | * @property {number} baseline 82 | * @property {number} marginTop 83 | * @property {number} marginBottom 84 | * @property {number} marginLeft 85 | * @property {number} marginRight 86 | * @property {number} paddingTop 87 | * @property {number} paddingBottom 88 | * @property {number} paddingLeft 89 | * @property {number} paddingRight 90 | * @property {number} borderTop 91 | * @property {number} borderBottom 92 | * @property {number} borderLeft 93 | * @property {number} borderRight 94 | * @property {string} floatPosition "TOP" or "BOTTOM" 95 | * @property {PrinceBox[]} children 96 | * @property {PrinceBox} parent 97 | * @property {Element|null} element 98 | * @property {string|null} pseudo 99 | * @property {string} text 100 | * @property {string} src 101 | * @property {CSSStyleSheet} style 102 | * */ 103 | --------------------------------------------------------------------------------