Merge branch 'develop' into conf.RESPEC_API_BASE

This commit is contained in:
Sid Vishnoi 2021-10-05 15:31:30 +05:30
commit 3020747756
No known key found for this signature in database
GPG Key ID: 84A3BFDA84D2675A
197 changed files with 10808 additions and 5580 deletions

View File

@ -1,3 +0,0 @@
builds/*
js/*
!js/profile-w3c-common.js

View File

@ -1,10 +1,9 @@
{
"ignorePatterns": ["builds/**", "js/**"],
"env": {
"browser": true,
"es6": true,
"node": true,
"jquery": true,
"amd": true,
"worker": true
},
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
@ -43,13 +42,6 @@
"spaced-comment": ["error", "always", { "block": { "balanced": true } }]
},
"globals": {
"flushIframes": true,
"hyperHTML": true,
"makeBasicConfig": true,
"makeDefaultBody": true,
"makeRSDoc": true,
"makeStandardOps": true,
"pickRandomsFromList": true,
"respecConfig": true
},
"plugins": ["import", "prettier"]

View File

@ -1,11 +1,18 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
time: '10:00'
open-pull-requests-limit: 10
labels:
- dependencies
versioning-strategy: increase
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
time: "10:00"
open-pull-requests-limit: 10
labels: [dependencies]
versioning-strategy: increase
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
time: "10:00"
open-pull-requests-limit: 10
labels: [dependencies]

View File

@ -13,7 +13,10 @@ jobs:
- uses: actions/checkout@v2
with:
ref: develop
- uses: actions/setup-ruby@v1
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6
bundler-cache: true
- run: gem install github_changelog_generator
- run: github_changelog_generator -u w3c -p respec --no-unreleased
env:

View File

@ -14,6 +14,7 @@ env:
PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome
# See https://github.com/w3c/respec/pull/3306
LC_ALL: en_US.UTF-8
FORCE_COLOR: 1
jobs:
lint:
@ -21,14 +22,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Cache node modules
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-12-${{ hashFiles('**/package-lock.json') }}
- name: Ensure builds/ has not changed
run: |
curl -sL https://api.github.com/repos/w3c/respec/pulls/${{ github.event.pull_request.number }}/files -o /tmp/pr_files.json
cat /tmp/pr_files.json | jq -r '.[].filename' | grep -qE "^builds/" || exit 0
echo "::error::Uh oh! builds/ was changed."; exit 1
- uses: actions/setup-node@v2
with: { node-version: 14, cache: npm }
- run: npm ci
- run: npm run lint
@ -38,18 +38,11 @@ jobs:
needs: lint
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Cache node modules
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-12-${{ hashFiles('**/package-lock.json') }}
- name: install & build
run: |
npm ci
npm run build:w3c
- uses: actions/setup-node@v2
with: { node-version: 14, cache: npm }
- run: npm ci
- run: npm run test:build
- run: npm run build:w3c
- run: npm run test:headless
test-karma:
@ -61,18 +54,33 @@ jobs:
needs: lint
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Cache node modules
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-12-${{ hashFiles('**/package-lock.json') }}
- uses: actions/setup-node@v2
with: { node-version: 14, cache: npm }
- name: install & build
run: |
npm ci
npm run build:w3c & npm run build:geonovum
- run: npm run test:karma
- run: npm run test:unit
env:
BROWSERS: ${{ matrix.browser }}
- run: npm run test:integration
env:
BROWSERS: ${{ matrix.browser }}
html-validate:
name: Validate HTML
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with: { node-version: 14, cache: npm }
- name: install & build
run: |
npm ci
npm run build:w3c
- name: run validator
run: |
vnu=$(node -p "require('vnu-jar')")
./tools/respec2html.js examples/basic.built.html /tmp/basic.built.html --verbose
java -jar $vnu /tmp/basic.built.html

View File

@ -24,14 +24,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Cache node modules
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-12-${{ hashFiles('**/package-lock.json') }}
- uses: actions/setup-node@v2
with: { node-version: 14, cache: npm }
- name: install & build
run: |
npm ci
@ -55,18 +49,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- name: Cache node modules
uses: actions/cache@v1
with:
path: ~/.npm
key: ${{ runner.os }}-node-12-${{ hashFiles('**/package-lock.json') }}
- uses: actions/setup-node@v2
with: { node-version: 14, cache: npm }
- name: install & build
run: |
npm ci
npm run build:w3c & npm run build:geonovum
- run: npm run test:karma
- run: npm test
env:
BROWSERS: ChromeHeadless

59
.github/workflows/regressions.yml vendored Normal file
View File

@ -0,0 +1,59 @@
name: Regressions
on: workflow_dispatch
env:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome
FORCE_COLOR: 1
jobs:
build:
name: Build W3C profile
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with: { node-version: 14, cache: npm }
- run: npm ci
- run: npm run build:w3c
- uses: actions/upload-artifact@v2
with:
name: builds
path: |
builds/respec-w3c.js*
builds/respec-worker.js*
builds/respec-highlight.js*
retention-days: 1
test:
name: Diff ${{ matrix.source }}
runs-on: ubuntu-latest
needs: build
strategy:
matrix:
source:
- https://w3c.github.io/manifest/
- https://w3c.github.io/payment-request/
- https://w3c.github.io/trace-context/
- https://w3c.github.io/screen-wake-lock/
- https://w3c.github.io/push-api/
- https://w3c.github.io/geolocation-api/
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with: { node-version: 14, cache: npm }
- run: npm ci --production --ignore-scripts
- uses: actions/download-artifact@v2
with:
name: builds
path: ./builds/
- run: git status
- run: node ./tools/respec2html ${{ matrix.source }} before.html -t 30 --verbose
- run: node ./tools/respec2html ${{ matrix.source }} after.html -t 30 --verbose --use-local
- uses: sidvishnoi/git-delta@v1
- name: git diff
run: |
set -o pipefail
git diff --exit-code --no-index before.html after.html | delta --diff-so-fancy
exit $?

View File

@ -1,5 +1,422 @@
# Changelog
## [v26.8.11](https://github.com/w3c/respec/tree/v26.8.11) (2021-06-07)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.10...v26.8.11)
**Merged pull requests:**
- style\(styles/\*\): run through prettier [\#3606](https://github.com/w3c/respec/pull/3606) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(styles/caniuse\): use cursor: default [\#3605](https://github.com/w3c/respec/pull/3605) ([marcoscaceres](https://github.com/marcoscaceres))
- style\(styles/respec\): whitespace fixes [\#3600](https://github.com/w3c/respec/pull/3600) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.8.10](https://github.com/w3c/respec/tree/v26.8.10) (2021-06-04)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.9...v26.8.10)
**Fixed bugs:**
- xref not able to link \[=ReadableStream/set up/pullAlgorithm=\] [\#3595](https://github.com/w3c/respec/issues/3595)
**Closed issues:**
- Support expansion of headings across specs [\#3589](https://github.com/w3c/respec/issues/3589)
**Merged pull requests:**
- fix\(core/inlines\): support \[=deep/for/links=\] [\#3596](https://github.com/w3c/respec/pull/3596) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.8.9](https://github.com/w3c/respec/tree/v26.8.9) (2021-06-04)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.8...v26.8.9)
**Fixed bugs:**
- `title` attr added to panel'd dfns is breaking Bikeshed [\#3592](https://github.com/w3c/respec/issues/3592)
**Merged pull requests:**
- fix\(core/dfn-panel\): don't add title attribute to dfn [\#3593](https://github.com/w3c/respec/pull/3593) ([sidvishnoi](https://github.com/sidvishnoi))
## [v26.8.8](https://github.com/w3c/respec/tree/v26.8.8) (2021-06-03)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.7...v26.8.8)
**Fixed bugs:**
- Referencing IDL Constants fails [\#3590](https://github.com/w3c/respec/issues/3590)
**Merged pull requests:**
- fix\(core/inline-idl-parser\): link interface contants [\#3591](https://github.com/w3c/respec/pull/3591) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(deps\): bump ws from 7.4.3 to 7.4.6 [\#3587](https://github.com/w3c/respec/pull/3587) ([dependabot[bot]](https://github.com/apps/dependabot))
- chore\(CHANGELOG\): regenerate [\#3582](https://github.com/w3c/respec/pull/3582) ([github-actions[bot]](https://github.com/apps/github-actions))
## [v26.8.7](https://github.com/w3c/respec/tree/v26.8.7) (2021-06-02)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.6...v26.8.7)
**Fixed bugs:**
- `aria-label` generated for github issues raises a warning in the HTML validator [\#3584](https://github.com/w3c/respec/issues/3584)
**Merged pull requests:**
- fix\(core/issues-notes\): invalid aria-label [\#3586](https://github.com/w3c/respec/pull/3586) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.8.6](https://github.com/w3c/respec/tree/v26.8.6) (2021-06-01)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.5...v26.8.6)
**Merged pull requests:**
- chore\(deps\): update dependencies [\#3578](https://github.com/w3c/respec/pull/3578) ([sidvishnoi](https://github.com/sidvishnoi))
## [v26.8.5](https://github.com/w3c/respec/tree/v26.8.5) (2021-05-31)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.4...v26.8.5)
**Merged pull requests:**
- fix\(core/inlines\): expand markdown headings [\#3567](https://github.com/w3c/respec/pull/3567) ([sidvishnoi](https://github.com/sidvishnoi))
## [v26.8.4](https://github.com/w3c/respec/tree/v26.8.4) (2021-05-29)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.3...v26.8.4)
**Implemented enhancements:**
- File a Bug -\> Issue [\#3564](https://github.com/w3c/respec/issues/3564)
**Merged pull requests:**
- refactor\(core/utils\): add JSDoc TypeScript annotations [\#3568](https://github.com/w3c/respec/pull/3568) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(core/github\): file an issue, not bug [\#3565](https://github.com/w3c/respec/pull/3565) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.8.3](https://github.com/w3c/respec/tree/v26.8.3) (2021-05-25)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.2...v26.8.3)
**Fixed bugs:**
- Deprecate Person.mailto... use Person.url [\#3532](https://github.com/w3c/respec/issues/3532)
**Merged pull requests:**
- chore\(package\): update deps [\#3559](https://github.com/w3c/respec/pull/3559) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(core/webmonetization\): use respec.org payment pointer [\#3552](https://github.com/w3c/respec/pull/3552) ([sidvishnoi](https://github.com/sidvishnoi))
## [v26.8.2](https://github.com/w3c/respec/tree/v26.8.2) (2021-05-21)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.1...v26.8.2)
**Fixed bugs:**
- It's currently not possible for a Draft Finding to link to its most recently published Finding [\#3541](https://github.com/w3c/respec/issues/3541)
- Missing 'p-org' hcard classes [\#3530](https://github.com/w3c/respec/issues/3530)
**Merged pull requests:**
- fix\(w3c/style\): misspelled finding-draft [\#3551](https://github.com/w3c/respec/pull/3551) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(w3c/templates/headers\): show versions links for TAG docs [\#3550](https://github.com/w3c/respec/pull/3550) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(core/show-people\): more robust error checking [\#3531](https://github.com/w3c/respec/pull/3531) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.8.1](https://github.com/w3c/respec/tree/v26.8.1) (2021-05-20)
[Full Changelog](https://github.com/w3c/respec/compare/v26.8.0...v26.8.1)
**Fixed bugs:**
- How to remove "W3C Document" [\#3542](https://github.com/w3c/respec/issues/3542)
**Merged pull requests:**
- fix\(core/ui\): display plugin name after error msg [\#3548](https://github.com/w3c/respec/pull/3548) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(deps-dev\): bump webidl2 from 24.0.1 to 24.1.1 [\#3547](https://github.com/w3c/respec/pull/3547) ([dependabot[bot]](https://github.com/apps/dependabot))
- chore\(ci/pr\): ensure changes to builds/ aren't committed [\#3546](https://github.com/w3c/respec/pull/3546) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(w3c/headers\): don't add W3C to h2 for 'base' documents [\#3545](https://github.com/w3c/respec/pull/3545) ([marcoscaceres](https://github.com/marcoscaceres))
- docs\(examples\): update basic.html and basic.built.html [\#3527](https://github.com/w3c/respec/pull/3527) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(CHANGELOG\): regenerate [\#3505](https://github.com/w3c/respec/pull/3505) ([github-actions[bot]](https://github.com/apps/github-actions))
## [v26.8.0](https://github.com/w3c/respec/tree/v26.8.0) (2021-05-19)
[Full Changelog](https://github.com/w3c/respec/compare/v26.7.0...v26.8.0)
**Closed issues:**
- Tests `toBe(null)`, should use `toBeNull()` [\#3538](https://github.com/w3c/respec/issues/3538)
**Merged pull requests:**
- feat\(aom\): define AOM profile [\#3543](https://github.com/w3c/respec/pull/3543) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(deps\): update dependencies [\#3540](https://github.com/w3c/respec/pull/3540) ([sidvishnoi](https://github.com/sidvishnoi))
- Changed toBe\(null\) to toBeNull\(\) [\#3539](https://github.com/w3c/respec/pull/3539) ([himanshu007-creator](https://github.com/himanshu007-creator))
## [v26.7.0](https://github.com/w3c/respec/tree/v26.7.0) (2021-05-13)
[Full Changelog](https://github.com/w3c/respec/compare/v26.6.6...v26.7.0)
**Fixed bugs:**
- dfn.role=link containing a link [\#3482](https://github.com/w3c/respec/issues/3482)
**Closed issues:**
- How to turn off the abbreviations feature? [\#3514](https://github.com/w3c/respec/issues/3514)
**Merged pull requests:**
- fix\(core/dfn-panel\): stop assignig role=link to dfn [\#3529](https://github.com/w3c/respec/pull/3529) ([marcoscaceres](https://github.com/marcoscaceres))
- test\(core/xref-spec\): point to webauthn-3 now [\#3528](https://github.com/w3c/respec/pull/3528) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(tools/dev-server\): point to examples [\#3526](https://github.com/w3c/respec/pull/3526) ([marcoscaceres](https://github.com/marcoscaceres))
- feat\(core/inlines\): allow excluding abbr from inlines [\#3515](https://github.com/w3c/respec/pull/3515) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(deps\): bump hosted-git-info from 2.8.8 to 2.8.9 [\#3513](https://github.com/w3c/respec/pull/3513) ([dependabot[bot]](https://github.com/apps/dependabot))
- chore\(package\): update deps [\#3512](https://github.com/w3c/respec/pull/3512) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.6.6](https://github.com/w3c/respec/tree/v26.6.6) (2021-05-07)
[Full Changelog](https://github.com/w3c/respec/compare/v26.6.5...v26.6.6)
**Fixed bugs:**
- Automatic link to sub-sections duplicate definition markup [\#3486](https://github.com/w3c/respec/issues/3486)
**Merged pull requests:**
- test\(core/xref-spec\): use specific webauthn version [\#3511](https://github.com/w3c/respec/pull/3511) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(lint/wpt-tests-exist\): make separate warnings [\#3509](https://github.com/w3c/respec/pull/3509) ([marcoscaceres](https://github.com/marcoscaceres))
- tests\(w3c/group-spec\): ipr disclousures have new URLs [\#3508](https://github.com/w3c/respec/pull/3508) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(core/anchor-expander\): don't copy attributes when expanding [\#3507](https://github.com/w3c/respec/pull/3507) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(deps-dev\): bump rollup from 2.45.2 to 2.47.0 [\#3506](https://github.com/w3c/respec/pull/3506) ([dependabot[bot]](https://github.com/apps/dependabot))
## [v26.6.5](https://github.com/w3c/respec/tree/v26.6.5) (2021-05-04)
[Full Changelog](https://github.com/w3c/respec/compare/v26.6.4...v26.6.5)
**Fixed bugs:**
- Underlined space after ORCID icon [\#3501](https://github.com/w3c/respec/issues/3501)
**Merged pull requests:**
- fix\(templates/show-people\): remove extraneous WS from ORCID link [\#3502](https://github.com/w3c/respec/pull/3502) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(CHANGELOG\): regenerate [\#3499](https://github.com/w3c/respec/pull/3499) ([github-actions[bot]](https://github.com/apps/github-actions))
## [v26.6.4](https://github.com/w3c/respec/tree/v26.6.4) (2021-04-30)
[Full Changelog](https://github.com/w3c/respec/compare/v26.6.3...v26.6.4)
**Fixed bugs:**
- Automatic link to sub-sections duplicate definition markup [\#3486](https://github.com/w3c/respec/issues/3486)
**Merged pull requests:**
- fix\(core/utils\): removes attributes from dfn when calling makeSafeCopy [\#3500](https://github.com/w3c/respec/pull/3500) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.6.3](https://github.com/w3c/respec/tree/v26.6.3) (2021-04-30)
[Full Changelog](https://github.com/w3c/respec/compare/v26.6.2...v26.6.3)
**Fixed bugs:**
- Respec generates wrong patent policy text for joint publication. [\#3494](https://github.com/w3c/respec/issues/3494)
**Merged pull requests:**
- chore\(ci/pr\): don't re-install html validator [\#3498](https://github.com/w3c/respec/pull/3498) ([sidvishnoi](https://github.com/sidvishnoi))
- refactor\(core/data-cite\): use switch case in linkElem [\#3497](https://github.com/w3c/respec/pull/3497) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(w3c/templates/sotd\): pluralize groups when noRecTrack [\#3496](https://github.com/w3c/respec/pull/3496) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(core/xref\): use full hrefs when available [\#3495](https://github.com/w3c/respec/pull/3495) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(CHANGELOG\): add v26.6.2 [\#3493](https://github.com/w3c/respec/pull/3493) ([github-actions[bot]](https://github.com/apps/github-actions))
- refactor\(core/inlines\): use switch instead of ifs [\#3492](https://github.com/w3c/respec/pull/3492) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.6.2](https://github.com/w3c/respec/tree/v26.6.2) (2021-04-27)
[Full Changelog](https://github.com/w3c/respec/compare/v26.6.1...v26.6.2)
**Fixed bugs:**
- dfn.role=link containing a link [\#3482](https://github.com/w3c/respec/issues/3482)
**Merged pull requests:**
- fix\(core/inlines\): don't link IDL inside dfn [\#3491](https://github.com/w3c/respec/pull/3491) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(deps\): update dependencies [\#3490](https://github.com/w3c/respec/pull/3490) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(CHANGELOG\): regenerate [\#3485](https://github.com/w3c/respec/pull/3485) ([github-actions[bot]](https://github.com/apps/github-actions))
## [v26.6.1](https://github.com/w3c/respec/tree/v26.6.1) (2021-04-26)
[Full Changelog](https://github.com/w3c/respec/compare/v26.6.0...v26.6.1)
**Fixed bugs:**
- dfn.role=link containing a link [\#3482](https://github.com/w3c/respec/issues/3482)
- dfn panel for data-cite dfn are unused [\#3254](https://github.com/w3c/respec/issues/3254)
**Closed issues:**
- Add support for partial interface references [\#3264](https://github.com/w3c/respec/issues/3264)
- "npm start" devserver doesn't work with Firefox [\#3070](https://github.com/w3c/respec/issues/3070)
**Merged pull requests:**
- fix\(core/dfn-panel\): don't add panel for data-cite [\#3484](https://github.com/w3c/respec/pull/3484) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(tools/release\): colors\[match\] not a function [\#3481](https://github.com/w3c/respec/pull/3481) ([marcoscaceres](https://github.com/marcoscaceres))
- refactor\(tools/release\): add type checking [\#3480](https://github.com/w3c/respec/pull/3480) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(CHANGELOG\): regenerate [\#3479](https://github.com/w3c/respec/pull/3479) ([github-actions[bot]](https://github.com/apps/github-actions))
## [v26.6.0](https://github.com/w3c/respec/tree/v26.6.0) (2021-04-22)
[Full Changelog](https://github.com/w3c/respec/compare/v26.5.3...v26.6.0)
**Merged pull requests:**
- test\(core/xref-spec\): fix bad URL [\#3478](https://github.com/w3c/respec/pull/3478) ([sidvishnoi](https://github.com/sidvishnoi))
- refactor\(core/linter-rules/a11y\): convert core/a11y to linter rule [\#3477](https://github.com/w3c/respec/pull/3477) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(dependabot\): enable updates for github-actions [\#3476](https://github.com/w3c/respec/pull/3476) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(tools/release\): validate HTML before release [\#3475](https://github.com/w3c/respec/pull/3475) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(CHANGELOG\): regenerate [\#3474](https://github.com/w3c/respec/pull/3474) ([github-actions[bot]](https://github.com/apps/github-actions))
- chore\(workflows/pr\): validate HTML [\#3473](https://github.com/w3c/respec/pull/3473) ([marcoscaceres](https://github.com/marcoscaceres))
- feat\(core/web-monetization\): support adding monetization meta tag [\#2966](https://github.com/w3c/respec/pull/2966) ([sidvishnoi](https://github.com/sidvishnoi))
## [v26.5.3](https://github.com/w3c/respec/tree/v26.5.3) (2021-04-21)
[Full Changelog](https://github.com/w3c/respec/compare/v26.5.2...v26.5.3)
**Fixed bugs:**
- \<aside role=dialog\> fails spec-prod validation [\#3470](https://github.com/w3c/respec/issues/3470)
**Merged pull requests:**
- test\(core/xref-spec\): URL changed [\#3472](https://github.com/w3c/respec/pull/3472) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(core/dfn-pane\): aside should be a div [\#3471](https://github.com/w3c/respec/pull/3471) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(CHANGELOG\): regenerate [\#3469](https://github.com/w3c/respec/pull/3469) ([github-actions[bot]](https://github.com/apps/github-actions))
## [v26.5.2](https://github.com/w3c/respec/tree/v26.5.2) (2021-04-20)
[Full Changelog](https://github.com/w3c/respec/compare/v26.5.1...v26.5.2)
**Fixed bugs:**
- Open dfnPanel using keyboard [\#2825](https://github.com/w3c/respec/issues/2825)
**Merged pull requests:**
- chore\(deps\): update dependencies [\#3468](https://github.com/w3c/respec/pull/3468) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(CHANGELOG\): regenerate [\#3461](https://github.com/w3c/respec/pull/3461) ([github-actions[bot]](https://github.com/apps/github-actions))
- fix\(core/dfn-panel\): make dfn panel keyboard accessible [\#3458](https://github.com/w3c/respec/pull/3458) ([marcoscaceres](https://github.com/marcoscaceres))
## [v26.5.1](https://github.com/w3c/respec/tree/v26.5.1) (2021-04-19)
[Full Changelog](https://github.com/w3c/respec/compare/v26.5.0...v26.5.1)
**Fixed bugs:**
- WebIDL CSS isn't injected [\#3442](https://github.com/w3c/respec/issues/3442)
- Some refs from HTML starting with "DOM" aren't available [\#3441](https://github.com/w3c/respec/issues/3441)
**Closed issues:**
- Add a link to specref with last-updated time [\#3440](https://github.com/w3c/respec/issues/3440)
- Documentation: export is no longer using shepherd [\#3430](https://github.com/w3c/respec/issues/3430)
**Merged pull requests:**
- fix\(core/webidl\): ensure loading of webidl.css [\#3460](https://github.com/w3c/respec/pull/3460) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(tools/dev-server\): output red errors [\#3459](https://github.com/w3c/respec/pull/3459) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(tools/dev-server\): run dev server before tests, help msg [\#3456](https://github.com/w3c/respec/pull/3456) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(tools/\*\): disallow unknown options [\#3455](https://github.com/w3c/respec/pull/3455) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(tools/respec2html\): disallow unknown options [\#3454](https://github.com/w3c/respec/pull/3454) ([sidvishnoi](https://github.com/sidvishnoi))
- refactor\(core/linter-rules/\*\): run as regular plugins [\#3453](https://github.com/w3c/respec/pull/3453) ([sidvishnoi](https://github.com/sidvishnoi))
- refactor\(core/linter/check-punctuation\): run as regular plugin [\#3452](https://github.com/w3c/respec/pull/3452) ([sidvishnoi](https://github.com/sidvishnoi))
- refactor\(core/linter/check-internal-slots\): run as regular plugin [\#3451](https://github.com/w3c/respec/pull/3451) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(package\): use node v14, npm v6 [\#3450](https://github.com/w3c/respec/pull/3450) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(deps\): update dependencies [\#3449](https://github.com/w3c/respec/pull/3449) ([sidvishnoi](https://github.com/sidvishnoi))
- tests\(core/a11y\): increase timeout [\#3439](https://github.com/w3c/respec/pull/3439) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(ci/regressions\): test respec-w3c differences across branches [\#3428](https://github.com/w3c/respec/pull/3428) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(CHANGELOG\): regenerate [\#3427](https://github.com/w3c/respec/pull/3427) ([github-actions[bot]](https://github.com/apps/github-actions))
- refactor\(tests/webidl\): update legacy constructors + cleanup [\#3425](https://github.com/w3c/respec/pull/3425) ([marcoscaceres](https://github.com/marcoscaceres))
- refactor\(core/linter/check-charset\): run as regular plugin [\#3401](https://github.com/w3c/respec/pull/3401) ([sidvishnoi](https://github.com/sidvishnoi))
## [v26.5.0](https://github.com/w3c/respec/tree/v26.5.0) (2021-03-31)
[Full Changelog](https://github.com/w3c/respec/compare/v26.4.1...v26.5.0)
**Fixed bugs:**
- wgPatentPolicy and CG? [\#3420](https://github.com/w3c/respec/issues/3420)
**Closed issues:**
- Add a global ReSpec API [\#3089](https://github.com/w3c/respec/issues/3089)
- Use custom ReSpec version while generating via respec2html [\#2874](https://github.com/w3c/respec/issues/2874)
**Merged pull requests:**
- fix\(w3c/headers\): wgPatentPolicy may be falsy [\#3424](https://github.com/w3c/respec/pull/3424) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(deps-dev\): bump karma from 6.2.0 to 6.3.2 [\#3423](https://github.com/w3c/respec/pull/3423) ([dependabot[bot]](https://github.com/apps/dependabot))
- chore\(deps-dev\): bump rollup from 2.42.2 to 2.44.0 [\#3422](https://github.com/w3c/respec/pull/3422) ([dependabot[bot]](https://github.com/apps/dependabot))
- refactor\(tests/headless\): use jasmine instead of custom runner [\#3421](https://github.com/w3c/respec/pull/3421) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(CHANGELOG\): regenerate [\#3419](https://github.com/w3c/respec/pull/3419) ([github-actions[bot]](https://github.com/apps/github-actions))
- tests: support testing individual modules [\#3418](https://github.com/w3c/respec/pull/3418) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(deps\): update dependencies [\#3417](https://github.com/w3c/respec/pull/3417) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(tools/builder\): add a --watch flag [\#3406](https://github.com/w3c/respec/pull/3406) ([sidvishnoi](https://github.com/sidvishnoi))
- feat\(core/respec-global\): add toHTML\(\) method [\#3403](https://github.com/w3c/respec/pull/3403) ([sidvishnoi](https://github.com/sidvishnoi))
- feat\(respecDocWriter\): use locally installed respec version [\#2957](https://github.com/w3c/respec/pull/2957) ([sidvishnoi](https://github.com/sidvishnoi))
## [v26.4.1](https://github.com/w3c/respec/tree/v26.4.1) (2021-03-29)
[Full Changelog](https://github.com/w3c/respec/compare/v26.4.0...v26.4.1)
**Fixed bugs:**
- "group" option still complains about patent policy [\#3399](https://github.com/w3c/respec/issues/3399)
**Merged pull requests:**
- fix\(w3c/headers\): handle multiple patent policies [\#3411](https://github.com/w3c/respec/pull/3411) ([marcoscaceres](https://github.com/marcoscaceres))
- refactor\(tools/dev-server\): add promisified KarmaServer [\#3410](https://github.com/w3c/respec/pull/3410) ([sidvishnoi](https://github.com/sidvishnoi))
- tests\(build\): fix fileExists check, remove chai, run in CI [\#3408](https://github.com/w3c/respec/pull/3408) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(karma.conf.js\): cleanup [\#3407](https://github.com/w3c/respec/pull/3407) ([sidvishnoi](https://github.com/sidvishnoi))
- refactor\(ui/search-specref\): embed iframe from respec.org [\#3405](https://github.com/w3c/respec/pull/3405) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(core/biblio\): use specref.org API endpoint [\#3404](https://github.com/w3c/respec/pull/3404) ([sidvishnoi](https://github.com/sidvishnoi))
- refactor\(styles\): convert css files to js [\#3402](https://github.com/w3c/respec/pull/3402) ([sidvishnoi](https://github.com/sidvishnoi))
- chore: remove unused files, config and packges [\#3400](https://github.com/w3c/respec/pull/3400) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(deps\): update dependencies [\#3397](https://github.com/w3c/respec/pull/3397) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(deps-dev\): use updated rollup plugins [\#3392](https://github.com/w3c/respec/pull/3392) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(CHANGELOG\): regenerate [\#3391](https://github.com/w3c/respec/pull/3391) ([github-actions[bot]](https://github.com/apps/github-actions))
- refactor: build respec-highlight alongside respec-worker [\#3390](https://github.com/w3c/respec/pull/3390) ([sidvishnoi](https://github.com/sidvishnoi))
## [v26.4.0](https://github.com/w3c/respec/tree/v26.4.0) (2021-03-21)
[Full Changelog](https://github.com/w3c/respec/compare/v26.3.0...v26.4.0)
**Fixed bugs:**
- The browser support feature shows incorrect data [\#3213](https://github.com/w3c/respec/issues/3213)
**Merged pull requests:**
- fix\(core/link-to-dfn\): make shortName matching stricter [\#3389](https://github.com/w3c/respec/pull/3389) ([marcoscaceres](https://github.com/marcoscaceres))
- fix\(conformance\): use datatracker for IETF urls [\#3388](https://github.com/w3c/respec/pull/3388) ([dontcallmedom](https://github.com/dontcallmedom))
- feat\(tools/respec2html\): provide more detailed logs [\#3387](https://github.com/w3c/respec/pull/3387) ([sidvishnoi](https://github.com/sidvishnoi))
- feat\(tools/respec2html\): make logs prettier [\#3386](https://github.com/w3c/respec/pull/3386) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(deps\): update dependencies [\#3385](https://github.com/w3c/respec/pull/3385) ([sidvishnoi](https://github.com/sidvishnoi))
- fix\(core/xref\): missing . in error message [\#3380](https://github.com/w3c/respec/pull/3380) ([marcoscaceres](https://github.com/marcoscaceres))
- chore\(CHANGELOG\): regenerate [\#3375](https://github.com/w3c/respec/pull/3375) ([github-actions[bot]](https://github.com/apps/github-actions))
## [v26.3.0](https://github.com/w3c/respec/tree/v26.3.0) (2021-03-11)
[Full Changelog](https://github.com/w3c/respec/compare/v26.2.0...v26.3.0)
**Merged pull requests:**
- feat\(core/inlines\): support attr-value type for elements [\#3374](https://github.com/w3c/respec/pull/3374) ([sidvishnoi](https://github.com/sidvishnoi))
- styles\(caniuse\): align styles with caniuse.com [\#3371](https://github.com/w3c/respec/pull/3371) ([sidvishnoi](https://github.com/sidvishnoi))
- chore\(CHANGELOG\): regenerate [\#3370](https://github.com/w3c/respec/pull/3370) ([github-actions[bot]](https://github.com/apps/github-actions))
## [v26.2.0](https://github.com/w3c/respec/tree/v26.2.0) (2021-03-08)
[Full Changelog](https://github.com/w3c/respec/compare/v26.1.2...v26.2.0)

View File

@ -1,7 +0,0 @@
/* For assertions in lists containing algorithms */
.assert {
background: #EEE;
border-left: 0.5em solid #AAA;
padding: 0.3em;
}

View File

@ -1,54 +0,0 @@
/* --- ISSUES/NOTES --- */
.issue-label {
text-transform: initial;
}
.warning > p:first-child { margin-top: 0 }
.warning {
padding: .5em;
border-left-width: .5em;
border-left-style: solid;
}
span.warning { padding: .1em .5em .15em; }
.issue.closed span.issue-number {
text-decoration: line-through;
}
.warning {
border-color: #f11;
border-width: .2em;
border-style: solid;
background: #fbe9e9;
}
.warning-title:before{
content: "⚠"; /*U+26A0 WARNING SIGN*/
font-size: 1.3em;
float: left;
padding-right: .3em;
margin-top: -0.3em;
}
li.task-list-item {
list-style: none;
}
input.task-list-item-checkbox {
margin: 0 0.35em 0.25em -1.6em;
vertical-align: middle;
}
.issue a.respec-gh-label {
padding: 5px;
margin: 0 2px 0 2px;
font-size: 10px;
text-transform: none;
text-decoration: none;
font-weight: bold;
border-radius: 4px;
position: relative;
bottom: 2px;
border: none;
display: inline-block;
}

476
builds/respec-aom.js Normal file

File diff suppressed because one or more lines are too long

1
builds/respec-aom.js.map Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,4 @@
// ReSpec Worker v1.0.0
"use strict";
try {
importScripts("https://www.w3.org/Tools/respec/respec-highlight");
} catch (err) {

View File

@ -1,52 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta charset="utf-8" />
<title>Replace me with a real title</title>
<script src="../builds/respec-w3c.js" class='remove'></script>
<script class='remove'>
<script src="../builds/respec-w3c.js" class="remove" defer></script>
<script class="remove">
// All config options at https://respec.org/docs/
var respecConfig = {
specStatus: "ED",
editors: [{
name: "Your Name",
url: "https://your-site.com",
}],
github: "https://github.com/w3c/some-API/",
editors: [
{
name: "Your Name",
url: "https://your-site.com",
},
],
github: "w3c/respec",
shortName: "respec",
xref: "web-platform",
group: "webapps",
};
</script>
</head>
<body>
<section id='abstract'>
<p>
This is required.
</p>
<section id="abstract">
<p>This is required.</p>
</section>
<section id='sotd'>
<p>
This is required.
</p>
<section id="sotd">
<p>This is required.</p>
</section>
<section class="informative">
<h2>Introduction</h2>
<p>Some informative introductory text.</p>
<aside class="note" title="A useful note">
<p>I'm a note!</p>
</aside>
</section>
<section>
<h2>A section</h2>
<aside class="example">
<p>This is an example.</p>
<pre class="js">
// Automatic syntax highlighting
function someJavaScript(){}
</pre>
</aside>
<section>
<h3>I'm a sub-section</h3>
<p class="issue" data-number="3524">
<!-- Issue can automatically be populated from GitHub -->
</p>
</section>
</section>
<section data-dfn-for="Foo">
<h2><dfn>Foo</dfn> interface</h2>
<h2>Start your spec!</h2>
<pre class="idl">
[Exposed=Window]
interface Foo {
constructor();
[Exposed=Window]
interface Foo {
attribute DOMString bar;
undefined doTheFoo();
};
</pre>
<p>The <a>Foo</a> interface is nice. Lets you do stuff.</p>
<p>The <dfn>Foo.constructor()</dfn> creates a Foo instance.</p>
<p>The <dfn>bar</dfn> attribute, returns 🍺.</p>
<p>The <dfn>doTheFoo()</dfn> method, returns nothing.</p>
<pre class="js example" title="Usage example">
const foo = new Foo();
if (foo.bar === "my bar") {
foo.doTheBar();
}
};
</pre>
<p>The <dfn>Foo</dfn> interface represents a {{Foo}}.</p>
<p>
The <dfn>doTheFoo()</dfn> method does the foo. Call it by running
{{Foo/doTheFoo()}}.
</p>
<ol class="algorithm">
<li>A |variable:DOMString| can be declared like this.</li>
</ol>
</section>
<section id="conformance">
<p>
This is required for specifications that contain normative material.
</p>
</section>
<section id="index"></section>
</body>
</html>

View File

@ -1,120 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>
Replace me with a real title
</title>
<script src='../profiles/w3c.js' async class='remove'></script>
<script class='remove'>
<meta charset="utf-8" />
<title>Replace me with a real title</title>
<script src="../profiles/w3c.js" type="module" class="remove"></script>
<script class="remove">
// All config options at https://respec.org/docs/
var respecConfig = {
specStatus: "ED",
subtitle: "Subtitle",
shortName: "shortname-api",
editors: [{
name: "Your Name",
url: "https://your-site.com",
}],
github: "https://github.com/w3c/some-API/",
testSuiteURI: "https://w3c-test.org/some-API/",
implementationReportURI: "https://w3c.github.io/test-results/some-API",
// RESPEC_API_BASE: "http://localhost:8000/",
caniuse: "payment-request",
xref: "web-platform"
editors: [
{
name: "Your Name",
url: "https://your-site.com",
},
],
github: "w3c/respec",
shortName: "respec",
xref: "web-platform",
group: "webapps",
};
</script>
</head>
<body>
<section id='abstract'>
<p>
This is required.
</p>
<section id="abstract">
<p>This is required.</p>
</section>
<section id='sotd'>
<p>
This is required.
</p>
<section id="sotd">
<p>This is required.</p>
</section>
<section class="introductory" id="overview">
<h2>
Overview
</h2>
<p>
This section has a CSS class "<code>introductory</code>", which means
it will not appear in the <abbr title="Table of Contents">TOC</abbr>.
Example Link to Section <a href="#logo"></a>.
</p>
<section class="informative">
<h2>Introduction</h2>
<p>Some informative introductory text.</p>
<aside class="note" title="A useful note">
<p>I'm a note!</p>
</aside>
</section>
<section class="informative" id="intro">
<h2>
Introduction
</h2>
<p>
This section has a CSS class "<code>informative</code>", so it is
listed in the TOC. ReSpec will list this reference as non-normative
since this is an informative section [[DOM]].
</p>
<section>
<h2>A section</h2>
<aside class="example">
<p>This is an example.</p>
<pre class="js">
// Automatic syntax highlighting
function someJavaScript(){}
</pre>
</aside>
<section>
<h3>I'm a sub-section</h3>
<p class="issue" data-number="3524">
<!-- Issue can automatically be populated from GitHub -->
</p>
</section>
</section>
<section data-dfn-for="Foo">
<h2>
<dfn>Foo</dfn> interface
</h2>
<h2>Start your spec!</h2>
<pre class="idl">
[Exposed=Window]
interface Foo {
constructor();
[Exposed=Window]
interface Foo {
attribute DOMString bar;
undefined doTheFoo();
};
};
</pre>
<p>The <dfn>Foo</dfn> interface represents a {{Foo}}.</p>
<p>
The <a>Foo</a> interface is nice. Lets you do stuff.
The <dfn>doTheFoo()</dfn> method does the foo. Call it by running
{{Foo/doTheFoo()}}.
</p>
<p>
The <dfn>Foo.constructor()</dfn> creates a Foo instance.
</p>
<p>
The <dfn>bar</dfn> attribute, returns 🍺.
</p>
<p>
The <dfn>doTheFoo()</dfn> method, returns nothing.
</p>
<p>
ReSpec will list this reference as normative [[ECMASCRIPT]].
</p>
<p>
This is an example of a non-normative reference in a normative section
[[?HTML]]
</p>
<pre class="js example" title="Usage example">
const foo = new Foo();
if (foo.bar === "my bar") {
foo.doTheBar();
}
</pre>
<ol class="algorithm">
<li>A |variable:DOMString| can be declared like this.</li>
</ol>
</section>
<section id="logo">
<h2>
Figure
</h2>
<figure id="figure">
<img src="figure.svg" alt="W3C Logo">
<figcaption>
The W3C logo
</figcaption>
</figure>
<section id="conformance">
<p>
Link to <a href="#figure"></a>
This is required for specifications that contain normative material.
</p>
</section>
<section id="idl-index" class="appendix">
<h2>
IDL Index
</h2>
<p>
All the Web IDL in this specification is collected in this section.
</p>
</section>
<section id="tof" class="informative appendix"></section>
<section id="index"></section>
</body>
</html>

93
examples/example_aom.html Normal file
View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>
Replace me with a real title
</title>
<script src='../profiles/aom.js' type="module" class='remove'></script>
<script class='remove'>
var respecConfig = {
specStatus: "PD",
subtitle: "Subtitle",
shortName: "shortname-spec",
editors: [{
name: "Your Name",
url: "https://your-site.com",
company: "My company",
companyURL: "https://your-site.com",
}, {
name: "Your Name 2",
url: "https://your-site.com",
mailto: "name2@editor.org",
note: "A great spec editor"
}],
formerEditors: [{
name: "Your Name",
url: "https://your-site.com",
}, {
name: "Your Name 2",
url: "https://your-site.com",
}],
publishDate: "2021-04-23",
// Defaults to 'gh-pages' branch.
// See https://respec.org/docs/#github if you need a different branch (e.g., "main").
github: "AOMedia/some-spec"
};
</script>
</head>
<body>
<section id='abstract'>
<p>
This is the abstract (required).
</p>
</section>
<section class="informative" id="intro">
<h2>
Introduction
</h2>
<p>
This section has a CSS class "<code>informative</code>", so it is
listed in the TOC. ReSpec will list this reference as non-normative
since this is an informative section [[HTML]].
</p>
</section>
<section id="conformance">
</section>
<section data-dfn-for="Foo">
<h2>
<dfn>Foo</dfn> structure
</h2>
<p>
The <a>Foo</a> structure is nice. Lets you do stuff. You SHALL use it.
</p>
<p>
ReSpec will list this reference as normative, like [[AV1]]. You SHOULD use references.
</p>
<p>
This is an example of a non-normative reference in a normative section
[[?JPEG]]
</p>
</section>
<section id="example-figure">
<h2>
Figure
</h2>
<figure id="figure">
<img src="https://aomedia.org/assets/images/aomedia-icon-only.png" alt="AOM Logo">
<figcaption>
The AOM logo
</figcaption>
</figure>
<p>
Link to [[[#figure]]] ... the three square-brackets "expand" a local reference.
</p>
</section>
<section id="tof" class="appendix">
<!-- a table of figures will be added here -->
</section>
<section id="index" class="appendix">
<!-- Index of terms defined/used by this specification will be added here -->
</section>
</body>
</html>

View File

@ -1,6 +1,6 @@
// rollup.config.js
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
const template = {
output: {

View File

@ -1,148 +0,0 @@
// Karma configuration
// Generated on Fri Feb 26 2016 13:09:51 GMT+1100 (AEDT)
"use strict";
module.exports = function (config) {
const options = {
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: "./",
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ["jasmine"],
// list of files / patterns to load in the browser
files: [
{
pattern: "assets/**/*.*",
included: false,
},
{
pattern: "tests/support-files/**/*",
included: false,
},
{
pattern: "builds/**/*.*",
included: false,
},
{
pattern: "js/**/*.*",
included: false,
},
{
pattern: "src/**/*.*",
included: false,
},
{
pattern: "node_modules/idb/**/*.js",
included: false,
},
{
pattern: "node_modules/hyperhtml/**/*.js",
included: false,
},
{
pattern: "node_modules/marked/**/*.js",
included: false,
},
{
pattern: "node_modules/webidl2/**/*.js",
included: false,
},
{
pattern: "tests/**/*-spec.js",
type: "module",
included: false,
},
{
pattern: "tests/data/**/*",
included: false,
},
{
pattern: "tests/**/*.html",
included: false,
},
{
pattern: "worker/*.js",
included: false,
},
{
pattern: "tests/spec/SpecHelper.js",
type: "module",
included: false,
},
{
pattern: "tests/test-main.js",
type: "module",
},
],
// list of files to exclude
exclude: ["**/*.swp", "*.swp", ".DS_Store"],
proxies: {
"/about-blank.html": "/base/tests/about-blank.html",
"/assets/": "/base/assets/",
"/js/": "/base/js/",
"/src/": "/base/src/",
"/node_modules/": "/base/node_modules/",
"/builds/": "/base/builds/",
"/tests/": "/base/tests/",
"/spec/": "/base/tests/spec/",
"/deps/": "/base/js/deps/",
"/js/deps/": "/base/js/deps/",
"/base/deps/": "/base/js/deps/",
"/worker/respec-worker.js": "/base/worker/respec-worker.js",
"/support-files/hljs-testlang.js":
"/base/tests/support-files/hljs-testlang.js",
},
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {},
// test results reporter to use
// possible values: "dots", "progress"
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["mocha", "kjhtml"],
// web server port
port: config.port || 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: true,
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: false,
// Concurrency level
// how many browser should be started simultaneous
concurrency: 1,
browserNoActivityTimeout: 100000,
reportSlowerThan: 300,
client: {
args: ["--grep", config.grep || ""],
},
};
if (process.env.BROWSERS) {
options.browsers = process.env.BROWSERS.split(" ");
}
if (process.env.GITHUB_WORKFLOW) {
const localPlugins = [require("./tools/github-action-reporter.js")];
options.reporters.push("respec-github-action");
options.plugins = ["karma-*"].concat(localPlugins);
}
config.set(options);
};

1645
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
{
"name": "respec",
"version": "26.2.0",
"version": "26.13.4",
"license": "W3C",
"description": "A technical specification pre-processor.",
"engines": {
"node": ">=10",
"npm": ">=5"
"node": ">=14",
"npm": ">=6"
},
"bin": {
"respec": "tools/respec2html.js",
@ -23,67 +23,71 @@
"Robin Berjon"
],
"devDependencies": {
"@types/marked": "^2.0.0",
"@rollup/plugin-alias": "^3.1.5",
"@rollup/plugin-commonjs": "^21.0.0",
"@rollup/plugin-node-resolve": "^13.0.5",
"@types/marked": "^3.0.1",
"@types/pluralize": "0.0.29",
"boxen": "^5.0.0",
"chai": "^4.3.3",
"chokidar": "^3.5.1",
"clean-css": "^5.1.1",
"boxen": "^5.1.2",
"chokidar": "^3.5.2",
"clean-css": "^5.2.1",
"epipebomb": "^1.0.0",
"eslint": "^7.21.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jasmine": "^4.1.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-prettier": "^4.0.0",
"highlight.js": "^11.2.0",
"hyperhtml": "^2.34.0",
"idb": "^6.0.0",
"jasmine": "^3.6.4",
"jquery": "^3.6.0",
"karma": "^6.1.1",
"idb": "^6.1.4",
"jasmine": "^3.9.0",
"jasmine-core": "^3.9.0",
"karma": "^6.3.4",
"karma-chrome-launcher": "^3.1.0",
"karma-firefox-launcher": "^2.1.0",
"karma-firefox-launcher": "^2.1.1",
"karma-jasmine": "^4.0.1",
"karma-jasmine-html-reporter": "^1.5.4",
"karma-mocha": "^2.0.1",
"karma-jasmine-html-reporter": "^1.7.0",
"karma-mocha-reporter": "^2.2.5",
"karma-safari-launcher": "^1.0.0",
"loading-indicator": "^2.0.0",
"marked": "^2.0.1",
"pluralize": "^8.0.0",
"prettier": "^2.2.1",
"prompt": "^1.1.0",
"rollup": "^2.40.0",
"rollup-plugin-alias": "^2.2.0",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-node-resolve": "^5.2.0",
"prettier": "^2.4.1",
"prompt": "^1.2.0",
"rollup": "^2.58.0",
"rollup-plugin-minify-html-literals": "^1.2.6",
"rollup-plugin-terser": "^7.0.2",
"serve": "^11.3.2",
"typescript": "^4.2.3",
"webidl2": "^23.13.1"
"serve": "^12.0.1",
"sniffy-mimetype": "^1.1.1",
"typescript": "^4.4.3",
"vnu-jar": "^21.9.2",
"webidl2": "^24.1.2"
},
"scripts": {
"build:aom-debug": "npm run build:aom -- --debug",
"build:aom": "node ./tools/builder.js aom",
"build:geonovum-debug": "npm run build:geonovum -- --debug",
"build:geonovum": "node ./tools/builder.js geonovum",
"build:w3c-debug": "npm run build:w3c -- --debug",
"build:w3c-with-jquery": "node ./tools/builder.js w3c-common",
"build:w3c": "node ./tools/builder.js w3c",
"build:dini-debug": "node run build:dini -- --debug",
"build:dini": "node ./tools/builder.js dini",
"builddeps": "rollup -c js/deps/rollup.config.js",
"karma": "karma start --single-run",
"builddeps": "rollup -c js/deps/rollup.config.js && rollup -c worker/rollup.config.js",
"lint": "tsc -p src/jsconfig.json && eslint .",
"prepare": "npm run builddeps",
"release": "node ./tools/release.js",
"server": "serve",
"start": "node ./tools/dev-server.js",
"test:build": "jasmine --random=false ./tests/test-build.js",
"test:headless": "node ./tests/headless.js",
"test:karma": "npm run karma"
"test:headless": "jasmine --random=false ./tests/headless.js",
"test": "npm run test:unit && npm run test:integration",
"test:unit": "karma start ./tests/unit/karma.conf.js --single-run",
"test:integration": "karma start ./tests/spec/karma.conf.js --single-run"
},
"dependencies": {
"colors": "^1.4.0",
"finalhandler": "^1.1.2",
"puppeteer": "^8.0.0",
"marked": "^3.0.4",
"puppeteer": "^10.4.0",
"sade": "^1.7.4",
"serve-static": "^1.14.1"
},

58
profiles/aom.js Normal file
View File

@ -0,0 +1,58 @@
import * as ReSpec from "../src/respec.js";
const modules = [
// order is significant
import("../src/core/location-hash.js"),
import("../src/core/l10n.js"),
import("../src/aom/defaults.js"),
import("../src/core/style.js"),
import("../src/aom/style.js"),
import("../src/core/data-include.js"),
import("../src/core/markdown.js"),
import("../src/core/reindent.js"),
import("../src/core/title.js"),
import("../src/aom/headers.js"),
import("../src/aom/abstract.js"),
import("../src/core/data-transform.js"),
import("../src/core/data-abbr.js"),
import("../src/core/inlines.js"),
import("../src/aom/conformance.js"),
import("../src/core/dfn.js"),
import("../src/core/pluralize.js"),
import("../src/core/examples.js"),
import("../src/core/issues-notes.js"),
import("../src/core/best-practices.js"),
import("../src/core/figures.js"),
import("../src/core/biblio.js"),
import("../src/core/link-to-dfn.js"),
import("../src/core/data-cite.js"),
import("../src/core/render-biblio.js"),
import("../src/core/contrib.js"),
import("../src/core/fix-headers.js"),
import("../src/core/structure.js"),
import("../src/core/informative.js"),
import("../src/core/id-headers.js"),
import("../src/ui/save-html.js"),
import("../src/ui/about-respec.js"),
import("../src/core/seo.js"),
import("../src/core/highlight.js"),
import("../src/core/list-sorter.js"),
import("../src/core/highlight-vars.js"),
import("../src/core/dfn-panel.js"),
import("../src/core/data-type.js"),
import("../src/core/algorithms.js"),
import("../src/core/anchor-expander.js"),
import("../src/core/custom-elements/index.js"),
/* Linter must be the last thing to run */
import("../src/core/linter-rules/check-charset.js"),
import("../src/core/linter-rules/check-punctuation.js"),
import("../src/core/linter-rules/local-refs-exist.js"),
import("../src/core/linter-rules/no-headingless-sections.js"),
import("../src/core/linter-rules/no-unused-vars.js"),
import("../src/core/linter-rules/privsec-section.js"),
import("../src/core/linter-rules/no-http-props.js"),
];
Promise.all(modules)
.then(plugins => ReSpec.run(plugins))
.catch(err => console.error(err));

View File

@ -1,13 +1,7 @@
"use strict";
// In case everything else fails, we want the error
window.addEventListener("error", ev => {
console.error(ev.error, ev.message, ev);
});
import * as ReSpec from "../src/respec.js";
const modules = [
// order is significant
import("../src/core/base-runner.js"),
import("../src/core/ui.js"),
import("../src/core/location-hash.js"),
import("../src/core/l10n.js"),
import("../src/dini/defaults.js"),
@ -50,26 +44,15 @@ const modules = [
import("../src/core/anchor-expander.js"),
import("../src/core/custom-elements/index.js"),
/* Linter must be the last thing to run */
import("../src/core/linter.js"),
import("../src/core/linter-rules/check-charset.js"),
import("../src/core/linter-rules/check-punctuation.js"),
import("../src/core/linter-rules/local-refs-exist.js"),
import("../src/core/linter-rules/no-headingless-sections.js"),
import("../src/core/linter-rules/no-unused-vars.js"),
import("../src/core/linter-rules/privsec-section.js"),
import("../src/core/linter-rules/no-http-props.js"),
];
async function domReady() {
if (document.readyState === "loading") {
await new Promise(resolve =>
document.addEventListener("DOMContentLoaded", resolve)
);
}
}
(async () => {
const [runner, { ui }, ...plugins] = await Promise.all(modules);
try {
ui.show();
await domReady();
await runner.runAll(plugins);
} finally {
ui.enable();
}
})().catch(err => {
console.error(err);
});
Promise.all(modules)
.then(plugins => ReSpec.run(plugins))
.catch(err => console.error(err));

View File

@ -1,13 +1,7 @@
"use strict";
// In case everything else fails, we want the error
window.addEventListener("error", ev => {
console.error(ev.error, ev.message, ev);
});
import * as ReSpec from "../src/respec.js";
const modules = [
// order is significant
import("../src/core/base-runner.js"),
import("../src/core/ui.js"),
import("../src/core/location-hash.js"),
import("../src/core/l10n.js"),
import("../src/geonovum/defaults.js"),
@ -48,26 +42,15 @@ const modules = [
import("../src/core/algorithms.js"),
import("../src/core/anchor-expander.js"),
/* Linter must be the last thing to run */
import("../src/core/linter.js"),
import("../src/core/linter-rules/check-charset.js"),
import("../src/core/linter-rules/check-punctuation.js"),
import("../src/core/linter-rules/local-refs-exist.js"),
import("../src/core/linter-rules/no-headingless-sections.js"),
import("../src/core/linter-rules/no-unused-vars.js"),
import("../src/core/linter-rules/privsec-section.js"),
import("../src/core/linter-rules/no-http-props.js"),
];
async function domReady() {
if (document.readyState === "loading") {
await new Promise(resolve =>
document.addEventListener("DOMContentLoaded", resolve)
);
}
}
(async () => {
const [runner, { ui }, ...plugins] = await Promise.all(modules);
try {
ui.show();
await domReady();
await runner.runAll(plugins);
} finally {
ui.enable();
}
})().catch(err => {
console.error(err);
});
Promise.all(modules)
.then(plugins => ReSpec.run(plugins))
.catch(err => console.error(err));

View File

@ -1,107 +0,0 @@
/**
* The w3c-common profile is DEPRECATED.
* Update (Apr 15, 2020): The w3c-common profile will no longer receive updates.
* It is frozen at v25.5.0.
*
* The difference between w3c-common and the w3c profile
* is that this profile includes jQuery to support legacy
* specs.
*
* However, you are strongly discouraged from using
* jQuery. Please use vanilla JS/DOM instead:
*
* http://youmightnotneedjquery.com
*
* If you need help, reach out to the ReSpec maintainers.
*/
"use strict";
// In case everything else fails, we want the error
window.addEventListener("error", ev => {
console.error(ev.error, ev.message, ev);
});
const modules = [
// order is significant
import("../src/core/base-runner.js"),
import("../src/core/ui.js"),
import("../src/core/jquery-enhanced.js"),
import("../src/core/location-hash.js"),
import("../src/core/l10n.js"),
import("../src/w3c/defaults.js"),
import("../src/core/style.js"),
import("../src/w3c/style.js"),
import("../src/w3c/l10n.js"),
import("../src/core/github.js"),
import("../src/core/data-include.js"),
import("../src/core/markdown.js"),
import("../src/core/reindent.js"),
import("../src/core/title.js"),
import("../src/w3c/level.js"),
import("../src/w3c/headers.js"),
import("../src/w3c/abstract.js"),
import("../src/core/data-transform.js"),
import("../src/core/data-abbr.js"),
import("../src/core/inlines.js"),
import("../src/w3c/conformance.js"),
import("../src/core/dfn.js"),
import("../src/core/pluralize.js"),
import("../src/core/examples.js"),
import("../src/core/issues-notes.js"),
import("../src/core/best-practices.js"),
import("../src/core/figures.js"),
import("../src/core/webidl.js"),
import("../src/core/biblio.js"),
import("../src/core/link-to-dfn.js"),
import("../src/core/xref.js"),
import("../src/core/data-cite.js"),
import("../src/core/webidl-index.js"),
import("../src/core/render-biblio.js"),
import("../src/core/dfn-index.js"),
import("../src/core/contrib.js"),
import("../src/core/fix-headers.js"),
import("../src/core/structure.js"),
import("../src/core/informative.js"),
import("../src/core/id-headers.js"),
import("../src/core/caniuse.js"),
import("../src/core/mdn-annotation.js"),
import("../src/ui/save-html.js"),
import("../src/ui/search-specref.js"),
import("../src/ui/about-respec.js"),
import("../src/core/seo.js"),
import("../src/w3c/seo.js"),
import("../src/core/highlight.js"),
import("../src/core/webidl-clipboard.js"),
import("../src/core/data-tests.js"),
import("../src/core/list-sorter.js"),
import("../src/core/highlight-vars.js"),
import("../src/core/dfn-panel.js"),
import("../src/core/data-type.js"),
import("../src/core/algorithms.js"),
import("../src/core/anchor-expander.js"),
import("../src/core/custom-elements/index.js"),
/* Linter must be the last thing to run */
import("../src/core/linter.js"),
import("../src/core/a11y.js"),
];
async function domReady() {
if (document.readyState === "loading") {
await new Promise(resolve =>
document.addEventListener("DOMContentLoaded", resolve)
);
}
}
(async () => {
const [runner, { ui }, ...plugins] = await Promise.all(modules);
try {
ui.show();
await domReady();
await runner.runAll(plugins);
} finally {
ui.enable();
}
})().catch(err => {
console.error(err);
});

View File

@ -1,13 +1,7 @@
"use strict";
// In case everything else fails, we want the error
window.addEventListener("error", ev => {
console.error(ev.error, ev.message, ev);
});
import * as ReSpec from "../src/respec.js";
const modules = [
// order is significant
import("../src/core/base-runner.js"),
import("../src/core/ui.js"),
import("../src/core/location-hash.js"),
import("../src/core/l10n.js"),
import("../src/w3c/group.js"),
@ -64,28 +58,20 @@ const modules = [
import("../src/core/algorithms.js"),
import("../src/core/anchor-expander.js"),
import("../src/core/custom-elements/index.js"),
import("../src/core/web-monetization.js"),
/* Linters must be the last thing to run */
import("../src/core/linter.js"),
import("../src/core/a11y.js"),
import("../src/core/linter-rules/check-charset.js"),
import("../src/core/linter-rules/check-punctuation.js"),
import("../src/core/linter-rules/check-internal-slots.js"),
import("../src/core/linter-rules/local-refs-exist.js"),
import("../src/core/linter-rules/no-headingless-sections.js"),
import("../src/core/linter-rules/no-unused-vars.js"),
import("../src/core/linter-rules/privsec-section.js"),
import("../src/core/linter-rules/wpt-tests-exist.js"),
import("../src/core/linter-rules/no-http-props.js"),
import("../src/core/linter-rules/a11y.js"),
];
async function domReady() {
if (document.readyState === "loading") {
await new Promise(resolve =>
document.addEventListener("DOMContentLoaded", resolve)
);
}
}
(async () => {
const [runner, { ui }, ...plugins] = await Promise.all(modules);
try {
ui.show();
await domReady();
await runner.runAll(plugins);
} finally {
ui.enable();
}
})().catch(err => {
console.error(err);
});
Promise.all(modules)
.then(plugins => ReSpec.run(plugins))
.catch(err => console.error(err));

29
src/aom/abstract.js Normal file
View File

@ -0,0 +1,29 @@
// @ts-check
// Module aom/abstract
// Handle the abstract section properly.
import { getIntlData, showError } from "../core/utils.js";
export const name = "aom/abstract";
const localizationStrings = {
en: {
abstract: "Abstract",
},
};
const l10n = getIntlData(localizationStrings);
export async function run() {
const abs = document.getElementById("abstract");
if (!abs) {
const msg = `Document must have one element with \`id="abstract"`;
showError(msg, name);
return;
}
abs.classList.add("introductory");
let abstractHeading = document.querySelector("#abstract>h2");
if (abstractHeading) {
return;
}
abstractHeading = document.createElement("h2");
abstractHeading.textContent = l10n.abstract;
abs.prepend(abstractHeading);
}

66
src/aom/conformance.js Normal file
View File

@ -0,0 +1,66 @@
// @ts-check
// Module aom/conformance
// Handle the conformance section properly.
import { getIntlData, htmlJoinAnd, showWarning } from "../core/utils.js";
import { html } from "../core/import-maps.js";
import { renderInlineCitation } from "../core/render-biblio.js";
import { rfc2119Usage } from "../core/inlines.js";
export const name = "aom/conformance";
const localizationStrings = {
en: {
conformance: "Conformance",
normativity:
"As well as sections marked as non-normative, all authoring guidelines, " +
"diagrams, examples, and notes in this specification are non-normative. " +
"Everything else in this specification is normative.",
keywordInterpretation(keywords, plural) {
return html`<p>
The key word${plural ? "s" : ""} ${keywords} in this document
${plural ? "are" : "is"} to be interpreted as described in
<a href="https://datatracker.ietf.org/doc/html/bcp14">BCP 14</a>
${renderInlineCitation("RFC2119")} ${renderInlineCitation("RFC8174")}
when, and only when, they appear in all capitals, as shown here.
</p>`;
},
},
};
const l10n = getIntlData(localizationStrings);
/**
* @param {Element} conformance
* @param {*} conf
*/
function processConformance(conformance, conf) {
const terms = [...Object.keys(rfc2119Usage)];
// Add RFC2119 to bibliography
if (terms.length) {
conf.normativeReferences.add("RFC2119");
conf.normativeReferences.add("RFC8174");
}
// Put in the 2119 clause and reference
const keywords = htmlJoinAnd(
terms.sort(),
item => html`<em class="rfc2119">${item}</em>`
);
const plural = terms.length > 1;
const content = html`
<h2>${l10n.conformance}</h2>
<p>${l10n.normativity}</p>
${terms.length ? l10n.keywordInterpretation(keywords, plural) : null}
`;
conformance.prepend(...content.childNodes);
}
export function run(conf) {
const conformance = document.querySelector("section#conformance");
if (conformance && !conformance.classList.contains("override")) {
processConformance(conformance, conf);
}
// Warn when there are RFC2119/RFC8174 keywords, but not conformance section
if (!conformance && Object.keys(rfc2119Usage).length) {
const msg = `Document uses RFC2119 keywords but lacks a conformance section.`;
const hint = 'Please add a `<section id="conformance">`.';
showWarning(msg, name, { hint });
}
}

63
src/aom/defaults.js Normal file
View File

@ -0,0 +1,63 @@
// @ts-check
/**
* Sets the defaults for AOM specs
*/
export const name = "aom/defaults";
import { coreDefaults } from "../core/defaults.js";
const licenses = new Map([
[
"aom",
{
name: "Alliance for Open Media License",
short: "AOM",
url: "http://aomedia.org/license/",
},
],
]);
const aomDefaults = {
// treat document as "Common Markdown" (with a little bit of HTML).
// choice between Markdown and HTML depends on the complexity of the spec
// example of Markdown spec: https://github.com/WICG/netinfo/blob/gh-pages/index.html
// Helpful guide: https://respec.org/docs/#markdown
format: "markdown",
logos: [
{
src: "https://aomedia.org/assets/images/aomedia-icon-only.png",
alt: "AOM",
id: "AOM",
height: 170,
width: 170,
url: "https://aomedia.org/",
},
],
license: "aom",
};
function computeProps(conf) {
return {
licenseInfo: licenses.get(conf.license),
};
}
export function run(conf) {
// assign the defaults
const lint =
conf.lint === false
? false
: {
...coreDefaults.lint,
...aomDefaults.lint,
...conf.lint,
};
Object.assign(conf, {
...coreDefaults,
...aomDefaults,
...conf,
lint,
});
// computed properties
Object.assign(conf, computeProps(conf));
}

153
src/aom/headers.js Normal file
View File

@ -0,0 +1,153 @@
// @ts-check
// Module aom/headers
// Generate the headers material based on the provided configuration.
// CONFIGURATION
// - specStatus: the short code for the specification's maturity level or type (required)
// - editors: an array of people editing the document (at least one is required). People
// are defined using:
// - name: the person's name (required)
// - url: URI for the person's home page
// - company: the person's company
// - companyURL: the URI for the person's company
// - mailto: the person's email
// - note: a note on the person (e.g. former editor)
// - authors: an array of people who are contributing authors of the document.
// - formerEditors: an array of people that had earlier edited the document but no longer edit.
// - subtitle: a subtitle for the specification
// - publishDate: the date to use for the publication, default to document.lastModified, and
// failing that to now. The format is YYYY-MM-DD or a Date object.
// - alternateFormats: a list of alternate formats for the document, each of which being
// defined by:
// - uri: the URI to the alternate
// - label: a label for the alternate
// - lang: optional language
// - type: optional MIME type
// - logos: a list of logos to use instead of the W3C logo, each of which being defined by:
// - src: the URI to the logo (target of <img src=>)
// - alt: alternate text for the image (<img alt=>), defaults to "Logo" or "Logo 1", "Logo 2", ...
// if src is not specified, this is the text of the "logo"
// - height: optional height of the logo (<img height=>)
// - width: optional width of the logo (<img width=>)
// - url: the URI to the organization represented by the logo (target of <a href=>)
// - id: optional id for the logo, permits custom CSS (wraps logo in <span id=>)
// - each logo element must specify either src or alt
// - otherLinks: an array of other links that you might want in the header (e.g., link github, twitter, etc).
// Example of usage: [{key: "foo", href:"https://b"}, {key: "bar", href:"https://"}].
// Allowed values are:
// - key: the key for the <dt> (e.g., "Bug Tracker"). Required.
// - value: The value that will appear in the <dd> (e.g., "GitHub"). Optional.
// - href: a URL for the value (e.g., "https://foo.com/issues"). Optional.
// - class: a string representing CSS classes. Optional.
// - license: can be one of the following
// - "aom"
import { ISODate, showError } from "../core/utils.js";
import headersTmpl from "./templates/headers.js";
import { pub } from "../core/pubsubhub.js";
export const name = "aom/headers";
const status2text = {
PD: "Pre-Draft",
WGD: "AOM Work Group Draft",
WGA: "AOM Working Group Approved Draft",
FD: "AOM Final Deliverable",
};
const AOMDate = new Intl.DateTimeFormat(["en-US"], {
timeZone: "UTC",
year: "numeric",
month: "long",
day: "2-digit",
});
/**
* @param {*} conf
* @param {string} prop
* @param {string | number | Date} fallbackDate
*/
function validateDateAndRecover(conf, prop, fallbackDate = new Date()) {
const date = conf[prop] ? new Date(conf[prop]) : new Date(fallbackDate);
// if date is valid
if (Number.isFinite(date.valueOf())) {
const formattedDate = ISODate.format(date);
return new Date(formattedDate);
}
const msg =
`[\`${prop}\`](https://github.com/w3c/respec/wiki/${prop}) ` +
`is not a valid date: "${conf[prop]}". Expected format 'YYYY-MM-DD'.`;
showError(msg, name);
return new Date(ISODate.format(new Date()));
}
export function run(conf) {
if (!conf.specStatus) {
const msg = "Missing required configuration: `specStatus`";
showError(msg, name);
}
conf.title = document.title || "No title";
if (!conf.subtitle) conf.subtitle = "";
conf.publishDate = validateDateAndRecover(
conf,
"publishDate",
document.lastModified
);
conf.thisVersion = `https://aomediacodec.github.io/${conf.shortName}/`;
conf.issueTracker = `https://github.com/AOMediaCodec/${conf.shortName}/issues/`;
conf.publishYear = conf.publishDate.getUTCFullYear();
conf.publishHumanDate = AOMDate.format(conf.publishDate);
const peopCheck = function (it) {
if (!it.name) {
const msg = "All authors and editors must have a name.";
showError(msg, name);
}
};
if (!conf.formerEditors) conf.formerEditors = [];
if (conf.editors) {
conf.editors.forEach(peopCheck);
// Move any editors with retiredDate to formerEditors.
for (let i = 0; i < conf.editors.length; i++) {
const editor = conf.editors[i];
if ("retiredDate" in editor) {
conf.formerEditors.push(editor);
conf.editors.splice(i--, 1);
}
}
}
if (!conf.editors || conf.editors.length === 0) {
const msg = "At least one editor is required";
showError(msg, name);
}
if (conf.formerEditors.length) {
conf.formerEditors.forEach(peopCheck);
}
if (conf.authors) {
conf.authors.forEach(peopCheck);
}
conf.multipleEditors = conf.editors && conf.editors.length > 1;
conf.multipleFormerEditors = conf.formerEditors.length > 1;
conf.multipleAuthors = conf.authors && conf.authors.length > 1;
// (conf.alternateFormats || []).forEach(it => {
// if (!it.uri || !it.label) {
// const msg = "All alternate formats must have a uri and a label.";
// showError(msg, name);
// }
// });
if (conf.copyrightStart && conf.copyrightStart == conf.publishYear)
conf.copyrightStart = "";
conf.textStatus = status2text[conf.specStatus];
conf.dashDate = ISODate.format(conf.publishDate);
conf.publishISODate = conf.publishDate.toISOString();
// configuration done - yay!
// insert into document
const header = headersTmpl(conf);
document.body.prepend(header);
document.body.classList.add("h-entry");
// Requested by https://github.com/w3c/respec/issues/504
// Makes a record of a few auto-generated things.
pub("amend-user-config", {
publishISODate: conf.publishISODate,
generatedSubtitle: `${conf.longStatus} ${conf.publishHumanDate}`,
});
}

134
src/aom/style.js Normal file
View File

@ -0,0 +1,134 @@
// @ts-check
// Module aom/style
// Inserts a link to the appropriate style for the specification's maturity level.
// CONFIGURATION
// - specStatus: the short code for the specification's maturity level or type (required)
import {
createResourceHint,
linkCSS,
showWarning,
toKeyValuePairs,
} from "../core/utils.js";
import { sub } from "../core/pubsubhub.js";
export const name = "aom/style";
function attachFixupScript(doc, version) {
const script = doc.createElement("script");
if (location.hash) {
script.addEventListener(
"load",
() => {
window.location.href = location.hash;
},
{ once: true }
);
}
script.src = `https://www.w3.org/scripts/TR/${version}/fixup.js`;
doc.body.appendChild(script);
}
/**
* Make a best effort to attach meta viewport at the top of the head.
* Other plugins might subsequently push it down, but at least we start
* at the right place. When ReSpec exports the HTML, it again moves the
* meta viewport to the top of the head - so to make sure it's the first
* thing the browser sees. See js/ui/save-html.js.
*/
function createMetaViewport() {
const meta = document.createElement("meta");
meta.name = "viewport";
const contentProps = {
width: "device-width",
"initial-scale": "1",
"shrink-to-fit": "no",
};
meta.content = toKeyValuePairs(contentProps).replace(/"/g, "");
return meta;
}
function createBaseStyle() {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "https://www.w3.org/StyleSheets/TR/2016/base.css";
link.classList.add("removeOnSave");
return link;
}
function createResourceHints() {
/** @type ResourceHintOption[] */
const opts = [
{
hint: "preconnect", // for W3C styles and scripts.
href: "https://www.w3.org",
},
{
hint: "preload", // all specs need it, and we attach it on end-all.
href: "https://www.w3.org/scripts/TR/2016/fixup.js",
as: "script",
},
{
hint: "preload", // all specs include on base.css.
href: "https://www.w3.org/StyleSheets/TR/2016/base.css",
as: "style",
},
];
const resourceHints = document.createDocumentFragment();
for (const link of opts.map(createResourceHint)) {
resourceHints.appendChild(link);
}
return resourceHints;
}
// Collect elements for insertion (document fragment)
const elements = createResourceHints();
// Opportunistically apply base style
elements.appendChild(createBaseStyle());
if (!document.head.querySelector("meta[name=viewport]")) {
// Make meta viewport the first element in the head.
elements.prepend(createMetaViewport());
}
document.head.prepend(elements);
function styleMover(linkURL) {
return exportDoc => {
const w3cStyle = exportDoc.querySelector(`head link[href="${linkURL}"]`);
exportDoc.querySelector("head").append(w3cStyle);
};
}
export function run(conf) {
if (!conf.specStatus) {
const msg = "`respecConfig.specStatus` missing. Defaulting to 'base'.";
conf.specStatus = "base";
showWarning(msg, name);
}
let styleFile = "";
// Figure out which style file to use.
switch (conf.specStatus.toUpperCase()) {
case "PD":
styleFile = "W3C-UD";
break;
default:
styleFile = "base.css";
break;
}
// Attach W3C fixup script after we are done.
if (!conf.noToc) {
sub(
"end-all",
() => {
attachFixupScript(document, "2016");
},
{ once: true }
);
}
const finalStyleURL = `https://www.w3.org/StyleSheets/TR/2016/${styleFile}`;
linkCSS(document, finalStyleURL);
// Make sure the W3C stylesheet is the last stylesheet, as required by W3C Pub Rules.
const moveStyle = styleMover(finalStyleURL);
sub("beforesave", moveStyle);
}

View File

@ -0,0 +1,113 @@
// @ts-check
import { getIntlData } from "../../core/utils.js";
import { html } from "../../core/import-maps.js";
import showLink from "../../core/templates/show-link.js";
import showLogo from "../../core/templates/show-logo.js";
import showPeople from "../../core/templates/show-people.js";
const localizationStrings = {
en: {
author: "Author:",
authors: "Authors:",
editor: "Editor:",
editors: "Editors:",
former_editor: "Former editor:",
former_editors: "Former editors:",
latest_editors_draft: "Latest editor's draft:",
latest_published_version: "Latest approved version:",
this_version: "This version:",
issue_tracker: "Issue Tracker:",
},
};
export const l10n = getIntlData(localizationStrings);
function getSpecSubTitleElem(conf) {
let specSubTitleElem = document.querySelector("h2#subtitle");
if (specSubTitleElem && specSubTitleElem.parentElement) {
specSubTitleElem.remove();
conf.subtitle = specSubTitleElem.textContent.trim();
} else if (conf.subtitle) {
specSubTitleElem = document.createElement("h2");
specSubTitleElem.textContent = conf.subtitle;
specSubTitleElem.id = "subtitle";
}
if (specSubTitleElem) {
specSubTitleElem.classList.add("subtitle");
}
return specSubTitleElem;
}
export default conf => {
return html`<div class="head">
${conf.specStatus !== "PD" ? conf.logos.map(showLogo) : ""}
${document.querySelector("h1#title")} ${getSpecSubTitleElem(conf)}
<h2>
${conf.textStatus} -
<time class="dt-published" datetime="${conf.dashDate}"
>${conf.publishHumanDate}</time
>
</h2>
<dl>
<dt>${l10n.this_version}</dt>
<dd>
<a class="u-url" href="${conf.thisVersion}">${conf.thisVersion}</a>
</dd>
<dt>${l10n.issue_tracker}</dt>
<dd>
<a class="u-url" href="${conf.issueTracker}">${conf.issueTracker}</a>
</dd>
<dt>${conf.multipleEditors ? l10n.editors : l10n.editor}</dt>
${showPeople(conf, "editors")}
${Array.isArray(conf.formerEditors) && conf.formerEditors.length > 0
? html`
<dt>
${conf.multipleFormerEditors
? l10n.former_editors
: l10n.former_editor}
</dt>
${showPeople(conf, "formerEditors")}
`
: ""}
${conf.authors
? html`
<dt>${conf.multipleAuthors ? l10n.authors : l10n.author}</dt>
${showPeople(conf, "authors")}
`
: ""}
${conf.otherLinks ? conf.otherLinks.map(showLink) : ""}
</dl>
${renderCopyright(conf)}
<hr />
</div>`;
};
function renderCopyright(conf) {
// If there is already a copyright, let's relocate it.
const existingCopyright = document.querySelector(".copyright");
if (existingCopyright) {
existingCopyright.remove();
return existingCopyright;
}
return html`<p class="copyright">
Copyright ${conf.publishYear},
<a href="https://www.w3.org/"
><abbr title="The Alliance for Open Media">AOM</abbr></a
><br />
Licensing information is available at http://aomedia.org/license/<br />
The MATERIALS ARE PROVIDED AS IS. The Alliance for Open Media, its
members,and its contributors expressly disclaim any warranties (express,
implied, or otherwise), including implied warranties of merchantability,
non-infringement, fitness for a particular purpose, or title, related to the
materials. The entire risk as to implementing or otherwise using the
materials is assumed by the implementer and user. IN NO EVENT WILL THE
ALLIANCE FOR OPEN MEDIA, ITS MEMBERS, OR CONTRIBUTORS BE LIABLE TO ANY OTHER
PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, INCIDENTAL, OR
CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF ACTION OF ANY KIND
WITH RESPECT TO THIS DELIVERABLE OR ITS GOVERNING AGREEMENT, WHETHER BASED
ON BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND
WHETHER OR NOT THE OTHER MEMBER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
</p>`;
}

View File

@ -1,54 +0,0 @@
// @ts-check
/**
* @typedef {object} LinterResult
* @property {string} description
* @property {string} help
* @property {string} howToFix
* @property {string} name
* @property {number} occurrences
* @property {HTMLElement[]} offendingElements
*
* @typedef {(conf: any, doc: Document) => (LinterResult | Promise<LinterResult>)} LintingFunction
*/
/** @type {WeakMap<LinterRule, { name: string, lintingFunction: LintingFunction }>} */
const privs = new WeakMap();
/**
* Checks if the linter rule is enabled.
*
* @param {Object} conf ReSpec config object.
* @param {string} name linter rule name
*/
function canLint(conf, name) {
return !(
conf.hasOwnProperty("lint") === false ||
conf.lint === false ||
!conf.lint[name]
);
}
export default class LinterRule {
/**
*
* @param {String} name the name of the rule
* @param {LintingFunction} lintingFunction
*/
constructor(name, lintingFunction) {
privs.set(this, { name, lintingFunction });
}
get name() {
return privs.get(this).name;
}
/**
* Runs linter rule.
*
* @param {Object} conf The ReSpec config.
* @param {Document} doc The document to be checked.
*/
lint(conf = { lint: { [this.name]: false } }, doc = document) {
if (canLint(conf, this.name)) {
return privs.get(this).lintingFunction(conf, doc);
}
}
}

View File

@ -2,29 +2,18 @@
/**
Currently used only for adding 'assert' class to algorithm lists
*/
import { fetchAsset } from "./text-loader.js";
import css from "../styles/algorithms.css.js";
export const name = "core/algorithms";
const cssPromise = loadStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/algorithms.css")).default;
} catch {
return fetchAsset("algorithms.css");
}
}
export async function run() {
export function run() {
const elements = Array.from(document.querySelectorAll("ol.algorithm li"));
elements
.filter(li => li.textContent.trim().startsWith("Assert: "))
.forEach(li => li.classList.add("assert"));
if (document.querySelector(".assert")) {
const style = document.createElement("style");
style.textContent = await cssPromise;
style.textContent = css;
document.head.appendChild(style);
}
}

View File

@ -80,6 +80,7 @@ function processFigure(matchingElement, id, a) {
}
// remove the figure's title
const children = [...makeSafeCopy(figcaption).childNodes].filter(
// @ts-ignore
node => !node.classList || !node.classList.contains("fig-title")
);
// drop an empty space at the end.
@ -109,6 +110,7 @@ function processSection(matchingElement, id, a) {
function processHeading(heading, a) {
const hadSelfLink = heading.querySelector(".self-link");
const children = [...makeSafeCopy(heading).childNodes].filter(
// @ts-ignore
node => !node.classList || !node.classList.contains("self-link")
);
a.append(...children);

View File

@ -36,7 +36,8 @@ const readyPromise = openIdb();
* @returns {Promise<import("idb").IDBPDatabase<BiblioDb>>}
*/
async function openIdb() {
return await idb.openDB("respec-biblio2", 12, {
/** @type {import("idb").IDBPDatabase<BiblioDb>} */
const db = await idb.openDB("respec-biblio2", 12, {
upgrade(db) {
Array.from(db.objectStoreNames).map(storeName =>
db.deleteObjectStore(storeName)
@ -46,6 +47,23 @@ async function openIdb() {
db.createObjectStore("reference", { keyPath: "id" });
},
});
// Clean the database of expired biblio entries.
const now = Date.now();
for (const storeName of [...ALLOWED_TYPES]) {
const store = db.transaction(storeName, "readwrite").store;
const range = IDBKeyRange.lowerBound(now);
let result = await store.openCursor(range);
while (result?.value) {
/** @type {BiblioData} */
const entry = result.value;
if (entry.expires === undefined || entry.expires < now) {
await store.delete(entry.id);
}
result = await result.continue();
}
}
return db;
}
export const biblioDB = {
@ -57,7 +75,7 @@ export const biblioDB = {
* If it's an alias, it resolves it.
*
* @param {String} id The reference or alias to look for.
* @return {Promise<Object?>} The reference or null.
* @return {Promise<BiblioData?>} The reference or null.
*/
async find(id) {
if (await this.isAlias(id)) {
@ -116,7 +134,7 @@ export const biblioDB = {
*
* @param {AllowedType} type The type as per ALLOWED_TYPES.
* @param {string} id The id for what to look up.
* @return {Promise<Object?>} Resolves with the retrieved object, or null.
* @return {Promise<BiblioData?>} Resolves with the retrieved object, or null.
*/
async get(type, id) {
if (!ALLOWED_TYPES.has(type)) {
@ -135,15 +153,17 @@ export const biblioDB = {
* Adds references and aliases to database. This is usually the data from
* Specref's output (parsed JSON).
*
* @param {Object} data An object that contains references and aliases.
* @param {BibliographyMap} data An object that contains references and aliases.
* @param {number} expires The date/time when the data expires.
*/
async addAll(data) {
async addAll(data, expires) {
if (!data) {
return;
}
const aliasesAndRefs = { alias: [], reference: [] };
for (const id of Object.keys(data)) {
const obj = { id, ...data[id] };
/** @type {BiblioData} */
const obj = { id, ...data[id], expires };
if (obj.aliasOf) {
aliasesAndRefs.alias.push(obj);
} else {
@ -159,7 +179,7 @@ export const biblioDB = {
* Adds a reference or alias to the database.
*
* @param {AllowedType} type The type as per ALLOWED_TYPES.
* @param {Object} details The object to store.
* @param {BiblioData} details The object to store.
*/
async add(type, details) {
if (!ALLOWED_TYPES.has(type)) {
@ -172,9 +192,18 @@ export const biblioDB = {
throw new TypeError("Invalid alias object.");
}
const db = await this.ready;
const isInDB = await this.has(type, details.id);
const store = db.transaction(type, "readwrite").store;
let isInDB = await this.has(type, details.id);
// update or add, depending of already having it in db
// or if it's expired
if (isInDB) {
const entry = await this.get(type, details.id);
if (entry?.expires < Date.now()) {
const { store } = db.transaction(type, "readwrite");
await store.delete(details.id);
isInDB = false;
}
}
const { store } = db.transaction(type, "readwrite");
return isInDB ? await store.put(details) : await store.add(details);
},
/**

View File

@ -4,19 +4,15 @@
// Configuration:
// - localBiblio: override or supplement the official biblio with your own.
/* jshint jquery: true */
import { biblioDB } from "./biblio-db.js";
import { createResourceHint } from "./utils.js";
/** @type {Conf['biblio']} */
export const biblio = {};
// for backward compatibity
export { wireReference, stringifyReference } from "./render-biblio.js";
export const name = "core/biblio";
const bibrefsURL = new URL("https://specref.herokuapp.com/bibrefs?refs=");
const bibrefsURL = new URL("https://api.specref.org/bibrefs?refs=");
// Opportunistically dns-prefetch to bibref server, as we don't know yet
// if we will actually need to download references yet.
@ -53,8 +49,14 @@ export async function updateFromNetwork(
}
/** @type {Conf['biblio']} */
const data = await response.json();
// SpecRef updates every hour, so we should follow suit
// https://github.com/tobie/specref#hourly-auto-updating
const oneHourFromNow = Date.now() + 1000 * 60 * 60 * 1;
try {
await biblioDB.addAll(data);
const expires = response.headers.has("Expires")
? Math.min(Date.parse(response.headers.get("Expires")), oneHourFromNow)
: oneHourFromNow;
await biblioDB.addAll(data, expires);
} catch (err) {
console.error(err);
}
@ -82,7 +84,6 @@ export async function resolveRef(key) {
*/
async function getReferencesFromIdb(neededRefs) {
const idbRefs = [];
// See if we have them in IDB
try {
await biblioDB.ready; // can throw
@ -154,7 +155,10 @@ export class Plugin {
.sort()
)
);
const idbRefs = await getReferencesFromIdb(neededRefs);
const idbRefs = neededRefs.length
? await getReferencesFromIdb(neededRefs)
: [];
const split = { hasData: [], noData: [] };
idbRefs.forEach(ref => {
(ref.data ? split.hasData : split.noData).push(ref);

View File

@ -4,9 +4,9 @@
* Adds a caniuse support table for a "feature" #1238
* Usage options: https://github.com/w3c/respec/wiki/caniuse
*/
import { codedJoinAnd, docLink, showError, showWarning } from "./utils.js";
import { pub, sub } from "./pubsubhub.js";
import { showError, showWarning } from "./utils.js";
import { fetchAsset } from "./text-loader.js";
import css from "../styles/caniuse.css.js";
import { html } from "./import-maps.js";
export const name = "core/caniuse";
@ -29,15 +29,7 @@ const BROWSERS = new Set([
"samsung",
]);
async function loadStyle() {
try {
return (await import("text!../../assets/caniuse.css")).default;
} catch {
return fetchAsset("caniuse.css");
}
}
export async function prepare(conf) {
export function prepare(conf) {
if (!conf.caniuse) {
return; // nothing to do.
}
@ -47,9 +39,11 @@ export async function prepare(conf) {
return; // no feature to show
}
const caniuseCss = await loadStyle();
document.head.appendChild(html`<style class="removeOnSave">
${caniuseCss}
document.head.appendChild(html`<style
id="caniuse-stylesheet"
class="${options.removeOnSave ? "removeOnSave" : ""}"
>
${css}
</style>`);
const apiUrl = new URL("caniuse", conf.RESPEC_API_BASE).href;
@ -72,14 +66,15 @@ export async function run(conf) {
return html`${{ html: stats }}`;
} catch (err) {
const msg = `Couldn't find feature "${options.feature}" on caniuse.com.`;
const hint =
"Please check the feature key on [caniuse.com](https://caniuse.com)";
const hint = docLink`Please check the feature key on [caniuse.com](https://caniuse.com) and update ${"[caniuse]"}`;
showError(msg, name, { hint });
console.error(err);
return html`<a href="${featureURL}">caniuse.com</a>`;
}
})();
const definitionPair = html`<dt class="caniuse-title">Browser support:</dt>
const definitionPair = html`<dt class="caniuse-title">
Browser support (caniuse.com):
</dt>
<dd class="caniuse-stats">
${{
any: contentPromise,
@ -88,13 +83,17 @@ export async function run(conf) {
</dd>`;
headDlElem.append(...definitionPair.childNodes);
await contentPromise;
// remove from export
pub("amend-user-config", { caniuse: options.feature });
sub("beforesave", outputDoc => {
html.bind(outputDoc.querySelector(".caniuse-stats"))`
<a href="${featureURL}">caniuse.com</a>`;
});
if (options.removeOnSave) {
// Will remove the browser support cells.
headDlElem
.querySelectorAll(".caniuse-browser")
.forEach(elem => elem.classList.add("removeOnSave"));
sub("beforesave", outputDoc => {
html.bind(outputDoc.querySelector(".caniuse-stats"))`
<a href="${featureURL}">caniuse.com</a>`;
});
}
}
/**
@ -102,7 +101,7 @@ export async function run(conf) {
* @param {Object} conf configuration settings
*/
function getNormalizedConf(conf) {
const DEFAULTS = { versions: 4 };
const DEFAULTS = { versions: 4, removeOnSave: false };
if (typeof conf.caniuse === "string") {
return { feature: conf.caniuse, ...DEFAULTS };
}
@ -111,10 +110,8 @@ function getNormalizedConf(conf) {
if (Array.isArray(browsers)) {
const invalidBrowsers = browsers.filter(browser => !BROWSERS.has(browser));
if (invalidBrowsers.length) {
const names = invalidBrowsers.map(b => `"\`${b}\`"`).join(", ");
const msg =
`Ignoring invalid browser(s): ${names} in ` +
"[`respecConfig.caniuse.browsers`](https://github.com/w3c/respec/wiki/caniuse)";
const names = codedJoinAnd(invalidBrowsers, { quotes: true });
const msg = docLink`Invalid browser(s): (${names}) in the \`browser\` property of ${"[caniuse]"}.`;
showWarning(msg, name);
}
}

View File

@ -4,7 +4,7 @@
// in the content of elements with key identifiers:
// #gh-contributors: people whose PR have been merged.
// Spec editors get filtered out automatically.
import { fetchAndCache, joinAnd, showError } from "./utils.js";
import { docLink, fetchAndCache, joinAnd, showError } from "./utils.js";
import { html } from "./import-maps.js";
export const name = "core/contrib";
@ -15,9 +15,7 @@ export async function run(conf) {
}
if (!conf.github) {
const msg =
"Requested list of contributors from GitHub, but " +
"[`github`](https://github.com/w3c/respec/wiki/github) configuration option is not set.";
const msg = docLink`Requested list of contributors from GitHub, but ${"[github]"} configuration option is not set.`;
showError(msg, name);
return;
}

View File

@ -29,7 +29,7 @@ export const THIS_SPEC = "__SPEC__";
* @param {CiteDetails} citeDetails
*/
async function getLinkProps(citeDetails) {
const { key, frag, path } = citeDetails;
const { key, frag, path, href: canonicalHref } = citeDetails;
let href = "";
let title = "";
// This is just referring to this document
@ -44,13 +44,18 @@ async function getLinkProps(citeDetails) {
href = entry.href;
title = entry.title;
}
if (path) {
// See: https://github.com/w3c/respec/issues/1856#issuecomment-429579475
const relPath = path.startsWith("/") ? `.${path}` : path;
href = new URL(relPath, href).href;
}
if (frag) {
href = new URL(frag, href).href;
if (canonicalHref) {
// Xref gave us a canonical link, so let's use that.
href = canonicalHref;
} else {
if (path) {
// See: https://github.com/w3c/respec/issues/1856#issuecomment-429579475
const relPath = path.startsWith("/") ? `.${path}` : path;
href = new URL(relPath, href).href;
}
if (frag) {
href = new URL(frag, href).href;
}
}
return { href, title };
}
@ -66,41 +71,44 @@ function linkElem(elem, linkProps, citeDetails) {
const { href, title } = linkProps;
const wrapInCiteEl = !citeDetails.path && !citeDetails.frag;
if (elem.localName === "a") {
const anchor = /** @type {HTMLAnchorElement} */ (elem);
if (anchor.textContent === "" && anchor.dataset.lt !== "the-empty-string") {
anchor.textContent = title;
switch (elem.localName) {
case "a": {
const el = /** @type {HTMLAnchorElement} */ (elem);
if (el.textContent === "" && el.dataset.lt !== "the-empty-string") {
el.textContent = title;
}
el.href = href;
if (wrapInCiteEl) {
const cite = document.createElement("cite");
el.replaceWith(cite);
cite.append(el);
}
break;
}
anchor.href = href;
if (wrapInCiteEl) {
const cite = document.createElement("cite");
anchor.replaceWith(cite);
cite.append(anchor);
case "dfn": {
const anchor = document.createElement("a");
anchor.href = href;
if (!elem.textContent) {
anchor.textContent = title;
elem.append(anchor);
} else {
wrapInner(elem, anchor);
}
if (wrapInCiteEl) {
const cite = document.createElement("cite");
cite.append(anchor);
elem.append(cite);
}
if ("export" in elem.dataset) {
const msg = "Exporting an linked external definition is not allowed.";
const hint = "Please remove the `data-export` attribute.";
showError(msg, name, { hint, elements: [elem] });
delete elem.dataset.export;
}
elem.classList.add("externalDFN");
elem.dataset.noExport = "";
break;
}
return;
}
if (elem.localName === "dfn") {
const anchor = document.createElement("a");
anchor.href = href;
if (!elem.textContent) {
anchor.textContent = title;
elem.append(anchor);
} else {
wrapInner(elem, anchor);
}
if (wrapInCiteEl) {
const cite = document.createElement("cite");
cite.append(anchor);
elem.append(cite);
}
if ("export" in elem.dataset) {
const msg = "Exporting an linked external definition is not allowed.";
const hint = "Please remove the `data-export` attribute.";
showError(msg, name, { hint, elements: [elem] });
delete elem.dataset.export;
}
elem.dataset.noExport = "";
}
}
@ -124,13 +132,14 @@ const findPath = makeComponentFinder("/");
* @property {boolean} isNormative
* @property {string} frag
* @property {string} path
*
* @property {string} [href] - canonical href coming from xref
* @param {HTMLElement} elem
* @return {CiteDetails};
*/
export function toCiteDetails(elem) {
const { dataset } = elem;
const { cite: rawKey, citeFrag, citePath } = dataset;
const { cite: rawKey, citeFrag, citePath, citeHref } = dataset;
// The key is a fragment, resolve using the shortName as key
if (rawKey.startsWith("#") && !citeFrag) {
// Closes data-cite not starting with "#"
@ -152,7 +161,7 @@ export function toCiteDetails(elem) {
// key is before "/" and "#" but after "!" or "?" (e.g., ?key/path#frag)
const hasPrecedingMark = /^[?|!]/.test(rawKey);
const key = rawKey.split(/[/|#]/)[0].substring(Number(hasPrecedingMark));
const details = { key, isNormative, frag, path };
const details = { key, isNormative, frag, path, href: citeHref };
return details;
}

View File

@ -7,36 +7,34 @@
*
* `data-tests` takes a space separated list of URLs, e.g. data-test="foo.html bar.html".
*
* Docs: https://github.com/w3c/respec/wiki/data-tests
* Docs: https://respec.org/doc/#data-tests
*/
import { getIntlData, showError, showWarning } from "./utils.js";
import {
codedJoinAnd,
docLink,
getIntlData,
showError,
showWarning,
} from "./utils.js";
import { html } from "./import-maps.js";
const localizationStrings = {
en: {
missing_test_suite_uri:
"Found tests in your spec, but missing '" +
"[`testSuiteURI`](https://github.com/w3c/respec/wiki/testSuiteURI)' in your ReSpec config.",
missing_test_suite_uri: docLink`Found tests in your spec, but missing ${"[testSuiteURI]"} in your ReSpec config.`,
tests: "tests",
test: "test",
},
ja: {
missing_test_suite_uri:
"この仕様内にテストの項目を検出しましたがReSpec の設定に '" +
"[`testSuiteURI`](https://github.com/w3c/respec/wiki/testSuiteURI)' が見つかりません.",
missing_test_suite_uri: docLink`この仕様内にテストの項目を検出しましたがReSpec の設定に ${"[testSuiteURI]"} が見つかりません.`,
tests: "テスト",
test: "テスト",
},
de: {
missing_test_suite_uri:
"Die Spezifikation enthält Tests, aber in der ReSpec-Konfiguration ist keine '" +
"[`testSuiteURI`](https://github.com/w3c/respec/wiki/testSuiteURI)' angegeben.",
missing_test_suite_uri: docLink`Die Spezifikation enthält Tests, aber in der ReSpec-Konfiguration ist keine ${"[testSuiteURI]"} angegeben.`,
tests: "Tests",
test: "Test",
},
zh: {
missing_test_suite_uri:
"本规范中包含测试,但在 ReSpec 配置中缺少 '" +
"[`testSuiteURI`](https://github.com/w3c/respec/wiki/testSuiteURI)'。",
missing_test_suite_uri: docLink`本规范中包含测试,但在 ReSpec 配置中缺少 ${"[testSuiteURI]"}`,
tests: "测试",
test: "测试",
},
@ -106,7 +104,7 @@ export function run(conf) {
for (const elem of testables) {
const tests = elem.dataset.tests.split(/,/gm).map(url => url.trim());
const testURLs = toTestURLs(tests, conf.testSuiteURI);
const testURLs = toTestURLs(tests, conf.testSuiteURI, elem);
handleDuplicates(testURLs, elem);
const details = toHTML(testURLs);
elem.append(details);
@ -116,15 +114,16 @@ export function run(conf) {
/**
* @param {string[]} tests
* @param {string} testSuiteURI
* @param {HTMLElement} elem
*/
function toTestURLs(tests, testSuiteURI) {
function toTestURLs(tests, testSuiteURI, elem) {
return tests
.map(test => {
try {
return new URL(test, testSuiteURI).href;
} catch {
const msg = `Bad URI: ${test}`;
showWarning(msg, name);
const msg = docLink`Invalid URL in ${"[data-tests]"} attribute: ${test}.`;
showWarning(msg, name, { elements: [elem] });
}
})
.filter(href => href);
@ -139,10 +138,9 @@ function handleDuplicates(testURLs, elem) {
(link, i, self) => self.indexOf(link) !== i
);
if (duplicates.length) {
const msg = `Duplicate tests found`;
const hint = `To fix, remove duplicates from "data-tests": ${duplicates
.map(url => new URL(url).pathname)
.join(", ")}`;
const msg = docLink`Duplicate tests found in the ${"[data-tests]"} attribute.`;
const tests = codedJoinAnd(duplicates, { quotes: true });
const hint = docLink`To fix, remove duplicates from ${"[data-tests]"}: ${tests}.`;
showWarning(msg, name, { hint, elements: [elem] });
}
}

View File

@ -5,27 +5,17 @@
* Also adds the CSS for the data type tooltip.
* Set `conf.highlightVars = true` to enable.
*/
import { fetchAsset } from "./text-loader.js";
import css from "../styles/datatype.css.js";
export const name = "core/data-type";
const tooltipStylePromise = loadStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/datatype.css")).default;
} catch {
return fetchAsset("datatype.css");
}
}
export async function run(conf) {
export function run(conf) {
if (!conf.highlightVars) {
return;
}
const style = document.createElement("style");
style.textContent = await tooltipStylePromise;
style.textContent = css;
document.head.appendChild(style);
let section = null;

View File

@ -3,26 +3,6 @@
* Sets the core defaults
*/
export const name = "core/defaults";
import { rule as checkCharset } from "./linter-rules/check-charset.js";
import { rule as checkInternalSlots } from "./linter-rules/check-internal-slots.js";
import { rule as checkPunctuation } from "./linter-rules/check-punctuation.js";
import linter from "./linter.js";
import { rule as localRefsExist } from "./linter-rules/local-refs-exist.js";
import { rule as noHeadinglessSectionsRule } from "./linter-rules/no-headingless-sections.js";
import { rule as noHttpPropsRule } from "./linter-rules/no-http-props.js";
import { rule as noUnusedVars } from "./linter-rules/no-unused-vars.js";
import { rule as privsecSection } from "./linter-rules/privsec-section.js";
linter.register(
noHttpPropsRule,
noHeadinglessSectionsRule,
noUnusedVars,
checkPunctuation,
localRefsExist,
checkInternalSlots,
checkCharset,
privsecSection
);
export const coreDefaults = {
lint: {

View File

@ -182,7 +182,10 @@ export function decorateDfn(dfnElem, idlAst, parent, name) {
const lCaseParent = parent.toLowerCase();
const middle = lCaseParent ? `${lCaseParent}-` : "";
let last = name.toLowerCase().replace(/[()]/g, "").replace(/\s/g, "-");
if (last === "") last = "the-empty-string";
if (last === "") {
last = "the-empty-string";
dfnElem.setAttribute("aria-label", "the empty string");
}
dfnElem.id = `dom-${middle}${last}`;
}
dfnElem.dataset.idl = idlAst.type;

View File

@ -6,7 +6,7 @@
*/
import { addId, getIntlData, norm } from "./utils.js";
import { fetchAsset } from "./text-loader.js";
import css from "../styles/dfn-index.css.js";
import { getTermFromElement } from "./xref.js";
import { html } from "./import-maps.js";
import { renderInlineCitation } from "./render-biblio.js";
@ -46,14 +46,14 @@ const CODE_TYPES = new Set([
* @typedef {{ term: string, type: string, linkFor: string, elem: HTMLAnchorElement }} Entry
*/
export async function run() {
export function run() {
const index = document.querySelector("section#index");
if (!index) {
return;
}
const styleEl = document.createElement("style");
styleEl.textContent = await loadStyle();
styleEl.textContent = css;
document.head.appendChild(styleEl);
index.classList.add("appendix");
@ -362,14 +362,6 @@ function getTermText(entry) {
return text;
}
async function loadStyle() {
try {
return (await import("text!../../assets/dfn-index.css")).default;
} catch {
return fetchAsset("dfn-index.css");
}
}
/** @param {Document} doc */
function cleanup(doc) {
doc

View File

@ -2,14 +2,14 @@
// Constructs "dfn panels" which show all the local references to a dfn and a
// self link to the selected dfn. Based on Bikeshed's dfn panels at
// https://github.com/tabatkins/bikeshed/blob/ef44162c2e/bikeshed/dfnpanels.py
import { fetchAsset, fetchBase } from "./text-loader.js";
import css from "../styles/dfn-panel.css.js";
import { fetchBase } from "./text-loader.js";
import { html } from "./import-maps.js";
import { norm } from "./utils.js";
export const name = "core/dfn-panel";
export async function run() {
const css = await loadStyle();
document.head.insertBefore(
html`<style>
${css}
@ -19,11 +19,15 @@ export async function run() {
/** @type {NodeListOf<HTMLElement>} */
const elems = document.querySelectorAll(
"dfn[id], #index-defined-elsewhere .index-term"
"dfn[id]:not([data-cite]), #index-defined-elsewhere .index-term"
);
const panels = document.createDocumentFragment();
for (const el of elems) {
panels.append(createPanel(el));
// Make it possible to reach el by tabbing,
// allowing keyboard action as needed.
el.tabIndex = 0;
el.setAttribute("aria-haspopup", "dialog");
}
document.body.append(panels);
@ -41,17 +45,30 @@ function createPanel(dfn) {
const links = document.querySelectorAll(`a[href="${href}"]:not(.index-term)`);
const panelId = `dfn-panel-for-${dfn.id}`;
const definition = dfn.getAttribute("aria-label") || norm(dfn.textContent);
/** @type {HTMLElement} */
const panel = html`
<aside class="dfn-panel" id="${panelId}" hidden>
<div
class="dfn-panel"
id="${panelId}"
hidden
role="dialog"
aria-modal="true"
aria-label="Links in this document to definition: ${definition}"
>
<span class="caret"></span>
<div>
<a class="self-link" href="${href}">Permalink</a>
<a
class="self-link"
href="${href}"
aria-label="Permalink for definition: ${definition}. Activate to close this dialog."
>Permalink</a
>
${dfnExportedMarker(dfn)} ${idlMarker(dfn, links)}
</div>
<b>Referenced in:</b>
<p><b>Referenced in:</b></p>
${referencesToHTML(id, links)}
</aside>
</div>
`;
return panel;
}
@ -114,13 +131,17 @@ function referencesToHTML(id, links) {
/**
* Returns a list that is easier to render in `listItemToHTML`.
* @param {[string, string[]]} entry an entry from `titleToIDs`
* @returns {{ title: string, id: string }[]} The first list item contains
* @returns {{ title: string, text: string, id: string, }[]} The first list item contains
* title from `getReferenceTitle`, rest of items contain strings like `(2)`,
* `(3)` as title.
*/
const toLinkProps = ([title, ids]) => {
return [{ title, id: ids[0] }].concat(
ids.slice(1).map((id, i) => ({ title: `(${i + 2})`, id }))
return [{ title, id: ids[0], text: title }].concat(
ids.slice(1).map((id, i) => ({
title: `Reference ${i + 2}`,
text: `(${i + 2})`,
id,
}))
);
};
@ -130,7 +151,8 @@ function referencesToHTML(id, links) {
*/
const listItemToHTML = entry => html`<li>
${toLinkProps(entry).map(
link => html`<a href="#${link.id}">${link.title}</a>${" "}`
link =>
html`<a href="#${link.id}" title="${link.title}">${link.text}</a>${" "}`
)}
</li>`;
@ -145,15 +167,7 @@ function getReferenceTitle(link) {
if (!section) return null;
const heading = section.querySelector("h1, h2, h3, h4, h5, h6");
if (!heading) return null;
return norm(heading.textContent);
}
async function loadStyle() {
try {
return (await import("text!../../assets/dfn-panel.css")).default;
} catch {
return fetchAsset("dfn-panel.css");
}
return `§ ${norm(heading.textContent)}`;
}
async function loadScript() {

View File

@ -1,27 +1,38 @@
// @ts-check
if (document.respec) {
document.respec.ready.then(dfnPanel);
document.respec.ready.then(setupPanel);
} else {
dfnPanel();
setupPanel();
}
function dfnPanel() {
function setupPanel() {
const listener = panelListener();
document.body.addEventListener("keydown", listener);
document.body.addEventListener("click", listener);
}
function panelListener() {
/** @type {HTMLElement} */
let panel;
document.body.addEventListener("click", event => {
if (!(event.target instanceof HTMLElement)) return;
let panel = null;
return event => {
const { target, type } = event;
/** @type {HTMLElement} */
const el = event.target;
if (!(target instanceof HTMLElement)) return;
// For keys, we only care about Enter key to activate the panel
// otherwise it's activated via a click.
if (type === "keydown" && event.key !== "Enter") return;
const action = deriveAction(event);
const action = deriveAction(el);
switch (action) {
case "show": {
if (panel) hidePanel(panel);
hidePanel(panel);
/** @type {HTMLElement} */
const dfn = el.closest("dfn, .index-term");
const dfn = target.closest("dfn, .index-term");
panel = document.getElementById(`dfn-panel-for-${dfn.id}`);
displayPanel(dfn, panel, { x: event.clientX, y: event.clientY });
const coords = deriveCoordinates(event);
displayPanel(dfn, panel, coords);
break;
}
case "dock": {
@ -32,30 +43,59 @@ function dfnPanel() {
}
case "hide": {
hidePanel(panel);
panel = null;
break;
}
}
});
};
}
/** @param {HTMLElement} clickTarget */
function deriveAction(clickTarget) {
const hitALink = !!clickTarget.closest("a");
if (clickTarget.closest("dfn, .index-term")) {
return hitALink ? null : "show";
/**
* @param {MouseEvent|KeyboardEvent} event
*/
function deriveCoordinates(event) {
const target = /** @type HTMLElement */ (event.target);
// We prevent synthetic AT clicks from putting
// the dialog in a weird place. The AT events sometimes
// lack coordinates, so they have clientX/Y = 0
const rect = target.getBoundingClientRect();
if (
event instanceof MouseEvent &&
event.clientX >= rect.left &&
event.clientY >= rect.top
) {
// The event probably happened inside the bounding rect...
return { x: event.clientX, y: event.clientY };
}
if (clickTarget.closest(".dfn-panel")) {
// Offset to the middle of the element
const x = rect.x + rect.width / 2;
// Placed at the bottom of the element
const y = rect.y + rect.height;
return { x, y };
}
/**
* @param {Event} event
*/
function deriveAction(event) {
const target = /** @type {HTMLElement} */ (event.target);
const hitALink = !!target.closest("a");
if (target.closest("dfn:not([data-cite]), .index-term")) {
return hitALink ? "none" : "show";
}
if (target.closest(".dfn-panel")) {
if (hitALink) {
const clickedSelfLink = clickTarget.classList.contains("self-link");
return clickedSelfLink ? "hide" : "dock";
return target.classList.contains("self-link") ? "hide" : "dock";
}
const panel = clickTarget.closest(".dfn-panel");
return panel.classList.contains("docked") ? "hide" : null;
const panel = target.closest(".dfn-panel");
return panel.classList.contains("docked") ? "hide" : "none";
}
if (document.querySelector(".dfn-panel:not([hidden])")) {
return "hide";
}
return null;
return "none";
}
/**
@ -97,10 +137,85 @@ function displayPanel(dfn, panel, { x, y }) {
const caret = panel.querySelector(".caret");
caret.style.left = `${newCaretOffset}px`;
}
// As it's a dialog, we trap focus.
// TODO: when <dialog> becomes a implemented, we should really
// use that.
trapFocus(panel, dfn);
}
/**
* @param {HTMLElement} panel
* @param {HTMLElement} dfn
* @returns
*/
function trapFocus(panel, dfn) {
/** @type NodeListOf<HTMLAnchorElement> elements */
const anchors = panel.querySelectorAll("a[href]");
// No need to trap focus
if (!anchors.length) return;
// Move focus to first anchor element
const first = anchors.item(0);
first.focus();
const trapListener = createTrapListener(anchors, panel, dfn);
panel.addEventListener("keydown", trapListener);
// Hiding the panel releases the trap
const mo = new MutationObserver(records => {
const [record] = records;
const target = /** @type HTMLElement */ (record.target);
if (target.hidden) {
panel.removeEventListener("keydown", trapListener);
mo.disconnect();
}
});
mo.observe(panel, { attributes: true, attributeFilter: ["hidden"] });
}
/**
*
* @param {NodeListOf<HTMLAnchorElement>} anchors
* @param {HTMLElement} panel
* @param {HTMLElement} dfn
* @returns
*/
function createTrapListener(anchors, panel, dfn) {
const lastIndex = anchors.length - 1;
let currentIndex = 0;
return event => {
switch (event.key) {
// Hitting "Tab" traps us in a nice loop around elements.
case "Tab": {
event.preventDefault();
currentIndex += event.shiftKey ? -1 : +1;
if (currentIndex < 0) {
currentIndex = lastIndex;
} else if (currentIndex > lastIndex) {
currentIndex = 0;
}
anchors.item(currentIndex).focus();
break;
}
// Hitting "Enter" on an anchor releases the trap.
case "Enter":
hidePanel(panel);
break;
// Hitting "Escape" returns focus to dfn.
case "Escape":
hidePanel(panel);
dfn.focus();
return;
}
};
}
/** @param {HTMLElement} panel */
function hidePanel(panel) {
if (!panel) return;
panel.hidden = true;
panel.classList.remove("docked");
}

View File

@ -0,0 +1,74 @@
import { MIMEType } from "./import-maps.js";
import { showError } from "./utils.js";
/**
* Validates MIME types strings.
*
* @type {DefinitionValidator} */
export function validateMimeType(text, type, elem, pluginName) {
try {
// Constructor can throw.
const type = new MIMEType(text);
if (type.toString() !== text) {
throw new Error(`Input doesn't match its canonical form: "${type}".`);
}
} catch (error) {
const msg = `Invalid ${type} "${text}": ${error.message}.`;
const hint =
"Check that the MIME type has both a type and a sub-type, and that it's in a canonical form (e.g., `text/plain`).";
showError(msg, pluginName, { hint, elements: [elem] });
return false;
}
return true;
}
/**
* Validates the names of DOM attribute and elements.
* @param {"attribute" | "element"} type
* @type {DefinitionValidator} */
export function validateDOMName(text, type, elem, pluginName) {
try {
switch (type) {
case "attribute":
document.createAttribute(text);
return true;
case "element":
document.createElement(text);
return true;
}
} catch (err) {
const msg = `Invalid ${type} name "${text}": ${err.message}`;
const hint = `Check that the ${type} name is allowed per the XML's Name production for ${type}.`;
showError(msg, pluginName, { hint, elements: [elem] });
}
return false;
}
/**
* Validates common variable or other named thing in a spec, like event names.
*
* @type {DefinitionValidator}
*/
export function validateCommonName(text, type, elem, pluginName) {
// Check a-z, maybe a dash and letters, case insensitive.
// Also, no spaces.
if (/^[a-z]+(-[a-z]+)*$/i.test(text)) {
return true; // all good
}
const msg = `Invalid ${type} name "${text}".`;
const hint = `Check that the ${type} name is allowed per the naming rules for this type.`;
showError(msg, pluginName, { hint, elements: [elem] });
return false;
}
/**
* @type {DefinitionValidator} */
export function validateQuotedString(text, type, elem, pluginName) {
if (text.startsWith(`"`) && text.endsWith(`"`)) {
return validateCommonName(text.slice(1, -1), type, elem, pluginName);
}
const msg = `Invalid ${type} "${text}".`;
const hint = `Check that the ${type} is quoted with double quotes.`;
showError(msg, pluginName, { hint, elements: [elem] });
return false;
}

View File

@ -2,33 +2,233 @@
// Module core/dfn
// - Finds all <dfn> elements and populates definitionMap to identify them.
import { getDfnTitles, norm } from "./utils.js";
import {
codedJoinOr,
docLink,
getDfnTitles,
norm,
showError,
toMDCode,
} from "./utils.js";
import {
validateCommonName,
validateDOMName,
validateMimeType,
validateQuotedString,
} from "./dfn-validators.js";
import { registerDefinition } from "./dfn-map.js";
import { slotRegex } from "./inline-idl-parser.js";
import { sub } from "./pubsubhub.js";
export const name = "core/dfn";
/** @type {Map<string, { requiresFor: boolean, validator?: DefinitionValidator, associateWith?: string}>} */
const knownTypesMap = new Map([
["abstract-op", { requiresFor: false }],
["attribute", { requiresFor: false, validator: validateDOMName }],
[
"attr-value",
{
requiresFor: true,
associateWith: "an HTML attribute",
validator: validateCommonName,
},
],
["element", { requiresFor: false, validator: validateDOMName }],
[
"element-state",
{
requiresFor: true,
associateWith: "an HTML attribute",
validator: validateCommonName,
},
],
["event", { requiresFor: false, validator: validateCommonName }],
["http-header", { requiresFor: false }],
["media-type", { requiresFor: false, validator: validateMimeType }],
["scheme", { requiresFor: false, validator: validateCommonName }],
["permission", { requiresFor: false, validator: validateQuotedString }],
]);
const knownTypes = [...knownTypesMap.keys()];
export function run() {
document.querySelectorAll("dfn").forEach(dfn => {
for (const dfn of document.querySelectorAll("dfn")) {
const titles = getDfnTitles(dfn);
registerDefinition(dfn, titles);
// Treat Internal Slots as IDL.
if (!dfn.dataset.dfnType && /^\[\[\w+\]\]$/.test(titles[0])) {
dfn.dataset.dfnType = "idl";
// It's a legacy cite or redefining a something it doesn't own, so it gets no benefit.
if (dfn.dataset.cite && /\b#\b/.test(dfn.dataset.cite)) {
continue;
}
// Per https://tabatkins.github.io/bikeshed/#dfn-export, a dfn with dfnType
// other than dfn and not marked with data-no-export is to be exported.
// We also skip "imported" definitions via data-cite.
const ds = dfn.dataset;
if (ds.dfnType && ds.dfnType !== "dfn" && !ds.cite && !ds.noExport) {
dfn.dataset.export = "";
}
const [linkingText] = titles;
computeType(dfn, linkingText);
computeExport(dfn);
// Only add `lt`s that are different from the text content
if (titles.length === 1 && titles[0] === norm(dfn.textContent)) {
return;
if (titles.length === 1 && linkingText === norm(dfn.textContent)) {
continue;
}
dfn.dataset.lt = titles.join("|");
});
}
sub("plugins-done", addContractDefaults);
}
/**
* @param {HTMLElement} dfn
* @param {string} linkingText
* */
function computeType(dfn, linkingText) {
let type = "";
switch (true) {
// class defined type (e.g., "<dfn class="element">)
case knownTypes.some(name => dfn.classList.contains(name)):
// First one wins
type = [...dfn.classList].find(className => knownTypesMap.has(className));
validateDefinition(linkingText, type, dfn);
break;
// Internal slots: attributes+ methods (e.g., [[some words]](with, optional, arguments))
case slotRegex.test(linkingText):
type = processAsInternalSlot(linkingText, dfn);
break;
}
// Derive closest type
if (!type && !dfn.matches("[data-dfn-type]")) {
/** @type {HTMLElement} */
const closestType = dfn.closest("[data-dfn-type]");
type = closestType?.dataset.dfnType;
}
// only if we have type and one wasn't explicitly given.
if (type && !dfn.dataset.dfnType) {
dfn.dataset.dfnType = type;
}
// Finally, addContractDefaults() will add the type to the dfn if it's not there.
// But other modules may end up adding a type (e.g., the WebIDL module)
}
// Deal with export/no export
function computeExport(dfn) {
switch (true) {
// Error if we have both exports and no exports.
case dfn.matches(".export.no-export"): {
const msg = docLink`Declares both "${"[no-export]"}" and "${"[export]"}" CSS class.`;
const hint = "Please use only one.";
showError(msg, name, { elements: [dfn], hint });
break;
}
// No export wins
case dfn.matches(".no-export, [data-noexport]"):
if (dfn.matches("[data-export]")) {
const msg = docLink`Declares ${"[no-export]"} CSS class, but also has a "${"[data-export]"}" attribute.`;
const hint = "Please chose only one.";
showError(msg, name, { elements: [dfn], hint });
delete dfn.dataset.export;
}
dfn.dataset.noexport = "";
break;
// If the author explicitly asked for it to be exported, so let's export it.
case dfn.matches(":is(.export):not([data-noexport], .no-export)"):
dfn.dataset.export = "";
break;
}
}
/**
* @param {string} text
* @param {string} type
* @param {HTMLElement} dfn
*/
function validateDefinition(text, type, dfn) {
const entry = knownTypesMap.get(type);
if (entry.requiresFor && !dfn.dataset.dfnFor) {
const msg = docLink`Definition of type "\`${type}\`" requires a ${"[data-dfn-for]"} attribute.`;
const { associateWith } = entry;
const hint = docLink`Use a ${"[data-dfn-for]"} attribute to associate this with ${associateWith}.`;
showError(msg, name, { hint, elements: [dfn] });
}
if (entry.validator) {
entry.validator(text, type, dfn, name);
}
}
/**
*
* @param {string} title
* @param {HTMLElement} dfn
*/
function processAsInternalSlot(title, dfn) {
if (!dfn.dataset.hasOwnProperty("idl")) {
dfn.dataset.idl = "";
}
// Automatically use the closest data-dfn-for as the parent.
/** @type HTMLElement */
const parent = dfn.closest("[data-dfn-for]");
if (dfn !== parent && parent?.dataset.dfnFor) {
dfn.dataset.dfnFor = parent.dataset.dfnFor;
}
// Assure that it's data-dfn-for= something.
if (!dfn.dataset.dfnFor) {
const msg = `Internal slot "${title}" must be associated with a WebIDL interface.`;
const hint = docLink`Use a ${"[data-dfn-for]"} attribute to associate this dfn with a WebIDL interface.`;
showError(msg, name, { hint, elements: [dfn] });
}
// Don't export internal slots by default, as they are not supposed to be public.
if (!dfn.matches(".export, [data-export]")) {
dfn.dataset.noexport = "";
}
// If it ends with a ), then it's method. Attribute otherwise.
const derivedType = title.endsWith(")") ? "method" : "attribute";
if (!dfn.dataset.dfnType) {
return derivedType;
}
// Perform validation on the dfn's type.
const allowedSlotTypes = ["attribute", "method"];
const { dfnType } = dfn.dataset;
if (!allowedSlotTypes.includes(dfnType) || derivedType !== dfnType) {
const msg = docLink`Invalid ${"[data-dfn-type]"} attribute on internal slot.`;
const prettyTypes = codedJoinOr(allowedSlotTypes, {
quotes: true,
});
const hint = `The only allowed types are: ${prettyTypes}. The slot "${title}" seems to be a "${toMDCode(
derivedType
)}"?`;
showError(msg, name, { hint, elements: [dfn] });
return "dfn";
}
return dfnType;
}
function addContractDefaults() {
// Find all dfns that don't have a type and default them to "dfn".
/** @type NodeListOf<HTMLElement> */
const dfnsWithNoType = document.querySelectorAll(
"dfn:is([data-dfn-type=''],:not([data-dfn-type]))"
);
for (const dfn of dfnsWithNoType) {
dfn.dataset.dfnType = "dfn";
}
// Per "the contract", export all definitions, except where:
// - Explicitly marked with data-noexport.
// - The type is "dfn" and not explicitly marked for export (i.e., just a regular definition).
// - definitions was included via (legacy) data-cite="foo#bar".
/** @type NodeListOf<HTMLElement> */
const exportableDfns = document.querySelectorAll(
"dfn:not([data-noexport], [data-export], [data-dfn-type='dfn'], [data-cite])"
);
for (const dfn of exportableDfns) {
dfn.dataset.export = "";
}
}

0
src/core/docLink.js Normal file
View File

View File

@ -7,7 +7,7 @@
// be used by a containing shell to extract all examples.
import { addId, getIntlData } from "./utils.js";
import { fetchAsset } from "./text-loader.js";
import css from "../styles/examples.css.js";
import { html } from "./import-maps.js";
import { pub } from "./pubsubhub.js";
@ -39,16 +39,6 @@ const localizationStrings = {
const l10n = getIntlData(localizationStrings);
const cssPromise = loadStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/examples.css")).default;
} catch {
return fetchAsset("examples.css");
}
}
/**
* @typedef {object} Report
* @property {number} number
@ -73,14 +63,13 @@ function makeTitle(elem, num, report) {
</div>`;
}
export async function run() {
export function run() {
/** @type {NodeListOf<HTMLElement>} */
const examples = document.querySelectorAll(
"pre.example, pre.illegal-example, aside.example"
);
if (!examples.length) return;
const css = await cssPromise;
document.head.insertBefore(
html`<style>
${css}
@ -101,13 +90,7 @@ export async function run() {
++number;
const div = makeTitle(example, number, report);
example.prepend(div);
if (title) {
addId(example, `example-${number}`, title); // title gets used
} else {
// use the number as the title... so, e.g., "example-5"
addId(example, "example", String(number));
}
const { id } = example;
const id = addId(example, "example", title || String(number));
const selfLink = div.querySelector("a.self-link");
selfLink.href = `#${id}`;
pub("example", report);
@ -126,10 +109,7 @@ export async function run() {
const div = html`<div class="example" id="${id}">
${exampleTitle} ${example.cloneNode(true)}
</div>`;
if (title) {
addId(div, `example-${number}`, title);
}
addId(div, `example`, String(number));
addId(div, "example", title || String(number));
const selfLink = div.querySelector("a.self-link");
selfLink.href = `#${div.id}`;
example.replaceWith(div);

View File

@ -36,7 +36,7 @@ export function rsDocToDataURL(mimeType, doc = document) {
return `data:${mimeType};charset=utf-8,${encodedString}`;
}
function serialize(format, doc) {
export function serialize(format, doc) {
const cloneDoc = doc.cloneNode(true);
cleanup(cloneDoc);
let result = "";

View File

@ -5,7 +5,7 @@
* @see https://github.com/w3c/respec/wiki/github
*/
import { getIntlData, showError, showWarning } from "../core/utils.js";
import { docLink, getIntlData, showError, showWarning } from "../core/utils.js";
export const name = "core/github";
let resolveGithubPromise;
@ -21,7 +21,7 @@ export const github = new Promise((resolve, reject) => {
const localizationStrings = {
en: {
file_a_bug: "File a bug",
file_a_bug: "File an issue",
participate: "Participate:",
commit_history: "Commit history",
},
@ -65,36 +65,40 @@ export async function run(conf) {
typeof conf.github === "object" &&
!conf.github.hasOwnProperty("repoURL")
) {
const msg =
"Config option `[github](https://github.com/w3c/respec/wiki/github)` " +
"is missing property `repoURL`.";
const msg = docLink`Config option ${"[github]"} is missing property \`repoURL\`.`;
rejectGithubPromise(msg);
return;
}
let tempURL = conf.github.repoURL || conf.github;
if (!tempURL.endsWith("/")) tempURL += "/";
/** @type URL */
let ghURL;
try {
ghURL = new URL(tempURL, "https://github.com");
} catch {
const msg = `\`respecConf.github\` is not a valid URL? (${ghURL})`;
const msg = docLink`${"[github]"} configuration option is not a valid URL? (${tempURL}).`;
rejectGithubPromise(msg);
return;
}
if (ghURL.origin !== "https://github.com") {
const msg = `\`respecConf.github\` must be HTTPS and pointing to GitHub. (${ghURL})`;
const msg = docLink`${"[github]"} configuration option must be HTTPS and pointing to GitHub. (${
ghURL.href
}).`;
rejectGithubPromise(msg);
return;
}
const [org, repo] = ghURL.pathname.split("/").filter(item => item);
if (!org || !repo) {
const msg =
"`respecConf.github` URL needs a path with, for example, w3c/my-spec";
const msg = docLink`${"[github]"} URL needs a path. For example, "w3c/my-spec".`;
rejectGithubPromise(msg);
return;
}
const branch = conf.github.branch || "gh-pages";
const issueBase = new URL("./issues/", ghURL).href;
const commitHistoryURL = new URL(
`./commits/${conf.github.branch ?? ""}`,
ghURL.href
);
const newProps = {
edDraftURI: `https://${org.toLowerCase()}.github.io/${repo}/`,
githubToken: undefined,
@ -105,27 +109,6 @@ export async function run(conf) {
pullBase: new URL("./pulls/", ghURL).href,
shortName: repo,
};
const otherLink = {
key: l10n.participate,
data: [
{
value: `GitHub ${org}/${repo}`,
href: ghURL,
},
{
value: l10n.file_a_bug,
href: newProps.issueBase,
},
{
value: l10n.commit_history,
href: new URL(`./commits/${branch}`, ghURL.href).href,
},
{
value: "Pull requests",
href: newProps.pullBase,
},
],
};
// Assign new properties, but retain existing ones
let githubAPI = "https://respec.org/github";
if (conf.githubAPI) {
@ -133,15 +116,47 @@ export async function run(conf) {
// for testing
githubAPI = conf.githubAPI;
} else {
const msg = "`respecConfig.githubAPI` should not be added manually.";
const msg =
"The `githubAPI` configuration option is private and should not be added manually.";
showWarning(msg, name);
}
}
if (!conf.excludeGithubLinks) {
const otherLink = {
key: l10n.participate,
data: [
{
value: `GitHub ${org}/${repo}`,
href: ghURL,
},
{
value: l10n.file_a_bug,
href: newProps.issueBase,
},
{
value: l10n.commit_history,
href: commitHistoryURL.href,
},
{
value: "Pull requests",
href: newProps.pullBase,
},
],
};
if (!conf.otherLinks) {
conf.otherLinks = [];
}
conf.otherLinks.unshift(otherLink);
}
const normalizedGHObj = {
branch,
repoURL: ghURL.href,
apiBase: githubAPI,
fullName: `${org}/${repo}`,
issuesURL: issueBase,
pullsURL: newProps.pullBase,
newIssuesURL: new URL("./new/choose", issueBase).href,
commitHistoryURL: commitHistoryURL.href,
};
resolveGithubPromise(normalizedGHObj);
@ -152,5 +167,4 @@ export async function run(conf) {
githubAPI,
};
Object.assign(conf, normalizedConfig);
conf.otherLinks.unshift(otherLink);
}

View File

@ -7,28 +7,18 @@
* All is done while keeping in mind that exported html stays clean
* on export.
*/
import { fetchAsset } from "./text-loader.js";
import css from "../styles/var.css.js";
import { norm } from "./utils.js";
import { sub } from "./pubsubhub.js";
export const name = "core/highlight-vars";
const hlVarsPromise = loadStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/var.css")).default;
} catch {
return fetchAsset("var.css");
}
}
export async function run(conf) {
export function run(conf) {
if (!conf.highlightVars) {
return;
}
const styleElement = document.createElement("style");
styleElement.textContent = await hlVarsPromise;
styleElement.textContent = css;
styleElement.classList.add("removeOnSave");
document.head.appendChild(styleElement);

View File

@ -4,7 +4,7 @@
*
* Performs syntax highlighting to all pre and code elements.
*/
import { fetchAsset } from "./text-loader.js";
import css from "../styles/highlight.css.js";
import { html } from "./import-maps.js";
import { msgIdGenerator } from "./utils.js";
import { workerPromise } from "./worker.js";
@ -12,16 +12,6 @@ export const name = "core/highlight";
const nextMsgId = msgIdGenerator("highlight");
const ghCssPromise = loadStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/highlight.css")).default;
} catch {
return fetchAsset("highlight.css");
}
}
function getLanguageHint(classList) {
return Array.from(classList)
.filter(item => item !== "highlight" && item !== "nolinks")
@ -101,10 +91,9 @@ export async function run(conf) {
const promisesToHighlight = highlightables
.filter(elem => elem.textContent.trim())
.map(highlightElement);
const ghCss = await ghCssPromise;
document.head.appendChild(
html`<style>
${ghCss}
${css}
</style>`
);
await Promise.all(promisesToHighlight);

View File

@ -3,6 +3,7 @@
import * as _idb from "../../node_modules/idb/build/esm/index.js";
import * as _webidl2 from "../../node_modules/webidl2/index.js";
import { MIMEType as _MIMEType } from "../../node_modules/sniffy-mimetype/index.js";
import _marked from "../../node_modules/marked/lib/marked.esm.js";
import _pluralize from "../../js/deps/builds/pluralize.js";
import hyperHTML from "../../node_modules/hyperhtml/esm.js";
@ -20,3 +21,7 @@ export const marked = _marked;
/** @type {import("pluralize")} */
// @ts-ignore
export const pluralize = _pluralize;
/** @type {import("sniffy-mimetype")} */
// @ts-ignore
export const MIMEType = _MIMEType;

View File

@ -4,23 +4,27 @@
import { htmlJoinComma, showError } from "./utils.js";
import { html } from "./import-maps.js";
const idlPrimitiveRegex = /^[a-z]+(\s+[a-z]+)+$/; // {{unrestricted double}} {{ double }}
const idlPrimitiveRegex = /^[a-z]+(\s+[a-z]+)+\??$/; // {{unrestricted double?}} {{ double }}
const exceptionRegex = /\B"([^"]*)"\B/; // {{ "SomeException" }}
const methodRegex = /(\w+)\((.*)\)$/;
const slotRegex = /^\[\[(\w+)\]\]$/;
export const slotRegex = /\[\[(\w+(?: +\w+)*)\]\](\([^)]*\))?$/;
// matches: `value` or `[[value]]`
// NOTE: [[value]] is actually a slot, but database has this as type="attribute"
const attributeRegex = /^((?:\[\[)?(?:\w+)(?:\]\])?)$/;
const attributeRegex = /^((?:\[\[)?(?:\w+(?: +\w+)*)(?:\]\])?)$/;
const baseRegex = /^(?:\w+)\??$/;
const enumRegex = /^(\w+)\["([\w- ]*)"\]$/;
// TODO: const splitRegex = /(?<=\]\]|\b)\./
// https://github.com/w3c/respec/pull/1848/files#r225087385
const methodSplitRegex = /\.?(\w+\(.*\)$)/;
const slotSplitRegex = /\/(.+)/;
const isProbablySlotRegex = /\[\[.+\]\]/;
/**
* @typedef {object} IdlBase
* @property {"base"} type
* @property {string} identifier
* @property {boolean} renderParent
* @property {boolean} nullable
* @property {InlineIdl | null} [parent]
*
* @typedef {object} IdlAttribute
@ -32,8 +36,10 @@ const methodSplitRegex = /\.?(\w+\(.*\)$)/;
* @typedef {object} IdlInternalSlot
* @property {"internal-slot"} type
* @property {string} identifier
* @property {string[]} [args]
* @property {boolean} renderParent
* @property {InlineIdl | null} [parent]
* @property {"attribute"|"method"} slotType
*
* @typedef {object} IdlMethod
* @property {"method"} type
@ -56,6 +62,7 @@ const methodSplitRegex = /\.?(\w+\(.*\)$)/;
*
* @typedef {object} IdlPrimitive
* @property {"idl-primitive"} type
* @property {boolean} nullable
* @property {string} identifier
* @property {boolean} renderParent
* @property {InlineIdl | null} [parent]
@ -68,10 +75,18 @@ const methodSplitRegex = /\.?(\w+\(.*\)$)/;
* @returns {InlineIdl[]}
*/
function parseInlineIDL(str) {
const [nonMethodPart, methodPart] = str.split(methodSplitRegex);
const tokens = nonMethodPart
// If it's got [[ string ]], then split as an internal slot
const isSlot = isProbablySlotRegex.test(str);
const splitter = isSlot ? slotSplitRegex : methodSplitRegex;
const [forPart, childString] = str.split(splitter);
if (isSlot && forPart && !childString) {
throw new SyntaxError(
`Internal slot missing "for" part. Expected \`{{ InterfaceName/${forPart}}}\` }.`
);
}
const tokens = forPart
.split(/[./]/)
.concat(methodPart)
.concat(childString)
.filter(s => s && s.trim())
.map(s => s.trim());
const renderParent = !str.includes("/");
@ -105,8 +120,19 @@ function parseInlineIDL(str) {
}
// internal slot
if (slotRegex.test(value)) {
const [, identifier] = value.match(slotRegex);
results.push({ type: "internal-slot", identifier, renderParent });
const [, identifier, allArgs] = value.match(slotRegex);
const slotType = allArgs ? "method" : "attribute";
const args = allArgs
?.slice(1, -1)
.split(/,\s*/)
.filter(arg => arg);
results.push({
type: "internal-slot",
slotType,
identifier,
args,
renderParent,
});
continue;
}
// attribute
@ -116,12 +142,21 @@ function parseInlineIDL(str) {
continue;
}
if (idlPrimitiveRegex.test(value)) {
results.push({ type: "idl-primitive", identifier: value, renderParent });
const nullable = value.endsWith("?");
const identifier = nullable ? value.slice(0, -1) : value;
results.push({
type: "idl-primitive",
identifier,
renderParent,
nullable,
});
continue;
}
// base, always final token
if (attributeRegex.test(value) && tokens.length === 0) {
results.push({ type: "base", identifier: value, renderParent });
if (baseRegex.test(value) && tokens.length === 0) {
const nullable = value.endsWith("?");
const identifier = nullable ? value.slice(0, -1) : value;
results.push({ type: "base", identifier, renderParent, nullable });
continue;
}
throw new SyntaxError(`IDL micro-syntax parsing error in \`{{ ${str} }}\``);
@ -139,10 +174,13 @@ function parseInlineIDL(str) {
*/
function renderBase(details) {
// Check if base is a local variable in a section
const { identifier, renderParent } = details;
const { identifier, renderParent, nullable } = details;
if (renderParent) {
return html`<a data-xref-type="_IDL_" data-link-type="idl"
><code>${identifier}</code></a
return html`<a
data-xref-type="_IDL_"
data-link-type="idl"
data-lt="${identifier}"
><code>${identifier + (nullable ? "?" : "")}</code></a
>`;
}
}
@ -152,19 +190,32 @@ function renderBase(details) {
* @param {IdlInternalSlot} details
*/
function renderInternalSlot(details) {
const { identifier, parent, renderParent } = details;
const { identifier, parent, slotType, renderParent, args } = details;
const { identifier: linkFor } = parent || {};
const lt = `[[${identifier}]]`;
const isMethod = slotType === "method";
const argsHtml = isMethod
? html`(${htmlJoinComma(args, htmlArgMapper)})`
: null;
const textArgs = isMethod ? `(${args.join(", ")})` : "";
const lt = `[[${identifier}]]${textArgs}`;
const element = html`${parent && renderParent ? "." : ""}<a
data-xref-type="attribute"
data-link-for=${linkFor}
data-xref-for=${linkFor}
data-xref-type="${slotType}"
data-link-for="${linkFor}"
data-xref-for="${linkFor}"
data-lt="${lt}"
><code>[[${identifier}]]</code></a
><code>[[${identifier}]]${argsHtml}</code></a
>`;
return element;
}
function htmlArgMapper(str, i, array) {
if (i < array.length - 1) return html`<var>${str}</var>`;
// only the last argument can be variadic
const parts = str.split(/(^\.{3})(.+)/);
const isVariadic = parts.length > 1;
const arg = isVariadic ? parts[2] : parts[0];
return html`${isVariadic ? "..." : null}<var>${arg}</var>`;
}
/**
* Attribute: .identifier
* @param {IdlAttribute} details
@ -174,7 +225,7 @@ function renderAttribute(details) {
const { identifier: linkFor } = parent || {};
const element = html`${renderParent ? "." : ""}<a
data-link-type="idl"
data-xref-type="attribute|dict-member"
data-xref-type="attribute|dict-member|const"
data-link-for="${linkFor}"
data-xref-for="${linkFor}"
><code>${identifier}</code></a
@ -189,7 +240,7 @@ function renderAttribute(details) {
function renderMethod(details) {
const { args, identifier, type, parent, renderParent } = details;
const { identifier: linkFor } = parent || {};
const argsText = htmlJoinComma(args, arg => html`<var>${arg}</var>`);
const argsText = htmlJoinComma(args, htmlArgMapper);
const searchText = `${identifier}(${args.join(", ")})`;
const element = html`${parent && renderParent ? "." : ""}<a
data-link-type="idl"
@ -244,12 +295,13 @@ function renderException(details) {
* @param {IdlPrimitive} details
*/
function renderIdlPrimitiveType(details) {
const { identifier } = details;
const { identifier, nullable } = details;
const element = html`<a
data-link-type="idl"
data-cite="WebIDL"
data-xref-type="interface"
><code>${identifier}</code></a
data-lt="${identifier}"
><code>${identifier + (nullable ? "?" : "")}</code></a
>`;
return element;
}

View File

@ -12,7 +12,6 @@ import {
getTextNodes,
norm,
refTypeFromContext,
showError,
showWarning,
} from "./utils.js";
import { html } from "./import-maps.js";
@ -22,37 +21,36 @@ import { renderInlineCitation } from "./render-biblio.js";
export const name = "core/inlines";
export const rfc2119Usage = {};
/** @param {RegExp[]} regexes */
const joinRegex = regexes => new RegExp(regexes.map(re => re.source).join("|"));
const localizationStrings = {
en: {
rfc2119Keywords() {
return new RegExp(
[
"\\bMUST(?:\\s+NOT)?\\b",
"\\bSHOULD(?:\\s+NOT)?\\b",
"\\bSHALL(?:\\s+NOT)?\\b",
"\\bMAY\\b",
"\\b(?:NOT\\s+)?REQUIRED\\b",
"\\b(?:NOT\\s+)?RECOMMENDED\\b",
"\\bOPTIONAL\\b",
].join("|")
);
return joinRegex([
/\bMUST(?:\s+NOT)?\b/,
/\bSHOULD(?:\s+NOT)?\b/,
/\bSHALL(?:\s+NOT)?\b/,
/\bMAY?\b/,
/\b(?:NOT\s+)?REQUIRED\b/,
/\b(?:NOT\s+)?RECOMMENDED\b/,
/\bOPTIONAL\b/,
]);
},
},
de: {
rfc2119Keywords() {
return new RegExp(
[
"\\bMUSS\\b",
"\\bERFORDERLICH\\b",
"\\b(?:NICHT\\s+)?NÖTIG\\b",
"\\bDARF(?:\\s+NICHT)?\\b",
"\\bVERBOTEN\\b",
"\\bSOLL(?:\\s+NICHT)?\\b",
"\\b(?:NICHT\\s+)?EMPFOHLEN\\b",
"\\bKANN\\b",
"\\bOPTIONAL\\b",
].join("|")
);
return joinRegex([
/\bMUSS\b/,
/\bERFORDERLICH\b/,
/\b(?:NICHT\s+)?NÖTIG\b/,
/\bDARF(?:\s+NICHT)?\b/,
/\bVERBOTEN\b/,
/\bSOLL(?:\s+NICHT)?\b/,
/\b(?:NICHT\s+)?EMPFOHLEN\b/,
/\bKANN\b/,
/\bOPTIONAL\b/,
]);
},
},
};
@ -62,8 +60,8 @@ const l10n = getIntlData(localizationStrings);
// TODO: Replace (?!`) at the end with (?:<!`) at the start when Firefox + Safari
// add support.
const inlineCodeRegExp = /(?:`[^`]+`)(?!`)/; // `code`
const inlineIdlReference = /(?:{{[^}]+}})/; // {{ WebIDLThing }}
const inlineVariable = /\B\|\w[\w\s]*(?:\s*:[\w\s&;<>]+)?\|\B/; // |var : Type|
const inlineIdlReference = /(?:{{[^}]+\?*}})/; // {{ WebIDLThing }}, {{ WebIDLThing? }}
const inlineVariable = /\B\|\w[\w\s]*(?:\s*:[\w\s&;<>]+\??)?\|\B/; // |var : Type?|
const inlineCitation = /(?:\[\[(?:!|\\|\?)?[\w.-]+(?:|[^\]]+)?\]\])/; // [[citation]]
const inlineExpansion = /(?:\[\[\[(?:!|\\|\?)?#?[\w-.]+\]\]\])/; // [[[expand]]]
const inlineAnchor = /(?:\[=[^=]+=\])/; // Inline [= For/link =]
@ -77,16 +75,29 @@ const inlineElement = /(?:\[\^[^^]+\^\])/; // Inline [^element^]
*/
function inlineElementMatches(matched) {
const value = matched.slice(2, -2).trim();
const [element, attribute] = value.split("/", 2).map(s => s && s.trim());
const [xrefType, xrefFor, textContent] = attribute
? ["element-attr", element, attribute]
: ["element", null, element];
const code = html`<code
const [forPart, attribute, attrValue] = value
.split("/", 3)
.map(s => s && s.trim())
.filter(s => !!s);
const [xrefType, xrefFor, textContent] = (() => {
// [^ /role ^], for example
const isGlobalAttr = value.startsWith("/");
if (isGlobalAttr) {
return ["element-attr", null, forPart];
} else if (attrValue) {
return ["attr-value", `${forPart}/${attribute}`, attrValue];
} else if (attribute) {
return ["element-attr", forPart, attribute];
} else {
return ["element", null, forPart];
}
})();
return html`<code
><a data-xref-type="${xrefType}" data-xref-for="${xrefFor}"
>${textContent}</a
></code
>`;
return code;
}
/**
@ -111,25 +122,25 @@ function inlineRefMatches(matched) {
if (!ref.startsWith("#")) {
return html`<a data-cite="${ref}"></a>`;
}
if (document.querySelector(ref)) {
return html`<a href="${ref}"></a>`;
}
const badReference = html`<span>${matched}</span>`;
const msg = `Wasn't able to expand ${matched} as it didn't match any id in the document.`;
const hint = `Please make sure there is element with id ${ref} in the document.`;
showError(msg, name, { hint, elements: [badReference] });
return badReference;
return html`<a href="${ref}"></a>`;
}
/**
* @param {string} matched
* @param {Text} text
*/
function inlineXrefMatches(matched) {
function inlineXrefMatches(matched, text) {
// slices "{{" at the beginning and "}}" at the end
const ref = matched.slice(2, -2).trim();
return ref.startsWith("\\")
? matched.replace("\\", "")
: idlStringToHtml(norm(ref));
const ref = norm(matched.slice(2, -2));
if (ref.startsWith("\\")) {
return matched.replace("\\", "");
}
const node = idlStringToHtml(ref);
// If it's inside a dfn, it should just be coded, not linked.
// This is because dfn elements are treated as links by ReSpec via role=link.
const renderAsCode = !!text.parentElement.closest("dfn");
return renderAsCode ? inlineCodeMatches(`\`${node.textContent}\``) : node;
}
/**
@ -146,7 +157,7 @@ function inlineBibrefMatches(matched, txt, conf) {
}
const [spec, linkText] = ref.split("|").map(norm);
const { type, illegal } = refTypeFromContext(spec, txt.parentNode);
const { type, illegal } = refTypeFromContext(spec, txt.parentElement);
const cite = renderInlineCitation(spec, linkText);
const cleanRef = spec.replace(/^(!|\?)/, "");
if (illegal && !conf.normativeReferences.has(cleanRef)) {
@ -196,7 +207,7 @@ function inlineVariableMatches(matched) {
*/
function inlineAnchorMatches(matched) {
matched = matched.slice(2, -2); // Chop [= =]
const parts = splitBySlash(matched, 2);
const parts = splitByFor(matched);
const [isFor, content] = parts.length === 2 ? parts : [null, parts[0]];
const [linkingText, text] = content.includes("|")
? content.split("|", 2).map(s => s.trim())
@ -243,12 +254,15 @@ export function run(conf) {
// PRE-PROCESSING
/** @type {NodeListOf<HTMLElement>} */
const abbrs = document.querySelectorAll("abbr[title]");
for (const abbr of abbrs) {
abbrMap.set(abbr.textContent, abbr.title);
const abbrElements = document.querySelectorAll("abbr[title]:not(.exclude)");
for (const { textContent, title } of abbrElements) {
const key = norm(textContent);
const value = norm(title);
abbrMap.set(key, value);
}
const aKeys = [...abbrMap.keys()];
const abbrRx = aKeys.length ? `(?:\\b${aKeys.join("\\b)|(?:\\b")}\\b)` : null;
const abbrRx = abbrMap.size
? new RegExp(`(?:\\b${[...abbrMap.keys()].join("\\b)|(?:\\b")}\\b)`)
: null;
// PROCESSING
// Don't gather text nodes for these:
@ -257,21 +271,24 @@ export function run(conf) {
wsNodes: false, // we don't want nodes with just whitespace
});
const keywords = l10n.rfc2119Keywords();
const rx = new RegExp(
`(${[
keywords.source,
inlineIdlReference.source,
inlineVariable.source,
inlineCitation.source,
inlineExpansion.source,
inlineAnchor.source,
inlineCodeRegExp.source,
inlineElement.source,
...(abbrRx ? [abbrRx] : []),
].join("|")})`
const inlinesRegex = new RegExp(
`(${
joinRegex([
keywords,
inlineIdlReference,
inlineVariable,
inlineCitation,
inlineExpansion,
inlineAnchor,
inlineCodeRegExp,
inlineElement,
...(abbrRx ? [abbrRx] : []),
]).source
})`
);
for (const txt of txts) {
const subtxt = txt.data.split(rx);
const subtxt = txt.data.split(inlinesRegex);
if (subtxt.length === 1) continue;
const df = document.createDocumentFragment();
let matched = true;
@ -279,38 +296,36 @@ export function run(conf) {
matched = !matched;
if (!matched) {
df.append(t);
} else if (t.startsWith("{{")) {
const node = inlineXrefMatches(t);
df.append(node);
} else if (t.startsWith("[[[")) {
const node = inlineRefMatches(t);
df.append(node);
} else if (t.startsWith("[[")) {
const nodes = inlineBibrefMatches(t, txt, conf);
df.append(...nodes);
} else if (t.startsWith("|")) {
const node = inlineVariableMatches(t);
df.append(node);
} else if (t.startsWith("[=")) {
const node = inlineAnchorMatches(t);
df.append(node);
} else if (t.startsWith("`")) {
const node = inlineCodeMatches(t);
df.append(node);
} else if (t.startsWith("[^")) {
const node = inlineElementMatches(t);
df.append(node);
} else if (abbrMap.has(t)) {
const node = inlineAbbrMatches(t, txt, abbrMap);
df.append(node);
} else if (keywords.test(t)) {
const node = inlineRFC2119Matches(t);
df.append(node);
} else {
// FAIL -- not sure that this can really happen
throw new Error(
`Found token '${t}' but it does not correspond to anything`
);
continue;
}
switch (true) {
case t.startsWith("{{"):
df.append(inlineXrefMatches(t, txt));
break;
case t.startsWith("[[["):
df.append(inlineRefMatches(t));
break;
case t.startsWith("[["):
df.append(...inlineBibrefMatches(t, txt, conf));
break;
case t.startsWith("|"):
df.append(inlineVariableMatches(t));
break;
case t.startsWith("[="):
df.append(inlineAnchorMatches(t));
break;
case t.startsWith("`"):
df.append(inlineCodeMatches(t));
break;
case t.startsWith("[^"):
df.append(inlineElementMatches(t));
break;
case abbrMap.has(t):
df.append(inlineAbbrMatches(t, txt, abbrMap));
break;
case keywords.test(t):
df.append(inlineRFC2119Matches(t));
break;
}
}
txt.replaceWith(df);
@ -318,15 +333,27 @@ export function run(conf) {
}
/**
* Split a string by slash (`/`) unless it's escaped by a backslash (`\`)
* Linking strings are always composed of:
*
* (for-part /)+ linking-text
*
* E.g., " ReadableStream / set up / pullAlgorithm ".
* Where "ReadableStream/set up/" is for-part, and "pullAlgorithm" is
* the linking-text.
*
* The for part is optional, but when present can be two or three levels deep.
*
* @param {string} str
*
* TODO: Use negative lookbehind (`str.split(/(?<!\\)\//)`) when supported.
* https://github.com/w3c/respec/issues/2869
*/
function splitBySlash(str, limit = Infinity) {
return str
.replace("\\/", "%%")
.split("/", limit)
.map(s => s && s.trim().replace("%%", "/"));
function splitByFor(str) {
const cleanUp = str => str.replace("%%", "/").split("/").map(norm).join("/");
const safeStr = str.replace("\\/", "%%");
const lastSlashIdx = safeStr.lastIndexOf("/");
if (lastSlashIdx === -1) {
return [cleanUp(safeStr)];
}
const forPart = safeStr.substring(0, lastSlashIdx);
const linkingText = safeStr.substring(lastSlashIdx + 1, safeStr.length);
return [cleanUp(forPart), cleanUp(linkingText)];
}

View File

@ -14,12 +14,11 @@
import {
addId,
getIntlData,
joinAnd,
parents,
showError,
showWarning,
} from "./utils.js";
import { fetchAsset } from "./text-loader.js";
import css from "../styles/issues-notes.css.js";
import { html } from "./import-maps.js";
import { pub } from "./pubsubhub.js";
@ -78,16 +77,6 @@ const localizationStrings = {
},
};
const cssPromise = loadStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/issues-notes.css")).default;
} catch {
return fetchAsset("issues-notes.css");
}
}
const l10n = getIntlData(localizationStrings);
/**
@ -307,17 +296,9 @@ function makeIssueSectionSummary(issueList) {
*/
function createLabelsGroup(labels, title, repoURL) {
const labelsGroup = labels.map(label => createLabel(label, repoURL));
const labelNames = labels.map(label => label.name);
const joinedNames = joinAnd(labelNames);
if (labelsGroup.length) {
labelsGroup.unshift(document.createTextNode(" "));
}
if (labelNames.length) {
const ariaLabel = `This issue is labelled as ${joinedNames}.`;
return html`<span class="issue-label" aria-label="${ariaLabel}"
>: ${title}${labelsGroup}</span
>`;
}
return html`<span class="issue-label">: ${title}${labelsGroup}</span>`;
}
@ -336,10 +317,12 @@ function createLabel(label, repoURL) {
issuesURL.searchParams.set("q", `is:issue is:open label:"${label.name}"`);
const color = textColorFromBgColor(bgColor);
const style = `background-color: #${bgColor}; color: ${color}`;
return html`<a
const ariaLabel = `GitHub label: ${name}`;
return html` <a
class="respec-gh-label"
style="${style}"
href="${issuesURL.href}"
aria-label="${ariaLabel}"
>${name}</a
>`;
}
@ -385,7 +368,6 @@ export async function run(conf) {
return; // nothing to do.
}
const ghIssues = await fetchAndStoreGithubIssues(conf.github);
const css = await cssPromise;
const { head: headElem } = document;
headElem.insertBefore(
html`<style>

View File

@ -287,17 +287,17 @@ function showLinkingError(elems) {
* @param {Conf} conf
*/
function updateReferences(conf) {
const shortName = new RegExp(
String.raw`\b${(conf.shortName || "").toLowerCase()}([^-])\b`,
"i"
);
const { shortName = "" } = conf;
// Match shortName in a data-cite (with optional leading ?!), while skipping shortName as prefix.
// https://regex101.com/r/rsZyIJ/5
const regex = new RegExp(String.raw`^([?!])?${shortName}\b([^-])`, "i");
/** @type {NodeListOf<HTMLElement>} */
const elems = document.querySelectorAll(
"dfn[data-cite]:not([data-cite='']), a[data-cite]:not([data-cite=''])"
);
for (const elem of elems) {
elem.dataset.cite = elem.dataset.cite.replace(shortName, `${THIS_SPEC}$1`);
elem.dataset.cite = elem.dataset.cite.replace(regex, `$1${THIS_SPEC}$2`);
const { key, isNormative } = toCiteDetails(elem);
if (key === THIS_SPEC) continue;

View File

@ -1,12 +1,11 @@
// @ts-check
/**
* Module: core/a11y
* Lints for accessibility issues using axe-core package.
*/
import { showError, showWarning } from "./utils.js";
import { showError, showWarning } from "../utils.js";
export const name = "core/a11y";
export const name = "core/linter-rules/a11y";
const DISABLED_RULES = [
"color-contrast", // too slow 🐢
@ -16,11 +15,12 @@ const DISABLED_RULES = [
];
export async function run(conf) {
if (!conf.a11y) {
if (!conf.lint?.a11y && /** legacy */ !conf.a11y) {
return;
}
const config = conf.lint?.a11y || /** legacy */ conf.a11y;
const options = conf.a11y === true ? {} : conf.a11y;
const options = config === true ? {} : config;
const violations = await getViolations(options);
for (const violation of violations) {
/**

View File

@ -1,35 +1,31 @@
// @ts-check
/**
* Linter rule "check-charset".
*
* Checks whether the document has `<meta charset="utf-8">` properly.
*/
import LinterRule from "../LinterRule.js";
import { lang as defaultLang } from "../l10n.js";
import { getIntlData, showWarning } from "../utils.js";
const name = "check-charset";
const meta = {
const ruleName = "check-charset";
export const name = "core/linter-rules/check-charset";
const localizationStrings = {
en: {
description: `Document must only contain one \`<meta>\` tag with charset set to 'utf-8'`,
howToFix: `Add this line in your document \`<head>\` section - \`<meta charset="utf-8">\` or set charset to "utf-8" if not set already.`,
msg: `Document must only contain one \`<meta>\` tag with charset set to 'utf-8'`,
hint: `Add this line in your document \`<head>\` section - \`<meta charset="utf-8">\` or set charset to "utf-8" if not set already.`,
},
zh: {
description: `文档只能包含一个 charset 属性为 utf-8 的 \`<meta>\` 标签`,
howToFix: `将此行添加到文档的 \`<head>\` 部分—— \`<meta charset="utf-8">\` 或将 charset 设置为 utf-8如果尚未设置`,
msg: `文档只能包含一个 charset 属性为 utf-8 的 \`<meta>\` 标签`,
hint: `将此行添加到文档的 \`<head>\` 部分—— \`<meta charset="utf-8">\` 或将 charset 设置为 utf-8如果尚未设置`,
},
};
const l10n = getIntlData(localizationStrings);
// Fall back to english, if language is missing
const lang = defaultLang in meta ? defaultLang : "en";
export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
/**
* Runs linter rule.
*
* @param {Object} _ The ReSpec config.
* @param {Document} doc The document to be checked.
*/
function linterFunction(_, doc) {
const metas = doc.querySelectorAll("meta[charset]");
/** @type {NodeListOf<HTMLMetaElement>} */
const metas = document.querySelectorAll("meta[charset]");
const val = [];
for (const meta of metas) {
val.push(meta.getAttribute("charset").trim().toLowerCase());
@ -38,15 +34,10 @@ function linterFunction(_, doc) {
// only a single meta[charset] and is set to utf-8, correct case
if (utfExists && metas.length === 1) {
return [];
return;
}
// if more than one meta[charset] tag defined along with utf-8
// or
// no meta[charset] present in the document
return {
name,
occurrences: metas.length,
...meta[lang],
};
showWarning(l10n.msg, name, { hint: l10n.hint, elements: [...metas] });
}
export const rule = new LinterRule(name, linterFunction);

View File

@ -2,30 +2,27 @@
/**
* Linter rule "check-internal-slots".
*/
import LinterRule from "../LinterRule.js";
import { lang as defaultLang } from "../l10n.js";
import { getIntlData, showWarning } from "../utils.js";
const name = "check-internal-slots";
const ruleName = "check-internal-slots";
export const name = "core/linter-rules/check-internal-slots";
const meta = {
const localizationStrings = {
en: {
description: "Internal slots should be preceded by a '.'",
howToFix: "Add a '.' between the elements mentioned.",
help: "See developer console.",
msg: "Internal slots should be preceded by a '.'",
hint: "Add a '.' between the elements mentioned.",
},
};
const l10n = getIntlData(localizationStrings);
// Fall back to english, if language is missing
const lang = defaultLang in meta ? defaultLang : "en";
export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
/**
* Runs linter rule.
* @param {Object} _ The ReSpec config.
* @param {Document} doc The document to be checked.
* @return {import("../../core/LinterRule").LinterResult}
*/
function linterFunction(_, doc) {
const offendingElements = [...doc.querySelectorAll("var+a")].filter(
/** @type {NodeListOf<HTMLAnchorElement>} */
const elems = document.querySelectorAll("var+a");
const offendingElements = [...elems].filter(
({ previousSibling: { nodeName } }) => {
const isPrevVar = nodeName && nodeName === "VAR";
return isPrevVar;
@ -36,12 +33,5 @@ function linterFunction(_, doc) {
return;
}
return {
name,
offendingElements,
occurrences: offendingElements.length,
...meta[lang],
};
showWarning(l10n.msg, name, { hint: l10n.hint, elements: offendingElements });
}
export const rule = new LinterRule(name, linterFunction);

View File

@ -1,47 +1,42 @@
// @ts-check
/**
* Linter rule "check-punctuation". Makes sure the there are no punctuations missing at the end of a <p>
* in the ReSpec config.
* Linter rule "check-punctuation". Makes sure the there are no punctuations missing at the end of a <p>.
*/
import LinterRule from "../LinterRule.js";
import { lang as defaultLang } from "../l10n.js";
import { getIntlData, showWarning } from "../utils.js";
const ruleName = "check-punctuation";
export const name = "core/linter-rules/check-punctuation";
const name = "check-punctuation";
const punctuationMarks = [".", ":", "!", "?"];
const humanMarks = punctuationMarks.map(mark => `"${mark}"`).join(", ");
const meta = {
const localizationStrings = {
en: {
description: "`p` elements should end with a punctuation mark.",
howToFix: `Please make sure \`p\` elements end with one of: ${humanMarks}.`,
msg: "`p` elements should end with a punctuation mark.",
hint: `Please make sure \`p\` elements end with one of: ${humanMarks}.`,
},
};
// Fall back to english, if language is missing
const lang = defaultLang in meta ? defaultLang : "en";
const l10n = getIntlData(localizationStrings);
export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
/**
* Runs linter rule.
*
* @param {Object} _ The ReSpec config.
* @param {Document} doc The document to be checked.
* @return {import("../../core/LinterRule").LinterResult}
*/
function lintingFunction(_, doc) {
// Check string ends with one of ., !, ?, :, ], or is empty.
const punctuatingRegExp = new RegExp(
`[${punctuationMarks.join("")}\\]]$|^ *$`,
"m"
);
const offendingElements = [
...doc.querySelectorAll("p:not(#back-to-top)"),
].filter(elem => !punctuatingRegExp.test(elem.textContent.trim()));
/** @type {NodeListOf<HTMLParagraphElement>} */
const elems = document.querySelectorAll("p:not(#back-to-top)");
const offendingElements = [...elems].filter(
elem => !punctuatingRegExp.test(elem.textContent.trim())
);
if (!offendingElements.length) {
return;
}
return {
name,
offendingElements,
occurrences: offendingElements.length,
...meta[lang],
};
showWarning(l10n.msg, name, { hint: l10n.hint, elements: offendingElements });
}
export const rule = new LinterRule(name, lintingFunction);

View File

@ -3,44 +3,34 @@
* Linter rule "warn-local-ref".
* Warns about href's that link to nonexistent id's in a spec
*/
import LinterRule from "../LinterRule.js";
import { lang as defaultLang } from "../l10n.js";
import { getIntlData, showWarning } from "../utils.js";
const name = "local-refs-exist";
const ruleName = "local-refs-exist";
export const name = "core/linter-rules/local-refs-exist";
const meta = {
const localizationStrings = {
en: {
description: "Broken local reference found in document.",
howToFix: "Please fix the links mentioned.",
help: "See developer console.",
msg: "Broken local reference found in document.",
hint: "Please fix the links mentioned.",
},
};
const l10n = getIntlData(localizationStrings);
// Fall back to english, if language is missing
const lang = defaultLang in meta ? defaultLang : "en";
/**
* Runs linter rule.
* @param {Object} _ The ReSpec config.
* @param {Document} doc The document to be checked.
* @return {import("../../core/LinterRule").LinterResult}
*/
function linterFunction(_, doc) {
const offendingElements = [...doc.querySelectorAll("a[href^='#']")].filter(
isBrokenHyperlink
);
if (!offendingElements.length) {
export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
return {
name,
offendingElements,
occurrences: offendingElements.length,
...meta[lang],
};
}
export const rule = new LinterRule(name, linterFunction);
/** @type {NodeListOf<HTMLAnchorElement>} */
const elems = document.querySelectorAll("a[href^='#']");
const offendingElements = [...elems].filter(isBrokenHyperlink);
if (offendingElements.length) {
showWarning(l10n.msg, name, {
hint: l10n.hint,
elements: offendingElements,
});
}
}
function isBrokenHyperlink(elem) {
const id = elem.getAttribute("href").substring(1);

View File

@ -5,50 +5,43 @@
* Checks that there are no sections in the document that don't start
* with a heading element (h1-6).
*/
import LinterRule from "../LinterRule.js";
import { lang as defaultLang } from "../l10n.js";
const name = "no-headingless-sections";
const meta = {
import { getIntlData, showWarning } from "../utils.js";
const ruleName = "no-headingless-sections";
export const name = "core/linter-rules/no-headingless-sections";
const localizationStrings = {
en: {
description: "All sections must start with a `h2-6` element.",
howToFix: "Add a `h2-6` to the offending section or use a `<div>`.",
help: "See developer console.",
msg: "All sections must start with a `h2-6` element.",
hint: "Add a `h2-6` to the offending section or use a `<div>`.",
},
nl: {
description: "Alle secties moeten beginnen met een `h2-6` element.",
howToFix:
"Voeg een `h2-6` toe aan de conflicterende sectie of gebruik een `<div>`.",
help: "Zie de developer console.",
msg: "Alle secties moeten beginnen met een `h2-6` element.",
hint: "Voeg een `h2-6` toe aan de conflicterende sectie of gebruik een `<div>`.",
},
zh: {
description: "所有章节section都必须以 `h2-6` 元素开头。",
howToFix: "将 `h2-6` 添加到有问题的章节或使用 `<div>`。",
msg: "所有章节section都必须以 `h2-6` 元素开头。",
hint: "将 `h2-6` 添加到有问题的章节或使用 `<div>`。",
},
};
const l10n = getIntlData(localizationStrings);
// Fall back to english, if language is missing
const lang = defaultLang in meta ? defaultLang : "en";
const hasNoHeading = ({ firstElementChild: elem }) => {
return elem === null || /^h[1-6]$/.test(elem.localName) === false;
};
/**
* @param {*} _
* @param {Document} doc
* @return {import("../../core/LinterRule").LinterResult}
*/
function linterFunction(_, doc) {
const offendingElements = [...doc.querySelectorAll("section")].filter(
hasNoHeading
);
if (!offendingElements.length) {
export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
return {
name,
offendingElements,
occurrences: offendingElements.length,
...meta[lang],
};
const offendingElements = [...document.querySelectorAll("section")].filter(
hasNoHeading
);
if (offendingElements.length) {
showWarning(l10n.msg, name, {
hint: l10n.hint,
elements: offendingElements,
});
}
}
export const rule = new LinterRule(name, linterFunction);

View File

@ -3,56 +3,44 @@
* Linter rule "no-http-props". Makes sure the there are no URLs that
* start with http:// in the ReSpec config.
*/
import LinterRule from "../LinterRule.js";
import { lang as defaultLang } from "../l10n.js";
import { docLink, getIntlData, joinAnd, showWarning } from "../utils.js";
const name = "no-http-props";
const ruleName = "no-http-props";
export const name = "core/linter-rules/no-http-props";
const meta = {
const localizationStrings = {
en: {
description: "Insecure URLs are not allowed in `respecConfig`.",
howToFix: "Please change the following properties to 'https://': ",
msg: docLink`Insecure URLs are not allowed in ${"[respecConfig]"}.`,
hint: "Please change the following properties to 'https://': ",
},
zh: {
description: "`respecConfig` 中不允许使用不安全的URL.",
howToFix: "请将以下属性更改为 https://",
msg: docLink`${"[respecConfig]"} 中不允许使用不安全的URL.`,
hint: "请将以下属性更改为 https://",
},
};
const l10n = getIntlData(localizationStrings);
// Fall back to english, if language is missing
const lang = defaultLang in meta ? defaultLang : "en";
/**
* Runs linter rule.
*
* @param {Object} conf The ReSpec config.
* @param {Document} doc The document to be checked.
*/
function lintingFunction(conf, doc) {
// We can only really perform this check over http/https
if (!doc.location.href.startsWith("http")) {
export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
// We can only really perform this check over http/https
// Using parent's location as tests are loaded in iframe as a srcdoc.
if (!parent.location.href.startsWith("http")) {
return;
}
const offendingMembers = Object.getOwnPropertyNames(conf)
// this check is cheap, "prevED" is w3c exception.
.filter(key => key.endsWith("URI") || key === "prevED")
.filter(key => (key.endsWith("URI") && conf[key]) || key === "prevED")
// this check is expensive, so separate step
.filter(key =>
new URL(conf[key], doc.location.href).href.startsWith("http://")
new URL(conf[key], parent.location.href).href.startsWith("http://")
);
if (!offendingMembers.length) {
return;
}
/** @type {import("../../core/LinterRule").LinterResult} */
const result = {
name,
occurrences: offendingMembers.length,
...meta[lang],
};
result.howToFix += `${offendingMembers
.map(item => `\`${item}\``)
.join(", ")}.`;
return result;
}
export const rule = new LinterRule(name, lintingFunction);
if (offendingMembers.length) {
const keys = joinAnd(offendingMembers, key => docLink`${`[${key}]`}`);
showWarning(l10n.msg, name, { hint: l10n.hint + keys });
}
}

View File

@ -5,28 +5,24 @@
* Checks that an variable is used if declared (the first use is treated as
* declaration).
*/
import LinterRule from "../LinterRule.js";
import { lang as defaultLang } from "../l10n.js";
import { norm } from "../utils.js";
import { getIntlData, norm, showWarning } from "../utils.js";
const name = "no-unused-vars";
const ruleName = "no-unused-vars";
export const name = "core/linter-rules/no-unused-vars";
const meta = {
const localizationStrings = {
en: {
description: "Variable was defined, but never used.",
howToFix: "Add a `data-ignore-unused` attribute to the `<var>`.",
help: "See developer console.",
msg: "Variable was defined, but never used.",
hint: "Add a `data-ignore-unused` attribute to the `<var>`.",
},
};
// Fall back to english, if language is missing
const lang = defaultLang in meta ? defaultLang : "en";
const l10n = getIntlData(localizationStrings);
export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
/**
* @param {*} _
* @param {Document} doc
* @return {import("../LinterRule").LinterResult}
*/
function linterFunction(_, doc) {
const offendingElements = [];
/**
@ -48,7 +44,7 @@ function linterFunction(_, doc) {
":scope > :not(section) ~ .algorithm, :scope > :not(section) .algorithm"
);
for (const section of doc.querySelectorAll("section")) {
for (const section of document.querySelectorAll("section")) {
if (!sectionContainsAlgorithm(section)) continue;
/**
@ -73,14 +69,10 @@ function linterFunction(_, doc) {
}
}
if (!offendingElements.length) {
return;
if (offendingElements.length) {
showWarning(l10n.msg, name, {
hint: l10n.hint,
elements: offendingElements,
});
}
return {
name,
offendingElements,
occurrences: offendingElements.length,
...meta[lang],
};
}
export const rule = new LinterRule(name, linterFunction);

View File

@ -7,21 +7,20 @@
* case-insensitive, multi-line check.
*
*/
import LinterRule from "../../core/LinterRule.js";
import { lang as defaultLang } from "../../core/l10n.js";
const name = "privsec-section";
const meta = {
import { getIntlData, showWarning } from "../utils.js";
const ruleName = "privsec-section";
export const name = "core/linter-rules/privsec-section";
const localizationStrings = {
en: {
description:
"Document must a 'Privacy and/or Security' Considerations section.",
howToFix: "Add a privacy and/or security considerations section.",
help:
msg: "Document must have a 'Privacy and/or Security' Considerations section.",
hint:
"Add a privacy and/or security considerations section. " +
"See the [Self-Review Questionnaire](https://w3ctag.github.io/security-questionnaire/).",
},
};
// Fall back to english, if language is missing
const lang = defaultLang in meta ? defaultLang : "en";
const l10n = getIntlData(localizationStrings);
function hasPriSecConsiderations(doc) {
return Array.from(doc.querySelectorAll("h2, h3, h4, h5, h6")).some(
@ -33,15 +32,12 @@ function hasPriSecConsiderations(doc) {
);
}
/**
* @param {*} conf
* @param {Document} doc
* @return {import("../LinterRule").LinterResult}
*/
function lintingFunction(conf, doc) {
if (conf.isRecTrack && !hasPriSecConsiderations(doc)) {
return { name, occurrences: 1, ...meta[lang] };
export function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
if (conf.isRecTrack && !hasPriSecConsiderations(document)) {
showWarning(l10n.msg, name, { hint: l10n.hint });
}
}
export const rule = new LinterRule(name, lintingFunction);

View File

@ -3,70 +3,47 @@
* Linter rule "wpt-tests-exist".
* Warns about nonexistent web platform tests.
*/
import LinterRule from "../LinterRule.js";
import { lang as defaultLang } from "../l10n.js";
import { showWarning } from "../utils.js";
import { getIntlData, showWarning } from "../utils.js";
const name = "wpt-tests-exist";
const ruleName = "wpt-tests-exist";
export const name = "core/linter-rules/wpt-tests-exist";
const meta = {
const localizationStrings = {
en: {
description: "Non-existent Web Platform Tests",
howToFix: "Please fix the tests mentioned.",
help: "See developer console.",
msg: "The following test could not be found in Web Platform Tests:",
hint: "Check [wpt.live](https://wpt.live) to see if it was deleted or renamed.",
},
};
const l10n = getIntlData(localizationStrings);
const lang = defaultLang in meta ? defaultLang : "en";
export async function run(conf) {
if (!conf.lint?.[ruleName]) {
return;
}
/**
* Runs linter rule.
* @param {Object} conf The ReSpec config.
* @param {Document} doc The document to be checked.
* @return {Promise<import("../LinterRule").LinterResult>}
*/
async function linterFunction(conf, doc) {
const filesInWPT = await getFilesInWPT(conf.testSuiteURI, conf.githubAPI);
if (!filesInWPT) {
return;
}
const offendingElements = [];
const offendingTests = new Set();
/** @type {NodeListOf<HTMLElement>} */
const elems = doc.querySelectorAll("[data-tests]");
const elems = document.querySelectorAll("[data-tests]");
const testables = [...elems].filter(elem => elem.dataset.tests);
for (const elem of testables) {
const tests = elem.dataset.tests
elem.dataset.tests
.split(/,/gm)
.map(test => test.trim().split("#")[0])
.filter(test => test);
const missingTests = tests.filter(test => !filesInWPT.has(test));
if (missingTests.length) {
offendingElements.push(elem);
missingTests.forEach(test => offendingTests.add(test));
}
.filter(test => test && !filesInWPT.has(test))
.map(missingTest => {
showWarning(`${l10n.msg} \`${missingTest}\`.`, name, {
hint: l10n.hint,
elements: [elem],
});
});
}
if (!offendingElements.length) {
return;
}
const missingTests = [...offendingTests].map(test => `\`${test}\``);
return {
name,
offendingElements,
occurrences: offendingElements.length,
...meta[lang],
description: `${meta[lang].description}: ${missingTests.join(", ")}.`,
};
}
export const rule = new LinterRule(name, linterFunction);
/**
* @param {string} testSuiteURI
* @param {string} githubAPIBase

View File

@ -1,87 +0,0 @@
// @ts-check
/**
* Module core/linter
*
* Core linter module. Exports a linter object.
*/
import { showWarning } from "./utils.js";
export const name = "core/linter";
/** @type {WeakMap<Linter, { rules: Set<import("./LinterRule").default> }>} */
const privates = new WeakMap();
class Linter {
constructor() {
privates.set(this, {
rules: new Set(),
});
}
get rules() {
return privates.get(this).rules;
}
/**
* @param {...import("./LinterRule").default} newRules
*/
register(...newRules) {
newRules.forEach(newRule => this.rules.add(newRule));
}
async lint(conf, doc = window.document) {
const promisesToLint = [...privates.get(this).rules].map(rule =>
toLinterWarning(rule.lint(conf, doc))
);
await promisesToLint;
}
}
const linter = new Linter();
export default linter;
const baseResult = {
name: "unknown",
description: "",
occurrences: 0,
howToFix: "",
offendingElements: [], // DOM Nodes
help: "", // where to get help
};
/**
* @typedef {import("./LinterRule").LinterResult} LinterResult
* @param {LinterResult | Promise<LinterResult>} [resultPromise]
*/
async function toLinterWarning(resultPromise) {
const result = await resultPromise;
if (!result) {
return;
}
const output = { ...baseResult, ...result };
const {
description,
help,
howToFix,
name: linterName,
occurrences,
offendingElements,
} = output;
const msg = offendingElements.length
? description
: `${description} (Count: ${occurrences})`;
const plugin = `${name}/${linterName}`;
const hint = `${howToFix} ${help}`;
showWarning(msg, plugin, { hint, elements: offendingElements });
}
export function run(conf) {
if (conf.lint === false) {
return; // nothing to do
}
// return early, continue processing other things
(async () => {
await document.respec.ready;
try {
await linter.lint(conf, document);
} catch (err) {
console.error("Error ocurred while running the linter", err);
}
})();
}

View File

@ -61,6 +61,7 @@ class Renderer extends marked.Renderer {
return `<pre class="idl">${code}</pre>`;
}
// @ts-expect-error
const html = super.code(code, language, isEscaped);
const { example, illegalExample } = metaData;
@ -100,6 +101,7 @@ class Renderer extends marked.Renderer {
const [, textContent, id] = text.match(headingWithIdRegex);
return `<h${level} id="${id}">${textContent}</h${level}>`;
}
// @ts-expect-error
return super.heading(text, level, raw, slugger);
}
}

View File

@ -1,6 +1,6 @@
// @ts-check
import { fetchAndCache, getIntlData, showError } from "./utils.js";
import { fetchAsset } from "./text-loader.js";
import css from "../styles/mdn-annotation.css.js";
import { html } from "./import-maps.js";
export const name = "core/mdn-annotation";
@ -42,14 +42,6 @@ const localizationStrings = {
};
const l10n = getIntlData(localizationStrings);
async function loadStyle() {
try {
return (await import("text!../../assets/mdn-annotation.css")).default;
} catch {
return fetchAsset("mdn-annotation.css");
}
}
/**
* @param {HTMLElement} node
*/
@ -141,7 +133,7 @@ export async function run(conf) {
if (!mdnSpecJson) return;
const style = document.createElement("style");
style.textContent = await loadStyle();
style.textContent = css;
document.head.append(style);
for (const elem of findElements(mdnSpecJson)) {
@ -176,10 +168,8 @@ function getMdnKey(conf) {
* @returns {Promise<MdnData|undefined>}
*/
async function getMdnData(key, mdnConf) {
const {
baseJsonPath = BASE_JSON_PATH,
maxAge = 60 * 60 * 24 * 1000,
} = mdnConf;
const { baseJsonPath = BASE_JSON_PATH, maxAge = 60 * 60 * 24 * 1000 } =
mdnConf;
const url = new URL(`${key}.json`, baseJsonPath).href;
const res = await fetchAndCache(url, maxAge);
if (res.status === 404) {

View File

@ -59,16 +59,6 @@ const REF_STATUSES = new Map([
["WG-NOTE", "W3C Working Group Note"],
]);
const defaultsReference = Object.freeze({
authors: [],
date: "",
href: "",
publisher: "",
status: "",
title: "",
etAl: false,
});
const endWithDot = endNormalizer(".");
/** @param {Conf} conf */
@ -229,38 +219,8 @@ function endNormalizer(endStr) {
};
}
export function wireReference(rawRef, target = "_blank") {
if (typeof rawRef !== "object") {
throw new TypeError("Only modern object references are allowed");
}
const ref = Object.assign({}, defaultsReference, rawRef);
const authors = ref.authors.join("; ") + (ref.etAl ? " et al" : "");
const status = REF_STATUSES.get(ref.status) || ref.status;
return html.wire(ref)`
<cite>
<a
href="${ref.href}"
target="${target}"
rel="noopener noreferrer">
${ref.title.trim()}</a>.
</cite>
<span class="authors">
${endWithDot(authors)}
</span>
<span class="publisher">
${endWithDot(ref.publisher)}
</span>
<span class="pubDate">
${endWithDot(ref.date)}
</span>
<span class="pubStatus">
${endWithDot(status)}
</span>
`;
}
/** @param {BiblioData|string} ref */
export function stringifyReference(ref) {
function stringifyReference(ref) {
if (typeof ref === "string") return ref;
let output = `<cite>${ref.title}</cite>`;

View File

@ -8,6 +8,7 @@
* This module also adds the legacy `document.respecIsReady` property for
* backward compatibility. It is now an alias to `document.respec.ready`.
*/
import { serialize } from "../core/exporter.js";
import { showWarning } from "../core/utils.js";
import { sub } from "./pubsubhub.js";
@ -40,6 +41,10 @@ class ReSpec {
get ready() {
return this._respecDonePromise;
}
async toHTML() {
return serialize("html", document);
}
}
export function init() {

View File

@ -4,22 +4,22 @@
// CONFIGURATION:
// - noTOC: if set to true, no TOC is generated and sections are not numbered
// - tocIntroductory: if set to true, the introductory material is listed in the TOC
// - lang: can change the generated text (supported: en, fr)
// - maxTocLevel: only generate a TOC so many levels deep
import {
addId,
docLink,
getIntlData,
parents,
renameElement,
showError,
showWarning,
} from "./utils.js";
import { html } from "./import-maps.js";
import { pub } from "./pubsubhub.js";
const lowerHeaderTags = ["h2", "h3", "h4", "h5", "h6"];
const headerTags = ["h1", ...lowerHeaderTags];
export const name = "core/structure";
@ -135,11 +135,9 @@ function appendixNumber(num) {
*
* @param {Element} parent
*/
function getSectionTree(parent, { tocIntroductory = false } = {}) {
function getSectionTree(parent) {
/** @type {NodeListOf<HTMLElement>} */
const sectionElements = tocIntroductory
? parent.querySelectorAll(":scope > section")
: parent.querySelectorAll(":scope > section:not(.introductory)");
const sectionElements = parent.querySelectorAll(":scope > section");
/** @type {Section[]} */
const sections = [];
@ -158,9 +156,9 @@ function getSectionTree(parent, { tocIntroductory = false } = {}) {
element: section,
header,
title,
isIntro: section.classList.contains("introductory"),
isIntro: Boolean(section.closest(".introductory")),
isAppendix: section.classList.contains("appendix"),
subsections: getSectionTree(section, { tocIntroductory }),
subsections: getSectionTree(section),
});
}
return sections;
@ -194,9 +192,12 @@ function filterHeader(h) {
}
export function run(conf) {
if ("tocIntroductory" in conf === false) {
conf.tocIntroductory = false;
if ("tocIntroductory" in conf && conf.tocIntroductory === false) {
const msg = "Configuration option `tocIntroductory` is deprecated.";
const hint = docLink`Use the ${"[`notoc`|#notoc-class]"} CSS class to remove a section from the Table of Contents.`;
showWarning(msg, name, { hint });
}
if ("maxTocLevel" in conf === false) {
conf.maxTocLevel = Infinity;
}
@ -206,9 +207,7 @@ export function run(conf) {
// makeTOC
if (!conf.noTOC) {
skipFromToC();
const sectionTree = getSectionTree(document.body, {
tocIntroductory: conf.tocIntroductory,
});
const sectionTree = getSectionTree(document.body);
const result = scanSections(sectionTree, conf.maxTocLevel);
if (result) {
createTableOfContents(result);
@ -234,12 +233,11 @@ function renameSectionHeaders() {
}
function getNonintroductorySectionHeaders() {
const headerSelector = headerTags
.map(h => `section:not(.introductory) ${h}:first-child`)
.join(",");
return [...document.querySelectorAll(headerSelector)].filter(
elem => !elem.closest("section.introductory")
);
return [
...document.querySelectorAll(
"section:not(.introductory) :is(h1,h2,h3,h4,h5,h6):first-child"
),
].filter(elem => !elem.closest("section.introductory"));
}
/**

View File

@ -9,30 +9,23 @@
// CONFIGURATION
// - noReSpecCSS: if you're using a profile that loads this module but you don't want
// the style, set this to true
import { fetchAsset } from "./text-loader.js";
import css from "../styles/respec.css.js";
export const name = "core/style";
// Opportunistically inserts the style, with the chance to reduce some FOUC
const styleElement = insertStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/respec.css")).default;
} catch {
return fetchAsset("respec.css");
}
}
async function insertStyle() {
function insertStyle() {
const styleElement = document.createElement("style");
styleElement.id = "respec-mainstyle";
styleElement.textContent = await loadStyle();
styleElement.textContent = css;
document.head.appendChild(styleElement);
return styleElement;
}
export async function run(conf) {
export function run(conf) {
if (conf.noReSpecCSS) {
(await styleElement).remove();
styleElement.remove();
}
}

View File

@ -1,10 +1,12 @@
// @ts-check
import { docLink, showError } from "../../core/utils.js";
import { html } from "../../core/import-maps.js";
import { showWarning } from "../../core/utils.js";
const name = "core/templates/show-logo";
/**
* Logo mapper. Takes a logo structure and converts it to HTML.
*
* @param {object} logo
* @param {string} logo.src
* @param {string} logo.url
@ -12,24 +14,30 @@ const name = "core/templates/show-logo";
* @param {string} [logo.id]
* @param {number} [logo.width]
* @param {number} [logo.height]
* @param {number} index
*/
export default function showLogo(logo) {
export default function showLogo(logo, index) {
/** @type {HTMLAnchorElement} */
const a = html`<a href="${logo.url || ""}" class="logo"></a>`;
const a = html`<a href="${logo.url || null}" class="logo"
><img
alt="${logo.alt || null}"
crossorigin
height="${logo.height || null}"
id="${logo.id || null}"
src="${logo.src || null}"
width="${logo.width || null}"
/>
</a>`;
if (!logo.alt) {
const msg = "Found spec logo without an `alt` attribute.";
showWarning(msg, name, { elements: [a] });
const src = logo.src ? `, with \`src\` ${logo.src}, ` : "";
const msg = `Logo at index ${index}${src} is missing required "\`alt\`" property.`;
const hint = docLink`Add the missing "\`alt\`" property describing the logo. See ${"[logos]"} for more information.`;
showError(msg, name, { hint, elements: [a] });
}
if (!logo.src) {
const msg = `Logo at index ${index} is missing "\`src\`" property.`;
const hint = docLink`The \`src\` property is required on every logo. See ${"[logos]"} for more information.`;
showError(msg, name, { hint, elements: [a] });
}
/** @type {HTMLImageElement} */
const img = html`<img
id="${logo.id}"
alt="${logo.alt}"
width="${logo.width}"
height="${logo.height}"
/>`;
// avoid triggering 404 requests from dynamically generated
// hyperHTML attribute values
img.src = logo.src;
a.append(img);
return a;
}

View File

@ -1,10 +1,16 @@
// @ts-check
import { humanDate, showError, toShortIsoDate } from "../../core/utils.js";
import { lang as defaultLang } from "../../core/l10n.js";
import { html } from "../../core/import-maps.js";
const name = "core/templates/show-people";
import {
humanDate,
isValidConfDate,
showError,
showWarning,
} from "../../core/utils.js";
import { lang as defaultLang } from "../../core/l10n.js";
import { html } from "../../core/import-maps.js";
const localizationStrings = {
en: {
until(date) {
@ -40,7 +46,7 @@ const localizationStrings = {
const lang = defaultLang in localizationStrings ? defaultLang : "en";
const orcidIcon = html`<svg
const orcidIcon = () => html`<svg
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
@ -62,115 +68,229 @@ const orcidIcon = html`<svg
</svg>`;
/**
* @typedef {object} Person
* @property {string} [Person.name]
* @property {string} [Person.company]
* @property {string|number} [Person.w3cid]
* @property {string} [Person.mailto]
* @property {string} [Person.url]
* @property {string} [Person.orcid]
* @property {string} [Person.company]
* @property {string} [Person.companyURL]
* @property {string} [Person.note]
* @property {string} [Person.retiredDate]
* @property {PersonExtras} [Person.extras]
*
* @typedef {object} PersonExtras
* @property {string} PersonExtras.name
* @property {string} [PersonExtras.class]
* @property {string} [PersonExtras.href]
*
* @param {Person[]} persons
* @param {Conf} conf
* @param {"editors" | "authors" | "formerEditors"} propName - the name of the property of the people to render.
*/
export default function showPeople(persons = []) {
const l10n = localizationStrings[lang];
return persons.map(getItem);
export default function showPeople(conf, propName) {
const people = conf[propName];
if (!Array.isArray(people) || !people.length) return; // nothing to show...
function getItem(p) {
const personName = [p.name]; // treated as opt-in HTML by hyperHTML
const company = [p.company];
const editorid = p.w3cid ? parseInt(p.w3cid, 10) : null;
/** @type {HTMLElement} */
const dd = html`<dd
class="p-author h-card vcard"
data-editor-id="${editorid}"
></dd>`;
const span = document.createDocumentFragment();
const contents = [];
if (p.mailto) {
contents.push(html`<a
class="ed_mailto u-email email p-name"
href="${`mailto:${p.mailto}`}"
>${personName}</a
>`);
} else if (p.url) {
contents.push(
html`<a class="u-url url p-name fn" href="${p.url}">${personName}</a>`
);
} else {
contents.push(html`<span class="p-name fn">${personName}</span>`);
}
if (p.orcid) {
contents.push(
html`<a class="p-name orcid" href="${p.orcid}"
>${orcidIcon.cloneNode(true)}
</a>`
);
}
if (p.company) {
if (p.companyURL) {
contents.push(
html`
(<a class="p-org org h-org h-card" href="${p.companyURL}"
>${company}</a
>)
`
);
} else {
contents.push(html` (${company}) `);
}
}
if (p.note) contents.push(document.createTextNode(` (${p.note})`));
if (p.extras) {
const results = p.extras
// Remove empty names
.filter(extra => extra.name && extra.name.trim())
// Convert to HTML
.map(getExtra);
for (const result of results) {
contents.push(document.createTextNode(", "), result);
}
}
if (p.retiredDate) {
const retiredDate = new Date(p.retiredDate);
const isValidDate = retiredDate.toString() !== "Invalid Date";
const timeElem = document.createElement("time");
timeElem.textContent = isValidDate
? humanDate(retiredDate)
: "Invalid Date"; // todo: Localise invalid date
if (!isValidDate) {
const msg = "The date is invalid. The expected format is YYYY-MM-DD.";
const title = "Invalid date";
showError(msg, name, { title, elements: [timeElem] });
}
timeElem.dateTime = toShortIsoDate(retiredDate);
contents.push(html` - ${l10n.until(timeElem)} `);
}
// @ts-ignore: hyperhtml types only support Element but we use a DocumentFragment here
html.bind(span)`${contents}`;
dd.appendChild(span);
return dd;
}
function getExtra(extra) {
const span = html`<span class="${extra.class || null}"></span>`;
let textContainer = span;
if (extra.href) {
textContainer = html`<a href="${extra.href}"></a>`;
span.appendChild(textContainer);
}
textContainer.textContent = extra.name;
return span;
}
const validatePerson = personValidator(propName);
return people.filter(validatePerson).map(personToHTML);
}
/**
* @param {Person} person
*/
function personToHTML(person) {
const l10n = localizationStrings[lang];
// The following are treated as opt-in HTML by hyperHTML
// we need to deprecate this!
const personName = [person.name];
const company = [person.company];
const editorId = person.w3cid || null;
const contents = [];
if (person.mailto) {
person.url = `mailto:${person.mailto}`;
}
if (person.url) {
const url = new URL(person.url, document.location.href);
const classList =
url.protocol === "mailto:"
? "ed_mailto u-email email p-name"
: "u-url url p-name fn";
contents.push(
html`<a class="${classList}" href="${person.url}">${personName}</a>`
);
} else {
contents.push(html`<span class="p-name fn">${personName}</span>`);
}
if (person.orcid) {
contents.push(
html`<a class="p-name orcid" href="${person.orcid}">${orcidIcon()}</a>`
);
}
if (person.company) {
const hCard = "p-org org h-org";
const companyElem = person.companyURL
? html`<a class="${hCard}" href="${person.companyURL}">${company}</a>`
: html`<span class="${hCard}">${company}</span>`;
contents.push(html` (${companyElem})`);
}
if (person.note) {
contents.push(document.createTextNode(` (${person.note})`));
}
if (person.extras) {
contents.push(...person.extras.map(extra => html`, ${renderExtra(extra)}`));
}
if (person.retiredDate) {
const { retiredDate } = person;
const time = html`<time datetime="${retiredDate}"
>${humanDate(retiredDate)}</time
>`;
contents.push(html` - ${l10n.until(time)} `);
}
const dd = html`<dd
class="editor p-author h-card vcard"
data-editor-id="${editorId}"
>
${contents}
</dd>`;
return dd;
}
function renderExtra(extra) {
const classVal = extra.class || null;
const { name, href } = extra;
return href
? html`<a href="${href}" class="${classVal}">${name}</a>`
: html`<span class="${classVal}">${name}</span>`;
}
/**
*
* @param {string} prop
*/
function personValidator(prop) {
/**
* @param {Person} person
* @param {Number} index
*/
return function validatePerson(person, index) {
const docsUrl = "https://respec.org/docs/";
const seePersonHint = `See [person](${docsUrl}#person) configuration for available options.`;
const preamble =
`Error processing the [person object](${docsUrl}#person) ` +
`at index ${index} of the "[\`${prop}\`](${docsUrl}#${prop})" configuration option.`;
if (!person.name) {
const msg = `${preamble} Missing required property \`"name"\`.`;
showError(msg, name, { hint: seePersonHint });
return false;
}
if (person.orcid) {
const { orcid } = person;
const orcidUrl = new URL(orcid, "https://orcid.org/");
if (orcidUrl.origin !== "https://orcid.org") {
const msg = `${preamble} ORCID "${person.orcid}" at index ${index} is invalid.`;
const hint = `The origin should be "https://orcid.org", not "${orcidUrl.origin}".`;
showError(msg, name, { hint });
return false;
}
// trailing slash would mess up checksum
const orcidId = orcidUrl.pathname.slice(1).replace(/\/$/, "");
if (!/^\d{4}-\d{4}-\d{4}-\d{3}(\d|X)$/.test(orcidId)) {
const msg = `${preamble} ORCID "${orcidId}" has wrong format.`;
const hint = `ORCIDs have the format "1234-1234-1234-1234."`;
showError(msg, name, { hint });
return false;
}
if (!checkOrcidChecksum(orcid)) {
const msg = `${preamble} ORCID "${orcid}" failed checksum check.`;
const hint = "Please check that the ORCID is valid.";
showError(msg, name, { hint });
return false;
}
// canonical form
person.orcid = orcidUrl.href;
}
if (person.retiredDate && !isValidConfDate(person.retiredDate)) {
const msg = `${preamble} The property "\`retiredDate\`" is not a valid date.`;
showError(msg, name, {
hint: `The expected format is YYYY-MM-DD. ${seePersonHint}`,
});
return false;
}
if (
person.hasOwnProperty("extras") &&
!validateExtras(person.extras, seePersonHint, preamble)
) {
return false;
}
if (person.url && person.mailto) {
const msg = `${preamble} Has both "url" and "mailto" property.`;
showWarning(msg, name, {
hint: `Please choose either "url" or "mailto" ("url" is preferred). ${seePersonHint}`,
});
}
if (person.companyURL && !person.company) {
const msg = `${preamble} Has a "\`companyURL\`" property but no "\`company\`" property.`;
showWarning(msg, name, {
hint: `Please add a "\`company\`" property. ${seePersonHint}.`,
});
}
return true;
};
}
/**
*
* @param {PersonExtras[]} extras
* @param {string} hint
* @param {string} preamble
*/
function validateExtras(extras, hint, preamble) {
if (!Array.isArray(extras)) {
showError(
`${preamble}. A person's "extras" member must be an array.`,
name,
{ hint }
);
return false;
}
return extras.every((extra, index) => {
switch (true) {
case typeof extra !== "object":
showError(
`${preamble}. Member "extra" at index ${index} is not an object.`,
name,
{
hint,
}
);
return false;
case !extra.hasOwnProperty("name"):
showError(
`${preamble} \`PersonExtra\` object at index ${index} is missing required "name" member.`,
name,
{ hint }
);
return false;
case typeof extra.name === "string" && extra.name.trim() === "":
showError(
`${preamble} \`PersonExtra\` object at index ${index} "name" can't be empty.`,
name,
{ hint }
);
return false;
}
return true;
});
}
/**
* @param {string} orcid
* @returns {boolean}
*/
function checkOrcidChecksum(orcid) {
// calculate checksum as per https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier
const lastDigit = orcid[orcid.length - 1];
const remainder = orcid
.split("")
.slice(0, -1)
.filter(c => /\d/.test(c))
.map(Number)
.reduce((acc, c) => (acc + c) * 2, 0);
const lastDigitInt = (12 - (remainder % 11)) % 11;
const lastDigitShould = lastDigitInt === 10 ? "X" : String(lastDigitInt);
return lastDigit === lastDigitShould;
}

View File

@ -5,10 +5,3 @@ export async function fetchBase(path) {
const response = await fetch(new URL(`../../${path}`, import.meta.url));
return await response.text();
}
/**
* @param {string} fileName
*/
export async function fetchAsset(fileName) {
return fetchBase(`assets/${fileName}`);
}

View File

@ -1,7 +1,6 @@
// @ts-check
// Module core/ui
// Handles the ReSpec UI
/* jshint laxcomma:true */
// XXX TODO
// - look at other UI things to add
// - list issues
@ -10,7 +9,7 @@
// - make a release candidate that people can test
// - once we have something decent, merge, ship as 3.2.0
import { html, pluralize } from "./import-maps.js";
import { fetchAsset } from "./text-loader.js";
import css from "../styles/ui.css.js";
import { joinAnd } from "./utils.js";
import { markdownToHtml } from "./markdown.js";
import { sub } from "./pubsubhub.js";
@ -19,18 +18,10 @@ export const name = "core/ui";
// Opportunistically inserts the style, with the chance to reduce some FOUC
insertStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/ui.css")).default;
} catch {
return fetchAsset("ui.css");
}
}
async function insertStyle() {
function insertStyle() {
const styleElement = document.createElement("style");
styleElement.id = "respec-ui-styles";
styleElement.textContent = await loadStyle();
styleElement.textContent = css;
styleElement.classList.add("removeOnSave");
document.head.appendChild(styleElement);
return styleElement;
@ -273,7 +264,7 @@ function rsErrorToHTML(err) {
return err;
}
const plugin = err.plugin ? `(${err.plugin}): ` : "";
const plugin = err.plugin ? ` <small>(Plugin: "${err.plugin}")</small>.` : "";
const hint = err.hint ? ` ${err.hint}` : "";
const elements = Array.isArray(err.elements)
? ` Occurred at: ${joinAnd(err.elements.map(generateMarkdownLink))}.`
@ -282,7 +273,7 @@ function rsErrorToHTML(err) {
? `\n\n<details>\n${err.details}\n</details>\n`
: "";
const text = `${plugin}${err.message}${hint}${elements}${details}`;
const text = `${err.message}${hint}${elements}${plugin}${details}`;
return markdownToHtml(text);
}

View File

@ -8,10 +8,10 @@ import { pub } from "./pubsubhub.js";
export const name = "core/utils";
const dashes = /-/g;
/**
* Hashes a string from char code. Can return a negative number.
* Based on https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0
*
* @param {String} text
*/
function hashString(text) {
@ -22,18 +22,6 @@ function hashString(text) {
return String(hash);
}
const localizationStrings = {
en: {
x_and_y: " and ",
x_y_and_z: ", and ",
},
de: {
x_and_y: " und ",
x_y_and_z: " und ",
},
};
const l10n = getIntlData(localizationStrings);
export const ISODate = new Intl.DateTimeFormat(["en-ca-iso8601"], {
timeZone: "UTC",
year: "numeric",
@ -41,7 +29,7 @@ export const ISODate = new Intl.DateTimeFormat(["en-ca-iso8601"], {
day: "2-digit",
});
// CSS selector for matching elements that are non-normative
/** CSS selector for matching elements that are non-normative */
export const nonNormativeSelector =
".informative, .note, .issue, .example, .ednote, .practice, .introductory";
@ -78,6 +66,9 @@ export function createResourceHint(opts) {
}
// RESPEC STUFF
/**
* @param {Document} doc
*/
export function removeReSpec(doc) {
doc.querySelectorAll(".remove, script[data-requiremodule]").forEach(elem => {
elem.remove();
@ -101,39 +92,60 @@ function markAsOffending(elem, msg, title) {
}
// STRING HELPERS
// Takes an array and returns a string that separates each of its items with the proper commas and
// "and". The second argument is a mapping function that can convert the items before they are
// joined
export function joinAnd(array = [], mapper = item => item, lang = docLang) {
const items = array.map(mapper);
if (Intl.ListFormat && typeof Intl.ListFormat === "function") {
const formatter = new Intl.ListFormat(lang, {
style: "long",
type: "conjunction",
/**
* @param {"conjunction"|"disjunction"} type
* @param {"long"|"narrow"} style
*/
function joinFactory(type, style = "long") {
const formatter = new Intl.ListFormat(docLang, { style, type });
/**
* @template T
* @param {string[]} items
* @param {(value: string, index: number, array: string[]) => any} [mapper]
*/
return (items, mapper) => {
let elemCount = 0;
return formatter.formatToParts(items).map(({ type, value }) => {
if (type === "element" && mapper) {
return mapper(value, elemCount++, items);
}
return value;
});
return formatter.format(items);
}
switch (items.length) {
case 0:
case 1: // "x"
return items.toString();
case 2: // x and y
return items.join(l10n.x_and_y);
default: {
// x, y, and z
const str = items.join(", ");
const lastComma = str.lastIndexOf(",");
const and = l10n.x_y_and_z;
return `${str.substr(0, lastComma)}${and}${str.slice(lastComma + 2)}`;
}
}
};
}
// Takes a string, applies some XML escapes, and returns the escaped string.
// Note that overall using either Handlebars' escaped output or jQuery is much
// preferred to operating on strings directly.
export function xmlEscape(s) {
return s
/**
* Takes an array and returns a string that separates each of its items with the
* proper commas and "and". The second argument is a mapping function that can
* convert the items before they are joined.
*/
const conjunction = joinFactory("conjunction");
const disjunction = joinFactory("disjunction");
/**
*
* @param {string[]} items
* @param {(value: undefined, index: number, array: undefined[]) => string} [mapper]
*/
export function joinAnd(items, mapper) {
return conjunction(items, mapper).join("");
}
/**
*
* @param {string[]} items
* @param {(value: undefined, index: number, array: undefined[]) => string} [mapper]
*/
export function joinOr(items, mapper) {
return disjunction(items, mapper).join("");
}
/**
* Takes a string, applies some XML escapes, and returns the escaped string.
* @param {string} str
*/
export function xmlEscape(str) {
return str
.replace(/&/g, "&amp;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
@ -141,7 +153,8 @@ export function xmlEscape(s) {
}
/**
* Trims string at both ends and replaces all other white space with a single space
* Trims string at both ends and replaces all other white space with a single
* space.
* @param {string} str
*/
export function norm(str) {
@ -182,19 +195,29 @@ export function getIntlData(localizationStrings, lang = docLang) {
}
// --- DATE HELPERS -------------------------------------------------------------------------------
// Takes a Date object and an optional separator and returns the year,month,day representation with
// the custom separator (defaulting to none) and proper 0-padding
/**
* Takes a Date object and an optional separator and returns the year,month,day
* representation with the custom separator (defaulting to none) and proper
* 0-padding.
* @param {Date} date
*/
export function concatDate(date, sep = "") {
return ISODate.format(date).replace(dashes, sep);
}
// formats a date to "yyyy-mm-dd"
/**
* Formats a date to "yyyy-mm-dd".
* @param {Date} date
*/
export function toShortIsoDate(date) {
return ISODate.format(date);
}
// given either a Date object or a date in YYYY-MM-DD format,
// return a human-formatted date suitable for use in a W3C specification
/**
* Given either a Date object or a date in `YYYY-MM-DD` format, return a
* human-formatted date suitable for use in the specification.
* @param {Date | string} [date]
*/
export function humanDate(
date = new Date(),
lang = document.documentElement.lang || "en"
@ -216,15 +239,32 @@ export function humanDate(
// date month year
return `${day} ${month} ${year}`;
}
// given either a Date object or a date in YYYY-MM-DD format,
// return an ISO formatted date suitable for use in a xsd:datetime item
/**
* Given either a Date object or a date in `YYYY-MM-DD` format, return an ISO
* formatted date suitable for use in a xsd:datetime item
* @param {Date | string} date
*/
export function isoDate(date) {
return (date instanceof Date ? date : new Date(date)).toISOString();
}
// Given an object, it converts it to a key value pair separated by
// ("=", configurable) and a delimiter (" ," configurable).
// for example, {"foo": "bar", "baz": 1} becomes "foo=bar, baz=1"
/**
* Checks if a date is in expected format used by ReSpec (yyyy-mm-dd)
* @param {string} rawDate
*/
export function isValidConfDate(rawDate) {
const date = /\d{4}-\d{2}-\d{2}/.test(rawDate)
? new Date(rawDate)
: "Invalid Date";
return date.toString() !== "Invalid Date";
}
/**
* Given an object, it converts it to a key value pair separated by ("=", configurable) and a delimiter (" ," configurable).
* @example {"foo": "bar", "baz": 1} becomes "foo=bar, baz=1"
* @param {Record<string, any>} obj
*/
export function toKeyValuePairs(obj, delimiter = ", ", separator = "=") {
return Array.from(Object.entries(obj))
.map(([key, value]) => `${key}${separator}${JSON.stringify(value)}`)
@ -232,10 +272,14 @@ export function toKeyValuePairs(obj, delimiter = ", ", separator = "=") {
}
// STYLE HELPERS
// take a document and either a link or an array of links to CSS and appends
// a <link/> element to the head pointing to each
export function linkCSS(doc, styles) {
const stylesArray = [].concat(styles);
/**
* Take a document and either a link or an array of links to CSS and appends a
* `<link rel="stylesheet">` element to the head pointing to each.
* @param {Document} doc
* @param {string | string[]} urls
*/
export function linkCSS(doc, urls) {
const stylesArray = [].concat(urls);
const frag = stylesArray
.map(url => {
const link = doc.createElement("link");
@ -251,14 +295,17 @@ export function linkCSS(doc, styles) {
}
// TRANSFORMATIONS
// Run list of transforms over content and return result.
// Please note that this is a legacy method that is only kept in order
// to maintain compatibility
// with RSv1. It is therefore not tested and not actively supported.
/**
* Run list of transforms over content and return result.
*
* Please note that this is a legacy method that is only kept in order to
* maintain compatibility with RSv1. It is therefore not tested and not actively
* supported.
* @this {any}
* @param {string} content
* @param {string} [flist]
* @param {string} [flist] List of global function names.
* @param {unknown[]} [funcArgs] Arguments to pass to each function.
*/
export function runTransforms(content, flist, ...funcArgs) {
const args = [this, content, ...funcArgs];
@ -286,13 +333,13 @@ export function runTransforms(content, flist, ...funcArgs) {
/**
* Cached request handler
* @param {RequestInfo} input
* @param {number} maxAge cache expiration duration in ms. defaults to 24 hours (86400000 ms)
* @param {number} maxAge cache expiration duration in ms. defaults to 24 hours
* @return {Promise<Response>}
* if a cached response is available and it's not stale, return it
* else: request from network, cache and return fresh response.
* If network fails, return a stale cached version if exists (else throw)
*/
export async function fetchAndCache(input, maxAge = 86400000) {
export async function fetchAndCache(input, maxAge = 24 * 60 * 60 * 1000) {
const request = new Request(input);
const url = new URL(request.url);
@ -341,30 +388,25 @@ export async function fetchAndCache(input, maxAge = 86400000) {
// --- DOM HELPERS -------------------------------
/**
* Separates each item with proper commas.
* @template T
* @param {T[]} array
* @param {(item: T) => any} [mapper]
*/
export function htmlJoinComma(array, mapper = item => item) {
const items = array.map(mapper);
const joined = items.slice(0, -1).map(item => html`${item}, `);
return html`${joined}${items[items.length - 1]}`;
}
/**
* Separates each item with proper commas and "and".
*
* @param {string[]} array
* @param {(str: any) => object} mapper
* @param {(item: any) => any[]} [mapper]
*/
export function htmlJoinAnd(array, mapper = item => item) {
const items = array.map(mapper);
switch (items.length) {
case 0:
case 1: // "x"
return items[0];
case 2: // x and y
return html`${items[0]}${l10n.x_and_y}${items[1]}`;
default: {
const joined = htmlJoinComma(items.slice(0, -1));
return html`${joined}${l10n.x_y_and_z}${items[items.length - 1]}`;
}
}
export function htmlJoinAnd(array, mapper) {
const result = [].concat(conjunction(array, mapper));
return result.map(item => (typeof item === "string" ? html`${item}` : item));
}
/**
@ -380,8 +422,8 @@ export function addHashId(elem, prefix = "") {
}
/**
* Creates and sets an ID to an element (elem)
* using a specific prefix if provided, and a specific text if given.
* Creates and sets an ID to an element (elem) using a specific prefix if
* provided, and a specific text if given.
* @param {HTMLElement} elem element
* @param {String} pfx prefix
* @param {String} txt text
@ -430,7 +472,7 @@ export function addId(elem, pfx = "", txt = "", noLC = false) {
* @param {Node} el
* @param {string[]} exclusions node localName to exclude
* @param {object} options
* @param {boolean} options.wsNodes if nodes that only have whitespace are returned.
* @param {boolean} options.wsNodes return only whitespace-only nodes.
* @returns {Text[]}
*/
export function getTextNodes(el, exclusions = [], options = { wsNodes: true }) {
@ -459,15 +501,13 @@ export function getTextNodes(el, exclusions = [], options = { wsNodes: true }) {
}
/**
* For any element, returns an array of title strings that applies
* the algorithm used for determining the actual title of a
* <dfn> element (but can apply to other as well).
* if args.isDefinition is true, then the element is a definition, not a
* reference to a definition. Any @title will be replaced with
* @data-lt to be consistent with Bikeshed / Shepherd.
* This method now *prefers* the data-lt attribute for the list of
* titles. That attribute is added by this method to dfn elements, so
* subsequent calls to this method will return the data-lt based list.
* For any element, returns an array of title strings that applies the algorithm
* used for determining the actual title of a `<dfn>` element (but can apply to
* other as well).
*
* This method now *prefers* the `data-lt` attribute for the list of titles.
* That attribute is added by this method to `<dfn>` elements, so subsequent
* calls to this method will return the `data-lt` based list.
* @param {HTMLElement} elem
* @returns {String[]} array of title strings
*/
@ -510,8 +550,8 @@ export function getDfnTitles(elem) {
}
/**
* For an element (usually <a>), returns an array of targets that
* element might refer to, of the form
* For an element (usually <a>), returns an array of targets that element might
* refer to, in the object structure:
* @typedef {object} LinkTarget
* @property {string} for
* @property {string} title
@ -552,14 +592,23 @@ export function getLinkTargets(elem) {
* Changes name of a DOM Element
* @param {Element} elem element to rename
* @param {String} newName new element name
* @param {Object} options
* @param {boolean} options.copyAttributes
*
* @returns {Element} new renamed element
*/
export function renameElement(elem, newName) {
export function renameElement(
elem,
newName,
options = { copyAttributes: true }
) {
if (elem.localName === newName) return elem;
const newElement = elem.ownerDocument.createElement(newName);
// copy attributes
for (const { name, value } of elem.attributes) {
newElement.setAttribute(name, value);
if (options.copyAttributes) {
for (const { name, value } of elem.attributes) {
newElement.setAttribute(name, value);
}
}
// copy child nodes
newElement.append(...elem.childNodes);
@ -567,6 +616,10 @@ export function renameElement(elem, newName) {
return newElement;
}
/**
* @param {string} ref
* @param {HTMLElement} element
*/
export function refTypeFromContext(ref, element) {
const closestInformative = element.closest(nonNormativeSelector);
let isInformative = false;
@ -622,8 +675,8 @@ export function parents(element, selector) {
}
/**
* Calculates indentation when the element starts after a newline.
* The value will be empty if no newline or any non-whitespace exists after one.
* Calculates indentation when the element starts after a newline. The value
* will be empty if no newline or any non-whitespace exists after one.
* @param {Element} element
*
* @example ` <div></div>` returns " " (4 spaces).
@ -651,6 +704,7 @@ export function getElementIndentation(element) {
* @param {number} counter A number, which can start at a given value.
*/
export function msgIdGenerator(namespace, counter = 0) {
/** @returns {Generator<string, never, never>} */
function* idGenerator(namespace, counter) {
while (true) {
yield `${namespace}:${counter}`;
@ -663,6 +717,7 @@ export function msgIdGenerator(namespace, counter = 0) {
};
}
/** @extends {Set<string>} */
export class InsensitiveStringSet extends Set {
/**
* @param {Array<String>} [keys] Optional, initial keys
@ -713,15 +768,23 @@ export class InsensitiveStringSet extends Set {
}
}
/**
* @param {HTMLElement} node
*/
export function makeSafeCopy(node) {
const clone = node.cloneNode(true);
clone.querySelectorAll("[id]").forEach(elem => elem.removeAttribute("id"));
clone.querySelectorAll("dfn").forEach(dfn => renameElement(dfn, "span"));
clone.querySelectorAll("dfn").forEach(dfn => {
renameElement(dfn, "span", { copyAttributes: false });
});
if (clone.hasAttribute("id")) clone.removeAttribute("id");
removeCommentNodes(clone);
return clone;
}
/**
* @param {Node} node
*/
export function removeCommentNodes(node) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT);
for (const comment of [...walkTree(walker)]) {
@ -837,10 +900,66 @@ export function showWarning(message, pluginName, options = {}) {
}
/**
* Creates a quick markdown link to a property in the docs.
* Makes a string `coded`.
*
* @param {string} prop ReSpec configuration property to link to in docs.
* @param {string} item
* @returns {string}
*/
export function docLink(prop) {
return `[\`${prop}\`](https://respec.org/docs/#${prop})`;
export function toMDCode(item) {
return item ? `\`${item}\`` : "";
}
/**
* Joins an array of strings, wrapping each string in back-ticks (`) for inline markdown code.
*
* @param {string[]} array
* @param {object} options
* @param {boolean} options.quotes Surround each item in quotes
*/
export function codedJoinOr(array, { quotes } = { quotes: false }) {
return joinOr(array, quotes ? s => toMDCode(addQuotes(s)) : toMDCode);
}
/**
* Wraps in back-ticks ` for code.
*
* @param {string[]} array
* @param {object} options
* @param {boolean} options.quotes Surround each item in quotes
*/
export function codedJoinAnd(array, { quotes } = { quotes: false }) {
return joinAnd(array, quotes ? s => toMDCode(addQuotes(s)) : toMDCode);
}
function addQuotes(item) {
return String(item) ? `"${item}"` : "";
}
/**
* Tagged template string, helps with linking to documentation.
* Things inside [squareBrackets] are considered direct links to the documentation.
* To alias something, one can use a "|", like [respecConfig|#respec-configuration].
* @param {TemplateStringsArray} strings
* @param {string[]} keys
*/
export function docLink(strings, ...keys) {
return strings
.map((s, i) => {
const key = keys[i];
if (!key) {
return s;
}
// Linkables are wrapped in square brackets
if (!key.startsWith("[") && !key.endsWith("]")) {
return s + key;
}
const [linkingText, href] = key.slice(1, -1).split("|");
if (href) {
const url = new URL(href, "https://respec.org/docs/");
return `${s}[${linkingText}](${url})`;
}
return `${s}[\`${linkingText}\`](https://respec.org/docs/#${linkingText})`;
})
.join("");
}

View File

@ -0,0 +1,52 @@
// @ts-check
/**
* This module adds a "monetization" meta-tag to enable web-monetization.
*
* The meta-tag is added only to "live" documents, and is removed from generated
* static documents.
*/
import { html } from "./import-maps.js";
export const name = "core/web-monetization";
export function run(conf) {
if (conf.monetization === false) {
return;
}
const { monetization } = conf;
const { removeOnSave, paymentPointer } = canonicalizeConfig(monetization);
const cssClass = removeOnSave ? "removeOnSave" : null;
document.head.append(html`<meta
name="monetization"
content="${paymentPointer}"
class="${cssClass}"
/>`);
}
/**
* @param {object|string} rawConfig
* - {string} paymentPointer - The payment pointer to use.
* - {boolean} removeOnSave - Whether to remove the meta tag when the document is saved.
*/
function canonicalizeConfig(rawConfig) {
const config = {
paymentPointer: "$respec.org",
removeOnSave: true,
};
switch (typeof rawConfig) {
case "string":
config.paymentPointer = rawConfig;
break;
case "object":
if (rawConfig.paymentPointer) {
config.paymentPointer = String(rawConfig.paymentPointer);
}
if (rawConfig.removeOnSave === false) {
config.removeOnSave = false;
}
break;
}
return config;
}

View File

@ -6,6 +6,7 @@
// - don't use generated content in the CSS!
import {
addHashId,
docLink,
showError,
showWarning,
wrapInner,
@ -14,7 +15,7 @@ import {
import { decorateDfn, findDfn } from "./dfn-finder.js";
import { html, webidl2 } from "./import-maps.js";
import { addCopyIDLButton } from "./webidl-clipboard.js";
import { fetchAsset } from "./text-loader.js";
import css from "../styles/webidl.css.js";
import { registerDefinition } from "./dfn-map.js";
export const name = "core/webidl";
@ -141,7 +142,7 @@ function defineIdlName(escaped, data, parent) {
const linkType = getDfnType(data.type);
if (dfn) {
if (!data.partial) {
dfn.dataset.export = "";
if (!dfn.matches("[data-noexport]")) dfn.dataset.export = "";
dfn.dataset.dfnType = linkType;
}
decorateDfn(dfn, data, parentName, name);
@ -186,8 +187,9 @@ function defineIdlName(escaped, data, parent) {
if (showWarnings) {
const styledName = data.type === "operation" ? `${name}()` : name;
const ofParent = parentName ? ` \`${parentName}\`'s` : "";
const msg = `Missing \`<dfn>\` for${ofParent} \`${styledName}\` ${data.type}. [More info](https://github.com/w3c/respec/wiki/WebIDL-thing-is-not-defined).`;
showWarning(msg, pluginName, { elements: [unlinkedAnchor] });
const msg = `Missing \`<dfn>\` for${ofParent} \`${styledName}\` ${data.type}.`;
const hint = docLink`See ${"using `data-dfn-for`|#data-dfn-for"} in ReSpec's documentation.`;
showWarning(msg, pluginName, { elements: [unlinkedAnchor], hint });
}
return unlinkedAnchor;
}
@ -313,6 +315,17 @@ function getDefnName(defn) {
}
}
// IDL types that never need a data-dfn-for
const topLevelIdlTypes = [
"interface",
"interface mixin",
"dictionary",
"namespace",
"enum",
"typedef",
"callback",
];
/**
* @param {Element} idlElement
* @param {number} index
@ -344,8 +357,10 @@ function renderWebIDL(idlElement, index) {
}
const title = elem.dataset.title;
// Select the nearest ancestor element that can contain members.
const idlType = elem.dataset.dfnType;
const parent = elem.parentElement.closest("[data-idl][data-title]");
if (parent) {
if (parent && !topLevelIdlTypes.includes(idlType)) {
elem.dataset.dfnFor = parent.dataset.title;
}
if (elem.localName === "dfn") {
@ -377,29 +392,14 @@ export function addIDLHeader(pre) {
addCopyIDLButton(header);
}
const cssPromise = loadStyle();
async function loadStyle() {
try {
return (await import("text!../../assets/webidl.css")).default;
} catch {
return fetchAsset("webidl.css");
}
}
export async function run() {
const idls = document.querySelectorAll("pre.idl, pre.webidl");
if (!idls.length) {
return;
}
if (!document.querySelector(".idl:not(pre), .webidl:not(pre)")) {
const link = document.querySelector("head link");
if (link) {
const style = document.createElement("style");
style.textContent = await cssPromise;
link.before(style);
}
}
const style = document.createElement("style");
style.textContent = css;
document.querySelector("head link, head > *:last-child").before(style);
const astArray = [...idls].map(renderWebIDL);

View File

@ -19,6 +19,9 @@
import { cacheXrefData, resolveXrefCache } from "./xref-db.js";
import {
createResourceHint,
docLink,
joinAnd,
joinOr,
nonNormativeSelector,
norm as normalize,
showError,
@ -74,8 +77,8 @@ export async function run(conf) {
const queryKeys = [];
for (const elem of elems) {
const entry = getRequestEntry(elem);
const id = await objectHash(entry);
queryKeys.push({ ...entry, id });
entry.id = await objectHash(entry);
queryKeys.push(entry);
}
const xrefApiBase = conf.state[name].apiBase;
@ -92,8 +95,7 @@ export async function run(conf) {
function findExplicitExternalLinks() {
/** @type {NodeListOf<HTMLElement>} */
const links = document.querySelectorAll(
"a[data-cite]:not([data-cite='']):not([data-cite*='#']), " +
"dfn[data-cite]:not([data-cite='']):not([data-cite*='#'])"
":is(a,dfn)[data-cite]:not([data-cite=''],[data-cite*='#'])"
);
/** @type {NodeListOf<HTMLElement>} */
const externalDFNs = document.querySelectorAll("dfn.externalDFN");
@ -135,7 +137,7 @@ function normalizeConfig(xref) {
if (xref.profile) {
const profile = xref.profile.toLowerCase();
if (profile in profiles) {
const specs = (xref.specs || []).concat(profiles[profile]);
const specs = (xref.specs ?? []).concat(profiles[profile]);
Object.assign(config, { specs });
} else {
invalidProfileError(xref.profile);
@ -150,9 +152,7 @@ function normalizeConfig(xref) {
return config;
function invalidProfileError(profile) {
const supportedProfiles = Object.keys(profiles)
.map(p => `"${p}"`)
.join(", ");
const supportedProfiles = joinOr(Object.keys(profiles), s => `"${s}"`);
const msg =
`Invalid profile "${profile}" in \`respecConfig.xref\`. ` +
`Please use one of the supported profiles: ${supportedProfiles}.`;
@ -175,6 +175,10 @@ function getRequestEntry(elem) {
const forContext = getForContext(elem, isIDL);
return {
// Add an empty `id` to ensure the shape of object returned stays same when
// actual `id` is added later (minor perf optimization, also makes
// TypeScript happy).
id: "",
term,
types,
...(specs.length && { specs }),
@ -394,11 +398,17 @@ function addDataCite(elem, query, result, conf) {
const { uri, shortname, spec, normative, type, for: forContext } = result;
// if authored spec context had `result.spec`, use it instead of shortname
const cite = specs.flat().includes(spec) ? spec : shortname;
const url = new URL(uri, "https://example.org");
// we use this "partial" URL to resolve parts of urls...
// but sometimes we get lucky and we get an absolute URL from xref
// which we can then use in other places (e.g., data-cite.js)
const url = new URL(uri, "https://partial");
const { pathname: citePath } = url;
const citeFrag = url.hash.slice(1);
const dataset = { cite, citePath, citeFrag, type };
if (forContext) dataset.linkFor = forContext[0];
if (url.origin && url.origin !== "https://partial") {
dataset.citeHref = url.href;
}
Object.assign(elem.dataset, dataset);
addToReferences(elem, cite, normative, term, conf);
@ -432,8 +442,8 @@ function addToReferences(elem, cite, normative, term, conf) {
return;
}
const msg = `Normative reference to "${term}" found but term is defined informatively in "${cite}"`;
const title = "Error: Normative reference to informative term";
const msg = `Normative reference to "${term}" found but term is defined "informatively" in "${cite}".`;
const title = "Normative reference to non-normative term.";
showWarning(msg, name, { title, elements: [elem] });
}
@ -448,32 +458,37 @@ function showErrors({ ambiguous, notFound }, formBaseURL) {
if (query.for) url.searchParams.set("for", query.for);
url.searchParams.set("types", query.types.join(","));
if (specs.length) url.searchParams.set("specs", specs.join(","));
return url;
return url.href;
};
const howToFix = howToCiteURL =>
"[Learn more about this error](https://respec.org/docs/#error-term-not-found)" +
` or see [how to cite to resolve the error](${howToCiteURL})`;
const howToFix = (howToCiteURL, originalTerm) => {
return docLink`
[See search matches for "${originalTerm}"](${howToCiteURL}) or
${"[Learn about this error|#error-term-not-found]"}.`;
};
for (const { query, elems } of notFound.values()) {
const specs = query.specs ? [...new Set(query.specs.flat())].sort() : [];
const originalTerm = getTermFromElement(elems[0]);
const formUrl = getPrefilledFormURL(originalTerm, query);
const specsString = specs.map(spec => `\`${spec}\``).join(", ");
const hint = howToFix(formUrl);
const msg = `Couldn't match "**${originalTerm}**" to anything in the document or in any other document cited in this specification: ${specsString}.`;
const title = "Error: No matching dfn found.";
const specsString = joinAnd(specs, s => `**[${s}]**`);
const hint = howToFix(formUrl, originalTerm);
const forParent = query.for ? `, for **"${query.for}"**, ` : "";
const msg = `Couldn't find "**${originalTerm}**"${forParent} in this document or other cited documents: ${specsString}.`;
const title = "No matching definition found.";
showError(msg, name, { title, elements: elems, hint });
}
for (const { query, elems, results } of ambiguous.values()) {
const specs = [...new Set(results.map(entry => entry.shortname))].sort();
const specsString = specs.map(s => `**${s}**`).join(", ");
const specsString = joinAnd(specs, s => `**[${s}]**`);
const originalTerm = getTermFromElement(elems[0]);
const formUrl = getPrefilledFormURL(originalTerm, query, specs);
const hint = howToFix(formUrl);
const msg = `The term "**${originalTerm}**" is defined in ${specsString} in multiple ways, so it's ambiguous.`;
const title = "Error: Linking an ambiguous dfn.";
const forParent = query.for ? `, for **"${query.for}"**, ` : "";
const moreInfo = howToFix(formUrl, originalTerm);
const hint = docLink`To fix, use the ${"[data-cite]"} attribute to pick the one you mean from the appropriate specification. ${moreInfo}.`;
const msg = `The term "**${originalTerm}**"${forParent} is ambiguous because it's defined in ${specsString}.`;
const title = "Definition is ambiguous.";
showError(msg, name, { title, elements: elems, hint });
}
}

View File

@ -18,7 +18,7 @@ const localizationStrings = {
return html`<p>
The key word${plural ? "s" : ""} ${keywords} in this document
${plural ? "are" : "is"} to be interpreted as described in
<a href="https://tools.ietf.org/html/bcp14">BCP 14</a>
<a href="https://datatracker.ietf.org/doc/html/bcp14">BCP 14</a>
${renderInlineCitation("RFC2119")} ${renderInlineCitation("RFC8174")}
when, and only when, they appear in all capitals, as shown here.
</p>`;
@ -34,7 +34,7 @@ const localizationStrings = {
return html`<p>
${plural ? "Die Schlüsselwörter" : "Das Schlüsselwort"} ${keywords} in
diesem Dokument ${plural ? "sind" : "ist"} gemäß
<a href="https://tools.ietf.org/html/bcp14">BCP 14</a>
<a href="https://datatracker.ietf.org/doc/html/bcp14">BCP 14</a>
${renderInlineCitation("RFC2119")} ${renderInlineCitation("RFC8174")}
und unter Berücksichtigung von
<a href="https://github.com/adfinis-sygroup/2119/blob/master/2119de.rst"

View File

@ -4,10 +4,6 @@
*/
export const name = "dini/defaults";
import { coreDefaults } from "../core/defaults.js";
import linter from "../core/linter.js";
import { rule as privsecSectionRule } from "../core/linter-rules/privsec-section.js";
linter.register(privsecSectionRule);
const licenses = new Map([
[
@ -29,8 +25,7 @@ const licenses = new Map([
[
"cc-by-sa",
{
name:
"Creative Commons Attribution-ShareAlike 4.0 International Public License",
name: "Creative Commons Attribution-ShareAlike 4.0 International Public License",
short: "CC-BY-SA",
url: "https://creativecommons.org/licenses/by-sa/4.0/legalcode",
},

View File

@ -1,5 +1,4 @@
// @ts-check
/* jshint strict: true, browser:true, jquery: true */
// Module dini/style
// Inserts a link to the appropriate W3C style for the specification's maturity level.
// CONFIGURATION

View File

@ -116,7 +116,7 @@ export default conf => {
</h2>
<dl>
<dt>${conf.multipleEditors ? l10n.editors : l10n.editor}</dt>
${showPeople(conf.editors)}
${showPeople(conf, "editors")}
${Array.isArray(conf.formerEditors) && conf.formerEditors.length > 0
? html`
<dt>
@ -124,13 +124,13 @@ export default conf => {
? l10n.former_editors
: l10n.former_editor}
</dt>
${showPeople(conf.formerEditors)}
${showPeople(conf, "formerEditors")}
`
: ""}
${conf.authors
? html`
<dt>${conf.multipleAuthors ? l10n.authors : l10n.author}</dt>
${showPeople(conf.authors)}
${showPeople(conf, "authors")}
`
: ""}
${conf.otherLinks ? conf.otherLinks.map(showLink) : ""}

View File

@ -4,10 +4,6 @@
*/
export const name = "geonovum/defaults";
import { coreDefaults } from "../core/defaults.js";
import linter from "../core/linter.js";
import { rule as privsecSectionRule } from "../core/linter-rules/privsec-section.js";
linter.register(privsecSectionRule);
const licenses = new Map([
[
@ -29,8 +25,7 @@ const licenses = new Map([
[
"cc-by-nd",
{
name:
"Creative Commons Attribution-NoDerivatives 4.0 International Public License",
name: "Creative Commons Attribution-NoDerivatives 4.0 International Public License",
short: "CC-BY-ND",
url: "https://creativecommons.org/licenses/by-nd/4.0/legalcode.nl",
},

25
src/respec.js Normal file
View File

@ -0,0 +1,25 @@
import { runAll } from "./core/base-runner.js";
import { ui } from "./core/ui.js";
// In case everything else fails, we want the error
window.addEventListener("error", ev => {
console.error(ev.error, ev.message, ev);
});
export async function run(plugins) {
try {
ui.show();
await domReady();
await runAll(plugins);
} finally {
ui.enable();
}
}
async function domReady() {
if (document.readyState === "loading") {
await new Promise(resolve =>
document.addEventListener("DOMContentLoaded", resolve)
);
}
}

View File

@ -0,0 +1,12 @@
/* For assertions in lists containing algorithms */
const css = String.raw;
// Prettier ignore only to keep code indented from level 0.
// prettier-ignore
export default css`
.assert {
background: #eee;
border-left: 0.5em solid #aaa;
padding: 0.3em;
}
`;

View File

@ -1,10 +1,14 @@
/* container for stats */
const css = String.raw;
// Prettier ignore only to keep code indented from level 0.
// prettier-ignore
export default css`
.caniuse-stats {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: baseline;
cursor: pointer;
}
button.caniuse-cell {
@ -23,7 +27,7 @@ button.caniuse-cell {
padding: 0.5em;
}
.caniuse-cell.n::before{
.caniuse-cell.n::before {
content: "❌";
padding: 0.5em;
}
@ -50,16 +54,15 @@ button.caniuse-cell {
margin-top: 1px;
}
.caniuse-stats a {
.caniuse-stats a[href] {
white-space: nowrap;
align-self: center;
margin-left: .5em;
margin-left: 0.5em;
}
/* a browser version */
.caniuse-cell {
display: flex;
color: rgba(0, 0, 0, 0.8);
font-size: 90%;
height: 0.8cm;
margin-right: 1px;
@ -68,6 +71,22 @@ button.caniuse-cell {
overflow: visible;
justify-content: center;
align-items: center;
--supported: #2a8436;
--no-support: #c44230;
--no-support-alt: #b43b2b;
--partial: #807301;
--partial-alt: #746c00;
color: #fff;
background: repeating-linear-gradient(
var(--caniuse-angle, 45deg),
var(--caniuse-bg) 0,
var(--caniuse-bg-alt) 1px,
var(--caniuse-bg-alt) 0.4em,
var(--caniuse-bg) calc(0.25em + 1px),
var(--caniuse-bg) 0.75em
);
}
li.caniuse-cell {
@ -78,27 +97,31 @@ li.caniuse-cell {
outline: none;
}
.caniuse-cell:hover {
color: rgba(0, 0, 0, 1);
}
/* supports */
.caniuse-cell.y {
background: #8bc34a;
background: var(--supported);
}
/* no support */
.caniuse-cell.n {
background: #e53935;
/* no support, disabled by default */
.caniuse-cell.n,
.caniuse-cell.d {
--caniuse-angle: 45deg;
--caniuse-bg: var(--no-support);
--caniuse-bg-alt: var(--no-support-alt);
}
.caniuse-cell.d {
--caniuse-angle: 180deg;
}
/* not supported by default / partial support etc
see https://github.com/Fyrd/caniuse/blob/master/CONTRIBUTING.md for stats */
.caniuse-cell.d,
.caniuse-cell.a,
.caniuse-cell.x,
.caniuse-cell.p {
background: #ffc107;
--caniuse-angle: 90deg;
--caniuse-bg: var(--partial);
--caniuse-bg-alt: var(--partial-alt);
}
/* show rest of the browser versions */
@ -106,3 +129,4 @@ see https://github.com/Fyrd/caniuse/blob/master/CONTRIBUTING.md for stats */
.caniuse-stats .caniuse-browser:hover > ul {
display: block;
}
`;

View File

@ -1,3 +1,8 @@
const css = String.raw;
// Prettier ignore only to keep code indented from level 0.
// prettier-ignore
export default css`
var {
position: relative;
cursor: pointer;
@ -43,3 +48,4 @@ var[data-type]:hover::after,
var[data-type]:hover::before {
opacity: 1;
}
`;

Some files were not shown because too many files have changed in this diff Show More